A .NET support library for the Microsoft.Web.WebView2
control to aid with common operations and .NET / JavaScript interop as well as Html to Pdf generation.
The library provides:
-
Base
WebViewHandler
behavior class
Base class that can be overridden or used as is to facilitate initializing the WebView control for interaction via JavaScript and custom navigation. Provides easy support for folder to virtual site mapping, easy Interop calls into JavaScript and a number of helpers for navigation and request interception. -
JavaScript Interop Class
Provides a base class that can be used for easily wrapping Javascript interop calls. The class allows JS method invocation and handles all inbound and outbound data serialization. -
Cached WebView Environment
The WebView handler optionally uses a cached WebView environment to ensure consistent WebView environment reuse. For more info see:* A CachedWebView Environment to ensure consistent Environment reuse -
Html To Pdf Conversion: HtmlToPdfHost
A standaloneHtmlToPdfHost
component that can be used to convert Html content from any Url or local file into PDF files or streams using the Chromium PDF generation engine. Unlike many other Html to Pdf generation tools, this component uses the built-in Windows WebView Runtime, so there's no large runtime dependency in your applications.
HtmlToPdf functionality is documented separately.
The WebView Handler is meant to be used when you need to do a lot of Interop between your .NET and JavaScript code. It ties together the WebView initialization, calling of methods in JavaScript and receiving callbacks back into .NET from JavaScript and hooking common events that you might have to deal with.
To install the library install the NuGet package from:
install-package Westwind.WebView
The WebView Handler is primarily meant to be used when you need to do a lot of Interop between your .NET and JavaScript code. It ties together the WebView initialization, calling of methods in JavaScript and receiving callbacks back into .NET from JavaScript. Initialization initializes the WebView but also provides hooks for when content has loaded so you can start running JavaScript code and pass in state when the WebView initially loads.
There are three distinct components:
-
The
WebViewHandler
This is the top level object that handles WebView initialization, setting up things like mapping a local file path to a Web domain (if needed), hooking up a .NET callback object that can be called from .NET and creating an instance of a JavaScript proxy that allows more easily calling into JavaScript. -
The JavaScript Interop Object
This is a class that acts as an RPC proxy into .NET that basically helps you makeExecuteScriptAsync()
calls into .NET by automatically handling parameter serialization and result deserialization. Using a Reflection like interface that lets you useInvoke()
andGet<T>()
,Set<T>()
methods to interop with JavaScript code. -
A Dotnet Callback Object
This objects is 'passed into JavaScript' and accessible as a host object in JavaScript via:// Async let result = await window.chrome.webview.hostObjects.websurge.RunRequest(url); // Sync let success = window.chrome.webview.hostObjects.sync.websurge.NavigateLink(url);
A host object is just a .NET POCO object that contains methods to callback to either sync or async. You don't need to provide this if you don't have callbacks, or if you have only very few you can just use the JavaScriptInterop object to hold those methods and pass that.
Both the JavaScript and Dotnet objects are optional. You can pass those in as null values and they won't be set or used for anything, but if that's the case you probably don't have much need for the
WebViewHandler
in the first place. -
A Cached WebView Environment Class
This class handles consistently initializing and re-using a single WebView environment inside of an application to avoid odd behaviors due to incompatible environment settings. Avoids among other things creating a default environment which may not work in some application if permissions don't allow access to create the default environment folder. The static methods of this class are used by default by theWebViewHandler
to initialize the environment viaCoreWebView2Environment.CreateAsync()
.
WebViewHandler
is a behavior class that is attached to an existing instance of a WebView control, typically assigned in the constructor of the WebView host control.
The recommended way to use these tools is:
- Create an application specific subclass for
WebViewHandler
and - as needed -JavaScriptInterop
andDotnetInterop
objects - Attach the behavior to a WebView control in the host's control or form CTOR.
The recommended way to use these classes is by deriving an application specific subclass for the WebViewHandler
, and if needed JavaScriptInterop
and DotnetInterop
objects:
/// <summary>
/// Create an application specific implementation of the WebView Handler
///
/// CTOR/base configures the optional dependencies for the JavaScript and Dotnet
/// interop objects.
/// </summary>
public class DocumentationPreviewHandler : WebViewHandler<DocumentationPreviewJavaScriptInterop>
{
public DocumentationPreviewHandler(WebView2 webBrowser) :
base(webBrowser, wsApp.Constants.WebViewEnvironmentFolderName, new DocumentationPreviewDotnetInterop())
{
}
}
/// <summary>
/// Subclass from the BaseJavaScriptInterop class to get the abililty to easily
/// call methods in the JavaScript code.
///
/// Recommend you create a method for each JavaScript call you make using `Invoke()`
/// </summary>
public class DocumentationPreviewJavaScriptInterop : BaseJavaScriptInterop
{
public DocumentationViewer DocumentationViewer { get; set; }
public DocumentationPreviewJavaScriptInterop(WebView2 webBrowser, string baseInvocationTarget = "window") : base(webBrowser,baseInvocationTarget)
{
}
/// <summary>
/// Update the document with an HTML string. Optional line number
/// on where to scroll the document to.
/// </summary>
/// <param name="html"></param>
/// <param name="lineNo"></param>
public async Task UpdateDocumentContent(string html, int lineNo)
{
await Invoke("updateDocumentContent", html, lineNo);
}
/// <summary>
/// Scroll to a specific line in the document
/// </summary>
public async Task ScrollToPragmaLine(int editorLineNumber = -1,
string headerId = null,
bool updateCodeBlocks = true,
bool noScrollTimeout = false, bool noScrollTopAdjustment = false)
{
await Invoke("scrollToPragmaLine",
editorLineNumber, headerId,
noScrollTimeout, noScrollTopAdjustment);
}
}
/// <summary>
/// If you have a lot of callbacks use a separate object.
/// Otherwise you may just use the JavaScript object above
/// to send callbacks to.
///
/// This is a plain .NET object - keep it simple as this it
/// uses COM for its marshaling.
/// </summary>
[ComVisible(true)]
public class DocumentationPreviewDotnetInterop
{
}
Once you've created the handler you can then assign it to a Web View control.
In its simplest for you can just instantiate the handler:
// Host control
public partial class DocumentationViewer : UserControl
{
DocumentationViewerModel Model { get; }
public DocumentationPreviewHandler PreviewHandler { get; set; }
// do this either in the CTOR or Loaded
private void DocumentationViewer_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
var jsInterop = new DocumentationPreviewJavaScriptInterop(PreviewBrowser, "window")
{
DocumentationViewer = this // custom state
};
PreviewHandler = new DocumentationPreviewHandler(PreviewBrowser)
{
JsInterop = jsInterop // optional
};
}
}
Something a little more sophisticated might look like this where we specify a host of additional settings:
void ConfigureEditor(RequestDocumentationItem documentation)
{
var dotnetHostObject = new DocumentationEditorDotnetHostObject(AppModel.Current, this, null)
{
DocItem = documentation
};
var jsInterop = new DocumentationEditorJavaScriptInterop(EditorBrowser, "window.textEditor");
//Loaded += DocumentationViewer_Loaded;
#if DEBUG
//var editorPath = "Editor"; // production folder
var editorPath = @"d:\projects\WebSurge2\WebSurge\Html\Editor";
#else
var editorPath = System.IO.Path.GetFullPath(".\\HTML\\Editor"); // production folder
#endif
EditorHandler = new DocumentationEditorWebViewHandler(EditorBrowser, dotnetHostObject)
{
JsInterop = jsInterop,
HostObjectName = "mm", // HostObject name inside of WebView
ShowDevTools = false,
HostWebRootFolder = editorPath, // folder used as web site
HostWebHostNameForFolder = "websurge.doceditor", // mapped domain
InitialUrl = "https://websurge.doceditor/editor.htm"
};
// additional app specific properties in custom version that are used for initial nav
EditorHandler.InitialValue = documentation.Documentation; // custom logic applied
}
This initial assignment triggers the initialization of the WebView and essentially starts an initial navigation with the assigned InitialUrl
(or Source
if not assigned). InitialUrl is a delayed navigation that ensures that the URL is not set until after the Host folder is mapped. This avoids failed navigations on initial display of the WebView.
This class can be used to initialize the WebView Environment consistently. It's also used internally by WebViewHandler
if no explicit environment is passed set in the CTOR.
This class can be used independently of WebViewHandler
if you manually instantiate your WebView environment.
The first step is to initialize the WebView Environment folder location and set any options. This should be done before the WebView is first instantiated preferably during startup of the application.
In WPF OnStartup()
is a good place:
protected override void OnStartup(StartupEventArgs e)
{
// initialize single environment folder for all WebViews
CachedWebViewEnvironment.Current.EnvironmentFolderName = Path.Combine(
mmApp.Configuration.LocalAppDataFolder,
mmApp.Constants.WebViewEnvironmentFolderName);
// Optionally - set any custom startup flags and options.
// Typically this can be left at null
// CachedWebViewEnvironment.Current.EnvironmentOptions = null;
...
}
Then, anywhere you need to use a WebView Environment, you can then initialize the WebView with this environment via the InitializeWebViewEnvironment()
method, which either creates a new environment if it doesn't exist yet, or reuses the previously created one that is cached.
This method calls webBrowser.EnsureCoreWebView2Async()
to wait for the WebView to be initialized and become UI active (!) using the cached environment as its parameter.
Note the
EnsureCoreWebView2Async()
and by extensionInitializeWebViewEnvironment()
can take a long time to complete as it waits for UI activation before returning.If the WebView is not visible (ie. inactive on another tab, or otherwise not visible) it will not return until it becomes active.
In a usage scenario you can use InitializeWebViewEnvironment()
like this during WebView initialization:
// Manual WebView Initialization
protected async Task InitializeAsync()
{
if (JsInterop == null)
JsInterop = CreateJsInteropInstance();
if (!IsInitialized) // Ensure this doesn't run more than once
{
// THIS
await CachedWebViewEnvironment.Current.InitializeWebViewEnvironment(WebBrowser);
if(InitializeComplete != null)
InitializeComplete();
}
...
// Code to set up Virtual Folder mapping
// initial navigation etc.
}
Rinse and repeat this process if you have multiple WebView controls in your application, or if you are repeatedly creating the same control.