🍥

Blazor Hybrid (WPF) で MudBlazor を使う

に公開

はじめに

この記事は、以下を下敷きにしています。

https://zenn.dev/tetr4lab/articles/74bd50585434ab

MudBlazorについても、そちらをご参照ください。

実証コードが以下にあります。

https://github.com/tetr4lab/NovelsStandAlone

Blazor Hybrid 'WPF'?

この記事で扱うxamlは、メインウィンドウにWebView2を配置する部分だけです。
WebView2を動かすプラットフォームとしてWPFを使っているだけで、中身はBlazorです。
決して、WPFの記事ではございません。

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/hybrid/tutorials/wpf

Blazor Hybrid 'MAUI'?

Blazor Hybrid (WPF)​がWindows専用なのに対して、マルチプラットフォーム対応の Blazor Hybrid (MAUI)​というのもありますが、WebView2のプラットフォームがMAUIになるだけですね。
こちらは、テンプレート(.NET MAUI Blazor Hybrid)を使って比較的容易に構成できます。
Blazor Hybrid (MAUI)​は、この記事では扱いません。

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/hybrid/tutorials/maui

やりたいこと

MudBlazorを使用したBlazor Web App​を、Blazor Hybrid (WPF)​に移植して、Windowsスタンドアロンアプリにします。

環境

  • .NET 8.0
  • Microsoft.AspNetCore.Components.WebView.Wpf 8.0.100
  • MudBlazor 8.9.0
  • Visual Studio Community 2022 17.14.7

プロジェクトの構成

  • 新規プロジェクトから、「WPF アプリケーション」テンプレートを選択して生成します。
  • このテンプレートを元に、Blazor Hybrid (WPF)​を構成していきます。

NuGet パッケージの導入

  • パッケージマネージャコンソール、または、管理タブで、NuGetパッケージMicrosoft.AspNetCore.Components.WebView.WpfMudBlazorを導入します。
    • あるいは、次のcsprojの編集で導入します。

プロジェクトファイルの編集

  • ソリューションエクスプローラから「プロジェクトファイルの編集」を選んでcsprojを更新します。
    • 以降、«Project»は、プロジェクト名に置き換えてください。
«Project».csproj
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk.Razor">
 
   <PropertyGroup>
     <OutputType>WinExe</OutputType>
     <TargetFramework>net8.0-windows</TargetFramework>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
     <UseWPF>true</UseWPF>
+    <RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Wpf" Version="8.0.100" />
+    <PackageReference Include="MudBlazor" Version="8.9.0" />
+  </ItemGroup>
+
 </Project>
  • SDKがRazorに変わります。
  • WPF用WebView2MudBlazorを導入します。

フォルダの構成

  • プロジェクトのルートにwwwrootフォルダを作ります。
    • css、js、faviconなどの構成はBlazor Web App​と同じです。
  • 移植元がServerの場合、_Imports.razorRoutes.razor、および、LayoutPagesを含むその他のrazorファイルは、Componentsフォルダ以下に配置されているものと想定します。
    • 対して、移植元がClientStandalone (WASM)の場合、_Imports.razorPagesはプロジェクトルートに配置されているものと思います。
  • この記事では、主にServerを想定して、ディレクトリの構造とファイルの内容をできるだけ変えずに移植することを目指します。

Components の配置

  • 移植元のComponentsフォルダの中身をプロジェクトのルートに展開して、_Imports.razorRoutes.razorLayoutPagesなどをルートに配置するのが一般的なようです。
  • この記事では、展開せずにComponentsフォルダのまま配置する場合を(も)想定します。
    • この場合、_Imports.razorには、以下のような記述が含まれるものと思います。
      _Imports.razor
      @using «Project»
      @using «Project».Components
      @using «Project».Components.Layout
      @using «Project».Components.Pages
      
      • あるいは、ディレクトリ構造に依存しない名前空間を構成している場合は、適切な定義と使用宣言が含まれていると思います。

MudBlazor を使う Blazor Hybrid (WPF) の構成

App.razor の代替

  • App.razorに書いていた基底のHTMLは、代わりにwwwroot/index.htmlに記述します。
    • App.razorwwwrootに移動してindex.htmlに名前を変えます。
    • wwwroot/index.htmlの名前は、後述するMainWindow.xamlで定義されています。
  • 以下は、「MudBlazorを使うBlazor Web App​の構成」との比較です。
wwwroot/index.html
 <!DOCTYPE html>
 <html lang="ja">
 
 <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <base href="/" />
     <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
     <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
     <link href="app.css" rel="stylesheet" />
     <link href="«Project».styles.css" rel="stylesheet" />
     <link href="favicon.png" rel="icon" type="image/png" />
-    <HeadOutlet />
 </head>
 
 <body>
-    <Routes @rendermode="@RenderMode.InteractiveServer" />
-    <script src="_framework/blazor.web.js"></script>
+    <div id="app">Loading...</div>
+    <script src="_framework/blazor.webview.js"></script>
     <script src="_content/MudBlazor/MudBlazor.min.js"></script>
 </body>
  • フレームワークを、webからwebviewに変更します。
  • ルート指定を、<Routesから<div id="app"に変更します。
    • この<div id="app">Loading...</div>ChildContent(Loading...)が、後述するMainWindow.xamlの記述に従って、Route.razorの内容に置き換わります。
    • この置き換えは、後述するMainWindow.xamlで定義されています。

Route の構成

  • Route.razorは基本的に同じですが、型Programが未定義になります。
  • 以下は、「MudBlazorを使うBlazor Web App​の構成」との比較です。
Route.razor
-<Router AppAssembly="@(typeof (Program).Assembly)">
+<Router AppAssembly="@(typeof (Routes).Assembly)">
     <Found Context="routeData">
         <RouteView RouteData="@routeData" DefaultLayout="@(typeof (MainLayout))" />
         <FocusOnNavigate RouteData="@routeData" Selector="h1" />
     </Found>
     <NotFound>
         <LayoutView Layout="@(typeof (MainLayout))">
             <p>Sorry, there's nothing at this address.</p>
         </LayoutView>
     </NotFound>
 </Router>
  • Program.csは作らないので、その代わりに、自身(Routes.razor)の型からアセンブリ名を取得します。

WebView2 の配置

  • MainWindow.xamlでは、WebView2を使ってRoutes.razorを描画した結果を、WPFのアプリウィンドウに配置しています。
    • MainWindow.xamlの名前はApp.xamlで定義されていますが、この記事では扱いません。
  • 以降は、テンプレートとの差分に戻ります。
MainWindow.xaml
 <Window x:Class="«Project».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:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
+        xmlns:components="clr-namespace:«Project».Components"
         xmlns:local="clr-namespace:«Project»"
         mc:Ignorable="d"
-        Title="MainWindow" Height="450" Width="800">
+        Title="MainWindow" Height="1380" Width="1200" WindowStartupLocation="Manual" Left="2600" Top="0">
     <Grid>
+        <blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">
+            <blazor:BlazorWebView.RootComponents>
+                <blazor:RootComponent Selector="#app" ComponentType="{x:Type components:Routes}" />
+            </blazor:BlazorWebView.RootComponents>
+        </blazor:BlazorWebView>
     </Grid>
 </Window>

  • グリッドの定義(<Grid>~</Grid>)によって、wwwroot/index.htmlid="app"要素の中身がRoutesクラスのインスタンスで置き換えられ、WebView2で描画されます。
  • xmlns:components="clr-namespace:«Project».Components"によって、名前空間«Project».Componentsを指すcomponentsというエイリアスが作られます。
    • そのため、ルートコンポーネントの型を示すcomponents:Routesは、«Project».Components.Routesという意味になります。
    • 対して、Componentsフォルダの中身をプロジェクトのルートに展開した場合(および、元々Componentsフォルダがない場合)は、単にlocal:Routesと指定すれば済みます。
  • Height="1380" Width="1200" WindowStartupLocation="Manual" Left="2600" Top="0"辺りは、個人的な設定なので、流用の際は画面外にならないようにご留意ください。
  • このファイルで「servicesが解決できない」とか、「Routesが名前空間にない」だとかで、エラーする原因が、他の.razor.csファイルでのエラーという場合があります。
    • .xamlは、一度ビルドが通らないと解決できないとかなんとか…っぽいです。

サービスの登録

  • Program.csに書いていたDIコンテナへの各種サービスの登録は、代わりにMainWinodow.xaml.csに記述します。
MainWinodow.xaml.cs
+using Microsoft.Extensions.DependencyInjection;
 using System.Text;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Navigation;
 using System.Windows.Shapes;
+using MudBlazor;
+using MudBlazor.Services;
 
 namespace «Project»;

 /// <summary>
 /// Interaction logic for MainWindow.xaml
 /// </summary>
 public partial class MainWindow : Window {
     public MainWindow () {
         InitializeComponent ();
+        var serviceCollection = new ServiceCollection ();
+        serviceCollection.AddWpfBlazorWebView ();
+        serviceCollection.AddBlazorWebViewDeveloperTools ();
+        serviceCollection.AddMudServices (config => {
+            config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
+            config.SnackbarConfiguration.PreventDuplicates = false;
+            config.SnackbarConfiguration.NewestOnTop = false;
+            config.SnackbarConfiguration.ShowCloseIcon = true;
+            config.SnackbarConfiguration.VisibleStateDuration = 10000;
+            config.SnackbarConfiguration.HideTransitionDuration = 500;
+            config.SnackbarConfiguration.ShowTransitionDuration = 500;
+            config.SnackbarConfiguration.SnackbarVariant = Variant.Filled;
+        });
+        Resources.Add ("services", serviceCollection.BuildServiceProvider ());
     }
 }
  • ここでは、WPFに必要なものに加えて、MudBlazorのサービス(とMudSnackBarの設定)を登録しています。
    • 動的リソースに格納(Resources.Add ("services", ~);)されたコレクションは、MainWindow.xaml側で、Services="{DynamicResource services}としてWebView2に引き渡されています。

おわりに

最後までお読みいただきありがとうございました。
執筆者は、諸々において初学者ですので、誤りもあるかと思います。
お気づきの点があればコメントいただけると助かります。

Discussion