Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

WebView2: Waiting for Document Loaded


:P
On this page:

Is It Ready Yet

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

Posted in WebView  Windows  WPF  


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2025