Using CEFSharp to load web pages and interact in WPF

Posted by sciwaysoft on Mon, 01 Nov 2021 11:48:33 +0100

preface

Now commonly used schemes

  • Duilib+CEF It only supports the choice of Windows. The advantage is that the packaged files are small (using C + +) QQ, wechat and Youdao excellent courses.
  • Qt+CEF supports cross platform, but the disadvantage is that the packaged file is large (using C + +).
  • WPF/(WPF+CEFSharp) has small packaged files, but its performance is weaker than the first two, but stronger than Electron. It occupies a high memory and only supports Windows.
  • Electron packaging file is large, but its performance is weak and memory consumption is high. It supports cross platform.

Several schemes have their own advantages and disadvantages. They can be selected according to the situation of the team. They are relatively good. Other schemes such as fluent and Java are not recommended.

At present, due to the technical stack of C + +, our team mainly uses WPF or Electron for desktop development.

Some interfaces are better developed by web, so here we will integrate CEFSharp to load

be careful

Adding CEF will greatly increase the installation package size.

Why use CEF

  • The WebBrowser provided with. NET is the IE most hated by WEB developers, with low performance and poor compatibility
  • Webkit: Project no longer supports
  • Cef is the key to the Chrome kernel, performance and compatibility. The disadvantage is that there are too many and too large DLL s. A release version should be about 150M, and the X86+X64 will be 300M faster. In addition, EXE loading speed will be slightly slower.

Installation dependency

Through Nuget installation, right-click the project - > manage Nuget package - > search CefSharp in the open interface, install CefSharp.Common and CefSharp.Wpf successively, and cef.redist.x64 and cef.redist.x86 will be installed automatically.

Configure solution platform

Because CefSharp does not support Any CPU, to configure x86 and x64, click menu generation - > configuration manager.

Select the solution platform, click Edit, delete x64 and x86, and then create a new one. It is easier to reconfigure.

Any CPU support

If we want to support Any CPU, we have to implement it ourselves.

using System.Windows;
using System;
using System.Runtime.CompilerServices;
using CefSharp;
using System.IO;
using System.Reflection;
using System.Windows.Threading;
using CefSharpWpfDemo.Log;

namespace CEFSharpTest
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public App()
        {
            // Add Custom assembly resolver
            AppDomain.CurrentDomain.AssemblyResolve += Resolver;
            //Any CefSharp references have to be in another method with NonInlining
            // attribute so the assembly rolver has time to do it's thing.
            InitializeCefSharp();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void InitializeCefSharp()
        {
            var settings = new CefSettings();

            // Set BrowserSubProcessPath based on app bitness at runtime
            settings.BrowserSubprocessPath = Path.Combine(
                AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
                Environment.Is64BitProcess ? "x64" : "x86",
                "CefSharp.BrowserSubprocess.exe"
            );

            // Make sure you set performDependencyCheck false
            Cef.Initialize(settings, performDependencyCheck: false, browserProcessHandler: null);
        }

        // Will attempt to load missing assembly from either x86 or x64 subdir
        // Required by CefSharp to load the unmanaged dependencies when running using AnyCPU
        private static Assembly Resolver(object sender, ResolveEventArgs args)
        {
            if (args.Name.StartsWith("CefSharp"))
            {
                string assemblyName = args.Name.Split(new[] { ',' }, 2)[0] + ".dll";
                string archSpecificPath = Path.Combine(
                    AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
                    Environment.Is64BitProcess ? "x64" : "x86",
                    assemblyName
                );

                return File.Exists(archSpecificPath)
                    ? Assembly.LoadFile(archSpecificPath)
                    : null;
            }

            return null;
        }
    }
}

use

When using, you can directly add the chromium WebBrowser control to the xaml file, but the chromium WebBrowser control consumes special memory, so it is also a good choice to add it dynamically in the code.

Add browser in xaml

Insert reference in xmal file header

xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"

Add controls as follows:

<Grid x:Name="ctrlBrowerGrid">
    <wpf:ChromiumWebBrowser x:Name="Browser"/>
</Grid>

To access the web address of the operation control in the cs file:

Browser.Load("https://www.psvmc.cn");

Code add browser

Add browser class:

using CefSharp.Wpf;

using System.ComponentModel;
using System.Windows;

namespace CEFSharpTest.view
{
    internal sealed class CollapsableChromiumWebBrowser : ChromiumWebBrowser
    {
        public CollapsableChromiumWebBrowser()
        {
            Loaded += BrowserLoaded;
        }

        private void BrowserLoaded(object sender, RoutedEventArgs e)
        {
            // Avoid loading CEF in designer
            if (DesignerProperties.GetIsInDesignMode(this)) {
                return;
            }
            // Avoid NRE in AbstractRenderHandler.OnPaint
            ApplyTemplate();
        }
    }
}

Dynamically add and manipulate controls:

private CollapsableChromiumWebBrowser MyBrowser = null;
private void InitWebBrower() {
    MyBrowser = new CollapsableChromiumWebBrowser();
    //Page insert control
    ctrlBrowerGrid.Children.Add(MyBrowser);
    //The Load() method cannot be used here, and an error will be reported.
    MyBrowser.Address = "https://www.psvmc.cn";
}

Get cookies and Html

Add Cookie access class

using CefSharp;

using System;

namespace CEFSharpTest.view
{
    public class CookieVisitor : ICookieVisitor
    {
        private string Cookies = null;

        public event Action<object> Action;

        public bool Visit(Cookie cookie, int count, int total, ref bool deleteCookie)
        {
            if (count == 0)
                Cookies = null;

            Cookies += cookie.Name + "=" + cookie.Value + ";";
            deleteCookie = false;
            return true;
        }

        public void Dispose()
        {
            if (Action != null)
                Action(Cookies);
            return;
        }
    }
}

The browser control accesses the web address and sets the callback

private CollapsableChromiumWebBrowser MyBrowser = null;

private void InitWebBrower()
{
    MyBrowser = new CollapsableChromiumWebBrowser();
    //Page insert control
    ctrlBrowerGrid.Children.Add(MyBrowser);
    MyBrowser.FrameLoadEnd += Browser_FrameLoadEnd;
    //The Load() method cannot be used here, and an error will be reported.
    MyBrowser.Address = "https://www.psvmc.cn";
}

private async void Browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e)
{
    CookieVisitor visitor = new CookieVisitor();
    string html = await MyBrowser.GetSourceAsync();
    Console.WriteLine("html:" + html);
    visitor.Action += RecieveCookie;
    Cef.GetGlobalCookieManager().VisitAllCookies(visitor);
    return;
}

public async void RecieveCookie(object data)
{
    string cookies = (string)data;
    Console.WriteLine("cookies:" + cookies);
    return;
}

Load local pages and JS callbacks

Add HTML

Add HTML path under item html\index.html

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8" />
    <script type="text/javascript">
        function callback() {
            callbackObj.showMessage('message from js');
        }

        function alert_msg(msg) {
            alert(msg);
        }
    </script>
</head>
<body>
    <button onclick="callback()">Click</button>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        body {
            background-color: #f3f3f3;
            width: 100vw;
            height: 100vh;
            display:flex;
            align-items:center;
            justify-content:center;
        }
    </style>
</body>
</html>

Copy page to destination directory

Mode 1

Project - > properties - > generate event - > pre build event command line

Add as follows

xcopy /Y /i /e $(ProjectDir)\html $(TargetDir)\html

Mode 2

Right click the properties of the file to set the copy to output directory and generate operation.

If there are many documents, mode 1 is recommended.

code

Register a JS object

private ChromiumWebBrowser MyBrowser = null;

private void InitWebBrower()
{
    CefSettings cSettings = new CefSettings()
    {
        Locale = "zh-CN",
        CachePath = Directory.GetCurrentDirectory() + @"\Cache"
    };
    cSettings.MultiThreadedMessageLoop = true;
    cSettings.CefCommandLineArgs.Add("proxy-auto-detect", "0");
    cSettings.CefCommandLineArgs.Add("--disable-web-security", "");
    //Disable GPU acceleration
    cSettings.CefCommandLineArgs.Add("disable-gpu");
    //Disable GPU vsync
    cSettings.CefCommandLineArgs.Add("disable-gpu-vsync");
    //This configuration allows the camera to turn on the camera
    cSettings.CefCommandLineArgs.Add("enable-media-stream", "1");
    Cef.Initialize(cSettings);

    string pagepath = string.Format(@"{0}html\index.html", AppDomain.CurrentDomain.BaseDirectory);

    if (!File.Exists(pagepath))
    {
        MessageBox.Show("HTML non-existent: " + pagepath);
        return;
    }

    // Create a browser component
    MyBrowser = new ChromiumWebBrowser();

    //Disable right-click menu
    MyBrowser.MenuHandler = new MenuHandler();

    //Disable pop ups
    MyBrowser.LifeSpanHandler = new LifeSpanHandler();

    MyBrowser.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#f3f3f3"));
    //Page insert control
    ctrlBrowerGrid.Children.Add(MyBrowser);

    MyBrowser.Address = pagepath;

    MyBrowser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true;
    MyBrowser.JavascriptObjectRepository.Register(
        "callbackObj", 
        new CallbackObjectForJs(),
        isAsync: true, 
        options: BindingOptions.DefaultBinder
    );
}

Call JS method

private void Button_Click(object sender, RoutedEventArgs e)
{
    MyBrowser.ExecuteScriptAsync("alert_msg('123')");
}

Event callback class

public class CallbackObjectForJs
{
    public void showMessage(string msg)
    {
        MessageBox.Show(msg);
    }
}

Disable classes for right-click menus

public class MenuHandler : IContextMenuHandler
{
    public void OnBeforeContextMenu(
        IWebBrowser browserControl, 
        IBrowser browser, 
        IFrame frame, 
        IContextMenuParams parameters, 
        IMenuModel model
    )
    {
        model.Clear();
    }

    public bool OnContextMenuCommand(
        IWebBrowser browserControl, 
        IBrowser browser, 
        IFrame frame, 
        IContextMenuParams parameters, 
        CefMenuCommand commandId, 
        CefEventFlags eventFlags
    )
    {
        return false;
    }

    public void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame)
    {
    }

    public bool RunContextMenu(
        IWebBrowser browserControl,
        IBrowser browser, 
        IFrame frame, 
        IContextMenuParams parameters, 
        IMenuModel model, 
        IRunContextMenuCallback callback
    )
    {
        return false;
    }
}

Open linked class from original window

public class LifeSpanHandler : ILifeSpanHandler
{
    //Event triggered before pop-up
    public bool OnBeforePopup(
        IWebBrowser webBrowser, 
        IBrowser browser, 
        IFrame frame,
        string targetUrl,
        string targetFrameName, 
        WindowOpenDisposition targetDisposition, 
        bool userGesture, 
        IPopupFeatures popupFeatures,
        IWindowInfo windowInfo, 
        IBrowserSettings browserSettings, 
        ref bool noJavascriptAccess, 
        out IWebBrowser newBrowser)
    {
        //Use the source window to open the link and cancel the creation of a new window
        newBrowser = null;
        var chromiumWebBrowser = (ChromiumWebBrowser)webBrowser;
        chromiumWebBrowser.Load(targetUrl);
        return true;
    }

    public void OnAfterCreated(IWebBrowser chromiumWebBrowser, IBrowser browser)
    {
    }

    public bool DoClose(IWebBrowser chromiumWebBrowser, IBrowser browser)
    {
        return true;
    }

    public void OnBeforeClose(IWebBrowser chromiumWebBrowser, IBrowser browser)
    {
    }
}

Attention items

API change

//Old Method
MyBrowser.RegisterAsyncJsObject("callbackObj", new CallbackObjectForJs(), options: BindingOptions.DefaultBinder);

//Replaced with
MyBrowser.JavascriptObjectRepository.Settings.LegacyBindingEnabled = true;
MyBrowser.JavascriptObjectRepository.Register("callbackObj", new CallbackObjectForJs(), isAsync: true, options: BindingOptions.DefaultBinder);

Local file path

The file path cannot contain special characters, otherwise it cannot be loaded. Before, my project was in the C# directory and the page could not be loaded.