Click here to Skip to main content
15,885,278 members
Articles / Web Development / ASP.NET

Take MVC to the Next Level in .NET

Rate me:
Please Sign up or sign in to vote.
4.62/5 (11 votes)
30 Apr 2013GPL315 min read 73.1K   858   75  
How to quickly build reusable and flexible WPF, Silverlight, or ASP.NET applications with the powerful Xomega Framework using the best MVVM principles.
// Copyright (c) 2010-2012 Xomega.Net. All rights reserved.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;

namespace Xomega.Framework
{
    /// <summary>
    /// The base class for all data objects, which contain a list of data properties
    /// and possibly a number of child objects or object lists.
    /// </summary>
    [DataContract]
    public abstract class DataObject : IDataObject
    {
        #region Construction

        /// <summary>
        /// Protected default constructor that delegates construction
        /// to the <c>Init</c> method.
        /// </summary>
        protected DataObject()
        {
            Init();
        }

        /// <summary>
        /// OnDeserializing callback to support deserialization, which
        /// delegates construction to the <c>Init</c> method.
        /// </summary>
        /// <param name="context"></param>
        [OnDeserializing]
        private void OnDeserializing(StreamingContext context)
        {
            Init();
        }

        /// <summary>
        /// Initializes the dictionaries for properties and child objects
        /// and calls the main <c>Initialize</c> method to add the actual
        /// properties and child objects.
        /// </summary>
        private void Init()
        {
            properties = new Dictionary<string, DataProperty>();
            childObjects = new Dictionary<string, IDataObject>();

            // call subclass initialization in a separate method
            // to make sure base initialization is always called
            Initialize();
            OnInitialized();
        }

        /// <summary>
        /// The abstract method to be implemented by the subclasses
        /// to add and initialize data object properties and child objects.
        /// </summary>
        protected abstract void Initialize();

        /// <summary>
        /// Additional initialization that happens after all the properties
        /// and child objects have been added and are therefore accessible.
        /// The base class implementation just calls the <c>Initialize</c> method
        /// on all the data properties to initialize themselves.
        /// If the main <c>Initialize</c> method is generated as part of the 
        /// generated object and therefore cannot be changed, this method
        /// can also be used by the non-generated part of the class
        /// to perform additional post-initialization.
        /// </summary>
        protected virtual void OnInitialized()
        {
            foreach (DataProperty p in properties.Values) p.Initialize();
        }

        /// <summary>
        /// Perform a deep copy of the state from another data object (presumably of the same type).
        /// </summary>
        /// <param name="obj">The object to copy the state from.</param>
        public virtual void CopyFrom(IDataObject obj)
        {
            DataObject dObj = obj as DataObject;
            if (dObj == null) return;
            foreach (DataProperty p in properties.Values)
                p.CopyFrom(dObj[p.Name]);
            foreach (string chName in childObjects.Keys)
                childObjects[chName].CopyFrom(dObj.GetChildObject(chName));
        }

        #endregion

        #region Properties

        /// <summary>
        /// The dictionary of the data object properties by their names.
        /// </summary>
        private Dictionary<string, DataProperty> properties;

        /// <summary>
        /// A string-based indexer that returns a data object property by its name
        /// or null if no property with this name is found on the object.
        /// </summary>
        /// <param name="name">The property of the name to return.</param>
        /// <returns>The data property of the data object with the given name or null if not found.</returns>
        public DataProperty this[string name] { get { return HasProperty(name) ? properties[name] : null; } }

        /// <summary>
        /// Checks of the data object has a property with the given name.
        /// </summary>
        /// <param name="name">The property name to check for existence.</param>
        /// <returns>True if the data object contains a property with the given name, false otherwise.</returns>
        public bool HasProperty(string name) { return properties.ContainsKey(name); }

        /// <summary>
        /// Returns an enumeration of the data object properties.
        /// </summary>
        public IEnumerable<DataProperty> Properties { get { return properties.Values; } }

        /// <summary>
        /// Adds the specified property to the data object.
        /// </summary>
        /// <param name="property">The property to add to the data object.</param>
        internal void AddProperty(DataProperty property)
        {
            properties[property.Name] = property;
        }

        #endregion

        #region Object hierarchy

        /// <summary>
        /// The parent data object for the current object if any.
        /// </summary>
        private IDataObject parent;

        /// <summary>
        /// Gets or sets the parent data object for the current object if any.
        /// </summary>
        public virtual IDataObject Parent
        {
            get { return parent; }
            set { parent = value; }
        }

        /// <summary>
        /// A dictionary of the current data object's child objects or object lists by their name.
        /// </summary>
        private Dictionary<string, IDataObject> childObjects;

        /// <summary>
        /// Adds the specified child object or object list to the current data object
        /// under the given name.
        /// </summary>
        /// <param name="name">The name, under which the child object will be added to the current data object.</param>
        /// <param name="obj">The child object to add to the current object.</param>
        protected void AddChildObject(string name, IDataObject obj)
        {
            childObjects[name] = obj;
            obj.Parent = this;
        }

        /// <summary>
        /// Gets the child object or object list for the given name or null
        /// if no child is found under this name.
        /// </summary>
        /// <param name="name">The name of the child object to return.</param>
        /// <returns>The child object or object list for the given name or null
        /// if no child is found under this name.</returns>
        public IDataObject GetChildObject(string name)
        {
            return childObjects.ContainsKey(name) ? childObjects[name] : null;
        }

        #endregion

        /// <summary>
        /// Fires a property change event recursively through all properties and child objects.
        /// </summary>
        /// <param name="args">Property change event arguments.</param>
        public void FirePropertyChange(PropertyChangeEventArgs args)
        {
            foreach (DataProperty p in properties.Values) p.FirePropertyChange(args);
            foreach (IDataObject obj in childObjects.Values) obj.FirePropertyChange(args);
        }

        /// <summary>
        /// Allows controlling if the property is required on the data object level.
        /// </summary>
        /// <param name="p">The property being checked if it's required.</param>
        /// <returns>True if the property should be required, false otherwise.</returns>
        public virtual bool IsPropertyRequired(BaseProperty p)
        {
            return parent == null || parent.IsPropertyRequired(p);
        }

        /// <summary>
        /// Allows controlling property visibility on the data object level.
        /// </summary>
        /// <param name="p">The property to check the visibility of.</param>
        /// <returns>True if the property should be visible, false otherwise.</returns>
        public virtual bool IsPropertyVisible(BaseProperty p)
        {
            return parent == null || parent.IsPropertyVisible(p);
        }
        
        #region Editability support

        /// <summary>
        /// An internal flag to allow manually making the data object uneditable.
        /// The default value is true.
        /// </summary>
        private bool editable = true;

        /// <summary>
        /// Returns a value indicating whether or not the data object is editable.
        /// This value is calculated based on the internal value of the editable field,
        /// the parent object's editability and the value of the security access level.
        /// Setting this value updates the internal editable flag and fires
        /// a property change event for all properties if necessary.
        /// </summary>
        public bool Editable
        {
            get
            {
                bool b = editable;
                if (parent != null) b &= parent.Editable;
                return b && AccessLevel > AccessLevel.ReadOnly;
            }
            set
            {
                bool oldValue = Editable;
                this.editable = value;
                if (Editable != oldValue) FirePropertyChange(
                    new PropertyChangeEventArgs(PropertyChange.Editable, oldValue, Editable));
            }
        }

        /// <summary>
        /// Allows controlling property editability on the data object level.
        /// Subclasses can override this method to define custom rules
        /// for property editability.
        /// </summary>
        /// <param name="p">The property to check the editability of.</param>
        /// <returns>True if the property should be editable, false otherwise.</returns>
        public virtual bool IsPropertyEditable(BaseProperty p)
        {
            return Editable && (parent == null || parent.IsPropertyEditable(p));
        }

        #endregion

        #region Security support

        /// <summary>
        /// Internal field that stores the security access level for the data object.
        /// The default value is full access.
        /// </summary>
        [DataMember]
        private AccessLevel accessLevel = AccessLevel.Full;

        /// <summary>
        /// Returns the current access level for the data object.
        /// Allows setting a new access level and fires a property change event
        /// for editability and visibility of all properties,
        /// since they both depend on the security access level.
        /// </summary>
        public AccessLevel AccessLevel
        {
            get { return accessLevel; }
            set
            {
                AccessLevel oldValue = accessLevel;
                accessLevel = value;
                FirePropertyChange(new PropertyChangeEventArgs(
                    PropertyChange.Editable + PropertyChange.Visible, oldValue, accessLevel));
            }
        }
        #endregion

        #region Data Contract support

        /// <summary>
        /// Sets the data object values from the given data contract object
        /// by copying the values of the data contract object fields to the
        /// data object properties or child objects with the same names.
        /// If there is no exact match between some data contract field names
        /// and the data object property names, this method can be overridden
        /// in the subclass to address each such case.
        /// </summary>
        /// <param name="dataContract">The data contract object to copy the values from.</param>
        public virtual void FromDataContract(object dataContract)
        {
            if (dataContract == null) return;
            SetModified(false, false);
            foreach (PropertyInfo pi in dataContract.GetType().GetProperties())
            {
                object val = pi.GetValue(dataContract, null);
                DataProperty dp = this[pi.Name];
                IDataObject child;
                if (dp != null)
                {
                    dp.Modified = null;
                    dp.SetValue(val);
                }
                else if ((child = this.GetChildObject(pi.Name)) != null)
                {
                    IDataObjectList childList = child as IDataObjectList;
                    IList vallist = val as IList;
                    DataObject childObj;
                    if (childList != null && vallist != null)
                    {
                        childList.Clear();
                        foreach (object contractItem in vallist)
                        {
                            DataObject objectItem = childList.NewDataObject();
                            objectItem.FromDataContract(contractItem);
                            childList.Add(objectItem);
                        }
                    }
                    else if (vallist == null && (childObj = child as DataObject) != null)
                        childObj.FromDataContract(val);
                }
                else if (val != null)
                {
                    foreach (PropertyInfo cpi in pi.PropertyType.GetProperties())
                    {
                        DataProperty cdp = this[pi.Name + "_" + cpi.Name];
                        if (cdp != null)
                        {
                            cdp.Modified = null;
                            cdp.SetValue(cpi.GetValue(val, null));
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Exports the data object property values and child object values
        /// to the given data contract object by setting all its properties
        /// to the values of the corresponding properties or child objects
        /// with the same names.
        /// If there is no exact match between some data contract property names
        /// and the data object property names, this method can be overridden
        /// in the subclass to address each such case.
        /// </summary>
        /// <param name="dataContract">The data contract object to export
        /// the current data object values to.</param>
        public virtual void ToDataContract(object dataContract)
        {
            if (dataContract == null) return;
            ToDataContractProperties(dataContract, dataContract.GetType().GetProperties());
        }

        /// <summary>
        /// Exports the data object property values and child object values
        /// to the given data contract object by setting the specified properties
        /// of the data contract to the values of the corresponding properties
        /// or child objects with the same names.
        /// This method can be used to partially export data object values
        /// to a data contract object.
        /// </summary>
        /// <param name="dataContract">The data contract object to export
        /// the current data object values to.</param>
        /// <param name="props">The data contract object fields to set.</param>
        protected void ToDataContractProperties(object dataContract, PropertyInfo[] props)
        {
            if (dataContract == null) return;
            foreach (PropertyInfo pi in props)
            {
                DataProperty dp = this[pi.Name];
                IDataObject child;
                if (dp != null)
                {
                    if (dp.IsValid(true))
                    {
                        if (dp.IsMultiValued)
                        {
                            IList lst = null;
                            IEnumerable valLst = dp.TransportValue as IEnumerable;
                            if (valLst != null)
                            {
                                // create the right type of list and copy the values rather than directly assign
                                lst = CreateInstance(pi.PropertyType) as IList;
                                if (lst != null) foreach (object o in valLst) lst.Add(o);
                            }
                            pi.SetValue(dataContract, lst, null);
                        }
                        else pi.SetValue(dataContract, dp.TransportValue, null);
                    }
                    continue;
                }
                object obj = null;
                try { CreateInstance(pi.PropertyType); }
                catch { continue; }
                if ((child = this.GetChildObject(pi.Name)) != null)
                {
                    bool isList = obj is IList;
                    DataObject childObj;
                    IDataObjectList childList;
                    if (isList && (childList = child as IDataObjectList) != null)
                        childList.ToDataContract(obj as IList);
                    else if (!isList && (childObj = child as DataObject) != null)
                        childObj.ToDataContract(obj);
                }
                else
                {
                    foreach (PropertyInfo cpi in pi.PropertyType.GetProperties())
                    {
                        DataProperty cdp = this[pi.Name + "_" + cpi.Name];
                        if (cdp != null && cdp.IsValid(true)) cpi.SetValue(obj, cdp.TransportValue, null);
                    }
                }
                pi.SetValue(dataContract, obj, null);
            }
        }

        /// <summary>
        /// Create an instance of a certain type. If the type is IEnumerable
        /// then creates a corresponding generic list or dictionary as appropriate.
        /// </summary>
        /// <param name="type">The type to create an instance of.</param>
        /// <returns>An instance of the corresponding type.</returns>
        protected object CreateInstance(Type type)
        {
            Type t = type;
            if (t.IsInterface && typeof(IEnumerable).IsAssignableFrom(t))
            {
                Type[] args = t.GetGenericArguments();
                if (args.Length > 1 && typeof(IDictionary).IsAssignableFrom(t))
                    t = typeof(Dictionary<,>).MakeGenericType(args[0], args[1]);
                else if (args.Length > 0) t = typeof(List<>).MakeGenericType(args[0]);
                else t = typeof(List<object>);
            }
            return Activator.CreateInstance(t);
        }

        #endregion

        #region Validation

        /// <summary>
        /// A list of validation errors that are not tied to any particular
        /// data property but rather to the data object as a whole.
        /// Null value means that the object has not been validated yet.
        /// </summary>
        protected ErrorList validationErrorList;

        /// <summary>
        /// Gets all validation errors from the data object, all its properties and child objects recursively.
        /// </summary>
        /// <returns>Validation errors from the data object, all its properties and child objects.</returns>
        public ErrorList GetValidationErrors()
        {
            ErrorList errLst = new ErrorList();
            if (validationErrorList != null) errLst.MergeWith(validationErrorList);
            foreach (DataProperty p in properties.Values) errLst.MergeWith(p.ValidationErrors);
            foreach (IDataObject obj in childObjects.Values) errLst.MergeWith(obj.GetValidationErrors());
            return errLst;
        }

        /// <summary>
        /// Resets validation status to not validated on the object
        /// by setting the validation error list to null.
        /// </summary>
        public void ResetValidation()
        {
            validationErrorList = null;
        }

        /// <summary>
        /// Resets validation status to not validated on the object,
        /// all its properties and child objects recursively.
        /// </summary>
        public void ResetAllValidation()
        {
            ResetValidation();
            foreach (DataProperty p in properties.Values) p.ResetValidation();
            foreach (IDataObject obj in childObjects.Values) obj.ResetAllValidation();
        }

        /// <summary>
        /// Validates the data object and all its properties and child objects recursively.
        /// </summary>
        /// <param name="force">True to validate regardless of
        /// whether or not it has been already validated.</param>
        public virtual void Validate(bool force)
        {
            foreach (DataProperty p in properties.Values) p.Validate(force);
            foreach (IDataObject obj in childObjects.Values) obj.Validate(force);

            if (force) ResetValidation();
            if (validationErrorList != null) return;

            validationErrorList = new ErrorList();
        }

        #endregion

        #region Modification support

        /// <summary>
        /// Tracks the modification state of the data object.
        /// Null means the date object has never been initialized with data.
        /// False means the data object has been initialized, but has not been changed since then.
        /// True means that the data object has been modified since it was initialized.
        /// </summary>
        protected bool? modified;

        /// <summary>
        /// Returns the modification state of the data object.
        /// </summary>
        /// <returns>The modification state of the data object.
        /// Null means the date object has never been initialized with data.
        /// False means the data object has been initialized, but has not been changed since then.
        /// True means that the data object has been modified since it was initialized.</returns>
        public bool? IsModified()
        {
            bool? res = modified;
            foreach (DataProperty prop in properties.Values)
                if (prop.Modified.HasValue) res |= prop.Modified;
            foreach (IDataObject child in childObjects.Values)
            {
                bool? childModified = child.IsModified();
                if (childModified.HasValue) res |= childModified;
            }
            return res;
        }

        /// <summary>
        /// Sets the modification state for the data object to the specified value.
        /// </summary>
        /// <param name="modified">The modification state value.
        /// Null means the date object has never been initialized with data.
        /// False means the data object has been initialized, but has not been changed since then.
        /// True means that the data object has been modified since it was initialized.</param>
        /// <param name="recursive">True to propagate the modification state
        /// to all properties and child objects, false otherwise.</param>
        public void SetModified(bool? modified, bool recursive)
        {
            this.modified = modified;
            if (recursive)
            {
                foreach (DataProperty prop in properties.Values) prop.Modified = modified;
                foreach (IDataObject child in childObjects.Values) child.SetModified(modified, true);
            }
        }

        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Architect Xomega.Net
United States United States
Xomega Team is striving to increase productivity and development quality by utilizing Model Driven Development coupled with Code Generation and the best design practices for application development.
We provide MDD tools, code generators and frameworks for Visual Studio and .Net development.
Visit us at http://www.xomega.net
This is a Organisation

1 members

Comments and Discussions