// Copyright © 2013 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Windows.Forms;
using CefSharp.Internals;
using CefSharp.Web;
using CefSharp.WinForms.Internals;
namespace CefSharp.WinForms
{
///
/// ChromiumWebBrowser is the WinForms web browser control
///
///
///
[Docking(DockingBehavior.AutoDock), DefaultEvent("LoadingStateChanged"), ToolboxBitmap(typeof(ChromiumWebBrowser)),
Description("CefSharp ChromiumWebBrowser - Chromium Embedded Framework .Net wrapper. https://github.com/cefsharp/CefSharp"),
Designer(typeof(ChromiumWebBrowserDesigner))]
public partial class ChromiumWebBrowser : Control, IWebBrowserInternal, IWinFormsWebBrowser
{
//TODO: If we start adding more consts then extract them into a common class
//Possibly in the CefSharp assembly and move the WPF ones into there as well.
private const uint WS_EX_NOACTIVATE = 0x08000000;
///
/// The managed cef browser adapter
///
private IBrowserAdapter managedCefBrowserAdapter;
///
/// The parent form message interceptor
///
private ParentFormMessageInterceptor parentFormMessageInterceptor;
///
/// The browser
///
private IBrowser browser;
///
/// A flag that indicates whether or not the designer is active
/// NOTE: DesignMode becomes false by the time we get to the destructor/dispose so it gets stored here
///
private bool designMode;
///
/// A flag that indicates whether or not has been called.
///
private bool initialized;
///
/// Has the underlying Cef Browser been created (slightly different to initialized in that
/// the browser is initialized in an async fashion)
///
private bool browserCreated;
///
/// A flag indicating if the was used when calling CreateBrowser
/// If false and contains a non empty string Load will be called
/// on the main frame
///
private bool initialAddressLoaded;
///
/// If true the the WS_EX_NOACTIVATE style will be removed so that future mouse clicks
/// inside the browser correctly activate and focus the window.
///
private bool removeExNoActivateStyle;
///
/// Browser initialization settings
///
private IBrowserSettings browserSettings;
///
/// The request context (we deliberately use a private variable so we can throw an exception if
/// user attempts to set after browser created)
///
private IRequestContext requestContext;
///
/// The value for disposal, if it's 1 (one) then this instance is either disposed
/// or in the process of getting disposed
///
private int disposeSignaled;
///
/// Parking control used to temporarily host the CefBrowser instance
/// when is true.
///
private Control parkingControl;
///
/// Gets a value indicating whether this instance is disposed.
///
/// if this instance is disposed; otherwise, .
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(false)]
public new bool IsDisposed
{
get
{
return Interlocked.CompareExchange(ref disposeSignaled, 1, 1) == 1;
}
}
///
/// Set to true while handing an activating WM_ACTIVATE message.
/// MUST ONLY be cleared by DefaultFocusHandler.
///
/// true if this instance is activating; otherwise, false.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(false)]
public bool IsActivating { get; set; }
///
/// Gets or sets the browser settings.
///
/// The browser settings.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IBrowserSettings BrowserSettings
{
get
{
//We keep a reference to the browserSettings for the case where
//the Control Handle is destroyed then Created see https://github.com/cefsharp/CefSharp/issues/2840
//As it's not possible to change settings after the browser has been
//created, and changing browserSettings then creating a new handle will
//give a subtle different user experience if you aren't expecting it we
//return null here even though we still have a reference.
if (browserCreated)
{
return null;
}
return browserSettings;
}
set
{
if (browserCreated)
{
throw new Exception("Browser has already been created. BrowserSettings must be " +
"set before the underlying CEF browser is created.");
}
if (value != null && !Core.ObjectFactory.BrowserSetingsType.IsAssignableFrom(value.UnWrap().GetType()))
{
throw new Exception(string.Format("BrowserSettings can only be of type {0} or null", Core.ObjectFactory.BrowserSetingsType));
}
browserSettings = value;
}
}
///
/// Activates browser upon creation, the default value is false. Prior to version 73
/// the default behaviour was to activate browser on creation (Equivalent of setting this property to true).
/// To restore this behaviour set this value to true immediately after you create the instance.
/// https://bitbucket.org/chromiumembedded/cef/issues/1856/branch-2526-cef-activates-browser-window
///
public bool ActivateBrowserOnCreation { get; set; }
///
/// Gets or sets the request context.
///
/// The request context.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public IRequestContext RequestContext
{
get { return requestContext; }
set
{
if (browserCreated)
{
throw new Exception("Browser has already been created. RequestContext must be " +
"set before the underlying CEF browser is created.");
}
if (value != null && !Core.ObjectFactory.RequestContextType.IsAssignableFrom(value.UnWrap().GetType()))
{
throw new Exception(string.Format("RequestContext can only be of type {0} or null", Core.ObjectFactory.RequestContextType));
}
requestContext = value;
}
}
///
/// A flag that indicates whether the control is currently loading one or more web pages (true) or not (false).
///
/// true if this instance is loading; otherwise, false.
/// In the WPF control, this property is implemented as a Dependency Property and fully supports data
/// binding.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(false)]
public bool IsLoading { get; private set; }
///
/// The text that will be displayed as a ToolTip
///
/// The tooltip text.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public string TooltipText { get; private set; }
///
/// The address (URL) which the browser control is currently displaying.
/// Will automatically be updated as the user navigates to another page (e.g. by clicking on a link).
///
/// The address.
/// In the WPF control, this property is implemented as a Dependency Property and fully supports data
/// binding.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(null)]
public string Address { get; private set; }
///
/// Occurs when the browser address changed.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang..
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
///
public event EventHandler AddressChanged;
///
/// Occurs when the browser title changed.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang..
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
///
public event EventHandler TitleChanged;
///
/// Event called after the underlying CEF browser instance has been created.
/// It's important to note this event is fired on a CEF UI thread, which by default is not the same as your application UI
/// thread. It is unwise to block on this thread for any length of time as your browser will become unresponsive and/or hang..
/// To access UI elements you'll need to Invoke/Dispatch onto the UI Thread.
///
public event EventHandler IsBrowserInitializedChanged;
///
/// A flag that indicates whether the state of the control currently supports the GoForward action (true) or not (false).
///
/// true if this instance can go forward; otherwise, false.
/// In the WPF control, this property is implemented as a Dependency Property and fully supports data
/// binding.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(false)]
public bool CanGoForward { get; private set; }
///
/// A flag that indicates whether the state of the control current supports the GoBack action (true) or not (false).
///
/// true if this instance can go back; otherwise, false.
/// In the WPF control, this property is implemented as a Dependency Property and fully supports data
/// binding.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(false)]
public bool CanGoBack { get; private set; }
///
/// A flag that indicates whether the WebBrowser is initialized (true) or not (false).
///
/// true if this instance is browser initialized; otherwise, false.
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(false)]
public bool IsBrowserInitialized
{
get { return InternalIsBrowserInitialized(); }
}
///
/// ParentFormMessageInterceptor hooks the Form handle and forwards
/// the move/active messages to the browser, the default is true
/// and should only be required when using
/// set to true.
///
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(true)]
public bool UseParentFormMessageInterceptor { get; set; } = true;
///
/// By default when is called
/// the underlying Browser Hwnd is only parked (moved to a temp parent)
/// when is true, there are a few other
/// cases where parking of the control is desired, you can force parking by setting
/// this property to true.
///
///
/// You may wish to set this property to true when using the browser in conjunction
/// with https://github.com/dockpanelsuite/dockpanelsuite
///
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DefaultValue(true)]
public bool ParkControlOnHandleDestroyed { get; set; } = false;
///
/// Initializes static members of the class.
///
static ChromiumWebBrowser()
{
if (CefSharpSettings.ShutdownOnExit)
{
Application.ApplicationExit += OnApplicationExit;
}
}
///
/// Handles the event.
///
/// The sender.
/// The instance containing the event data.
private static void OnApplicationExit(object sender, EventArgs e)
{
Cef.Shutdown();
}
///
/// This constructor exists as the WinForms designer requires a parameterless constructor, if you are instantiating
/// an instance of this class in code then use the
/// constructor overload instead. Using this constructor in code is unsupported and you may experience 's
/// when attempting to access some of the properties immediately after instantiation.
///
[Obsolete("Should only be used by the WinForms Designer. Use the ChromiumWebBrowser(string, IRequestContext) constructor overload instead.")]
public ChromiumWebBrowser()
{
}
///
/// Initializes a new instance of the class.
/// **Important** - When using this constructor the property
/// will default to .
///
/// html string to be initially loaded in the browser.
/// (Optional) Request context that will be used for this browser instance, if null the Global
/// Request Context will be used.
public ChromiumWebBrowser(HtmlString html, IRequestContext requestContext = null) : this(html.ToDataUriString(), requestContext)
{
}
///
/// Initializes a new instance of the class.
/// **Important** - When using this constructor the property
/// will default to .
///
/// The address.
/// (Optional) Request context that will be used for this browser instance, if null the Global
/// Request Context will be used.
public ChromiumWebBrowser(string address, IRequestContext requestContext = null)
{
Dock = DockStyle.Fill;
Address = address;
RequestContext = requestContext;
InitializeFieldsAndCefIfRequired();
}
///
/// Required for designer support - this method cannot be inlined as the designer
/// will attempt to load libcef.dll and will subsequently throw an exception.
/// TODO: Still not happy with this method name, need something better
///
[MethodImpl(MethodImplOptions.NoInlining)]
private void InitializeFieldsAndCefIfRequired()
{
if (!initialized)
{
if (!Cef.IsInitialized && !Cef.Initialize(new CefSettings()))
{
throw new InvalidOperationException("Cef::Initialize() failed");
}
Cef.AddDisposable(this);
if (FocusHandler == null)
{
//If the WinForms UI thread and the CEF UI thread are one in the same
//then we don't need the FocusHandler, it's only required when using
//MultiThreadedMessageLoop (the default)
if (!Cef.CurrentlyOnThread(CefThreadIds.TID_UI))
{
FocusHandler = new DefaultFocusHandler();
}
}
if (browserSettings == null)
{
browserSettings = Core.ObjectFactory.CreateBrowserSettings(autoDispose: true);
}
managedCefBrowserAdapter = ManagedCefBrowserAdapter.Create(this, false);
initialized = true;
}
}
///
/// If not in design mode; Releases unmanaged and - optionally - managed resources for the
///
/// to release both managed and unmanaged resources; to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
// Attempt to move the disposeSignaled state from 0 to 1. If successful, we can be assured that
// this thread is the first thread to do so, and can safely dispose of the object.
if (Interlocked.CompareExchange(ref disposeSignaled, 1, 0) != 0)
{
return;
}
if (!designMode)
{
InternalDispose(disposing);
}
base.Dispose(disposing);
}
///
/// Releases unmanaged and - optionally - managed resources for the
///
/// to release both managed and unmanaged resources; to release only unmanaged resources.
///
/// This method cannot be inlined as the designer will attempt to load libcef.dll and will subsequently throw an exception.
///
[MethodImpl(MethodImplOptions.NoInlining)]
private void InternalDispose(bool disposing)
{
if (disposing)
{
Interlocked.Exchange(ref browserInitialized, 0);
CanExecuteJavascriptInMainFrame = false;
// Don't maintain a reference to event listeners anylonger:
AddressChanged = null;
ConsoleMessage = null;
FrameLoadEnd = null;
FrameLoadStart = null;
IsBrowserInitializedChanged = null;
LoadError = null;
LoadingStateChanged = null;
StatusMessage = null;
TitleChanged = null;
JavascriptMessageReceived = null;
// Release reference to handlers, except LifeSpanHandler which is done after Disposing
// ManagedCefBrowserAdapter otherwise the ILifeSpanHandler.DoClose will not be invoked.
this.SetHandlersToNullExceptLifeSpan();
browser = null;
if (parentFormMessageInterceptor != null)
{
parentFormMessageInterceptor.Dispose();
parentFormMessageInterceptor = null;
}
if (managedCefBrowserAdapter != null)
{
managedCefBrowserAdapter.Dispose();
managedCefBrowserAdapter = null;
}
//Dispose of BrowserSettings if we created it, if user created then they're responsible
if (browserSettings != null && browserSettings.AutoDispose)
{
browserSettings.Dispose();
}
browserSettings = null;
parkingControl?.Dispose();
parkingControl = null;
// LifeSpanHandler is set to null after managedCefBrowserAdapter.Dispose so ILifeSpanHandler.DoClose
// is called.
LifeSpanHandler = null;
}
Cef.RemoveDisposable(this);
}
///
/// Loads the specified URL.
///
/// The URL to be loaded.
public void Load(string url)
{
if (IsBrowserInitialized)
{
using (var frame = this.GetMainFrame())
{
frame.LoadUrl(url);
}
}
else
{
Address = url;
}
}
///
/// The javascript object repository, one repository per ChromiumWebBrowser instance.
///
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IJavascriptObjectRepository JavascriptObjectRepository
{
get
{
InitializeFieldsAndCefIfRequired();
return managedCefBrowserAdapter == null ? null : managedCefBrowserAdapter.JavascriptObjectRepository;
}
}
///
/// Raises the event.
///
/// An that contains the event data.
protected override void OnHandleCreated(EventArgs e)
{
designMode = DesignMode;
if (!designMode)
{
InitializeFieldsAndCefIfRequired();
// NOTE: Had to move the code out of this function otherwise the designer would crash
CreateBrowser();
ResizeBrowser(Width, Height);
}
base.OnHandleCreated(e);
}
protected override void OnHandleDestroyed(EventArgs e)
{
if (!designMode)
{
// NOTE: Had to move the code out of this function otherwise the designer would crash
OnHandleDestroyedInternal();
}
base.OnHandleDestroyed(e);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void OnHandleDestroyedInternal()
{
//When the Control is being Recreated then we'll park
//the browser (set to a temp parent) and assign to
//our new handle when it's ready.
if (RecreatingHandle || ParkControlOnHandleDestroyed)
{
parkingControl = new Control();
parkingControl.CreateControl();
var host = this.GetBrowserHost();
var hwnd = host.GetWindowHandle();
NativeMethodWrapper.SetWindowParent(hwnd, parkingControl.Handle);
}
}
///
/// Override this method to handle creation of WindowInfo. This method can be used to customise aspects of
/// browser creation including configuration of settings such as .
/// Window Activation is disabled by default, you can re-enable it by overriding and removing the
/// WS_EX_NOACTIVATE style from .
///
/// Window handle for the Control
/// Window Info
///
/// To re-enable Window Activation then remove WS_EX_NOACTIVATE from ExStyle
///
/// const uint WS_EX_NOACTIVATE = 0x08000000;
/// windowInfo.ExStyle &= ~WS_EX_NOACTIVATE;
///
///
protected virtual IWindowInfo CreateBrowserWindowInfo(IntPtr handle)
{
var windowInfo = Core.ObjectFactory.CreateWindowInfo();
windowInfo.SetAsChild(handle);
if (!ActivateBrowserOnCreation)
{
//Disable Window activation by default
//https://bitbucket.org/chromiumembedded/cef/issues/1856/branch-2526-cef-activates-browser-window
windowInfo.ExStyle |= WS_EX_NOACTIVATE;
}
return windowInfo;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void CreateBrowser()
{
browserCreated = true;
if (((IWebBrowserInternal)this).HasParent == false)
{
//If we are Recreating our handle we will have re-parented our
//browser to parkingControl. We'll assign the browser to our newly
//created handle now.
if ((RecreatingHandle || ParkControlOnHandleDestroyed) && IsBrowserInitialized && browser != null)
{
var host = this.GetBrowserHost();
var hwnd = host.GetWindowHandle();
NativeMethodWrapper.SetWindowParent(hwnd, Handle);
parkingControl.Dispose();
parkingControl = null;
}
else
{
var windowInfo = CreateBrowserWindowInfo(Handle);
//We actually check if WS_EX_NOACTIVATE was set for instances
//the user has override CreateBrowserWindowInfo and not called base.CreateBrowserWindowInfo
removeExNoActivateStyle = (windowInfo.ExStyle & WS_EX_NOACTIVATE) == WS_EX_NOACTIVATE;
initialAddressLoaded = !string.IsNullOrEmpty(Address);
managedCefBrowserAdapter.CreateBrowser(windowInfo, browserSettings, requestContext, Address);
}
}
}
///
/// Called after browser created.
///
/// The browser.
void IWebBrowserInternal.OnAfterBrowserCreated(IBrowser browser)
{
this.browser = browser;
Interlocked.Exchange(ref browserInitialized, 1);
// By the time this callback gets called, this control
// is most likely hooked into a browser Form of some sort.
// (Which is what ParentFormMessageInterceptor relies on.)
// Ensure the ParentFormMessageInterceptor construction occurs on the WinForms UI thread:
if (UseParentFormMessageInterceptor)
{
this.InvokeOnUiThreadIfRequired(() =>
{
parentFormMessageInterceptor = new ParentFormMessageInterceptor(this);
});
}
ResizeBrowser(Width, Height);
//If Load was called after the call to CreateBrowser we'll call Load
//on the MainFrame
if (!initialAddressLoaded && !string.IsNullOrEmpty(Address))
{
browser.MainFrame.LoadUrl(Address);
}
IsBrowserInitializedChanged?.Invoke(this, EventArgs.Empty);
}
///
/// Sets the address.
///
/// The instance containing the event data.
void IWebBrowserInternal.SetAddress(AddressChangedEventArgs args)
{
Address = args.Address;
var handler = AddressChanged;
if (handler != null)
{
handler(this, args);
}
}
///
/// Sets the loading state change.
///
/// The instance containing the event data.
void IWebBrowserInternal.SetLoadingStateChange(LoadingStateChangedEventArgs args)
{
CanGoBack = args.CanGoBack;
CanGoForward = args.CanGoForward;
IsLoading = args.IsLoading;
if (removeExNoActivateStyle && browser != null)
{
removeExNoActivateStyle = false;
var host = this.GetBrowserHost();
var hwnd = host.GetWindowHandle();
//Remove the WS_EX_NOACTIVATE style so that future mouse clicks inside the
//browser correctly activate and focus the browser.
//https://github.com/chromiumembedded/cef/blob/9df4a54308a88fd80c5774d91c62da35afb5fd1b/tests/cefclient/browser/root_window_win.cc#L1088
NativeMethodWrapper.RemoveExNoActivateStyle(hwnd);
}
var handler = LoadingStateChanged;
if (handler != null)
{
handler(this, args);
}
}
///
/// Sets the title.
///
/// The instance containing the event data.
void IWebBrowserInternal.SetTitle(TitleChangedEventArgs args)
{
var handler = TitleChanged;
if (handler != null)
{
handler(this, args);
}
}
///
/// Sets the tooltip text.
///
/// The tooltip text.
void IWebBrowserInternal.SetTooltipText(string tooltipText)
{
TooltipText = tooltipText;
}
///
/// Manually implement Focused because cef does not implement it.
///
/// true if focused; otherwise, false.
/// This is also how the Microsoft's WebBrowserControl implements the Focused property.
public override bool Focused
{
get
{
if (base.Focused)
{
return true;
}
if (!IsHandleCreated)
{
return false;
}
return NativeMethodWrapper.IsFocused(Handle);
}
}
///
/// Raises the event.
///
/// An that contains the event data.
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
if (!designMode && initialized)
{
ResizeBrowser(Width, Height);
}
}
///
/// Resizes the browser.
///
private void ResizeBrowser(int width, int height)
{
if (IsBrowserInitialized)
{
managedCefBrowserAdapter.Resize(width, height);
}
}
///
/// When minimized set the browser window size to 0x0 to reduce resource usage.
/// https://github.com/chromiumembedded/cef/blob/c7701b8a6168f105f2c2d6b239ce3958da3e3f13/tests/cefclient/browser/browser_window_std_win.cc#L87
///
internal void HideInternal()
{
ResizeBrowser(0, 0);
}
///
/// Show the browser (called after previous minimised)
///
internal void ShowInternal()
{
ResizeBrowser(Width, Height);
}
///
/// Raises the event.
///
/// An that contains the event data.
protected override void OnGotFocus(EventArgs e)
{
if (IsBrowserInitialized)
{
browser.GetHost().SetFocus(true);
}
base.OnGotFocus(e);
}
///
/// Returns the current IBrowser Instance
///
/// browser instance or null
public IBrowser GetBrowser()
{
ThrowExceptionIfDisposed();
ThrowExceptionIfBrowserNotInitialized();
return browser;
}
///
/// Gets the default size of the control.
///
///
/// The default of the control.
///
protected override Size DefaultSize
{
get { return new Size(200, 100); }
}
///
/// Makes certain keys as Input keys when CefSettings.MultiThreadedMessageLoop = false
///
/// key data
/// true for a select list of keys otherwise defers to base.IsInputKey
protected override bool IsInputKey(Keys keyData)
{
//This code block is only called/required when CEF is running in the
//same message loop as the WinForms UI (CefSettings.MultiThreadedMessageLoop = false)
//Without this code, arrows and tab won't be processed
switch (keyData)
{
case Keys.Right:
case Keys.Left:
case Keys.Up:
case Keys.Down:
case Keys.Tab:
{
return true;
}
case Keys.Shift | Keys.Tab:
case Keys.Shift | Keys.Right:
case Keys.Shift | Keys.Left:
case Keys.Shift | Keys.Up:
case Keys.Shift | Keys.Down:
{
return true;
}
}
return base.IsInputKey(keyData);
}
}
}