Community.MvvmToolkit教程


通知单个属性值改变

  1. 不传参

    private string _firstName;
    
    public string FirstName {
        get { return _firstName; }
        set {
            _firstName = value;
            OnPropertyChanged(); // 不传参,CallerMemberNameAttribute修饰方法参数,不传参时默认实参是调用方的标识符
        }
    }
    
  2. nameof

    private string _lastName;
    
    public string LastName {
        get { return _lastName; }
        set {
            _lastName = value;
            OnPropertyChanged(nameof(LastName)); // nameof(属性标识符)
        }
    }
    
  3. Expression Tree

    private int _age;
    
    public int Age {
        get { return _age; }
        set {
            _age = value;
            OnPropertyChanged(() => Age); // Expression Tree
        }
    }
    
  4. 常量字符串

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

17点13分 2022年4月10日

检查属性是否存在

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

(0)
上一篇 2022年8月15日
下一篇 2022年8月15日

相关推荐

发表回复

登录后才能评论