IeditableObject是一个通用接口,用于支持对象编辑。当我们在界面上选择一个条目,然后对其进行编辑的时候,接下来会有两种操作,一个是保持编辑结果,一个取消编辑。这就要求我们保留原始值,否则我们只能到数据库里面再次查询。IeditableObject接口的三个方法定义为我们定义了这个行为规范:
public interface IEditableObject
{
// 开始编辑,一般在此方法内创建当前对象副本
void BeginEdit();
//取消编辑,当副本恢复到当前对象,并清除副本
void CancelEdit();
// 接受编辑结果,并清除副本
void EndEdit();
}
对于IeditableObject的实现,应该满足一下要求:
-
具有NonEditableAttribute标记的属性不参与编辑
-
如果某个属性类型也实现了IeditableObject, 那么将递归调用相应编辑方法。
-
对于集合对象,如果集合对象实现了IeditableObject,将会对集合的每个项调用相应编辑方法。
-
可以查询对象是否改变,包括任何标量属性的变化,关联的IeditableObject类型的属性的变化,集合属性的变化。
下面是具体实现:
首先要定义NonEditableAttribute类:
[AttributeUsage(AttributeTargets.Property,Inherited = true, AllowMultiple = false)]
public sealed class NonEditableAttribute : Attribute {}
其次是一个辅助类,用于找到一个类型内的标量属性,可编辑对象属性和集合属性,因为这三种属性需要不同的处理方式:
internal class EditableProperty
{
public EditableProperty(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
Scalars = new List<PropertyInfo>();
Editables = new List<PropertyInfo>();
Collections = new List<PropertyInfo>();
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
//忽略定义了NonEditableAttribute的属性。
if (property.IsDefined(typeof(NonEditableAttribute), false))
{
continue;
}
//不能读的属性不参与编辑
if (!property.CanRead)
{
continue;
}
Type propertyType = property.PropertyType;
if (propertyType.IsValueType || propertyType == typeof(string))
{
//标量属性需要是值类型或者string类型,并且可写。
if (property.CanWrite)
{
Scalars.Add(property);
}
}
//可编辑对象属性是递归参与编辑流程的。
else if ((typeof(IEditableObject).IsAssignableFrom(propertyType)))
{
Editables.Add(property);
}
//集合属性也是参与编辑流程的。
else if (typeof(IList).IsAssignableFrom(propertyType))
{
Collections.Add(property);
}
}
}
public List<PropertyInfo> Scalars { get; private set; }
public List<PropertyInfo> Editables { get; private set; }
public List<PropertyInfo> Collections { get; private set; }
}
下面是可编辑对象的实现:
[Serializable]
public abstract class EditableObject : NotifiableObject, IEditableObject
{
//缓存可编辑属性,不用每次重新获取这些元数据
private static ConcurrentDictionary<Type, EditableProperty> _cachedEditableProperties;
static EditableObject()
{
_cachedEditableProperties = new ConcurrentDictionary<Type, EditableProperty>();
}
//对象的副本
private object _stub;
private bool _isEditing;
//对象是不是处于编辑状态。
[NonEditable]
public bool IsEditing
{
get { return _isEditing; }
protected set
{
if (_isEditing != value)
{
_isEditing = value;
base.OnPropertyChanged("IsEditing");
}
}
}
//获取对象是不是改变了,比如说,调用了BeginEdit但是并没有修改任何属性,对象就没有改变,
//此时不需要保存,检查修改的时候内部做了对象相互引用造成的无穷递归情况。所以即使对象有相互应用
//也能正确检测。
[NonEditable]
public bool IsChanged
{
get
{
return GetIsChanged(new HashSet<EditableObject>());
}
}
//开始编辑
public void BeginEdit()
{
//如果已经处于编辑状态,那么什么也不做。
if (IsEditing)
{
return;
}
IsEditing = true;
//创建对象副本。
if (this is ICloneable)
{
ICloneable cloneable = this as ICloneable;
_stub = cloneable.Clone();
}
else
{
_stub = MemberwiseClone();
}
var editableProp = GetEditableProperty();
//对于每个管理的IeditableObject,递归调用BeginEdit
foreach (var item in editableProp.Editables)
{
var editableObject = item.GetValue(this, null) as IEditableObject;
if (editableObject != null)
{
editableObject.BeginEdit();
}
}
//对于集合属性中,如果任何项是IeditableObject,那么递归调用BeginEdit。
foreach (PropertyInfo collProperty in editableProp.Collections)
{
IList coll = collProperty.GetValue(this, null) as IList;
if (coll != null)
{
foreach (IEditableObject editableObject in coll.OfType<IEditableObject>())
{
editableObject.BeginEdit();
}
}
}
}
//取消编辑
public void CancelEdit()
{
//如果没有处于编辑状态,就什么也不做。
if (!IsEditing)
{
return;
}
IsEditing = false;
var editableProp = GetEditableProperty();
//还原标量属性的值。
foreach (PropertyInfo scalarProperty in editableProp.Scalars)
{
scalarProperty.SetValue(this,scalarProperty.GetValue(_stub, null), null);
}
//对于IeditableObject属性,递归调用CancelEdit
foreach (PropertyInfo editableProperty in editableProp.Editables)
{
IEditableObject editableObject = editableProperty.GetValue(this, null) as IEditableObject;
if (editableObject != null)
{
editableObject.CancelEdit();
}
}
foreach (PropertyInfo collProperty in editableProp.Collections)
{
IList collOld = collProperty.GetValue(_stub, null) as IList;
IList collNew = collProperty.GetValue(this, null) as IList;
//如果两个集合不相同,那么就恢复原始集合的引用。
if (!object.ReferenceEquals(collOld, collNew))
{
collProperty.SetValue(this, collOld, null);
}
//对原始集合中每个IeditableObject,递归调用CancelEdit
if (collOld != null)
{
foreach (IEditableObject editableObject in collOld.OfType<IEditableObject>())
{
editableObject.CancelEdit();
}
}
}
//清除副本
_stub = null;
}
public void EndEdit()
{
//如果没有处于编辑状态,就什么也不做。
if (!IsEditing)
{
return;
}
IsEditing = false;
var editableProp = GetEditableProperty();
//对于每个IeditableObject属性,递归调用EndEdit
foreach (PropertyInfo editableProperty in editableProp.Editables)
{
IEditableObject editableObject = editableProperty.GetValue(this, null) as tableObject;
if (editableObject != null)
{
editableObject.EndEdit();
}
}
//对于集合属性中每个项,如果其是IeditableObject,则递归调用EndEdit
foreach (PropertyInfo collProperty in editableProp.Collections)
{
IList collNew = collProperty.GetValue(this, null) as IList;
if (collNew != null)
{
foreach (IEditableObject editableObject in collNew.OfType<IEditableObject>())
{
editableObject.EndEdit();
}
}
}
//清除副本
_stub = null;
}
private bool GetIsChanged(HashSet<EditableObject> markedObjects)
{
//如果没有在编辑状态,那么表示对象没有改变
if (!IsEditing)
{
return false;
}
//如果对象已经被检查过了,说明出现循环引用,并且被检查过的对象没有改变。
if (markedObjects.Contains(this))
{
return false;
}
var editableProp = GetEditableProperty();
//检测标量属性有没有变化。
foreach (PropertyInfo scalarProperty in editableProp.Scalars)
{
object newValue = scalarProperty.GetValue(this, null);
object oldValue = scalarProperty.GetValue(_stub, null);
bool changed = false;
if (newValue != null)
{
changed =!newValue.Equals(oldValue);
}
else if (oldValue != null)
{
changed = true;
}
if (changed)
{
return true;
}
}
//标记此对象已经被检查过
markedObjects.Add(this);
//对于每一个IeditableObject属性,进行递归检查
foreach (PropertyInfo editableProperty in editableProp.Editables)
{
EditableObject editableObject = editableProperty.GetValue(this, null) as EditableObject;
if (editableObject != null)
{
if (editableObject.GetIsChanged(markedObjects))
{
return true;
}
}
}
//检查集合对象的想等性
foreach (PropertyInfocollectionProperty in editableProp.Collections)
{
IList empty = new object[0];
IList collOld = (collectionProperty.GetValue(_stub, null) as IList) ?? empty;
IList collNew = (collectionProperty.GetValue(this, null) as IList) ?? empty;
if (!object.ReferenceEquals(collOld, collNew))
{
//Detectif elements are added or deleted in Collection.
if (!collOld.Cast<object>().SequenceEqual(collNew.Cast<object>()))
{
return true;
}
}
//Detectif any element is changed in collection.
foreach (var item in collNew)
{
EditableObject editableObject = item as EditableObject;
if (editableObject != null)
{
if (editableObject.GetIsChanged(markedObjects))
{
return true;
}
}
}
}
return false;
}
private EditableProperty GetEditableProperty()
{
return _cachedEditableProperties.GetOrAdd(GetType(), t => new EditableProperty(t));
}
}
在WPF程序里面,大部分业务对象都要实现InotifyPropertyChanged以便数据绑定,所以我们实现了这个接口,并让EditableObject从这个实现派生,从而让Editableobject也具有绑定支持。NotifiableObject类非处简单,如下:
[Serializable]
public abstract class NotifiableObject : INotifyPropertyChanged
{
private const string ERROR_MSG = "{0}is not a public property of {1}";
private static readonly ConcurrentDictionary<string, PropertyChangedEventArgs> _eventArgCache;
static NotifiableObject()
{
//缓存PropertyChangedEventArgs,以提高性能。
_eventArgCache = new ConcurrentDictionary<string, PropertyChangedEventArgs>();
}
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
public static PropertyChangedEventArgs GetPropertyChangedEventArgs(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentException("propertyName cannotbe null or empty.");
}
return _eventArgCache.GetOrAdd(propertyName, p => new PropertyChangedEventArgs(p));
}
protected void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
VerifyProperty(propertyName);
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
var args = GetPropertyChangedEventArgs(propertyName);
handler(this, args);
}
}
[Conditional("DEBUG")]
private void VerifyProperty(string propertyName)
{
Type type = GetType();
PropertyInfo propInfo = type.GetProperty(propertyName);
if (propInfo == null)
{
Debug.Fail(string.Format(ERROR_MSG, propertyName, type.FullName));
}
}
}
下面的单元测试代码对EditableObject进行的简单的测试:
[TestClass]
public class EditableObjectTest
{
[TestMethod]
public void UseEditableObject_WithoutCallingMethodsOfIEditableObject()
{
User u = new User() { Name = "john", Age = 20, Wage = 200 };
Assert.AreEqual(u.Name, "john");
Assert.AreEqual(u.Age, 20);
Assert.AreEqual(u.Wage, 200);
Assert.IsFalse(u.IsChanged);
Assert.IsFalse(u.IsEditing);
u.Age = 21;
u.Wage = 250;
Assert.AreEqual(u.Name, "john");
Assert.AreEqual(u.Age, 21);
Assert.AreEqual(u.Wage, 250);
}
[TestMethod]
public void BeginEdit_EndEdit_ScalarProperties()
{
User u = new User() { Name = "john", Age = 20, Wage = 200 };
u.BeginEdit();
Assert.IsFalse(u.IsChanged);
Assert.IsTrue(u.IsEditing);
u.Age = 21;
Assert.IsTrue(u.IsChanged);
Assert.IsTrue(u.IsEditing);
u.EndEdit();
Assert.AreEqual(u.Age, 21);
Assert.IsFalse(u.IsEditing);
Assert.IsFalse(u.IsChanged);
}
[TestMethod]
public void BeginEdit_CancelEdit_ScalarProperties()
{
User u = new User() { Name = "john", Age = 20, Wage = 200 };
u.BeginEdit();
u.Wage = 250;
u.CancelEdit();
Assert.IsFalse(u.IsEditing);
Assert.IsFalse(u.IsChanged);
Assert.AreEqual(u.Wage, 200);
}
[TestMethod]
public void BeginEdit_EndEdit_EditableProperties()
{
DateTime start = new DateTime(2000, 1, 1);
DateTime newStart = new DateTime(2000, 1, 2);
User u = new User() { Name = "john", Age = 20, Wage = 200 };
Task t = new Task() { Name = "writereports", StartTime = start, Owner =u };
u.Task = t;
u.BeginEdit();
Assert.IsTrue(u.IsEditing);
Assert.IsFalse(u.IsChanged);
Assert.IsTrue(t.IsEditing);
Assert.IsFalse(t.IsChanged);
t.StartTime = newStart;
Assert.IsTrue(u.IsEditing);
Assert.IsTrue(u.IsChanged);
Assert.IsTrue(t.IsEditing);
Assert.IsTrue(t.IsChanged);
u.EndEdit();
Assert.AreEqual(t.StartTime, newStart);
Assert.IsFalse(u.IsEditing);
Assert.IsFalse(u.IsChanged);
Assert.IsFalse(t.IsEditing);
Assert.IsFalse(t.IsChanged);
}
[TestMethod]
public void BeginEdit_CancelEdit_EditableProperties()
{
DateTime start = new DateTime(2000, 1, 1);
DateTime newStart = new DateTime(2000, 1, 2);
User u = new User() { Name = "john", Age = 20, Wage = 200 };
Task t = new Task() { Name = "writereports", StartTime = start, Owner =u };
u.Task = t;
u.BeginEdit();
t.StartTime = newStart;
u.CancelEdit();
Assert.AreEqual(t.StartTime, start);
Assert.IsFalse(u.IsEditing);
Assert.IsFalse(u.IsChanged);
Assert.IsFalse(t.IsEditing);
Assert.IsFalse(t.IsChanged);
}
[TestMethod]
public void BeginEdit_EndEdit_CollectionProperties_WithModify()
{
User u = new User() { Name = "john", Age = 20, Wage = 200 };
var item = new SettingItem { Name = "setting1", Value = "10" };
u.Settings = new List<SettingItem>();
u.Settings.Add(item);
u.BeginEdit();
Assert.IsTrue(u.IsEditing);
Assert.IsFalse(u.IsChanged);
Assert.IsTrue(item.IsEditing);
Assert.IsFalse(item.IsChanged);
item.Value = "20";
Assert.IsTrue(u.IsEditing);
Assert.IsTrue(u.IsChanged);
Assert.IsTrue(item.IsEditing);
Assert.IsTrue(item.IsChanged);
u.EndEdit();
Assert.AreEqual(item.Value, "20");
Assert.IsFalse(u.IsEditing);
Assert.IsFalse(u.IsChanged);
Assert.IsFalse(item.IsEditing);
Assert.IsFalse(item.IsChanged);
}
[TestMethod]
public void BeginEdit_CancelEdit_CollectionProperties_WithModify()
{
User u = new User() { Name = "john", Age = 20, Wage = 200 };
var item = new SettingItem { Name = "setting1", Value = "10" };
u.Settings = new List<SettingItem>();
u.Settings.Add(item);
u.BeginEdit();
item.Value = "21";
u.CancelEdit();
Assert.AreEqual(item.Value, "10");
Assert.IsFalse(u.IsEditing);
Assert.IsFalse(u.IsChanged);
Assert.IsFalse(item.IsEditing);
Assert.IsFalse(item.IsChanged);
}
[TestMethod]
public void BeginEdit_EndEdit_CollectionProperties_WithAdd()
{
User u = new User() { Name = "john", Age = 20, Wage = 200 };
u.Settings = new List<SettingItem>();
u.BeginEdit();
var item = new SettingItem { Name = "setting1", Value = "10" };
u.Settings.Add(item);
Assert.IsTrue(u.IsEditing);
Assert.IsTrue(u.IsChanged);
Assert.IsFalse(item.IsEditing);
Assert.IsFalse(item.IsChanged);
u.EndEdit();
Assert.AreEqual(u.Settings[0].Value, "10");
Assert.IsFalse(u.IsEditing);
Assert.IsFalse(u.IsChanged);
Assert.IsFalse(item.IsEditing);
Assert.IsFalse(item.IsChanged);
}
[TestMethod]
public void BeginEdit_EndEdit_CollectionProperties_WithDelete()
{
User u = new User() { Name = "john", Age = 20, Wage = 200 };
var item = new SettingItem { Name = "setting1", Value = "10" };
u.Settings = new List<SettingItem>();
u.Settings.Add(item);
u.BeginEdit();
u.Settings.Clear();
Assert.IsTrue(u.IsEditing);
Assert.IsTrue(u.IsChanged);
u.EndEdit();
Assert.AreEqual(u.Settings.Count, 0);
Assert.IsFalse(u.IsEditing);
Assert.IsFalse(u.IsChanged);
}
}
class User : EditableObject, ICloneable
{
public string Name { get; set; }
public decimal Wage { get; set; }
public int Age { get; set; }
public Task Task { get; set; }
public List<SettingItem> Settings { get; set; }
public object Clone()
{
User u = MemberwiseClone() as User;
if (Settings != null)
{
u.Settings = new List<SettingItem>(Settings);
}
return u;
}
}
class Task : EditableObject
{
public string Name { get; set; }
public DateTime StartTime { get; set; }
public User Owner { get; set; }
}
class SettingItem : EditableObject
{
public string Name { get; set; }
public string Value { get; set; }
}