diff --git a/CefSharp.Avalonia.Example/App.axaml b/CefSharp.Avalonia.Example/App.axaml new file mode 100644 index 0000000..68b6696 --- /dev/null +++ b/CefSharp.Avalonia.Example/App.axaml @@ -0,0 +1,7 @@ + + + + + diff --git a/CefSharp.Avalonia.Example/App.axaml.cs b/CefSharp.Avalonia.Example/App.axaml.cs new file mode 100644 index 0000000..3452b65 --- /dev/null +++ b/CefSharp.Avalonia.Example/App.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using CefSharp.Avalonia.Example.ViewModels; +using CefSharp.Avalonia.Example.Views; +using ReactiveUI; +using Splat; + +namespace CefSharp.Avalonia.Example; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + + Locator.CurrentMutable.Register(() => new BrowserView(), typeof(IViewFor)); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var mainWindow = new MainWindow(); + + desktop.MainWindow = mainWindow; + desktop.ShutdownRequested += (s, e) => + { + mainWindow.Dispose(); + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/CefSharp.Avalonia.Example/CefSharp.Avalonia.Example.csproj b/CefSharp.Avalonia.Example/CefSharp.Avalonia.Example.csproj new file mode 100644 index 0000000..eaa4165 --- /dev/null +++ b/CefSharp.Avalonia.Example/CefSharp.Avalonia.Example.csproj @@ -0,0 +1,29 @@ + + + + WinExe + net6.0 + CefSharp.Avalonia.Example + CefSharp.Avalonia.Example + app.manifest + + + + + + + + + + + + + + + + + + + + + diff --git a/CefSharp.Avalonia.Example/MainWindow.axaml b/CefSharp.Avalonia.Example/MainWindow.axaml new file mode 100644 index 0000000..dd1bc13 --- /dev/null +++ b/CefSharp.Avalonia.Example/MainWindow.axaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CefSharp.Avalonia.Example/MainWindow.axaml.cs b/CefSharp.Avalonia.Example/MainWindow.axaml.cs new file mode 100644 index 0000000..3d3e00b --- /dev/null +++ b/CefSharp.Avalonia.Example/MainWindow.axaml.cs @@ -0,0 +1,55 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Interactivity; +using CefSharp.Avalonia.Example.ViewModels; +using CefSharp.Avalonia.Example.Views; +using CefSharp.OutOfProcess; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace CefSharp.Avalonia.Example; + +public partial class MainWindow : Window, IDisposable +{ +#if DEBUG + private string _buildType = "Debug"; +#else + private string _buildType = "Release"; +#endif + + private OutOfProcessHost _outOfProcessHost; + + public MainWindow() + { + InitializeComponent(); + + _ = InitializeComponentAsync(); + } + + private async Task InitializeComponentAsync() + { + var outOfProcessHostPath = Path.GetFullPath($"..\\..\\..\\..\\CefSharp.OutOfProcess.BrowserProcess\\bin\\{_buildType}"); + outOfProcessHostPath = Path.Combine(outOfProcessHostPath, OutOfProcessHost.HostExeName); + var cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\OutOfProcessCache"); + + var settings = Settings.WithCachePath(cachePath); + _outOfProcessHost = await OutOfProcessHost.CreateAsync(outOfProcessHostPath, settings); + + ChromiumWebBrowser.SetDefaultOutOfProcessHost(_outOfProcessHost); + + DataContext = new MainWindowViewModel(); + } + + private BrowserView ActiveBrowserView => (BrowserView) this.FindControl("tabControl").SelectedContent; + + private void OnFileExitMenuItemClick(object sender, RoutedEventArgs e) + { + Close(); + } + + public void Dispose() + { + _outOfProcessHost?.Dispose(); + } +} diff --git a/CefSharp.Avalonia.Example/Program.cs b/CefSharp.Avalonia.Example/Program.cs new file mode 100644 index 0000000..d8625a3 --- /dev/null +++ b/CefSharp.Avalonia.Example/Program.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.ReactiveUI; +using System; + +namespace CefSharp.Avalonia.Example; + +public static class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UseReactiveUI() + .UsePlatformDetect() + .LogToTrace(); +} diff --git a/CefSharp.Avalonia.Example/Roots.xml b/CefSharp.Avalonia.Example/Roots.xml new file mode 100644 index 0000000..d305257 --- /dev/null +++ b/CefSharp.Avalonia.Example/Roots.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/CefSharp.Avalonia.Example/ViewModels/BrowserViewModel.cs b/CefSharp.Avalonia.Example/ViewModels/BrowserViewModel.cs new file mode 100644 index 0000000..3882fdf --- /dev/null +++ b/CefSharp.Avalonia.Example/ViewModels/BrowserViewModel.cs @@ -0,0 +1,17 @@ +using ReactiveUI; +using System.Runtime.Serialization; + +namespace CefSharp.Avalonia.Example.ViewModels +{ + public class BrowserViewModel : ViewModelBase + { + private string _header; + + [DataMember] + public string Header + { + get => _header; + set => this.RaiseAndSetIfChanged(ref _header, value); + } + } +} diff --git a/CefSharp.Avalonia.Example/ViewModels/MainWindowViewModel.cs b/CefSharp.Avalonia.Example/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..c97a70a --- /dev/null +++ b/CefSharp.Avalonia.Example/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,19 @@ +using System.Collections.ObjectModel; + +namespace CefSharp.Avalonia.Example.ViewModels +{ + internal class MainWindowViewModel : ViewModelBase + { + public ObservableCollection Tabs { get; } = new(); + + public MainWindowViewModel() + { + AddTab(); + } + + public void AddTab() + { + Tabs.Add(new BrowserViewModel { Header = "New Tab" }); + } + } +} diff --git a/CefSharp.Avalonia.Example/ViewModels/ViewModelBase.cs b/CefSharp.Avalonia.Example/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..f5b728e --- /dev/null +++ b/CefSharp.Avalonia.Example/ViewModels/ViewModelBase.cs @@ -0,0 +1,22 @@ +using ReactiveUI; +using System.Reactive.Disposables; + +namespace CefSharp.Avalonia.Example.ViewModels +{ + public class ViewModelBase : ReactiveObject, IActivatableViewModel + { + public ViewModelActivator Activator { get; } + + public ViewModelBase() + { + Activator = new ViewModelActivator(); + this.WhenActivated((CompositeDisposable disposables) => + { + /* handle activation */ + Disposable + .Create(() => { /* handle deactivation */ }) + .DisposeWith(disposables); + }); + } + } +} diff --git a/CefSharp.Avalonia.Example/Views/BrowserView.axaml b/CefSharp.Avalonia.Example/Views/BrowserView.axaml new file mode 100644 index 0000000..ee09521 --- /dev/null +++ b/CefSharp.Avalonia.Example/Views/BrowserView.axaml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/CefSharp.Avalonia.Example/Views/BrowserView.axaml.cs b/CefSharp.Avalonia.Example/Views/BrowserView.axaml.cs new file mode 100644 index 0000000..e0a9cf0 --- /dev/null +++ b/CefSharp.Avalonia.Example/Views/BrowserView.axaml.cs @@ -0,0 +1,60 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Threading; +using CefSharp.Avalonia.Example.ViewModels; +using CefSharp.OutOfProcess; +using ReactiveUI; +using Tmds.DBus; + +namespace CefSharp.Avalonia.Example.Views; + +public partial class BrowserView : UserControl, IViewFor +{ + public BrowserView() + { + InitializeComponent(); + + Browser.Address = "https://www.google.com"; + Browser.AddressChanged += OnAddressChanged; + Browser.TitleChanged += OnTitleChanged; + } + + public BrowserViewModel? ViewModel { get; set; } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (BrowserViewModel?)value; + } + + private void OnTitleChanged(object sender, TitleChangedEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + ViewModel.Header = e.Title; + }); + } + + private void OnAddressChanged(object sender, AddressChangedEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + var addressTextBox = this.FindControl("addressTextBox"); + + addressTextBox.Text = e.Address; + }); + } + + private void OnAddressTextBoxKeyDown(object sender, global::Avalonia.Input.KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + Browser.Address = ((TextBox)sender).Text; + } + } + + public void Dispose() + { + //browser.Dispose(); + } +} \ No newline at end of file diff --git a/CefSharp.Avalonia.Example/app.manifest b/CefSharp.Avalonia.Example/app.manifest new file mode 100644 index 0000000..f2c18ac --- /dev/null +++ b/CefSharp.Avalonia.Example/app.manifest @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + true + true + + + + + + + diff --git a/CefSharp.Avalonia/CefSharp.Avalonia.csproj b/CefSharp.Avalonia/CefSharp.Avalonia.csproj new file mode 100644 index 0000000..d48ce5f --- /dev/null +++ b/CefSharp.Avalonia/CefSharp.Avalonia.csproj @@ -0,0 +1,30 @@ + + + + netstandard2.0 + CefSharp.Avalonia + CefSharp.Avalonia + true + CefSharp.Avalonia + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + Latest + + + + + + + + + + + + + + + + + + + diff --git a/CefSharp.Avalonia/ChromiumWebBrowser.cs b/CefSharp.Avalonia/ChromiumWebBrowser.cs new file mode 100644 index 0000000..330fe8c --- /dev/null +++ b/CefSharp.Avalonia/ChromiumWebBrowser.cs @@ -0,0 +1,768 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.VisualTree; +using CefSharp.Avalonia.Internals; +using CefSharp.Dom; +using CefSharp.OutOfProcess; +using CefSharp.OutOfProcess.Internal; +using CefSharp.OutOfProcess.Model; +using PInvoke; +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace CefSharp.Avalonia +{ + /// + /// The Avalonia CEF browser. + /// + public class ChromiumWebBrowser : NativeControlHost, IChromiumWebBrowserInternal + { + private static OutOfProcessHost _defaultOutOfProcessHost = null; + + private OutOfProcessHost _host; + private IntPtr _browserHwnd = IntPtr.Zero; + private OutOfProcessConnectionTransport _devToolsContextConnectionTransport; + private IDevToolsContext _devToolsContext; + private int _id; + private bool _devToolsReady; + + /// + /// Handle we'll use to host the browser + /// + private IntPtr _hwndHost; + /// + /// The ignore URI change + /// + private bool _ignoreUriChange; + /// + /// Initial address + /// + private string _initialAddress; + /// + /// Has the underlying Cef Browser been created (slightly different to initliazed in that + /// the browser is initialized in an async fashion) + /// + private bool _browserCreated; + /// + /// The browser initialized - boolean represented as 0 (false) and 1(true) as we use Interlocker to increment/reset + /// + private int _browserInitialized; + /// + /// A flag that indicates whether or not the designer is active + /// NOTE: Needs to be static for OnApplicationExit + /// + private static bool DesignMode; + + /// + /// 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; + + /// + /// Current DPI Scale + /// + private double _dpiScale; + + /// + /// This flag is set when the browser gets focus before the underlying CEF browser + /// has been initialized. + /// + private bool _initialFocus; + + /// + /// Can the browser navigate back. + /// + private bool _canGoBack; + + /// + /// Can the browser navigate forward. + /// + private bool _canGoForward; + + /// + /// Is the browser currently loading a web page. + /// + private bool _isLoading; + + /// + /// Browser Title. + /// + private string _title; + + /// + /// Address + /// + private string _address; + + /// + /// Activates browser upon creation, the default value is false. Prior to version 73 + /// the default behaviour was to activate browser on creation (Equivilent 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 a value indicating whether this instance is disposed. + /// + /// if this instance is disposed; otherwise, . + public bool IsDisposed + { + get + { + return Interlocked.CompareExchange(ref _disposeSignaled, 1, 1) == 1; + } + } + + /// + public event EventHandler DOMContentLoaded; + /// + public event EventHandler BrowserProcessCrashed; + /// + public event EventHandler FrameAttached; + /// + public event EventHandler FrameDetached; + /// + public event EventHandler FrameNavigated; + /// + public event EventHandler JavaScriptLoad; + /// + public event EventHandler RuntimeExceptionThrown; + /// + public event EventHandler Popup; + /// + public event EventHandler NetworkRequest; + /// + public event EventHandler NetworkRequestFailed; + /// + public event EventHandler NetworkRequestFinished; + /// + public event EventHandler NetworkRequestServedFromCache; + /// + public event EventHandler NetworkResponse; + /// + public event EventHandler AddressChanged; + /// + public event EventHandler LoadingStateChanged; + /// + public event EventHandler StatusMessage; + /// + public event EventHandler ConsoleMessage; + /// + public event EventHandler LifecycleEvent; + /// + public event EventHandler DevToolsContextAvailable; + + /// + /// Event handler that will get called when the browser title changes + /// + public event EventHandler TitleChanged; + + /// + /// Event called after the underlying CEF browser instance has been created + /// + public event EventHandler BrowserCreated; + + /// + /// Navigates to the previous page in the browser history. Will automatically be enabled/disabled depending on the + /// browser state. + /// + /// The back command. + public ICommand BackCommand { get; private set; } + /// + /// Navigates to the next page in the browser history. Will automatically be enabled/disabled depending on the + /// browser state. + /// + /// The forward command. + public ICommand ForwardCommand { get; private set; } + /// + /// Reloads the content of the current page. Will automatically be enabled/disabled depending on the browser state. + /// + /// The reload command. + public ICommand ReloadCommand { get; private set; } + + public ICommand StopCommand { get; private set; } + + /// + /// CanGoBack Property + /// + public static readonly DirectProperty CanGoBackProperty = + AvaloniaProperty.RegisterDirect(nameof(CanGoBack), o => o.CanGoBack); + + /// + /// 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. + public bool CanGoBack + { + get { return _canGoBack; } + private set { SetAndRaise(CanGoBackProperty, ref _canGoBack, value); } + } + + /// + /// CanGoBack Property + /// + public static readonly DirectProperty CanGoForwardProperty = + AvaloniaProperty.RegisterDirect(nameof(CanGoForward), o => o.CanGoForward); + + /// + /// A flag that indicates whether the state of the control current supports the GoForward action (true) or not (false). + /// + /// true if this instance can go forward; otherwise, false. + public bool CanGoForward + { + get { return _canGoForward; } + private set { SetAndRaise(CanGoForwardProperty, ref _canGoForward, value); } + } + + /// + /// The title of the web page being currently displayed. + /// + /// The title. + public string Title + { + get { return _title; } + set { SetAndRaise(TitleProperty, ref _title, value); } + } + + /// + /// The title property + /// + public static readonly DirectProperty TitleProperty = + AvaloniaProperty.RegisterDirect(nameof(Title), o => o.Title); + + /// + /// Handles the event. + /// + /// The d. + /// The instance containing the event data. + private static void OnTitleChanged(ChromiumWebBrowser owner, AvaloniaPropertyChangedEventArgs e) + { + var args = new TitleChangedEventArgs(e.GetNewValue()); + owner.TitleChanged?.Invoke(owner, args); + } + + static ChromiumWebBrowser() + { + AddressProperty.Changed.AddClassHandler(OnAddressChanged); + TitleProperty.Changed.AddClassHandler(OnTitleChanged); + IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged); + } + + /// + /// Initializes a new instance of the instance. + /// + public ChromiumWebBrowser() : this(null) + { + } + + /// + /// Initializes a new instance of the instance. + /// + /// Out of process host + /// address to load initially + public ChromiumWebBrowser(OutOfProcessHost host, string initialAddress = null) + { + _host = host; + _initialAddress = initialAddress; + + _host ??= _defaultOutOfProcessHost; + + if (_host == null) + { + throw new ArgumentNullException(nameof(host)); + } + + Focusable = true; + + BackCommand = new DelegateCommand(() => _devToolsContext.GoBackAsync(), () => CanGoBack); + ForwardCommand = new DelegateCommand(() => _devToolsContext.GoForwardAsync(), () => CanGoForward); + ReloadCommand = new DelegateCommand(() => _devToolsContext.ReloadAsync(), () => !IsLoading); + //StopCommand = new DelegateCommand(this.Stop); + + UseLayoutRounding = true; + LayoutUpdated += OnLayoutUpdated; + } + + private void OnLayoutUpdated(object sender, EventArgs e) + { + var bounds = Bounds; + + ResizeBrowser((int)bounds.Width, (int)bounds.Height); + } + + /// + int IChromiumWebBrowserInternal.Id + { + get { return _id; } + } + + /// + /// DevToolsContext - provides communication with the underlying browser + /// + public IDevToolsContext DevToolsContext + { + get + { + if (_devToolsReady) + { + return _devToolsContext; + } + + return default; + } + } + + /// + public bool IsBrowserInitialized => _browserHwnd != IntPtr.Zero; + + + /// + public Frame[] Frames => _devToolsContext?.Frames; + + /// + public Frame MainFrame => _devToolsContext?.MainFrame; + + /// + void IChromiumWebBrowserInternal.OnDevToolsMessage(string jsonMsg) + { + _devToolsContextConnectionTransport?.InvokeMessageReceived(jsonMsg); + } + + /// + void IChromiumWebBrowserInternal.OnDevToolsReady() + { + var ctx = (DevToolsContext)_devToolsContext; + + ctx.DOMContentLoaded += DOMContentLoaded; + ctx.Error += BrowserProcessCrashed; + ctx.FrameAttached += FrameAttached; + ctx.FrameDetached += FrameDetached; + ctx.FrameNavigated += FrameNavigated; + ctx.Load += JavaScriptLoad; + ctx.PageError += RuntimeExceptionThrown; + ctx.Popup += Popup; + ctx.Request += NetworkRequest; + ctx.RequestFailed += NetworkRequestFailed; + ctx.RequestFinished += NetworkRequestFinished; + ctx.RequestServedFromCache += NetworkRequestServedFromCache; + ctx.Response += NetworkResponse; + ctx.Console += ConsoleMessage; + ctx.LifecycleEvent += LifecycleEvent; + + _ = ctx.InvokeGetFrameTreeAsync().ContinueWith(t => + { + _devToolsReady = true; + + DevToolsContextAvailable?.Invoke(this, EventArgs.Empty); + + //NOW the user can start using the devtools context + }, TaskScheduler.Current); + + // Only call Load if initialAddress is null and Address is not empty + if (string.IsNullOrEmpty(_initialAddress) && !string.IsNullOrEmpty(Address)) + { + LoadUrl(Address); + } + } + + /// + public void LoadUrl(string url) + { + _ = _devToolsContext.GoToAsync(url); + } + + /// + public Task LoadUrlAsync(string url, int? timeout = null, WaitUntilNavigation[] waitUntil = null) + { + return _devToolsContext.GoToAsync(url, timeout, waitUntil); + } + + /// + public Task GoBackAsync(NavigationOptions options = null) + { + return _devToolsContext.GoBackAsync(options); + } + + /// + public Task GoForwardAsync(NavigationOptions options = null) + { + return _devToolsContext.GoForwardAsync(options); + } + + /// + public Task SetRequestContextPreferenceAsync(string name, object value) + { + if (_host == null) + { + throw new ObjectDisposedException(nameof(ChromiumWebBrowser)); + } + + return _host.SetRequestContextPreferenceAsync(_id, name, value); + } + + /// + protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) + { + var handle = base.CreateNativeControlCore(parent); + + _dpiScale = this.GetVisualRoot()?.RenderScaling ?? 1.0; + + _hwndHost = handle.Handle; + + _host.CreateBrowser(this, _hwndHost, url: _initialAddress, out _id); + + _devToolsContextConnectionTransport = new OutOfProcessConnectionTransport(_id, _host); + + var connection = DevToolsConnection.Attach(_devToolsContextConnectionTransport); + _devToolsContext = Dom.DevToolsContext.CreateForOutOfProcess(connection); + + return handle; + } + + /// + protected override void DestroyNativeControlCore(IPlatformHandle control) + { + _host.CloseBrowser(_id); + + base.DestroyNativeControlCore(control); + } + + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + + if (InternalIsBrowserInitialized()) + { + _host.SetFocus(_id, true); + } + else + { + _initialFocus = true; + } + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + + if (InternalIsBrowserInitialized()) + { + _host.SetFocus(_id, false); + } + } + + /// + void IChromiumWebBrowserInternal.SetAddress(string address) + { + UiThreadRun(() => + { + _ignoreUriChange = true; + Address = address; + _ignoreUriChange = false; + }); + } + + /// + void IChromiumWebBrowserInternal.SetLoadingStateChange(bool canGoBack, bool canGoForward, bool isLoading) + { + UiThreadRun(() => + { + CanGoBack = canGoBack; + CanGoForward = CanGoForward; + IsLoading = isLoading; + + ((DelegateCommand)BackCommand).RaiseCanExecuteChanged(); + ((DelegateCommand)ForwardCommand).RaiseCanExecuteChanged(); + ((DelegateCommand)ReloadCommand).RaiseCanExecuteChanged(); + }); + + LoadingStateChanged?.Invoke(this, new LoadingStateChangedEventArgs(canGoBack, canGoForward, isLoading)); + } + + /// + void IChromiumWebBrowserInternal.SetTitle(string title) + { + UiThreadRun(() => Title = title); + } + + /// + void IChromiumWebBrowserInternal.SetStatusMessage(string msg) + { + StatusMessage?.Invoke(this, new StatusMessageEventArgs(msg)); + } + + /// + void IChromiumWebBrowserInternal.OnAfterBrowserCreated(IntPtr hwnd) + { + if (IsDisposed) + { + return; + } + + _browserHwnd = hwnd; + + Interlocked.Exchange(ref _browserInitialized, 1); + + UiThreadRun(() => + { + if (!IsDisposed) + { + BrowserCreated?.Invoke(this, EventArgs.Empty); + + var bounds = Bounds; + + ResizeBrowser((int)bounds.Width, (int)bounds.Height); + } + }); + + if (_initialFocus) + { + _host.SetFocus(_id, true); + } + } + + /// + /// Resizes the browser to the specified and . + /// If and are both 0 then the browser + /// will be hidden and resource usage will be minimised. + /// + /// width + /// height + protected virtual void ResizeBrowser(int width, int height) + { + if (_browserHwnd != IntPtr.Zero) + { + if (_dpiScale > 1) + { + width = (int)(width * _dpiScale); + height = (int)(height * _dpiScale); + } + + if (width == 0 && height == 0) + { + // For windowed browsers when the frame window is minimized set the + // browser window size to 0x0 to reduce resource usage. + HideInternal(); + } + else + { + ShowInternal(width, height); + } + } + } + + private void ShowInternal(int width, int height) + { + if (_browserHwnd != IntPtr.Zero) + { + User32.SetWindowPos(_browserHwnd, IntPtr.Zero, 0, 0, width, height, User32.SetWindowPosFlags.SWP_NOZORDER); + } + } + + private void HideInternal() + { + if (_browserHwnd != IntPtr.Zero) + { + User32.SetWindowPos(_browserHwnd, IntPtr.Zero, 0, 0, 0, 0, User32.SetWindowPosFlags.SWP_NOZORDER | User32.SetWindowPosFlags.SWP_NOMOVE | User32.SetWindowPosFlags.SWP_NOACTIVATE); + } + } + + /// + /// 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. + public string Address + { + get { return _address; } + set { SetAndRaise(AddressProperty, ref _address, value); } + } + + /// + /// The address property + /// + public static readonly DirectProperty AddressProperty = AvaloniaProperty.RegisterDirect(nameof(Address), o => o.Address); + + private static void OnAddressChanged(ChromiumWebBrowser browser, AvaloniaPropertyChangedEventArgs e) + { + browser.OnAddressChanged(e.GetOldValue(), e.GetNewValue()); + + browser.AddressChanged?.Invoke(browser, new AddressChangedEventArgs(e.GetNewValue())); + } + + /// + /// Called when [address changed]. + /// + /// The old value. + /// The new value. + protected virtual void OnAddressChanged(string oldValue, string newValue) + { + if(!InternalIsBrowserInitialized()) + { + _initialAddress = newValue; + return; + } + + if (_ignoreUriChange || newValue == null) + { + return; + } + + LoadUrl(newValue); + } + + /// + /// 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. + public bool IsLoading + { + get { return _isLoading ; } + private set { SetAndRaise(IsLoadingProperty, ref _isLoading, value); } + } + + /// + /// The is loading property + /// + public static readonly DirectProperty IsLoadingProperty = AvaloniaProperty.RegisterDirect(nameof(IsLoading), o=> o.IsLoading); + + private static void OnVisibleChanged(ChromiumWebBrowser owner, AvaloniaPropertyChangedEventArgs args) + { + if (owner.InternalIsBrowserInitialized()) + { + var isVisible = args.GetNewValue(); + if (isVisible) + { + var bounds = owner.Bounds; + owner.ResizeBrowser((int)bounds.Width, (int)bounds.Height); + } + else + { + //Hide browser + owner.ResizeBrowser(0, 0); + } + } + } + + /// + /// Runs the specific Action on the Dispatcher in an async fashion + /// + /// The action. + /// The priority. + private void UiThreadRun(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) + { + action(); + } + + _ = Dispatcher.UIThread.InvokeAsync(action); + } + + /// + /// Check is browserisinitialized + /// + /// true if browser is initialized + private bool InternalIsBrowserInitialized() + { + // Use CompareExchange to read the current value - if disposeCount is 1, we set it to 1, effectively a no-op + // Volatile.Read would likely use a memory barrier which I believe is unnecessary in this scenario + return Interlocked.CompareExchange(ref _browserInitialized, 0, 0) == 1; + } + + /// + /// Sets a global (static) instance of that + /// will be used when no explicit implementation is provided. + /// + /// host + /// TODO: This needs improving + public static void SetDefaultOutOfProcessHost(OutOfProcessHost host) + { + _defaultOutOfProcessHost = host; + } + + protected virtual void Dispose(bool disposing) + { + // Attempt to move the disposeSignaled state from 0 to 1. + // If successful, we can safely dispose of the object. + if (Interlocked.CompareExchange(ref _disposeSignaled, 1, 0) != 0) + { + return; + } + + if (DesignMode) + { + return; + } + + if (disposing) + { + Interlocked.Exchange(ref _browserInitialized, 0); + + // Don't maintain a reference to event listeners anylonger: + BrowserCreated = null; + AddressChanged = null; + LoadingStateChanged = null; + StatusMessage = null; + TitleChanged = null; + + var ctx = (DevToolsContext)_devToolsContext; + + ctx.DOMContentLoaded -= DOMContentLoaded; + ctx.Error -= BrowserProcessCrashed; + ctx.FrameAttached -= FrameAttached; + ctx.FrameDetached -= FrameDetached; + ctx.FrameNavigated -= FrameNavigated; + ctx.Load -= JavaScriptLoad; + ctx.PageError -= RuntimeExceptionThrown; + ctx.Popup -= Popup; + ctx.Request -= NetworkRequest; + ctx.RequestFailed -= NetworkRequestFailed; + ctx.RequestFinished -= NetworkRequestFinished; + ctx.RequestServedFromCache -= NetworkRequestServedFromCache; + ctx.Response -= NetworkResponse; + ctx.Console -= ConsoleMessage; + ctx.LifecycleEvent -= LifecycleEvent; + + DOMContentLoaded = null; + BrowserProcessCrashed = null; + FrameAttached = null; + FrameNavigated = null; + JavaScriptLoad = null; + RuntimeExceptionThrown = null; + Popup = null; + NetworkRequest = null; + NetworkRequestFailed = null; + NetworkRequestFinished = null; + NetworkRequestServedFromCache = null; + NetworkResponse = null; + ConsoleMessage = null; + LifecycleEvent = null; + } + + _host?.CloseBrowser(_id); + _host = null; + } + + ~ChromiumWebBrowser() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/CefSharp.Avalonia/Internals/DelegateCommand.cs b/CefSharp.Avalonia/Internals/DelegateCommand.cs new file mode 100644 index 0000000..95676ca --- /dev/null +++ b/CefSharp.Avalonia/Internals/DelegateCommand.cs @@ -0,0 +1,67 @@ +// Copyright © 2019 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.Windows.Input; + +namespace CefSharp.Avalonia.Internals; + +/// +/// DelegateCommand +/// +/// +internal class DelegateCommand : ICommand +{ + /// + /// The command handler + /// + private readonly Action _commandHandler; + /// + /// The can execute handler + /// + private readonly Func _canExecuteHandler; + + /// + /// Occurs when changes occur that affect whether or not the command should execute. + /// + public event EventHandler CanExecuteChanged; + + /// + /// Initializes a new instance of the class. + /// + /// The command handler. + /// The can execute handler. + public DelegateCommand(Action commandHandler, Func canExecuteHandler = null) + { + _commandHandler = commandHandler; + _canExecuteHandler = canExecuteHandler; + } + + /// + /// Defines the method to be called when the command is invoked. + /// + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + public void Execute(object parameter) + { + _commandHandler(); + } + + /// + /// Defines the method that determines whether the command can execute in its current state. + /// + /// Data used by the command. If the command does not require data to be passed, this object can be set to null. + /// true if this command can be executed; otherwise, false. + public bool CanExecute(object parameter) + { + return _canExecuteHandler == null || _canExecuteHandler(); + } + + /// + /// Raises the can execute changed. + /// + public void RaiseCanExecuteChanged() + { + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } +} diff --git a/CefSharp.OutOfProcess.sln b/CefSharp.OutOfProcess.sln index 54e71e7..332f319 100644 --- a/CefSharp.OutOfProcess.sln +++ b/CefSharp.OutOfProcess.sln @@ -27,6 +27,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CefSharp.Avalonia", "CefSharp.Avalonia\CefSharp.Avalonia.csproj", "{5591951C-DAC1-4014-BD17-B692B91A6CC5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CefSharp.Avalonia.Example", "CefSharp.Avalonia.Example\CefSharp.Avalonia.Example.csproj", "{3F8073FB-B167-4C2B-A1D8-5040BD1F6E84}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -65,6 +69,14 @@ Global {7D39CC02-FB02-4F03-B329-E5DB567EC354}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D39CC02-FB02-4F03-B329-E5DB567EC354}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D39CC02-FB02-4F03-B329-E5DB567EC354}.Release|Any CPU.Build.0 = Release|Any CPU + {5591951C-DAC1-4014-BD17-B692B91A6CC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5591951C-DAC1-4014-BD17-B692B91A6CC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5591951C-DAC1-4014-BD17-B692B91A6CC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5591951C-DAC1-4014-BD17-B692B91A6CC5}.Release|Any CPU.Build.0 = Release|Any CPU + {3F8073FB-B167-4C2B-A1D8-5040BD1F6E84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F8073FB-B167-4C2B-A1D8-5040BD1F6E84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F8073FB-B167-4C2B-A1D8-5040BD1F6E84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F8073FB-B167-4C2B-A1D8-5040BD1F6E84}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE