聊聊WPF中INotifyPropertyChanged

一、INotifyPropertyChanged接口

在Windows Presentation Foundation(WPF)中,INotifyPropertyChanged是一个核心接口,用于实现类与视图之间的数据双向绑定。当实体类的某个属性值发生变化时,通过实现此接口可以立即通知绑定到属性的所有UI控件进行更新,ICommand主要针对的是关联到任何实现了ICommand接口的对象的方法。

在C#中,CallerMemberName是.NET框架提供的一个编译器特性(Compiler Feature),它允许你获取调用当前方法的成员名称,而无需硬编码该名称。这对于实现INotifyPropertyChanged接口特别有用,因为它可以减少手动输入属性名的工作量,提高代码的健壮性和可维护性。

不管是ICommand还是INotifyPropertyChanged都必须首先将ViewMode的实现设置为控件或整个界面的DataContext。如:

this.DataContext = new MainViewModel();

DataContext是UI层与数据逻辑层的桥梁

二、DataContext

DataContext是一个非常关键的概念,它是实现数据绑定的基础。DataContext是所有WPF控件都具有的一个依赖属性。它属于System.Windows.FrameworkElement类。这意味着所有继承自该类的控件都可以使用DataContext。

2.1/DataContext作用

DataContext作为一个容器,提供了UI层和数据层之间的连接点。在MVVM(Model-View-ViewModel)架构模式中,通常将ViewModel设置为控件或整个界面的DataContext,这样UI控件可以通过绑定直接访问ViewModel中的数据和命令。

2.2/DataContext特性

  • 继承性:DataContext具有继承特性,子控件如果没有明确设置自身的DataContext,则会从其元素继承DataContext的值。这意味着在整个控件树中可以共享同一个数据上下文对象。
  • 数据绑定:在WPF中,当你写一个数据绑定表达式如{Binding Path=PropertyName}时,默认情况下,Binding将查找当前元素的DataContext,并在其中寻找指定路径的属性。
  • 实现数据驱动视图:通过将业务逻辑对象或ViewModel对象设置为控件或整个界面的DataContext,WPF可以自动根据这些对象中的数据变化更新相关联的用户界面元素。

2.3/DataContext实例

<Window>
  <Window.DataContext>
     <local:MyViewModel/>
  </Window.DataContext>
  <StackPanel>
      <TextBox Text="{Binding Name}" />
      <Button Command="{Binding SaveCommand}" Content="Save" />
  </StackPanel>
</Window>

上面的示例,Window的DataContext被设置为了MyViewModel实例,因为TextBox和Button都可以通过数据绑定访问到MyViewModel中的Name属性和SaveCommand命令。

三、INotifyPropertyChanged接口的几种实现方式

3.1/简单INotifyPropertyChanged绑定

  1. 定义实例类并继承接口INotifyPropertyChanged
public class Person:INotifyPropertyChanged
{
    private string m_Name="默认值是XXXX";
    public string Name 
    {
        get =>m_Name;
        set
        {
           if (m_Name == value) return;
           m_Name = value;
           this.Notify("Name");
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    public void Notify(string propertyName) 
    {
       PropertyChangedEventHandler handler = this.PropertyChanged;
       if(handler!=null)
         handler(this, new PropertyChangedEventArgs(propertyName));
    }
}
  1. 界面绑定ViewModel
<Window x:Class="WpfToolkitApp.WinMvvmFirst"
        ...
        xmlns:vm="clr-namespace:WpfToolkitApp.Model">
       <Window.DataContext>
          <vm:Person></vm:Person>
       </Window.DataContext>
       <Grid>
          <Label Content="名称" HorizontalAlignment="Left" Margin="21,104,0,0" VerticalAlignment="Top"/>
          <TextBox HorizontalAlignment="Left" Margin="60,104,0,0" TextWrapping="Wrap"
              Text="{Binding Name}" 
              VerticalAlignment="Top" 
              Width="221" 
              Height="25"/>
        </Grid>       
</Window>
  1. 界面构造函数绑定DataContext
public partial class WinMvvmFirst : Window
{
    Person person = null;
    public WinMvvmFirst()
    {
        InitializeComponent();
        person = base.DataContext as Person;
    }
}

3.2/使用Lambda表达式,静态扩展语法

   public static class NotificationExtensions
    {
        public static void Notify(this PropertyChangedEventHandler eventHandler, Expression<Func<object>> expression) 
        {
            if (eventHandler == null) 
            {
                return;
            }
            var lambda = expression as LambdaExpression;
            MemberExpression memberExpression;
            if (lambda.Body is UnaryExpression)
            {
                var unaryExpression = lambda.Body as UnaryExpression;
                memberExpression = unaryExpression.Operand as MemberExpression;
            }
            else 
            {
                memberExpression = lambda.Body as MemberExpression;
            }
            var constantExpression = memberExpression.Expression as ConstantExpression;
            var propertyInfo = memberExpression.Member as PropertyInfo;

            foreach (var del in eventHandler.GetInvocationList())
            {
                del.DynamicInvoke(new object[] { constantExpression.Value, new PropertyChangedEventArgs(propertyInfo.Name) });
            }
        }
    }

静态扩展方法使用:

public class Employee : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string _trueName;
    public string TrueName
    {
        get{return this._trueName}
        set
        {
            this._trueName = value;
            this.PropertyChanged.Notify(()=>this.TrueName);
        }
    }
    
}

还可以添加一个很实用的扩展:

public static void SubscribeToChange<T>(this T objectThatNotifies, Expression<Func<object>> expression, PropertyChangedEventHandler<T> handler) where T :INotifyPropertyChanged
{
     objectThatNotifies.PropertyChanged +=(s, e) =>
     {
         var lambda = expression as LambdaExpression;
         MemberExpression memberExpression;
         if (lambda.Body is UnaryExpression)
         {
             var unaryExpression = lambda.Body as UnaryExpression;
             memberExpression = unaryExpression.Operand as MemberExpression;
         }
         else
         {
             memberExpression = lambda.Body as MemberExpression;
         }
         var propertyInfo = memberExpression.Member as PropertyInfo;
         if (e.PropertyName.Equals(propertyInfo.Name))
         {
            handler(objectThatNotifies);
         }
      };
}

3.3/Net4.5,框架提供的解决方法

private string m_myProperty;
public string MyProperty
{
    get{return m_myProperty;}
    set{
        m_myProperty = value;
        OnPropertyChanged();
    }
}
private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")
{
    
}

属性CallerMemberName的解决办法和方法二是基本相同的,不同的这个在net框架中解决的。更多信息可以查看CallerMemberName,Net4.5还提供了CallerFilePath、CallerLineNumber,稍微会详细讲解。

四、INotifyPropertyChanged总结

INotifyPropertyChanged接口用于向客户端发出某一属性值已更改的通知。在应用有两种方式OneTime模式、OneWay模式和TwoWay模式。

  • OneTime模式

OneTime模式是一个初始化一次绑定。不常用。

  • OneWay模式

绑定源的每一次变化都会通知绑定目标,但是绑定目标的改变不会改变绑定源。当绑定源的数据实体类没有实现INotifyPropertyChanged接口时,当改变了数据源,发现绑定目录的UI上的相应的数据不会立即变化。

  • TwoWay模式

TwoWay模式下,当绑定源的数据实体类没有实现INotifyPropertyChanged接口时,空间的更改会让数据源立即发改变,但是改变数据源,绑定目标控件却不会立即发送改变,所以当我们需要数据源改变时相对应的UI立即改变时,需要实现INotifyPropertyChanged接口。

05-09 07:29