WPF_MVVM

MVVM设计模式将应用分为三层,Model(模型层/实体层) ViewModel(视图模型层/UI模型层)View(视图层/UI层)。目前主流的开发都习惯用ORM框架进行数据库的设计,这里的模型层,存放的实体类,与数据表的字段一一对应。UI模型层,可以将模型层的数据进行选择性的读取(数据塑形),避免隐私数据泄露,同时作为UI层的数据类,与视图实现双向绑定,完成UI开发与业务开发的解耦,扩展性大大提高。

MVVM架构

ViewViewModel之间可以绑定两种属性,分别是数据属性和命令属性,数据属性可以双向绑定,命令属性只能由UI层调用视图模型层,也就是单向绑定。

这里展示了数据属性的双向绑定

1
2
ViewModel.Property <- Binding Engine -> UI.Control.Property
(INotifyPropertyChanged) (TwoWay Mode)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
graph TD
subgraph Backend
A[Backend System]
end

subgraph Model
B[Data Properties]
A --> B
end

subgraph ViewModel
C[Command Properties]
D[Data Properties]
B --> D
D --> B
end

subgraph View
E[TextBox]
F[Button]
E --> D
D --> E
F --> C
end

style Backend fill:#f9f,stroke:#333,stroke-width:4px
style Model fill:#bbf,stroke:#333,stroke-width:4px
style ViewModel fill:#bfb,stroke:#333,stroke-width:4px
style View fill:#ffb,stroke:#333,stroke-width:4px

手动实现过程

先创建ViewModelViewModel三个文件夹,存放对应的类,这里主要展示MVVM的基本实现。

NotIfiactionObject:所有ViewModel的基类,RaisePropertyChanged方法是实现双向绑定的核心,负责管理数据属性,由WPF 绑定引擎订阅事件,当相关的属性被修改的时候,会调用事件,WPF 绑定引擎接受到后,更新UI层的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WPF_MVVM_Learning.ViewModels
{
/// <summary>
/// ViewModel的基类
/// </summary>
class NotIfiactionObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

public void RaisePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}

DelegateCommand:负责处理命令属性。放在Command文件夹里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WPF_MVVM_Learning.Commands
{
class DelegateCommand : ICommand
{
public Action<object> ExecuteAction { get; set; }

public Func<object, bool> CanExecuteAction { get; set; }

public event EventHandler? CanExecuteChanged;

public bool CanExecute(object? parameter)
{
if (this.CanExecuteAction == null)
{
return true;
}
return this.CanExecuteAction.Invoke(parameter);
}

public void Execute(object? parameter)
{
if (this.ExecuteAction == null)
{
return;
}
this.ExecuteAction?.Invoke(parameter);
}
}
}

MainWindowsViewModel:WPF主窗体对应的视图模型,每一次设置属性的值,都会执行 this.RaisePropertyChanged(nameof(Input1));WPF 绑定引擎收到事件的发布后,更新页面的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WPF_MVVM_Learning.Commands;

namespace WPF_MVVM_Learning.ViewModels
{
class MainWindowsViewModel : NotIfiactionObject
{
private decimal input1;

public decimal Input1
{
get { return input1; }
set
{
input1 = value;
this.RaisePropertyChanged(nameof(Input1));
}
}

private decimal input2;

public decimal Input2
{
get { return input2; }
set
{
input2 = value;
this.RaisePropertyChanged(nameof(Input2));
}
}

private decimal result;

public decimal Result
{
get { return result; }
set
{
result = value;
this.RaisePropertyChanged(nameof(Result));
}
}

public DelegateCommand AddCommand { get; set; }

public DelegateCommand SaveCommand { get; set; }

private void Add(object parameter)
{
this.Result = 0;
this.Result = this.Input1 + this.Input2;
}

private void Save(object parameter)
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.ShowDialog();
}
public MainWindowsViewModel()
{
this.AddCommand = new DelegateCommand();
this.AddCommand.ExecuteAction = new Action<object>(this.Add);

this.SaveCommand = new DelegateCommand();
this.SaveCommand.ExecuteAction = new Action<object>(this.Save);
}
}
}

MainWindow.xaml.cs:通过DataContext拿到视图模型,UI层绑定使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WPF_MVVM_Learning.ViewModels;

namespace WPF_MVVM_Learning
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowsViewModel();
}
}
}

MainWindow.xaml实现,通过Binding绑定,数据属性主要用Text标签,命令属性主要用Command标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<Window
x:Class="WPF_MVVM_Learning.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:local="clr-namespace:WPF_MVVM_Learning"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="500"
Height="350"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Command="{Binding SaveCommand}" Content="Save" />
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<TextBox
x:Name="tb1"
Grid.Row="0"
Margin="4"
Background="LightBlue"
FontSize="24"
Text="{Binding Input1, Mode=TwoWay}" />
<TextBox
x:Name="tb2"
Grid.Row="1"
Margin="4"
Background="LightBlue"
FontSize="24"
Text="{Binding Input2}" />
<TextBox
x:Name="tb3"
Grid.Row="2"
Margin="4"
Background="LightBlue"
FontSize="24"
Text="{Binding Result}" />
<Button
x:Name="addButton"
Grid.Row="3"
Width="120"
Height="80"
Command="{Binding AddCommand}"
Content="Add"
FontSize="26" />
</Grid>
</Grid>
</Window>

在上述提到的MainWindowsViewModel类中,只有向UI层传递数据的操作,Text="{Binding Input1, Mode=TwoWay}" />,这里有一个模式(Mode)选择,其中TwoWay(双向绑定),就是UI层能将数据属性回传到UI模型层的配置,对于数据属性来说,这是默认配置,当我们将Mode的值改为OneWay(单向绑定),我们会发现UI层无法将数据回传到UI模型层。

借助Prism框架实现MVVM

MainWindowViewModel

其中SetProperty方法间接调用了RaisePropertyChanged,但是在Prism框架的实现过程中,将事件的发布放到了另一个函数中,名称为OnPropertyChangedRaisePropertyChanged的函数体,实际调用了OnPropertyChanged函数,从而完成事件发布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using Microsoft.Win32;
using Prism.Commands;
using Prism.Mvvm;
using Prism.Regions;
using System.DirectoryServices;

namespace BlankAppTest.ViewModels
{
public class MainWindowViewModel : BindableBase
{
private string _title = "简单加法";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}

private decimal input1;
public decimal Input1
{
get { return input1; }
set { SetProperty(ref input1, value); }
}

private decimal input2;

public decimal Input2
{
get { return input2; }
set { SetProperty(ref input2, value); }
}

private decimal result;

public decimal Result
{
get { return result; }
set { SetProperty(ref result, value); }
}

private DelegateCommand addCommand;
public DelegateCommand AddCommand =>
addCommand ?? (addCommand = new DelegateCommand(Add));

private void Add()
{
this.Result = 0;
this.Result = this.Input1 + this.Input2;
}

private DelegateCommand saveCommand;
public DelegateCommand SaveCommand =>
saveCommand ?? (saveCommand = new DelegateCommand(Save));

private void Save()
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.ShowDialog();
}

private DelegateCommand _NavigationCommand;
private readonly IRegionManager _manger;

public DelegateCommand NavigationCommand =>
_NavigationCommand ?? (_NavigationCommand = new DelegateCommand(Navigation));

private void Navigation()
{
_manger.RequestNavigate("ContentRegion", "HomeControl");
}

public MainWindowViewModel(IRegionManager regionManager)
{
_manger = regionManager;
}
}
}

Prism框架中,BindableBase是所有ViewModel的基类,和NotIfiactionObject一样,在Prism 5.0之后,NotIfiactionObjectBindableBase替换,二者都实现了INotifyPropertyChanged接口。新基类的实现过程过程中,对于形参添加了[CallerMemberName]特性,如protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null),该特性可以在方法中自动获取调用该方法的成员(如属性或方法)的名称,而不需要手动传递这个名称,因此在使用Prism框架的时候,无需手动传递实参,由编译器自动获取注入。

MainWindow.xaml视图

prism:ViewModelLocator.AutoWireViewModel="True"开启后会自动寻找ViewModels文件夹下与当前UI对应的ViewModel,并将其绑定到当前UI的DataContext上,直白点说就就是执行了对应的代码,如this.DataContext = new MainWindowsViewModel();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<Window
x:Class="BlankAppTest.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
Title="{Binding Title}"
Width="700"
Height="550"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button
x:Name="SaveButton"
Grid.Row="0"
Command="{Binding SaveCommand}"
Content="Save" />
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox
x:Name="Input1"
Grid.Row="0"
Margin="10"
FontSize="24"
Text="{Binding Input1}" />
<TextBox
x:Name="Input2"
Grid.Row="1"
Margin="10"
FontSize="24"
Text="{Binding Input2}" />
<TextBox
x:Name="Result"
Grid.Row="2"
Margin="10"
FontSize="24"
Text="{Binding Result}" />
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button
x:Name="AddButton"
Grid.Column="0"
Width="100"
Height="60"
Command="{Binding AddCommand}"
Content="Add"
FontSize="24" />

<Button
x:Name="NavigationButton"
Grid.Column="1"
Width="100"
Height="60"
Command="{Binding NavigationCommand}"
Content="Navigation"
FontSize="24" />
</Grid>
</Grid>
<ContentControl
Grid.Row="2"
Width="500"
Height="200"
prism:RegionManager.RegionName="ContentRegion" />
</Grid>
</Window>

局部页面跳转

HomeControl.xaml视图

1
2
3
4
5
6
7
8
9
10
11
12
13
<UserControl
x:Class="BlankAppTest.Views.HomeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<TextBlock
Width="200"
Height="40"
Text="{Binding Title}" />
</Grid>
</UserControl>

HomeControlViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;

namespace BlankAppTest.ViewModels
{
public class HomeControlViewModel : BindableBase
{
private string _title = "HomeControl";

public string Title
{
get { return _title = "HomeControl"; }
set { SetProperty(ref _title, value); }
}


public HomeControlViewModel()
{

}
}
}

App.xaml.cs文件,注册跳转页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using BlankAppTest.ViewModels;
using BlankAppTest.Views;
using Prism.Ioc;
using System.Windows;

namespace BlankAppTest
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<HomeControl, HomeControlViewModel>();
}
}
}

渲染位置,在MainWindows.xaml上,利用IRegionManager进行跳转。

1
2
3
4
5
<ContentControl
Grid.Row="2"
Width="500"
Height="200"
prism:RegionManager.RegionName="ContentRegion" />

实现局部页面跳转的部分代码,位于MainWindowViewModel类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private readonly IRegionManager _manger;

public DelegateCommand NavigationCommand =>
_NavigationCommand ?? (_NavigationCommand = new DelegateCommand(Navigation));

private void Navigation()
{
_manger.RequestNavigate("ContentRegion", "HomeControl");
}

public MainWindowViewModel(IRegionManager regionManager)
{
_manger = regionManager;
}

Prism框架部分分析

DelegateCommand简单分析:

  • 在使用单个参数的构造函数时,检查命令是否可以执行默认为True.
  • ObservesPropertyObservesCanExecute可以用来评估命令属性的可执行状态。
  • DelegateCommand 派生自 DelegateCommandBaseDelegateCommandBase又派生自ICommand,在ICommand中有一个事件CanExecuteChanged,WPF 绑定引擎监听事件,当命令状态变化的时候自动更新命令属性的启用。
  • ExecuteCanExecute 方法通常是由 WPF 绑定机制XAML 中的命令绑定 来调用的。用户点击按钮时,WPF 会调用 SaveCommand.Execute(),继而调用 DelegateCommand.Execute() 方法。在按钮状态变化时(例如数据变更后),WPF 会调用 SaveCommand.CanExecute() 来确定按钮是否应被启用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
using System;
using System.Linq.Expressions;
using System.Windows.Input;
using Prism.Properties;

namespace Prism.Commands
{
/// <summary>
/// <see cref="ICommand"/>,其委托不接受 <see cref="Execute()"/><see cref="CanExecute()"/> 的任何参数。
/// </summary>
/// <see cref="DelegateCommandBase"/>
/// <see cref="DelegateCommand{T}"/>
public class DelegateCommand : DelegateCommandBase
{
Action _executeMethod;
Func<bool> _canExecuteMethod;

/// <summary>
/// 创建 <see cref="DelegateCommand"/> 的新实例,并在执行时调用 <see cref="Action"/>
/// </summary>
/// <param name="executeMethod">The <see cref="Action"/> to invoke when <see cref="ICommand.Execute(object)"/> is called.</param>
public DelegateCommand(Action executeMethod)
: this(executeMethod, () => true)
{

}

/// <summary>
/// 创建 <see cref="DelegateCommand"/> 的新实例,并在执行时调用 <see cref="Action"/>,
/// 并使用 <see langword="Func" /> 进行查询以确定该命令是否可以执行。
/// </summary>
/// <param name="executeMethod">当调用 <see cref="ICommand.Execute"/> 时要调用的 <see cref="Action"/></param>
/// <param name="canExecuteMethod">调用 <see cref="ICommand.CanExecute"/> 时调用的 <see cref="Func{TResult}"/></param>
public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
: base()
{
if (executeMethod == null || canExecuteMethod == null)
throw new ArgumentNullException(nameof(executeMethod), Resources.DelegateCommandDelegatesCannotBeNull);

_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
}

///<summary>
/// 执行命令。
///</summary>
public void Execute()
{
_executeMethod();
}

/// <summary>
/// 确定命令是否可以执行。
/// </summary>
/// <returns>如果命令可以执行则返回<see langword="true"/>,否则返回<see langword="false"/></returns>
public bool CanExecute()
{
return _canExecuteMethod();
}

/// <summary>
/// 处理 <see cref="ICommand.Execute(object)"/> 的内部调用
/// </summary>
/// <param name="parameter">Command Parameter</param>
protected override void Execute(object parameter)
{
Execute();
}

/// <summary>
/// 处理 <see cref="ICommand.CanExecute(object)"/> 的内部调用
/// </summary>
/// <param name="parameter"></param>
/// <returns><see langword="true"/> if the Command Can Execute, otherwise <see langword="false" /></returns>
protected override bool CanExecute(object parameter)
{
return CanExecute();
}

/// <summary>
/// 观察实现 INotifyPropertyChanged 的属性,并在属性更改通知时自动调用 DelegateCommandBase.RaiseCanExecuteChanged。
/// </summary>
/// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
/// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public DelegateCommand ObservesProperty<T>(Expression<Func<T>> propertyExpression)
{
ObservesPropertyInternal(propertyExpression);
return this;
}

/// <summary>
/// 观察用于确定此命令是否可以执行的属性,如果它实现 INotifyPropertyChanged,它将在属性更改通知时自动调用 DelegateCommandBase.RaiseCanExecuteChanged。
/// </summary>
/// <param name="canExecuteExpression">The property expression. Example: ObservesCanExecute(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public DelegateCommand ObservesCanExecute(Expression<Func<bool>> canExecuteExpression)
{
_canExecuteMethod = canExecuteExpression.Compile();
ObservesPropertyInternal(canExecuteExpression);
return this;
}
}
}

ObservesPropertyObservesCanExecute 简单分析:

根据注释来看,二者都具有更改命令熟悉可执行状态的能力,都对 RaiseCanExecuteChanged 的调用改为了自动触发。但是ObservesProperty 更适合有多个数据属性影响命令属性的执行,而 ObservesCanExecute 适合单个数据属性影响命令属性的执行。

ObservesProperty 示例:有在 Input1Input2 都不为空时,才能执行一个计算命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private DelegateCommand calculateCommand;
public DelegateCommand CalculateCommand =>
calculateCommand ?? (calculateCommand = new DelegateCommand(Calculate, CanCalculate)
.ObservesProperty(() => Input1)
.ObservesProperty(() => Input2));

// Calculate method: Actual logic to be executed
private void Calculate()
{
SaveFileDialog saveFileDialog = new SaveFileDialog();
saveFileDialog.ShowDialog();
}

// CanCalculate method: Logic to determine if CalculateCommand can execute
private bool CanCalculate()
{
return Input1 != 0 && Input2 != 0;
}

ObservesCanExecute 示例:只有在 CanSaveFlagtrue 时,才能执行保存命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private bool canSaveFlag;
public bool CanSaveFlag
{
get { return canSaveFlag; }
set { SetProperty(ref canSaveFlag, value); }
}

private DelegateCommand saveCommand;
public DelegateCommand SaveCommand =>
saveCommand ?? (saveCommand = new DelegateCommand(Save)
.ObservesCanExecute(() => CanSaveFlag));

private void Save()
{
// Save logic here
}

ObservesProperty启停的详细解释:

真正决定命令是否可以执行的是 CanCalculate 方法,而 ObservesProperty 的作用是触发 RaiseCanExecuteChanged,从而让 CanCalculate 方法在相关属性变化时重新评估命令的可执行状态。为了更清晰地解释 ObservesProperty 的作用,可以按以下方式详细说明其在命令状态管理中的角色。

详细解释 ObservesProperty

  1. ObservesProperty 的作用
    • 观察属性ObservesProperty 方法用于监听一个属性的变化。
    • 自动触发 RaiseCanExecuteChanged:当被观察的属性发生变化时,ObservesProperty 自动调用 RaiseCanExecuteChanged,这会触发 CanCalculate 方法来重新评估 CalculateCommand 是否可以执行。
  2. 为什么需要 ObservesProperty
    • 在不使用 ObservesProperty 的情况下,每次 Input1Input2 改变时,需要手动调用 RaiseCanExecuteChanged 来更新命令的状态。
    • 使用 ObservesProperty 后,这个过程被自动化了,每当 Input1Input2 改变时,SaveCommand 会自动检测它是否可以执行,无需手动干预。

举个详细例子

假设没有使用 ObservesProperty,需要手动更新命令状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public decimal Input1
{
get { return input1; }
set
{
if (SetProperty(ref input1, value))
{
SaveCommand.RaiseCanExecuteChanged();
}
}
}

public decimal Input2
{
get { return input2; }
set
{
if (SetProperty(ref input2, value))
{
SaveCommand.RaiseCanExecuteChanged();
}
}
}

使用 ObservesProperty 后的简化

使用 ObservesProperty,可以简化为:

1
2
3
4
5
private DelegateCommand calculateCommand;
public DelegateCommand CalculateCommand =>
calculateCommand ?? (calculateCommand = new DelegateCommand(Calculate, CanCalculate)
.ObservesProperty(() => Input1)
.ObservesProperty(() => Input2));
  • ObservesProperty 自动监听 Input1Input2 的变化,每当这两个属性的值发生改变时,会自动调用 RaiseCanExecuteChanged,无需手动调用。
  • 这样,当 Input1Input2 发生变化时,CanSave 会被自动调用来重新评估 SaveCommand 是否可以执行。

结论

ObservesProperty 的作用在于自动化属性变化与命令状态更新之间的关联。虽然真正决定命令是否可以执行的是 CanCalculate 方法,但 ObservesProperty 确保了属性变化时会自动触发状态更新,从而减少了手动管理命令状态的繁琐工作。

ObservesCanExecuteObservesProperty函数与CanExecuteExecute函数之间的关系

ObservesCanExecuteObservesProperty 函数与 CanExecuteExecute 之间的关系体现在 命令状态管理执行逻辑 两个方面。它们共同作用,确保命令的可执行状态与相关属性的变化保持一致。

1. ExecuteCanExecute 的作用

  • Execute:定义了命令的具体执行逻辑。当用户触发某个命令时,Execute 方法被调用,执行命令绑定的操作。
  • CanExecute:决定命令是否可以执行。返回 true 表示命令可以执行,返回 false 表示命令不能执行。WPF 框架在需要决定控件(如按钮)是否启用时会调用 CanExecute

2. ObservesProperty 的作用

  • ObservesProperty 方法用于监控某个属性的变更。当指定的属性发生变化时,ObservesProperty 会自动调用 RaiseCanExecuteChanged,从而触发 CanExecuteChanged 事件。WPF 框架会因此再次调用 CanExecute 方法,重新评估命令的可执行性。
  • 关系ObservesPropertyCanExecute 之间的关系是间接的。ObservesProperty 监听属性变化,然后自动通知 CanExecute 重新执行,从而决定命令是否仍然可以执行。

3. ObservesCanExecute 的作用

  • ObservesCanExecute 方法不仅观察一个布尔表达式的变化,还将该表达式作为 CanExecute 方法的逻辑。它会自动在表达式变化时触发 RaiseCanExecuteChanged
  • 关系ObservesCanExecuteCanExecute 的关系更直接。ObservesCanExecute 的布尔表达式实际上替代了 CanExecute 的逻辑。当表达式的结果变化时,CanExecute 返回的值也会随之变化。

4. 综合关系

  • Execute:由 DelegateCommand 的构造函数直接设置,决定了命令的执行逻辑。
  • CanExecute:可以通过直接传递一个 Func<bool> 或者使用 ObservesCanExecute 来定义其逻辑。
  • ObservesProperty:用于监听某个属性变化,并在属性变化时自动触发 RaiseCanExecuteChanged,间接影响 CanExecute 的评估。
  • ObservesCanExecute:直接替代 CanExecute 的逻辑,并在相关属性变化时自动触发 RaiseCanExecuteChanged

示例

1
2
3
public DelegateCommand SaveCommand =>
saveCommand ?? (saveCommand = new DelegateCommand(Save)
.ObservesCanExecute(() => Input1 > 0 && Input2 > 0));

在这个例子中:

  • SaveExecute 的逻辑。
  • Input1 > 0 && Input2 > 0CanExecute 的逻辑,通过 ObservesCanExecute 直接定义。
  • 每当 Input1Input2 变化时,ObservesCanExecute 会自动触发 RaiseCanExecuteChanged,WPF 会重新检查 CanExecute 的返回值来决定 SaveCommand 是否可执行。
作者

神明大人

发布于

2025-02-18

更新于

2025-03-09

许可协议