【问题标题】:Embedding WebView2 DLL into wpf portable executable将 WebView2 DLL 嵌入到 wpf 可移植可执行文件中
【发布时间】:2022-02-26 01:39:00
【问题描述】:

我正在尝试将 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 的项目中)。有什么线索吗? 但是,似乎应用程序需要有不同的文件夹。如果是这样的话,这是一个错误的转折。我仍然需要一个可执行文件,仅此而已。

谢谢

【问题讨论】:

  • 如何创建复制到目标机器的主要可执行文件?
  • 我只是在 Visual Studio 中构建解决方案(Ctrl-Shift-B)
  • 那么程序集不会嵌入到.exe 中。这不是有效的部署。
  • 因为exe的大小不一样,另外,这适用于Newtonsoft.Json.dll和Microsoft.Web.WebView2.Wpf.dll。我不需要这些。我的问题是 Microsoft.Web.WebView2.Core.dll 和 WebView2Loader.dll (见我的编辑)
  • 除非你不使用这些,否则你怎么知道它“有效”?`

标签: c# wpf


【解决方案1】:

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

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

由于 WebView2 与原生库一起使用,您只需在 .csproj 文件的 PropertyGroup 中添加以下行。:

<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>

【讨论】:

  • 很遗憾,目前我无法使用 .Net Core。但是谢谢,我保留它以进行升级,让我可以使用 .Net Core
【解决方案2】:

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

因为DLL是inuse但是你自己的进程,除非你能成功uload 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");
}

第二个潜在选择

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

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

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

所以你可以像下面这样创建一个小的 Utils 类:

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

如上所述,这是一个潜在的解决方法,如果您无法成功删除 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 时遇到的问题可以回避。

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

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

【讨论】:

  • 您的解决方案可以解决,但是这些 DLL 必须在主应用程序所在的文件夹中提取...否则应用程序在使用 webview2 控件时崩溃
  • 啊,那有点垃圾。我刚刚发现这篇 SO 帖子为引用的 DLL 设置自定义路径,但不确定它是否会对您有所帮助。 stackoverflow.com/questions/1892492/…
  • 很遗憾没有
  • 嗨@CFou。我刚刚更新了我的答案,为您提供了第三种可能的选择!
  • 您好,感谢您的更新。不幸的是,我已经尝试过了。使用控件时应用程序崩溃。奇怪的是,Microsoft.Web.WebView2.Core.dll 需要在磁盘上,而WebView2Loader.dll 不需要。请参阅我的第四次编辑,我在没有Costura.Fody 的情况下管理了最后一个。
猜你喜欢
  • 1970-01-01
  • 2014-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-19
相关资源
最近更新 更多