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; }
}