通知单个属性值改变
-
不传参
private string _firstName; public string FirstName { get { return _firstName; } set { _firstName = value; OnPropertyChanged(); // 不传参,CallerMemberNameAttribute修饰方法参数,不传参时默认实参是调用方的标识符 } }
-
nameof
private string _lastName; public string LastName { get { return _lastName; } set { _lastName = value; OnPropertyChanged(nameof(LastName)); // nameof(属性标识符) } }
-
Expression Tree
private int _age; public int Age { get { return _age; } set { _age = value; OnPropertyChanged(() => Age); // Expression Tree } }
-
常量字符串
private string _email; public string Email { get { return _email; } set { _email = value; OnPropertyChanged("Email"); // 常量字符串 } }
优缺点分析:
不传参:简便
nameof:防”笔误”,避免敲错属性名
Expression Tree:防”笔误”,避免敲错属性名,但因为要解析Expression Tree,性能较低
常量字符串:容易”笔误”,敲错属性名
推荐使用优先级
无显示传参 > nameof > 常量字符串 > Expression Tree
通知多个属性值改变
当多个属性的值存在依赖关系,即其中一个属性的值发生了变化,其他的属性的值也受影响跟着发生了变化,这种情况下,在被修改的属性的setter中,也应该通知其他的属性的值发生了变化。
开发者可以多次调用通知单个属性的值发生了变化的api,也可以调用一次通知多个属性的值发生了变化的api.
案例:FullName = FirstName + LastName,修改了FirstName或LastName,FullName的值也会被改变。
class Person : ObservableObject
{
private string _firstName;
public string FirstName // 无忌 名
{
get { return _firstName; }
set
{
_firstName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(FullName));
}
}
private string _lastName;
public string LastName // 张 姓
{
get { return _lastName; }
set
{
_lastName = value;
OnPropertyChanged(nameof(LastName), nameof(FullName));
}
}
public string FullName // 张无忌 姓名
{
get
{
return FirstName + " " + LastName;
}
}
}
通知所有属性值改变
实参为空或空字符串,通知数据源的所有的属性的值发生了变化。
OnPropertyChanged(string.Empty);
OnPropertyChanged((string)null);
案例:Name被修改,UI未更新;Age被修改,UI未更新;Email被修改,此时会通知UI Name,Age,Email的值发生了变化,此时,UI才会显示出先前被修改后的Name和Age的新值。
我们也可以发现:属性=value与通知UI更新并不需要一定要同时出现在setter中,我们在想刷新UI时,随时调用OnPropertyChanged()即可。
class Person : ObservableObject
{
public string Name { get; set; }
public int Age { get; set; }
private string _email;
public string Email
{
get => _email;
set
{
_email = value;
OnPropertyChanged("");
}
}
}
取消不必要的通知提高程序性能
当我们修改源属性的值时,如果新值和原值相同,那么我们就没必要将新值赋予给属性,这样能免去不必要的UI刷新,这对提高程序的性能有很大帮助。假设我们开发一个实时显示气温的App,我们每秒采集一次温度刷新UI,但气温是个缓慢的渐变量,可能连续采集100次的气温都相同,那么我们就能减少99次不必要的UI刷新。
TrumpX.Toolkit.Mvvm提供了相应的Api,该Api能自动判断新值和旧值是否相等,若相等则不为属性赋值且不通知UI刷新。因为属性分为back-field property和logic property,所以TrumpX.Toolkit.Mvvm有两个此类Api,分别用于操作上述两种类型的属性。
back-field property
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null);
field
是属性的字段
newValue
是新值value,SetProperty内部进行判等决定是否更新属性
comparer
比较器,为null时采用默认的比较器(从Object继承的bool Equals(Object obj))判等,但可自定义比较器使用自定义的规则判等
class TempertureMonitor : ObservableObject
{
public TempertureMonitor()
{
PropertyChanged += TempertureMonitor_PropertyChanged;
}
private void TempertureMonitor_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
Console.WriteLine($"属性{e.PropertyName}的值变化成{sender.GetType().GetProperty($"{e.PropertyName}").GetValue(sender)}...");
}
private double _currentTemperture;
public double CurrentTemperture
{
get => _currentTemperture;
set
{
SetProperty(ref _currentTemperture, value);
}
}
}
logic property
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null);
oldValue
旧值
newValue
新值,SetProperty内部进行判等决定是否更新属性
callback
是具有一个参数的回调,此回调是Logic Property的Setter的赋值逻辑,在SetProperty内被调用,实参是newValue
,仅新值与旧值不相等时才会被执行
comparer
比较器,为null时采用默认的比较器(从Object继承的bool Equals(Object obj))判等,但可自定义比较器使用自定义的规则判等
class Person : ObservableObject
{
public Person()
{
PropertyChanged += Person_PropertyChanged;
}
private void Person_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if(e.PropertyName == "FullName")
{
MessageBox.Show($"属性{e.PropertyName}的值变化成{sender.GetType().GetProperty($"{e.PropertyName}").GetValue(sender)}...");
}
}
private string _firstName;
private string _lastName;
public string FirstName { get => _firstName; } // 无setter
public string LastName { get=>_lastName; } // 无setter
public string FullName
{
get => FirstName + " " + LastName;
set
{
SetProperty(FullName, value, (v) =>
{
string[] subs = v.Split(' ');
_firstName = subs[0];
_lastName = subs[1];
OnPropertyChanged(nameof(FirstName), nameof(LastName)); // 不相等才会调用回调方法,才会执行这行通知代码
});
}
}
}
自定义比较器
上述的两个API都有参数IEqualityComparer<T> comparer
,支持使用自定义判等器进行判等决定是否刷新UI.
public class ModelEqualityCompare : EqualityComparer<TModel>
{
public override bool Equals(TModel x, TModel y)
{
return x.Equals(y);
}
public override int GetHashCode(TModel obj)
{
return EqualityComparer<TModel>.Default.GetHashCode();
}
}
封装无通知机制的Model
开发应用程序都会复用底层的一些Model,但是底层开发不会也不应该考虑上层使用者,比如给WPF使用,所以Model一般都不会继承INotifyPropertyChanged。Microsoft.Toolkit.Mvvm提供了利用无通知的Model快速在ViewModel中封装一个具有通知能力的数据源的Api,其核心思想就是在封装时,用Model实例代替back-field来存储属性的值,并且加入了判等决定是否更新属性及刷新UI,回调方法决定setter的为属性赋值的逻辑。
SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null) where TModel : class
oldValue
:原值,即model中相应的属性的值
newValue
: 新值,即setter的value
model
:Model实例
callback
:logic peoperty的setter逻辑,参数是TModel model和T newValue,一般是将newValue赋值给model的相应属性
comparer
: 原值和新值的比较器,默认调用从Object继承的Equals,但可自定义比较器实现判等规则
propertyName
: 通知的源属性名称,默认是setter的属性,也可以用nameof显示传递属性名称
model
class Student // 无通知机制的Model
{
public string Name { get; set; }
}
viewmodel
class ObservableStudent : ObservableObject // 具有通知机制的ViewModel,可以与UI双向绑定
{
private Student _student;
public ObservableStudent(Student student) {
_student = student;
}
public string Name {
get => _student.Name;
set {
SetProperty(_student.Name, value, _student, (model, newValue) => model.Name = newValue);
}
}
}
SetProperty取舍
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null);
SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null) where TModel : class
后者是前者的一个特殊情况,即后者完全用前者替代。但后者是针对封装无通知机制的Model特供的API,书写和运行效率略优于前者。
监视Task属性
开发者可以预先创建有若干Task类型的属性的数据源,并将Task类型的属性与目标属性绑定;当需要被监听的任务被创建后,将任务实例的引用赋值给数据源Task类型的属性,这样就能通过UI监控任务的进度和结果。
private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask newValue, Action<TTask> callback, [CallerMemberName] string propertyName = null) where TTask : Task
protected bool SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T> taskNotifier, Task<T> newValue, Action<Task<T>> callback = null, [CallerMemberName] string propertyName = null)
共计两个API,一个用于无返回值的任务,一个用于有返回值的任务。
taskNotifier
: 包含一个Task属性的 TaskNotifier,TaskNotifier可以隐士转换成Task。
newValue
: 需要被监控的Task实例的引用。
callback
: 带有一个参数的回调方法,实参是newValue
,在newValue
完成后执行。
dll
// 推荐全局单例,存放与UI关联的Task属性。哪一个任务的引用赋值到此单例的Task属性,哪一个任务就被监控。
public class MonitorTasks : ObservableObject
{
public static MonitorTasks Instance = new MonitorTasks();
private MonitorTasks() { }
private TaskNotifier<string> _myTask;
public Task<string> MyTask
{
get => _myTask;
set => SetPropertyAndNotifyOnCompletion(ref _myTask, value);
}
}
exe
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel.Resources>
<local:TaskResultConvertrt x:Key="Trc"/>
<local:TaskStatusConverter x:Key="Tsc"/>
</StackPanel.Resources>
<StackPanel Orientation="Horizontal" Margin="5" >
<TextBlock Text="结果:"/>
<TextBox Width="150" BorderBrush="Aqua" Text="{Binding Path=MyTask ,Converter={StaticResource Trc}}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="进度:"/>
<TextBox Width="150" BorderBrush="Aqua" Text="{Binding Path=MyTask, Converter={StaticResource Tsc}}"/>
</StackPanel>
<Button Content="开启任务" Width="150" HorizontalAlignment="Left" Click="ButtonBase_OnClick" Margin="15,5,5,5"/>
</StackPanel>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = MonitorTasks.Instance;
}
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
MonitorTasks.Instance.MyTask = IsHitTestVisible();
await MonitorTasks.Instance.MyTask;
async Task<string> IsHitTestVisible()
{
await Task.Delay(1000);
return DateTime.Now.ToString(CultureInfo.InvariantCulture);
}
}
}
public class MonitorTasks : ObservableObject
{
private TaskNotifier<string> _myTask;
public Task<string> MyTask
{
get => _myTask;
set => SetPropertyAndNotifyOnCompletion(ref _myTask, value);
}
}
class TaskResultConvertrt:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Task<string> task)
{
if (task.IsCompleted)
{
return task.Result;
}
else if(task.IsFaulted)
{
return "任务失败...";
}
else if (task.IsCanceled)
{
return "任务已取消...";
}
else
{
return "正在计算中...";
}
}
else
{
return "还未指派任务";
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
class TaskStatusConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Task<string> task)
{
return task.Status.ToString();
}
else
{
return "还未指派任务";
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
检查属性是否存在
ObservableObject.EnableVerifyPropertyName = true;
ObservableObject.EnableVerifyPropertyName = false;
private int _age;
public int Age {
get { return _age; }
set {
_age = value;
OnPropertyChanged("age"); // “笔误”,传错属性名,无法通知Age值改变
}
}
ObservableObject所有通知属性值变化的方法,都会调用void VerifyPropertyName(string propertyName)
检查开发者传递的属性名称对应的属性在数据源中是否存在,不存在时会抛出异常。这让开发者很容易发现自己的”笔误”,如上述的代码,误将Age写成age。
但检查属性是否存在会使用反射,导致程序的性能受损,ObservableObject提供了开关属性EnableVerifyPropertyName,true开启检查功能,false关闭检查功能,默认关闭。开发者应当在开发期开启,在正式发布的产品关闭。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/280632.html