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 a POCO view models exposing a colection of entities of a given type and CRUD operations against these entities. /// This is a partial class that provides 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 partial class CollectionViewModel : CollectionViewModel where TEntity : class where TUnitOfWork : IUnitOfWork { /// /// Creates a new instance of CollectionViewModel as a POCO view model. /// /// A factory used to create a unit of work instance. /// A function that returns a repository representing entities of the given type. /// An optional parameter that provides a LINQ function used to customize a query for entities. The parameter, for example, can be used for sorting data. /// An optional parameter that provides a function to initialize a new entity. This parameter is used in the detail collection view models when creating a single object view model for a new entity. /// An optional parameter that used to specify that the selected entity should not be managed by PeekCollectionViewModel. public static CollectionViewModel CreateCollectionViewModel( IUnitOfWorkFactory unitOfWorkFactory, Func> getRepositoryFunc, Func, IQueryable> projection = null, Action newEntityInitializer = null, bool ignoreSelectEntityMessage = false) { return ViewModelSource.Create(() => new CollectionViewModel(unitOfWorkFactory, getRepositoryFunc, projection, newEntityInitializer, ignoreSelectEntityMessage)); } /// /// Initializes a new instance of the CollectionViewModel class. /// This constructor is declared protected to avoid an undesired instantiation of the CollectionViewModel type without the POCO proxy factory. /// /// A factory used to create a unit of work instance. /// A function that returns a repository representing entities of the given type. /// An optional parameter that provides a LINQ function used to customize a query for entities. The parameter, for example, can be used for sorting data. /// An optional parameter that provides a function to initialize a new entity. This parameter is used in the detail collection view models when creating a single object view model for a new entity. /// An optional parameter that used to specify that the selected entity should not be managed by PeekCollectionViewModel. protected CollectionViewModel( IUnitOfWorkFactory unitOfWorkFactory, Func> getRepositoryFunc, Func, IQueryable> projection = null, Action newEntityInitializer = null, bool ignoreSelectEntityMessage = false ) : base(unitOfWorkFactory, getRepositoryFunc, projection, newEntityInitializer, ignoreSelectEntityMessage) { } } /// /// The base class for a POCO view models exposing a collection of entities of a given type and CRUD operations against these entities. /// This is a partial class that provides extension point to add custom properties, commands and override methods without modifying the auto-generated code. /// /// A repository entity type. /// A projection entity type. /// A primary key value type. /// A unit of work type. public partial class CollectionViewModel : CollectionViewModelBase where TEntity : class where TProjection : class where TUnitOfWork : IUnitOfWork { /// /// Creates a new instance of CollectionViewModel as a POCO view model. /// /// A factory used to create a unit of work instance. /// A function that returns a repository representing entities of the given type. /// A LINQ function used to customize a query for entities. The parameter, for example, can be used for sorting data and/or for projecting data to a custom type that does not match the repository entity type. /// An optional parameter that provides a function to initialize a new entity. This parameter is used in the detail collection view models when creating a single object view model for a new entity. /// An optional parameter that used to specify that the selected entity should not be managed by PeekCollectionViewModel. public static CollectionViewModel CreateProjectionCollectionViewModel( IUnitOfWorkFactory unitOfWorkFactory, Func> getRepositoryFunc, Func, IQueryable> projection, Action newEntityInitializer = null, bool ignoreSelectEntityMessage = false) { return ViewModelSource.Create(() => new CollectionViewModel(unitOfWorkFactory, getRepositoryFunc, projection, newEntityInitializer, ignoreSelectEntityMessage)); } /// /// Initializes a new instance of the CollectionViewModel class. /// This constructor is declared protected to avoid an undesired instantiation of the CollectionViewModel type without the POCO proxy factory. /// /// A factory used to create a unit of work instance. /// A function that returns a repository representing entities of the given type. /// A LINQ function used to customize a query for entities. The parameter, for example, can be used for sorting data and/or for projecting data to a custom type that does not match the repository entity type. /// An optional parameter that provides a function to initialize a new entity. This parameter is used in the detail collection view models when creating a single object view model for a new entity. /// An optional parameter that used to specify that the selected entity should not be managed by PeekCollectionViewModel. protected CollectionViewModel( IUnitOfWorkFactory unitOfWorkFactory, Func> getRepositoryFunc, Func, IQueryable> projection, Action newEntityInitializer = null, bool ignoreSelectEntityMessage = false ) : base(unitOfWorkFactory, getRepositoryFunc, projection, newEntityInitializer, ignoreSelectEntityMessage) { } } /// /// The base class for POCO view models exposing a collection of entities of a given type and CRUD operations against these entities. /// It is not recommended to inherit directly from this class. Use the CollectionViewModel class instead. /// /// A repository entity type. /// A projection entity type. /// A primary key value type. /// A unit of work type. public abstract class CollectionViewModelBase : ReadOnlyCollectionViewModel where TEntity : class where TProjection : class where TUnitOfWork : IUnitOfWork { EntitiesChangeTracker ChangeTrackerWithKey { get { return (EntitiesChangeTracker)ChangeTracker; } } readonly Action newEntityInitializer; IRepository Repository { get { return (IRepository)ReadOnlyRepository; } } /// /// Initializes a new instance of the CollectionViewModelBase class. /// /// A factory used to create a unit of work instance. /// A function that returns a repository representing entities of the given type. /// A LINQ function used to customize a query for entities. The parameter, for example, can be used for sorting data and/or for projecting data to a custom type that does not match the repository entity type. /// A function to initialize a new entity. This parameter is used in the detail collection view models when creating a single object view model for a new entity. /// A parameter used to specify whether the selected entity should be managed by PeekCollectionViewModel. protected CollectionViewModelBase( IUnitOfWorkFactory unitOfWorkFactory, Func> getRepositoryFunc, Func, IQueryable> projection, Action newEntityInitializer, bool ignoreSelectEntityMessage ) : base(unitOfWorkFactory, getRepositoryFunc, projection) { VerifyProjectionType(); this.newEntityInitializer = newEntityInitializer; this.ignoreSelectEntityMessage = ignoreSelectEntityMessage; if(!this.IsInDesignMode()) RegisterSelectEntityMessage(); } /// /// Creates and shows a document that contains a single object view model for new entity. /// Since CollectionViewModelBase is a POCO view model, an the instance of this class will also expose the NewCommand property that can be used as a binding source in views. /// public virtual void New() { GetDocumentManagerService().ShowNewEntityDocument(this, newEntityInitializer); } /// /// Creates and shows a document that contains a single object view model for the existing entity. /// Since CollectionViewModelBase is a POCO view model, an the instance of this class will also expose the EditCommand property that can be used as a binding source in views. /// /// Entity to edit. public virtual void Edit(TProjection projectionEntity) { if(Repository.IsDetached(projectionEntity)) return; TPrimaryKey primaryKey = Repository.GetProjectionPrimaryKey(projectionEntity); int index = Entities.IndexOf(projectionEntity); projectionEntity = ChangeTrackerWithKey.FindActualProjectionByKey(primaryKey); if(index >= 0) { if(projectionEntity == null) Entities.RemoveAt(index); else Entities[index] = projectionEntity; } if(projectionEntity == null) { DestroyDocument(GetDocumentManagerService().FindEntityDocument(primaryKey)); return; } GetDocumentManagerService().ShowExistingEntityDocument(this, primaryKey); } /// /// Determines whether an entity can be edited. /// Since CollectionViewModelBase is a POCO view model, this method will be used as a CanExecute callback for EditCommand. /// /// An entity to edit. public bool CanEdit(TProjection projectionEntity) { return projectionEntity != null && !IsLoading; } /// /// Deletes a given entity from the repository and saves changes if confirmed by the user. /// Since CollectionViewModelBase is a POCO view model, an the instance of this class will also expose the DeleteCommand property that can be used as a binding source in views. /// /// An entity to edit. public virtual void Delete(TProjection projectionEntity) { if(MessageBoxService.ShowMessage(string.Format(CommonResources.Confirmation_Delete, typeof(TEntity).Name), CommonResources.Confirmation_Caption, MessageButton.YesNo) != MessageResult.Yes) return; try { Entities.Remove(projectionEntity); TPrimaryKey primaryKey = Repository.GetProjectionPrimaryKey(projectionEntity); TEntity entity = Repository.Find(primaryKey); if(entity != null) { OnBeforeEntityDeleted(primaryKey, entity); Repository.Remove(entity); Repository.UnitOfWork.SaveChanges(); OnEntityDeleted(primaryKey, entity); } } catch (DbException e) { Refresh(); MessageBoxService.ShowMessage(e.ErrorMessage, e.ErrorCaption, MessageButton.OK, MessageIcon.Error); } } /// /// Determines whether an entity can be deleted. /// Since CollectionViewModelBase is a POCO view model, this method will be used as a CanExecute callback for DeleteCommand. /// /// An entity to edit. public virtual bool CanDelete(TProjection projectionEntity) { return projectionEntity != null && !IsLoading; } /// /// Saves the given entity. /// Since CollectionViewModelBase is a POCO view model, the instance of this class will also expose the SaveCommand property that can be used as a binding source in views. /// /// An entity to save. [Display(AutoGenerateField = false)] public virtual void Save(TProjection projectionEntity) { TPrimaryKey primaryKey = Repository.GetProjectionPrimaryKey(projectionEntity); TEntity entity = Repository.Find(primaryKey); if(typeof(TProjection) != typeof(TEntity)) ApplyProjectionPropertiesToEntity(projectionEntity, entity); try { OnBeforeEntitySaved(primaryKey, entity); Repository.UnitOfWork.SaveChanges(); OnEntitySaved(primaryKey, entity); } catch (DbException e) { MessageBoxService.ShowMessage(e.ErrorMessage, e.ErrorCaption, MessageButton.OK, MessageIcon.Error); } } /// /// Determines whether entity local changes can be saved. /// Since CollectionViewModelBase is a POCO view model, this method will be used as a CanExecute callback for SaveCommand. /// /// An entity to save. public virtual bool CanSave(TProjection projectionEntity) { return projectionEntity != null && !IsLoading; } /// /// Notifies that SelectedEntity has been changed by raising the PropertyChanged event. /// Since CollectionViewModelBase is a POCO view model, an the instance of this class will also expose the UpdateSelectedEntityCommand property that can be used as a binding source in views. /// [Display(AutoGenerateField = false)] public virtual void UpdateSelectedEntity() { this.RaisePropertyChanged(x => x.SelectedEntity); } /// /// Closes the corresponding view. /// Since CollectionViewModelBase is a POCO view model, an the instance of this class will also expose the CloseCommand property that can be used as a binding source in views. /// [Display(AutoGenerateField = false)] public void Close() { if(DocumentOwner != null) DocumentOwner.Close(this); } protected IMessageBoxService MessageBoxService { get { return this.GetRequiredService(); } } protected virtual IDocumentManagerService GetDocumentManagerService() { return this.GetService(); } 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 override Func GetSelectedEntityCallback() { var entity = SelectedEntity; return () => FindLocalProjectionWithSameKey(entity); } TProjection FindLocalProjectionWithSameKey(TProjection projectionEntity) { bool primaryKeyAvailable = projectionEntity != null && Repository.ProjectionHasPrimaryKey(projectionEntity); return primaryKeyAvailable ? ChangeTrackerWithKey.FindLocalProjectionByKey(Repository.GetProjectionPrimaryKey(projectionEntity)) : null; } protected virtual void OnBeforeEntitySaved(TPrimaryKey primaryKey, TEntity entity) { } protected virtual void OnEntitySaved(TPrimaryKey primaryKey, TEntity entity) { Messenger.Default.Send(new EntityMessage(primaryKey, EntityMessageType.Changed)); } protected virtual void ApplyProjectionPropertiesToEntity(TProjection projectionEntity, TEntity entity) { throw new NotImplementedException("Override this method in the collection view model class and apply projection properties to the entity so that it can be correctly saved by unit of work."); } protected override void OnSelectedEntityChanged() { base.OnSelectedEntityChanged(); UpdateCommands(); } protected override void RestoreSelectedEntity(TProjection existingProjectionEntity, TProjection newProjectionEntity) { base.RestoreSelectedEntity(existingProjectionEntity, newProjectionEntity); if(ReferenceEquals(SelectedEntity, existingProjectionEntity)) SelectedEntity = newProjectionEntity; } protected override void OnIsLoadingChanged() { base.OnIsLoadingChanged(); UpdateCommands(); if(!IsLoading) RequestSelectedEntity(); } void UpdateCommands() { TProjection projectionEntity = null; this.RaiseCanExecuteChanged(x => x.Edit(projectionEntity)); this.RaiseCanExecuteChanged(x => x.Delete(projectionEntity)); this.RaiseCanExecuteChanged(x => x.Save(projectionEntity)); } protected void DestroyDocument(IDocument document) { if(document != null) document.Close(); } protected IRepository CreateRepository() { return (IRepository)CreateReadOnlyRepository(); } protected override IEntitiesChangeTracker CreateEntitiesChangeTracker() { return new EntitiesChangeTracker(this); } void VerifyProjectionType() { //string primaryKeyPropertyName = CreateRepository().GetPrimaryKeyExpression.Name; //if (TypeDescriptor.GetProperties(typeof(TProjection))[primaryKeyPropertyName] == null) // throw new ArgumentException(string.Format("Projection type {0} should have primary key property {1}", typeof(TProjection).Name, primaryKeyPropertyName), "TProjection"); } #region SelectEntityMessage protected class SelectEntityMessage { public SelectEntityMessage(TPrimaryKey primaryKey) { PrimaryKey = primaryKey; } public TPrimaryKey PrimaryKey { get; private set; } } protected class SelectedEntityRequest { } readonly bool ignoreSelectEntityMessage; void RegisterSelectEntityMessage() { if(!ignoreSelectEntityMessage) Messenger.Default.Register(this, x => OnSelectEntityMessage(x)); } void RequestSelectedEntity() { if(!ignoreSelectEntityMessage) Messenger.Default.Send(new SelectedEntityRequest()); } void OnSelectEntityMessage(SelectEntityMessage message) { if(!IsLoaded) return; var projectionEntity = ChangeTrackerWithKey.FindActualProjectionByKey(message.PrimaryKey); if(projectionEntity == null) { FilterExpression = null; projectionEntity = ChangeTrackerWithKey.FindActualProjectionByKey(message.PrimaryKey); } SelectedEntity = projectionEntity; } #endregion } /// /// Provides the extension methods that are used to implement the IDocumentManagerService interface. /// public static class DocumentManagerServiceExtensions { /// /// Creates and shows a document containing a single object view model for the existing entity. /// /// An instance of the IDocumentManager interface used to create and show the document. /// An object that is passed to the view model of the created view. /// An entity primary key. public static void ShowExistingEntityDocument(this IDocumentManagerService documentManagerService, object parentViewModel, TPrimaryKey primaryKey) { IDocument document = FindEntityDocument(documentManagerService, primaryKey) ?? CreateDocument(documentManagerService, primaryKey, parentViewModel); if(document != null) document.Show(); } /// /// Creates and shows a document containing a single object view model for new entity. /// /// An instance of the IDocumentManager interface used to create and show the document. /// An object that is passed to the view model of the created view. /// An optional parameter that provides a function that initializes a new entity. public static void ShowNewEntityDocument(this IDocumentManagerService documentManagerService, object parentViewModel, Action newEntityInitializer = null) { IDocument document = CreateDocument(documentManagerService, newEntityInitializer != null ? newEntityInitializer : x => DefaultEntityInitializer(x), parentViewModel); if(document != null) document.Show(); } /// /// Searches for a document that contains a single object view model editing entity with a specified primary key. /// /// An instance of the IDocumentManager interface used to find a document. /// An entity primary key. public static IDocument FindEntityDocument(this IDocumentManagerService documentManagerService, TPrimaryKey primaryKey) { if(documentManagerService == null) return null; foreach(IDocument document in documentManagerService.Documents) { ISingleObjectViewModel entityViewModel = document.Content as ISingleObjectViewModel; if(entityViewModel != null && object.Equals(entityViewModel.PrimaryKey, primaryKey)) return document; } return null; } static void DefaultEntityInitializer(TEntity entity) { } static IDocument CreateDocument(IDocumentManagerService documentManagerService, object parameter, object parentViewModel) { if(documentManagerService == null) return null; return documentManagerService.CreateDocument(typeof(TEntity).Name + "View", parameter, parentViewModel); } } }