From a5d2bbe145d61ebe50eed4ec8dbc1ec7054c715e Mon Sep 17 00:00:00 2001 From: Alex Maitland Date: Mon, 15 Nov 2021 09:45:57 +1000 Subject: [PATCH] OffScreen - Add async example - Old example is still there for reference, just not used by default - Add AsyncContext/SingleThreadSynchronizationContext to ensure async calls continue on main thread. --- .../AsyncContext.cs | 38 +++ .../CefSharp.MinimalExample.OffScreen.csproj | 5 + CefSharp.MinimalExample.OffScreen/Program.cs | 226 +++++++++++++----- .../SingleThreadSynchronizationContext.cs | 32 +++ 4 files changed, 246 insertions(+), 55 deletions(-) create mode 100644 CefSharp.MinimalExample.OffScreen/AsyncContext.cs create mode 100644 CefSharp.MinimalExample.OffScreen/SingleThreadSynchronizationContext.cs diff --git a/CefSharp.MinimalExample.OffScreen/AsyncContext.cs b/CefSharp.MinimalExample.OffScreen/AsyncContext.cs new file mode 100644 index 0000000..9eaf27e --- /dev/null +++ b/CefSharp.MinimalExample.OffScreen/AsyncContext.cs @@ -0,0 +1,38 @@ +/// https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/ + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace CefSharp.MinimalExample.OffScreen +{ + public static class AsyncContext + { + public static void Run(Func func) + { + var prevCtx = SynchronizationContext.Current; + + try + { + var syncCtx = new SingleThreadSynchronizationContext(); + + SynchronizationContext.SetSynchronizationContext(syncCtx); + + var t = func(); + + t.ContinueWith(delegate + { + syncCtx.Complete(); + }, TaskScheduler.Default); + + syncCtx.RunOnCurrentThread(); + + t.GetAwaiter().GetResult(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(prevCtx); + } + } + } +} diff --git a/CefSharp.MinimalExample.OffScreen/CefSharp.MinimalExample.OffScreen.csproj b/CefSharp.MinimalExample.OffScreen/CefSharp.MinimalExample.OffScreen.csproj index 86b8c34..5b45d38 100644 --- a/CefSharp.MinimalExample.OffScreen/CefSharp.MinimalExample.OffScreen.csproj +++ b/CefSharp.MinimalExample.OffScreen/CefSharp.MinimalExample.OffScreen.csproj @@ -84,6 +84,9 @@ app.manifest + + CefSharp.MinimalExample.OffScreen.Program + ..\packages\CefSharp.Common.95.7.141\lib\net452\CefSharp.dll @@ -103,8 +106,10 @@ + + diff --git a/CefSharp.MinimalExample.OffScreen/Program.cs b/CefSharp.MinimalExample.OffScreen/Program.cs index 2d29cb3..829bd14 100644 --- a/CefSharp.MinimalExample.OffScreen/Program.cs +++ b/CefSharp.MinimalExample.OffScreen/Program.cs @@ -1,4 +1,4 @@ -// Copyright © 2010-2015 The CefSharp Authors. All rights reserved. +// Copyright © 2010-2021 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. @@ -11,10 +11,19 @@ using System.Threading.Tasks; namespace CefSharp.MinimalExample.OffScreen { + /// + /// CefSharp.OffScreen Minimal Example + /// public class Program { - private static ChromiumWebBrowser browser; - + /// + /// Asynchronous demo using CefSharp.OffScreen + /// Loads google.com, uses javascript to fill out the search box then takes a screenshot which is opened + /// in the default image viewer. + /// For a synchronous demo see below. + /// + /// args + /// exit code public static int Main(string[] args) { #if ANYCPU @@ -28,6 +37,106 @@ namespace CefSharp.MinimalExample.OffScreen Console.WriteLine("You may see Chromium debugging output, please wait..."); Console.WriteLine(); + //Console apps don't have a SynchronizationContext, so to ensure our await calls continue on the main thread we use a super simple implementation from + //https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/ + //Continuations will happen on the main thread. Cef.Initialize/Cef.Shutdown must be called on the same Thread. + //The Nito.AsyncEx.Context Nuget package has a more advanced implementation + //should you wish to use a pre-build implementation. + //https://github.com/StephenCleary/AsyncEx/blob/8a73d0467d40ca41f9f9cf827c7a35702243abb8/doc/AsyncContext.md#console-example-using-asynccontext + //NOTE: This is only required if you use await + + AsyncContext.Run(async delegate + { + var settings = new CefSettings() + { + //By default CefSharp will use an in-memory cache, you need to specify a Cache Folder to persist data + CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Cache") + }; + + //Perform dependency check to make sure all relevant resources are in our output directory. + var success = await Cef.InitializeAsync(settings, performDependencyCheck: true, browserProcessHandler: null); + + if (!success) + { + throw new Exception("Unable to initialize CEF, check the log file."); + } + + // Create the CefSharp.OffScreen.ChromiumWebBrowser instance + using (var browser = new ChromiumWebBrowser(testUrl)) + { + var initialLoadResponse = await browser.WaitForInitialLoadAsync(); + + if (!initialLoadResponse.Success) + { + throw new Exception(string.Format("Page load failed with ErrorCode:{0}, HttpStatusCode:{1}", initialLoadResponse.ErrorCode, initialLoadResponse.HttpStatusCode)); + } + + var response = await browser.EvaluateScriptAsync("document.querySelector('[name=q]').value = 'CefSharp Was Here!'"); + + //Give the browser a little time to render + await Task.Delay(500); + // Wait for the screenshot to be taken. + var bitmap = await browser.ScreenshotAsync(); + + // File path to save our screenshot e.g. C:\Users\{username}\Desktop\CefSharp screenshot.png + var screenshotPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "CefSharp screenshot.png"); + + Console.WriteLine(); + Console.WriteLine("Screenshot ready. Saving to {0}", screenshotPath); + + // Save the Bitmap to the path. + // The image type is auto-detected via the ".png" extension. + bitmap.Save(screenshotPath); + + // We no longer need the Bitmap. + // Dispose it to avoid keeping the memory alive. Especially important in 32-bit applications. + bitmap.Dispose(); + + Console.WriteLine("Screenshot saved. Launching your default image viewer..."); + + // Tell Windows to launch the saved image. + Process.Start(new ProcessStartInfo(screenshotPath) + { + // UseShellExecute is false by default on .NET Core. + UseShellExecute = true + }); + + Console.WriteLine("Image viewer launched. Press any key to exit."); + } + + // Wait for user to press a key before exit + Console.ReadKey(); + + // Clean up Chromium objects. You need to call this in your application otherwise + // you will get a crash when closing. + Cef.Shutdown(); + }); + + return 0; + } + + /// + /// Synchronous demo using CefSharp.OffScreen + /// Loads google.com, uses javascript to fill out the search box then takes a screenshot which is opened + /// in the default image viewer. + /// For a asynchronous demo see above. + /// To use this demo simply delete the method and rename this method to Main. + /// + /// args + /// exit code + public static int MainSync(string[] args) + { +#if ANYCPU + //Only required for PlatformTarget of AnyCPU + CefRuntime.SubscribeAnyCpuAssemblyResolver(); +#endif + + const string testUrl = "https://www.google.com/"; + + Console.WriteLine("This example application will load {0}, take a screenshot, and save it to your desktop.", testUrl); + Console.WriteLine("You may see Chromium debugging output, please wait..."); + Console.WriteLine(); + var settings = new CefSettings() { //By default CefSharp will use an in-memory cache, you need to specify a Cache Folder to persist data @@ -38,69 +147,76 @@ namespace CefSharp.MinimalExample.OffScreen Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null); // Create the offscreen Chromium browser. - browser = new ChromiumWebBrowser(testUrl); + var browser = new ChromiumWebBrowser(testUrl); + + EventHandler handler = null; + + handler = (s, e) => + { + // Check to see if loading is complete - this event is called twice, one when loading starts + // second time when it's finished + if (!e.IsLoading) + { + // Remove the load event handler, because we only want one snapshot of the page. + browser.LoadingStateChanged -= handler; + + var scriptTask = browser.EvaluateScriptAsync("document.querySelector('[name=q]').value = 'CefSharp Was Here!'"); + + scriptTask.ContinueWith(t => + { + if(!t.Result.Success) + { + throw new Exception("EvaluateScriptAsync failed:" + t.Result.Message); + } + + //Give the browser a little time to render + Thread.Sleep(500); + // Wait for the screenshot to be taken. + var task = browser.ScreenshotAsync(); + task.ContinueWith(x => + { + // File path to save our screenshot e.g. C:\Users\{username}\Desktop\CefSharp screenshot.png + var screenshotPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "CefSharp screenshot.png"); + + Console.WriteLine(); + Console.WriteLine("Screenshot ready. Saving to {0}", screenshotPath); + + // Save the Bitmap to the path. + // The image type is auto-detected via the ".png" extension. + task.Result.Save(screenshotPath); + + // We no longer need the Bitmap. + // Dispose it to avoid keeping the memory alive. Especially important in 32-bit applications. + task.Result.Dispose(); + + Console.WriteLine("Screenshot saved. Launching your default image viewer..."); + + // Tell Windows to launch the saved image. + Process.Start(new ProcessStartInfo(screenshotPath) + { + // UseShellExecute is false by default on .NET Core. + UseShellExecute = true + }); + + Console.WriteLine("Image viewer launched. Press any key to exit."); + }, TaskScheduler.Default); + }); + } + }; // An event that is fired when the first page is finished loading. // This returns to us from another thread. - browser.LoadingStateChanged += BrowserLoadingStateChanged; + browser.LoadingStateChanged += handler; // We have to wait for something, otherwise the process will exit too soon. Console.ReadKey(); - // Clean up Chromium objects. You need to call this in your application otherwise + // Clean up Chromium objects. You need to call this in your application otherwise // you will get a crash when closing. + //The ChromiumWebBrowser instance will be disposed Cef.Shutdown(); return 0; } - - private static void BrowserLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) - { - // Check to see if loading is complete - this event is called twice, one when loading starts - // second time when it's finished - // (rather than an iframe within the main frame). - if (!e.IsLoading) - { - // Remove the load event handler, because we only want one snapshot of the initial page. - browser.LoadingStateChanged -= BrowserLoadingStateChanged; - - var scriptTask = browser.EvaluateScriptAsync("document.querySelector('[name=q]').value = 'CefSharp Was Here!'"); - - scriptTask.ContinueWith(t => - { - //Give the browser a little time to render - Thread.Sleep(500); - // Wait for the screenshot to be taken. - var task = browser.ScreenshotAsync(); - task.ContinueWith(x => - { - // Make a file to save it to (e.g. C:\Users\jan\Desktop\CefSharp screenshot.png) - var screenshotPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "CefSharp screenshot.png"); - - Console.WriteLine(); - Console.WriteLine("Screenshot ready. Saving to {0}", screenshotPath); - - // Save the Bitmap to the path. - // The image type is auto-detected via the ".png" extension. - task.Result.Save(screenshotPath); - - // We no longer need the Bitmap. - // Dispose it to avoid keeping the memory alive. Especially important in 32-bit applications. - task.Result.Dispose(); - - Console.WriteLine("Screenshot saved. Launching your default image viewer..."); - - // Tell Windows to launch the saved image. - Process.Start(new ProcessStartInfo(screenshotPath) - { - // UseShellExecute is false by default on .NET Core. - UseShellExecute = true - }); - - Console.WriteLine("Image viewer launched. Press any key to exit."); - }, TaskScheduler.Default); - }); - } - } } } diff --git a/CefSharp.MinimalExample.OffScreen/SingleThreadSynchronizationContext.cs b/CefSharp.MinimalExample.OffScreen/SingleThreadSynchronizationContext.cs new file mode 100644 index 0000000..aa79bc1 --- /dev/null +++ b/CefSharp.MinimalExample.OffScreen/SingleThreadSynchronizationContext.cs @@ -0,0 +1,32 @@ +/// https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/ + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; + +namespace CefSharp.MinimalExample.OffScreen +{ + public sealed class SingleThreadSynchronizationContext : SynchronizationContext + { + private readonly BlockingCollection> queue = + new BlockingCollection>(); + + public override void Post(SendOrPostCallback d, object state) + { + queue.Add(new KeyValuePair(d, state)); + } + + public void RunOnCurrentThread() + { + while (queue.TryTake(out var workItem, Timeout.Infinite)) + { + workItem.Key(workItem.Value); + } + } + + public void Complete() + { + queue.CompleteAdding(); + } + } +}