// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ViewModelBaseWithoutServices.cs" company="Catel development team">
// Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
// View model base for MVVM implementations. This class is based on the <see cref="DataObjectBase" />, and supports all
// common interfaces used by WPF.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows.Threading;
using Catel.ComponentModel;
using Catel.Data;
using Catel.LLBLGen;
using Catel.MVVM.Services;
using Catel.Properties;
using Catel.Reflection;
using log4net;
using Microsoft.Practices.Unity;
namespace Catel.MVVM
{
/// <summary>
/// View model base for MVVM implementations. This class is based on the <see cref="DataObjectBase"/>, and supports all
/// common interfaces used by WPF.
/// </summary>
/// <remarks>
/// This view model base does not add any services. The technique specific implementation should take care of that
/// (such as WPF, Silverlight, etc).
/// </remarks>
[AllowNonSerializableMembers]
public abstract class ViewModelBaseWithoutServices : DataObjectBase<ViewModelBaseWithoutServices>, IViewModel
{
#region Classes
/// <summary>
/// Class containing information about a specific model decorated with the <see cref="ModelAttribute"/>.
/// </summary>
private class ModelInfo
{
#region Variables
#endregion
#region Constructor & destructor
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices.ModelInfo"/> class.
/// </summary>
/// <param name="name">The name of the model property.</param>
/// <param name="attribute">The attribute.</param>
public ModelInfo(string name, ModelAttribute attribute)
{
Name = name;
SupportIEditableObject = attribute.SupportIEditableObject;
}
#endregion
#region Propertiess
/// <summary>
/// Gets the name of the model property.
/// </summary>
/// <value>The name of the model property.</value>
public string Name { get; private set; }
/// <summary>
/// Gets a value indicating whether the <see cref="IEditableObject"/> interface should be used on the model if possible.
/// </summary>
/// <value>
/// <c>true</c> if the <see cref="IEditableObject"/> interface should be used on the model if possible; otherwise, <c>false</c>.
/// </value>
public bool SupportIEditableObject { get; private set; }
#endregion
#region Methods
#endregion
}
/// <summary>
/// Model value class to store the mapping of the View Model to a Model mapping.
/// </summary>
private class ViewModelToModelMapping
{
#region Constructor & destructor
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices.ViewModelToModelMapping"/> class.
/// </summary>
/// <param name="viewModelProperty">The view model property.</param>
/// <param name="attribute">The <see cref="ViewModelToModelAttribute"/> that was used to define the mapping.</param>
public ViewModelToModelMapping(string viewModelProperty, ViewModelToModelAttribute attribute)
: this(viewModelProperty, attribute.Model, attribute.Property, attribute.SupportLLBLGen,
attribute.ViewModelLLBLGenEntityProperty, attribute.ModelLLBLGenEntityProperty, attribute.NullValue) { }
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices.ViewModelToModelMapping"/> class.
/// </summary>
/// <param name="viewModelProperty">The view model property.</param>
/// <param name="modelProperty">The model property.</param>
/// <param name="valueProperty">The value property.</param>
/// <param name="supportLLBLGen">if set to <c>true</c>, LLBLGen will be supported.</param>
/// <param name="viewModelLLBLGenEntityProperty">The view model LLBLGen entity property.</param>
/// <param name="modelLLBLGenEntityProperty">The model LLBLGen entity property.</param>
/// <param name="nullValue">The null value.</param>
public ViewModelToModelMapping(string viewModelProperty, string modelProperty, string valueProperty, bool supportLLBLGen,
string viewModelLLBLGenEntityProperty, string modelLLBLGenEntityProperty, object nullValue)
{
ViewModelProperty = viewModelProperty;
ModelProperty = modelProperty;
ValueProperty = valueProperty;
SupportLLBLGen = supportLLBLGen;
ViewModelLLBLGenEntityProperty = viewModelLLBLGenEntityProperty;
ModelLLBLGenEntityProperty = modelLLBLGenEntityProperty;
NullValue = nullValue;
}
#endregion
#region Properties
/// <summary>
/// Gets the property name of the mapping of the view model.
/// </summary>
/// <value>The model view property.</value>
public string ViewModelProperty { get; private set; }
/// <summary>
/// Gets the property name of the the model.
/// </summary>
/// <value>The model.</value>
public string ModelProperty { get; private set; }
/// <summary>
/// Gets the property property name of the property in the model.
/// </summary>
/// <value>The property.</value>
public string ValueProperty { get; private set; }
/// <summary>
/// Gets a value indicating whether to support LLBLGen.
/// </summary>
/// <value><c>true</c> if LLBLGen is supported; otherwise, <c>false</c>.</value>
public bool SupportLLBLGen { get; private set; }
/// <summary>
/// Gets the LLBLGen property to set when the view model property changes.
/// </summary>
/// <value>The LLBLGen property.</value>
public string ViewModelLLBLGenEntityProperty { get; private set; }
/// <summary>
/// Gets the LLBLGen property to set when the view model property changes.
/// </summary>
/// <value>The LLBLGen property.</value>
public string ModelLLBLGenEntityProperty { get; private set; }
/// <summary>
/// Gets the value to set on the LLBLGen entity when the view model property is null.
/// </summary>
/// <value>The null value.</value>
public object NullValue { get; private set; }
#endregion
}
#endregion
#region Constants
/// <summary>
/// Name of the restore point when beginning to edit a model that is an LLBLGen entity.
/// </summary>
private const string LLBLGenRestorePointName = "_viewModelRestorePoint";
#endregion
#region Variables
/// <summary>
/// Dictionary of available models inside the view model.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
private readonly Dictionary<string, object> _modelObjects = new Dictionary<string, object>();
/// <summary>
/// Dictionary with info about the available models inside the view model.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
private readonly Dictionary<string, ModelInfo> _modelObjectsInfo = new Dictionary<string, ModelInfo>();
#if SILVERLIGHT
/// <summary>
/// Dictionary containing all the previous values of the available models inside the view model.
/// <para />
/// Because Silverlight doesn't implement <c>INotifyPropertyChanging</c>, we need to keep track or the values
/// ourselves to be able to clean up changed models.
/// </summary>
private readonly Dictionary<string, object> _previousModelObjects = new Dictionary<string, object>();
#endif
/// <summary>
/// List of child view models which can be registed by the <see cref="RegisterChildViewModel"/> method.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
private readonly List<IViewModel> _childViewModels = new List<IViewModel>();
/// <summary>
/// Value to determine whether child view models have errors or not.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
private bool _childViewModelsHaveErrors;
/// <summary>
/// Gets the view model manager.
/// </summary>
/// <value>The view model manager.</value>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
protected static readonly ViewModelManager ViewModelManager = new ViewModelManager();
/// <summary>
/// Service manager to manager services of the view models.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
protected static readonly ViewModelServiceManager ViewModelServiceManager = new ViewModelServiceManager();
/// <summary>
/// Dictionary of LLBLGen ValidateEntity fields that are available on the models. This mapping is required to dynamically determine
/// whether a model is an LLBLGen Pro entity, and therefore must be validated via a call to ValidateEntity.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
private readonly Dictionary<string, MethodInfo> _modelObjectValidateEntityMethods = new Dictionary<string, MethodInfo>();
/// <summary>
/// Mappings from view model properties to models and their properties.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
private readonly Dictionary<string, ViewModelToModelMapping> _viewModelToModelMap = new Dictionary<string, ViewModelToModelMapping>();
/// <summary>
/// A list of commands that implement the <see cref="ICatelCommand"/> interface.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
private readonly List<ICatelCommand> _registeredCommands = new List<ICatelCommand>();
/// <summary>
/// A value indiciating whether the services are already registered. If not, the <see cref="RegisterViewModelServices"/> will
/// be invoked.
/// </summary>
#if !SILVERLIGHT
[field: NonSerialized]
#endif
private static bool _registeredServices;
#endregion
#region Constructor & destructor
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class with support for <see cref="IEditableObject"/>.
/// </summary>
/// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
/// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
protected ViewModelBaseWithoutServices()
: this(true) { }
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
/// </summary>
/// <param name="supportIEditableObject">if set to <c>true</c>, the view model will natively support models that
/// implement the <see cref="IEditableObject"/> interface.</param>
/// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
/// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
protected ViewModelBaseWithoutServices(bool supportIEditableObject)
: this(supportIEditableObject, false) { }
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
/// </summary>
/// <param name="supportIEditableObject">if set to <c>true</c>, the view model will natively support models that
/// implement the <see cref="IEditableObject"/> interface.</param>
/// <param name="ignoreMultipleModelsWarning">if set to <c>true</c>, the warning when using multiple models is ignored.</param>
/// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
/// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
protected ViewModelBaseWithoutServices(bool supportIEditableObject, bool ignoreMultipleModelsWarning)
: this(null, supportIEditableObject, ignoreMultipleModelsWarning) { }
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
/// <para/>
/// This constructor allows services to be injected. When <param name="services"/> contains any elements, the
/// <see cref="RegisterViewModelServices"/> method is not invoked.
/// </summary>
/// <param name="services">Dictionary of services to register.</param>
/// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
/// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
protected ViewModelBaseWithoutServices(Dictionary<Type, object> services)
: this(services, true) { }
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
/// <para/>
/// This constructor allows services to be injected. When <param name="services"/> contains any elements, the
/// <see cref="RegisterViewModelServices"/> method is not invoked.
/// </summary>
/// <param name="services">Dictionary of services to register.</param>
/// <param name="supportIEditableObject">if set to <c>true</c>, the view model will natively support models that
/// implement the <see cref="IEditableObject"/> interface.</param>
/// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
/// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
protected ViewModelBaseWithoutServices(Dictionary<Type, object> services, bool supportIEditableObject)
: this(services, supportIEditableObject, false) { }
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBaseWithoutServices"/> class.
/// <para />
/// This constructor allows services to be injected. When <param name="services"/> contains any elements, the
/// <see cref="RegisterViewModelServices"/> method is not invoked.
/// </summary>
/// <param name="services">Dictionary of services to register.</param>
/// <param name="supportIEditableObject">if set to <c>true</c>, the view model will natively support models that
/// implement the <see cref="IEditableObject"/> interface.</param>
/// <param name="ignoreMultipleModelsWarning">if set to <c>true</c>, the warning when using multiple models is ignored.</param>
/// <exception cref="ModelNotRegisteredException">when a mapped model is not registered.</exception>
/// <exception cref="PropertyNotFoundInModelException">when a mapped model property is not found.</exception>
protected ViewModelBaseWithoutServices(Dictionary<Type, object> services, bool supportIEditableObject, bool ignoreMultipleModelsWarning)
{
if ((services != null) && (services.Count > 0))
{
ViewModelServiceManager.Clear();
foreach (KeyValuePair<Type, object> service in services)
{
ViewModelServiceManager.Add(service.Key, service.Value);
}
}
else if (!_registeredServices)
{
_registeredServices = true;
Log.Debug("Registering view model services");
RegisterViewModelServices(IoC.UnityContainer.Instance.Container);
Log.Debug("Registered view model services");
}
// In silverlight, automatically invalidate commands when property changes
#if SILVERLIGHT
InvalidateCommandsOnPropertyChanged = true;
#else
InvalidateCommandsOnPropertyChanged = false;
#endif
ViewModelConstructionTime = DateTime.Now;
#if SILVERLIGHT
if (System.Windows.Application.Current.RootVisual != null)
{
Dispatcher = System.Windows.Application.Current.RootVisual.Dispatcher;
}
else
{
Dispatcher = System.Windows.Deployment.Current.Dispatcher;
}
#else
Dispatcher = Dispatcher.CurrentDispatcher;
#endif
// Do not automatically dipose members (we don't want to dispose models)
AutomaticallyDisposeChildObjectsOnDispose = false;
SuspendValidation = true;
SupportIEditableObject = supportIEditableObject;
Type type = GetType();
List<PropertyInfo> properties = new List<PropertyInfo>();
properties.AddRange(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
foreach (PropertyInfo propertyInfo in properties)
{
var modelAttribute = Attribute.GetCustomAttribute(propertyInfo, typeof(ModelAttribute)) as ModelAttribute;
if (modelAttribute != null)
{
lock (_modelObjects)
{
_modelObjects.Add(propertyInfo.Name, null);
_modelObjectsInfo.Add(propertyInfo.Name, new ModelInfo(propertyInfo.Name, modelAttribute));
}
}
var viewModelToModelAttribute = Attribute.GetCustomAttribute(propertyInfo, typeof(ViewModelToModelAttribute)) as ViewModelToModelAttribute;
if (viewModelToModelAttribute != null)
{
if (string.IsNullOrEmpty(viewModelToModelAttribute.Property))
{
// Assume the property name in the model is the same as in the view model
viewModelToModelAttribute.Property = propertyInfo.Name;
}
if (viewModelToModelAttribute.SupportLLBLGen)
{
if (string.IsNullOrEmpty(viewModelToModelAttribute.ModelLLBLGenEntityProperty))
{
viewModelToModelAttribute.ModelLLBLGenEntityProperty = LLBLGenHelper.GetRelatedEntityPropertyName(propertyInfo.PropertyType, viewModelToModelAttribute.Property);
}
if (string.IsNullOrEmpty(viewModelToModelAttribute.ViewModelLLBLGenEntityProperty))
{
viewModelToModelAttribute.ViewModelLLBLGenEntityProperty = LLBLGenHelper.GetRelatedEntityPropertyName(propertyInfo.PropertyType, viewModelToModelAttribute.Property);
}
}
_viewModelToModelMap.Add(propertyInfo.Name, new ViewModelToModelMapping(propertyInfo.Name, viewModelToModelAttribute));
}
if (LLBLGenHelper.IsLLBLGenType(propertyInfo.PropertyType))
{
TypesNotToDispose.Add(propertyInfo.PropertyType.Name);
}
}
if (SupportIEditableObject)
{
lock (_modelObjects)
{
foreach (KeyValuePair<string, object> modelKeyValuePair in _modelObjects)
{
if (_modelObjectsInfo[modelKeyValuePair.Key].SupportIEditableObject)
{
if (!(modelKeyValuePair.Value is DataObjectBase) || !((DataObjectBase)modelKeyValuePair.Value).IsInEditSession)
{
BeginEditObject(modelKeyValuePair.Value);
}
}
}
}
}
// Validate view model to model mappings
foreach (KeyValuePair<string, ViewModelToModelMapping> viewModelToModelMapping in _viewModelToModelMap)
{
var mapping = viewModelToModelMapping.Value;
if (!IsModelRegistered(mapping.ModelProperty))
{
Log.Error(TraceMessages.ModelNotRegistered, mapping.ModelProperty, mapping.ViewModelProperty);
throw new ModelNotRegisteredException(mapping.ModelProperty, mapping.ViewModelProperty);
}
PropertyInfo viewModelPropertyInfo = GetPropertyInfo(mapping.ViewModelProperty);
PropertyInfo modelPropertyInfo = GetPropertyInfo(mapping.ModelProperty);
Type modelType = modelPropertyInfo.PropertyType;
PropertyInfo modelPropertyPropertyInfo = modelType.GetProperty(mapping.ValueProperty, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (modelPropertyPropertyInfo == null)
{
Log.Error(TraceMessages.PropertyNotFoundInModel, mapping.ViewModelProperty, mapping.ModelProperty, mapping.ValueProperty);
throw new PropertyNotFoundInModelException(mapping.ViewModelProperty, mapping.ModelProperty, mapping.ValueProperty);
}
if (viewModelPropertyInfo.PropertyType != modelPropertyPropertyInfo.PropertyType)
{
Log.Warn(TraceMessages.WrongViewModelPropertyType, mapping.ViewModelProperty, mapping.ValueProperty,
viewModelPropertyInfo.PropertyType, modelPropertyPropertyInfo.PropertyType);
}
}
if (!ignoreMultipleModelsWarning)
{
lock (_modelObjects)
{
if (_modelObjects.Count > 1)
{
Log.Warn(TraceMessages.ViewModelImplementsMoreThanOneModelWarning, GetType().Name, _modelObjects.Count);
}
}
}
ViewModelManager.RegisterViewModelInstance(this);
object[] interestedInAttributes = GetType().GetCustomAttributes(typeof(InterestedInAttribute), true);
foreach (InterestedInAttribute interestedInAttribute in interestedInAttributes)
{
ViewModelManager.AddInterestedViewModel(interestedInAttribute.ViewModelType, this);
}
}
#endregion
#region Events
/// <summary>
/// Occurs when the view model is about the be saved.
/// </summary>
public event EventHandler<EventArgs> Saving;
/// <summary>
/// Occurs when the view model is saved successfully.
/// </summary>
public event EventHandler<EventArgs> Saved;
/// <summary>
/// Occurs when the view model is about to be canceled.
/// </summary>
public event EventHandler<EventArgs> Canceling;
/// <summary>
/// Occurrs when the view model is canceled.
/// </summary>
public event EventHandler<EventArgs> Canceled;
/// <summary>
/// Occurs when the view model is being closed.
/// </summary>
public event EventHandler<EventArgs> Closed;
#endregion
#region Properties
/// <summary>
/// Gets the view model construction time.
/// </summary>
/// <value>The view model construction time.</value>
public DateTime ViewModelConstructionTime { get; private set; }
/// <summary>
/// Gets the dispatcher.
/// </summary>
/// <value>The dispatcher.</value>
protected Dispatcher Dispatcher { get; private set; }
/// <summary>
/// Gets the parent view model.
/// </summary>
/// <value>The parent view model.</value>
protected IViewModel ParentViewModel { get; private set; }
/// <summary>
/// Gets a value indicating whether the commands should automatically be invalidated on a property change.
/// <para />
/// If this property is <c>false</c>, properties should either be invalidated by the .NET Framework or by a manual
/// call to the <see cref="InvalidateCommands"/> method.
/// </summary>
/// <value>
/// <c>true</c> if the commands should automatically be invalidated on a property change; otherwise, <c>false</c>.
/// </value>
protected bool InvalidateCommandsOnPropertyChanged { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether models that implement <see cref="IEditableObject"/> are supported correctly.
/// </summary>
/// <value>
/// <c>true</c> if models that implement <see cref="IEditableObject"/> are supported correctly; otherwise, <c>false</c>.
/// </value>
private bool SupportIEditableObject { get; set; }
/// <summary>
/// Gets a value indicating whether the view model is initialized.
/// </summary>
/// <value>
/// <c>true</c> if the view model is initialized; otherwise, <c>false</c>.
/// </value>
public new bool IsInitialized { get; private set; }
/// <summary>
/// Gets a value indicating whether the view model is closed.
/// </summary>
/// <value><c>true</c> if the view model is closed; otherwise, <c>false</c>.</value>
protected bool IsClosed { get; private set; }
/// <summary>
/// Gets the title of the view model.
/// </summary>
/// <value>The title.</value>
public virtual string Title
{
get { return string.Empty; }
}
/// <summary>
/// Gets a value indicating whether this object contains any field or business errors.
/// </summary>
/// <value>
/// <c>true</c> if this instance has errors; otherwise, <c>false</c>.
/// </value>
public new bool HasErrors
{
get { return base.HasErrors || _childViewModelsHaveErrors; }
}
#endregion
#region Methods
/// <summary>
/// Sets the parent view model.
/// </summary>
/// <param name="parent">The parent.</param>
internal void SetParentViewModel(IViewModel parent)
{
if (ParentViewModel != parent)
{
ParentViewModel = parent;
OnPropertyChanged("ParentViewModel");
}
}
/// <summary>
/// Registers a child view model.
/// </summary>
/// <param name="child">The child view model.</param>
/// <exception cref="ArgumentNullException">when <paramref name="child"/> is <c>null</c>.</exception>
internal void RegisterChildViewModel(IViewModel child)
{
if (child == null)
{
throw new ArgumentNullException("child");
}
lock (_childViewModels)
{
if (!_childViewModels.Contains(child))
{
_childViewModels.Add(child);
child.PropertyChanged += OnChildViewModelPropertyChanged;
child.Closed += OnChildViewModelClosed;
}
}
}
/// <summary>
/// Called when a property has changed on the child view model.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
private void OnChildViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "HasErrors")
{
Validate(true);
}
}
/// <summary>
/// Called when the child view model is closed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private void OnChildViewModelClosed(object sender, EventArgs e)
{
UnregisterChildViewModel((IViewModel)sender);
}
/// <summary>
/// Unregisters the child view model.
/// </summary>
/// <param name="child">The child.</param>
/// <exception cref="ArgumentNullException">when <paramref name="child"/> is <c>null</c>.</exception>
internal void UnregisterChildViewModel(IViewModel child)
{
if (child == null)
{
throw new ArgumentNullException("child");
}
lock (_childViewModels)
{
int index = _childViewModels.IndexOf(child);
if (index == -1)
{
return;
}
_childViewModels[index].PropertyChanged -= OnChildViewModelPropertyChanged;
_childViewModels[index].Closed -= OnChildViewModelClosed;
_childViewModels.Remove(child);
}
}
/// <summary>
/// Gets all models that are decorated with the <see cref="ModelAttribute"/>.
/// </summary>
/// <returns>Array of models.</returns>
protected object[] GetAllModels()
{
return _modelObjects.Values.ToArray();
}
#if !SILVERLIGHT
/// <summary>
/// Called when a property value is changing.
/// </summary>
/// <param name="propertyName">Name of the property that is changing.</param>
protected override void OnPropertyChanging(string propertyName)
{
lock (_modelObjects)
{
if (_modelObjects.ContainsKey(propertyName))
{
object model = _modelObjects[propertyName];
if (model is INotifyPropertyChanged)
{
((INotifyPropertyChanged)model).PropertyChanged -= Model_PropertyChanged;
}
if (SupportIEditableObject)
{
if (_modelObjectsInfo[propertyName].SupportIEditableObject)
{
if (!(model is DataObjectBase) || ((DataObjectBase)model).IsInEditSession)
{
CancelEditObject(model);
}
}
}
}
}
base.OnPropertyChanging(propertyName);
}
#endif
/// <summary>
/// Called when a property value has changed.
/// </summary>
/// <param name="propertyName">Name of the property that has changed.</param>
protected override void OnPropertyChanged(string propertyName)
{
#if SILVERLIGHT
// This code is normally handled in the OnPropertyChanging method. However, Silverlight doesn't support that, so
// we can only clean up here
lock (_previousModelObjects)
{
if (_previousModelObjects.ContainsKey(propertyName))
{
object model = _previousModelObjects[propertyName];
if (model is INotifyPropertyChanged)
{
((INotifyPropertyChanged)model).PropertyChanged -= Model_PropertyChanged;
}
if (SupportIEditableObject)
{
if (_modelObjectsInfo[propertyName].SupportIEditableObject)
{
if (!(model is DataObjectBase) || ((DataObjectBase)model).IsInEditSession)
{
CancelEditObject(model);
}
}
}
lock (_modelObjects)
{
_previousModelObjects[propertyName] = _modelObjects[propertyName];
}
}
}
#endif
lock (_modelObjects)
{
if (_modelObjects.ContainsKey(propertyName))
{
_modelObjects[propertyName] = GetValue(propertyName);
object model = _modelObjects[propertyName];
if (model is INotifyPropertyChanged)
{
((INotifyPropertyChanged)model).PropertyChanged += Model_PropertyChanged;
}
_modelObjectValidateEntityMethods.Remove(propertyName);
MethodInfo methodInfo = LLBLGenHelper.GetValidateEntityMethod(model);
if (methodInfo != null)
{
_modelObjectValidateEntityMethods.Add(propertyName, methodInfo);
}
if (SupportIEditableObject)
{
if (_modelObjectsInfo[propertyName].SupportIEditableObject)
{
if (!(model is DataObjectBase) || !((DataObjectBase)model).IsInEditSession)
{
BeginEditObject(model);
}
}
}
// Since the model has been changed, copy all values from the model to the view model
foreach (KeyValuePair<string, ViewModelToModelMapping> viewModelToModelMap in _viewModelToModelMap)
{
ViewModelToModelMapping mapping = viewModelToModelMap.Value;
if (mapping.ModelProperty == propertyName)
{
SetValue(mapping.ViewModelProperty, PropertyHelper.GetPropertyValue(_modelObjects[propertyName], mapping.ValueProperty));
}
}
}
}
// If we are validating, don't map view model values back to the model
if (!IsValidating)
{
if (_viewModelToModelMap.ContainsKey(propertyName))
{
lock (_modelObjects)
{
ViewModelToModelMapping mapping = _viewModelToModelMap[propertyName];
object model = _modelObjects[mapping.ModelProperty];
if (model == null)
{
Log.Warn(TraceMessages.CannotMapFromViewModelToModelBecauseModelIsNull, mapping.ModelProperty);
}
else
{
object viewModelValue = GetValue(propertyName);
object modelValue = PropertyHelper.GetPropertyValue(model, mapping.ValueProperty);
if (!TypeHelper.AreObjectsEqual(viewModelValue, modelValue))
{
object valueToSet = viewModelValue;
string propertyToSet = mapping.ValueProperty;
if (mapping.SupportLLBLGen)
{
propertyToSet = mapping.ModelLLBLGenEntityProperty;
valueToSet = (viewModelValue != null) ? PropertyHelper.GetPropertyValue(viewModelValue, mapping.ViewModelLLBLGenEntityProperty) : null;
}
try
{
PropertyHelper.SetPropertyValue(model, propertyToSet, valueToSet);
}
catch (CannotSetPropertyValueException)
{
// This is accepted behavior in case of a read-only property, already logged by framework
}
}
}
}
}
}
if (InvalidateCommandsOnPropertyChanged)
{
InvalidateCommands();
}
base.OnPropertyChanged(propertyName);
}
/// <summary>
/// Called when a property has changed for a view model type that the current view model is interested in. This can
/// be accomplished by decorating the view model with the <see cref="InterestedInAttribute"/>.
/// </summary>
/// <param name="viewModel">The view model.</param>
/// <param name="propertyName">Name of the property.</param>
/// <remarks>
/// This method is internal so the <see cref="ManagedViewModel"/> can invoke it. This method is only used as a pass-through
/// to the actual <see cref="OnViewModelPropertyChanged"/> method.
/// </remarks>
internal void ViewModelPropertyChanged(IViewModel viewModel, string propertyName)
{
OnViewModelPropertyChanged(viewModel, propertyName);
}
/// <summary>
/// Called when a property has changed for a view model type that the current view model is interested in. This can
/// be accomplished by decorating the view model with the <see cref="InterestedInAttribute"/>.
/// </summary>
/// <param name="viewModel">The view model.</param>
/// <param name="propertyName">Name of the property.</param>
protected virtual void OnViewModelPropertyChanged(IViewModel viewModel, string propertyName)
{
Log.Debug(TraceMessages.InterestingViewModelCallPropertyChanged, viewModel.GetType(), GetType(), propertyName);
}
/// <summary>
/// Begins an edit on an object. Also correctly supports LLBLGen entities.
/// </summary>
/// <param name="obj">The object.</param>
private static void BeginEditObject(object obj)
{
if (obj == null)
{
return;
}
if (obj.IsEntity())
{
obj.SaveFields(LLBLGenRestorePointName);
}
else if (obj is IEditableObject)
{
var editableModel = (IEditableObject)obj;
editableModel.BeginEdit();
}
}
/// <summary>
/// Pushes changes since the last <see cref="IEditableObject.EndEdit()"/> call. Also correctly supports LLBLGen entities.
/// </summary>
/// <param name="obj">The object.</param>
private static void EndEditObject(object obj)
{
if (obj == null)
{
return;
}
if (obj.IsEntity())
{
// TODO: Have a discussion whether we should discard all saved fields. This might have impact on other saved
// fields that the user might have created
}
else if (obj is IEditableObject)
{
var editableModel = (IEditableObject)obj;
editableModel.EndEdit();
}
}
/// <summary>
/// Discards changes since the last <see cref="IEditableObject.BeginEdit()"/> call. Also correctly supports LLBLGen entities.
/// </summary>
/// <param name="obj">The object.</param>
private static void CancelEditObject(object obj)
{
if (obj == null)
{
return;
}
if (obj.IsEntity())
{
obj.RollbackFieldsAndRelations(LLBLGenRestorePointName);
}
else if (obj is IEditableObject)
{
var editableModel = (IEditableObject)obj;
editableModel.CancelEdit();
}
}
/// <summary>
/// Handles the PropertyChanged event of a Model.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
foreach (KeyValuePair<string, ViewModelToModelMapping> map in _viewModelToModelMap)
{
ViewModelToModelMapping mapping = map.Value;
if (mapping.ValueProperty == e.PropertyName)
{
// Check if this is the right model (duplicate mappings might exist)
if (_modelObjects[mapping.ModelProperty] == sender)
{
object viewModelValue = GetValue(mapping.ViewModelProperty);
object modelValue = PropertyHelper.GetPropertyValue(sender, e.PropertyName);
if (!TypeHelper.AreObjectsEqual(viewModelValue, modelValue))
{
bool updatePropertyValue = true;
if (mapping.SupportLLBLGen)
{
object relatedFieldValue = PropertyHelper.GetPropertyValue(sender, mapping.ModelLLBLGenEntityProperty);
if (TypeHelper.AreObjectsEqual(relatedFieldValue, mapping.NullValue))
{
// Do not change the value, the view model probably set the value to the null-value
updatePropertyValue = false;
}
}
if (updatePropertyValue)
{
SetValue(mapping.ViewModelProperty, modelValue);
}
}
break;
}
}
}
}
/// <summary>
/// Called when the object is validating.
/// </summary>
protected override void OnValidating()
{
base.OnValidating();
lock (_modelObjects)
{
foreach (KeyValuePair<string, object> model in _modelObjects)
{
if (model.Value is DataObjectBase)
{
((DataObjectBase)model.Value).Validate();
}
else if (_modelObjectValidateEntityMethods.ContainsKey(model.Key))
{
_modelObjectValidateEntityMethods[model.Key].Invoke(model.Value, new object[] { });
}
}
}
lock (_childViewModels)
{
_childViewModelsHaveErrors = false;
foreach (IViewModel childViewModel in _childViewModels)
{
childViewModel.Validate();
if (childViewModel.HasErrors)
{
_childViewModelsHaveErrors = true;
}
}
}
}
/// <summary>
/// Called when the object is validating the fields.
/// </summary>
protected override void OnValidatingFields()
{
base.OnValidatingFields();
// Map all field errors and warnings from the model to this viewmodel
foreach (KeyValuePair<string, ViewModelToModelMapping> viewModelToModelMap in _viewModelToModelMap)
{
ViewModelToModelMapping mapping = viewModelToModelMap.Value;
var model = GetValue(mapping.ModelProperty);
string modelProperty = mapping.SupportLLBLGen ? mapping.ModelLLBLGenEntityProperty : mapping.ValueProperty;
// Error
var dataErrorInfo = model as IDataErrorInfo;
if (dataErrorInfo != null)
{
if (!string.IsNullOrEmpty(dataErrorInfo[modelProperty]))
{
SetFieldError(mapping.ViewModelProperty, dataErrorInfo[modelProperty]);
}
}
// Warning
var dataWarningInfo = model as IDataWarningInfo;
if (dataWarningInfo != null)
{
if (!string.IsNullOrEmpty(dataWarningInfo[modelProperty]))
{
SetFieldWarning(mapping.ViewModelProperty, dataWarningInfo[modelProperty]);
}
}
}
}
/// <summary>
/// Called when the object is validating the business rules.
/// </summary>
protected override void OnValidatingBusinessRules()
{
base.OnValidatingBusinessRules();
lock (_modelObjects)
{
foreach (KeyValuePair<string, object> modelObject in _modelObjects)
{
// Error
var dataErrorInfo = modelObject.Value as IDataErrorInfo;
if (dataErrorInfo != null)
{
SetBusinessRuleError(dataErrorInfo.Error);
}
// Warning
var dataWarningInfo = modelObject.Value as IDataWarningInfo;
if (dataWarningInfo != null)
{
SetBusinessRuleWarning(dataWarningInfo.Warning);
}
}
}
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposeManagedResources"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected override void Dispose(bool disposeManagedResources)
{
if (disposeManagedResources)
{
ViewModelManager.UnregisterViewModelInstance(this);
}
base.Dispose(disposeManagedResources);
}
/// <summary>
/// Initializes the object by setting default values.
/// </summary>
protected virtual void Initialize() { }
/// <summary>
/// Cancels the editing of the data.
/// </summary>
protected virtual void Cancel() { }
/// <summary>
/// Saves the data.
/// </summary>
/// <returns>
/// <c>true</c> if successful; otherwise <c>false</c>.
/// </returns>
protected virtual bool Save() { return true; }
/// <summary>
/// Closes this instance. Always called after the <see cref="Cancel"/> of <see cref="Save"/> method.
/// </summary>
/// <remarks>
/// When implementing this method in a base class, make sure to call the base, otherwise <see cref="IsClosed"/> will
/// not be set to true.
/// </remarks>
protected virtual void Close()
{
IsClosed = true;
}
/// <summary>
/// Determines whether a specific property is registered as a model.
/// </summary>
/// <param name="name">The name of the registered model.</param>
/// <returns>
/// <c>true</c> if a specific property is registered as a model; otherwise, <c>false</c>.
/// </returns>
protected bool IsModelRegistered(string name)
{
if (!IsPropertyRegistered(name))
{
return false;
}
return _modelObjects.ContainsKey(name);
}
/// <summary>
/// Registers all the commands that implement the <see cref="ICatelCommand"/>.
/// </summary>
private void RegisterCommands()
{
lock (_registeredCommands)
{
Type type = GetType();
List<PropertyInfo> properties = new List<PropertyInfo>();
properties.AddRange(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
foreach (PropertyInfo propertyInfo in properties)
{
if (propertyInfo.PropertyType.GetInterface(typeof(ICatelCommand).Name, false) != null)
{
ICatelCommand command = propertyInfo.GetValue(this, null) as ICatelCommand;
if (command != null)
{
Log.Debug(TraceMessages.FoundCommandOnViewModel, propertyInfo.Name, type.Name);
_registeredCommands.Add(command);
}
}
}
}
}
/// <summary>
/// Invalidates all the commands that implement the <see cref="ICatelCommand"/>.
/// </summary>
public void InvalidateCommands()
{
lock (_registeredCommands)
{
foreach (ICatelCommand command in _registeredCommands)
{
command.RaiseCanExecuteChanged();
}
}
}
#endregion
#region Services
/// <summary>
/// Gets the service of the specified type.
/// </summary>
/// <param name="serviceType">Type of the service.</param>
/// <returns>Service object or <c>null</c> if the service is not found.</returns>
public object GetService(Type serviceType)
{
return ViewModelServiceManager.GetService(serviceType);
}
/// <summary>
/// Gets the service of the specified type.
/// </summary>
/// <typeparam name="T">Type of the service.</typeparam>
/// <returns>Service object or <c>null</c> if the service is not found.</returns>
public T GetService<T>()
{
return (T)GetService(typeof(T));
}
/// <summary>
/// Registers the known view model services.
/// </summary>
/// <param name="container">The IoC container.</param>
protected abstract void RegisterViewModelServices(IUnityContainer container);
#endregion
#region IViewModel Members
/// <summary>
/// Initializes the object by setting default values.
/// </summary>
void IViewModel.Initialize()
{
Initialize();
RegisterCommands();
IsInitialized = true;
OnPropertyChanged("Title");
SuspendValidation = false;
}
/// <summary>
/// Validates the data.
/// </summary>
/// <returns>
/// <c>true</c> if validation succeeds; otherwise <c>false</c>.
/// </returns>
bool IViewModel.Validate()
{
return ((IViewModel)this).Validate(false, true);
}
/// <summary>
/// Validates the specified notify changed properties only.
/// </summary>
/// <param name="force">if set to <c>true</c>, a validation is forced (even if the object knows it is already validated).</param>
/// <param name="notifyChangedPropertiesOnly">if set to <c>true</c> only the properties for which the warnings or errors have been changed
/// will be updated via <see cref="INotifyPropertyChanged.PropertyChanged"/>; otherwise all the properties that
/// had warnings or errors but not anymore and properties still containing warnings or errors will be updated.</param>
/// <returns>
/// <c>true</c> if validation succeeds; otherwise <c>false</c>.
/// </returns>
/// <remarks>
/// This method is useful when the view model is initialized before the window, and therefore WPF does not update the errors and warnings.
/// </remarks>
bool IViewModel.Validate(bool force, bool notifyChangedPropertiesOnly)
{
if (IsClosed)
{
return true;
}
Validate(force, notifyChangedPropertiesOnly);
return !HasErrors;
}
/// <summary>
/// Cancels the editing of the data.
/// </summary>
void IViewModel.Cancel()
{
if (Canceling != null)
{
Canceling(this, EventArgs.Empty);
}
if (SupportIEditableObject)
{
lock (_modelObjects)
{
foreach (KeyValuePair<string, object> modelKeyValuePair in _modelObjects)
{
try
{
if (_modelObjectsInfo[modelKeyValuePair.Key].SupportIEditableObject)
{
if (!(modelKeyValuePair.Value is DataObjectBase) || ((DataObjectBase)modelKeyValuePair.Value).IsInEditSession)
{
CancelEditObject(modelKeyValuePair.Value);
}
}
}
catch (Exception ex)
{
Log.Warn(ex, TraceMessages.FailedToCancelEditOfModel, modelKeyValuePair.Key);
}
}
}
}
Cancel();
Log.Info(TraceMessages.CanceledViewModel, GetType());
if (Canceled != null)
{
Canceled(this, EventArgs.Empty);
}
}
/// <summary>
/// Cancels the editing of the data, but also closes the view model in the same call.
/// </summary>
void IViewModel.CancelAndClose()
{
((IViewModel)this).Cancel();
((IViewModel)this).Close();
}
/// <summary>
/// Saves the data.
/// </summary>
/// <returns>
/// <c>true</c> if successful; otherwise <c>false</c>.
/// </returns>
bool IViewModel.Save()
{
if (Saving != null)
{
Saving(this, EventArgs.Empty);
}
bool saved = Save();
Log.Info(saved ? TraceMessages.SavedViewModel : TraceMessages.FailedToSaveViewModel, GetType());
if (saved)
{
if (SupportIEditableObject)
{
lock (_modelObjects)
{
foreach (KeyValuePair<string, object> modelKeyValuePair in _modelObjects)
{
try
{
if (_modelObjectsInfo[modelKeyValuePair.Key].SupportIEditableObject)
{
if (!(modelKeyValuePair.Value is DataObjectBase) || ((DataObjectBase) modelKeyValuePair.Value).IsInEditSession)
{
// End edit
EndEditObject(modelKeyValuePair.Value);
}
}
}
catch (Exception ex)
{
Log.Warn(ex, TraceMessages.FailedToEndEditOfModel, modelKeyValuePair.Key);
}
}
}
}
if (Saved != null)
{
Saved(this, EventArgs.Empty);
}
}
return saved;
}
/// <summary>
/// Saves the data, but also closes the view model in the same call if the save succeeds.
/// </summary>
/// <returns>
/// <c>true</c> if successful; otherwise <c>false</c>.
/// </returns>
bool IViewModel.SaveAndClose()
{
bool result = ((IViewModel)this).Save();
if (result)
{
((IViewModel)this).Close();
}
return result;
}
/// <summary>
/// Closes this instance. Always called after the <see cref="Cancel"/> of <see cref="Save"/> method.
/// </summary>
void IViewModel.Close()
{
Close();
Log.Info(TraceMessages.ClosedViewModel, GetType());
if (Closed != null)
{
Closed(this, EventArgs.Empty);
}
}
#endregion
}
}