Click here to Skip to main content
15,883,901 members
Articles / Desktop Programming / WPF

Catel - Part 4 of n: Unit testing with Catel

Rate me:
Please Sign up or sign in to vote.
4.55/5 (10 votes)
28 Jan 2011CPOL11 min read 48.9K   572   11  
This article explains how to write unit tests for MVVM using Catel.
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ViewModelBaseWithoutServices.cs" company="Catel development team">
//   Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
//   View model base for MVVM implementations. This class is based on the <see cref="DataObjectBase" />, and supports all
//   common interfaces used by WPF.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows.Threading;
using Catel.ComponentModel;
using Catel.Data;
using Catel.LLBLGen;
using Catel.MVVM.Services;
using Catel.Properties;
using Catel.Reflection;
using log4net;
using Microsoft.Practices.Unity;

namespace Catel.MVVM
{
    /// <summary>
    /// View model base for MVVM implementations. This class is based on the <see cref="DataObjectBase"/>, and supports all
    /// common interfaces used by WPF.
    /// </summary>
    /// <remarks>
    /// This view model base does not add any services. The technique specific implementation should take care of that
    /// (such as WPF, Silverlight, etc).
    /// </remarks>
    [AllowNonSerializableMembers]
    public abstract class ViewModelBaseWithoutServices : DataObjectBase<ViewModelBaseWithoutServices>, IViewModel
    {
        #region Classes
        /// <summary>
        /// Class containing information about a specific model decorated with the <see cref="ModelAttribute"/>.
        /// </summary>
        private class ModelInfo
        {
            #region Variables
            #endregion

            #region Constructor & destructor
            /// <summary>
            /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices.ModelInfo"/> class.
            /// </summary>
            /// <param name="name">The name of the model property.</param>
            /// <param name="attribute">The attribute.</param>
            public ModelInfo(string name, ModelAttribute attribute)
            {
                Name = name;
                SupportIEditableObject = attribute.SupportIEditableObject;
            }
            #endregion

            #region Propertiess
            /// <summary>
            /// Gets the name of the model property.
            /// </summary>
            /// <value>The name of the model property.</value>
            public string Name { get; private set; }

            /// <summary>
            /// Gets a value indicating whether the <see cref="IEditableObject"/> interface should be used on the model if possible.
            /// </summary>
            /// <value>
            /// 	<c>true</c> if the <see cref="IEditableObject"/> interface should be used on the model if possible; otherwise, <c>false</c>.
            /// </value>
            public bool SupportIEditableObject { get; private set; }
            #endregion

            #region Methods
            #endregion
        }

        /// <summary>
        /// Model value class to store the mapping of the View Model to a Model mapping.
        /// </summary>
        private class ViewModelToModelMapping
        {
            #region Constructor & destructor
            /// <summary>
            /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices.ViewModelToModelMapping"/> class.
            /// </summary>
            /// <param name="viewModelProperty">The view model property.</param>
            /// <param name="attribute">The <see cref="ViewModelToModelAttribute"/> that was used to define the mapping.</param>
            public ViewModelToModelMapping(string viewModelProperty, ViewModelToModelAttribute attribute)
                : this(viewModelProperty, attribute.Model, attribute.Property, attribute.SupportLLBLGen,
                       attribute.ViewModelLLBLGenEntityProperty, attribute.ModelLLBLGenEntityProperty, attribute.NullValue) { }

            /// <summary>
            /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices.ViewModelToModelMapping"/> class.
            /// </summary>
            /// <param name="viewModelProperty">The view model property.</param>
            /// <param name="modelProperty">The model property.</param>
            /// <param name="valueProperty">The value property.</param>
            /// <param name="supportLLBLGen">if set to <c>true</c>, LLBLGen will be supported.</param>
            /// <param name="viewModelLLBLGenEntityProperty">The view model LLBLGen entity property.</param>
            /// <param name="modelLLBLGenEntityProperty">The model LLBLGen entity property.</param>
            /// <param name="nullValue">The null value.</param>
            public ViewModelToModelMapping(string viewModelProperty, string modelProperty, string valueProperty, bool supportLLBLGen,
                string viewModelLLBLGenEntityProperty, string modelLLBLGenEntityProperty, object nullValue)
            {
                ViewModelProperty = viewModelProperty;
                ModelProperty = modelProperty;
                ValueProperty = valueProperty;
                SupportLLBLGen = supportLLBLGen;
                ViewModelLLBLGenEntityProperty = viewModelLLBLGenEntityProperty;
                ModelLLBLGenEntityProperty = modelLLBLGenEntityProperty;
                NullValue = nullValue;
            }
            #endregion

            #region Properties
            /// <summary>
            /// Gets the property name of the mapping of the view model.
            /// </summary>
            /// <value>The model view property.</value>
            public string ViewModelProperty { get; private set; }

            /// <summary>
            /// Gets the property name of the the model.
            /// </summary>
            /// <value>The model.</value>
            public string ModelProperty { get; private set; }

            /// <summary>
            /// Gets the property property name of the property in the model.
            /// </summary>
            /// <value>The property.</value>
            public string ValueProperty { get; private set; }

            /// <summary>
            /// Gets a value indicating whether to support LLBLGen.
            /// </summary>
            /// <value><c>true</c> if LLBLGen is supported; otherwise, <c>false</c>.</value>
            public bool SupportLLBLGen { get; private set; }

            /// <summary>
            /// Gets the LLBLGen property to set when the view model property changes.
            /// </summary>
            /// <value>The LLBLGen property.</value>
            public string ViewModelLLBLGenEntityProperty { get; private set; }

            /// <summary>
            /// Gets the LLBLGen property to set when the view model property changes.
            /// </summary>
            /// <value>The LLBLGen property.</value>
            public string ModelLLBLGenEntityProperty { get; private set; }

            /// <summary>
            /// Gets the value to set on the LLBLGen entity when the view model property is null.
            /// </summary>
            /// <value>The null value.</value>
            public object NullValue { get; private set; }
            #endregion
        }
        #endregion

        #region Constants
        /// <summary>
        /// Name of the restore point when beginning to edit a model that is an LLBLGen entity.
        /// </summary>
        private const string LLBLGenRestorePointName = "_viewModelRestorePoint";
        #endregion

        #region Variables
        /// <summary>
        /// Dictionary of available models inside the view model.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        private readonly Dictionary<string, object> _modelObjects = new Dictionary<string, object>();

        /// <summary>
        /// Dictionary with info about the available models inside the view model.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        private readonly Dictionary<string, ModelInfo> _modelObjectsInfo = new Dictionary<string, ModelInfo>();

#if SILVERLIGHT
        /// <summary>
        /// Dictionary containing all the previous values of the available models inside the view model.
        /// <para />
        /// Because Silverlight doesn't implement <c>INotifyPropertyChanging</c>, we need to keep track or the values
        /// ourselves to be able to clean up changed models.
        /// </summary>
        private readonly Dictionary<string, object> _previousModelObjects = new Dictionary<string, object>();
#endif

        /// <summary>
        /// List of child view models which can be registed by the <see cref="RegisterChildViewModel"/> method.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        private readonly List<IViewModel> _childViewModels = new List<IViewModel>();

        /// <summary>
        /// Value to determine whether child view models have errors or not.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        private bool _childViewModelsHaveErrors;

        /// <summary>
        /// Gets the view model manager.
        /// </summary>
        /// <value>The view model manager.</value>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        protected static readonly ViewModelManager ViewModelManager = new ViewModelManager();

        /// <summary>
        /// Service manager to manager services of the view models.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        protected static readonly ViewModelServiceManager ViewModelServiceManager = new ViewModelServiceManager();

        /// <summary>
        /// Dictionary of LLBLGen ValidateEntity fields that are available on the models. This mapping is required to dynamically determine
        /// whether a model is an LLBLGen Pro entity, and therefore must be validated via a call to ValidateEntity.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        private readonly Dictionary<string, MethodInfo> _modelObjectValidateEntityMethods = new Dictionary<string, MethodInfo>();

        /// <summary>
        /// Mappings from view model properties to models and their properties.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        private readonly Dictionary<string, ViewModelToModelMapping> _viewModelToModelMap = new Dictionary<string, ViewModelToModelMapping>();

        /// <summary>
        /// A list of commands that implement the <see cref="ICatelCommand"/> interface.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        private readonly List<ICatelCommand> _registeredCommands = new List<ICatelCommand>();

        /// <summary>
        /// A value indiciating whether the services are already registered. If not, the <see cref="RegisterViewModelServices"/> will
        /// be invoked.
        /// </summary>
#if !SILVERLIGHT
        [field: NonSerialized]
#endif
        private static bool _registeredServices;
        #endregion

        #region Constructor & destructor
        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class with support for <see cref="IEditableObject"/>.
        /// </summary>
        /// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
        /// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
        protected ViewModelBaseWithoutServices()
            : this(true) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
        /// </summary>
        /// <param name="supportIEditableObject">if set to <c>true</c>, the view model will natively support models that
        /// implement the <see cref="IEditableObject"/> interface.</param>
        /// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
        /// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
        protected ViewModelBaseWithoutServices(bool supportIEditableObject)
            : this(supportIEditableObject, false) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
        /// </summary>
        /// <param name="supportIEditableObject">if set to <c>true</c>, the view model will natively support models that
        /// implement the <see cref="IEditableObject"/> interface.</param>
        /// <param name="ignoreMultipleModelsWarning">if set to <c>true</c>, the warning when using multiple models is ignored.</param>
        /// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
        /// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
        protected ViewModelBaseWithoutServices(bool supportIEditableObject, bool ignoreMultipleModelsWarning)
            : this(null, supportIEditableObject, ignoreMultipleModelsWarning) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
        /// <para/>
        /// This constructor allows services to be injected. When <param name="services"/> contains any elements, the
        /// <see cref="RegisterViewModelServices"/> method is not invoked.
        /// </summary>
        /// <param name="services">Dictionary of services to register.</param>
        /// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
        /// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
        protected ViewModelBaseWithoutServices(Dictionary<Type, object> services)
            : this(services, true) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
        /// <para/>
        /// This constructor allows services to be injected. When <param name="services"/> contains any elements, the
        /// <see cref="RegisterViewModelServices"/> method is not invoked.
        /// </summary>
        /// <param name="services">Dictionary of services to register.</param>
        /// <param name="supportIEditableObject">if set to <c>true</c>, the view model will natively support models that
        /// implement the <see cref="IEditableObject"/> interface.</param>
        /// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
        /// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
        protected ViewModelBaseWithoutServices(Dictionary<Type, object> services, bool supportIEditableObject)
            : this(services, supportIEditableObject, false) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
        /// <para />
        /// This constructor allows services to be injected. When <param name="services"/> contains any elements, the
        /// <see cref="RegisterViewModelServices"/> method is not invoked.
        /// </summary>
        /// <param name="services">Dictionary of services to register.</param>
        /// <param name="supportIEditableObject">if set to <c>true</c>, the view model will natively support models that
        /// implement the <see cref="IEditableObject"/> interface.</param>
        /// <param name="ignoreMultipleModelsWarning">if set to <c>true</c>, the warning when using multiple models is ignored.</param>
        /// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
        /// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
        protected ViewModelBaseWithoutServices(Dictionary<Type, object> services, bool supportIEditableObject, bool ignoreMultipleModelsWarning)
        {
            if ((services != null) && (services.Count > 0))
            {
                ViewModelServiceManager.Clear();
                foreach (KeyValuePair<Type, object> service in services)
                {
                    ViewModelServiceManager.Add(service.Key, service.Value);
                }
            }
            else if (!_registeredServices)
            {
                _registeredServices = true;

                Log.Debug("Registering view model services");

                RegisterViewModelServices(IoC.UnityContainer.Instance.Container);

                Log.Debug("Registered view model services");
            }

            // In silverlight, automatically invalidate commands when property changes
#if SILVERLIGHT
            InvalidateCommandsOnPropertyChanged = true;
#else
            InvalidateCommandsOnPropertyChanged = false;
#endif

            ViewModelConstructionTime = DateTime.Now;

#if SILVERLIGHT
            if (System.Windows.Application.Current.RootVisual != null)
            {
                Dispatcher = System.Windows.Application.Current.RootVisual.Dispatcher;
            }
            else
            {
                Dispatcher = System.Windows.Deployment.Current.Dispatcher;
            }
#else
            Dispatcher = Dispatcher.CurrentDispatcher;
#endif

            // Do not automatically dipose members (we don't want to dispose models)
            AutomaticallyDisposeChildObjectsOnDispose = false;
            SuspendValidation = true;
            SupportIEditableObject = supportIEditableObject;

            Type type = GetType();
            List<PropertyInfo> properties = new List<PropertyInfo>();
            properties.AddRange(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));

            foreach (PropertyInfo propertyInfo in properties)
            {
                var modelAttribute = Attribute.GetCustomAttribute(propertyInfo, typeof(ModelAttribute)) as ModelAttribute;
                if (modelAttribute != null)
                {
                    lock (_modelObjects)
                    {
                        _modelObjects.Add(propertyInfo.Name, null);
                        _modelObjectsInfo.Add(propertyInfo.Name, new ModelInfo(propertyInfo.Name, modelAttribute));
                    }
                }

                var viewModelToModelAttribute = Attribute.GetCustomAttribute(propertyInfo, typeof(ViewModelToModelAttribute)) as ViewModelToModelAttribute;
                if (viewModelToModelAttribute != null)
                {
                    if (string.IsNullOrEmpty(viewModelToModelAttribute.Property))
                    {
                        // Assume the property name in the model is the same as in the view model
                        viewModelToModelAttribute.Property = propertyInfo.Name;
                    }

                    if (viewModelToModelAttribute.SupportLLBLGen)
                    {
                        if (string.IsNullOrEmpty(viewModelToModelAttribute.ModelLLBLGenEntityProperty))
                        {
                            viewModelToModelAttribute.ModelLLBLGenEntityProperty = LLBLGenHelper.GetRelatedEntityPropertyName(propertyInfo.PropertyType, viewModelToModelAttribute.Property);
                        }

                        if (string.IsNullOrEmpty(viewModelToModelAttribute.ViewModelLLBLGenEntityProperty))
                        {
                            viewModelToModelAttribute.ViewModelLLBLGenEntityProperty = LLBLGenHelper.GetRelatedEntityPropertyName(propertyInfo.PropertyType, viewModelToModelAttribute.Property);
                        }
                    }

                    _viewModelToModelMap.Add(propertyInfo.Name, new ViewModelToModelMapping(propertyInfo.Name, viewModelToModelAttribute));
                }

                if (LLBLGenHelper.IsLLBLGenType(propertyInfo.PropertyType))
                {
                    TypesNotToDispose.Add(propertyInfo.PropertyType.Name);
                }
            }

            if (SupportIEditableObject)
            {
                lock (_modelObjects)
                {
                    foreach (KeyValuePair<string, object> modelKeyValuePair in _modelObjects)
                    {
                        if (_modelObjectsInfo[modelKeyValuePair.Key].SupportIEditableObject)
                        {
                            if (!(modelKeyValuePair.Value is DataObjectBase) || !((DataObjectBase)modelKeyValuePair.Value).IsInEditSession)
                            {
                                BeginEditObject(modelKeyValuePair.Value);
                            }
                        }
                    }
                }
            }

            // Validate view model to model mappings
            foreach (KeyValuePair<string, ViewModelToModelMapping> viewModelToModelMapping in _viewModelToModelMap)
            {
                var mapping = viewModelToModelMapping.Value;
                if (!IsModelRegistered(mapping.ModelProperty))
                {
                    Log.Error(TraceMessages.ModelNotRegistered, mapping.ModelProperty, mapping.ViewModelProperty);
                    throw new ModelNotRegisteredException(mapping.ModelProperty, mapping.ViewModelProperty);
                }

                PropertyInfo viewModelPropertyInfo = GetPropertyInfo(mapping.ViewModelProperty);
                PropertyInfo modelPropertyInfo = GetPropertyInfo(mapping.ModelProperty);
                Type modelType = modelPropertyInfo.PropertyType;

                PropertyInfo modelPropertyPropertyInfo = modelType.GetProperty(mapping.ValueProperty, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                if (modelPropertyPropertyInfo == null)
                {
                    Log.Error(TraceMessages.PropertyNotFoundInModel, mapping.ViewModelProperty, mapping.ModelProperty, mapping.ValueProperty);
                    throw new PropertyNotFoundInModelException(mapping.ViewModelProperty, mapping.ModelProperty, mapping.ValueProperty);
                }

                if (viewModelPropertyInfo.PropertyType != modelPropertyPropertyInfo.PropertyType)
                {
                    Log.Warn(TraceMessages.WrongViewModelPropertyType, mapping.ViewModelProperty, mapping.ValueProperty,
                        viewModelPropertyInfo.PropertyType, modelPropertyPropertyInfo.PropertyType);
                }
            }

            if (!ignoreMultipleModelsWarning)
            {
                lock (_modelObjects)
                {
                    if (_modelObjects.Count > 1)
                    {
                        Log.Warn(TraceMessages.ViewModelImplementsMoreThanOneModelWarning, GetType().Name, _modelObjects.Count);
                    }
                }
            }

            ViewModelManager.RegisterViewModelInstance(this);

            object[] interestedInAttributes = GetType().GetCustomAttributes(typeof(InterestedInAttribute), true);
            foreach (InterestedInAttribute interestedInAttribute in interestedInAttributes)
            {
                ViewModelManager.AddInterestedViewModel(interestedInAttribute.ViewModelType, this);
            }
        }
        #endregion

        #region Events
        /// <summary>
        /// Occurs when the view model is about the be saved.
        /// </summary>
        public event EventHandler<EventArgs> Saving;

        /// <summary>
        /// Occurs when the view model is saved successfully.
        /// </summary>
        public event EventHandler<EventArgs> Saved;

        /// <summary>
        /// Occurs when the view model is about to be canceled.
        /// </summary>
        public event EventHandler<EventArgs> Canceling;

        /// <summary>
        /// Occurrs when the view model is canceled.
        /// </summary>
        public event EventHandler<EventArgs> Canceled;

        /// <summary>
        /// Occurs when the view model is being closed.
        /// </summary>
        public event EventHandler<EventArgs> Closed;
        #endregion

        #region Properties
        /// <summary>
        /// Gets the view model construction time.
        /// </summary>
        /// <value>The view model construction time.</value>
        public DateTime ViewModelConstructionTime { get; private set; }

        /// <summary>
        /// Gets the dispatcher.
        /// </summary>
        /// <value>The dispatcher.</value>
        protected Dispatcher Dispatcher { get; private set; }

        /// <summary>
        /// Gets the parent view model.
        /// </summary>
        /// <value>The parent view model.</value>
        protected IViewModel ParentViewModel { get; private set; }

        /// <summary>
        /// Gets a value indicating whether the commands should automatically be invalidated on a property change.
        /// <para />
        /// If this property is <c>false</c>, properties should either be invalidated by the .NET Framework or by a manual
        /// call to the <see cref="InvalidateCommands"/> method.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if the commands should automatically be invalidated on a property change; otherwise, <c>false</c>.
        /// </value>
        protected bool InvalidateCommandsOnPropertyChanged { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether models that implement <see cref="IEditableObject"/> are supported correctly.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if models that implement <see cref="IEditableObject"/> are supported correctly; otherwise, <c>false</c>.
        /// </value>
        private bool SupportIEditableObject { get; set; }

        /// <summary>
        /// Gets a value indicating whether the view model is initialized.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if the view model is initialized; otherwise, <c>false</c>.
        /// </value>
        public new bool IsInitialized { get; private set; }

        /// <summary>
        /// Gets a value indicating whether the view model is closed.
        /// </summary>
        /// <value><c>true</c> if the view model is closed; otherwise, <c>false</c>.</value>
        protected bool IsClosed { get; private set; }

        /// <summary>
        /// Gets the title of the view model.
        /// </summary>
        /// <value>The title.</value>
        public virtual string Title
        {
            get { return string.Empty; }
        }

        /// <summary>
        /// Gets a value indicating whether this object contains any field or business errors.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance has errors; otherwise, <c>false</c>.
        /// </value>
        public new bool HasErrors
        {
            get { return base.HasErrors || _childViewModelsHaveErrors; }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Sets the parent view model.
        /// </summary>
        /// <param name="parent">The parent.</param>
        internal void SetParentViewModel(IViewModel parent)
        {
            if (ParentViewModel != parent)
            {
                ParentViewModel = parent;

                OnPropertyChanged("ParentViewModel");
            }
        }

        /// <summary>
        /// Registers a child view model.
        /// </summary>
        /// <param name="child">The child view model.</param>
        /// <exception cref="ArgumentNullException">when <paramref name="child"/> is <c>null</c>.</exception>
        internal void RegisterChildViewModel(IViewModel child)
        {
            if (child == null)
            {
                throw new ArgumentNullException("child");
            }

            lock (_childViewModels)
            {
                if (!_childViewModels.Contains(child))
                {
                    _childViewModels.Add(child);

                    child.PropertyChanged += OnChildViewModelPropertyChanged;
                    child.Closed += OnChildViewModelClosed;
                }
            }
        }

        /// <summary>
        /// Called when a property has changed on the child view model.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private void OnChildViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "HasErrors")
            {
                Validate(true);
            }
        }

        /// <summary>
        /// Called when the child view model is closed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnChildViewModelClosed(object sender, EventArgs e)
        {
            UnregisterChildViewModel((IViewModel)sender);
        }

        /// <summary>
        /// Unregisters the child view model.
        /// </summary>
        /// <param name="child">The child.</param>
        /// <exception cref="ArgumentNullException">when <paramref name="child"/> is <c>null</c>.</exception>
        internal void UnregisterChildViewModel(IViewModel child)
        {
            if (child == null)
            {
                throw new ArgumentNullException("child");
            }

            lock (_childViewModels)
            {
                int index = _childViewModels.IndexOf(child);
                if (index == -1)
                {
                    return;
                }

                _childViewModels[index].PropertyChanged -= OnChildViewModelPropertyChanged;
                _childViewModels[index].Closed -= OnChildViewModelClosed;

                _childViewModels.Remove(child);
            }
        }

        /// <summary>
        /// Gets all models that are decorated with the <see cref="ModelAttribute"/>.
        /// </summary>
        /// <returns>Array of models.</returns>
        protected object[] GetAllModels()
        {
            return _modelObjects.Values.ToArray();
        }

#if !SILVERLIGHT
        /// <summary>
        /// Called when a property value is changing.
        /// </summary>
        /// <param name="propertyName">Name of the property that is changing.</param>
        protected override void OnPropertyChanging(string propertyName)
        {
            lock (_modelObjects)
            {
                if (_modelObjects.ContainsKey(propertyName))
                {
                    object model = _modelObjects[propertyName];

                    if (model is INotifyPropertyChanged)
                    {
                        ((INotifyPropertyChanged)model).PropertyChanged -= Model_PropertyChanged;
                    }

                    if (SupportIEditableObject)
                    {
                        if (_modelObjectsInfo[propertyName].SupportIEditableObject)
                        {
                            if (!(model is DataObjectBase) || ((DataObjectBase)model).IsInEditSession)
                            {
                                CancelEditObject(model);
                            }
                        }
                    }
                }
            }

            base.OnPropertyChanging(propertyName);
        }
#endif

        /// <summary>
        /// Called when a property value has changed.
        /// </summary>
        /// <param name="propertyName">Name of the property that has changed.</param>
        protected override void OnPropertyChanged(string propertyName)
        {
#if SILVERLIGHT
            // This code is normally handled in the OnPropertyChanging method. However, Silverlight doesn't support that, so
            // we can only clean up here
            lock (_previousModelObjects)
            {
                if (_previousModelObjects.ContainsKey(propertyName))
                {
                    object model = _previousModelObjects[propertyName];

                    if (model is INotifyPropertyChanged)
                    {
                        ((INotifyPropertyChanged)model).PropertyChanged -= Model_PropertyChanged;
                    }

                    if (SupportIEditableObject)
                    {
                        if (_modelObjectsInfo[propertyName].SupportIEditableObject)
                        {
                            if (!(model is DataObjectBase) || ((DataObjectBase)model).IsInEditSession)
                            {
                                CancelEditObject(model);
                            }
                        }
                    }

                    lock (_modelObjects)
                    {
                        _previousModelObjects[propertyName] = _modelObjects[propertyName];
                    }
                }
            }
#endif

            lock (_modelObjects)
            {
                if (_modelObjects.ContainsKey(propertyName))
                {
                    _modelObjects[propertyName] = GetValue(propertyName);
                    object model = _modelObjects[propertyName];
                    if (model is INotifyPropertyChanged)
                    {
                        ((INotifyPropertyChanged)model).PropertyChanged += Model_PropertyChanged;
                    }

                    _modelObjectValidateEntityMethods.Remove(propertyName);
                    MethodInfo methodInfo = LLBLGenHelper.GetValidateEntityMethod(model);
                    if (methodInfo != null)
                    {
                        _modelObjectValidateEntityMethods.Add(propertyName, methodInfo);
                    }

                    if (SupportIEditableObject)
                    {
                        if (_modelObjectsInfo[propertyName].SupportIEditableObject)
                        {
                            if (!(model is DataObjectBase) || !((DataObjectBase)model).IsInEditSession)
                            {
                                BeginEditObject(model);
                            }
                        }
                    }

                    // Since the model has been changed, copy all values from the model to the view model
                    foreach (KeyValuePair<string, ViewModelToModelMapping> viewModelToModelMap in _viewModelToModelMap)
                    {
                        ViewModelToModelMapping mapping = viewModelToModelMap.Value;
                        if (mapping.ModelProperty == propertyName)
                        {
                            SetValue(mapping.ViewModelProperty, PropertyHelper.GetPropertyValue(_modelObjects[propertyName], mapping.ValueProperty));
                        }
                    }
                }
            }

            // If we are validating, don't map view model values back to the model
            if (!IsValidating)
            {
                if (_viewModelToModelMap.ContainsKey(propertyName))
                {
                    lock (_modelObjects)
                    {
                        ViewModelToModelMapping mapping = _viewModelToModelMap[propertyName];
                        object model = _modelObjects[mapping.ModelProperty];
                        if (model == null)
                        {
                            Log.Warn(TraceMessages.CannotMapFromViewModelToModelBecauseModelIsNull, mapping.ModelProperty);
                        }
                        else
                        {
                            object viewModelValue = GetValue(propertyName);
                            object modelValue = PropertyHelper.GetPropertyValue(model, mapping.ValueProperty);
                            if (!TypeHelper.AreObjectsEqual(viewModelValue, modelValue))
                            {
                                object valueToSet = viewModelValue;
                                string propertyToSet = mapping.ValueProperty;

                                if (mapping.SupportLLBLGen)
                                {
                                    propertyToSet = mapping.ModelLLBLGenEntityProperty;

                                    valueToSet = (viewModelValue != null) ? PropertyHelper.GetPropertyValue(viewModelValue, mapping.ViewModelLLBLGenEntityProperty) : null;
                                }

                                try
                                {
                                    PropertyHelper.SetPropertyValue(model, propertyToSet, valueToSet);
                                }
                                catch (CannotSetPropertyValueException)
                                {
                                    // This is accepted behavior in case of a read-only property, already logged by framework
                                }
                            }
                        }
                    }
                }
            }

            if (InvalidateCommandsOnPropertyChanged)
            {
                InvalidateCommands();
            }

            base.OnPropertyChanged(propertyName);
        }

        /// <summary>
        /// Called when a property has changed for a view model type that the current view model is interested in. This can
        /// be accomplished by decorating the view model with the <see cref="InterestedInAttribute"/>.
        /// </summary>
        /// <param name="viewModel">The view model.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <remarks>
        /// This method is internal so the <see cref="ManagedViewModel"/> can invoke it. This method is only used as a pass-through
        /// to the actual <see cref="OnViewModelPropertyChanged"/> method.
        /// </remarks>
        internal void ViewModelPropertyChanged(IViewModel viewModel, string propertyName)
        {
            OnViewModelPropertyChanged(viewModel, propertyName);
        }

        /// <summary>
        /// Called when a property has changed for a view model type that the current view model is interested in. This can
        /// be accomplished by decorating the view model with the <see cref="InterestedInAttribute"/>.
        /// </summary>
        /// <param name="viewModel">The view model.</param>
        /// <param name="propertyName">Name of the property.</param>
        protected virtual void OnViewModelPropertyChanged(IViewModel viewModel, string propertyName)
        {
            Log.Debug(TraceMessages.InterestingViewModelCallPropertyChanged, viewModel.GetType(), GetType(), propertyName);
        }

        /// <summary>
        /// Begins an edit on an object. Also correctly supports LLBLGen entities.
        /// </summary>
        /// <param name="obj">The object.</param>
        private static void BeginEditObject(object obj)
        {
            if (obj == null)
            {
                return;
            }

            if (obj.IsEntity())
            {
                obj.SaveFields(LLBLGenRestorePointName);
            }
            else if (obj is IEditableObject)
            {
                var editableModel = (IEditableObject)obj;
                editableModel.BeginEdit();
            }
        }

        /// <summary>
        /// Pushes changes since the last <see cref="IEditableObject.EndEdit()"/> call. Also correctly supports LLBLGen entities.
        /// </summary>
        /// <param name="obj">The object.</param>
        private static void EndEditObject(object obj)
        {
            if (obj == null)
            {
                return;
            }

            if (obj.IsEntity())
            {
                // TODO: Have a discussion whether we should discard all saved fields. This might have impact on other saved
                // fields that the user might have created
            }
            else if (obj is IEditableObject)
            {
                var editableModel = (IEditableObject)obj;
                editableModel.EndEdit();
            }
        }

        /// <summary>
        /// Discards changes since the last <see cref="IEditableObject.BeginEdit()"/> call. Also correctly supports LLBLGen entities.
        /// </summary>
        /// <param name="obj">The object.</param>
        private static void CancelEditObject(object obj)
        {
            if (obj == null)
            {
                return;
            }

            if (obj.IsEntity())
            {
                obj.RollbackFieldsAndRelations(LLBLGenRestorePointName);
            }
            else if (obj is IEditableObject)
            {
                var editableModel = (IEditableObject)obj;
                editableModel.CancelEdit();
            }
        }

        /// <summary>
        /// Handles the PropertyChanged event of a Model.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            foreach (KeyValuePair<string, ViewModelToModelMapping> map in _viewModelToModelMap)
            {
                ViewModelToModelMapping mapping = map.Value;
                if (mapping.ValueProperty == e.PropertyName)
                {
                    // Check if this is the right model (duplicate mappings might exist)
                    if (_modelObjects[mapping.ModelProperty] == sender)
                    {
                        object viewModelValue = GetValue(mapping.ViewModelProperty);
                        object modelValue = PropertyHelper.GetPropertyValue(sender, e.PropertyName);
                        if (!TypeHelper.AreObjectsEqual(viewModelValue, modelValue))
                        {
                            bool updatePropertyValue = true;

                            if (mapping.SupportLLBLGen)
                            {
                                object relatedFieldValue = PropertyHelper.GetPropertyValue(sender, mapping.ModelLLBLGenEntityProperty);
                                if (TypeHelper.AreObjectsEqual(relatedFieldValue, mapping.NullValue))
                                {
                                    // Do not change the value, the view model probably set the value to the null-value
                                    updatePropertyValue = false;
                                }
                            }

                            if (updatePropertyValue)
                            {
                                SetValue(mapping.ViewModelProperty, modelValue);
                            }
                        }

                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Called when the object is validating.
        /// </summary>
        protected override void OnValidating()
        {
            base.OnValidating();

            lock (_modelObjects)
            {
                foreach (KeyValuePair<string, object> model in _modelObjects)
                {
                    if (model.Value is DataObjectBase)
                    {
                        ((DataObjectBase)model.Value).Validate();
                    }
                    else if (_modelObjectValidateEntityMethods.ContainsKey(model.Key))
                    {
                        _modelObjectValidateEntityMethods[model.Key].Invoke(model.Value, new object[] { });
                    }
                }
            }

            lock (_childViewModels)
            {
                _childViewModelsHaveErrors = false;

                foreach (IViewModel childViewModel in _childViewModels)
                {
                    childViewModel.Validate();
                    if (childViewModel.HasErrors)
                    {
                        _childViewModelsHaveErrors = true;
                    }
                }
            }
        }

        /// <summary>
        /// Called when the object is validating the fields.
        /// </summary>
        protected override void OnValidatingFields()
        {
            base.OnValidatingFields();

            // Map all field errors and warnings from the model to this viewmodel
            foreach (KeyValuePair<string, ViewModelToModelMapping> viewModelToModelMap in _viewModelToModelMap)
            {
                ViewModelToModelMapping mapping = viewModelToModelMap.Value;
                var model = GetValue(mapping.ModelProperty);
                string modelProperty = mapping.SupportLLBLGen ? mapping.ModelLLBLGenEntityProperty : mapping.ValueProperty;

                // Error
                var dataErrorInfo = model as IDataErrorInfo;
                if (dataErrorInfo != null)
                {
                    if (!string.IsNullOrEmpty(dataErrorInfo[modelProperty]))
                    {
                        SetFieldError(mapping.ViewModelProperty, dataErrorInfo[modelProperty]);
                    }
                }

                // Warning
                var dataWarningInfo = model as IDataWarningInfo;
                if (dataWarningInfo != null)
                {
                    if (!string.IsNullOrEmpty(dataWarningInfo[modelProperty]))
                    {
                        SetFieldWarning(mapping.ViewModelProperty, dataWarningInfo[modelProperty]);
                    }
                }
            }
        }

        /// <summary>
        /// Called when the object is validating the business rules.
        /// </summary>
        protected override void OnValidatingBusinessRules()
        {
            base.OnValidatingBusinessRules();

            lock (_modelObjects)
            {
                foreach (KeyValuePair<string, object> modelObject in _modelObjects)
                {
                    // Error
                    var dataErrorInfo = modelObject.Value as IDataErrorInfo;
                    if (dataErrorInfo != null)
                    {
                        SetBusinessRuleError(dataErrorInfo.Error);
                    }

                    // Warning
                    var dataWarningInfo = modelObject.Value as IDataWarningInfo;
                    if (dataWarningInfo != null)
                    {
                        SetBusinessRuleWarning(dataWarningInfo.Warning);
                    }
                }
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposeManagedResources"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposeManagedResources)
        {
            if (disposeManagedResources)
            {
                ViewModelManager.UnregisterViewModelInstance(this);
            }

            base.Dispose(disposeManagedResources);
        }

        /// <summary>
        /// Initializes the object by setting default values.
        /// </summary>
        protected virtual void Initialize() { }

        /// <summary>
        /// Cancels the editing of the data.
        /// </summary>
        protected virtual void Cancel() { }

        /// <summary>
        /// Saves the data.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if successful; otherwise <c>false</c>.
        /// </returns>
        protected virtual bool Save() { return true; }

        /// <summary>
        /// Closes this instance. Always called after the <see cref="Cancel"/> of <see cref="Save"/> method.
        /// </summary>
        /// <remarks>
        /// When implementing this method in a base class, make sure to call the base, otherwise <see cref="IsClosed"/> will
        /// not be set to true.
        /// </remarks>
        protected virtual void Close()
        {
            IsClosed = true;
        }

        /// <summary>
        /// Determines whether a specific property is registered as a model.
        /// </summary>
        /// <param name="name">The name of the registered model.</param>
        /// <returns>
        /// 	<c>true</c> if a specific property is registered as a model; otherwise, <c>false</c>.
        /// </returns>
        protected bool IsModelRegistered(string name)
        {
            if (!IsPropertyRegistered(name))
            {
                return false;
            }

            return _modelObjects.ContainsKey(name);
        }

        /// <summary>
        /// Registers all the commands that implement the <see cref="ICatelCommand"/>.
        /// </summary>
        private void RegisterCommands()
        {
            lock (_registeredCommands)
            {
                Type type = GetType();
                List<PropertyInfo> properties = new List<PropertyInfo>();
                properties.AddRange(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));

                foreach (PropertyInfo propertyInfo in properties)
                {
                    if (propertyInfo.PropertyType.GetInterface(typeof(ICatelCommand).Name, false) != null)
                    {
                        ICatelCommand command = propertyInfo.GetValue(this, null) as ICatelCommand;
                        if (command != null)
                        {
                            Log.Debug(TraceMessages.FoundCommandOnViewModel, propertyInfo.Name, type.Name);

                            _registeredCommands.Add(command);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Invalidates all the commands that implement the <see cref="ICatelCommand"/>.
        /// </summary>
        public void InvalidateCommands()
        {
            lock (_registeredCommands)
            {
                foreach (ICatelCommand command in _registeredCommands)
                {
                    command.RaiseCanExecuteChanged();
                }
            }
        }
        #endregion

        #region Services
        /// <summary>
        /// Gets the service of the specified type.
        /// </summary>
        /// <param name="serviceType">Type of the service.</param>
        /// <returns>Service object or <c>null</c> if the service is not found.</returns>
        public object GetService(Type serviceType)
        {
            return ViewModelServiceManager.GetService(serviceType);
        }

        /// <summary>
        /// Gets the service of the specified type.
        /// </summary>
        /// <typeparam name="T">Type of the service.</typeparam>
        /// <returns>Service object or <c>null</c> if the service is not found.</returns>
        public T GetService<T>()
        {
            return (T)GetService(typeof(T));
        }

        /// <summary>
        /// Registers the known view model services.
        /// </summary>
        /// <param name="container">The IoC container.</param>
        protected abstract void RegisterViewModelServices(IUnityContainer container);
        #endregion

        #region IViewModel Members
        /// <summary>
        /// Initializes the object by setting default values.
        /// </summary>
        void IViewModel.Initialize()
        {
            Initialize();

            RegisterCommands();

            IsInitialized = true;

            OnPropertyChanged("Title");

            SuspendValidation = false;
        }

        /// <summary>
        /// Validates the data.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if validation succeeds; otherwise <c>false</c>.
        /// </returns>
        bool IViewModel.Validate()
        {
            return ((IViewModel)this).Validate(false, true);
        }

        /// <summary>
        /// Validates the specified notify changed properties only.
        /// </summary>
        /// <param name="force">if set to <c>true</c>, a validation is forced (even if the object knows it is already validated).</param>
        /// <param name="notifyChangedPropertiesOnly">if set to <c>true</c> only the properties for which the warnings or errors have been changed
        /// will be updated via <see cref="INotifyPropertyChanged.PropertyChanged"/>; otherwise all the properties that
        /// had warnings or errors but not anymore and properties still containing warnings or errors will be updated.</param>
        /// <returns>
        /// 	<c>true</c> if validation succeeds; otherwise <c>false</c>.
        /// </returns>
        /// <remarks>
        /// This method is useful when the view model is initialized before the window, and therefore WPF does not update the errors and warnings.
        /// </remarks>
        bool IViewModel.Validate(bool force, bool notifyChangedPropertiesOnly)
        {
            if (IsClosed)
            {
                return true;
            }

            Validate(force, notifyChangedPropertiesOnly);

            return !HasErrors;
        }

        /// <summary>
        /// Cancels the editing of the data.
        /// </summary>
        void IViewModel.Cancel()
        {
            if (Canceling != null)
            {
                Canceling(this, EventArgs.Empty);
            }

            if (SupportIEditableObject)
            {
                lock (_modelObjects)
                {
                    foreach (KeyValuePair<string, object> modelKeyValuePair in _modelObjects)
                    {
                        try
                        {
                            if (_modelObjectsInfo[modelKeyValuePair.Key].SupportIEditableObject)
                            {
                                if (!(modelKeyValuePair.Value is DataObjectBase) || ((DataObjectBase)modelKeyValuePair.Value).IsInEditSession)
                                {
                                    CancelEditObject(modelKeyValuePair.Value);
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            Log.Warn(ex, TraceMessages.FailedToCancelEditOfModel, modelKeyValuePair.Key);
                        }
                    }
                }
            }

            Cancel();

            Log.Info(TraceMessages.CanceledViewModel, GetType());

            if (Canceled != null)
            {
                Canceled(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Cancels the editing of the data, but also closes the view model in the same call.
        /// </summary>
        void IViewModel.CancelAndClose()
        {
            ((IViewModel)this).Cancel();
            ((IViewModel)this).Close();
        }

        /// <summary>
        /// Saves the data.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if successful; otherwise <c>false</c>.
        /// </returns>
        bool IViewModel.Save()
        {
            if (Saving != null)
            {
                Saving(this, EventArgs.Empty);
            }

            bool saved = Save();
            
            Log.Info(saved ? TraceMessages.SavedViewModel : TraceMessages.FailedToSaveViewModel, GetType());

            if (saved)
            {
                if (SupportIEditableObject)
                {
                    lock (_modelObjects)
                    {
                        foreach (KeyValuePair<string, object> modelKeyValuePair in _modelObjects)
                        {
                            try
                            {
                                if (_modelObjectsInfo[modelKeyValuePair.Key].SupportIEditableObject)
                                {
                                    if (!(modelKeyValuePair.Value is DataObjectBase) || ((DataObjectBase) modelKeyValuePair.Value).IsInEditSession)
                                    {
                                        // End edit
                                        EndEditObject(modelKeyValuePair.Value);
                                    }
                                }
                            }
                            catch (Exception ex)
                            {
                                Log.Warn(ex, TraceMessages.FailedToEndEditOfModel, modelKeyValuePair.Key);
                            }
                        }
                    }
                }

                if (Saved != null)
                {
                    Saved(this, EventArgs.Empty);
                }
            }

            return saved;
        }

        /// <summary>
        /// Saves the data, but also closes the view model in the same call if the save succeeds.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if successful; otherwise <c>false</c>.
        /// </returns>
        bool IViewModel.SaveAndClose()
        {
            bool result = ((IViewModel)this).Save();
            if (result)
            {
                ((IViewModel)this).Close();
            }

            return result;
        }

        /// <summary>
        /// Closes this instance. Always called after the <see cref="Cancel"/> of <see cref="Save"/> method.
        /// </summary>
        void IViewModel.Close()
        {
            Close();

            Log.Info(TraceMessages.ClosedViewModel, GetType());

            if (Closed != null)
            {
                Closed(this, EventArgs.Empty);
            }
        }
        #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 Code Project Open License (CPOL)


Written By
Software Developer
Netherlands Netherlands
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions