Working with locally built web page in CefSharp

2019-02-03 11:50发布

问题:

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);
        }
    }
}

回答1:

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.



回答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



回答3:

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()
});


回答4:

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");