using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using DevExpress.Mvvm;
using DevExpress.Mvvm.POCO;
using DevExpress.Mvvm.DataAnnotations;
using DevExpress.DevAV.Common.Utils;
using DevExpress.Mvvm.DataModel;
namespace DevExpress.DevAV.Common.ViewModel {
///
/// The base class for POCO view models exposing a single entity of a given type and CRUD operations against this entity.
/// This is a partial class that provides the extension point to add custom properties, commands and override methods without modifying the auto-generated code.
///
/// An entity type.
/// A primary key value type.
/// A unit of work type.
public abstract partial class SingleObjectViewModel : SingleObjectViewModelBase
where TEntity : class
where TUnitOfWork : IUnitOfWork {
///
/// Initializes a new instance of the SingleObjectViewModel class.
///
/// A factory used to create the unit of work instance.
/// A function that returns the repository representing entities of a given type.
/// An optional parameter that provides a function to obtain the display text for a given entity. If ommited, the primary key value is used as a display text.
protected SingleObjectViewModel(IUnitOfWorkFactory unitOfWorkFactory, Func> getRepositoryFunc, Func getEntityDisplayNameFunc = null)
: base(unitOfWorkFactory, getRepositoryFunc, getEntityDisplayNameFunc) {
}
}
///
/// The base class for POCO view models exposing a single entity of a given type and CRUD operations against this entity.
/// It is not recommended to inherit directly from this class. Use the SingleObjectViewModel class instead.
///
/// An entity type.
/// A primary key value type.
/// A unit of work type.
[POCOViewModel]
public abstract class SingleObjectViewModelBase : ISingleObjectViewModel, ISupportParameter, IDocumentContent
where TEntity : class
where TUnitOfWork : IUnitOfWork {
object title;
protected readonly Func> getRepositoryFunc;
protected readonly Func getEntityDisplayNameFunc;
Action entityInitializer;
bool isEntityNewAndUnmodified;
readonly Dictionary lookUpViewModels = new Dictionary();
///
/// Initializes a new instance of the SingleObjectViewModelBase class.
///
/// A factory used to create the unit of work instance.
/// A function that returns repository representing entities of a given type.
/// An optional parameter that provides a function to obtain the display text for a given entity. If ommited, the primary key value is used as a display text.
protected SingleObjectViewModelBase(IUnitOfWorkFactory unitOfWorkFactory, Func> getRepositoryFunc, Func getEntityDisplayNameFunc) {
UnitOfWorkFactory = unitOfWorkFactory;
this.getRepositoryFunc = getRepositoryFunc;
this.getEntityDisplayNameFunc = getEntityDisplayNameFunc;
UpdateUnitOfWork();
if(this.IsInDesignMode())
this.Entity = this.Repository.FirstOrDefault();
else
OnInitializeInRuntime();
}
///
/// The display text for a given entity used as a title in the corresponding view.
///
///
public object Title { get { return title; } }
///
/// An entity represented by this view model.
/// Since SingleObjectViewModelBase is a POCO view model, this property will raise INotifyPropertyChanged.PropertyEvent when modified so it can be used as a binding source in views.
///
///
public virtual TEntity Entity { get; protected set; }
///
/// Updates the Title property value and raises CanExecute changed for relevant commands.
/// Since SingleObjectViewModelBase is a POCO view model, an instance of this class will also expose the UpdateCommand property that can be used as a binding source in views.
///
[Display(AutoGenerateField = false)]
public void Update() {
isEntityNewAndUnmodified = false;
UpdateTitle();
UpdateCommands();
}
///
/// Saves changes in the underlying unit of work.
/// Since SingleObjectViewModelBase is a POCO view model, an instance of this class will also expose the SaveCommand property that can be used as a binding source in views.
///
public virtual void Save() {
SaveCore();
}
///
/// Determines whether entity has local changes that can be saved.
/// Since SingleObjectViewModelBase is a POCO view model, this method will be used as a CanExecute callback for SaveCommand.
///
public virtual bool CanSave() {
return Entity != null && !HasValidationErrors() && NeedSave();
}
///
/// Saves changes in the underlying unit of work and closes the corresponding view.
/// Since SingleObjectViewModelBase is a POCO view model, an instance of this class will also expose the SaveAndCloseCommand property that can be used as a binding source in views.
///
[Command(CanExecuteMethodName = "CanSave")]
public void SaveAndClose() {
if(SaveCore())
Close();
}
///
/// Saves changes in the underlying unit of work and create new entity.
/// Since SingleObjectViewModelBase is a POCO view model, an instance of this class will also expose the SaveAndNewCommand property that can be used as a binding source in views.
///
[Command(CanExecuteMethodName = "CanSave")]
public void SaveAndNew() {
if(SaveCore())
CreateAndInitializeEntity(this.entityInitializer);
}
///
/// Reset entity local changes.
/// Since SingleObjectViewModelBase is a POCO view model, an instance of this class will also expose the ResetCommand property that can be used as a binding source in views.
///
[Display(Name = "Reset Changes")]
public void Reset() {
MessageResult confirmationResult = MessageBoxService.ShowMessage(CommonResources.Confirmation_Reset, CommonResources.Confirmation_Caption, MessageButton.OKCancel);
if(confirmationResult == MessageResult.OK)
Reload();
}
///
/// Determines whether entity has local changes.
/// Since SingleObjectViewModelBase is a POCO view model, this method will be used as a CanExecute callback for ResetCommand.
///
public bool CanReset() {
return NeedReset();
}
///
/// Deletes the entity, save changes and closes the corresponding view if confirmed by a user.
/// Since SingleObjectViewModelBase is a POCO view model, an instance of this class will also expose the DeleteCommand property that can be used as a binding source in views.
///
public virtual void Delete() {
if(MessageBoxService.ShowMessage(string.Format(CommonResources.Confirmation_Delete, typeof(TEntity).Name), GetConfirmationMessageTitle(), MessageButton.YesNo) != MessageResult.Yes)
return;
try {
OnBeforeEntityDeleted(PrimaryKey, Entity);
Repository.Remove(Entity);
UnitOfWork.SaveChanges();
TPrimaryKey primaryKeyForMessage = PrimaryKey;
TEntity entityForMessage = Entity;
Entity = null;
OnEntityDeleted(primaryKeyForMessage, entityForMessage);
Close();
} catch (DbException e) {
MessageBoxService.ShowMessage(e.ErrorMessage, e.ErrorCaption, MessageButton.OK, MessageIcon.Error);
}
}
///
/// Determines whether the entity can be deleted.
/// Since SingleObjectViewModelBase is a POCO view model, this method will be used as a CanExecute callback for DeleteCommand.
///
public virtual bool CanDelete() {
return Entity != null && !IsNew();
}
///
/// Closes the corresponding view.
/// Since SingleObjectViewModelBase is a POCO view model, an instance of this class will also expose the CloseCommand property that can be used as a binding source in views.
///
public void Close() {
if(!TryClose())
return;
if(DocumentOwner != null)
DocumentOwner.Close(this);
}
protected IUnitOfWorkFactory UnitOfWorkFactory { get; private set; }
protected TUnitOfWork UnitOfWork { get; private set; }
protected virtual bool SaveCore() {
try {
bool isNewEntity = IsNew();
if(!isNewEntity) {
Repository.SetPrimaryKey(Entity, PrimaryKey);
Repository.Update(Entity);
}
OnBeforeEntitySaved(PrimaryKey, Entity, isNewEntity);
UnitOfWork.SaveChanges();
LoadEntityByKey(Repository.GetPrimaryKey(Entity));
OnEntitySaved(PrimaryKey, Entity, isNewEntity);
return true;
} catch (DbException e) {
MessageBoxService.ShowMessage(e.ErrorMessage, e.ErrorCaption, MessageButton.OK, MessageIcon.Error);
return false;
}
}
protected virtual void OnBeforeEntitySaved(TPrimaryKey primaryKey, TEntity entity, bool isNewEntity) { }
protected virtual void OnEntitySaved(TPrimaryKey primaryKey, TEntity entity, bool isNewEntity) {
Messenger.Default.Send(new EntityMessage(primaryKey, isNewEntity ? EntityMessageType.Added : EntityMessageType.Changed));
}
protected virtual void OnBeforeEntityDeleted(TPrimaryKey primaryKey, TEntity entity) { }
protected virtual void OnEntityDeleted(TPrimaryKey primaryKey, TEntity entity) {
Messenger.Default.Send(new EntityMessage(primaryKey, EntityMessageType.Deleted));
}
protected virtual void OnInitializeInRuntime() {
Messenger.Default.Register>(this, x => OnEntityMessage(x));
Messenger.Default.Register(this, x => Save());
Messenger.Default.Register(this, x => OnClosing(x));
}
protected virtual void OnEntityMessage(EntityMessage message) {
if(Entity == null) return;
if(message.MessageType == EntityMessageType.Deleted && object.Equals(message.PrimaryKey, PrimaryKey))
Close();
}
protected virtual void OnEntityChanged() {
if(Entity != null && Repository.HasPrimaryKey(Entity)) {
PrimaryKey = Repository.GetPrimaryKey(Entity);
RefreshLookUpCollections(true);
}
Update();
}
protected IRepository Repository { get { return getRepositoryFunc(UnitOfWork); } }
protected TPrimaryKey PrimaryKey { get; private set; }
protected IMessageBoxService MessageBoxService { get { return this.GetRequiredService(); } }
protected virtual void OnParameterChanged(object parameter) {
var initializer = parameter as Action;
if(initializer != null)
CreateAndInitializeEntity(initializer);
else if(parameter is TPrimaryKey)
LoadEntityByKey((TPrimaryKey)parameter);
else
Entity = null;
}
protected virtual TEntity CreateEntity() {
return Repository.Create();
}
protected void Reload() {
if(Entity == null || IsNew())
CreateAndInitializeEntity(this.entityInitializer);
else
LoadEntityByKey(PrimaryKey);
}
protected void CreateAndInitializeEntity(Action entityInitializer) {
UpdateUnitOfWork();
this.entityInitializer = entityInitializer;
var entity = CreateEntity();
if(this.entityInitializer != null)
this.entityInitializer(entity);
Entity = entity;
isEntityNewAndUnmodified = true;
}
protected void LoadEntityByKey(TPrimaryKey primaryKey) {
UpdateUnitOfWork();
Entity = Repository.Find(primaryKey);
}
void UpdateUnitOfWork() {
UnitOfWork = UnitOfWorkFactory.CreateUnitOfWork();
}
void UpdateTitle() {
if(Entity == null)
title = null;
else if(IsNew())
title = GetTitleForNewEntity();
else
title = GetTitle(GetState() == EntityState.Modified);
this.RaisePropertyChanged(x => x.Title);
}
protected virtual void UpdateCommands() {
this.RaiseCanExecuteChanged(x => x.Save());
this.RaiseCanExecuteChanged(x => x.SaveAndClose());
this.RaiseCanExecuteChanged(x => x.SaveAndNew());
this.RaiseCanExecuteChanged(x => x.Delete());
this.RaiseCanExecuteChanged(x => x.Reset());
}
protected IDocumentOwner DocumentOwner { get; private set; }
protected virtual void OnDestroy() {
Messenger.Default.Unregister(this);
RefreshLookUpCollections(false);
}
protected virtual bool TryClose() {
if(HasValidationErrors()) {
MessageResult warningResult = MessageBoxService.ShowMessage(CommonResources.Warning_SomeFieldsContainInvalidData, CommonResources.Warning_Caption, MessageButton.OKCancel);
return warningResult == MessageResult.OK;
}
if(!NeedReset()) return true;
MessageResult result = MessageBoxService.ShowMessage(CommonResources.Confirmation_Save, GetConfirmationMessageTitle(), MessageButton.YesNoCancel);
if(result == MessageResult.Yes)
return SaveCore();
return result != MessageResult.Cancel;
}
protected virtual void OnClosing(CloseAllMessage message) {
if(!message.Cancel)
message.Cancel = !TryClose();
}
protected virtual string GetConfirmationMessageTitle() {
return GetTitle();
}
protected bool IsNew() {
return GetState() == EntityState.Added;
}
protected virtual bool NeedSave() {
if(Entity == null)
return false;
EntityState state = GetState();
return state == EntityState.Modified || state == EntityState.Added;
}
protected virtual bool NeedReset() {
return NeedSave() && !isEntityNewAndUnmodified;
}
protected virtual bool HasValidationErrors() {
IDataErrorInfo dataErrorInfo = Entity as IDataErrorInfo;
return dataErrorInfo != null && IDataErrorInfoHelper.HasErrors(dataErrorInfo);
}
string GetTitle(bool entityModified) {
return GetTitle() + (entityModified ? CommonResources.Entity_Changed : string.Empty);
}
protected virtual string GetTitleForNewEntity() {
return typeof(TEntity).Name + CommonResources.Entity_New;
}
protected virtual string GetTitle() {
return (typeof(TEntity).Name + " - " + Convert.ToString(getEntityDisplayNameFunc != null ? getEntityDisplayNameFunc(Entity) : PrimaryKey))
.Split(new string[] { "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
}
protected EntityState GetState() {
try {
return Repository.GetState(Entity);
} catch(InvalidOperationException) {
Repository.SetPrimaryKey(Entity, PrimaryKey);
return Repository.GetState(Entity);
}
}
#region look up and detail view models
protected virtual void RefreshLookUpCollections(bool raisePropertyChanged) {
var values = lookUpViewModels.ToArray();
lookUpViewModels.Clear();
foreach(var item in values) {
item.Value.OnDestroy();
if(raisePropertyChanged)
((IPOCOViewModel)this).RaisePropertyChanged(item.Key);
}
}
protected CollectionViewModel GetDetailsCollectionViewModel(
Expression>> propertyExpression,
Func> getRepositoryFunc,
Expression> foreignKeyExpression,
Action setMasterEntityKeyAction,
Func, IQueryable> projection = null) where TDetailEntity : class {
return GetCollectionViewModelCore, TDetailEntity, TDetailEntity, TForeignKey>(propertyExpression, foreignKeyExpression,
() => CollectionViewModel.CreateCollectionViewModel(UnitOfWorkFactory, getRepositoryFunc, projection, CreateForeignKeyPropertyInitializer(setMasterEntityKeyAction, PrimaryKey), true));
}
protected CollectionViewModel GetDetailProjectionsCollectionViewModel(
Expression>> propertyExpression,
Func> getRepositoryFunc,
Expression> foreignKeyExpression,
Action setMasterEntityKeyAction,
Func, IQueryable> projection = null) where TDetailEntity : class where TDetailProjection : class {
return GetCollectionViewModelCore, TDetailEntity, TDetailProjection, TForeignKey>(propertyExpression, foreignKeyExpression,
() => CollectionViewModel.CreateProjectionCollectionViewModel(UnitOfWorkFactory, getRepositoryFunc, projection, CreateForeignKeyPropertyInitializer(setMasterEntityKeyAction, PrimaryKey), true));
}
protected ReadOnlyCollectionViewModel GetReadOnlyDetailsCollectionViewModel(
Expression>> propertyExpression,
Func> getRepositoryFunc,
Expression> foreignKeyExpression,
Func, IQueryable> projection = null) where TDetailEntity : class {
return GetCollectionViewModelCore, TDetailEntity, TDetailEntity, TForeignKey>(propertyExpression, foreignKeyExpression,
() => ReadOnlyCollectionViewModel.CreateReadOnlyCollectionViewModel(UnitOfWorkFactory, getRepositoryFunc, projection));
}
protected ReadOnlyCollectionViewModel GetReadOnlyDetailProjectionsCollectionViewModel(
Expression>> propertyExpression,
Func> getRepositoryFunc,
Expression> foreignKeyExpression,
Func, IQueryable> projection) where TDetailEntity : class where TDetailProjection : class {
return GetCollectionViewModelCore, TDetailEntity, TDetailProjection, TForeignKey>(propertyExpression, foreignKeyExpression,
() => ReadOnlyCollectionViewModel.CreateReadOnlyProjectionCollectionViewModel(UnitOfWorkFactory, getRepositoryFunc, projection));
}
protected IEntitiesViewModel GetLookUpEntitiesViewModel(Expression>> propertyExpression, Func> getRepositoryFunc, Func, IQueryable> projection = null) where TLookUpEntity : class {
return GetLookUpProjectionsViewModel(propertyExpression, getRepositoryFunc, projection);
}
protected virtual IEntitiesViewModel GetLookUpProjectionsViewModel(Expression>> propertyExpression, Func> getRepositoryFunc, Func, IQueryable> projection) where TLookUpEntity : class where TLookUpProjection : class {
return GetEntitiesViewModelCore, TLookUpProjection>(propertyExpression, () => LookUpEntitiesViewModel.Create(UnitOfWorkFactory, getRepositoryFunc, projection));
}
static Action CreateForeignKeyPropertyInitializer(Action setMasterEntityKeyAction, TForeignKey masterEntityKey) where TDetailEntity : class {
return x => setMasterEntityKeyAction(x, (TPrimaryKey)(object)masterEntityKey);
}
TViewModel GetCollectionViewModelCore(
LambdaExpression propertyExpression,
Expression> foreignKeyExpression,
Func createViewModelCallback)
where TViewModel : ReadOnlyCollectionViewModel
where TDetailEntity : class
where TDetailProjection : class {
return GetEntitiesViewModelCore(propertyExpression,
() => CreateAndInitializeCollectionViewModel(createViewModelCallback, foreignKeyExpression));
}
TViewModel CreateAndInitializeCollectionViewModel(Func createViewModelCallback, Expression> foreignKeyExpression)
where TViewModel : ReadOnlyCollectionViewModel
where TDetailEntity : class
where TDetailProjection : class {
TViewModel lookUpViewModel = createViewModelCallback().SetParentViewModel(this);
lookUpViewModel.FilterExpression = ExpressionHelper.GetValueEqualsExpression(foreignKeyExpression, (TForeignKey)(object)PrimaryKey);
return lookUpViewModel;
}
TViewModel GetEntitiesViewModelCore(LambdaExpression propertyExpression, Func createViewModelCallback)
where TViewModel : IEntitiesViewModel
where TDetailEntity : class {
IDocumentContent result = null;
string propertyName = ExpressionHelper.GetPropertyName(propertyExpression);
if(!lookUpViewModels.TryGetValue(propertyName, out result)) {
result = createViewModelCallback();
lookUpViewModels[propertyName] = result;
}
return (TViewModel)result;
}
#endregion
#region ISupportParameter
object ISupportParameter.Parameter {
get { return null; }
set { OnParameterChanged(value); }
}
#endregion
#region IDocumentContent
object IDocumentContent.Title { get { return Title; } }
void IDocumentContent.OnClose(CancelEventArgs e) {
e.Cancel = !TryClose();
}
void IDocumentContent.OnDestroy() {
OnDestroy();
}
IDocumentOwner IDocumentContent.DocumentOwner {
get { return DocumentOwner; }
set { DocumentOwner = value; }
}
#endregion
#region ISingleObjectViewModel
TEntity ISingleObjectViewModel.Entity { get { return Entity; } }
TPrimaryKey ISingleObjectViewModel.PrimaryKey { get { return PrimaryKey; } }
#endregion
}
}