Click here to Skip to main content
15,892,253 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 49.1K   572   11  
This article explains how to write unit tests for MVVM using Catel.
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ControlToViewModelMappingHelper.cs" company="Catel development team">
//   Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
//   Helper class to fix <see cref="ControlToViewModelMapping"/> for <see cref="FrameworkElement"/>.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using Catel.Reflection;
using Catel.Windows.Properties;
using log4net;

namespace Catel.MVVM.UI
{
    /// <summary>
    /// Helper class to fix <see cref="ControlToViewModelMapping"/> for <see cref="FrameworkElement"/>.
    /// </summary>
    internal class ControlToViewModelMappingHelper
    {
        #region Variables
        /// <summary>
        /// The <see cref="ILog">log</see> object.
        /// </summary>
        protected static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        /// <summary>
        /// Dictionary of <see cref="IViewModelContainer"/> instances managed by this helper class.
        /// </summary>
        private static readonly Dictionary<IViewModelContainer, ControlToViewModelMappingHelper> _viewModelContainers = new Dictionary<IViewModelContainer, ControlToViewModelMappingHelper>();

        /// <summary>
        /// Dictionary of <see cref="ControlToViewModelMappingContainer"/> instances per type (which should be a <see cref="DependencyObject"/>).
        /// </summary>
        private static readonly Dictionary<Type, ControlToViewModelMappingContainer> _controlToViewModelMappingContainers = new Dictionary<Type, ControlToViewModelMappingContainer>();

        /// <summary>
        /// List of properties in the view model that should be ignored.
        /// </summary>
        private readonly List<string> _ignoredViewModelChanges = new List<string>();

        /// <summary>
        /// List of properties in the user control that should be ignored.
        /// </summary>
        private readonly List<string> _ignoredControlChanges = new List<string>();
        #endregion

        #region Constructor & destructor
        /// <summary>
        /// Initializes a new instance of the <see cref="ControlToViewModelMappingHelper"/> class.
        /// </summary>
        /// <param name="viewModelContainer">The view model container.</param>
        /// <exception cref="ArgumentNullException">when <paramref name="viewModelContainer"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">when <paramref name="viewModelContainer"/> is not a <see cref="DependencyObject"/>.</exception>
        public ControlToViewModelMappingHelper(IViewModelContainer viewModelContainer)
        {
            if (viewModelContainer == null)
            {
                throw new ArgumentNullException("viewModelContainer");
            }

            if (!(viewModelContainer is DependencyObject))
            {
                throw new ArgumentException(Exceptions.ViewModelContainerMustBeOfTypeDependencyObject, "viewModelContainer");
            }

            ViewModelContainer = viewModelContainer;

            if (!_controlToViewModelMappingContainers.ContainsKey(ViewModelContainerType))
            {
                _controlToViewModelMappingContainers.Add(ViewModelContainerType, new ControlToViewModelMappingContainer((DependencyObject)viewModelContainer));
            }

            ViewModelContainer.ViewModelChanged += OnViewModelChanged;

#if !SILVERLIGHT
            ViewModelContainer.PropertyChanged += OnViewModelContainerPropertyChanged;
#endif

            InitializeViewModel(ViewModelContainer.ViewModel);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the view model container.
        /// </summary>
        /// <value>The view model container.</value>
        public IViewModelContainer ViewModelContainer { get; private set; }

        /// <summary>
        /// Gets the type of the view model container.
        /// </summary>
        /// <value>The type of the view model container.</value>
        private Type ViewModelContainerType { get { return ViewModelContainer.GetType(); } }

        /// <summary>
        /// Gets or sets the previous view model.
        /// </summary>
        /// <value>The previous view model.</value>
        private IViewModel PreviousViewModel { get; set; }

        /// <summary>
        /// Gets the current view model.
        /// </summary>
        /// <value>The current view model.</value>
        private IViewModel CurrentViewModel { get { return ViewModelContainer.ViewModel; } }
        #endregion

        #region Methods
        /// <summary>
        /// Initializes the <see cref="ControlToViewModelMapping"/> for the specified <see cref="IViewModelContainer"/>.
        /// </summary>
        /// <param name="viewModelContainer">The view model container to initialize the mappings for.</param>
        /// <exception cref="ArgumentNullException">when <paramref name="viewModelContainer"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">when <paramref name="viewModelContainer"/> is not a <see cref="DependencyObject"/>.</exception>
        public static void InitializeControlToViewModelMappings(IViewModelContainer viewModelContainer)
        {
#if SILVERLIGHT
            throw new NotSupportedInSilverlightException("DependencyObject has no OnPropertyChanged method or PropertyChanged event");
#endif

            if (viewModelContainer == null)
            {
                throw new ArgumentNullException("viewModelContainer");
            }

            if (_viewModelContainers.ContainsKey(viewModelContainer))
            {
                return;
            }

            Log.Debug(TraceMessages.InitializingViewModelContainer);

            _viewModelContainers.Add(viewModelContainer, new ControlToViewModelMappingHelper(viewModelContainer));

            Log.Debug(TraceMessages.InitializedViewModelContainer);
        }

        /// <summary>
        /// Uninitializes the <see cref="ControlToViewModelMapping"/> for the specified <see cref="IViewModelContainer"/>.
        /// </summary>
        /// <param name="viewModelContainer">The view model container the uninitialize the mappings for.</param>
        /// <exception cref="ArgumentNullException">when <paramref name="viewModelContainer"/> is <c>null</c>.</exception>
        public static void UninitializeControlToViewModelMappings(IViewModelContainer viewModelContainer)
        {
            if (viewModelContainer == null)
            {
                throw new ArgumentNullException("viewModelContainer");
            }

            if (_viewModelContainers.ContainsKey(viewModelContainer))
            {
                _viewModelContainers[viewModelContainer].UninitializeControlToViewModelMappings();
            }
        }

        /// <summary>
        /// Uninitializes the <see cref="ControlToViewModelMapping"/> for the registered <see cref="IViewModelContainer"/>.
        /// </summary>
        private void UninitializeControlToViewModelMappings()
        {
            Log.Debug(TraceMessages.UninitializingViewModelContainer);

            ViewModelContainer.ViewModelChanged -= OnViewModelChanged;

#if !SILVERLIGHT
            ViewModelContainer.PropertyChanged -= OnViewModelContainerPropertyChanged;
#endif

            UninitializeViewModel(CurrentViewModel);

            Log.Debug(TraceMessages.UninitializedViewModelContainer);
        }

        /// <summary>
        /// Initializes the specified view model.
        /// </summary>
        /// <param name="viewModel">The view model.</param>
        private void InitializeViewModel(IViewModel viewModel)
        {
            Log.Debug(Resources.InitializingViewModel);

            UninitializeViewModel(PreviousViewModel);

            PreviousViewModel = viewModel;

            if (viewModel != null)
            {
                // If there are mappings, sync them in the right way
                foreach (ControlToViewModelMapping mapping in _controlToViewModelMappingContainers[ViewModelContainerType].GetAllControlToViewModelMappings())
                {
                    try
                    {
                        if ((mapping.MappingType == ControlViewModelModelMappingType.TwoWayControlWins) ||
                            (mapping.MappingType == ControlViewModelModelMappingType.ControlToViewModel))
                        {
                            TransferValueFromControlToViewModel(viewModel, mapping.ControlPropertyName, mapping.ViewModelPropertyName);
                        }
                        else if ((mapping.MappingType == ControlViewModelModelMappingType.TwoWayViewModelWins) ||
                                 (mapping.MappingType == ControlViewModelModelMappingType.ViewModelToControl))
                        {
                            TransferValueFromViewModelToControl(viewModel, mapping.ControlPropertyName, mapping.ViewModelPropertyName);
                        }
                    }
                    catch (Exception ex)
                    {
                        Log.Error(ex, TraceMessages.FailedToTransferValueForControlToViewModelMapping, mapping.ControlPropertyName, mapping.ViewModelPropertyName);
                    }
                }

                viewModel.PropertyChanged += OnViewModelPropertyChanged;
            }

            Log.Debug(Resources.InitializedViewModel);
        }

        /// <summary>
        /// Uninitializes the specified view model.
        /// </summary>
        /// <param name="viewModel">The view model.</param>
        private void UninitializeViewModel(IViewModel viewModel)
        {
            if (viewModel == null)
            {
                return;
            }

            Log.Debug(TraceMessages.UninitializingViewModel, viewModel.GetType().Name);

            viewModel.PropertyChanged -= OnViewModelPropertyChanged;

            Log.Debug(TraceMessages.UninitializedViewModel, viewModel.GetType().Name);
        }

        /// <summary>
        /// Called when the view model on the view model container has changed.
        /// </summary>
        /// <param name="sender">The view model container.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        private void OnViewModelChanged(object sender, EventArgs e)
        {
            InitializeViewModel(CurrentViewModel);
        }

        /// <summary>
        /// Called when a property on the view model has changed.
        /// </summary>
        /// <param name="sender">The view model.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (_controlToViewModelMappingContainers[ViewModelContainerType].ContainsViewModelToControlMapping(e.PropertyName))
            {
                ControlToViewModelMapping mapping = _controlToViewModelMappingContainers[ViewModelContainerType].GetViewModelToControlMapping(e.PropertyName);
                if (_ignoredViewModelChanges.Contains(mapping.ControlPropertyName))
                {
                    Log.Debug(TraceMessages.IgnoredPropertyChangedEventForViewModel, mapping.ControlPropertyName);
                }
                else
                {
                    if ((mapping.MappingType == ControlViewModelModelMappingType.TwoWayDoNothing) ||
                        (mapping.MappingType == ControlViewModelModelMappingType.TwoWayControlWins) ||
                        (mapping.MappingType == ControlViewModelModelMappingType.TwoWayViewModelWins) ||
                        (mapping.MappingType == ControlViewModelModelMappingType.ViewModelToControl))
                    {
                        TransferValueFromViewModelToControl(CurrentViewModel, mapping.ControlPropertyName, mapping.ViewModelPropertyName);
                    }
                }
            }
        }

        /// <summary>
        /// Called when a property on the view model container has changed.
        /// </summary>
        /// <param name="sender">The view model container.</param>
        /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
        private void OnViewModelContainerPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (_controlToViewModelMappingContainers[ViewModelContainerType].ContainsControlToViewModelMapping(e.PropertyName))
            {
                ControlToViewModelMapping mapping = _controlToViewModelMappingContainers[ViewModelContainerType].GetControlToViewModelMapping(e.PropertyName);
                if (_ignoredControlChanges.Contains(mapping.ControlPropertyName))
                {
                    Log.Debug(TraceMessages.IgnoredPropertyChangedEventForControl, mapping.ControlPropertyName);
                }
                else
                {
                    if ((mapping.MappingType == ControlViewModelModelMappingType.TwoWayDoNothing) ||
                        (mapping.MappingType == ControlViewModelModelMappingType.TwoWayControlWins) ||
                        (mapping.MappingType == ControlViewModelModelMappingType.TwoWayViewModelWins) ||
                        (mapping.MappingType == ControlViewModelModelMappingType.ControlToViewModel))
                    {
                        TransferValueFromControlToViewModel(CurrentViewModel, mapping.ControlPropertyName, mapping.ViewModelPropertyName);
                    }
                }
            }
        }

        /// <summary>
        /// Transfers the value from a control property to the view model property.
        /// <para/>
        /// This method does nothing when <paramref name="viewModel"/> is <c>null</c>.
        /// </summary>
        /// <param name="viewModel">The view model.</param>
        /// <param name="controlPropertyName">Name of the control property.</param>
        /// <param name="viewModelPropertyName">Name of the view model property.</param>
        /// <exception cref="ArgumentException">when <paramref name="controlPropertyName"/> is <c>null</c> or empty.</exception>
        /// <exception cref="ArgumentException">when <paramref name="viewModelPropertyName"/> is <c>null</c> or empty.</exception>
        /// <remarks>
        /// This method does not check the type of the properties. If the types are incorrect, an exception will be thrown by
        /// the .NET Framework.
        /// </remarks>
        protected void TransferValueFromControlToViewModel(IViewModel viewModel, string controlPropertyName, string viewModelPropertyName)
        {
            if (viewModel == null)
            {
                Log.Warn(TraceMessages.CannotTransferValueFromControlToViewModelBecauseViewModelIsNull);
                return;
            }

            Log.Debug(TraceMessages.IgnoringNextPropertyChangedEventForViewModelProperty, viewModelPropertyName);

            // Ignore this property (we will soon receive an event that it has changed)
            if (!_ignoredViewModelChanges.Contains(viewModelPropertyName))
            {
                _ignoredViewModelChanges.Add(viewModelPropertyName);
            }

            TransferValue(ViewModelContainer, controlPropertyName, viewModel, viewModelPropertyName);

            Log.Debug(TraceMessages.StoppedIgnoringNextPropertyChangedEventForViewModelProperty, viewModelPropertyName);

            _ignoredViewModelChanges.Remove(viewModelPropertyName);
        }

        /// <summary>
        /// Transfers the value from a view model property to the control property.
        /// <para/>
        /// This method does nothing when <paramref name="viewModel"/> is <c>null</c>.
        /// </summary>
        /// <param name="viewModel">The view model.</param>
        /// <param name="controlPropertyName">Name of the control property.</param>
        /// <param name="viewModelPropertyName">Name of the view model property.</param>
        /// <exception cref="ArgumentException">when <paramref name="controlPropertyName"/> is <c>null</c> or empty.</exception>
        /// <exception cref="ArgumentException">when <paramref name="viewModelPropertyName"/> is <c>null</c> or empty.</exception>
        /// <remarks>
        /// This method does not check the type of the properties. If the types are incorrect, an exception will be thrown by
        /// the .NET Framework.
        /// </remarks>
        protected void TransferValueFromViewModelToControl(IViewModel viewModel, string controlPropertyName, string viewModelPropertyName)
        {
            if (viewModel == null)
            {
                Log.Warn(TraceMessages.CannotTransferValueFromViewModelToControlBecauseViewModelIsNull);
                return;
            }

            Log.Debug(TraceMessages.IgnoringNextPropertyChangedEventForControlProperty, controlPropertyName);

            if (!_ignoredControlChanges.Contains(controlPropertyName))
            {
                _ignoredControlChanges.Add(controlPropertyName);
            }

            TransferValue(viewModel, viewModelPropertyName, ViewModelContainer, controlPropertyName);

            Log.Debug(TraceMessages.StoppedIgnoringNextPropertyChangedEventForControlProperty, controlPropertyName);

            _ignoredControlChanges.Remove(controlPropertyName);
        }

        /// <summary>
        /// Transfers a value from the source property to the target property.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="sourcePropertyName">Name of the source property.</param>
        /// <param name="target">The target.</param>
        /// <param name="targetPropertyName">Name of the target property.</param>
        /// <exception cref="ArgumentNullException">when <paramref name="source"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentNullException">when <paramref name="target"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">when <paramref name="sourcePropertyName"/> is <c>null</c> or empty.</exception>
        /// <exception cref="ArgumentException">when <paramref name="targetPropertyName"/> is <c>null</c> or empty.</exception>
        /// <remarks>
        /// This method does not check the type of the properties. If the types are incorrect, an exception will be thrown by
        /// the .NET Framework.
        /// </remarks>
        private static void TransferValue(object source, string sourcePropertyName, object target, string targetPropertyName)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            if (string.IsNullOrEmpty(sourcePropertyName))
            {
                throw new ArgumentException(Exceptions.ArgumentCannotBeNullOrEmpty, "sourcePropertyName");
            }

            if (string.IsNullOrEmpty(targetPropertyName))
            {
                throw new ArgumentException(Exceptions.ArgumentCannotBeNullOrEmpty, "targetPropertyName");
            }

            object valueToTransfer = PropertyHelper.GetPropertyValue(source, sourcePropertyName);

            Log.Debug(TraceMessages.TransferringValue, source.GetType().Name, sourcePropertyName, target.GetType().Name, targetPropertyName);

            PropertyHelper.SetPropertyValue(target, targetPropertyName, valueToTransfer);

            Log.Debug(TraceMessages.TransferredValue);
        }
        #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