💻
WinUI3もMVVMで実装する(画面遷移, DI)
はじめに
前回の記事では、CommunityToolkit.Mvvm
を使用してWinUI3をMVVM実装した。
MAUIと比較して、画面遷移やDI部分が気になったので試してみる。
環境
Windows 11
Visual Studio 2022
.NET 8.0
Microsoft.WindowsAppSDK 1.7.250606001
CommunityToolkit.Mvvm 8.4.0
Microsoft.Extensions.Hosting 9.0.8 → DI用に今回導入
Microsoft.Xaml.Behaviors.WinUI 3.0.0 → イベントにコマンドをバインドするため今回導入
FrameとNavigationViewで画面遷移
今回はNavigationView
のサイドメニューで選択されたページへの遷移を実装する。
- Frame ... ページを動的に切り替えるためのコンテナ
- NavigationView ... サイドメニュー
ページ作成
- Pagesフォルダを作成
- 新規ページを作成
MainPage.xaml
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="WinUiApp.Pages.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUiApp.Pages"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid HorizontalAlignment="Center">
<TextBlock Text="メインページ" FontSize="20" />
</Grid>
</Page>
もう一つ新規ページを作成し、前回実装したMainWindowのxamlとViewModelを移動。
SecondPage.xaml
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="WinUiApp.Pages.SecondPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUiApp.Pages"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox x:Name="NameInput" Width="200" PlaceholderText="名前を入力" />
<Button Content="{x:Bind ViewModel.ButtonText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Command="{x:Bind ViewModel.ButtonClickedCommand}"
CommandParameter="{x:Bind NameInput.Text, Mode=OneWay}" />
</StackPanel>
</Grid>
</Page>
SecondPage.xaml.cs
using Microsoft.UI.Xaml.Controls;
namespace WinUiApp.Pages
{
public sealed partial class SecondPage : Page
{
public MainWindowViewModel ViewModel { get; } = new();
public SecondPage()
{
this.InitializeComponent();
}
}
}
SecondPageViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace WinUiApp.ViewModels
{
public partial class SecondPageViewModel : ObservableObject
{
[ObservableProperty]
private string buttonText = "Click Me";
[RelayCommand]
private void OnButtonClicked(string name)
{
ButtonText = $"Clicked by {name}";
}
}
}
MainWindowにFrameとNavigationViewを追加
動作確認のため、一旦コードビハインドに実装する。
MainWindow.xaml
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="WinUiApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUiApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="WinUiApp">
<NavigationView SelectionChanged="NavigationView_SelectionChanged"
Loaded="NavigationView_Loaded">
<NavigationView.MenuItems>
<NavigationViewItem Content="メイン" Tag="MainPage" Icon="Home"/>
<NavigationViewItem Content="セカンド" Tag="SecondPage" Icon="Setting"/>
</NavigationView.MenuItems>
<Frame x:Name="ContentFrame">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition />
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
</NavigationView>
</Window>
MainWindow.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using WinUiApp.Pages;
namespace WinUiApp
{
public sealed partial class MainWindow : Window
{
public MainWindowViewModel ViewModel { get; } = new();
public MainWindow()
{
this.InitializeComponent();
}
/// <summary>
/// NavigationViewの選択項目が変更されたときに呼び出されるイベントハンドラー
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.SelectedItemContainer is NavigationViewItem selectedItem)
{
string? pageTag = selectedItem.Tag?.ToString();
Type? pageType = pageTag switch
{
"MainPage" => typeof(MainPage),
"SecondPage" => typeof(SecondPage),
_ => null
};
if (pageType != null && ContentFrame.CurrentSourcePageType != pageType)
{
ContentFrame.Navigate(pageType);
}
}
}
/// <summary>
/// 初期化時にMainPageを表示するためのイベントハンドラー
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void NavigationView_Loaded(object sender, RoutedEventArgs e)
{
ContentFrame.Navigate(typeof(MainPage));
}
}
}
以下を確認。
- Frameによる画面遷移
- NavigationViewの動作
- 前回作成した処理が正しく動くこと
画面遷移をMVVMパターン化
- 画面遷移をサービス化
- サービスをDI
- ViewModelでサービスを使用して画面遷移
NavigationService を定義
INavigationService.cs
public interface INavigationService
{
void NavigateTo(object? selectedPage);
void NavigateToMain();
}
以下2つのメソッドを実装。
-
NavigationViewItem
に設定されたTag
をもとにPage遷移 -
MainPage
へ遷移
NavigationService.cs
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using WinUiApp.Pages;
namespace WinUiApp.Services
{
public class NavigationService : INavigationService
{
private Frame? _frame;
/// <summary>
/// タグとページの型をマッピングする辞書。
/// </summary>
private readonly Dictionary<string, Type> _pageMap = new()
{
{ "MainPage", typeof(Pages.MainPage) },
{ "SecondPage", typeof(Pages.SecondPage) }
};
public void Initialize(Frame frame)
{
_frame = frame;
}
public void NavigateTo(object? selectedPage)
{
string? tag = null;
if (selectedPage is NavigationViewItem navItem)
{
tag = navItem.Tag as string;
}
else if (selectedPage is string str)
{
tag = str;
}
if (tag != null && _pageMap.TryGetValue(tag, out var pageType) && _frame?.CurrentSourcePageType != pageType)
{
_frame?.Navigate(pageType);
}
}
public void NavigateToMain()
{
_frame?.Navigate(typeof(MainPage));
}
}
}
DI設定
MainWindow
のFrame
をアプリケーション起動時にサービス登録するため、公開する。
MainWindow.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace WinUiApp
{
public sealed partial class MainWindow : Window
{
public MainWindowViewModel ViewModel { get; private set; }
// NavigationServiceに設定するため公開
public Frame ContentFrame => this.contentFrame;
public MainWindow(MainWindowViewModel vm) : base()
{
this.InitializeComponent();
this.ViewModel = vm;
}
}
}
IHostApplicatonBuilder
でホストを作成し、NavigationService
のDI設定
App.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.Extensions.Hosting;
using WinUiApp.Services;
using Microsoft.Extensions.DependencyInjection;
namespace WinUiApp
{
public partial class App : Application
{
public App()
{
this.InitializeComponent();
}
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
var builder = Host.CreateApplicationBuilder();
// NavigationService を登録
builder.Services.AddSingleton<INavigationService, NavigationService>();
builder.Services.AddSingleton<MainWindowViewModel>();
builder.Services.AddSingleton<MainWindow>();
var host = builder.Build();
m_window = host.Services.GetService<MainWindow>();
// NavigationService を取得
var navigationService = host.Services.GetService<INavigationService>();
// Frame を m_window から取得して NavigationService に設定
if (navigationService is NavigationService navService && m_window is MainWindow main)
{
navService.Initialize(main.ContentFrame);
}
m_window?.Activate();
}
private Window? m_window;
}
}
MainWindowのMVVM化
View
- 初期表示(Loaded)時の画面遷移コマンドをバインドするために、
Microsoft.Xaml.Behaviors.WinUI
を使用する。(NuGetでインストール)
-
NavigationView.SelectgedItem
をViewModelのプロパティにバインド
MainWindow.xaml
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="WinUiApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUiApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:behaviors="using:Microsoft.Xaml.Interactivity"
Title="WinUiApp">
<NavigationView x:Name="NavView" SelectedItem="{x:Bind ViewModel.SelectedPage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<!--初期表示時の画面遷移-->
<behaviors:Interaction.Behaviors>
<behaviors:EventTriggerBehavior EventName="Loaded">
<behaviors:InvokeCommandAction
Command="{x:Bind ViewModel.NavigationLoadedCommand}" />
</behaviors:EventTriggerBehavior>
</behaviors:Interaction.Behaviors>
<NavigationView.MenuItems>
<NavigationViewItem Content="メイン" Tag="MainPage" Icon="Home"/>
<NavigationViewItem Content="セカンド" Tag="SecondPage" Icon="Setting"/>
</NavigationView.MenuItems>
<Frame x:Name="contentFrame">
<Frame.ContentTransitions>
<TransitionCollection>
<NavigationThemeTransition />
</TransitionCollection>
</Frame.ContentTransitions>
</Frame>
</NavigationView>
</Window>
ViewModel
- DIによってNavigationServiceが注入される
-
NavigationView.SelectgedItem
をViewModelのプロパティにバインドし、プロパティ変更時の処理でNavigationService
による画面遷移を実施する。
MainWindowViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using WinUiApp.Services;
namespace WinUiApp
{
public partial class MainWindowViewModel(INavigationService navigation) : ObservableObject
{
private readonly INavigationService _navigationService = navigation;
[ObservableProperty]
private object selectedPage;
// SelectedPage変更時の処理を実装
partial void OnSelectedPageChanged(object value)
{
this._navigationService.NavigateTo(value);
}
[RelayCommand]
private void OnNavigationLoaded()
{
this._navigationService.NavigateToMain();
}
}
}
プロパティ変更時の処理について
以下のように、ObservableProperty
を付与した場合はコードが自動生成される。
今回は変更後の値が設定された後に実行される、OnSelectedPageChanged()
で画面遷移を行うように実装した。
自動生成されるコード例
public object? SelectedPage
{
get => selectedPage;
set
{
if (!EqualityComparer<object?>.Default.Equals(selectedPage, value))
{
object? oldValue = selectedPage;
OnSelectedPageChanging(value);
OnSelectedPageChanging(oldValue, value);
OnPropertyChanging();
selectedPage = value;
// ↓このタイミングで画面遷移
OnSelectedPageChanged(value);
OnSelectedPageChanged(oldValue, value);
OnPropertyChanged();
}
}
}
partial void OnSelectedPageChanging(object? value);
partial void OnSelectedPageChanged(object? value);
partial void OnSelectedPageChanging(object? oldValue, object? newValue);
partial void OnSelectedPageChanged(object? oldValue, object? newValue);
SelectionChangedではうまくいかない
以下のようにNavigationView
のSelectionChanged
イベントでは正しく画面遷移しない。
イベントが以下の順で発生するため、直前に選択していた画面に遷移することになる。
-
SelectionChanged
イベント - バインドされたプロパティの更新
MainWindow.xaml
...
<NavigationView x:Name="NavView">
<!--選択項目変更時のイベント-->
<behaviors:EventTriggerBehavior EventName="SelectionChanged">
<behaviors:InvokeCommandAction
Command="{x:Bind ViewModel.NavigationSelectionChangedCommand}"
CommandParameter="{x:Bind NavView.SelectedItem, Mode=OneWay}" />
</behaviors:EventTriggerBehavior>
...
MainWindowViewModel.cs
[RelayCommand]
private void OnNavigationSelectionChanged(object? selectedItem)
{
this._navigationService.NavigateTo(selectedItem);
}
完成
Discussion