WPF Prism 框架:打造高效、可维护的 WPF 应用 电脑版发表于:2025/4/22 15:32  ># WPF Prism 框架:打造高效、可维护的 WPF 应用 [TOC] Prism 框架简介 ------------ tn2>Prism 是一个用于构建松耦合、可维护且可测试的 XAML 应用程序的框架,支持 WPF、.NET MAUI、Uno Platform 和 Xamarin Forms 等多个平台。 它提供了多种设计模式的实现,如 MVVM(Model-View-ViewModel)、依赖注入、命令、事件聚合器等,这些模式有助于编写结构良好且易于维护的 XAML 应用程序。 tn>官网地址:https://docs.prismlibrary.com/ Prism 框架关键库 ------------ | 库名 | 描述 | | ------------ | ------------ | | `Prism.Core` | 实现MVVM的核心功能,属于一个与平台无关的项目。 | | `Prism.WPF` | 包含了DialogService、Region、Module、Navigation,其他的一些WPF的功能 | | `Prism.Unity` | Prism.Unity 是 Prism 框架的一个扩展,它集成了 Unity 依赖注入容器。 | tn2>这里我创建了一个`LearningPrism`项目,并安装好了这三个包,如下图所示: ```xml <ItemGroup> <PackageReference Include="Prism.Core" Version="9.0.537" /> <PackageReference Include="Prism.Unity" Version="9.0.537" /> <PackageReference Include="Prism.Wpf" Version="9.0.537" /> </ItemGroup> ```  ### BindableBase tn2>BindableBase 是 Prism 提供的一个基类,实现了 INotifyPropertyChanged 接口。 它简化了属性变更通知的触发逻辑,确保 UI 能够自动响应数据变化。 ### INotifyDataErrorInfo tn2>INotifyDataErrorInfo 是 WPF 4.0 引入的一个接口,用于实现自定义的数据验证逻辑。它包含以下三个成员: | 方法 | 描述 | | ------------ | ------------ | | `bool HasErrors` | 一个只读属性,用于指示是否存在错误。 | | `event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged` | 一个事件,用于通知绑定的控件错误信息已更改。| | `IEnumerable GetErrors(string propertyName)` | 一个方法,用于获取指定属性的错误信息。 | tn2>下面是一个简单的例子,当Value大于`100`报错。 ```csharp public class MainViewModel : BindableBase, INotifyDataErrorInfo { private int _value = 100; public int Value { get { return _value; } set { if (value > 100) { ErrorsContainer.SetErrors("Value",new string[] { "值不能大于100" }); } SetProperty(ref _value, value); } } /// <summary> /// 定义 OnErrorsContainerCreate 方法,用于在 ErrorsContainer 中添加错误时触发 /// </summary> /// <param name="arg"></param> private void OnErrorsContainerCreate(string arg) { // 触发 ErrorsChanged 事件,通知 UI 错误信息已更改 ErrorsChanged?.Invoke(this,new DataErrorsChangedEventArgs(arg)); } /// <summary> /// 定义一个私有字段 _errorsContainer,用于存储错误信息 /// </summary> private ErrorsContainer<string> _errorsContainer; public ErrorsContainer<string> ErrorsContainer { get { if (_errorsContainer == null) { _errorsContainer = new ErrorsContainer<string>(OnErrorsContainerCreate); } return _errorsContainer; } set { _errorsContainer = value; } } /// <summary> /// 判断是否存在异常 /// </summary> public bool HasErrors => ErrorsContainer.HasErrors; /// <summary> /// 用于通知 UI 错误信息已更改 /// </summary> public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged; /// <summary> /// 用于获取指定属性的错误信息 /// </summary> /// <param name="propertyName"></param> /// <returns></returns> public IEnumerable GetErrors(string? propertyName)=> ErrorsContainer.GetErrors(propertyName); } ``` tn2>修改`MainWindow.xaml`。 ```xml <Window x:Class="LearningPrism.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:local="clr-namespace:LearningPrism" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <!-- 设置窗口的数据上下文为 MainViewModel --> <Window.DataContext> <local:MainViewModel/> </Window.DataContext> <!-- 定义一个资源字典,用于存储控件模板 --> <Window.Resources> <!-- 定义一个 ControlTemplate,用于自定义 TextBox 的样式 --> <ControlTemplate TargetType="{x:Type TextBox}" x:Key="ct"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <!-- 定义一个 Border,用于显示 TextBox 的边框 --> <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True" CornerRadius="5"> <!-- 定义一个 ScrollViewer,用于显示 TextBox 的内容 --> <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" VerticalContentAlignment="Center" Margin="3,5" BorderThickness="0"/> </Border> <!-- 定义一个 TextBlock,用于显示错误信息 --> <TextBlock Grid.Row="1" Text="{Binding (Validation.Errors)[0].ErrorContent,RelativeSource={RelativeSource AncestorType=TextBox,Mode=FindAncestor}}" Foreground="Red" Margin="10,5" Name="txtError"/> </Grid> <!-- 定义触发器,用于在 TextBox 验证失败时显示错误信息 --> <ControlTemplate.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Visibility" Value="Visible" TargetName="txtError"/> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Window.Resources> <Grid> <StackPanel> <TextBlock Text="{Binding Value}"/> <TextBox Text="{Binding Value,UpdateSourceTrigger=PropertyChanged}" Template="{StaticResource ct}"/> </StackPanel> </Grid> </Window> ```  tn2>当我们进行修改100为110时,它将会报错。  行为处理 ------------ ### DelegateCommand tn2>DelegateCommand 是 Prism 框架中提供的一个命令类(如果你没有使用 Prism 框架,可能也会有类似的自定义命令类来实现类似功能),它用来表示一个可以执行的操作(命令)。 DelegateCommand 包含了两个重要的部分: `Execute`:表示命令被执行时要执行的方法(后面会看到绑定的 Execute 方法)。 `CanExecute`:用于判断命令是否可以执行,根据这个方法的返回值来决定绑定的按钮是否启用(是否变灰)。<br/> 我们将在`MainViewModel`类中定义一个 `DelegateCommand` 类型的属性 `BtnCheckCommand`。 ```csharp public DelegateCommand BtnCheckCommand { get; } ``` tn2>接着我们定义命令执行逻辑 ```csharp private bool CheckExecute() { return Value == 100; } private void Execute() { this.Value = 0; } ``` tn2>`CheckExecute` 方法是 `CanExecute` 的具体实现,它会根据 `Value` 的值来判断命令是否可以执行。 如果 `Value` 的值等于 100,则返回 `true` 表示命令可以执行;否则返回 `false`,表示命令不能执行。 `Execute` 方法是命令被执行时的操作,也就是当按钮可以点击并且被点击时,会调用这个方法。 这里简单地将 `Value` 的值设置为 0。 接着我们初始化命令 ```csharp public MainViewModel() { BtnCheckCommand = new DelegateCommand(Execute,CheckExecute); } ``` tn2>我们给值属性添加一行代码,这行代码的作用是通知所有与这个命令相关的控件(比如按钮),命令的可执行状态可能发生了变化,让它们重新检查命令是否可以执行。 ```csharp public int Value { get { return _value; } set { if (value > 100) { ErrorsContainer.SetErrors("Value",new string[] { "值不能大于100" }); } SetProperty(ref _value, value); // 触发命令相关的可执行的检查逻辑 BtnCheckCommand.RaiseCanExecuteChanged(); } } ``` tn2>接着我们在窗体绑定命令按钮。 然后运行测试。 ```csharp <Button Content="检查" Command="{Binding BtnCheckCommand}"></Button> ```  tn2>我们将点击检查,发现值等于100,然后将100改成0后,按钮将被禁用了。  ### 检查方式 tn2>除了上面的`BtnCheckCommand.RaiseCanExecuteChanged();`检查方式我们还有其他两种方式。 `ObservesProperty` 是 Prism 框架中用于简化命令的 CanExecute 方法和属性变化通知的一种特性。它可以帮助你自动触发命令的 `CanExecuteChanged` 事件,而无需手动调用 `RaiseCanExecuteChanged()`。下面是如何使用 ObservesProperty 来实现命令的自动检测: ```csharp public MainViewModel() { BtnCheckCommand = new DelegateCommand(Execute,CheckExecute) .ObservesProperty(()=>Value); } ``` ```csharp // 触发命令相关的克执行的检查逻辑 //BtnCheckCommand.RaiseCanExecuteChanged(); ```   tn2>我们还可以使用`ObservesCanExecute`来进行检测。 但是`ObservesCanExecute` 方法只能用于观察一个简单的布尔属性,所以我们这里定义一个状态来解决。 ```csharp private bool CheckExecute() { return Value == 100; } private void Execute() { this.Value = 0; Status = !Status; } private bool _status; public bool Status { get => Value == 100; set { SetProperty(ref _status,value); } } public MainViewModel() { BtnCheckCommand = new DelegateCommand(Execute,CheckExecute) //.ObservesProperty(()=>Value) .ObservesCanExecute(()=> Status) ; } ```  异步命令 ------------ tn2>异步命令非常简单,就只是在普通命令的方法处理上面添加上`async`就可以了。 ```csharp public DelegateCommand BtnAsyncCommand { get=>new DelegateCommand(DoAsyncSoming); } private async void DoAsyncSoming() { await Task.Delay(3000); this.Value = 30; } ``` tn2>在窗体我们添加一个异步按钮。 ```xml <Button Content="异步" Command="{Binding BtnAsyncCommand}"></Button> ```  泛型参数 ------------ tn2>首先实例化一个泛型`DelegateCommand`类,这里我们以string作为一个参数进行实现。 ```csharp public ICommand BtnPpCommand { get => new DelegateCommand<string>(DoPpSoming); } private async void DoPpSoming(string obj) { await Task.Delay(3000); this.Value = 40; } ``` tn2>前端我们通过`CommandParameter`进行传入参数 ```xml <Button Content="传参" Command="{Binding BtnPpCommand}" CommandParameter="传递的参数"></Button> ```  事件命令 ------------ tn2>我们这里用一个`ComboBox`做一个示范,当我们进行选择时将触发该事件。 首先我们在Windows节点中添加下面两个引用。 ```xml xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:prism="http://prismlibrary.com/" ``` tn2>添加下面的`ComboBox`代码 ```xml <!-- 定义一个 ComboBox 控件,默认选中第一个选项 --> <ComboBox SelectedIndex="0"> <!-- 设置交互触发器,用于在发生特定事件时执行相应动作 --> <i:Interaction.Triggers> <!-- 指定在 ComboBox 的 SelectionChanged 事件发生时触发动作 --> <i:EventTrigger EventName="SelectionChanged"> <!-- 调用视图模型中的命令 BtnEventCommand --> <prism:InvokeCommandAction Command="{Binding BtnEventCommand}"> </prism:InvokeCommandAction> </i:EventTrigger> </i:Interaction.Triggers> <!-- 向 ComboBox 添加可选择的项目 --> <ComboBoxItem Content="11111" /> <ComboBoxItem Content="22222" /> <ComboBoxItem Content="33333" /> <ComboBoxItem Content="44444" /> <ComboBoxItem Content="55555" /> </ComboBox> ``` tn2>添加一下`BtnEventCommand`命令。 ```csharp public ICommand BtnEventCommand { get => new DelegateCommand<object>(DoEventSoming); } private void DoEventSoming(object obj) { } ``` tn2>选择后我们发现,它可以接收到我们的参数。   tn2>如果我们只希望接收到`Source.SelectedItem.Content`中的参数的话可以这样写。 ```xml <prism:InvokeCommandAction Command="{Binding BtnEventCommand}" TriggerParameterPath="Source.SelectedItem.Content"/> ``` 