🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

Client side Blazor on WPF WebViewを実験的に作って挫折した話

WPF(.NET Core)のWebView上に、publishしたClient side Blazorを表示するDesktop Appを実験的に作って挫折したのでその備忘録です。
※挫折した、という点がポイントです。

Demo

最低限動くところまでは作ったので、その様子だけ載せておきます。

f:id:piyo_esq:20191123113254g:plain
demo

きっかけ

先日MicrosoftのSteve Sanderson氏が氏のBlogにWebWindow.Blazorなるlibraryの記事を執筆されました。

blog.stevensanderson.com

これはElectronやPWAを使わずにWebアプリをHybrid Desktop App化する話で

  • Windows, Mac, Linuxをサポート

  • サイズがめっちゃ小さく済む

  • メモリめっちゃ少なく済む

って感じの実験的Libraryです。
中身はC++とか使ってるっぽいですね ※詳細は氏のBlogをご参照ください。

この記事を見て、「Windowsだけを対象とすればWPFのWebViewでできるかな~」と何も考えず軽いノリで始めたのがきっかけです。

localのClient side Blazorをブラウザで開く

そもそもClient side Blaozrってlocalのブラウザで開けるのでしょうか。

Client side Blazorをpublishしてindex.htmlをGoogle Chromeで開いてみました。

f:id:piyo_esq:20191123110028p:plain

ファイルのパスがダメっぽいですね。
index.htmlの<base href="/" />を一時的に削除してブラウザを更新します。

f:id:piyo_esq:20191123110048p:plain

やっぱダメですね。

方針

Client side Blazorをlocalのブラウザで開くにはちゃんとサーバ建てないとダメっぽいですね。
仕方がないのでWPF上でASP.NET Coreをホストするやけくそ構成で行きましょう。
※これ、結局ホストするならClient side Blazorである必要がないですよね。しかもWPF上でホストするって。

プロジェクトの作成

とりあえず作っていきます。
※環境は.NET Core 3.1 Preview3です

dotnet new sln -o BlazorOnWebView
cd BlazorOnWebView
dotnet new blazorwasm -o BlazorOnWebView.Blazor
dotnet new wpf -o BlazorOnWebView.WPF
dotnet sln BlazorOnWebView.sln add BlazorOnWebView.Blazor
dotnet sln BlazorOnWebView.sln add BlazorOnWebView.WPF

Client side BlaozrのPublish

先にClient side BlazorをPublishしておきます。

dotnet publish -c Release

これでBlazorOnWebView.Blazor\bin\Release\netstandard2.0\publish\BlazorOnWebView.Blazor\dist以下にClient side BlazorがPublishされました。

上記パスのファイルをBlazorOnWebView.WPF\wwwroot\にコピーしておきます。
※イケてない感が満載ですが、.csprojに以下を追加してpublishしても良いです。

  <Target Name="PostPublish" AfterTargets="AfterPublish">
    <Exec Command="xcopy $(TargetDir)publish\BlazorOnWebView.Blazor\dist\* $(ProjectDir)..\BlazorOnWebView.WPF\wwwroot\ /E /I /Y" />
  </Target>

BlazorOnWebView.WPFへLibraryの追加

BlazorOnWebView.WPFに必要なLibraryを追加します。
WebViewはWPF上でEdgeの表示ができるMicrosoft.Toolkit.Wpf.UI.Controls.WebViewを使用します。
※WebViewはWebView2に置き換えられるのでビルド時に警告が発せられますがひとまず無視します。

cd BlazorOnWebView.WPF
dotnet add package Microsoft.AspNetCore
dotnet add package Microsoft.AspNetCore.Hosting
dotnet add package Microsoft.AspNetCore.SpaServices.Extensions
dotnet add package Microsoft.AspNetCore.StaticFiles
dotnet add package Microsoft.Toolkit.Wpf.UI.Controls.WebView

コーディング

準備完了です。
WPF側コードを編集します。

BlazorOnWebView.WPF/BlazorOnWebView.WPF.csprojの編集

ビルド時にClient side BlazorでpublishしたファイルをBlazorOnWebView.WPF/wwwroot/フォルダにコピーするよう.csprojを編集します。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
    <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.0-preview3.19555.2" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
    <PackageReference Include="Microsoft.Toolkit.Wpf.UI.Controls.WebView" Version="6.0.0" />
  </ItemGroup>

+ <ItemGroup>
+   <Content Include="wwwroot\**\*.*">
+     <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+   </Content>
+ </ItemGroup>

</Project>

BlazorOnWebView.WPF/MainWindow.xamlの編集

xamlにWebViewを追加します。
IsPrivateNetworkClientServerCapabilityEnabledを指定することでLocalHostのURLにもアクセスできるようになるそうです。

<Window x:Class="BlazorOnWebView.WPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BlazorOnWebView.WPF"
+        xmlns:Controls="clr-namespace:Microsoft.Toolkit.Wpf.UI.Controls;assembly=Microsoft.Toolkit.Wpf.UI.Controls.WebView"
        mc:Ignorable="d"
+       Closing="Window_Closing"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
+       <Controls:WebView x:Name="WebView" Source="https://localhost:5001/" IsPrivateNetworkClientServerCapabilityEnabled="True" />
    </Grid>
</Window>

BlazorOnWebView.WPF/MainWindow.xaml.cs

コードビハインドにASP.NET CoreでWebサーバをホストする処理を追加します。

本当はちゃんとサーバが起動してからWebViewの遷移を開始するべきですが、割愛します。

using System.IO;
using System.Windows;
+ using Microsoft.AspNetCore;
+ using Microsoft.AspNetCore.Hosting;

namespace BlazorOnWebView.WPF
{
    public partial class MainWindow : Window
    {
+       CancellationTokenSource cts = new CancellationTokenSource();
        public MainWindow()
        {
            InitializeComponent();

+           WebHost.CreateDefaultBuilder()
+                   .UseStartup<Startup>()
+                   .Build().RunAsync(cts.Token);
        }

+       private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+       {
+           cts.Cancel();
+       }
    }
}

BlazorOnWebView.WPF/Startup.cs

Startupクラスを新規作成します。
Webサーバの設定ですね。

Client side Blazorが使用する_framework/_bin/**.dllはデフォルトだとMIMEtext/htmlか何かになってた気がするので application/x-msdownloadに変更します。

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using System.IO;

namespace BlazorOnWebView.WPF
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSpaStaticFiles(opt => opt.RootPath = "wwwroot");
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            var provider = new FileExtensionContentTypeProvider();
            provider.Mappings[".dll"] = @"application/x-msdownload";
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "_framework", "_bin")),
                RequestPath = "/_framework/_bin",
                ContentTypeProvider = provider
            });

            app.UseSpaStaticFiles();
            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "wwwroot";
            });
        }
    }
}

checknetisolationの設定

ここでdotnet runしたものの、画面が真っ白で何も表示されませんでした。
ぐぐってみると、以下のissueを発見しました。

github.com

上記issueに記載されたコマンドを管理者権限で実行します。

checknetisolation LoopbackExempt -a -n="Microsoft.Win32WebViewHost_cw5n1h2txyewy"

f:id:piyo_esq:20191123110133p:plain
checknetisolation

実行

BlazorOnWebView.WPFdotnet runします。

f:id:piyo_esq:20191123113254g:plain
demo

おわり

WebViewはchecknetisolationの件があるので配布は厳しいですね。
詳細は調べていませんが、CefSharpを使えばなんとかなるのかな?
それかChromium版Edgeが動くWebView2を使うとか(こっちはC++ですが。)

WebViewのキャッシュが残るようで、wwwrootの中身を更新してもなかなかWPF上に反映されない点が辛かったです。

というわけで、実用性のないClient side Blaozr on WPF(WebView)ができました。
やっぱりSteve Sanderson氏のようなスーパーエンジニアはすごいなぁ...。