// 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); } } }