Working with locally built web page in CefSharp
Asked Answered
A

4

16

I have a CefSharp browser created in my Winform and I need to dynamically build an HTML page in memory and then have CefSharp render it.

Ideally I would like to pass the constructor a string with the HTML in it but it is expecting a URL. The answer is probably no, but is there a directive you can prepend the string with to let CefSharp know it is a string that contains a web page? Then CefSharp will create a temp file?

If not, where is the Chromium temp folder set to? Will it work if I write a file to there and then pass that as a fully qualified path? I know Chrome will support something like file:///Users/dmacdonald/Documents/myFile.htm as a URL but not sure how to form a URL if using the temp structure.

Here is my new code but my browser object doesn't have a ResourceHandler property. I see it has a ResourceHandlerFactory

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using CefSharp.WinForms;
using CefSharp;


namespace DanCefWinForm
{
    public partial class Form1 : Form
    {
        public const string TestResourceUrl = "http://maps/resource/load";

        public Form1()
        {
            InitializeComponent();


        }

        private void Form1_Load(object sender, EventArgs e)
        {
            ChromiumWebBrowser browser = new ChromiumWebBrowser("http://maps/resource/load")
            {
                Dock = DockStyle.Fill,
            };

            var handler = browser.ResourceHandler;

           browser.Location = new Point(20, 20);
           browser.Size = new Size(100, 100);
            this.Controls.Add(browser);
        }
    }
}
Austriahungary answered 24/2, 2015 at 13:53 Comment(1)
You can use a resource factory. Here's the code: https://mcmap.net/q/729850/-working-with-locally-built-web-page-in-cefsharp based on Michael's answer.Handtohand
F
14

The Simple Approach (one "file", one page)

LoadString() can be used to load directly from a string:

ChromiumWebBrowser.LoadString(string html, string url);

Alternatively, LoadHtml() can load from a string in a given encoding:

ChromiumWebBrowser.LoadHtml(string html, string url, Encoding encoding);

I tried both, and they both seem to work, at least with CefSharp.Wpf v51.0.0. According to WebBrowserExtensions.cs, LoadHtml() uses RegisterHandler() to register a ResourceHandler. It is not clear to me how LoadString() works, but both functions seem to have the same effect.

Be sure to use a valid URL format for the fake URL, such as:

https://myfakeurl.com

The Complex Approach (multiple "files", such as doc + images)

  1. Create a class deriving from IResourceHandlerFactory. Using VS2015, mousing over the red-underlined name should give the option of Implement interface. This auto-complete option vastly simplifies creation of the class, so be sure to use it.

  2. Similar to in step 1, create a class deriving from IResourceHandler. Be sure to use the Implement interface auto-complete option if you can.

  3. In the class created in step 1 (derived from IResourceHandlerFactory), there is a function called GetResourceHandler(). Within this function, return a new instance of your derived class from step 2 (based on IResourceHandler). Using new here is essential since the Web browser may request multiple files simultaneously. Each IResourceHandler instance should handle one request from the browser (no worries, this is done for you).

  4. As mentioned by OP, the browser control has a member called ResourceHandlerFactory. Set this member equal to a new instance of your class you created in step 1 (deriving from IResourceHandlerFactory). This is what links the Chromium Web Browser control to your interface classes. In step 3 you linked both your classes, so we have a full chain.

  5. Within the class from step 2, there is a function called ProcessRequest(). This is the first function called when a request is made by a Web page. Your goal here is to record the requested URL and any POST data, then decide whether to allow the request, calling either callback.Continue() or callback.Cancel(). Return true to continue.

  6. Again in the class from step 2, there is a function called GetResponseHeaders(). This is the second function called. Your goal here is to check the URL, possibly fetching file data from wherever you store it (but not yet sending it), determine the response length (file or string size), and set an appropriate status code within the response object. Be sure to set all these variables so the request can proceed correctly.

  7. Your final step, again in the class from step 2, is to complete the request within the third called function: ReadResponse(). Within this function, write your data fetched in step 6 to the dataOut stream. If your data exceeds about 32kB, you may need to send it in multiple chunks. Be absolutely sure to limit the amount you write in a given call to the length of the dataOut stream. Set bytesRead to whatever you wrote in this particular call. On the last call, when no more data remains, simply set bytesRead to zero and return false. Because you may be called upon multiple times for a given file, be sure to track your current read location so you know where you are and how much data has been sent.

For those unfamiliar with the matter, you can store data files directly compiled into your EXE by adding them to your project and setting their "Build Action" to "Embedded Resource", followed by loading their data programmatically using System.Reflection.Assembly.GetManifestResourceStream(). Using the above methods, there is no need to create or read any files from disk.

Flammable answered 18/9, 2016 at 17:22 Comment(6)
Greatly appreciated Michael. This makes life much easier as i tried the ResourceHandler and had it working in a standalone project but when I put it in my real project something went awry.Austriahungary
@user461051: Ironically, I ended up not using these functions in my project since I needed to be able to serve several different pieces of content (HTML, Gif, JavaScript, CSS, and more). I did not want to create temp files, so I used IResourceHandlerFactory, IResourceHandler, GetResourceHandler(), ProcessRequest(), GetResponseHeaders(), and ReadResponse() to allow "serving" content directly from memory. Nevertheless, for a simple page, LoadHtml() seems ideal.Flammable
I have updated my answer to include this more advanced approach.Flammable
A model implementation that also includes many mime types: github.com/cefsharp/CefSharp/blob/master/CefSharp/…Monotony
Are there restrictions on the fake url or html content submitted in the easy method? If I submit htmlcontent and a url to a page hosted on IIS, I get the IIS page matching the URL. I turn off IIS, and an empty page loads despite htmlContent not being empty. In both cases it ignores the htmlcontent string I submit.Acreage
LoadString has been removed, see github.com/cefsharp/CefSharp/issues/2955Firewarden
K
5

You probably need to use custom scheme handler, in order to serve local files, and "bypass" chromium security regarding file protocol.

I wrote blog post on this matter.

What you want to add is your scheme handler and its factory:

using System;
using System.IO;
using CefSharp;

namespace MyProject.CustomProtocol
{
    public class CustomProtocolSchemeHandler : ResourceHandler
    {
        // Specifies where you bundled app resides.
        // Basically path to your index.html
        private string frontendFolderPath;

        public CustomProtocolSchemeHandler()
        {
            frontendFolderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "./bundle/");
        }

        // Process request and craft response.
        public override bool ProcessRequestAsync(IRequest request, ICallback callback)
        {
            var uri = new Uri(request.Url);
            var fileName = uri.AbsolutePath;

            var requestedFilePath = frontendFolderPath + fileName;

            if (File.Exists(requestedFilePath))
            {
                byte[] bytes = File.ReadAllBytes(requestedFilePath);
                Stream = new MemoryStream(bytes);

                var fileExtension = Path.GetExtension(fileName);
                MimeType = GetMimeType(fileExtension);

                callback.Continue();
                return true;
            }

            callback.Dispose();
            return false;
        }
    }

    public class CustomProtocolSchemeHandlerFactory : ISchemeHandlerFactory
    {
        public const string SchemeName = "customFileProtocol";

        public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
        {
            return new CustomProtocolSchemeHandler();
        }
    }
}

And then register it before calling Cef.Initialize:

var settings = new CefSettings
{
  BrowserSubprocessPath = GetCefExecutablePath()
};

settings.RegisterScheme(new CefCustomScheme
{
  SchemeName = CustomProtocolSchemeHandlerFactory.SchemeName,
  SchemeHandlerFactory = new CustomProtocolSchemeHandlerFactory()
});
Kippy answered 24/4, 2018 at 20:14 Comment(0)
M
2

See https://github.com/cefsharp/CefSharp/blob/v39.0.0-pre02/CefSharp.Example/CefExample.cs#L44 for an example of registering a ResourceHandler for an in-memory string.

As you can see, it still has an URL (web resources generally tend to have that) but it can be a dummy one of your choice.

Here's the GitHub search for how it's called in the WinForms (and WPF) example apps: https://github.com/cefsharp/CefSharp/search?utf8=%E2%9C%93&q=RegisterTestResources

Another, probably less favourable, option with a temp file (anywhere?) in the local file system is to use FileAccessFromFileUrlsAllowed

Update from the comments below:

What CefSharp version are you on now? Note if you look at github.com/cefsharp/CefSharp/releases and search for resource you see the API changed in version 49 (look under breaking changes for that version) - see comments below for furtther gotcha's

Magazine answered 24/2, 2015 at 16:6 Comment(7)
I had seen in the CefExamples the URL "custom://CefSharp/bindingTest.html and thought that may be a lead but there was nothing in the code nor searches that explained what custom: does. So if you set up a ResourceHandler it automatically intercepts all URL requests that the browser makes and automatically sends back the IO stream you specify? in the example code they have a string that says the page was loaded from System.IO.StreamAustriahungary
When I say intercepts all URL requests, I meant for the URL you specify in your request handler. in the Cef example it is test/resource/load. so if that URL is entered into the Chromium the special handler is invoked and the string specified in the handler is loaded by Chromium.Austriahungary
Jornh, hoping you can help me again. I finally got back to this project and am trying to create a custom URL processor from the article you posted but my browser object doesn't have the ResourceHandler method. I put my code as an edit in my original question.Austriahungary
What CefSharp version are you on now? Note if you look at github.com/cefsharp/CefSharp/releases and search for resource you see the API changed in version 49 (look under breaking changes for that version)Magazine
Thanks. That would be the issue as I grabbed the latest with my git install and this thread was from a while ago. Need to read the new docs.Austriahungary
Jornh I created classes for ResourceHandlerFactory and ResourceHandler but I put break points in my HandlerFactory and the HasHandlers property is never referenced nor is GetResourceHandler so I must be missing something else to configure this. Given I am working with v51 now should I create a new Question for this so as to not pollute this question? I can post my code there. Thanks again for all the help.Austriahungary
I spent the night doing lots more experimenting and figured it out. Thanks again for all your help. One of the issues that threw me for a loop is the creation of the factory handler is commented out of CefExample. //browser.ResourceHandlerFactory = new FlashResourceHandlerFactory(); So when I was looking for references in Visual Studio it didn't find any.Austriahungary
H
1

Here is an example of a custom factory that loads resources from the file system:

public class FileResourceHandlerFactory : ISchemeHandlerFactory {
    private string scheme, host, folder, default_filename;

    public string Scheme => scheme;

    public FileResourceHandlerFactory(string scheme, string host, string folder, string default_filename = "index.html") {
        this.scheme = scheme;
        this.host = host;
        this.folder = folder;
        this.default_filename = default_filename;
    }

    private string get_content(Uri uri, out string extension) {
        var path = uri.LocalPath.Substring(1);
        path = string.IsNullOrWhiteSpace(path) ? this.default_filename : path;
        extension = Path.GetExtension(path);
        return File.ReadAllText(Path.Combine(this.folder, path));
    }

    IResourceHandler ISchemeHandlerFactory.Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
        var uri = new Uri(request.Url);
        return ResourceHandler.FromString(get_content(uri, out var extension), extension);
    }
}

And here is how you would apply it:

var settings = new CefSettings();
settings.RegisterScheme(new CefCustomScheme {
    SchemeName = "app",
    SchemeHandlerFactory = fileResourceHandlerFactory,
    IsSecure = true //treated with the same security rules as those applied to "https" URLs
});
var chromeBrowser = new ChromiumWebBrowser();
chromeBrowser.Load("app://local");
Handtohand answered 14/12, 2017 at 3:19 Comment(1)
I tried ur approach, but I am simply getting a black screen. Any ideas why JS files are not loading. I have also asked a question regarding it here : #52039564Lydgate

© 2022 - 2024 — McMap. All rights reserved.