--首发于博客园, 转载请保留此链接 博客原文地址
业务逻辑与界面的分离对于维护与迁移是非常重要的,在界面上给某属性赋值,后台要检测到其已经发生变化
问题:
输入某物品 单价 Price, 数量Amount, 要求自动计算总价,即: TotalPrice = Price * Amount, 如下图:
普通的实现方式
TextBox.TextChanged() 事件中可以检测到值发生改变,并且可以给其他的 TextBox.Text 赋值,但是如果 TextBox 太多,给每个 TextBox 加这样的一个事件工作量会比较大。
下面介绍另外一种方法:
使用 DataBindings 与 INotifyPropertyChanged 配合可以轻松实现这个需求。
Step 1. 先写一个类 NotifyPropertyChanged 继承 INotifyPropertyChanged 实现 OnPropertyChanged , 当界面添加 PropertyChanged 的事件时可以实现刷新
public class NotifyPropertyChanged : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public bool SuppressNotifyPropertyChanged { get; set; } protected virtual void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null && !SuppressNotifyPropertyChanged) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } protected virtual void OnPropertyChanged<T>(Expression<Func<T>> expr) { this.OnPropertyChanged(Utils.GetMemberName(expr)); } /// <summary> /// 如果没有其他的业务逻辑,对 lambda 表达式比较熟悉的同学可以考虑用以下方法实现属性名称传递 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="propField"></param> /// <param name="value"></param> /// <param name="expr"></param> 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); } } public class Utils { public static string GetMemberName<T>(Expression<Func<T>> expr) { var bodyExpr = expr.Body as System.Linq.Expressions.MemberExpression; if (bodyExpr == null) return string.Empty; return bodyExpr.Member.Name; } }
Step 2. 写一个类 实现 Price, Amount, TotalPrice 的逻辑, 当然这个类继承 NotifyPropertyChanged
public class TestBindingClass : NotifyPropertyChanged { #region Properties private decimal _price; public decimal Price { get { return _price; } set { _price = value; _totalPrice = Amount * Price; OnPropertyChanged(() => this.Price); OnPropertyChanged(() => this.TotalPrice); } } private decimal _amount; public decimal Amount { get { return _amount; } set { _amount = value; _totalPrice = Amount * Price; OnPropertyChanged(() => this.Amount); OnPropertyChanged(() => this.TotalPrice); } } private decimal _totalPrice; public decimal TotalPrice { get { return _totalPrice; } set { _totalPrice = value; if (Amount != 0) _price = TotalPrice / Amount; // Note:don't call method Price_Set, or it goes into an infinite loop OnPropertyChanged(() => this.TotalPrice); OnPropertyChanged(() => this.Price); } } #endregion }
Step 3. 界面绑定相关属性,这样就给相关的属性加了 PropertyChanged 事件
public partial class Form1 : Form { public Form1() { InitializeComponent(); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); textBox1.DataBindings.Add(ControlBindingProperty.Text, myClass, () => myClass.Price); textBox2.DataBindings.Add(ControlBindingProperty.Text, myClass, () => myClass.Amount); textBox3.DataBindings.Add(ControlBindingProperty.Text, myClass, () => myClass.TotalPrice); } TestBindingClass myClass = new TestBindingClass(); } public static class GUIUtils { public static void Add<T>(this ControlBindingsCollection bindings, string propertyName, object dataSource, Expression<Func<T>> expr) { string dataMember = Utils.GetMemberName(expr); bindings.Add(propertyName, dataSource, dataMember); } } public static class ControlBindingProperty { public const string Text = "Text"; }
总结:
(1). 在其他属性的 set 方法里给其他属性赋值时最好用小写的属性名,而不要直接调用 OtherProperty_set 方法, 否则容易进入死循环
(2). 可以看到step 3 里,添加了 ControlBindingsCollection 的扩展方法,这样就不用担心"PropertyName" 之类容易出错的看起来很恶心的写法,事实上 IDE 的提示功能使 lambda 表达式写起来非常方便,并且在 Build 的时候就可以查出属性名是否对应,提高写代码的效率,减少出错的机会