具有 Observable 属性的 Model View ViewModel 的组合方法

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前, 星球 内第2个项目《仿小红书(微服务架构)》正在更新中。第1个项目:全栈前后端分离博客项目已经完结,演示地址:http://116.62.199.48/。采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 255 小节,累计 39w+ 字,讲解图:1716 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 1300+ 小伙伴加入,欢迎点击围观

MVVM 和 XAML 一起玩得非常好。在 XAML 中构建 UI 的主要构建块是能够绑定视图模型(MVVM 中的 VM,我们在这里讨论模式的一半)。一种方式或两种方式绑定,没关系。您的逻辑可以更新属性,UI 将神奇地刷新。

可绑定对象及其属性

XAML 期望视图模型满足特定条件以实现该绑定。实现 INotifyPropertyChanged 的​​类被认为 是可绑定的 ,对基础属性的任何更改都必须使用通知来正确地通知 XAML 该值需要再次获取。

考虑这个小视图模型:


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


那里有很多噪音,我们刚刚(正确)实现了一个属性。代码看起来是重复的,许多 MVVM 框架涌现出来,带有帮助类来减少噪音。一个这样的例子是 MVVMLight ,这是一个用于快速实现 MVVM 的很棒的库。我们上面的课程可以缩短为:


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


我们仍然保留了实现细节,即我们的私有字段,我们损失了一些性能,但是每个属性和类的减少是显而易见的。

通过消除对支持字段的需要,可以进一步减少此代码。你可以在 YAWL.Common.Mvvm.ViewModelBase 中找到这样的尝试:


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


为每个属性自动生成私有字段,并将其放入字典中供以后查找。进一步减少代码需要在性能部门做出小的牺牲。

然而,人们可以走得更远,使用 Fody、PostSharp 和任何其他编织器来生成通知代码,作为构建过程的一部分。我们只剩下简单明了的类(使用 Fody/PropertyChanged ):


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


简短,没有性能损失,构建时间增加很小,如果不熟悉编织框架,可能会缺乏清晰度。

派生属性

无论选择哪种方法来实现视图模型,都存在一个小问题:创建依赖于其他属性的属性。例如,可以像这样实现购物车视图模型:


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


很明显, Price 只不过是 Items 属性中项目的价格总和。由于它没有 setter,因此无论何时从购物车中添加或删除商品,它都不会自行更新。我们仍然可以通过在 Items 集合上添加一个事件处理程序来手动触发更新,该事件处理程序将在它发生更改时发出通知。代码可能看起来像这样:


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


有几种方法可以编写此类依赖项,并且有一些库可以帮助构建此类通知链。通知其他属性的变化很快变得乏味,并且通知的碎片散布在整个视图模型中。更糟糕的是,跨父/子关系的链接通知变得很麻烦,因为一切都需要手动完成。当代码演化时,这些关系如此耦合,以至于增加了维护成本。

很明显,不能从其他属性创建依赖属性和派生属性。您不能将属性传递给其他人并允许他们依赖更改。随着应用程序复杂性的增加,仅具有用于将 XAML 绑定到视图模型的单一级别的通知是相当有限的。

c := a + b 这样简单的东西不是更好吗?

有一种方法可以做到这一点。

可观察属性

ReactiveProperty ReactiveUI 的启发,我们可以构建一个能够发出更改事件并可以与其他属性组合的属性。让我们看看我们将如何从上面重构我们的代码:


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


有点冗长,不需要基类,XAML 需要从 {Binding Name} 更改为 {Binding Name.Value} ,因为值现在被包装类似于 Nullable<T> 类。要更改值,请更改内部 Value 属性。


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


好的,访问属性时增加复杂性值得吗?有些视图模型可能不会从这种方法中受益,有些可能。但是,在整个代码库中应用此模式会产生一致性。

让我们来看看我们将如何编写派生属性。熟悉 LINQ 的读者会注意到与可观察属性的相似之处。


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


Reduce 是一种扩展方法,它将自身附加到 ObservableCollection 并监视任何更改。给一个 Func<IEnumerable<T>, R> 类型的 reducer lambda 计算初始值。当原始集合发生变化时,将执行重新计算并且目标属性将自行更新。

代码简洁、一致,并以声明方式写在一个地方。通过将派生属性初始化放在一个地方,如果他们想了解它何时更改,则不再需要扫描整个文件或搜索引用。它还可以防止在值更改时忘记更新依赖/派生属性。程序员更少的工作意味着更好的代码。这段代码也是反应式的,这在我们想要解耦事物时很重要。

让我们看看其他组合运算符。


 public class UserViewModel : INotifyPropertyChanged
{
    // Implementing INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
// Cache across instances
private static readonly PropertyChangedEventArgs NameChangedArgs
    = new PropertyChangedEventArgs(nameof(Name));
 
private string _name;
 
public string Name
{
    get { return _name; }
    set
    {
        if (_name != value)
        {
            _name = value;
            PropertyChanged?.Invoke(this, NameChangedArgs);
        }
    }
}

}


请注意,最后一个示例展示了如何自然而简洁地组合布尔属性。

Map 创建一个新的依赖属性,给定一个转换函数确保目标属性始终是原始值的转换版本。
Combine 可以通过将多个值与指定的组合器函数组合来构建新值。

熟悉 Rx 的读者会注意到,这些是在流中使用的相同基础块。在已经提到的 ReactiveUI 库中可以找到类似的实现。这种风格实际上是受函数式编程的启发,试图用通用的操作和简单的构建块来构建复杂的特性。

在下一篇文章中,我们将研究如何转换 MVVM 中的其他构建块,例如命令。

ObservableProperty 可以在 github 上找到: YAWL.Composition.ObservableProperty

最后由 更新于 .