将 WebView2 DLL 嵌入到 wpf 可移植可执行文件中

Embedding WebView2 DLL into wpf portable executable

我正在尝试将 WebView2 DLL 嵌入到 C# 项目中。我添加了 3 个 DLL:

Microsoft.Web.WebView2.Wpf.dll
Microsoft.Web.WebView2.Core.dll
WebView2Loader.dll

作为我项目的嵌入式资源。 因为它是一个 WPF 项目,所以我不需要 Microsoft.Web.WebView2.Forms.dll.

我已经Newtonsoft.Json.dll正确合并了。

在调试模式下,没有问题,因为所有的DLL都被复制到输出目录。

因为我希望可执行文件是可移植的,所以我只需要一个文件(我知道用 exe 复制 DLL 可以,但我需要一个文件)。 当我将主要可执行文件仅移动到另一个文件夹时,应用程序在我使用 webview2 控件时崩溃(该控件在启动时未加载)。

我试图弄清楚是否涉及所有 DLL。 Microsoft.Web.WebView2.Wpf.dll 已正确嵌入,目标文件夹中不需要它。

不过,最后两个还是需要的。我认为这可能是因为它们是由 Microsoft.Web.WebView2.Wpf.dll 而不是主要组件调用的。

如何从主程序集中加载最后两个文件以拥有一个执行文件?

编辑:所有 DLL 都作为嵌入式资源添加到项目中

编辑2

感谢@mm8 给我一些想法(欢迎任何完整解决方案的建议)实现这个。

因此,可以将 3 WebView2 DLL 添加到您的项目中,方法是添加它们并将它们设置为嵌入资源。 Microsoft.Web.WebView2.Wpf.dll(或Microsoft.Web.WebView2.WinForms.dll,取决于你的选择)可以直接加载到内存中。但是最后两个(Microsoft.Web.WebView2.Core.dllWebView2Loader.dll)需要保存为文件:

public MainWindow()
{
   // For Newtonsoft.Json.dll, Microsoft.Web.WebView2.Wpf.dll, Microsoft.Web.WebView2.Core.dll and WebView2Loader.dll
   // Incorporation as embedded resource
   // To avoid having multiple files, only one executable
   AppDomain.CurrentDomain.AssemblyResolve += GetEmbeddedDll;
   ExtractWebDLLFromAssembly();

   InitializeComponent();
}

private byte[] GetAssemblyBytes(string assemblyResource)
{
    using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(assemblyResource))
    {
        return new BinaryReader(stream).ReadBytes((int)stream.Length);
    }
}

// For Newtonsoft.Json.dll and Microsoft.Web.WebView2.Wpf.dll
private Assembly GetEmbeddedDll(object sender, ResolveEventArgs args)
{
    string keyName = new AssemblyName(args.Name).Name;

    // If DLL is loaded then don't load it again just return
    if (_libs.ContainsKey(keyName))
    {
        return _libs[keyName];
    }

    Assembly assembly = Assembly.Load(GetAssemblyBytes(Assembly.GetExecutingAssembly().GetName().Name + "." + keyName + ".dll"));
    _libs[keyName] = assembly;
    return assembly;
}

// For Microsoft.Web.WebView2.Core.dll and WebView2Loader.dll
// Incorporation as embedded resource but need to be extracted
private void ExtractWebDLLFromAssembly()
{
    string[] dllNames = new string[] { "Microsoft.Web.WebView2.Core.dll", "WebView2Loader.dll" };
    string thisAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
    foreach (string dllName in dllNames)
    {
        string dllFullPath = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, dllName);
        try
        {
            File.WriteAllBytes(dllFullPath, GetAssemblyBytes(thisAssemblyName + "." + dllName));
        }
        catch
        {
            // Depending on what you want to do, continue or exit
        }
    }
}

ExtractWebDLLFromAssembly方法基于this post @nawfal的回答

由于ExtractWebDLLFromAssembly方法只调用一次,其代码也可以直接集成在MainWindow.

还缺少一件事:

当我尝试在 MainWindow_OnClosing 事件中删除这些文件时遇到问题,说文件正在被另一个进程(实际上是主进程)使用,即使控件未加载或被处置。 我正在使用此代码来确保 CoreWebView2 已在控件内卸载:

int maxTimeout = 2000;
uint wvProcessId = wv_ctrl.CoreWebView2.BrowserProcessId;
string userDataFolder = wv_ctrl.CoreWebView2.Environment.UserDataFolder;
int timeout = 10;
wv_ctrl.Dispose();
try
{
    while (System.Diagnostics.Process.GetProcessById(Convert.ToInt32(wvProcessId)) != null && timeout < maxTimeout)
    {
        System.Threading.Thread.Sleep(10);
        timeout += 10;
    }
}
catch { }

此代码允许我正确删除 UserDataFolder,但 DLL 仍在使用中。

进一步检查后,我知道 Microsoft.Web.WebView2.Core.dll 仍然作为程序集加载到 CurrentAppDomain 中,这显然是一个问题。
并且WebView2Loader.dll被加载为进程的一个模块。 如果我尝试用

强制卸载它
 [DllImport("kernel32.dll", SetLastError = true)]
 public static extern void FreeLibrary(IntPtr module)

这是行不通的。我认为这是因为 Microsoft.Web.WebView2.Core.dll 使用它。而这应该是留给我的根本问题。

编辑 3

我试图在另一个线程中加载 WebView 控件。我不需要从控件中检索值。我只需要在后面的代码上设置源代码。 我尝试了几件事都没有成功,线程控件的源未更新(控件 Loaded 事件后调用的 EnsureCoreWebView2Async 方法无限期等待)。 实现这一点的主要思想是从 here

中得到启发

编辑 4

好的,我管理一个文件:)。 WebView2Loader.dll 可以用 FreeLibrary 卸载,与我在编辑 2 中所说的相反,我试图在 Dispose() 方法之后卸载它,而您需要等待处理完成。所以你需要在catch

之后调用它
int maxTimeout = 2000;
uint wvProcessId = wv_ctrl.CoreWebView2.BrowserProcessId;
string userDataFolder = wv_ctrl.CoreWebView2.Environment.UserDataFolder;
int timeout = 10;
wv_ctrl.Dispose();
try
{
    while (System.Diagnostics.Process.GetProcessById(Convert.ToInt32(wvProcessId)) != null && timeout < maxTimeout)
    {
        System.Threading.Thread.Sleep(10);
        timeout += 10;
    }
}
catch { }
UnloadModule("WebView2Loader.dll");
if (Settings.Default.DeleteBrowserNavigationData)
{
    try
    {
        Directory.Delete(userDataFolder, true);
    }
    catch { }
}
// Delete WebView2 DLLs (2nd line throws an error)
try
{
    File.Delete(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "WebView2Loader.dll"));
    File.Delete(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Microsoft.Web.WebView2.Core.dll"));
 }
 catch { }

UnloadModule方法取自this, @SI4's answer

最后一个 dll,Microsoft.Web.WebView2.Core.dllCurrentAppDomain 中打开,无法以这种方式卸载也无法删除。我可能应该将它加载到一个新的 AppDomain 中,以便我可以卸载它。但是,我不知道如何通过更改控件的 Source 属性 来实现这一点。可能整个代码应该加载到一个新的 AppDomain ?

编辑 5

我发现了一个可能是解决方案的新选项:WPF Add-In 我从示例中创建了文件,用户控件包含 WebView2 而不是按钮。但是,对于 AddInAdapterHostAdapterFrameworkElementAdapters 都标有错误“该名称在当前上下文中不存在”??? (参考资料已添加到以 .Net 4.6.2 为目标的项目中)。任何线索? 但是,应用程序似乎需要有不同的文件夹。如果是这样的话,这是一个错误的转弯。我还需要一个可执行文件,仅此而已。

谢谢

看起来您已经解决了问题的嵌入部分,剩下的只是删除提取的 DLL。

因为DLL是inuse但是你自己的进程,除非你能成功卸载DLL,否则我认为你将无法删除DLL。

第一个潜在选项

您可以尝试做的一件事是安排在重新启动后删除文件。 SO post 解释了如何使用 P/Invoke 和 MoveFileEx

他们举的例子如下:

if (!NativeMethods.MoveFileEx("a.txt", null, MoveFileFlags.DelayUntilReboot))
{
    Console.Error.WriteLine("Unable to schedule 'a.txt' for deletion");
}

第二个潜在选项

如果这对您不起作用(可能需要管理员权限等),作为 work-around,您可以做的一件事是不要将 DLL 直接提取到应用程序文件夹,而是使用特殊环境变量 Environment.SpecialFolder.LocalApplicationDataSystem.IO.Path.GetTempPath() 将您的临时文件解压到那里。

  • GetTempPath() 解析为:C:\Users\UserName\AppData\Local\Temp\
  • LocalApplicationData 解析为:C:\Users\UserName\AppData\Local

将这些文件放到临时文件夹中,就表示可以删除这些文件了。在 Windows 10 中,用户可以选择在一定天数后删除临时文件,或者通过手动 运行 磁盘清理工具。

因此您可以创建一个小实用程序 class,如下所示:

public static class Utils
{
    public static void CreateTempFolder()
    {
        try
        {
            string localAppData = System.IO.Path.GetTempPath()
            string userFilePath = Path.Combine(localAppData, "AppNameTempFiles");

            if (!Directory.Exists(userFilePath))
                Directory.CreateDirectory(userFilePath);
        }
        catch (Exception ex)
        {
        }
    }

    public static string GetTempFolder()
    {
        string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        string userFilePath = Path.Combine(localAppData, "AppNameTempFiles");

        return userFilePath;
    }

    public static void DelateAllFilesInTempFolder()
    {
        try
        {
            string[] filePaths = Directory.GetFiles(GetTempFolder());
            foreach (string filePath in filePaths)
                System.IO.File.Delete(filePath);
        }
        catch (Exception ex)
        {
        }
    }
}

然后在启动时您可以创建临时文件夹并作为完整性测试删除其中的所有文件,然后将您的文件解压缩到该文件夹​​。

private void MainForm_Load(object sender, EventArgs e)
{
   Utils.CreateTempFolder();
   Utils.DelateAllFilesInTempFolder();
   ExtractWebDLLFromAssembly(Utils.GetTempFolder())
}

并修改您的 ExtractWebDLLFromAssembly 获取一个位置

private void ExtractWebDLLFromAssembly(string fileLocation)
{
    string[] dllNames = new string[] { "Microsoft.Web.WebView2.Core.dll", "WebView2Loader.dll" };
    string thisAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
    foreach (string dllName in dllNames)
    {
        string dllFullPath = Path.Combine(fileLocation, dllName);
        try
        {
            File.WriteAllBytes(dllFullPath, GetAssemblyBytes(thisAssemblyName + "." + dllName));
        }
        catch
        {
            // Depending on what you want to do, continue or exit
        }
    }
}

如上所述,这是一个潜在的 work-around,如果您不能成功删除 DLL,因为它正在使用中。

第三个潜在选项

我想我可能为您提供了一种替代方法,它不需要您在 .exe 中手动嵌入以下 DLL

  • Microsoft.Web.WebView2.Wpf.dll
  • Microsoft.Web.WebView2.Core.dll

相反,如果您在项目中包含 nuget library Costura.Fody,此包将收集所有 .NET 管理的 DLL 并自动将它们包含在您的 .exe 中(类似于新的 . NET 5/6 允许您在构建时执行)。

它的工作原理是,嵌入式 DLL 被加载到内存中,因此实际上并未写入磁盘。这意味着您在应用程序退出之前尝试删除 DLL Microsoft.Web.WebView2.Core.dll 时遇到的问题可能是 side-stepped.

但是 WebView2Loader.dll 仍然存在一个小问题。因为这是本机 DLL 而不是托管 DLL,看起来您仍然需要手动将其嵌入到您的应用程序中,并在启动时提取它并在退出时删除它。

它可能是您的一个潜在选择。

如果您的应用程序不需要针对尚未集成此功能的特定框架,您可以选择使用自 .NET Core 3.0 以来可用的Single File Application

我尝试过使用 WebView2 的 WPF 应用程序,使用这种方法您可以在同一个 .exe 文件中发布应用程序和引用。

由于 WebView2 使用本机库,您只需在 .csproj 文件的 PropertyGroup 中添加以下行。:

<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>