Use Local Files in CefSharp

Applies to: CefSharp, C#

CefSharp is an open source project which provides Embedded Chromium for .NET (WPF & WinForms). It’s a great way to get Chrome hosted inside of your .NET application. Recently I had the need of loading web files directly from the file system. The easiest way to do that is to provide a custom scheme. You do this by creating an instance of CefSettings and using the RegisterScheme method with a CefCustomScheme wrapper object which requires an implementation of the ISchemeHandlerFactory which will in turn require an implementation of the ISchemeHandler. Got it?! It’s actually much less confusing than it seems. It will make much more sense in a moment.

Quick Note: This isn’t a tutorial on how to get CefSharp working or integrated into your product. You’ll want to install the packages using NuGet and go to the CefSharp project page for details.

ISchemeHandler

SchemeHandler objects process custom scheme-based requests asynchronously. In other words, you can provide a URL in the form customscheme://folder/yourfile.html”. The SchemeHandler object takes the request and gives you a chance to provide a custom response. In our case, we want to take anything with the scheme local and pull it from the file system. Here’s what that looks like:

using CefSharp;
using System.IO;
public class LocalSchemeHandler : ISchemeHandler
{
    public bool ProcessRequestAsync(IRequest request, ISchemeHandlerResponse response, OnRequestCompletedHandler requestCompletedCallback)
    {
        Uri u = new Uri(request.Url);
        String file = u.Authority + u.AbsolutePath;

        if (File.Exists(file))
        {
            Byte[] bytes = File.ReadAllBytes(file);
            response.ResponseStream = new MemoryStream(bytes);
            switch (Path.GetExtension(file))
            {
                case ".html":
                    response.MimeType = "text/html";
                    break;
                case ".js":
                    response.MimeType = "text/javascript";
                    break;
                case ".png":
                    response.MimeType = "image/png";
                    break;
                case ".appcache":
                case ".manifest":
                    response.MimeType = "text/cache-manifest";
                    break;
                default:
                    response.MimeType = "application/octet-stream";
                    break;
            }
            requestCompletedCallback();
            return true;
        }
        return false;
    }
}

I’ve named my implementation LocalSchemeHandler because I’m creative like that. You can see we are implementing the ISchemeHandler interface in line 3 (If this isn’t recognized be sure to include the using statements in lines 1 and 2 above). This interface only requires a single method, ProcessRequestAsync. Our job is to translate the request into a response stream, call requestCompletedCallback and indicate if we handled the request or not (return true or false).

The first thing we do is translate the request URL into a local file path (lines 7-8). If the file doesn’t exist, there isn’t any way for us to handle the request so we return false (line 36). Otherwise, we set the response.ResponseStream to a MemoryStream from the file’s bytes (lines 12-13). We then guess the Mime Type based on the file’s extension (lines 14-32). This is a pretty limited list but it was all I needed – Just expand as necessary.

Once we’ve got everything we need, we call the requestCompletedCallback and return true to indicate we have handled the request.

ISchemeHandlerFactory

SchemeHandlerFactory objects create the appropriate SchemeHandler objects. I’ve also added a convenience property to help out during registration. Here’s mine:

using CefSharp;
public class LocalSchemeHandlerFactory : ISchemeHandlerFactory
{
    public ISchemeHandler Create()
    {
        return new LocalSchemeHandler();
    }

    public static string SchemeName { get { return "local"; } }
}

This one is called LocalSchemeHandlerFactory (surprise!) and you can see we are implementing the ISchemeHandlerFactory interface in line 2 (If this isn’t recognized be sure to include the using statement in line 1). This interface also only requires a single method, Create. All we have to do is return a new instance of our custom SchemeHandler (line 6).

The SchemeName property in line 9 is just there to make things easier during registration.

Registering Your Custom Scheme

Now that we have our custom SchemeHandler and the corresponding Factory, how do we get the Chromium web browser controls to take advantage of them? This has to be done before things are initialized (before the controls are loaded). In WPF this can usually be done in your ViewModel’s constructor and in WinForms within the Form Load event. Here’s what it looks like:

CefSettings settings = new CefSettings();
settings.RegisterScheme(new CefCustomScheme()
    {
        SchemeName = LocalSchemeHandlerFactory.SchemeName,
        SchemeHandlerFactory = new LocalSchemeHandlerFactory()
    });
Cef.Initialize(settings);

You’ll create a new instance of a CefSettings object (line 1) and call the RegisterScheme method which takes a CefCustomScheme object (lines 2-6). the CefCustomScheme object needs to have its SchemeName set to whatever you are going to be using in the URLs to distinguish your custom scheme (we are using local) and its SchemeHandlerFactory should be set to a new instance of your custom SchemeHandlerFactory. Then call the Cef.Intialize with the settings object and all the plumbing is hooked up.

Giving it a go

In my application’s directory (bin/x64/debug) I have created a folder called web. Inside the folder is an html file (index.html) and an images folder with a single image (truck.png). The html page is very simple:

<!DOCTYPE html>
<html>
<body>
<h1>My First Heading</h1>
<p>My first paragraph.</p>
<img src="images/truck.png"/>
</body>
</html>

Now if I set my ChromiumWebBrowser (WPF) or WebView (WinForms) Address property to local://web/index.html I get the following:

LocalSchemeHandler

You can see the web page loaded both the HTML and the linked image from the file system. There’s even a couple of dummy buttons underneath the truck to show you that it’s just a standard WPF window. This is all you need for simple web files stored locally. In some advanced cases you will need to adjust the BrowserSettings for your control to adjust permission levels (specifically FileAccessFromFileUrlsAllowed and UniversalAccessFromFileUrlsAllowed).