// --------------------------------------------------------------------------------------------------------------------
// <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
}
}