WPF 現代化開發教程:使用 Microsoft.Extensions.Hosting 和 CommunityToolkit.Mvvm

阿遇而已發表於2024-08-28

介紹

隨著WPF應用程式的複雜性增加,使用現代化的開發工具和模式變得尤為重要。本教程將指導你如何使用 Microsoft.Extensions.HostingCommunityToolkit.Mvvm 來開發一個現代化的WPF應用程式。這些工具為開發者提供了依賴注入、應用程式生命週期管理、MVVM模式支援等功能。

先決條件

  • Visual Studio 2022 或更高版本
  • .NET 6.0 或更高版本
  • 基本的WPF和MVVM知識

第1步:建立WPF專案

  • 開啟Visual Studio,建立一個新的WPF專案。
  • 選擇 .NET 6.0 作為目標框架。

第2步:安裝NuGet包

在專案中安裝以下NuGet包:

  • Microsoft.Extensions.Hosting - 用於託管應用程式的通用介面。
  • CommunityToolkit.Mvvm - 提供簡潔易用的MVVM模式支援。

使用以下命令在NuGet包管理器控制檯中安裝:

Install-Package Microsoft.Extensions.Hosting
Install-Package CommunityToolkit.Mvvm

第3步:設定應用程式的Host

使用 Microsoft.Extensions.Hosting 來管理應用程式的啟動和依賴注入。

修改 App.xaml.cs

public partial class App
{
    // The.NET Generic Host provides dependency injection, configuration, logging, and other services.
    // https://docs.microsoft.com/dotnet/core/extensions/generic-host
    // https://docs.microsoft.com/dotnet/core/extensions/dependency-injection
    // https://docs.microsoft.com/dotnet/core/extensions/configuration
    // https://docs.microsoft.com/dotnet/core/extensions/logging
    private static readonly IHost _host = Host.CreateDefaultBuilder()
        .ConfigureAppConfiguration(c =>
        {
            var basePath =
                Path.GetDirectoryName(AppContext.BaseDirectory)
                ?? throw new DirectoryNotFoundException(
                    "Unable to find the base directory of the application."
                );
            _ = c.SetBasePath(basePath);
        })
        .ConfigureServices(
            (context, services) =>
            {
                // App Host
                _ = services.AddHostedService<ApplicationHostService>();

                // Main window
                _ = services.AddSingleton<INavigationWindow, Views.MainWindow>();
                _ = services.AddSingleton<ViewModels.MainWindowViewModel>();
            }
        )
        .Build();

    /// <summary>
    /// Gets services.
    /// </summary>
    public static IServiceProvider Services
    {
        get { return _host.Services; }
    }

    /// <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();
    }

    /// <summary>
    /// Occurs when an exception is thrown by an application but not handled.
    /// </summary>
    private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
    {
        // For more info see https://docs.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception?view=windowsdesktop-6.0
    }
}

修改 App.cs

<Application
    x:Class="Demo.Mvvm.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    DispatcherUnhandledException="OnDispatcherUnhandledException"
    Exit="OnExit"
    Startup="OnStartup">
    <Application.Resources/>
</Application>

解釋

  • Host.CreateDefaultBuilder():建立一個預設的主機構建器,它會自動配置應用程式,包括配置和日誌記錄。

  • ConfigureServices:在這裡你可以註冊你的依賴項,例如ViewModel、服務等。

  • OnStartupOnExit:在應用程式啟動和退出時,分別啟動和停止主機。

    ApplicationHostService

    Microsoft.Extensions.Hosting 框架中,AddHostedService<TService> 方法用於將一個實現了 IHostedService 介面的服務新增到依賴注入容器中,並讓它在應用程式啟動時自動執行。

    1. IHostedService 介面

    IHostedService 是一個用於定義後臺任務或長期執行的服務的介面。它有兩個主要方法:

    • StartAsync(CancellationToken cancellationToken):當應用程式啟動時呼叫。你可以在這裡初始化資源、啟動任務或其他需要在應用程式執行時保持活躍的邏輯。
    • StopAsync(CancellationToken cancellationToken):當應用程式關閉時呼叫。你可以在這裡清理資源、停止任務或儲存狀態。

    實現 IHostedService 介面的類可以用來在應用程式啟動和停止時執行一些自定義的初始化和清理操作。

    2. AddHostedService<TService> 方法

    Microsoft.Extensions.DependencyInjection 中,AddHostedService<TService> 是一個擴充套件方法,用於將一個 IHostedService 實現新增到依賴注入容器中。這個方法會確保該服務在應用程式啟動時自動啟動,並在應用程式停止時自動停止。

    /// <summary>
    /// Managed host of the application.
    /// </summary>
    public class ApplicationHostService(IServiceProvider serviceProvider) : IHostedService
    {
        /// <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 (!Application.Current.Windows.OfType<MainWindow>().Any())
            {
                _navigationWindow = (serviceProvider.GetService(typeof(MainWindow)) as MainWindow)!;
    			_navigationWindow!.ShowWindow();
            }
    
            await Task.CompletedTask;
        }
    }
    
    

    第4步:使用 CommunityToolkit.Mvvm 實現MVVM

    CommunityToolkit.Mvvm 簡化了MVVM模式的實現,提供了屬性變更通知、命令和依賴注入等功能。

    首先建立一個簡單的 ViewModel:

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using System.Windows;
    
    namespace Demo.Mvvm.App.ViewModel;
    
    public partial class MainViewModel : ObservableObject
    {
        [ObservableProperty]
        private string _message;
    
        public MainViewModel()
        {
            Message = "Hello, WPF with MVVM!";
        }
    
        [RelayCommand]
        private void ShowMessage()
        {
            MessageBox.Show(Message);
        }
    }
    
    

    解釋

    • ObservableObject:這是 CommunityToolkit.Mvvm 提供的基類,簡化了 INotifyPropertyChanged 的實現。
    • [ObservableProperty]:自動生成屬性和通知邏輯。
    • [RelayCommand]:簡化了命令的建立。

第5步:繫結 ViewModel 到 View

MainWindow.xaml 中,設定 DataContext 並繫結控制元件到 ViewModel。

修改 MainWindow.xaml

<Window x:Class="Demo.Mvvm.App.View.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Demo.Mvvm.App.View"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        d:DataContext="{d:DesignInstance local:MainWindow,
                                 IsDesignTimeCreatable=True}"
        Title="Modern WPF App" Height="200" Width="400">
    <Grid>
        <TextBlock Text="{Binding Message}" 
                   HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24" />
        <Button Command="{Binding ShowMessageCommand}" Content="Show Message"
                HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,20"/>
    </Grid>
</Window>

解釋

  • DataContext:透過在 App.xaml.cs 中註冊,MainViewModel 自動作為 DataContext 繫結到 MainWindow
  • TextBlockButton:繫結到 MainViewModelMessage 屬性和 ShowMessageCommand 命令。

修改 MainWindow.xaml.cs

public partial class MainWindow
{
    public MainWindowViewModel ViewModel { get;}
    public MainWindow(MainWindowViewModel viewModel)
    {
        ViewModel = viewModel;
        DataContext = ViewModel;
        InitializeComponent();
    }
}

解釋

  • DataContext:透過依賴注入的方式自動獲取到ViewModel物件,然後指定過去。

第6步:執行應用程式

執行應用程式,你將看到一個簡單的視窗,展示了透過MVVM模式繫結的文字和按鈕。當你點選按鈕時,顯示的訊息將透過ViewModel的命令被觸發。

為什麼需要使用現代化開發呢?

1.最佳化重複性的通知屬性和Command宣告

傳統的開發邏輯如下:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace TraditionalMvvmExample
{
    public class MainViewModel : INotifyPropertyChanged
    {
        // INotifyPropertyChanged implementation
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        // Properties
        private string _property1;
        public string Property1
        {
            get => _property1;
            set
            {
                if (_property1 != value)
                {
                    _property1 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property2;
        public string Property2
        {
            get => _property2;
            set
            {
                if (_property2 != value)
                {
                    _property2 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property3;
        public string Property3
        {
            get => _property3;
            set
            {
                if (_property3 != value)
                {
                    _property3 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property4;
        public string Property4
        {
            get => _property4;
            set
            {
                if (_property4 != value)
                {
                    _property4 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property5;
        public string Property5
        {
            get => _property5;
            set
            {
                if (_property5 != value)
                {
                    _property5 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property6;
        public string Property6
        {
            get => _property6;
            set
            {
                if (_property6 != value)
                {
                    _property6 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property7;
        public string Property7
        {
            get => _property7;
            set
            {
                if (_property7 != value)
                {
                    _property7 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property8;
        public string Property8
        {
            get => _property8;
            set
            {
                if (_property8 != value)
                {
                    _property8 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property9;
        public string Property9
        {
            get => _property9;
            set
            {
                if (_property9 != value)
                {
                    _property9 = value;
                    OnPropertyChanged();
                }
            }
        }

        private string _property10;
        public string Property10
        {
            get => _property10;
            set
            {
                if (_property10 != value)
                {
                    _property10 = value;
                    OnPropertyChanged();
                }
            }
        }

        // Commands
        private ICommand _simpleCommand;
        public ICommand SimpleCommand
        {
            get
            {
                if (_simpleCommand == null)
                {
                    _simpleCommand = new RelayCommand(param => ExecuteSimpleCommand(), param => CanExecuteSimpleCommand());
                }
                return _simpleCommand;
            }
        }

        private void ExecuteSimpleCommand()
        {
            // Command execution logic here
            Property1 = "Command Executed!";
        }

        private bool CanExecuteSimpleCommand()
        {
            return !string.IsNullOrEmpty(Property1);
        }
    }

    // RelayCommand implementation
    public class RelayCommand : ICommand
    {
        private readonly Action<object> _execute;
        private readonly Predicate<object> _canExecute;

        public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute(parameter);
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }
}

使用toolkit.mvvm工具包最佳化之後的效果如下:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace ModernMvvmExample
{
    public partial class MainViewModel : ObservableObject
    {
        // 使用 [ObservableProperty] 特性生成屬性和 OnPropertyChanged 通知
        [ObservableProperty]
        private string property1;

        [ObservableProperty]
        private string property2;

        [ObservableProperty]
        private string property3;

        [ObservableProperty]
        private string property4;

        [ObservableProperty]
        private string property5;

        [ObservableProperty]
        private string property6;

        [ObservableProperty]
        private string property7;

        [ObservableProperty]
        private string property8;

        [ObservableProperty]
        private string property9;

        [ObservableProperty]
        private string property10;

        // 使用 [RelayCommand] 特性生成命令
        [RelayCommand]
        private void ExecuteSimpleCommand()
        {
            Property1 = "Command Executed!";
        }
    }
}

2.生命週期的可控制性

傳統 WPF 開發

  • 在傳統的 WPF 開發中,應用程式的啟動、關閉等生命週期事件通常在 App.xaml.cs 中手動管理。這種管理方式隨著應用程式的複雜性增加,容易變得混亂且難以維護。
  • 各個部分(如資料庫連線、外部服務)的初始化和清理需要手動處理,容易遺漏,且難以集中管理。

現代化 WPF 開發

  • Microsoft.Extensions.Hosting:提供了統一的應用程式生命週期管理介面。你可以使用 IHostIHostedService 等介面來統一管理應用程式的啟動、停止和資源清理。
  • 這種方式提高了程式碼的模組化程度,所有的初始化和清理邏輯都可以集中管理,從而提高了應用程式的可靠性和可維護性。

3.模組化開發與可擴充性

傳統 WPF 開發

  • 在傳統 WPF 應用程式中,隨著業務邏輯的增長,程式碼庫容易變得雜亂無章。模組化通常依賴於開發者的自律性和程式碼組織能力。
  • 在沒有 DI 容器的情況下,實現模組化設計和擴充套件應用程式功能變得更加困難,尤其是在需要動態載入或替換模組時。

現代化 WPF 開發

  • Microsoft.Extensions.Hosting:透過 DI 容器和服務註冊,應用程式的各個部分(如服務、ViewModel、資料訪問層)都可以輕鬆地解耦和模組化。
  • 這種模組化設計使得應用程式的擴充套件和維護變得更加容易,可以透過註冊不同的服務實現動態功能擴充套件,且不影響現有程式碼的穩定性。

透過採用 Microsoft.Extensions.HostingCommunityToolkit.Mvvm,現代化的 WPF 開發不僅提高了程式碼的可維護性和可測試性,還顯著提升了開發效率和開發體驗。這種方式引入了統一的應用程式生命週期管理、依賴注入、配置和日誌管理等現代化開發工具,使得 WPF 應用程式的開發更加規範、模組化和易於擴充套件。對於中大型專案,這種現代化開發方式尤其有利於提高專案的可管理性和長期維護性。

相關文章