前言
在WPF中使用導航功能可以使用Frame控制元件,這是比較基礎的一種方法。前幾天分享了wpfui中NavigationView的基本用法,但是如果真正在專案中使用起來,基礎的用法是無法滿足的。今天透過wpfui中的mvvm例子來說明在wpfui中如何透過依賴注入與MVVM模式使用導航功能。實踐起來,我個人覺得這個例子中實現導航功能還是有點麻煩的,但我也不知道怎麼能更優雅,也是學到了一些東西吧。
wpfui中MVVM例子的地址在:https://github.com/lepoco/wpfui/tree/main/src/Wpf.Ui.Demo.Mvvm
實現效果如下所示:
如果你對此感興趣,可以繼續閱讀。
實踐
使用依賴注入
將主窗體與主窗體的ViewModel與每個頁面與每個頁面的ViewModel都存入依賴注入容器中:
當然不只是窗體頁面與ViewModel,也需要註冊一些服務。
為了實現導航功能,使用了兩個服務分別是NavigationService與PageService。
NavigationService在wpfui庫中已經自帶了,直接使用即可:
具體程式碼可自行研究,這裡就不放了。
而PageService在wpfui中沒有自帶,需要自己定義,MVVM例子中的定義如下所示:
public class PageService : IPageService
{
/// <summary>
/// Service which provides the instances of pages.
/// </summary>
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="PageService"/> class and attaches the <see cref="IServiceProvider"/>.
/// </summary>
public PageService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <inheritdoc />
public T? GetPage<T>()
where T : class
{
if (!typeof(FrameworkElement).IsAssignableFrom(typeof(T)))
{
throw new InvalidOperationException("The page should be a WPF control.");
}
return (T?)_serviceProvider.GetService(typeof(T));
}
/// <inheritdoc />
public FrameworkElement? GetPage(Type pageType)
{
if (!typeof(FrameworkElement).IsAssignableFrom(pageType))
{
throw new InvalidOperationException("The page should be a WPF control.");
}
return _serviceProvider.GetService(pageType) as FrameworkElement;
}
}
現在已經將所有窗體、頁面、ViewModels與相關服務都註冊到容器中了。
ViewModel
在MainWindowViewModel中將頁面存入一個屬性中:
在非首頁的ViewModel中實現INavigationAware介面:
View
MainWindow.cs如下所示:
public partial class MainWindow : INavigationWindow
{
public ViewModels.MainWindowViewModel ViewModel { get; }
public MainWindow(
ViewModels.MainWindowViewModel viewModel,
IPageService pageService,
INavigationService navigationService
)
{
ViewModel = viewModel;
DataContext = this;
Wpf.Ui.Appearance.SystemThemeWatcher.Watch(this);
InitializeComponent();
SetPageService(pageService);
navigationService.SetNavigationControl(RootNavigation);
}
public INavigationView GetNavigation() => RootNavigation;
public bool Navigate(Type pageType) => RootNavigation.Navigate(pageType);
public void SetPageService(IPageService pageService) => RootNavigation.SetPageService(pageService);
public void ShowWindow() => Show();
public void CloseWindow() => Close();
/// <summary>
/// Raises the closed event.
/// </summary>
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
// Make sure that closing this window will begin the process of closing the application.
Application.Current.Shutdown();
}
INavigationView INavigationWindow.GetNavigation()
{
throw new NotImplementedException();
}
public void SetServiceProvider(IServiceProvider serviceProvider)
{
throw new NotImplementedException();
}
}
首先實現了INavigationWindow介面。在建構函式中注入所需的依賴類。注意這裡的RootNavigation其實就是頁面中NavigationView的名稱:
剛開始看這裡沒注意到,卡殼了很久。
因為你在程式碼中檢視定義,它會轉到這個地方:
沒經驗不知道是什麼,但是這次過後,知道這是在Xaml中定義,由工具自動生成的程式碼了。
其他的頁面改成了這樣的寫法:
public partial class DashboardPage : INavigableView<DashboardViewModel>
{
public DashboardViewModel ViewModel { get; }
public DashboardPage(DashboardViewModel viewModel)
{
ViewModel = viewModel;
this.DataContext = this;
InitializeComponent();
}
}
都實現了INavigableView<out T>
介面:
顯示主窗體與主頁面
現在準備工作都做好了,下一步就是顯示主窗體與主頁面了。
在容器中我們也注入了這個:
ApplicationHostService如下所示:
/// <summary>
/// Managed host of the application.
/// </summary>
public class ApplicationHostService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
private INavigationWindow? _navigationWindow;
public ApplicationHostService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
public async Task StartAsync(CancellationToken cancellationToken)
{
await HandleActivationAsync();
}
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
public async Task StopAsync(CancellationToken cancellationToken)
{
await Task.CompletedTask;
}
/// <summary>
/// Creates main window during activation.
/// </summary>
private async Task HandleActivationAsync()
{
await Task.CompletedTask;
if (!System.Windows.Application.Current.Windows.OfType<MainWindow>().Any())
{
_navigationWindow = (
_serviceProvider.GetService(typeof(INavigationWindow)) as INavigationWindow
)!;
_navigationWindow!.ShowWindow();
_ = _navigationWindow.Navigate(typeof(DashboardPage));
}
await Task.CompletedTask;
}
}
}
在app.xaml中定義了程式啟動與退出事件的處理程式:
/// <summary>
/// Occurs when the application is loading.
/// </summary>
private async void OnStartup(object sender, StartupEventArgs e)
{
await _host.StartAsync();
}
/// <summary>
/// Occurs when the application is closing.
/// </summary>
private async void OnExit(object sender, ExitEventArgs e)
{
await _host.StopAsync();
_host.Dispose();
}
整個過程回顧
在OnStartup方法中打個斷點,理解這個過程:
點選下一步:
到ApplicationHostService中了,一步一步除錯,注意這個地方:
因為主窗體實現了INavigationWindow
介面,這裡獲取了主窗體並將主窗體顯示,然後呼叫主窗體中的Navigate方法,導航到DashPage頁面,之後點繼續,結果如下所示:
最後
以上就是自己最近學習wpfui中導航功能實現的筆記,在自己的專案中也成功使用,對於可能會經常修改程式碼增加功能的程式這樣做感覺挺好的,但是如果你只是使用WPF做一個簡單的小工具,感覺這樣做增加了複雜度,不用依賴注入,不用做這麼複雜的導航,甚至不使用MVVM模式都可以。