// 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; using CefSharp.WinForms.Host; using CefSharp.DevTools.Page; using System.Threading.Tasks; 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 : ChromiumHostControlBase, 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; /// /// 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; /// /// Parking control used to temporarily host the CefBrowser instance /// when is true. /// private Control parkingControl; /// /// This flag is set when the browser gets focus before the underlying CEF browser /// has been initialized. /// private bool initialFocus; /// /// 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; } } /// /// 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://github.com/chromiumembedded/cef/issues/1856 /// 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; /// /// 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(); } /// /// To control how is called, this method will /// unsubscribe from , /// public static void UnregisterShutdownHandler() { Application.ApplicationExit -= OnApplicationExit; } /// /// Important!!! /// 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. /// [EditorBrowsable(EditorBrowsableState.Never)] 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(CefInitializeFailedErrorMessage); } 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; 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. // We also leave FocusHandler and override with a NoFocusHandler implementation as // it so we can block taking Focus (we're dispoing afterall). Issue #3715 FreeHandlersExceptLifeSpanAndFocus(); FocusHandler = new NoFocusHandler(); browser = null; BrowserCore = 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); } /// public void Load(string url) { if (IsDisposed) { return; } var browserCore = BrowserCore; //There's a small window here between CreateBrowser //and OnAfterBrowserCreated where the Address prop //will be updated, no MainFrame.LoadUrl call will be made. if (browserCore == null) { Address = url; } else { if(browserCore.IsDisposed) { return; } if(!browserCore.IsValid) { throw new InvalidOperationException("IBrowser instance is no longer valid. Control.Handle was likely destroyed."); } using (var frame = browserCore.MainFrame) { //Only attempt to call load if frame is valid //I've seen so far one case where the MainFrame is invalid. //As yet unable to reproduce if (frame.IsValid) { frame.LoadUrl(url); } } } } /// /// Capture page screenshot. /// /// Image compression format (defaults to png). /// Compression quality from range [0..100] (jpeg only). /// Capture the screenshot of a given region only. /// Capture the screenshot from the surface, rather than the view. Defaults to true. /// Capture the screenshot beyond the viewport. Defaults to false. /// A task that can be awaited to obtain the screenshot as a byte[]. public async Task CaptureScreenshotAsync(CaptureScreenshotFormat format = CaptureScreenshotFormat.Png, int? quality = null, Viewport viewPort = null, bool fromSurface = true, bool captureBeyondViewport = false) { ThrowExceptionIfDisposed(); ThrowExceptionIfBrowserNotInitialized(); if(viewPort != null && viewPort.Scale <= 0) { throw new ArgumentException($"{nameof(viewPort)}.{nameof(viewPort.Scale)} must be greater than 0."); } using (var devToolsClient = browser.GetDevToolsClient()) { var screenShot = await devToolsClient.Page.CaptureScreenshotAsync(format, quality, viewPort, fromSurface, captureBeyondViewport).ConfigureAwait(continueOnCapturedContext: false); return screenShot.Data; } } /// /// 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; } } /// /// Indicates if one of the Ancestors of this control is sited /// and that site in DesignMode. /// // Roughly based on https://github.com/dotnet/winforms/pull/5375 private bool IsParentInDesignMode(Control control) { if(control == null) { throw new ArgumentNullException(nameof(control)); } //Check if our Site is in DesignMode //If not then walk up the tree //Until we find a Site that is or our parent is null if(control.Site?.DesignMode ?? false) { return true; } if(control.Parent == null) { return false; } return IsParentInDesignMode(control.Parent); } /// /// Raises the event. /// /// An that contains the event data. protected override void OnHandleCreated(EventArgs e) { designMode = DesignMode; //Check if our Parent is in design mode. if (!designMode) { try { designMode = IsParentInDesignMode(this); } catch (Exception) { //TODO: We should log the exception //Need to provide a wrapper around CEF Log first } } if(designMode) { //For design mode only we remove our custom ApplicationExit event handler //As we must avoid making all unmanaged calls Application.ApplicationExit -= OnApplicationExit; } else { 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(); // Possible host is null // https://github.com/cefsharp/CefSharp/issues/3931 if (host != null) { 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://github.com/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 from when we set focus /// to the CefBrowser instance via . /// Method is only called if the browser got focus via /// before the call to . /// Can be overridden to provide custom behaviour. /// protected virtual void OnSetBrowserInitialFocus() { // MultiThreadedMessageLoop = true // Starting in M104 CEF changes mean that calling CefBrowserHost::SetFocus(true) // directly in OnAfterCreated result in the browser focus being in a strange state when using // MultiThreadedMessageLoop. Dealaying the SetFocus call results in the correct behaviour. // Here we Invoke back into the WinForms UI Thread, check if we have Focus then call // SetFocus (which will call back onto the CEF UI Thread). // It's possible to use Cef.PostAction to invoke directly on the CEF UI Thread, // this also seems to work as expected, using the WinForms UI Thread allows // us to check the Focused property to determine if we actully have focus // https://github.com/chromiumembedded/cef/issues/3436/chromium-based-browser-loses-focus-when if (InvokeRequired) { BeginInvoke((Action)(() => { if (Disposing || IsDisposed || browser?.IsDisposed == true) { return; } if (Focused) { browser?.GetHost()?.SetFocus(true); } })); } } /// /// Called after browser created. /// /// The browser. partial void OnAfterBrowserCreated(IBrowser browser) { BrowserHwnd = browser.GetHost().GetWindowHandle(); // 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); parentFormMessageInterceptor.Moving += (sender, args) => { if (IsBrowserInitialized && !IsDisposed) { browser?.GetHost()?.NotifyMoveOrResizeStarted(); } }; }); } 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); } if(initialFocus) { OnSetBrowserInitialFocus(); } RaiseIsBrowserInitializedChangedEvent(); } /// /// 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. partial void 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); } } /// /// 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); } } /// protected override void ResizeBrowser(int width, int height) { if (!designMode && 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 override void HideInternal() { ResizeBrowser(0, 0); } /// /// Show the browser (called after previous minimised) /// internal override void ShowInternal() { ResizeBrowser(Width, Height); } /// protected override void OnGotFocus(EventArgs e) { if (IsBrowserInitialized) { browser.GetHost().SetFocus(true); } else { initialFocus = true; } base.OnGotFocus(e); } /// /// Returns the current IBrowser Instance /// /// browser instance public IBrowser GetBrowser() { ThrowExceptionIfDisposed(); ThrowExceptionIfBrowserNotInitialized(); return browser; } /// /// Gets the associated with /// a specific instance. /// /// browser /// returns the assocaited or null if Disposed or no host found. public static ChromiumWebBrowser FromBrowser(IBrowser browser) { return FromBrowser(browser); } } }