为WPF程序中的数据(Model)添加编辑功能

WPF的数据绑定是一个很强大的功能,TwoWay的绑定模式,让程序员不用编写一行代码,就可以实现程序界面(UI)与后台数据的同步。比如说后台数据的People.Name属性变化了,程序界面中显示People.Name的文本框控件就自动更新,反过来,文本框里面的数据被用户编辑了以后,后台数据的People.Name也自动更新成编辑过后的数据。很好,很强大,但问题是,如果用户在程序界面上更改文本框中的文本后,想取消编辑操作(Undo功能)怎么办?

一般来说,编辑数据的程序界面至少有两个按钮,例如下图:

为WPF程序中的数据(Model)添加编辑功能_第1张图片

用户在界面上做出修改以后,点击“确定”按钮,程序才会将内存中对象实例的数据更新成程序界面上的数据,点击“取消”的话,程序界面上的数据就丢掉了,内存中的对象不会发生任何改变。

然而数据绑定的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类,用来封装任意一个可以在WPF 程序中编辑的数据(数据类型就是泛型参数T)。

2.       ModelBase实现了IEditableObject接口,这个接口的定义如下:

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类型实现了IEditableObject接口后,在WPF 程序里面,就可以在前台使用TwoWay的绑定模式,用户在界面上修改完数据后,“确定”按钮的事件处理函数只需要调用类似下面的代码,就可以将修改提交:

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通过提供一个自定义的TypeDescriptor,将定义在其内部封装的真实数据对象的属性合并到自己的类型里面,也就是说,当WPF通过在一个ModelBase的实例上查询定义在类型PeopleName属性的时候,可以查询到这个属性。对于WPF来说,Name属性是直接定义在ModelBase里面的,因为我们自定义的TypeDescriptor欺骗了WPF的数据绑定代码(当然这是WPF的数据绑定故意被骗的,周瑜打黄盖 ,一个愿打,一个愿挨)。

4.       当用户在程序界面上修改某个属性(比如Name属性)的值的时候,WPF的数据绑定将值传给ModelBase实例,ModelBase实例将这个属性的值先缓存起来,并不是直接将封装的实例的属性值修改。

5.       EndEdit函数调用的时候,ModelBase这时才将缓存的修改刷新到封装的实例上。如果CancelEdit函数被调用,ModelBase直接将缓存的修改丢掉,这样封装的实例并不会看到用户的修改。

这里是整个项目的源代码,其中ViewModel文件夹里面的代码是关键代码,下一篇文章再注释里面的关键代码。

/Files/killmyday/EditableObject.zip

你可能感兴趣的:(为WPF程序中的数据(Model)添加编辑功能)