文章目录
- 引言
- INotifyPropertyChanged接口基础
- 接口定义
- 工作原理
- 基本实现方式
- 标准实现示例
- CallerMemberName特性
- 高级实现技术
- 基类实现
- 通知多个属性变化
- 使用PropertyChanging事件
- MVVM框架中的实现
- MVVM模式简介
- MVVM框架中的实现
- Prism框架
- MVVM Light框架
- 自定义MVVM基类
- 性能优化技术
- 避免不必要的通知
- 使用"空字符串"通知所有属性变化
- 使用WeakEventManager
- 条件编译
- 批量更新模式
- 实际应用示例
- WPF数据绑定示例
- MVVM模式示例
- 调试INotifyPropertyChanged
- 诊断绑定错误
- 常见问题与解决方案
- INotifyPropertyChanged与依赖属性比较
- 何时使用INotifyPropertyChanged
- 何时使用依赖属性
- 总结与最佳实践
- 总结
- 最佳实践
- 学习资源
可以根据Github拉取示例程序运行
GitHub程序演示地址(点击直达)
也可以在本文资源中下载
引言
在WPF应用程序开发中,数据绑定是最核心的特性之一,它使视图层(UI)和数据层能够自动同步。而INotifyPropertyChanged接口则是实现这种同步机制的基础,它允许对象在属性值变化时通知UI进行更新。本文将深入探讨INotifyPropertyChanged接口的实现与应用,帮助开发者构建响应式的WPF应用程序。
INotifyPropertyChanged接口基础
接口定义
INotifyPropertyChanged接口是.NET Framework中System.ComponentModel命名空间下的一个简单接口,它只包含一个事件:
// INotifyPropertyChanged接口定义
namespace System.ComponentModel
{// 该接口用于通知客户端属性值已更改public interface INotifyPropertyChanged{// 当属性值更改时发生event PropertyChangedEventHandler PropertyChanged;}// 属性更改事件的委托public delegate void PropertyChangedEventHandler(object sender, // 发送通知的对象PropertyChangedEventArgs e // 包含更改的属性名称);
}
工作原理
当一个类实现了INotifyPropertyChanged接口,并在属性值发生变化时触发PropertyChanged事件,WPF的绑定系统就能够捕获这个事件,并自动更新绑定到该属性的UI元素。
基本实现方式
标准实现示例
下面是一个实现INotifyPropertyChanged接口的基本示例:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;namespace WpfDemo
{// 实现INotifyPropertyChanged接口以支持WPF数据绑定public class Person : INotifyPropertyChanged{// INotifyPropertyChanged接口的事件public event PropertyChangedEventHandler PropertyChanged;// 私有字段private string _name;private int _age;// 姓名属性public string Name{get => _name;set{// 检查值是否变化if (_name != value){// 设置新值_name = value;// 通知UI该属性已变化OnPropertyChanged();}}}// 年龄属性public int Age{get => _age;set{if (_age != value){_age = value;OnPropertyChanged();}}}// 属性变化通知方法protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){// 调用事件处理器PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}
}
CallerMemberName特性
在上述示例中,我们使用了CallerMemberName
特性,这是C# 5.0引入的一个非常有用的特性,它可以自动获取调用方法的成员名称,无需手动指定属性名称字符串,减少了潜在的错误。
// 不使用CallerMemberName时的写法
protected virtual void OnPropertyChanged(string propertyName)
{PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}// 调用时需要显式指定属性名称
OnPropertyChanged("Name");// 使用CallerMemberName后的简化调用
OnPropertyChanged(); // 编译器会自动传入调用该方法的属性名称
高级实现技术
基类实现
在实际开发中,我们通常会创建一个实现了INotifyPropertyChanged接口的基类,然后让其他模型类继承这个基类:
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;namespace WpfDemo
{// 实现INotifyPropertyChanged的基类public abstract class NotifyPropertyBase : INotifyPropertyChanged{// INotifyPropertyChanged接口事件public event PropertyChangedEventHandler PropertyChanged;// 属性变化通知方法protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}// 设置属性值并通知变化的辅助方法protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null){// 如果值没有变化,返回falseif (EqualityComparer<T>.Default.Equals(field, value))return false;// 设置新值field = value;// 通知属性变化OnPropertyChanged(propertyName);return true;}}// 使用基类的示例public class Employee : NotifyPropertyBase{private string _name;private double _salary;public string Name{get => _name;set => SetProperty(ref _name, value);}public double Salary{get => _salary;set => SetProperty(ref _salary, value);}}
}
通知多个属性变化
有时候,更改一个属性可能会影响其他属性的值,这种情况下我们需要通知多个属性的变化:
using System;public class Circle : NotifyPropertyBase
{private double _radius;// 半径属性public double Radius{get => _radius;set{if (SetProperty(ref _radius, value)){// 半径变化后,面积和周长也会变化,需要同时通知OnPropertyChanged(nameof(Area));OnPropertyChanged(nameof(Perimeter));}}}// 面积属性(只读,依赖于半径)public double Area => Math.PI * _radius * _radius;// 周长属性(只读,依赖于半径)public double Perimeter => 2 * Math.PI * _radius;
}
使用PropertyChanging事件
除了PropertyChanged事件外,.NET还提供了PropertyChanging事件,它在属性值即将变化之前触发,可以用于在属性值变化前执行某些操作:
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;public class AdvancedNotifyPropertyBase : INotifyPropertyChanged, INotifyPropertyChanging
{// 属性变化前事件public event PropertyChangingEventHandler PropertyChanging;// 属性变化后事件public event PropertyChangedEventHandler PropertyChanged;// 属性变化前通知方法protected virtual void OnPropertyChanging([CallerMemberName] string propertyName = null){PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));}// 属性变化后通知方法protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}// 设置属性值并通知变化的辅助方法(包含变化前和变化后通知)protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null){// 如果值没有变化,返回falseif (EqualityComparer<T>.Default.Equals(field, value))return false;// 通知属性即将变化OnPropertyChanging(propertyName);// 设置新值field = value;// 通知属性已经变化OnPropertyChanged(propertyName);return true;}
}
MVVM框架中的实现
MVVM模式简介
MVVM (Model-View-ViewModel) 是WPF应用程序中常用的设计模式,它将应用程序分为三个主要部分:
- Model:表示业务数据和逻辑
- View:表示用户界面
- ViewModel:作为View和Model之间的中介
在MVVM模式中,ViewModel几乎总是需要实现INotifyPropertyChanged接口,以便在数据变化时通知View更新。
MVVM框架中的实现
许多MVVM框架(如Prism、MVVM Light、Caliburn.Micro等)都提供了INotifyPropertyChanged的实现基类,使开发者能够更容易地创建符合MVVM模式的应用程序。以下是一些流行MVVM框架中的实现示例:
Prism框架
using Prism.Mvvm;// Prism框架提供的BindableBase基类已实现INotifyPropertyChanged
public class UserViewModel : BindableBase
{private string _username;private string _email;public string Username{get => _username;set => SetProperty(ref _username, value);}public string Email{get => _email;set => SetProperty(ref _email, value);}
}
MVVM Light框架
using GalaSoft.MvvmLight;// MVVM Light提供的ViewModelBase基类
public class ProductViewModel : ViewModelBase
{private string _productName;private decimal _price;public string ProductName{get => _productName;set => Set(ref _productName, value);}public decimal Price{get => _price;set => Set(ref _price, value);}
}
自定义MVVM基类
在没有使用MVVM框架的情况下,我们也可以创建自己的ViewModel基类:
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;namespace CustomMvvm
{// 自定义ViewModel基类public abstract class ViewModelBase : INotifyPropertyChanged{// INotifyPropertyChanged接口实现public event PropertyChangedEventHandler PropertyChanged;// 属性变化通知protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}// 设置属性值protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null){if (EqualityComparer<T>.Default.Equals(storage, value))return false;storage = value;OnPropertyChanged(propertyName);return true;}// 通知多个属性变化protected void OnPropertiesChanged(params string[] propertyNames){if (propertyNames == null)return;foreach (var name in propertyNames){OnPropertyChanged(name);}}}// 使用自定义ViewModel基类的示例public class CustomerViewModel : ViewModelBase{private string _firstName;private string _lastName;private string _fullName;public string FirstName{get => _firstName;set{if (SetProperty(ref _firstName, value)){UpdateFullName();}}}public string LastName{get => _lastName;set{if (SetProperty(ref _lastName, value)){UpdateFullName();}}}public string FullName{get => _fullName;private set => SetProperty(ref _fullName, value);}// 更新全名private void UpdateFullName(){FullName = $"{FirstName} {LastName}";}}
}
性能优化技术
避免不必要的通知
在实现INotifyPropertyChanged时,应避免不必要的通知,只有在属性值确实发生变化时才触发PropertyChanged事件:
public string Name
{get => _name;set{// 检查值是否变化,避免不必要的通知if (_name != value){_name = value;OnPropertyChanged();}}
}
使用"空字符串"通知所有属性变化
在某些情况下,我们可能需要通知对象的所有属性都已变化,这时可以使用空字符串作为属性名:
// 通知所有属性已变化
public void RefreshAllProperties()
{// 传递空字符串表示所有属性都已变化OnPropertyChanged(string.Empty);
}
不过,这种方法会导致所有绑定都重新计算,可能会影响性能,应谨慎使用。
使用WeakEventManager
在某些情况下,PropertyChanged事件可能会导致内存泄漏,特别是当订阅者比发布者生命周期更长时。使用WeakEventManager可以避免这个问题:
using System.ComponentModel;
using System.Windows;public class WeakNotifyPropertyChanged : INotifyPropertyChanged
{// 显式实现事件,防止外部直接订阅event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged{add => WeakEventManager<WeakNotifyPropertyChanged, PropertyChangedEventArgs>.AddHandler(this, "PropertyChanged", value);remove => WeakEventManager<WeakNotifyPropertyChanged, PropertyChangedEventArgs>.RemoveHandler(this, "PropertyChanged", value);}// 触发事件protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){WeakEventManager<WeakNotifyPropertyChanged, PropertyChangedEventArgs>.DeliverEvent(this, "PropertyChanged", new PropertyChangedEventArgs(propertyName));}
}
条件编译
在调试时启用更多验证,但在发布版本中禁用,可以提高性能:
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{#if DEBUG// 在调试版本中进行额外验证if (propertyName == null)throw new ArgumentNullException(nameof(propertyName));#endifif (EqualityComparer<T>.Default.Equals(field, value))return false;field = value;OnPropertyChanged(propertyName);return true;
}
批量更新模式
当需要更新多个属性但只想发送一个通知时,可以使用批量更新模式:
public class BatchNotifyModel : INotifyPropertyChanged
{public event PropertyChangedEventHandler PropertyChanged;private bool _isBatchUpdate = false;private HashSet<string> _pendingNotifications = new HashSet<string>();// 开始批量更新public void BeginUpdate(){_isBatchUpdate = true;_pendingNotifications.Clear();}// 结束批量更新并发送通知public void EndUpdate(){_isBatchUpdate = false;// 发送所有待处理的通知foreach (var propertyName in _pendingNotifications){OnPropertyChanged(propertyName);}_pendingNotifications.Clear();}// 属性变化通知protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){if (_isBatchUpdate){// 批量更新模式下,只记录属性名,不立即通知_pendingNotifications.Add(propertyName);}else{// 正常模式下,立即通知PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}// 使用示例public void UpdateCustomer(string firstName, string lastName, string email){BeginUpdate();FirstName = firstName;LastName = lastName;Email = email;EndUpdate();}// 属性定义省略...
}
实际应用示例
WPF数据绑定示例
下面是一个使用INotifyPropertyChanged接口实现数据绑定的完整示例:
// Model类定义
using System.ComponentModel;
using System.Runtime.CompilerServices;namespace WpfBindingDemo
{// 用户模型类public class User : INotifyPropertyChanged{private string _username;private string _email;private int _age;public event PropertyChangedEventHandler PropertyChanged;public string Username{get => _username;set{if (_username != value){_username = value;OnPropertyChanged();}}}public string Email{get => _email;set{if (_email != value){_email = value;OnPropertyChanged();}}}public int Age{get => _age;set{if (_age != value){_age = value;OnPropertyChanged();}}}protected void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}
}// MainWindow.xaml.cs
using System.Windows;namespace WpfBindingDemo
{public partial class MainWindow : Window{// 创建一个用户对象作为DataContextprivate User _currentUser = new User{Username = "张三",Email = "zhangsan@example.com",Age = 25};public MainWindow(){InitializeComponent();// 设置DataContextthis.DataContext = _currentUser;}// 更新按钮点击事件处理private void UpdateButton_Click(object sender, RoutedEventArgs e){// 更新用户属性 - 由于实现了INotifyPropertyChanged,UI会自动更新_currentUser.Username = usernameTextBox.Text;_currentUser.Email = emailTextBox.Text;_currentUser.Age = int.Parse(ageTextBox.Text);MessageBox.Show("用户信息已更新!");}}
}
对应的XAML代码:
<Window x:Class="WpfBindingDemo.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="INotifyPropertyChanged示例" Height="350" Width="500"><Grid Margin="20"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><!-- 标题 --><TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"Text="用户信息编辑" FontSize="20" FontWeight="Bold"Margin="0,0,0,20"/><!-- 用户名 --><TextBlock Grid.Row="1" Grid.Column="0" Text="用户名:" Margin="0,5,10,5"VerticalAlignment="Center"/><TextBox x:Name="usernameTextBox" Grid.Row="1" Grid.Column="1" Margin="0,5"Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}"/><!-- 邮箱 --><TextBlock Grid.Row="2" Grid.Column="0" Text="邮箱:" Margin="0,5,10,5"VerticalAlignment="Center"/><TextBox x:Name="emailTextBox" Grid.Row="2" Grid.Column="1" Margin="0,5"Text="{Binding Email, UpdateSourceTrigger=PropertyChanged}"/><!-- 年龄 --><TextBlock Grid.Row="3" Grid.Column="0" Text="年龄:" Margin="0,5,10,5"VerticalAlignment="Center"/><TextBox x:Name="ageTextBox" Grid.Row="3" Grid.Column="1" Margin="0,5"Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}"/><!-- 更新按钮 --><Button Grid.Row="4" Grid.Column="1" Content="更新信息"Click="UpdateButton_Click" HorizontalAlignment="Left"Margin="0,15,0,0" Padding="10,5"/><!-- 信息预览 --><Border Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"BorderBrush="LightGray" BorderThickness="1" Margin="0,20,0,0"Padding="10"><StackPanel><TextBlock Text="实时预览:" FontWeight="Bold" Margin="0,0,0,10"/><TextBlock><Run Text="用户名: "/><Run Text="{Binding Username, Mode=OneWay}" FontWeight="Bold"/></TextBlock><TextBlock><Run Text="邮箱: "/><Run Text="{Binding Email, Mode=OneWay}" FontWeight="Bold"/></TextBlock><TextBlock><Run Text="年龄: "/><Run Text="{Binding Age, Mode=OneWay}" FontWeight="Bold"/></TextBlock></StackPanel></Border></Grid>
</Window>
MVVM模式示例
下面是一个使用MVVM模式的示例,展示了INotifyPropertyChanged在ViewModel中的应用:
// ViewModelBase.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;namespace MvvmDemo
{// ViewModel基类public abstract class ViewModelBase : INotifyPropertyChanged{public event PropertyChangedEventHandler PropertyChanged;protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null){if (EqualityComparer<T>.Default.Equals(field, value))return false;field = value;OnPropertyChanged(propertyName);return true;}}
}// PersonViewModel.cs
using System.Windows.Input;namespace MvvmDemo
{// 人员ViewModelpublic class PersonViewModel : ViewModelBase{private string _firstName;private string _lastName;private string _fullName;private int _age;public string FirstName{get => _firstName;set{if (SetProperty(ref _firstName, value)){UpdateFullName();}}}public string LastName{get => _lastName;set{if (SetProperty(ref _lastName, value)){UpdateFullName();}}}public string FullName{get => _fullName;private set => SetProperty(ref _fullName, value);}public int Age{get => _age;set => SetProperty(ref _age, value);}// 更新全名private void UpdateFullName(){FullName = $"{FirstName} {LastName}";}// 构造函数public PersonViewModel(){// 初始化属性FirstName = "张";LastName = "三";Age = 30;}}
}// MainWindow.xaml.cs
using System.Windows;namespace MvvmDemo
{public partial class MainWindow : Window{public MainWindow(){InitializeComponent();// 设置DataContext为ViewModelthis.DataContext = new PersonViewModel();}}
}
对应的XAML:
<Window x:Class="MvvmDemo.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="MVVM示例" Height="350" Width="500"><Grid Margin="20"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><!-- 标题 --><TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"Text="人员信息" FontSize="20" FontWeight="Bold"Margin="0,0,0,20"/><!-- 姓氏 --><TextBlock Grid.Row="1" Grid.Column="0" Text="姓氏:" Margin="0,5,10,5"VerticalAlignment="Center"/><TextBox Grid.Row="1" Grid.Column="1" Margin="0,5"Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}"/><!-- 名字 --><TextBlock Grid.Row="2" Grid.Column="0" Text="名字:" Margin="0,5,10,5"VerticalAlignment="Center"/><TextBox Grid.Row="2" Grid.Column="1" Margin="0,5"Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}"/><!-- 年龄 --><TextBlock Grid.Row="3" Grid.Column="0" Text="年龄:" Margin="0,5,10,5"VerticalAlignment="Center"/><TextBox Grid.Row="3" Grid.Column="1" Margin="0,5"Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}"/><!-- 信息预览 --><Border Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2"BorderBrush="LightGray" BorderThickness="1" Margin="0,20,0,0"Padding="10"><StackPanel><TextBlock Text="人员信息预览:" FontWeight="Bold" Margin="0,0,0,10"/><TextBlock><Run Text="全名: "/><Run Text="{Binding FullName, Mode=OneWay}" FontWeight="Bold"/></TextBlock><TextBlock><Run Text="年龄: "/><Run Text="{Binding Age, Mode=OneWay}" FontWeight="Bold"/></TextBlock></StackPanel></Border></Grid>
</Window>
调试INotifyPropertyChanged
诊断绑定错误
当数据绑定不工作时,通常是因为PropertyChanged事件没有正确触发。可以通过以下方法诊断:
- 使用DebuggerBreakpoints在OnPropertyChanged方法处设置断点
- 使用PresentationTraceSources.TraceLevel附加属性设置绑定跟踪级别
- 使用Snoop或其他WPF调试工具检查绑定
<!-- 设置绑定跟踪级别 -->
<TextBox Text="{Binding Name, PresentationTraceSources.TraceLevel=High}"/>
常见问题与解决方案
-
属性值更改但UI不更新
- 检查是否正确实现了INotifyPropertyChanged接口
- 确保在属性setter中调用了OnPropertyChanged方法
- 确认属性名称拼写正确
-
绑定路径错误
- 检查绑定表达式是否正确
- 确认DataContext设置正确
-
PropertyChanged事件触发但UI不更新
- 检查绑定模式(Mode=OneWay或TwoWay)
- 确认UI线程访问是否正确(跨线程问题)
-
命名问题
- 使用nameof操作符避免硬编码字符串
- 确保属性名与OnPropertyChanged中的参数匹配
INotifyPropertyChanged与依赖属性比较
WPF提供了两种主要的属性变更通知机制:INotifyPropertyChanged接口和依赖属性系统。以下是它们的比较:
何时使用INotifyPropertyChanged
- 数据模型类和ViewModel类
- 不需要依赖属性其他功能(如值继承、动画等)的场景
- 希望保持类的简单,不依赖于WPF框架
何时使用依赖属性
- 创建自定义WPF控件
- 需要支持样式、模板、动画等WPF功能
- 需要属性值继承的场景
总结与最佳实践
总结
INotifyPropertyChanged接口是WPF数据绑定的核心机制之一,它允许对象在属性值变化时通知UI进行更新。通过正确实现这个接口,我们可以创建响应式的应用程序,使UI与数据保持同步。
最佳实践
-
创建基类:创建一个实现INotifyPropertyChanged的基类,以便在多个模型类中重用代码
-
使用CallerMemberName:利用CallerMemberName特性自动获取属性名称,避免硬编码字符串
-
避免不必要的通知:只在属性值真正变化时才触发PropertyChanged事件
-
使用SetProperty辅助方法:简化属性setter代码,并确保一致性
-
正确处理关联属性:当一个属性变化影响其他属性时,确保通知所有受影响的属性
-
考虑性能:对于频繁变化的属性,考虑使用批量更新模式,减少通知次数
-
测试属性通知:编写单元测试确保PropertyChanged事件正确触发
-
使用工具简化实现:考虑使用Fody.PropertyChanged等工具自动注入PropertyChanged通知代码
学习资源
-
Microsoft Docs - INotifyPropertyChanged接口
-
Microsoft Docs - 数据绑定概述
-
CodeProject - WPF中的高级数据绑定
-
GitHub - WPF样例集合
-
Fody.PropertyChanged - 自动注入属性变更通知