WPF的数据绑定是一个很强大的功能,TwoWay的绑定模式,让程序员不用编写一行代码,就可以实现程序界面(UI)与后台数据的同步。比如说后台数据的People.Name属性变化了,程序界面中显示People.Name的文本框控件就自动更新,反过来,文本框里面的数据被用户编辑了以后,后台数据的People.Name也自动更新成编辑过后的数据。很好,很强大,但问题是,如果用户在程序界面上更改文本框中的文本后,想取消编辑操作(Undo功能)怎么办?
一般来说,编辑数据的程序界面至少有两个按钮,例如下图:
用户在界面上做出修改以后,点击“确定”按钮,程序才会将内存中对象实例的数据更新成程序界面上的数据,点击“取消”的话,程序界面上的数据就丢掉了,内存中的对象不会发生任何改变。
然而数据绑定的TwoWay模式的功能太强大了,强大到根本不给我们“取消”的机会,只要用户的鼠标脱离文本框(例如“姓名”文本框),People.Name属性的数据就自动更新了。哼……那就不要用TwoWay模式了,用OneWay模式好了。使用OneWay模式,在创建编辑界面的时候,用户点击“确定”按钮的时候,编写类似的代码把UI的数据搬到对象上:
People.Name = nameTextBox.Text;
People.Birthday = DateTime.Parse(birthdayTextbox.Text);
…
呃……这样解决方案好像要为程序中每个对象编写类似的代码,也不是很好。
有没有办法即使用TwoWay的绑定模式,这样我们就不需要编写代码手工维护界面的数据和内存中对象实例的数据的同步工作了,然而同时又能提供回滚(Undo)的操作呢?经过一阵子搜索和研究之后,我发现WPF的数据绑定功能实际上是通过TypeDescriptor实现的,WPF数据绑定通过TypeDescriptor来发现一个类型里面定义了什么属性(原理我会在后续的文章里讲到), 而TypeDescriptor允许程序员插入一个自定义的TypeDescriptor。于是解决方案的思路:
1. 定义一个通用的ModelBase
2. ModelBase
namespace System.ComponentModel { // Summary: // Provides functionality to commit or rollback changes to an object that is // used as a data source. public interface IEditableObject { // Summary: // Begins an edit on an object. void BeginEdit(); // // Summary: // Discards changes since the last System.ComponentModel.IEditableObject.BeginEdit() // call. void CancelEdit(); // // Summary: // Pushes changes since the last System.ComponentModel.IEditableObject.BeginEdit() // or System.ComponentModel.IBindingList.AddNew() call into the underlying object. void EndEdit(); } } |
ModelBase
private void OkButton_OnClick(object sender, RoutedEventArgs e) { ((IEditableObject)DataContext).EndEdit(); } |
而“取消”按钮的事件处理函数只需要调用CancelEdit函数就可以取消修改了:
private void CancelButton_OnClick(object sender, RoutedEventArgs e) { ((IEditableObject)DataContext).CancelEdit(); } |
3. ModelBase
4. 当用户在程序界面上修改某个属性(比如Name属性)的值的时候,WPF的数据绑定将值传给ModelBase
5. 当EndEdit函数调用的时候,ModelBase
这里是整个项目的源代码,其中ViewModel文件夹里面的代码是关键代码,下一篇文章再注释里面的关键代码。
/Files/killmyday/EditableObject.zip