Click here to Skip to main content
15,881,455 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.8K   572   11  
This article explains how to write unit tests for MVVM using Catel.
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UserControl.cs" company="Catel development team">
//   Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
//   <see cref="UserControl" /> that supports MVVM by using a <see cref="IViewModel" /> typed parameter.
//   If the user control is not constructed with the right view model by the developer, it will try to create
//   the view model itself. It does this by keeping an eye on the <see cref="UserControl.DataContext" /> property. If
//   the property changes, the control will check the type of the DataContext and try to create the view model by using
//   the DataContext value as the constructor. If the view model can be constructed, the DataContext of the UserControl will
//   be replaced by the view model.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using Catel.MVVM.UI;
using Catel.Reflection;
using Catel.MVVM;
using Catel.Windows.Properties;
using log4net;

namespace Catel.Windows.Controls
{
    /// <summary>
    /// <see cref="UserControl"/> that supports MVVM by using a <see cref="IViewModel"/> typed parameter.
    /// If the user control is not constructed with the right view model by the developer, it will try to create
    /// the view model itself. It does this by keeping an eye on the <see cref="UserControl.DataContext"/> property. If
    /// the property changes, the control will check the type of the DataContext and try to create the view model by using
    /// the DataContext value as the constructor. If the view model can be constructed, the DataContext of the UserControl will
    /// be replaced by the view model.
    /// </summary>
    /// <typeparam name="TViewModel">The type of the view model.</typeparam>
    /// <remarks>
    /// This control suffers a lot from the bugs, or features "by design" as Microsoft likes to call it, of WPF. Below are the most 
    /// common issues that this control suffers from:
    /// <para />
    /// 1) WPF sometimes invokes the Loaded multiple times, without invoking Unloaded.
    /// <para />
    /// 2) The data context cannot be changed before the <see cref="UserControl.IsLoaded"/> property is true. This means that you will get a lot of binding errors,
    ///    but they are invalid. We think the reason you cannot change the DataContext before the IsLoaded property is true because of performance reasons.
    /// </remarks>
    public abstract class UserControl<TViewModel> : UserControl, IViewModelContainer
        where TViewModel : class, IViewModel
    {
        #region Variables
        /// <summary>
        /// The <see cref="ILog">log</see> object.
        /// </summary>
        protected static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        private IViewModel _viewModel;
        private bool _viewModelInitialized;
        private bool _updateDataContextOnLoad;

        private object _realDataContext;
        private DependencyObject _realDataContextOwner;
        private BindingBase _realDataContextBinding;
        private object _realDataContextPropertyValue;
        private string _dataContextPropertyName;
#if !SILVERLIGHT
        private DependencyPropertyDescriptor _dataContextDependencyPropertyDescriptor;
#endif

        private IViewModelContainer _parentViewModelContainer;
        private IViewModel _parentViewModel;

        private bool _isLoaded;
        private bool _isFirstValidation = true;
        private bool _isFirstDataContextChangeWhenControlIsAlreadyLoaded;
        private InfoBarMessageControl _infoBarMessageControl;
        #endregion

        #region Constructor & destructor
        /// <summary>
        /// Initializes a new instance of the <see cref="UserControl{TViewModel}"/> class.
        /// </summary>
        protected UserControl()
            : this(null) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="UserControl{TViewModel}"/> class.
        /// </summary>
        /// <param name="viewModel">The view model.</param>
        protected UserControl(TViewModel viewModel)
        {
            SupportParentViewModelContainers = true;

            ControlToViewModelMappingHelper.InitializeControlToViewModelMappings(this);

            if (viewModel != null)
            {
                ViewModel = viewModel;
            }
            else
            {
                Type viewModelType = typeof(TViewModel);

                try
                {
                    // Check if the object has an empty constructor
                    ConstructorInfo ctorInfo = viewModelType.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
                    if (ctorInfo != null)
                    {
                        ViewModel = (TViewModel)ctorInfo.Invoke(null);
                    }
                    else
                    {
                        Log.Debug(TraceMessages.CannotConstructViewModelNextTryOnDataContextChange, viewModelType);
                    }
                }
                catch (Exception ex)
                {
                    Log.Error(ex, TraceMessages.CannotConstructViewModelNextTryOnDataContextChange, viewModelType);
                }
            }

            if (_viewModel != null)
            {
                DataContext = _viewModel;
            }

#if SILVERLIGHT
            SetBinding(DataContextEventWrapperProperty, new Binding());
#else
            DataContextChanged += OnDataContextChanged;
#endif

            Loaded += delegate { OnLoaded(); };
            Unloaded += delegate { OnUnloaded(); };
        }
        #endregion

        #region Properties
#if SILVERLIGHT

        /// <summary>
        /// Since Silverlight doesn't provide a DataContextChanged event, we have to take care of this ourselves.
        /// </summary>
        /// <remarks>
        /// Solution originally comes from http://msmvps.com/blogs/theproblemsolver/archive/2008/12/29/how-to-know-when-the-datacontext-changed-in-your-control.aspx.
        /// </remarks>
        private static readonly DependencyProperty DataContextEventWrapperProperty =
            DependencyProperty.Register("MyProperty", typeof(object), typeof(UserControl<TViewModel>), 
            new PropertyMetadata((sender, e) => ((UserControl<TViewModel>)sender).OnDataContextChanged(sender, e)));
#endif

        /// <summary>
        /// Gets the view model that is contained by the container.
        /// </summary>
        /// <value>The view model.</value>
        IViewModel IViewModelContainer.ViewModel
        {
            get { return ViewModel; }
        }

        /// <summary>
        /// Gets the view model.
        /// </summary>
        /// <value>The view model.</value>
        public TViewModel ViewModel
        {
            get { return (TViewModel)_viewModel; }
            private set
            {
                if (_viewModel == value)
                {
                    return;
                }

                UnregisterViewModelAsChild();

                _viewModel = value;

                RegisterViewModelAsChild();

                OnViewModelChanged();
            }
        }

        /// <summary>
        /// Gets a value indicating whether there is a real DataContext this control is watching.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if there is a real DataContext this control is watching; otherwise, <c>false</c>.
        /// </value>
        protected bool HasRealDataContext { get { return _realDataContext != null; } }

        /// <summary>
        /// Gets a value indicating whether there is a parent view model container available.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if there is a parent view model container available; otherwise, <c>false</c>.
        /// </value>
        protected bool HasParentViewModelContainer { get { return _parentViewModelContainer != null; } }

        /// <summary>
        /// Gets a value indicating whether this instance is subscribed to a parent view model.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is subscribed to a parent view model; otherwise, <c>false</c>.
        /// </value>
        protected bool IsSubscribedToParentViewModel { get { return (_parentViewModel != null); } }

        /// <summary>
        /// Gets or sets a value indicating whether parent view model containers are supported. If supported,
        /// the user control will search for a <see cref="DependencyObject"/> that implements the <see cref="IViewModelContainer"/>
        /// interface. During this search, the user control will use both the visual and logical tree.
        /// <para />
        /// If a user control does not have any parent control implementing the <see cref="IViewModelContainer"/> interface, searching
        /// for it is useless and requires the control to search all the way to the top for the implementation. To prevent this from
        /// happening, set this property to <c>false</c>.
        /// <para />
        /// The default value is <c>true</c>.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if parent view model containers are supported; otherwise, <c>false</c>.
        /// </value>
        public bool SupportParentViewModelContainers { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether to skip the search for an info bar message control. If not skipped,
        /// the user control will search for a the first <see cref="InfoBarMessageControl"/> that can be found. 
        /// During this search, the user control will use both the visual and logical tree.
        /// <para />
        /// If a user control does not have any <see cref="InfoBarMessageControl"/>, searching
        /// for it is useless and requires the control to search all the way to the top for the implementation. To prevent this from
        /// happening, set this property to <c>true</c>.
        /// <para />
        /// The default value is <c>false</c>.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if the search for an info bar message control should be skipped; otherwise, <c>false</c>.
        /// </value>
        public bool SkipSearchingForInfoBarMessageControl { get; set; }
        #endregion

        #region Events
#if !SILVERLIGHT
        /// <summary>
        /// Occurs when a property on the container has changed.
        /// </summary>
        /// <remarks>
        /// This event makes it possible to externally subscribe to property changes of a <see cref="DependencyObject"/>
        /// (mostly the container of a view model) because the .NET Framework does not allows us to.
        /// </remarks>
        public event EventHandler<PropertyChangedEventArgs> PropertyChanged;
#endif

        /// <summary>
        /// Occurs when the <see cref="ViewModel"/> property has changed.
        /// </summary>
        public event EventHandler<EventArgs> ViewModelChanged;
        #endregion

        #region Methods
        /// <summary>
        /// Validates the data.
        /// </summary>
        /// <returns>True if successful, otherwise false.</returns>
        protected bool ValidateData()
        {
            // Make sure we have a view model
            if (_viewModel == null)
            {
                return false;
            }

            if (_viewModel.IsInitialized)
            {
                // Validate (first time, force validation)
                _viewModel.Validate(_isFirstValidation, false);
                _isFirstValidation = false;
            }

            // Return if there are errors
            return !_viewModel.HasErrors;
        }

        /// <summary>
        /// Discards all changes made by this window.
        /// </summary>
        protected void DiscardChanges()
        {
            if (_viewModel != null)
            {
                _viewModel.Cancel();
            }
        }

        /// <summary>
        /// Applies all changes made by this window.
        /// </summary>
        /// <returns>True if successful, otherwise false.</returns>
        protected bool ApplyChanges()
        {
            if (_viewModel == null)
            {
                return false;
            }

            return _viewModel.Save();
        }

        /// <summary>
        /// Called when the data context has changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            DetermineRealDataContext();

            if (e.OldValue != null)
            {
                ClearWarningsAndErrorsForObject(e.OldValue);
            }

            // Only update if the new value does not equal the current view model
            // We should also ignore null values because this control sets the DataContext to null
            // to force a refresh (thanks to buggy WPF)
            if ((e.NewValue != _viewModel) && (e.NewValue != null || !_updateDataContextOnLoad))
            {
                UpdateDataContextToUseViewModel(e.OldValue, e.NewValue);
            }
        }

        /// <summary>
        /// Initializes the user control.
        /// </summary>
        private void OnLoaded()
        {
            Log.Debug(TraceMessages.OnLoadedInObject, GetType().Name);

            // Don't do this again (another bug in WPF: OnLoaded is called more than OnUnloaded)
            if (_isLoaded)
            {
                return;
            }

            _isLoaded = true;

            ControlToViewModelMappingHelper.InitializeControlToViewModelMappings(this);

            DetermineRealDataContext();

            if (!SkipSearchingForInfoBarMessageControl)
            {
                Log.StartStopwatchTrace(TraceMessages.SearchingForInfoBarMessageControl);

                _infoBarMessageControl = FindParentByPredicate(o => o is InfoBarMessageControl) as InfoBarMessageControl;

                Log.StopStopwatchTrace(TraceMessages.SearchingForInfoBarMessageControl);

                if (_infoBarMessageControl == null)
                {
                    Log.Warn(TraceMessages.NoInfoBarMessageControlIsFoundConsiderUsingSkipSearchingForInfoBarMessageControlProperty, GetType().Name);
                }
            }
            else
            {
                Log.Debug(TraceMessages.SkippingSearchForInfoBarMessageControl);
            }

            // If there is a message bar, wrap a warning and error validator around the content
            if (_infoBarMessageControl != null)
            {
                if (WrapControlHelper.CanBeWrapped(Content as FrameworkElement))
                {
                    WrapControlHelper.Wrap(Content as FrameworkElement, WrapOptions.GenerateWarningAndErrorValidatorForDataContext, this as ContentControl);
                }
            }

            if (_updateDataContextOnLoad)
            {
                object dataContext = GetRealDataContext();
                UpdateDataContextToUseViewModel(null, dataContext);
            }
            else if (_viewModel == null)
            {
                // We don't have a view model yet, but we also don't have to update the new data context. This means that
                // the control doesn't have a valid data context yet, so the next DataContext change is the first one when
                // the control is already loaded.
                _isFirstDataContextChangeWhenControlIsAlreadyLoaded = true;
            }

            if ((_viewModel != null) && !_viewModelInitialized)
            {
                _viewModel.Initialize();
                _viewModelInitialized = true;
            }

            // Force validation the first time. Do this via the dispatcher so WPF can actually update
            Dispatcher.BeginInvoke((Action)delegate
                                                {
                                                    if (_viewModel != null)
                                                    {
                                                        _viewModel.Validate(true, false);
                                                    }
                                                });
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Window.Closed"/> event.
        /// </summary>
        private void OnUnloaded()
        {
            Log.Debug(TraceMessages.OnUnloadedInObject, GetType().Name);

            ControlToViewModelMappingHelper.UninitializeControlToViewModelMappings(this);

            UnsubscribeFromRealDataContext();

            UnsubscribeFromParentViewModelContainer();

            if (DataContext != null)
            {
                ClearWarningsAndErrorsForObject(DataContext);
            }

            CloseAndDiposeViewModel();

            _isLoaded = false;
        }

#if !SILVERLIGHT
        /// <summary>
        /// Invoked whenever the effective value of any dependency property on this <see cref="T:System.Windows.FrameworkElement"/> has been updated. The specific dependency property that changed is reported in the arguments parameter. Overrides <see cref="M:System.Windows.DependencyObject.OnPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs)"/>.
        /// </summary>
        /// <param name="e">The event data that describes the property that changed, as well as old and new values.</param>
        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (e.Property == DataContextProperty)
            {
                // Only invoke the event when the value is correct
                if ((e.NewValue == null) || (e.NewValue is TViewModel))
                {
                    base.OnPropertyChanged(e);
                }
                else
                {
                    // Handle the update ourselves
                    OnDataContextChanged(this, e);
                }

                return;
            }

            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(e.Property.Name));
            }

            base.OnPropertyChanged(e);
        }
#endif

        /// <summary>
        /// Called when the view model has changed.
        /// </summary>
        /// <remarks>
        /// If this method is overriden, it is important to call the base as well.
        /// </remarks>
        protected virtual void OnViewModelChanged()
        {
            if (ViewModelChanged != null)
            {
                ViewModelChanged(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Determines the real data context. If there already is a real data context, no new data context will
        /// be determined.
        /// </summary>
        private void DetermineRealDataContext()
        {
            if (HasRealDataContext)
            {
                return;
            }

            FrameworkElement element = this;
            while (element != null)
            {
                BindingExpression binding = element.GetBindingExpression(DataContextProperty);
                if (binding != null)
                {
                    SubscribeToRealDataContext(element, binding);
                    break;
                }

#if SILVERLIGHT
                element = element.Parent as FrameworkElement;
#else
                element = (element.Parent ?? element.TemplatedParent) as FrameworkElement;
#endif
            }
        }

        /// <summary>
        /// Subscribes to the real DataContext so changes can be reflected.
        /// </summary>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="binding">The binding.</param>
        private void SubscribeToRealDataContext(UIElement parentElement, BindingExpression binding)
        {
            if (HasRealDataContext)
            {
                return;
            }

            _dataContextPropertyName = binding.ParentBinding.Path.Path;

#if !SILVERLIGHT
            // There is currenly a binding, but we need to clean it up because the data context will 
            // not be used by bindings (even when we set it here). Make sure to watch the binding since
            // the value might change (and then we need to update as well). First check if it is a dependency object,
            // then use the DependencyPropertyMetaData.
            DependencyObject dataItemAsDependencyObject = binding.DataItem as DependencyObject;
            if (dataItemAsDependencyObject != null)
            {
                if (!string.IsNullOrEmpty(_dataContextPropertyName))
                {
                    DependencyPropertyDescriptor descriptor = GetDependencyPropertyDescription(dataItemAsDependencyObject, _dataContextPropertyName);
                    if (descriptor != null)
                    {
                        Log.Debug(TraceMessages.SubscribedToRealDataContextViaDependencyProperty, _dataContextPropertyName);

                        // Store the real data context
                        _realDataContext = binding.DataItem;
                        _realDataContextOwner = parentElement;
                        _realDataContextBinding = BindingOperations.GetBinding(parentElement, FrameworkElement.DataContextProperty);
                        _dataContextDependencyPropertyDescriptor = descriptor;

                        descriptor.AddValueChanged(_realDataContext, RealDataContext_PropertyChangedViaDependencyProperty);

                        // TODO: In silverlight, we can do this (source: http://forums.silverlight.net/forums/t/47216.aspx)
                        // You can Use the Property ParentBinding property of GetBindingExpression for getting the equivalent of GetBinding in Silverlight

                        // Clear the binding (to prevent unwanted binding errors on the real datacontext)
                        // TODO: find a way to do this, ClearValue and DataContext = null do not work (actually update the data context,
                        // which triggers the UpdateDataContext twice for the same object)

                        return;
                    }
                }
            }
#endif

            // If we failed to subscribe via a dependency property, subscribe via INotifyPropertyChanged
            INotifyPropertyChanged dataItemAsNotifyPropertyChanged = binding.DataItem as INotifyPropertyChanged;
            if (dataItemAsNotifyPropertyChanged != null)
            {
                Log.Debug(TraceMessages.SubscribedToRealDataContextViaINotifyPropertyChanged);

                _realDataContext = binding.DataItem;
                _realDataContextOwner = parentElement;
#if SILVERLIGHT
                var bindingExpression = ((FrameworkElement) parentElement).GetBindingExpression(FrameworkElement.DataContextProperty);
                _realDataContextBinding = (bindingExpression != null) ? bindingExpression.ParentBinding : null;
#else
                _realDataContextBinding = BindingOperations.GetBinding(parentElement, FrameworkElement.DataContextProperty);
#endif

                // Clear the binding (to prevent unwanted binding errors on the real datacontext)
                // TODO: find a way to do this, ClearValue and DataContext = null do not work (actually update the data context,
                // which triggers the UpdateDataContext twice for the same object)

                dataItemAsNotifyPropertyChanged.PropertyChanged += RealDataContext_PropertyChangedViaINotifyPropertyChanged;
            }

            SubscribeToParentViewModelContainer();
        }

        /// <summary>
        /// Unsubscribes from the real DataContext.
        /// </summary>
        private void UnsubscribeFromRealDataContext()
        {
            if (!HasRealDataContext)
            {
                return;
            }

            // Restore binding if the current object is the real data context owner
            if (_realDataContextOwner == this)
            {
                Log.Debug(TraceMessages.RestoringDataContext, (_realDataContextBinding != null) ? ((Binding)_realDataContextBinding).Path.Path : "nothing (null)");

                BindingOperations.SetBinding(this, FrameworkElement.DataContextProperty, _realDataContextBinding);
            }

#if !SILVERLIGHT
            // First try via a dependency property
            if (_dataContextDependencyPropertyDescriptor != null)
            {
                Log.Debug(TraceMessages.UnsubscribedFromRealDataContextViaDependencyProperty, _dataContextPropertyName);

                _dataContextDependencyPropertyDescriptor.RemoveValueChanged(_realDataContext, RealDataContext_PropertyChangedViaDependencyProperty);
            }
#endif

            // If we failed to unsubscribe via a dependency property, unsubscribe via INotifyPropertyChanged
            INotifyPropertyChanged dataItemAsNotifyPropertyChanged = _realDataContext as INotifyPropertyChanged;
            if (dataItemAsNotifyPropertyChanged != null)
            {
                Log.Debug(TraceMessages.UnsubscribedFromRealDataContextViaINotifyPropertyChanged);

                dataItemAsNotifyPropertyChanged.PropertyChanged -= RealDataContext_PropertyChangedViaINotifyPropertyChanged;
            }

            _realDataContext = null;
            _realDataContextOwner = null;
            _realDataContextBinding = null;
#if !SILVERLIGHT
            _dataContextDependencyPropertyDescriptor = null;
#endif

            UnsubscribeFromParentViewModel();
        }

        /// <summary>
        /// Subscribes to the parent view model container.
        /// </summary>
        private void SubscribeToParentViewModelContainer()
        {
            if (!SupportParentViewModelContainers)
            {
                return;
            }

            if (HasParentViewModelContainer)
            {
                return;
            }

            _parentViewModelContainer = FindParentByPredicate(o => o is IViewModelContainer) as IViewModelContainer;
            if (_parentViewModelContainer != null)
            {
                Log.Debug(TraceMessages.FoundParentViewModelContainer, _parentViewModelContainer.GetType().Name, GetType().Name);
            }
            else
            {
                Log.Debug(TraceMessages.NotFoundParentViewModelContainer);
            }

            if (_parentViewModelContainer != null)
            {
                _parentViewModelContainer.ViewModelChanged += ParentViewModelContainer_ViewModelChanged;

                SubscribeToParentViewModel(_parentViewModelContainer.ViewModel);
            }
        }

        /// <summary>
        /// Unsubscribes from the parent view model container.
        /// </summary>
        private void UnsubscribeFromParentViewModelContainer()
        {
            if (_parentViewModelContainer != null)
            {
                _parentViewModelContainer.ViewModelChanged -= ParentViewModelContainer_ViewModelChanged;

                _parentViewModelContainer = null;
            }
        }

        /// <summary>
        /// Subscribes to a parent view model.
        /// </summary>
        /// <param name="parentViewModel">The parent view model.</param>
        private void SubscribeToParentViewModel(IViewModel parentViewModel)
        {
            if (parentViewModel != null)
            {
                _parentViewModel = parentViewModel;

                RegisterViewModelAsChild();

                _parentViewModel.Saving += ParentViewModel_Saving;
                _parentViewModel.Canceling += ParentViewModel_Canceling;

                Log.Debug(TraceMessages.SubscribedToParentViewModel, parentViewModel.GetType());
            }
        }

        /// <summary>
        /// Unsubscribes from a parent view model.
        /// </summary>
        private void UnsubscribeFromParentViewModel()
        {
            if (_parentViewModel != null)
            {
                UnregisterViewModelAsChild();

                _parentViewModel.Saving -= ParentViewModel_Saving;
                _parentViewModel.Canceling -= ParentViewModel_Canceling;

                _parentViewModel = null;

                Log.Debug(TraceMessages.UnsubscribedFromParentViewModel);
            }
        }

        /// <summary>
        /// Registers the view model as child on the parent view model.
        /// </summary>
        private void RegisterViewModelAsChild()
        {
            if ((_parentViewModel is ViewModelBase) && (_viewModel != null))
            {
                ((ViewModelBase)_parentViewModel).RegisterChildViewModel(_viewModel);
                ((ViewModelBase)_viewModel).SetParentViewModel(_parentViewModel);
            }
        }

        /// <summary>
        /// Unregisters the view model as child on the parent view model.
        /// </summary>
        private void UnregisterViewModelAsChild()
        {
            if ((_parentViewModel is ViewModelBase) && (_viewModel != null))
            {
                ((ViewModelBase)_viewModel).SetParentViewModel(null);
                ((ViewModelBase)_parentViewModel).UnregisterChildViewModel(_viewModel);
            }
        }

        /// <summary>
        /// Updates the data context to use view model.
        /// </summary>
        /// <param name="oldDataContext">The old data context.</param>
        /// <param name="newDataContext">The new data context.</param>
        private void UpdateDataContextToUseViewModel(object oldDataContext, object newDataContext)
        {
#if !SILVERLIGHT
            if (!IsLoaded)
            {
                _updateDataContextOnLoad = true;
                return;
            }
#endif

            SubscribeToParentViewModelContainer();

            // Check if the new data context is different from the old one (and this is not the data context update in the onload event)
            if ((newDataContext == _realDataContextPropertyValue) && (!_updateDataContextOnLoad))
            {
                return;
            }

            // Check if the new data context is null, and this is the first data context change when the control is already loaded
            // We need to do this because the first time, it seems that WPF is binding to the wrong object when the user control is
            // already loaded
            if ((newDataContext == null) && (_isFirstDataContextChangeWhenControlIsAlreadyLoaded))
            {
                return;
            }

            if (newDataContext != null)
            {
                if (!(newDataContext is TViewModel))
                {
                    try
                    {
                        if (_viewModel != null)
                        {
                            _viewModel.Cancel();

                            CloseAndDiposeViewModel();
                        }

                        // Try to construct the view model with the data context
                        TViewModel viewModel = (TViewModel)Activator.CreateInstance(typeof(TViewModel), new[] { newDataContext });

                        // Store the real data context property value (it is valid, because we succeeded to create the view model)
                        _realDataContextPropertyValue = newDataContext;

                        ViewModel = viewModel;

                        // Initialize if the control is already loaded (otherwise, the OnLoaded method will initialize the view model)
                        viewModel.Initialize();
                        _viewModelInitialized = true;

                        if (_isFirstDataContextChangeWhenControlIsAlreadyLoaded)
                        {
                            _isFirstDataContextChangeWhenControlIsAlreadyLoaded = false;

                            Log.Debug(TraceMessages.UpdatingDataContextViaDispatcher);

                            // Update data context via dispatcher (so the user control will re-bind). Setting it directly does NOT work
                            Dispatcher.BeginInvoke((Action)delegate
                                                               {
                                                                   // Set data context
                                                                   DataContext = _viewModel;

                                                                   // Log
                                                                   Log.Debug(TraceMessages.UpdatedDataContextViaDispatcher);
                                                               }, new object[] { });
                        }
                        else
                        {
                            if (_updateDataContextOnLoad)
                            {
                                // Set to null to clear the data context (otherwise, the control won't notice the new data context)
                                DataContext = null;
                            }

                            DataContext = _viewModel;

                            // Don't update data context on load any longer (we just did, didn't we?)
                            _updateDataContextOnLoad = false;

                            Log.Info(TraceMessages.ViewModelAutomaticallyConstructedByDataContextChange, typeof(TViewModel));
                        }
                    }
                    catch (MissingMethodException)
                    {
                        Log.Debug(TraceMessages.ViewModelNotAutomaticallyConstructedByDataContextChangeBecauseThereIsNoConstructor, typeof(TViewModel), newDataContext.GetType());
                    }
                    catch (Exception ex)
                    {
                        Log.Error(ex, TraceMessages.ViewModelNotAutomaticallyConstructedByDataContextChange, typeof(TViewModel));
                    }
                }
            }
            else
            {
                _realDataContextPropertyValue = null;

                if (_viewModel != null)
                {
                    _viewModel.Cancel();

                    CloseAndDiposeViewModel();

                    DataContext = null;
                }
            }
        }

        /// <summary>
        /// Closes and diposes the current view model.
        /// </summary>
        private void CloseAndDiposeViewModel()
        {
            if (_viewModel != null)
            {
                _viewModel.Close();

                if (_viewModel is IDisposable)
                {
                    ((IDisposable)_viewModel).Dispose();
                }

                ViewModel = null;
            }
        }

        /// <summary>
        /// Handles the PropertyChanged event of the real DataContext via dependency properties.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void RealDataContext_PropertyChangedViaDependencyProperty(object sender, EventArgs e)
        {
            UpdateToNewDataContextAfterRealDataContextChange(sender);
        }

        /// <summary>
        /// Handles the PropertyChanged event of the real DataContext via INotifyPropertyChanged.
        /// </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 RealDataContext_PropertyChangedViaINotifyPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == _dataContextPropertyName)
            {
                UpdateToNewDataContextAfterRealDataContextChange(sender);
            }
        }

        /// <summary>
        /// Called when the real data context property has changed (so we must update).
        /// </summary>
        /// <param name="sender">The sender which is the real data context.</param>
        private void UpdateToNewDataContextAfterRealDataContextChange(object sender)
        {
            // If we don't have a view model yet (we still need to update the first time, ignore)
            if (_updateDataContextOnLoad)
            {
                return;
            }

            // TODO: get value via path, see http://www.devx.com/tips/Tip/42272
            // Get value
            object newDataContextPropertyValue = PropertyHelper.GetPropertyValue(sender, _dataContextPropertyName);

            // Check if it differs from the previous object
            if (newDataContextPropertyValue != _realDataContextPropertyValue)
            {
                UpdateDataContextToUseViewModel(DataContext, newDataContextPropertyValue);
            }
        }

        /// <summary>
        /// Handles the ViewModelChanged event of the parent ViewModel container.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void ParentViewModelContainer_ViewModelChanged(object sender, EventArgs e)
        {
            UnsubscribeFromParentViewModel();

            SubscribeToParentViewModel(((IViewModelContainer)sender).ViewModel);
        }

        /// <summary>
        /// Handles the Canceling event of the parent ViewModel.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void ParentViewModel_Canceling(object sender, EventArgs e)
        {
            // The parent view model is canceled, cancel our viewmodel as well
            if (_viewModel != null)
            {
                Log.Info(TraceMessages.ParentViewModelIsCanceledThusCancelingViewModelToo, _parentViewModel.GetType(), _viewModel.GetType());

                _viewModel.Cancel();
            }
        }

        /// <summary>
        /// Handles the Saving event of the parent ViewModel.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void ParentViewModel_Saving(object sender, EventArgs e)
        {
            // The parent view model is saved, save our viewmodel as well
            if (_viewModel != null)
            {
                Log.Info(TraceMessages.ParentViewModelIsSavedThusSavingViewModel, _parentViewModel.GetType(), _viewModel.GetType());

                _viewModel.Save();
            }
        }

        /// <summary>
        /// Clears the warnings and errors for the specified object.
        /// </summary>
        /// <param name="obj">The object.</param>
        /// <remarks>
        /// Since there is a "bug" in the .NET Framework (DataContext issue), this method clears the current
        /// warnings and errors in the InfoBarMessageControl if available.
        /// </remarks>
        private void ClearWarningsAndErrorsForObject(object obj)
        {
            if (obj == null)
            {
                return;
            }

            if (_infoBarMessageControl != null)
            {
                _infoBarMessageControl.ClearObjectMessages(obj);

                Log.Debug(TraceMessages.ClearedAllWarningsAndErrorsOfObject, obj);
            }
        }

        /// <summary>
        /// Gets the real data context. This means that the value will first check whether there is a real data context. If so,
        /// that data context will be used. If there is no real data context, the actual data context will be returned.
        /// </summary>
        /// <returns></returns>
        private object GetRealDataContext()
        {
            if (HasRealDataContext)
            {
                if (string.IsNullOrEmpty(_dataContextPropertyName))
                {
                    return _realDataContext;
                }

                return PropertyHelper.GetPropertyValue(_realDataContext, _dataContextPropertyName);
            }

            return DataContext;
        }

#if !SILVERLIGHT
        /// <summary>
        /// Gets the dependency property description for the specified property.
        /// </summary>
        /// <param name="dependencyObject">The dependency object that owns the property.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns><see cref="DependencyPropertyDescriptor"/> or <c>null</c> if the property is not found.</returns>
        /// <remarks>
        /// This method needs a special naming convention to succeed. If a property is called "Name", then the registered
        /// dependency property needs to be registered as "NameProperty".
        /// </remarks>
        /// <exception cref="ArgumentNullException">when <paramref name="dependencyObject"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">when <paramref name="propertyName"/> is <c>null</c> or empty.</exception>
        private static DependencyPropertyDescriptor GetDependencyPropertyDescription(DependencyObject dependencyObject, string propertyName)
        {
            if (dependencyObject == null)
            {
                throw new ArgumentNullException("dependencyObject");
            }

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

            Type dependencyObjectType = dependencyObject.GetType();

            // Get the dependency property (assume naming convention)
            string expectedDependencyPropertyName = string.Format("{0}Property", propertyName);
            FieldInfo fieldInfo = dependencyObjectType.GetField(expectedDependencyPropertyName, BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
            if (fieldInfo == null)
            {
                Log.Warn(TraceMessages.DependencyPropertyFieldNotFound, expectedDependencyPropertyName, dependencyObjectType);
                return null;
            }

            // Get dependency property
            DependencyProperty dependencyProperty = fieldInfo.GetValue(null) as DependencyProperty;
            if (dependencyProperty == null)
            {
                Log.Warn(TraceMessages.FailedToGetValueAsDependencyProperty, expectedDependencyPropertyName, dependencyObjectType);
                return null;
            }

            // Get dependency property description
            DependencyPropertyDescriptor dependencyPropertyDescriptor = DependencyPropertyDescriptor.FromProperty(dependencyProperty, dependencyProperty.OwnerType);
            return dependencyPropertyDescriptor;
        }
#endif

        /// <summary>
        /// Finds a parent by predicate. It first tries to find the parent via the <see cref="System.Windows.Controls.UserControl.Parent"/> property, and if that
        /// doesn't satisfy, it uses the <see cref="UserControl.TemplatedParent"/> property.
        /// </summary>
        /// <param name="predicate">The predicate.</param>
        /// <returns><see cref="DependencyObject"/> or <c>null</c> if no parent is found that matches the predicate.</returns>
        private DependencyObject FindParentByPredicate(Predicate<object> predicate)
        {
            return FindParentByPredicate(predicate, -1);
        }

        /// <summary>
        /// Finds a parent by predicate. It first tries to find the parent via the <see cref="System.Windows.Controls.UserControl.Parent"/> property, and if that
        /// doesn't satisfy, it uses the <see cref="UserControl.TemplatedParent"/> property.
        /// </summary>
        /// <param name="predicate">The predicate.</param>
        /// <param name="maxDepth">The maximum number of levels to go up when searching for the parent. If smaller than 0, no maximum is used.</param>
        /// <returns>
        /// 	<see cref="DependencyObject"/> or <c>null</c> if no parent is found that matches the predicate.
        /// </returns>
        private DependencyObject FindParentByPredicate(Predicate<object> predicate, int maxDepth)
        {
            object foundParent = null;

            List<DependencyObject> parents = new List<DependencyObject>();
            if (Parent != null)
            {
                parents.Add(Parent);
            }
#if !SILVERLIGHT
            if (TemplatedParent != null)
            {
                parents.Add(TemplatedParent);
            }
#endif
            foreach (DependencyObject parent in parents)
            {
                foundParent = parent.FindLogicalOrVisualAncestor(predicate, maxDepth);
                if (foundParent != null)
                {
                    break;
                }
            }

            return foundParent as DependencyObject;
        }
        #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