6

I am trying to embed WebView2 DLL in a C# project. I have added the 3 DLLs :

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

as embedded resource to my project. Since it is a WPF project I have no need of Microsoft.Web.WebView2.Forms.dll.

I have already Newtonsoft.Json.dll correctly incorporated.

In debug mode, there is no problem because all DLL are copied to the output directory.

As I want that the executable to be portable, I want only a single file (I know that copying the DLL with the exe will work, but I need a single file). When I move the main executable only to another folder, the application crashes when I use the webview2 control (the control is not loaded at start).

I have tried to figure out if all the DLL were involved. Microsoft.Web.WebView2.Wpf.dll is correctly embedded and I have no need of it in the destination folder.

However, the last two ones are still required. I think this is probably because they are called by Microsoft.Web.WebView2.Wpf.dll and not by the main assemby.

How can I load the last two ones from the main assembly to have a single execution file ?

EDIT: All DLL are added to the project as embedded resource

EDIT2

Thanks to @mm8 to give me ideas to partialy (any suggestions for complete solution are welcomed) achieve this.

So, It is possible to embed the 3 WebView2 DLL into you project by adding them and setting them as embedded resource. Microsoft.Web.WebView2.Wpf.dll (or Microsoft.Web.WebView2.WinForms.dll, depending on your choice) can be loaded directly into memory. But the last two ones (Microsoft.Web.WebView2.Core.dll and WebView2Loader.dll) need to be saved as files :

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

The ExtractWebDLLFromAssembly method is based from this post @nawfal's answer

As the ExtractWebDLLFromAssembly method is called only once, its code can also be directly integrated in MainWindow.

One thing is still missing :

I am facing issues when I try to delete these files in the MainWindow_OnClosing event saying that files are in use by another process (in fact, the main process), even if the control is not loaded or is disposed. I am using this code to be sure that CoreWebView2 was unloaded inside the control :

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

This code allows me to correctly delete the UserDataFolder, but the DLL are still in use.

After further inspection, I know that Microsoft.Web.WebView2.Core.dll is still loaded as an assembly in the CurrentAppDomain which is obviously a problem.
And WebView2Loader.dll is loaded as a module of the Process. If I try to force to unload it with

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

this does not work. I think this is because Microsoft.Web.WebView2.Core.dll use it. And this should be the root problem left for me.

EDIT 3

I have tried to load the WebView control in an other thread. I don't need to retrieve values from the control. I only need to set the source on code behind. I have tried several things without any success, the source of the threaded control is not updated (the EnsureCoreWebView2Async method called after the control Loaded event stays awaited indefinitly). The main idea to achieve this is inspired from here

EDIT 4

Ok, I manage one file :). WebView2Loader.dll can be unloaded with FreeLibrary contrary that what I said in edit 2, I was trying to unload it after the Dispose() method, while you need to wait the dispose is completed. So you need to call it after the catch block

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

The UnloadModule method is taken from this, @SI4's answer

The last dll, Microsoft.Web.WebView2.Core.dll is opened within the CurrentAppDomain and cannot unloaded this way nor be deleted. I should probably load it within a new AppDomain so that I can unload it. However, I don't know how to achieve this with the ability to change the Source property of the control. May be the whole code should be loaded in a new AppDomain ?

EDIT 5

I have found a new option that could be the solution : WPF Add-In I have created the files from the example, the user control contains WebView2 instead of a button. However, for both AddInAdapter and HostAdapter, FrameworkElementAdapters is marked with an error "The name does not exist in the current context" ??? (References has been added to the project which is targetting .Net 4.6.2). Any clue ? But, it seems that application needs to have different folders. If this is the case, this is a wrong turn. I still need one executable and nothing more.

Thanks

CFou
  • 978
  • 3
  • 13
  • How do you create the main executable that you copy to the target machine? – mm8 Feb 17 '22 at 13:06
  • I simply build the solution (Ctrl-Shift-B) in Visual Studio – CFou Feb 17 '22 at 13:09
  • Then the assemblies are not embedded into the `.exe`. This is not a valid deployment. – mm8 Feb 17 '22 at 13:10
  • They are because the exe has not the same size, furthermore, this works for Newtonsoft.Json.dll and Microsoft.Web.WebView2.Wpf.dll. I have no need of these ones. My problem is for Microsoft.Web.WebView2.Core.dll and WebView2Loader.dll (see my edit) – CFou Feb 17 '22 at 13:13
  • How do you know it "works" unless you don't use these?` – mm8 Feb 17 '22 at 13:17
  • Oh, just saw your edit, you're adding the assemblies as embedded resources. This is not going to work unless you extract them at runtime. – mm8 Feb 17 '22 at 13:18
  • how can I achieve this ? – CFou Feb 17 '22 at 13:24
  • Are you targeting .NET Core or the .NET Framework? – mm8 Feb 17 '22 at 13:26
  • .NET Framework. – CFou Feb 17 '22 at 13:28
  • There is no official support to merge assemblies into a single-file executable in .NET Framework. You should ship your application as a folder that contains all required dependencies or upgrade to .NET Core and try the new single-file publish option. – mm8 Feb 17 '22 at 13:30
  • Hi @CFou, I'm also in same situation, want to generate portable exe with webview. i was able to copy and refer the dlls from shared location but webview control is not loading when i tried to set webview control source from code-behind. Not sure what i'm missing. In XAML- In code behind - testBrowser.Source = new Uri("https://google.com"); – Mani Sandeep May 16 '23 at 18:57
  • @Mani, use `testBrowser.CoreWebView2.Navigate("https://yoururl.test");` – CFou May 20 '23 at 08:23

2 Answers2

3

If your application does not need to target a particular framework which does not yet integrate this functionality, you have the option of using the Single File Application available since .NET Core 3.0.

I have tried with a WPF Application using a WebView2, and with this method you can publish the application and the references in the same .exe file.

Since WebView2 works with native libraries, you just need to add the following line in your PropertyGroup of your .csproj file.:

<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
Léo SALVADOR
  • 754
  • 2
  • 3
  • 17
  • 1
    Unfortunally, at the moment I can't use .Net Core. But thanks, I keep that for an upgrade that will let me use .Net Core – CFou Feb 26 '22 at 09:41
2

Looks like you have resolved the embedding part of your question and are left with just the deletion of the extracted DLLs.

Because the DLL is inuse but your own process, unless you are able to uload the DLL successfully, I dont think you will be able to delete the DLL.

1st Potential Option

One thing you could try and do is schedule the deletion of the file after reboot. This SO post explains how to do that using P/Invoke and MoveFileEx.

The example they give is as follows:

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

2nd Potential Option

If that does not work for you (it might require admin privledges, etc), as a work-around, one thing you could do, is instead of extracting the DLLs directly to the applications folder, instead use the special environment variable Environment.SpecialFolder.LocalApplicationData or System.IO.Path.GetTempPath() to extract your temporary files there.

  • GetTempPath() resolves to: C:\Users\UserName\AppData\Local\Temp\
  • LocalApplicationData resolves to: C:\Users\UserName\AppData\Local

By putting these files into the temp folder, its a signal that these files can be deleted. In Windows 10 a user can choose to delete temp files after a certain number of days or by manually running the Disk Cleanup tool.

So you could create a little Utils class like below:

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

Then on start up you could create the temp folder and as a sanity test delete all the files in there, then Extract your files to that folder.

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

and modify your ExtractWebDLLFromAssembly taking in a location

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

As mentioned above this is a potential work-around, if you cant succesffuly delete the DLL because its in use.

3rd Potential Option

I think I potentially have an alternative approach for you that does not require you to manually embed the following DLL's within your .exe

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

Instead, if you include the nuget library Costura.Fody in your project, this package will collect all the .NET managed DLLs and automatically include them in you .exe (similar to what the new .NET 5/6 allows you to do) at build time.

How it works, is that the embedded DLLs are loaded into memory so are not actually written to disk. That means the problems you are having trying to delete the DLL Microsoft.Web.WebView2.Core.dll before the app exits could be side-stepped.

There is still a slight issue with WebView2Loader.dll however. Because this is a native DLL and not a managed DLL, it looks like you will still need to manually embed this in your app, and extract it upon start and delete it on exit.

It could be a potential option for you.

Ocean Airdrop
  • 2,793
  • 1
  • 26
  • 31
  • Your solution could be a work around, however these DLL have to be extracted in the folder where the main app resides... else the app crashes when using the webview2 control – CFou Feb 21 '22 at 12:51
  • Ahh, thats a bit rubbish then. I have just found this SO post to set a custom path for referenced DLLs but not sure if it will help you. https://stackoverflow.com/questions/1892492/set-custom-path-to-referenced-dlls – Ocean Airdrop Feb 21 '22 at 12:59
  • Unfortunally no – CFou Feb 21 '22 at 16:42
  • Hi @CFou. I have just updated my answer with a 3rd potential option for you! – Ocean Airdrop Feb 24 '22 at 08:19
  • Hi, thanks for your update. Unfortunally, I have already tried it. App crashes when using the control. Weirdly, `Microsoft.Web.WebView2.Core.dll` need to be on disk, while `WebView2Loader.dll` no. See my 4th Edit, I managed the last one without `Costura.Fody`. – CFou Feb 24 '22 at 16:06
  • Ahh Great stuff. Nice to hear you got it sorted. – Ocean Airdrop Feb 24 '22 at 18:03
  • Not completly yet ;) – CFou Feb 25 '22 at 07:56