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