🍥
Blazor Hybrid (WPF) で MudBlazor を使う
はじめに
この記事は、以下を下敷きにしています。
MudBlazorについても、そちらをご参照ください。
実証コードが以下にあります。
Blazor Hybrid 'WPF'?
この記事で扱うxamlは、メインウィンドウにWebView2を配置する部分だけです。
WebView2を動かすプラットフォームとしてWPFを使っているだけで、中身はBlazorです。
決して、WPFの記事ではございません。
Blazor Hybrid 'MAUI'?
Blazor Hybrid (WPF)がWindows専用なのに対して、マルチプラットフォーム対応の Blazor Hybrid (MAUI)というのもありますが、WebView2のプラットフォームがMAUIになるだけですね。
こちらは、テンプレート(.NET MAUI Blazor Hybrid)を使って比較的容易に構成できます。
Blazor Hybrid (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.WpfとMudBlazorを導入します。- あるいは、次の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用WebView2とMudBlazorを導入します。
フォルダの構成
- プロジェクトのルートに
wwwrootフォルダを作ります。- css、js、faviconなどの構成はBlazor Web Appと同じです。
- 移植元が
Serverの場合、_Imports.razor、Routes.razor、および、Layout、Pagesを含むその他のrazorファイルは、Componentsフォルダ以下に配置されているものと想定します。- 対して、移植元が
ClientやStandalone (WASM)の場合、_Imports.razorやPagesはプロジェクトルートに配置されているものと思います。
- 対して、移植元が
- この記事では、主に
Serverを想定して、ディレクトリの構造とファイルの内容をできるだけ変えずに移植することを目指します。
Components の配置
- 移植元の
Componentsフォルダの中身をプロジェクトのルートに展開して、_Imports.razor、Routes.razor、Layout、Pagesなどをルートに配置するのが一般的なようです。 - この記事では、展開せずに
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.razorをwwwrootに移動して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.htmlのid="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