Introduction
This article describes the usage and implementation details of a WPF CrudControl
which is a generic CRUD control implemented based on the MVVM pattern. The control abstracts both the UI and business logic to achieve a foundation for a complete CRUD operation implementation. In the getting started article, we will explain how to use the WPF CrudControl
.
Problem Definition
To develop a reusable control that serves all CRUD operations and related aspects (like validation and resetting) and facilitate developing a lookup data management module with minimum code.
UI Wire-frame
Consists of two main views as figure1 demonstrates:
View #1
- Search Criteria: Consists of a placeholder for a business related search criteria controls, search action, and reset action.
- Sorting: Consists of a sort-by combobox for business properties and a sort-direction combobox.
- Listing, Add, Edit & Delete Actions
- DataGrid: For listing data with the following columns:
- A checkbox column: To select row for deletion
- Business-related columns
- Edit action: Populates the pop-up window bound to the selected entity
- Add & Delete Actions
- Add action: Populates the pop-up window bound to a new entity
- Delete action: Deletes the selected entities
- Pager: Consists of next / previous actions, current page number, total records, and page-size combobox
View #2
- Add / Edit pop-up window: Consists of a placeholder for the business related input controls that will hold the entity values, save action, and an action to reset the changes to the original values.
Northwind Demo
The solution is applied on Northwind
database for two modules Suppliers
and Products
. In this article, we will demonstrate the solution with the Products
module as it has more advanced scenario. At the demo, we used Unity as a IoC/DI container, MVVMLight toolkit and WPF Modern UI library for styling the main window and navigation.
To run the demo:
- Restore packages using NuGet packages manager
- Install SQL Server LocalDB
Solution Design
MVVM based, and mainly relies on polymorphism and generics. Click to see the full design diagram. The solution currently depends on Microsoft.Practices.ServiceLocation
and EntityFramework
.
1. Core
The Entity class is the core for all base classes at view model layer, and it should be inherited by the business models as the figure demonstrates. It peforms the following:
- Implements the
INotifyDataErrorInfo interface and has a public method Validate() that is called by AddEditEntityBase class. It applies the ValidationAttribute data annotations that are provided with Entity's properties using ValidationContext , so an error is raised when an incorrect value is entered in the bound property, and the associated UI control colored with its error template.
- Implements
IEditableObject that backs-up the entity's original values to restore them when the reset action is fired.
- Has
IsSelected property that identifies that the Entity is selected from the UI for any usage (i.e. deletion). Also, it has IsSelectable property to be bound to identify whether the Entity is selectable or not based on business logic.
The generic repository interface IRepository<Entity> is constrained with Entity type. In this solution, the implementation of IRepository<Entity> and IUnitOfWork requires DbContext object during their instantiations.
| |
2. Views Layer
The abstracted views consist of five parts as the following figure demonstrates:
The main abstracted view is GenericCrudControl
that contains the XAML parts of the Listing
, Sorting
, Pager
and SearchCriteriaContainer
. The AddEditPopupWindow
and SearchCriteriaContainer
have a ContentControl
to hold the business controls.
The user control GenericCrudControl
uses DataGrid
control to list data and load business columns that are provided using an exposed collection-based dependency property of type CustomDataGridColumn
that inherits the base class DataGridColumn
. And it generates the element depending on the ColumnType
. It provides two types of columns, a TextColumn
as default type and TemplateColumn
type as DataTemplate
.
The SortingProperties
is a collection-based dependency property of type SortingProperty
to provide the business sorting-by properties and it is bound to Sorting-By combobox. It has a property called PropertyPath
that identifies the path that is used to generate a dynamic IOrderedQueryable<Entity>
based on the currently selected value.
All the UI CRUD controls styles could be customized using an exposed dependency properties of type Style
.
Usage
The following snippet XAML is from ProductList.xaml user control demonstrates the usage of GenericCrudControl
:
<crud:GenericCRUDControl>
<crud:GenericCRUDControl.SortingProperties>
<crud:SortingProperty DisplayName="Product Name" PropertyPath="ProductName">
</crud:SortingProperty>
<crud:SortingProperty DisplayName="Category" PropertyPath="Category.CategoryName">
</crud:SortingProperty>
<crud:SortingProperty DisplayName="Supplier" PropertyPath="Supplier.ContactName">
</crud:SortingProperty>
</crud:GenericCRUDControl.SortingProperties>
<crud:GenericCRUDControl.Columns>
<crud:CustomDataGridColumn Header="Category Name" BindingExpression="Category.CategoryName">
</crud:CustomDataGridColumn>
<crud:CustomDataGridColumn Header="Product Name" BindingExpression="ProductName">
</crud:CustomDataGridColumn>
<crud:CustomDataGridColumn ColumnType="TemplateColumn" Header="Stock">
<crud:CustomDataGridColumn.DataGridColumnTemplate>
<DataTemplate>
<ProgressBar Maximum="150" Value="{Binding UnitsInStock}"></ProgressBar>
</DataTemplate>
</crud:CustomDataGridColumn.DataGridColumnTemplate>
</crud:CustomDataGridColumn>
<crud:CustomDataGridColumn Header="Supplier Name"
BindingExpression="Supplier.ContactName" Width="*"></crud:CustomDataGridColumn>
</crud:GenericCRUDControl.Columns>
</crud:GenericCRUDControl>
3. View Model Layer
Contains the backbone logic of the solution as the following figure demonstrates:
The GenericCRUDBase<Entity>
is the controller class that is inherited by your business view model, so the usage is like that:
public class ProductsListViewModel : GenericCRUDBase<Product>
{
public ProductsListViewModel(ProductsSearchViewModel productsSearchViewModel,
ProductAddEditViewModel productAddEditViewModel)
: base(productsSearchViewModel, productAddEditViewModel)
{
ListingIncludes = new Expression<Func<Product, object>>[]
{
p => p.Category,
p => p.Supplier
};
}
}
It requires the SearchCriteriaBase<Entity>
and AddEditEntityBase<Entity>
concrete objects at the constructor. It is responsible for:
- Subscribing to the change criteria events for the searching and pager actions
- Loading data with searching, paging and sorting
- Populating Add popup window with new entity using
DialogService
- Populating Edit popup window with the selected entity using
DialogService
- Deleting selected entities
The ListingIncludes
is an array of lambda expressions that refers to the navigation properties needed to be included within data retrieval.
The AddEditEntityBase<Entity>
is a base view model class that is responsible for saving Entity
, reset Entity
changes and it closes its associated window after successful saving as it inherits the base class PopupViewModelBase
that has a delegate CloseAssociatedWindow
defined in DialogService
.
It has a ContentControl
that will hold the concrete view resolved by WPF engine based on specified DataTemplate
.
The SearchCriteriaBase<Entity>
has two virtual methods that are overridden in the concrete class:
GetSearchCriteria()
method that returns Expression<Func<Entity, bool>>
based on the entered search criteria.
ResetSearchCriteria()
method that resets the input controls.
There are a few delegates to inject a business logic before/after abstracted logic like:
At GenericCrudBase<Entity>
:
PostDataRetrievalDelegate
: A delegate that is called after retrieving data from database to manipulate the data based on business logic (i.e. updating some properties).
PreAddEditDelegate
: A delegate that is called before populating Add/Edit popup window in order to prepare its data based on business logic (i.e., base on Entity
values rebind a combobox's ItemSource
).
PostAddEditDelegate
: A delegate that is called after successfully saving the Entity
to apply a specific business logic.
The Result
The following screenshot demonstrates the result of applying WPF CrudControl
on Northwind
's Product
module.
Third-Party Implementations
INotifyDataErrorInfo
IEditableObject
ObservableObject
RelayCommand<T>
RelayCommand
Dynamic Sorting
EnumToItemsSource
Conclusion
Using WPF CrudControl
gives a huge productivity boost for straightforward Crud operations. It requires relatively minimal coding effort, while keeping it possible to customize its behavior.
You can enhance the WPF CrudControl
in this repository and use it directly through this NuGet package.