WPF 现代化开发教程:使用 Microsoft.Extensions.Hosting 和 CommunityToolkit.Mvvm

介绍

随着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 应用程序的开发更加规范、模块化和易于扩展。对于中大型项目,这种现代化开发方式尤其有利于提高项目的可管理性和长期维护性。

posted @ 2024-08-28 22:25  阿遇而已  阅读(449)  评论(0编辑  收藏  举报