WPF(.NET Core)のWebView上に、publishしたClient side Blazorを表示するDesktop Appを実験的に作って挫折したのでその備忘録です。
※挫折した、という点がポイントです。
Demo
最低限動くところまでは作ったので、その様子だけ載せておきます。
きっかけ
先日MicrosoftのSteve Sanderson氏が氏のBlogにWebWindow.Blazorなるlibraryの記事を執筆されました。
これはElectronやPWAを使わずにWebアプリをHybrid Desktop App化する話で
って感じの実験的Libraryです。
中身はC++とか使ってるっぽいですね
※詳細は氏のBlogをご参照ください。
この記事を見て、「Windowsだけを対象とすればWPFのWebViewでできるかな~」と何も考えず軽いノリで始めたのがきっかけです。
localのClient side Blazorをブラウザで開く
そもそもClient side Blaozrってlocalのブラウザで開けるのでしょうか。
Client side Blazorをpublishしてindex.htmlをGoogle Chromeで開いてみました。
ファイルのパスがダメっぽいですね。
index.htmlの<base href="/" />
を一時的に削除してブラウザを更新します。
やっぱダメですね。
方針
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
はデフォルトだとMIMEがtext/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を発見しました。
上記issueに記載されたコマンドを管理者権限で実行します。
checknetisolation LoopbackExempt -a -n="Microsoft.Win32WebViewHost_cw5n1h2txyewy"
実行
BlazorOnWebView.WPFでdotnet run
します。
おわり
WebViewはchecknetisolationの件があるので配布は厳しいですね。
詳細は調べていませんが、CefSharpを使えばなんとかなるのかな?
それかChromium版Edgeが動くWebView2を使うとか(こっちはC++ですが。)
WebViewのキャッシュが残るようで、wwwrootの中身を更新してもなかなかWPF上に反映されない点が辛かったです。
というわけで、実用性のないClient side Blaozr on WPF(WebView)ができました。
やっぱりSteve Sanderson氏のようなスーパーエンジニアはすごいなぁ...。