MicrosoftEdge / WebView2Feedback

Feedback and discussions about Microsoft Edge WebView2
https://aka.ms/webview2
452 stars 55 forks source link

WebView2 in a DLL with Blazor doesn't load static assets #1797

Open Waahse opened 3 years ago

Waahse commented 3 years ago

Description This is a follow on from https://github.com/MicrosoftEdge/WebView2Feedback/issues/730. It concerns Blazor Hybrid apps but since the underlying control is webview2 I have raised it here. I have created a boilerplate Blazor server from visual studio, taken the content into a razor control library, wrapped that around in a win forms application and displaying it using Webview2 in WinForms.

So: Razor Class Library + WinForms app = Working Hybrid Blazor App. (app calls razor class library). All good so far.

Now the next thing i need to do is move the webview2 into a class library (as I want to load this library in MS Office VSTO). I created a class library, added a win form and docked the webview2 into the WinForm. I create a separate WinForm project to load the class library.

So now i have: Razor Class Library (same as above) + Class Library (webview2 docked in a WinForm) + Windows App (loads the class library).

The whole thing works i.e. the website loads into the webview2 but there are no static assets (in this case css etc.) so the site works but it devoid of CSS. It seems that putting the WebView2 into a DLL means that some setup needed for the loading of static assets from the Razor Class Library occurs.

When i bring up the webview2 debugging, I can see that it simply cannot find the assets (screenshots attached).

To Summarize WinForms App + Razor Class Library = Working Hybrid app with all assets loaded. WinForms App loading class library with DLL in WinForms + Razor class library = working hybrid app but no assets.

I have created a repository to demonstrate this to make it easier to follow: https://github.com/Waahse/BlazorHybridTest

If you set the startup project as SampleWinformsApp it works perfectly, if you set the startup project as SampleWinformsLibrary Loader it works without assets. Both load the same Razor Class library the only difference is one is via a DLL.

Version Framework: .net 6 preview SDK: WebView2.core version 1.0.705.50, WebView2.Winforms version 1.0.705.50, OS: Windows 10

Screenshots Output showing lack of assets loading SampleWinformsLibraryLoaderOutput

Output showing assets loading (css) SampleWinformsAppOutput

Output from Edge Dev tools, assets loaded. SampleWinformsAppOutputFromEdgeDevTools

Output from Edge dev tools, assets cannot be found. SampleWinformsLibraryLoaderOutputFromEdgeDevTools

RickStrahl commented 3 years ago

Are you sure that 0.0.0.0 is a valid IP Address? 0.0.0.0 is a default route, used in assigning the local IP Address, but I don't think it's properly resolved by a browser.

I wonder if you could log on the local Web Server to see if those requests are actually reaching that server and are getting served and if not try explicit IP Addressing rather than using this virtual address.

Another alternative if the site is truly static only, is to use virtual host names that you can define with CoreWebView2.SetVirtualHostNameFolderMapping...

Waahse commented 3 years ago

Many thanks for the reply. Below is a bit rambling but it may jog a memory or thought. To be honest I am trying to compile up aspnetcore but despite following all the steps in Building aspnet core even on a fresh VM following all the steps to the letter i cannot get the WebView to compile. I could then give a detailed description of the problem.

So I took a look at the SetVirtualHostName... but i think it is down to incorrect location resolving in the BlazorWebView.cs in Microsoft.AspNetCore.Components.WebView.WindowsForms.

I downloaded Microsoft.AspNetCore.Components.WebView.WindowsForms from https://github.com/dotnet/maui and compiled it up. I then took a debug session and in the method StartWebViewCoreIfPossible():

The fileprovider is pointing to the wrong place in the case of the blazorview being in a loaded library. It is giving the location of the application creating the class instance which means that it cannot find any content. It should be looking at the class instance location. Subtle difference between an app running the blazorview and a class running blazorwebview and since the http://0.0.0.0 content is intercepted it cannot find the static assets.

Ultimately the static assets are attempted to load in the WebView2 WebView2WebViewManager class in https://github.com/dotnet/maui calling TryGetResponseContent which fails with a 404 status code and no static content.

Further investigation of this takes me into the aspnetcore WebView source and into the WebViewManager.cs. This in turn calls the staticContentProvider.TryGetResponseContent and this is where we get a 404 error because we have a failure to get the content via TryGetFromFileProvider or TryGetFrameworkFile. The StaticWebAssetsLoader.cs source file attempts to load the assets but fails and the whole things fails to load with a 404.

I am still trying to get a debug compile of the aspnetcore WebView so i can just debug and give far more pertinent information but still to no avail. It is quite frustrating since a simple debug of this would highlight the problem and lead to a quick solution/workaround i think.

Waahse commented 3 years ago

I have built all the aspnetcore source and located the problem and i believe it not so much a bug more an enhancement but a necessary one.

Description The Microsoft.AspNetCore.Components.WebView gets the wrong file provider when the Microsoft.AspNetCore.Components.WebView is hosted in a class library rather than in an application. The fileprovider should be a composite provider but instead is returning a physicalfileprovider.

This has major problems for Blazor Hybrid projects that load into another environment for example I create a self contained dll which is loaded via another app. No static assets would be loaded. This is particularly problematic in the case of Blazor Hybrid apps as they would ever be able to load the static CSS assets (and others).

Problem Area: The problem is in the WebViewManager.cs file of Microsoft.AspNetCore.Components.WebView. Within the constructor is the line: fileProvider = StaticWebAssetsLoader.UseStaticWebAssets(fileProvider);

The StaticWebAssetsLoader class has a method: ResolveRelativeToAssembly and in turn, this uses the command Assembly.GetEntryAssembly() to get the path.

Consider: Windows App loading web view -> Correct fileprovider If the webview is in a windows form in an app, the result of this call returns the full path to the file .staticwebassets.runtime.json. This file exists and the UseStaticWebAssets call correctly returns a composite provider. At this point all the assets load correctly and the hybrid blazor app displays perfectly.

Now consider Windows App loading library which has the webview in the library -> Incorrect fileprovider If the webview is in a library (DLL) which is loaded from somewhere else then we follow the same process: The file .staticwebassets.runtime.json is not found (because the staticwebassets.runtime.json is actually in the library location not the ‘app’ location) and as a result it fails and resorts to returning a physicalfileprovider. This results in no assets being loaded so while the webapp works it has no CSS and other assets.

I have attached a picture of the actual code in WebViewManager.cs This is particularly a problem loading libraries using reflection.

Possible Solution Looking through the source i am not aware of any workaround but I may be wrong. The solution to this I believe is to have a mechanism of passing an override to the place where the static assets are located i.e a static asset root location. This would then go into the StaticWebAssetsLoader.UseStaticWebAssets(fileProvider, assetsLocation);

Once this is done it will find the corresponding assetslocation.staticwebassets.runtime.json and initialize the composite fileprovider and deliver the correct results which in my case is a Blazor Hybrid app with the css loaded.

Implications of not providing Without this, it is not possible to have (for example) a library is a restricted location (e.g. program files) and have it load via reflection a webview library in another location. Since the staticwebassets.runtime.json could never be in the Assembly.GetEntryAssembly path as this would be locked and readonly. This is demonstrable via the project in top of my initial post: https://github.com/Waahse/BlazorHybridTest . Simply link the built source code of ASPNETCORE and put a breakpoint in the StaticWebAssetsLoader.UseStaticWebAssets method and you can see it return the compositefileprovider in a working case versus a physicalfileprovider in the case of a failure.

ProblemWithStaticAssets

MatthewKing commented 10 months ago

Just ran into this issue myself. I'm wondering if anyone has come up with a workaround or a fix in the two years since @Waahse raised this issue? If not, I will try to dig deeper myself, but wanted to check here first. Thanks.

belucha commented 10 months ago

I've created an extra Fileprovider type and added this file provider during startup.

blazorWebView.FileProviders = new IFileProvider[] {
                new SubDirFileProvider(new PhysicalFileProvider(@"c:/temp/sampleassets/"), "assets"),
                new SubDirFileProvider(new PhysicalFileProvider(@"c:/otherfolder/images/static"), "images/user"),
            };

The Subdirfileprovider is just a proxy I wrote to remount an existing fileprovider

public class SubDirFileProvider : IFileProvider, IAsyncDisposable
{
    readonly IFileProvider _fileProvider;
    readonly string _subDir;
    static readonly NullFileProvider _null = new();
    readonly StringComparison _stringComparison;
    public SubDirFileProvider(IFileProvider fileProvider, string subDir, StringComparison stringComparison = StringComparison.OrdinalIgnoreCase)
    {
        _fileProvider = fileProvider;
        _subDir = subDir;
        _stringComparison = stringComparison;
    }

    public IDirectoryContents GetDirectoryContents(string subpath)
    {
        if (subpath.StartsWith(_subDir, _stringComparison))
        {
            subpath = subpath.Substring(_subDir.Length);
            return _fileProvider.GetDirectoryContents(subpath);
        }
        return _null.GetDirectoryContents(subpath);
    }

    public IFileInfo GetFileInfo(string subpath)
    {
        if (subpath.StartsWith(_subDir, _stringComparison))
        {
            subpath = subpath.Substring(_subDir.Length);
            return _fileProvider.GetFileInfo(subpath);
        }
        return _null.GetFileInfo(subpath);
    }

    public IChangeToken Watch(string filter)
    {
        return _fileProvider.Watch(filter);
    }

    public ValueTask DisposeAsync()
    {
        return _fileProvider is IAsyncDisposable asyncDisposable ? asyncDisposable.DisposeAsync() : ValueTask.CompletedTask;
    }
}

Hope it helps