2012年10月3日星期三

WPF: MVVM中ViewModel数据存储/序列化的尴尬

WPF: MVVM中ViewModel数据存储/序列化的尴尬

示例程序,当然是用MVVM模式写的,支持ViewModel数据的存储,读取,和清空。用户点击“存储”后,整个ViewModel的数据会被存储到硬盘中,连列表中的选中项目也会被记录。选择“清空”后,整个ViewModel会被清空,那么界面上TextBox值为空,ListBox也不会包含任何数据。最后选择“读取”后,之前存储的数据会立刻被还原。

image

 

具体怎么做,没有完全用.NET中的传统序列化方式,这也是为什么标题会有尴尬两字,干扰ViewModel直接序列化的因素我看到的有两点:

  • ViewModel中通常有大量的自动属性,序列化是依靠字段名称的,自动属性由编译器生成名称,因此无法保证序列化数据的通用性。
  • 一些框架的ViewModel基类没有被标记有Serializable特性。比如MVVM Light,参考这个链接。

 

因此程序在ViewModel中加入了一些数据操作的借口,提供数据的清空,读取和保存操作,数据的操作当然可以有复杂的类型,这里我们以最基础的IDictionary<string, object>做示范。当然这个IDictionary<string, object>最终会以.NET序列化的方式存储到硬盘中,因此这里做的就是把ViewModel中的数据抽象到一个中间存储中,这个中间存储是有利于序列化的,比如IDictionary<string, object>。还有一些需要注意的问题,比如会有许多ViewModel,但是只有一个IDictionary<string, object>。如果只在一个IDictionary<string, object>的根层存储的话,命名会是个问题,因此需要加入一些辅助操作来在一个IDictionary<string, object>定义另一个子IDictionary<string, object>,对应一个ViewModel中的子ViewModel,每一个ViewModel都有自己的存储空间。最后的存储和读取在多个IDictionary<string, object>中跳转。

 

那么,这样定义我们的ViewModel:

using System;

using System.Collections.Generic;

using System.Runtime.CompilerServices;

using GalaSoft.MvvmLight;

 

namespace mgen_mvvmStorage

{

    //继承MVVM Light中的ViewModelBase

    class LogicBase : ViewModelBase

    {

        //清空数据

        public virtual void ClearStates()

        { }

 

        //读取数据

        public virtual void LoadStates(IDictionary<string, object> states)

        {

            ClearStates();

        }

 

        //保存数据

        public virtual void SaveStates(IDictionary<string, object> states)

        { }

 

        //添加数据槽

        public IDictionary<string, object> NewSlot(IDictionary<string, object> states, string name)

        {

            var dic = new Dictionary<string, object>();

            states[name] = dic;

            return dic;

        }

 

        //获取数据槽

        public IDictionary<string, object> GetSlot(IDictionary<string, object> states, string name)

        {

            return (IDictionary<string, object>)states[name];

        }

 

        //利用CallerMemberName进行属性改变通知

        protected virtual bool RaisePropertyChangedEx<T>(ref T oldValue, T newValue, bool broadcast, [CallerMemberName] string prop = null)

        {

            if (!Object.Equals(oldValue, newValue))

            {

                oldValue = newValue;

                RaisePropertyChanged(prop, oldValue, newValue, broadcast);

                return true;

            }

            return false;

        }

 

    }

}

 

CallerMemberName可以参考:.NET 4.5(C#): 利用CallerMemberName特性使MVVM Light属性定义更简洁

 

数据的操作通过改写上述方法来实现,比如一个子ViewModel叫ChildLogic,该ViewModel中包含集合数据:

//+ using System.Windows.Data;

//+ using System.Collections.ObjectModel;

class ChildLogic : LogicBase

{

    ObservableCollection<int> collec;

    public ListCollectionView Collection { get; private set; }

 

    public ChildLogic()

    {

        collec = new ObservableCollection<int>();

        Collection = new ListCollectionView(collec);

    }

}

 

那么数据操作应该把每一个值对应一个名称写入到Dictionary中,同时注意把CollectionView的当前值也可以存进去:

public override void ClearStates()

{

    base.ClearStates();

    collec.Clear();

}

 

public override void LoadStates(IDictionary<string, object> states)

{

    base.LoadStates(states);

 

    //数据

    int count = (int)states["count"];

    for (int i = 0; i < count; i++)

    {

        collec.Add((int)states["item" + i]);

    }

    //当前选择值

    Collection.MoveCurrentTo(states["current"]);

}

 

public override void SaveStates(IDictionary<string, object> states)

{

    base.SaveStates(states);

 

    states["count"] = collec.Count;

    for (int i = 0; i < collec.Count; i++)

    {

        states["item" + i] = collec[i];

    }

 

    states["current"] = Collection.CurrentItem;

}

 

 

还有刚才讲的子ViewModel问题,此时则需要上面LogicBase定义的“数据槽”功能,比如MainLogic中包含自己的数据Text,同时包含一个子ViewModel:ChildLogic,那么MainLogic中的数据操作需要同时调用子ViewModel的相应方法:

#region Text

 

private string _Text = null;

public string Text

{

    get { return _Text; }

    set { RaisePropertyChangedEx(ref _Text, value, false); }

}

 

#endregion

 

 

public ChildLogic ChildLogic { get; private set; }

 

public override void ClearStates()

{

    base.ClearStates();

    //MainLogic

    Text = "";

    //ChildLogic

    ChildLogic.ClearStates();

}

 

public override void LoadStates(IDictionary<string, object> states)

{

    base.LoadStates(states);

 

    //MainLogic

    Text = (string)states["text"];

    //ChildLogic

    ChildLogic.LoadStates(GetSlot(states, "child"));

}

 

public override void SaveStates(IDictionary<string, object> states)

{

    base.SaveStates(states);

 

    //MainLogic

    states["text"] = Text;

    //ChildLogic

    ChildLogic.SaveStates(NewSlot(states, "child"));

}

 

那么最后,一切ViewModel的数据都在这个Dictionary,可以轻松使用.NET中的序列化操作来处理了,比如,数据的存储:

protected virtual async void DoSave()

{

    try

    {

        await Task.Run(() =>

            {

                var dic = new Dictionary<string, object>();

                //调用LogicBase.SaveStates

                SaveStates(dic);

                //将Dictionary序列化并压缩最终写入到硬盘(具体执行可以参考源代码)

                SerializationHelper.ToBinaryCompressed(dic, "a.bin");

            });

    }

    catch (Exception ex)

    {

        System.Windows.MessageBox.Show(ex.Message);

    }

}

 

 

当前版本的源代码下载
下载地址
注意:此为微软SkyDrive存档,请用浏览器直接下载,用某些下载工具可能无法下载
源代码环境: Microsoft Visual Studio Express 2012 for Windows Desktop
注意:源代码不包含引用的外部类库文件:MVVM Light


TAG: