MVVM中实现 INotifyPropertyChanged 的 4 种方法

科技   2024-10-28 06:40   上海  


在 .NET 桌面和移动中,接口通常由数据对象(即_模型_层)实现,以便在发生更改和 UI 需要更新时通知_视图_层。对于诸如自动属性之类的琐碎情况,实现此接口非常简单,但对于计算属性或依赖于其他对象的属性,它变得更加复杂。在本文中,我们将探讨几种实现策略和工具,以简化 .INotifyPropertyChangedINotifyPropertyChanged

什么是 INotifyPropertyChanged?

该接口是 .NET Framework 的一部分,主要用于数据绑定方案。它的主要作用是为对象提供一种标准化的方式,以便在属性的值发生更改时通知订阅者,例如 UI 元素。该接口定义单个事件 ,每当更新属性的值时,都会触发该事件。通过在类中实现此接口并在属性 setter 中引发事件,可以有效地通知任何订阅者这些更改。INotifyPropertyChangedPropertyChangedPropertyChanged

当使用 Model-View-ViewModel (MVVM) 设计模式时,此机制尤其重要,该模式在 Windows Presentation Foundation (WPF) 应用程序中广泛使用。

为什么 INotifyPropertyChanged 很有用

该界面通过支持对用户界面的实时更新,在数据驱动的交互式应用程序中发挥着至关重要的作用。在 MVVM 框架中,它确保 ViewModel 属性的更改自动反映在 View 中。这种自动同步消除了在底层数据更改时手动更新 UI 的需要,从而降低了复杂性和出错的可能性。INotifyPropertyChanged

假设有一个应用程序根据矩形的宽度和高度计算矩形的面积。当用户为 width 或 height 输入新值时,应重新计算并立即显示该区域。这就是发挥作用的地方,确保在 or 属性更改时更新 UI。INotifyPropertyChangedWidthHeight

除了 UI 方案之外,在对象需要响应属性更改的上下文中(例如事件驱动的编程或数据同步任务)中也很有价值。它有助于创建一个松散耦合的响应式系统,其中组件可以对状态变化做出反应,而无需直接监视或修改对象。INotifyPropertyChanged

如何在 C# 中使用 INotifyPropertyChanged?

1. 从代码

要对代码中的属性更改做出反应,您可以订阅实现 .下面是一个示例:PropertyChangedINotifyPropertyChanged

rectangle.PropertyChanged += (_, args) => Console.WriteLine($"Property {args.PropertyName} has changed.");

在此示例中,每当对象的属性发生更改时,都会向控制台打印一条消息,指示哪个属性已更改。rectangle

2. 在 XAML 中,使用绑定

在基于 XAML 的应用程序(如 WPF 或 UWP)中,您可以将 UI 元素绑定到实现 .这允许 UI 在基础属性值更改时自动更新。下面是一个基本示例:INotifyPropertyChanged

<TextBlock Text="{Binding Path=Width}" />

在这种情况下,如果数据上下文实现并且属性发生更改,则 显示的文本将自动更新。INotifyPropertyChangedWidthTextBlock

这些方法有助于保持用户界面和数据模型有效同步,而无需对每次更改进行手动更新。

如何实现 INotifyPropertyChanged?

您有不同的选择:

  1. 手动地

  2. Metalama

  3. MVVM 社区工具包

  4. Fody.NotifyPropertyChanged

方法 1.手动编写代码

要手动实施,您需要:INotifyPropertyChanged

  • 使用命名空间。System.ComponentModel

  • 在类中定义事件。PropertyChanged

  • 创建一个受保护的方法,以使用已更改属性名称的 string 参数触发事件(可选,但强烈建议)。OnPropertyChangedPropertyChanged

  • 编辑修改对象状态并调用该方法的所有方法或属性 setter。OnPropertyChanged

让我们为面积计算器的示例类设置基础设施。Rectangle

public class Rectangle : INotifyPropertyChanged
{
//...
public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(
[CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs(propertyName));
}
}

设置事件和方法后,我们现在需要确保属性 setter 调用该方法来通知更改。在本文中,我们将介绍 4 种不同的场景,具体取决于属性的实现方式:OnPropertyChanged

  1. 简单的自动属性

  2. 计算属性,依赖于其他字段或属性

  3. 依赖于其他对象的属性

  4. 取决于基类属性的属性

1. 简单的属性

假设我们想向类中添加 and 属性。在每个属性的 setter 中,我们应该检查新值是否与当前值不同,以防止不必要的通知。如果是,则更新支持字段并调用 ,传递属性的名称。这可确保在发生任何更改时立即更新任何绑定的 UI 元素。下面是它在代码中的样子:WidthHeightRectangleOnPropertyChanged

private double _width;

public double Width
{
get => _width;
set
{
if (_width != value)
{
_width = value;
OnPropertyChanged(nameof(this.Width));
}
}
}

// Same pattern for the Height property

您甚至可以通过创建封装属性更改逻辑的帮助程序方法来稍微简化此代码。下面是如何使用此帮助程序方法重构属性的示例:Rectangle

protected void ChangeProperty<T>(ref T field, T value, 
[CallerMemberName] string? propertyName = null)
{
if (field != value)
{
field = value;
OnPropertyChanged(propertyName);
}
}

private double _width;

public double Width
{
get => _width;
set => ChangeProperty(ref _width, value);
}

但是,这个简单的场景,无论有没有帮助程序,在实际情况下很快就会变得繁琐,我们将在下一节中看到。

到目前为止,事情看起来相当简单。但是,一旦我们向对象添加了更高级的属性,它就会变得更加复杂。

2. 计算属性

为了说明这个新场景,让我们在基本类中添加一个依赖于 和 的属性。RectangleAreaWidthHeight

public double Area => this.Height * this.Width;

public double ScaledArea => this.Area * this.ScaleFactor;

当属性依赖于其他属性时,必须确保将所有相关属性通知给用户界面。在这种情况下,每次 or 属性更改时,我们都必须通知用户界面该属性也已更改。以下是在代码中执行此操作的方法:WidthHeightArea

private double _width;

public double Width
{
get
{
return this._width;
}

set
{
if ( this._width != value )
{
this._width = value;
this.OnPropertyChanged( nameof(this.Width) );
this.OnPropertyChanged( nameof(this.Area) );
this.OnPropertyChanged( nameof(this.ScaledArea) );
}
}
}

在这里,setter 不仅引发了 for 事件,还引发了 for due 它们的依赖关系。可以想象,这个手动过程很容易(而且很快)变得难以维护,因为我们定义了更多的依赖属性,在本例中为 .WidthPropertyChangedWidthAreaScaledArea

3. 取决于子对象的属性

此类属性的一个示例是当我们向依赖于属性的类添加属性时。在这种情况下,实例中用于计算矩形面积的属性是通过 .AreaRectangleCalcViewModelRectangleAreaRectangleAreaRectangleCalcViewModel

public double Area => this.Rectangle.Area;

在这种情况下,接口不会自动管理子对象中属性的更改。当子属性的值发生更改时(例如,在实例中),父对象 () 不会收到对象属性发生更改的通知,这会阻止 UI 按预期更新。INotifyPropertyChangedWidthRectangleRectangleCalcViewModelAreaRectangle

开发人员需要实现额外的逻辑来传播这些更改,这会增加复杂性和出错的可能性。

private Rectangle _rectangle = new( 10, 5 );

public Rectangle Rectangle
{
get => this._rectangle;

set
{
if ( !ReferenceEquals( value, this._rectangle ) )
{
this.UnsubscribeFromRectangle();
this._rectangle = value;
this.OnPropertyChanged( nameof(this.Rectangle) );
this.SubscribeToRectangle( this.Rectangle );
}
}
}

public double Area => this.Rectangle.Area;

private void SubscribeToRectangle( Rectangle value )
{
if ( value != null )
{
value.PropertyChanged += this.HandleRectanglePropertyChanged;
}
}

private void HandleRectanglePropertyChanged( object? sender,
PropertyChangedEventArgs e )
{
{
var propertyName = e.PropertyName;
if ( propertyName is null or nameof(this.Rectangle.Width)
or nameof(this.Rectangle.Height) )
{
this.OnPropertyChanged( nameof(this.Area) );
}
}
}
private void UnsubscribeFromRectangle()
{
if ( this._rectangle != null! )
{
this._rectangle.PropertyChanged -=
this.HandleRectanglePropertyChanged;
}
}

在此示例中,订阅对象的事件。这可确保对 ViewModel 中的属性进行更改或触发该属性的事件。手动管理此类订阅很繁琐,尤其是在处理多个子对象或更复杂的依赖项时。RectangleCalcViewModelPropertyChangedRectangleWidthHeightPropertyChangedArea

4. 取决于基类属性的属性

对于最后一种情况,让我们创建一个具有属性的超类,并创建一个 .然后,向类中添加一个属性,该属性返回的值乘以父类的值。因此,该属性将取决于 和 属性。PolygonScaleFactorRectanglePolygonScaledAreaRectangleAreaScaleFactorScaledAreaAreaScaleFactor

internal partial class Polygon : INotifyPropertyChanged
{
private double _scaleFactor = 1;

// This attribute represents a multiplier for dimensions
public double ScaleFactor
{
get
{
return this._scaleFactor;
}
set
{
if ( this._scaleFactor != value )
{
this._scaleFactor = value;
this.OnPropertyChanged( nameof(this.ScaleFactor) );
}
}
}

protected virtual void OnPropertyChanged( string propertyName )
{
this.PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}

public event PropertyChangedEventHandler? PropertyChanged;
}

当属性依赖于基类中的属性时,开发人员必须确保基类属性中的更改传播到派生类属性,从而导致更多的样板代码。让我们看看如何在类中解决这个问题:Rectangle

public double ScaledArea => this.Area * this.ScaleFactor;

protected override void OnPropertyChanged( string propertyName )
{
switch ( propertyName )
{
case nameof(this.ScaleFactor):
this.OnPropertyChanged( nameof(this.ScaledArea) );

break;
}

base.OnPropertyChanged( propertyName );
}

在此示例中,该类重写了该方法,以便在发生更改时引发事件。这种手动传播容易出错且难以维护,尤其是对于多个派生类和复杂的属性依赖项。RectangleOnPropertyChangedPropertyChangedScaledAreaScaleFactor

手动方法的局限性

虽然手动实现是一个可行的选项,但它有几个限制:INotifyPropertyChanged

  1. 样板代码

  • 重复的代码可能会使有意义的业务代码变得混乱,使其难以阅读和理解。

  • 增加由于疏忽而出错的可能性。

  • 忘记调用 property setter 的风险。OnPropertyChanged

  • 可能缺少依赖项。

2. 可扩展性

  • 随着属性和依赖项数量的增加,手动实现变得越来越复杂。

  • 随着复杂性的增加,维护起来更具挑战性。

  • 手动事件引发容易出错。

  • 缺少更新可能会导致应用程序行为不正确,难以通过测试或调试进行跟踪。

作为这两个限制的示例,请参阅在类中实现接口所需的代码量。此代码是重复的且容易出错,尤其是在处理多个属性和依赖项时。INotifyPropertyChangedRectangle

internal partial class Rectangle : Polygon
{
private double _width;

public double Width
{
get
{
return this._width;
}
set
{
if ( this._width != value )
{
this._width = value;
this.OnPropertyChanged( nameof(this.Width) );
this.OnPropertyChanged( nameof(this.Area) );
this.OnPropertyChanged( nameof(this.ScaledArea) );
}
}
}

private double _height;
public double Height
{
get
{
return this._height;
}
set
{
if ( this._height != value )
{
this._height = value;
this.OnPropertyChanged( nameof(this.Height) );
this.OnPropertyChanged( nameof(this.Area) );
this.OnPropertyChanged( nameof(this.ScaledArea) );
}
}
}
public double Area => this.Height * this.Width;
public double ScaledArea => this.Area * this.ScaleFactor;

protected override void OnPropertyChanged( string propertyName )
{
switch ( propertyName )
{
case nameof(this.ScaleFactor):
this.OnPropertyChanged( nameof(this.ScaledArea) );
break;
}
base.OnPropertyChanged( propertyName );
}

public Rectangle( double width, double height )
{
this.Width = width;
this.Height = height;
}
}

考虑添加更复杂的属性,如 、 、 等,以及这些新属性会以多快的速度使 和 属性变得混乱。PerimeterDiagonalWidthHeight

方法 2.Metalama

如上一节所述,手动实现可能容易出错且繁琐。在这种情况下,Metalama 可能非常有益。Metalama 是一种工具,允许您使用 aspects、在编译器或 IDE 中执行的特殊自定义属性来自动化代码库中的重复性任务,并动态转换源代码。INotifyPropertyChanged

您可以直接阅读我们的文章,了解如何使用 Metalama 在没有样板代码的情况下实现 INotifyPropertyChanged。但是,如果您想在这里进行简要概述,请允许我指导您完成它,因为使用 Metalama 有多种实现方法。INotifyPropertyChanged

最实用的方法是使用模式,这是 Metalama 提供的众多开源、生产就绪的方面之一。此模式旨在自动识别依赖于其他属性的属性,并为其发送更改通知。这意味着您不必为这些依赖属性、子对象或任何其他以前的情况手动触发事件,因为 aspect 会为您管理这些情况。ObservablePropertyChanged

让我使用我们之前使用的相同类来演示它,但现在采用以下模式:RectangleObservable

[Observable]
internal partial class Rectangle : Polygon
{
public Rectangle( double width, double height )
{
this.Width = width;
this.Height = height;
}

public double Width { get; set; }
public double Height { get; set; }
public double Area => this.Height * this.Width;
public double ScaledArea => this.Area * this.ScaleFactor;
}

就是这样。看看我们如何仍然可以使用我们心爱的自动属性,而不必处理复杂和重复的代码?您可能想知道,“好吧,但是派生类的依赖项呢?处理得怎么样?好吧,该模式也为您管理了这一点。ScaleFactorObservable

[Observable]
internal partial class Polygon
{
// This attribute represents a multiplier for dimensions
public double ScaleFactor { get; set; } = 1;
}

这同样适用于 .您可以观察该模式如何处理子对象的依赖关系,例如此实例中的属性。RectangleCalcViewModelObservableRectangle

[Observable]
internal partial class RectangleCalcViewModel
{
public Rectangle Rectangle { get; set; } = new( 10, 5 );

public double Area => this.Rectangle.Area;
}

虽然这看起来像魔法,但事实并非如此。这只是 Metalama 在做它的工作。在后台,Metalama 会分析您的代码以跟踪属性之间的所有托管关系。然后,它会动态生成必要的代码来为您实现接口。这样,您就可以专注于真正重要的事情:您的业务逻辑。INotifyPropertyChanged

好吧,所以你不要只听一个人的话,而是更喜欢获得不同的观点?让我们分析一下 实施时可以用来减少样板代码 的其他工具,尽管它们可能无法克服我们讨论过的所有问题。INotifyPropertyChanged

方法 3.社区工具包.Mvvm

CommunityToolkit.Mvvm 包是由 Microsoft 维护和发布的现代、高效和模块化的 MVVM 库。它是 .NET Community Toolkit 的一部分,提供了一组基本类、实用程序和帮助程序,旨在简化 .NET 应用程序中 Model-View-ViewModel (MVVM) 体系结构模式的实现。

CommunityToolkit.Mvvm 库包括一个名为 的类,该类负责接口的自动实现。它还包括 attribute,该属性将带注释的字段转换为在其值更改时发出事件的属性。ObservableObjectINotifyPropertyChangedObservablePropertyPropertyChanged

现在,让我们看看使用 attribute 的类会是什么样子:RectangleObservableProperty

internal partial class Rectangle : Polygon // Polygon inherits from ObservableObject
{
public Rectangle( double width, double height )
{
this.width = width;
this.height = height;
}

[ObservableProperty]
[NotifyPropertyChangedFor( nameof(Area) )]
[NotifyPropertyChangedFor( nameof(ScaledArea) )]
public double width;

[ObservableProperty]
[NotifyPropertyChangedFor( nameof(Area) )]
[NotifyPropertyChangedFor( nameof(ScaledArea) )]
public double height;

public double Area => this.Height * this.Width;

public double ScaledArea => this.Area * this.ScaleFactor;
}

如您所见,该属性应用于 and 字段,而该类派生自基类(通过类)。此基本设置会自动生成必要的代码,以便在 或 属性更改时触发事件。ObservablePropertywidthheightRectangleObservableObjectPolygonPropertyChangedwidthheight

请务必注意,该 应用于字段而不是属性。这是因为它通过将其名称转换为 UpperCamelCase 来生成相应的属性,并遵循正确的 .NET 命名约定。因此,该字段变为属性,height 也是如此。您可以使用该特性创建依赖于其他字段或属性的属性。此属性指定每当相关字段或属性发生更改时,哪些依赖属性应自动引发事件。ObservablePropertywidthWidthNotifyPropertyChangedForPropertyChanged

CommunityToolkit.Mvvm 方法的限制

与 的传统实现相比,使用该属性可以减少为每个属性触发事件所需的重复代码。但是,它有一些限制:INotifyPropertyChangedObservablePropertyPropertyChanged

  • 仍然需要手动干预来指定依赖属性,因此仍然存在忘记添加属性或缺少依赖项的风险。

  • 不支持对子对象的依赖关系。

不过,如果您寻求一种简单且轻量级的解决方案来最大限度地减少实施时的重复代码,那么利用 CommunityToolkit.Mvvm 库是一个很好的选择。INotifyPropertyChangedObservableProperty

方法 4.Fody.PropertyChanged

Fody 是一种流行的代码编织工具,可简化 .NET 应用程序中重复性任务的实现。Fody 可用的插件之一是 ,它会自动实现类和属性的接口。此插件对于减少样板代码和确保将属性更改自动传播到订阅者特别有用。PropertyChangedINotifyPropertyChanged

让我们看看使用插件时我们的类会是什么样子:RectangleFody.PropertyChanged

internal partial class Rectangle : Polygon  
{
public Rectangle( double width, double height )
{
this.Width = width;
this.Height = height;
}

public double Width { get; set; }
public double Height { get; set; }
public double Area => this.Height * this.Width;
public double ScaledArea => this.Area * this.ScaleFactor;
}

但是等等,接口实现在哪里?请记住,我们的类继承自 .让我们检查一下。INotifyPropertyChangedRectanglePolygon

internal partial class Polygon : INotifyPropertyChanged  
{
public event PropertyChangedEventHandler PropertyChanged;

// This attribute represents a multiplier for dimensions
public double ScaleFactor { get; set; } = 1;
}

好了,现在我们看看插件是如何工作的。你只需要在 class 中实现接口,插件将负责其余的工作。该插件会自动生成必要的代码,以便在属性更改时引发事件,无需在每个属性 setter 中手动实现。Fody.PropertyChangedINotifyPropertyChangedFody.PropertyChangedPropertyChanged

同样,与传统的 实现相比,使用该插件可显著减少样板代码并简化 .它还支持根据基类的属性(我们的计算属性)使用属性。INotifyPropertyChangedFody.PropertyChangedINotifyPropertyChangedScaledArea

Fody.PropertyChanged 方法的限制

虽然该插件是自动实现 的强大工具,但它有一些限制:Fody.PropertyChangedINotifyPropertyChanged

  • 不支持对子对象的依赖关系(前面介绍的场景 #3)。

比较

作为总结,让我们比较一下我们在本文中经历的不同方法:

在本文中,我们探讨了有效实现接口的各种方法,从而最大限度地减少手动编码。虽然手动方法当然是一种选择,但它可能会变得繁琐且容易出错,尤其是在处理复杂的属性层次结构或依赖项时。Metalama、MVVM Community Toolkit 和 Fody.PropertyChanged 等工具提供了自动实现 的替代解决方案,有助于减少样板代码。

如果你喜欢我的文章,请给我一个赞!谢谢

架构师老卢
资深软件架构师, 分享编程、软件设计经验, 教授前沿技术, 分享技术资源(每天发布电子书),每天进步一点点...
 最新文章