
When building hybrid Web applications where .NET code interacts with WebView HTML content, one of the biggest sticking points often is the event based disconnect between loading a document and then having to wait for the document to load and then starting to interact with the document via script code.
However, to achieve a more seamless experience, it's crucial to implement event handlers that can detect when the document is fully loaded. By subscribing to events like NavigationCompleted
or DOMContentLoaded
, developers can ensure that their scripts execute only after the content is ready. This approach not only enhances performance but also reduces errors that arise from attempting to manipulate the DOM prematurely.
It's easy to use the WebView control and just display a Url where you are essentially letting the browser complete the page load asynchronously in the background. Take something even as simple as the following when you want to preload some content into the HTML document:
WebView.Source = "http://localhost:5200/MyPage"
// BOOM!
await WebView.ExecuteAsync("alert('Document has loaded.')");
Yeah, dumb example, but you get the idea of what this tries to accomplish: Load a page and pop up an alert box over it when it loads.
But - the above code, simplistic as it is, doesn't work, because the script is executed before the page has completed loading. That code will fail!
It's not very difficult to make this work but it takes a little bit of knowledge about the WebView and how it works and you have to implement events in order to make this work. Here's a rough outline of the code you'd need to make the above code actually work:
string envPath = Path.Combine(Path.GetTempPath(), "WpfSample_WebView");
var environment = await CoreWebView2Environment.CreateAsync(envPath);
await webBrowser.EnsureCoreWebView2Async(environment);
WebView.Source = "http://localhost:5200/MyPage"
WebBrowser.CoreWebView2.DOMContentLoaded += async (s, args) => {
await WebView.ExecuteAsync("alert('Document has loaded.')");
}
Always specify a WebView Environment Folder
Setting up an environment is optional but highly recommended! You'll want to make sure you explicitly specify a WebView Environment folder preferably in a Windows temp location that is application specific.
By default the environment folder is created in a folder below the application's startup root. In installed apps that folder is often non-writable and that will cause application failures due to permissions that you likely won't catch until installed users start complaining of issues. More on this in a minute.
This code is a bit verbose and not easy to remember off the top of your head. Additionally the flow is async and it routes into a single Event handler for DomContentLoaded()
- if you have many documents and operations that occur you're going to have to differentiate between them as part of the event handler code (ie. if arg.Url.Contains()
checks or similar).
It's not exactly straight forward especially for simple requests.
Making this easier with WebView2Handler.WaitForDocumentLoaded()
The Westwind.WebView component is a behavior class that attaches to a WebView control and provides a number of useful enhancements that are aimed at making using the control easier.
It has many useful features:
- Smart automatic (but optional) WebView Environment creation
- Shared Environment for multiple controls in same application
- JavaScript Interop component
- Easy JS function invocation with parameter encoding
- Easy access to variables and properties
- Various different Navigation modes including forced Refresh
- Simple helper
WaitForDocumentLoaded()
- Checking for Compatible WebView Runtime installs
WaitForDocumentLoaded()
The pertinent feature for this post is WaitForDocumentLoaded()
which lets you wait for the document to be ready to be accessed so you can interact with it via script code. As the name suggests this is an async method that waits for the document to be ready and it has an optional timeout that you can specify to abort if for some reason the page fails to load.
The other advantage is that you async linear code to write your logic. Navigate → Wait → Process that makes it much easier to create multiple operations in a linear code manner, as opposed to the having a single event that has to figure out its own page routing.
To use this, first import the Westwind.WebView package into your project:
dotnet add package Westwind.WebView
Then to use it you add a property to your form or control that hosts the WebView control and pass in the WebView control:
// Window or Control level Property
public WebViewHandler WebViewHandler {get; set; }
// Window/Control ctor - do this early on so the WebView is read ASAP
WebViewHandler = new WebViewHandler(WebView);
Then when you're ready to navigate - in your Load()
handler or a button click:
// Note: Handler Method has to be async!
private async void EmojiWindow_Loaded(object sender, RoutedEventArgs e)
{
// here or immediately after instantiation of WVH
WebViewHandler.Navigate("http://localhost:5200/MyPage");
if(!await WebViewHandler.WaitForDocumentLoaded(5000))
throw new ApplicationException("Webpage failed to load in time...");
await WebBrowser.ExecuteScriptAsync("alert('You have arrived')");
}
Note that you can also use the built-in JavaScript Interop helper that allows to call methods on a specified 'base' object - window
by default. So instead of:
await WebBrowser.ExecuteScriptAsync("alert('You have arrived')");
you could use:
await WebViewHandler.JsInterop.Invoke("alert","You have arrived");
The JS Interop object automatically encodes parameters and their types so that they are safe to call Javascript code without having to manually encode strings. You can also subclass the BaseJavaScriptInterop
class and add custom methods that wrap any JS calls that you make for easier isolation of the the Interop code.
There are lots of use cases for the WebViewHandler and you can find out more on the Github page.
How does it work?
I'll show how WaitForDocumentLoadeded
works so you don't have to use the WebViewHandler, but there are a few moving parts and they are already integrated into the WebViewHandler class, so it's an an easy way to use this functionality.
The core method that on WebViewHandler
looks like this (full source on GitHub):
/// <summary>
/// Checks to see if the document has been loaded
/// </summary>
/// <param name="msTimeout">timeout to allow content to load in</param>
/// <returns>true if loaded in time, false if it fails to load in time</returns>
public async Task<bool> WaitForDocumentLoaded(int msTimeout = 5000){
if (IsLoaded)
return true;
if (IsLoadedTaskCompletionSource == null)
IsLoadedTaskCompletionSource = new TaskCompletionSource();
var task = IsLoadedTaskCompletionSource.Task;
if (task == null)
return false;
if (task.IsCompleted)
return true;
var timeoutTask = Task.Delay(msTimeout);
var completedTask = await Task.WhenAny(task, timeoutTask);
return completedTask == task;
}
This code uses a Task Completion source to wait on completion status which occurs when the document loaded event has fired (below). In order to track the timeout code also waits on a Task.Delay()
to check for a timeout. Whenever one of those tasks completes the first task is returned - if the task is the original load check task then the control is loaded and the method returns false. If it's the Delay task, then the request timed out and the method returns false.
As you can see there is an external dependency on a IsLoadedTaskCompletionSource
that is set as part of the control load sequence that the WebViewHandler
intercepts. The handler intercepts the CoreWebView2.DOMContentLoaded
event that was shown by the original manual event syntax.
Here's what these bits look like:
// Event hookup in OnInitAsync()
WebBrowser.CoreWebView2.DOMContentLoaded += OnDomContentLoaded;
// Task Completion source to track async completion
public TaskCompletionSource IsLoadedTaskCompletionSource { get; set; } = new TaskCompletionSource();
// Handler
protected virtual async void OnDomContentLoaded(object sender, CoreWebView2DOMContentLoadedEventArgs e)
{
IsLoaded = true; // unrelated - flag that indicates load completion
if (IsLoadedTaskCompletionSource?.Task is { IsCompleted: false })
IsLoadedTaskCompletionSource?.SetResult(); // complete
}
When this event fires the IsLoadedTaskCompletionSource
completion is set to 'done' if it is set, which then triggers the WaitForDocumentLoaded()
to complete.
The WebView can be used to navigate many times, so there's one more bit that's needed which is resetting the completion status on a new navigation by resetting IsCompleted
and the IsLoadedTaskCompletionSource
:
protected virtual void OnNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
{
IsLoaded = false;
IsLoadedTaskCompletionSource = new TaskCompletionSource();
}
This allows you to use this functionality multiple times. In keeping with the bad example from earlier 😁.
// here or immediately after instantiation of WVH
WebViewHandler.Navigate("http://localhost:5200/MyPage");
if(!await WebViewHandler.WaitForDocumentLoaded(5000))
throw new ApplicationException("Webpage 1 failed to load in time...");
await WebBrowser.ExecuteScriptAsync("alert('You have arrived')");
WebViewHandler.Navigate("http://localhost:5200/MyPage2");
if(!await WebViewHandler.WaitForDocumentLoaded(5000))
throw new ApplicationException("Webpage 2 failed to load in time...");
await WebBrowser.ExecuteScriptAsync("alert('You have arrived again')");
Summary
This is an example of a feature that comes as a benefit of some tooling that's already in place in the WebViewHandler. Obviously it's possible to use the original syntax that's shown in this post using the DOMContentLoaded
event with event handling, but in a few scenarios - especially very simple ones - it's often much easier to write linear await
rather than nested tree of doome event handler chains, especially if you need to run multiple operations - script typically - one after the other.
If you're using the WebView control in WPF, the WebViewHandler
control is a good choice to help with a number of small support tasks.
Resources
Other Posts you might also like