谈谈INotifyPropertyChanged 的实现
3种数据绑定模式 OneTime(一次绑定) OneWay(单项绑定) TwoWay(双向绑定)
OneTime:仅在数据绑定创建时使用数据源更新目标。
列子:
第一步,创建数据源对象让Person类实现INotifyPropertyChanged接口,该接口具有PropertyChanged事件,PropertyChanged事件在数据源发生变化时候通知绑定
.cs
namespace SilverlightApplication2 { public class Person:INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private String _Name; public String Name { get { return this._Name; } set { this._Name = value; NotifyPropertyChanged("Name"); } } private int _Age; public int Age { get { return this._Age; } set { this._Age = value; NotifyPropertyChanged("Age"); } } private String _Address; public String Address { get { return this._Address; } set { this._Address = value; NotifyPropertyChanged("Address"); } } public void NotifyPropertyChanged(String propertyName) { if(PropertyChanged!=null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
关于INotifyPropertyChanged 参见http://www.cnblogs.com/beginor/archive/2012/08/13/2636418.html
关于
参见http://www.cnblogs.com/beginor/archive/2012/08/13/2636418.html
INotifyPropertyChanged 接口是 WPF/Silverlight 开发中非常重要的接口, 它构成了 ViewModel 的基础, 数据绑定基本上都需要这个接口。 所以, 对它的实现也显得非常重要, 下面接贴出我知道的几种实现方式, 希望能起到抛砖引玉的作用。
一般的实现方式
这是一种再普通不过的实现方式, 代码如下:
1
2
3
4
5
6
7
8
9
10
|
public class NotifyPropertyChanged : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
virtual internal protected void OnPropertyChanged( string propertyName) {
if ( this .PropertyChanged != null ) {
this .PropertyChanged( this , new PropertyChangedEventArgs(propertyName));
}
}
} |
这种方式称之为一般的实现方式, 因为它确实是太普通不过了, 而且使用起来也让人感到厌恶, 因为必须指定手工指定属性名称:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class MyViewModel : NotifyPropertyChanged {
private int _myField;
public int MyProperty {
get { return _myField; }
set {
_myField = value;
OnPropertyChanged( "MyProperty" );
}
}
} |
lambda 表达式实现方式
对 lambda 表达式比较熟悉的同学可以考虑用 lambda 表达式实现属性名称传递, 在 NotifyPropertyChanged 类添加一个这样的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
protected void SetProperty<T>( ref T propField, T value, Expression<Func<T>> expr) {
var bodyExpr = expr.Body as System.Linq.Expressions.MemberExpression;
if (bodyExpr == null ) {
throw new ArgumentException( "Expression must be a MemberExpression!" , "expr" );
}
var propInfo = bodyExpr.Member as PropertyInfo;
if (propInfo == null ) {
throw new ArgumentException( "Expression must be a PropertyExpression!" , "expr" );
}
var propName = propInfo.Name;
propField = value;
this .OnPropertyChanged(propName);
} |
有了这个方法, NotifyPropertyChanged 基类使用起来就令人舒服了很多:
1
2
3
4
5
6
7
8
9
10
11
|
public class MyViewModel : NotifyPropertyChanged {
private int _myField;
public int MyProperty {
get { return _myField; }
set {
base .SetProperty( ref _myField, value, () => this .MyProperty);
}
}
} |
这样一来, 把属性名称用字符串传递改成了用 lambda 表达式传递, 减少了硬编码, 确实方便了不少, 但是还是感觉略微麻烦了一些, 还是要写一个 lambda 表达式来传递属性名称。
拦截方式实现
如果对 Castal.DynamicProxy 有印象的话, 可以考虑使用 DynamicProxy 进行拦截实现, 我的实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// 1. 先定义一个拦截器, 重写 PostProcess 方法, 当发现是调用以 set_ 开头的方法时, // 一般就是设置属性了, 可以在这里触发相应的事件。 internal class NotifyPropertyChangedInterceptor : StandardInterceptor {
protected override void PostProceed(IInvocation invocation) {
base .PostProceed(invocation);
var methodName = invocation.Method.Name;
if (methodName.StartsWith( "set_" )) {
var propertyName = methodName.Substring(4);
var target = invocation.Proxy as NotifyPropertyChanged;
if (target != null ) {
target.OnPropertyChanged(propertyName);
}
}
}
} // 2. 再定义一个帮助类, 提供一个工厂方法创建代理类。 public static class ViewModelHelper {
private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator();
private static readonly NotifyPropertyChangedInterceptor Interceptor
= new NotifyPropertyChangedInterceptor();
public static T CreateProxy<T>(T obj) where T : class , INotifyPropertyChanged {
return ProxyGenerator.CreateClassProxyWithTarget(obj, Interceptor);
}
} |
使用起来也是很方便的, 只是创建 ViewModel 对象时必须用帮助类来创建实例, 代码如下:
1
2
3
4
5
6
7
8
9
10
|
public class MyViewModel : NotifyPropertyChanged {
// 定义属性时不需要任何基类方法, 和普通属性没有什么两样。
public int MyProperty {
get ; set ;
}
} // 使用时需要这样创建实例: var viewModel = ViewModelHelper.CreateProxy<MyViewModel>();
viewModel.MyProperty = 100; |
不过这种实现的缺点就是所有的属性都会触发 PropertyChanged 事件, 而且只能触发一个事件, 而在实际开发中, 偶尔需要设置一个属性, 触发多个 PropertyChanged 事件。
未来 .Net 4.5 的实现方式
在即将发布的 .Net 4.5 中, 提供了 标记, 利用这个属性, 可以将上面提供的 SetProperty 方法进行改造, 这样的实现才是最完美的:
1
2
3
4
5
6
|
protected void SetProperty<T>( ref T storage, T value, [CallerMemberName] String propertyName = null ) {
if ( object .Equals(storage, value)) return ;
storage = value;
this .OnPropertyChanged(propertyName);
} |
由于有了 CallerMemberName 标记助阵, 可以说使用起来是非常方便了:
1
2
3
4
5
6
7
8
9
10
11
|
public class MyViewModel : NotifyPropertyChanged {
private int _myField;
public int MyProperty {
get { return _myField; }
set {
base .SetProperty( ref _myField, value);
}
}
} |
这种方法虽然好,不过却只有在 .Net 4.5 中才有, 而且也许永远不会添加到 Silverlight 中。
第二步:用户界面绑定数据对象,指定绑定模式
.xaml
<Grid x:Name="LayoutRoot" Background="Wheat" Loaded="LayoutRoot_Loaded"> <StackPanel> <TextBox Grid.Row="0" Grid.Column="0" Width="150" Height="30" HorizontalAlignment="Left" Text="{Binding Name,Mode=OneTime}"/> <TextBox Grid.Row="1" Grid.Column="0" Width="150" Height="30" HorizontalAlignment="Left" Text="{Binding Age,Mode=OneTime}"/> <TextBox Grid.Row="2" Grid.Column="0" Width="150" Height="30" HorizontalAlignment="Left" Text="{Binding Address,Mode=OneTime}"/> <Button x:Name="btnUpdata" Width="150" Height="30" Content="更新" Click="btnUpdata_Click"/> </StackPanel> </Grid>
第三步:数据绑定
.xaml.cs
Person person; void LayoutRoot_Loaded(object sender,RoutedEventArgs e) { person = new Person() { Name="Terry", Age=20, Address="Beijing" }; this.LayoutRoot.DataContext = person; } private void btnUpdata_Click(object sender, RoutedEventArgs e) { person.Name = "小哥"; person.Age = 23; person.Address = "上海"; }
由于是OneTime数据绑定模式,可以看出在单机更新按钮时,尽管改变了数据对象的属性值,但是用户界面的数据值依然是在绑定创建时候的数据值。