IEditableObject的一個通用實現

IeditableObject是一個通用接口,用於支持對象編輯。當我們在界面上選擇一個條目,然後對其進行編輯的時候,接下來會有兩種操作,一個是保持編輯結果,一個取消編輯。這就要求我們保留原始值,否則我們只能到數據庫裏面再次查詢。IeditableObject接口的三個方法定義爲我們定義了這個行爲規範:


    public interface IEditableObject


    {


        // 開始編輯,一般在此方法內創建當前對象副本


        void BeginEdit();


        //取消編輯,當副本恢復到當前對象,並清除副本


        void CancelEdit();


        // 接受編輯結果,並清除副本


        void EndEdit();


}


對於IeditableObject的實現,應該滿足一下要求:


  1. 具有NonEditableAttribute標記的屬性不參與編輯

  2. 如果某個屬性類型也實現了IeditableObject 那麼將遞歸調用相應編輯方法。

  3. 對於集合對象,如果集合對象實現了IeditableObject,將會對集合的每個項調用相應編輯方法。

  4. 可以查詢對象是否改變,包括任何標量屬性的變化,關聯的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; }


    }



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章