WPF: MVVM中ViewModel数据存储/序列化的尴尬
示例程序,当然是用MVVM模式写的,支持ViewModel数据的存储,读取,和清空。用户点击“存储”后,整个ViewModel的数据会被存储到硬盘中,连列表中的选中项目也会被记录。选择“清空”后,整个ViewModel会被清空,那么界面上TextBox值为空,ListBox也不会包含任何数据。最后选择“读取”后,之前存储的数据会立刻被还原。
具体怎么做,没有完全用.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: