通用 WPF CRUD 控件(入门)






3.78/5 (14投票s)
基于 MVVM 模式实现的通用 CRUD 控件。
引言
本文将逐步介绍如何使用 WPF CrudControl,这是一个基于 MVVM 模式实现的通用 CRUD 控件。该控件抽象了 UI 和业务逻辑,为完整的 CRUD 操作实现奠定了基础。在 解决方案设计文章中,我们详细解释了 WPF CrudControl。
问题定义
目标是开发一个可重用的控件,用于处理所有 CRUD 操作及相关方面(如验证和重置),并能以最少的代码方便地开发查找数据管理模块。
Northwind 演示
该演示应用于 Northwind 数据库的供应商和产品两个模块。在演示中,我们使用了 Unity 作为 IoC/DI 容器,MVVMLight 工具包,以及 WPF Modern UI 库来设置主窗口和导航的样式。
运行演示
- 使用 NuGet 包管理器恢复包。
- 安装 SQL Server LocalDB。
入门
使用 Nuget 进行安装
WPF CrudControl 可在 NuGet 上找到,您可以使用 NuGet 管理器安装它,或在程序包管理器控制台中运行以下命令。
PM> Install-Package WPFCRUDControl
WPF CrudControl 程序集
- 将以下 dll 添加到您的项目中:
- GenericCodes.Core.dll
- GenericCodes.CRUD.WPF.dll
- 将以下资源字典引用添加到 App.xaml 文件中
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/GenericCodes.CRUD.WPF;component/Resources/CRUDResources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
或通过 NuGet 管理器安装 WPF CrudControl 程序包,它将自动添加程序集并修改 App.xaml 文件。
供应商查找
现在我们将逐步创建一个供应商查找,效果如下图所示。我们将使用 演示 部分中提到的 Northwind 数据库。
步骤 1
实体模型
Supplier
模型必须继承自Entity
类。- 您必须通过在
Entity
类中使用实现的NotifyPropertyChanged()
方法,或使用 Fody(如我们在下面的代码中所做),或通过其他任何方式,来为 UI 中使用的所有属性引发属性更改。
[ImplementPropertyChanged]
public partial class Supplier: Entity
{
public Supplier()
{
Products = new HashSet<Product>();
}
public int SupplierID { get; set; }
[Required]
[StringLength(40)]
public string CompanyName { get; set; }
[StringLength(24)]
[Required]
public string Phone { get; set; }
...
...
...
}
第二步
CRUD 视图 & 视图模型
在此步骤中,我们需要创建Supplier
查找搜索、添加/编辑弹出窗口和列表视图及视图模型,如下所示:
1. 供应商搜索
首先,我们将创建 SupplierSearchView.xaml,它将定义所有供应商搜索字段的 UI,类似下面的 XAML。
<UserControl x:Class="Northwind.Demo.Views.Suppliers.SuppliersSearch"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="700">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="Company Name" VerticalAlignment="Center"
Margin="2,2,16,4" />
<TextBox Grid.Column="1" Grid.Row="0" Width="220" HorizontalAlignment="Left"
VerticalAlignment="Center" Margin="2,2,5,4"
Text="{Binding CompanyNameFilter, Mode=TwoWay}" />
<TextBlock Grid.Column="2" Grid.Row="0" Text="Phone" VerticalAlignment="Center"
Margin="50,2,16,4" />
<TextBox Grid.Column="3" Grid.Row="0" Width="220" HorizontalAlignment="Left"
VerticalAlignment="Center" Margin="2,2,5,4"
Text="{Binding PhoneFilter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
...
...
...
</Grid>
</UserControl>
然后,我们将创建 SupplierSearchViewModel.cs,类似下面的代码。此类应继承自抽象类 SearchCriteriaBase<Supplier>
,并必须实现其两个主要方法:
GetSearchCriteria()
:在此方法中,您将定义您的业务搜索条件。在我们的示例中,我们通过 CompanyName 和 Phone 过滤Supplier
。如果搜索条件字段留空,控件将返回所有记录;否则,它将返回与搜索条件匹配的记录。ResetSearchCriteria()
:在此方法中,您将定义搜索属性的默认值。
public class SuppliersSearchViewModel : SearchCriteriaBase<Supplier>
{
public SuppliersSearchViewModel()
: base()
{
}
#region Public Properties
private string _companyNameFilter = string.Empty;
public string CompanyNameFilter
{
get { return _companyNameFilter; }
set { Set(() => CompanyNameFilter, ref _companyNameFilter, value); }
}
private string _phoneFilter = string.Empty;
public string PhoneFilter
{
get { return _phoneFilter; }
set { Set(() => PhoneFilter, ref _phoneFilter, value); }
}
// other Properties
...
...
...
#endregion
public override System.Linq.Expressions.Expression<Func<Supplier, bool>> GetSearchCriteria()
{
return (supplier => (string.IsNullOrEmpty(_companyNameFilter) ||
supplier.CompanyName.ToLower().Contains(_companyNameFilter.ToLower())) &&
(string.IsNullOrEmpty(_phoneFilter) ||
supplier.Phone.ToLower().Contains(_phoneFilter.ToLower())));
}
public override void ResetSearchCriteria()
{
CompanyNameFilter = string.Empty;
PhoneFilter = string.Empty;
}
}
2. 供应商添加/编辑弹出窗口
首先,我们将创建 SupplierAddEditView.xaml,它将定义所有添加/编辑供应商字段的 UI,类似下面的 XAML 视图。
<UserControl x:Class="Northwind.Demo.Views.Suppliers.SuppliersAddEdit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Height="210" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"
Margin="5,0,16,4"
Text="Company Name"/>
<TextBox Grid.Row="0"
Grid.Column="1"
VerticalAlignment="Center"
Margin="0,0,5,4"
Text="{Binding Entity.CompanyName}"/>
<TextBlock Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center"
Margin="5,0,16,4"
Text="Phone"/>
<TextBox Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Center"
Margin="0,0,5,4"
Text="{Binding Entity.Phone}"/>
..
..
..
</Grid>
</UserControl>
注意:所有 UI 字段都将绑定到 Entity.PropertyName,如下面的 XAML 所示。
<TextBox Text="{Binding Entity.CompanyName}"/>
然后,我们将创建 SupplierAddEditViewModel.cs,类似下面的代码。此类应继承自抽象类 AddEditEntityBase<Supplier>
,该类将处理这些功能(保存、重置和验证)Entity
,您可以覆盖它们。
public class SuppliersAddEditViewModel : AddEditEntityBase<Supplier>
{
public SuppliersAddEditViewModel()
: base()
{
}
}
3. 供应商列表
首先,我们将创建 SuppliersUC.xaml。在此文件中,我们将执行以下操作:
- 定义 crud 命名空间
- 使用默认样式或自定义它们以匹配您的应用程序样式,例如(DataGridStyle、DataGridColumnHeaderStyle、SearchButtonStyle 等)。
- 通过 XAML 中的 EnableAdd、EnableDelete、EnableSortingPaging 和 EnableSearch 属性显示/隐藏功能,如启用(添加、搜索和删除)。
- 使用
SortingProperties
集合定义排序的列。 - 使用
Columns
集合定义列表的列,ColumnType
的默认类型为 TextColumn。 - 定义视图数据上下文
<UserControl x:Class="Northwind.Demo.Views.Suppliers.SuppliersUC"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:crud="https://genericcodes.com/crud"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="1000"
DataContext="{Binding Suppliers,Source={StaticResource Locator}}">
<Grid>
<Border Style="{DynamicResource BorderStyle}">
<ScrollViewer Margin="5,10,0,10">
<StackPanel MinWidth="200">
<crud:GenericCrudControl
x:Name="SupplierCrudControl"
EnableSortingPaging="{Binding EnableSortingPaging,Mode=TwoWay}"
DataGridStyle="{StaticResource CRUDDataGridStyle}"
DataGridColumnHeaderStyle="{StaticResource customDataGridColHeader}"
DataGridRowHeaderStyle="{StaticResource customDataGridRowHeader}"
DataGridRowStyle="{StaticResource DataGridRowStyle}"
DataGridCellStyle="{StaticResource DataGridCellStyle}"
SearchGroupBoxStyle="{StaticResource SupplierSearchGroupBoxStyle}"
SearchButtonStyle="{StaticResource CRUDSearchButtonStyle}"
SortingByComboBoxStyle="{StaticResource SortingByComboBoxStyle}"
SortingByLabelStyle="{StaticResource SortingByLabelStyle}"
SortingTypeComboBoxStyle="{StaticResource SortingTypeComboBoxStyle}"
SortingTypeLabelStyle="{StaticResource SortingTypeLabelStyle}"
ResetButtonStyle="{StaticResource CRUDResetButtonStyle}"
AddButtonStyle="{StaticResource CRUDAddButtonStyle}"
DeleteButtonStyle="{StaticResource CRUDDeleteButtonStyle}"
PagerTotalRecordLabelStyle="{StaticResource TotalRecordLabelStyle}"
PagerPageSizeLabelStyle="{StaticResource PageSizeLabelStyle}"
PagerFirstPageBtnStyle="{StaticResource PagerFirstPageButtonStyle}"
PagerLastPageBtnStyle="{StaticResource PagerLastPageButtonStyle}"
PagerNextPageBtnStyle="{StaticResource PagerNextPageButtonStyle}"
PagerPreviousPageBtnStyle="{StaticResource PagerPreviousPageButtonStyle}"
PagerCurrentPageTextBoxStyle="{StaticResource PagerCurrentPageTextBoxStyle}"
PagerPageSizeComboBoxStyle="{StaticResource PagerPageSizeComboBoxStyle}">
<crud:GenericCrudControl.SortingProperties>
<crud:SortingProperty DisplayName="Company Name" PropertyPath="CompanyName"/>
<crud:SortingProperty DisplayName="City" PropertyPath="City"/>
<crud:SortingProperty DisplayName="Country" PropertyPath="Country"/>
</crud:GenericCrudControl.SortingProperties>
<crud:GenericCrudControl.Columns>
<crud:CustomDataGridColumn Header="Company Name" BindingExpression="CompanyName"/>
<crud:CustomDataGridColumn Header="Country" BindingExpression="Country" Width="*"/>
<crud:CustomDataGridColumn Header="Phone" ColumnType="TemplateColumn">
<crud:CustomDataGridColumn.DataGridColumnTemplate>
<DataTemplate>
<TextBlock Text="{Binding Phone}"></TextBlock>
</DataTemplate>
</crud:CustomDataGridColumn.DataGridColumnTemplate>
</crud:CustomDataGridColumn>
</crud:GenericCrudControl.Columns>
</crud:GenericCrudControl>
</StackPanel>
</ScrollViewer>
</Border>
</Grid>
</UserControl>
然后,我们将创建 SuppliersViewModel.cs 类。此类应继承自抽象类 GenericCrudBase<Supplier>
。该类的构造函数需要 SuppliersSearchViewModel
和 SuppliersAddEditViewModel
对象。这将提供所有 CRUD 功能(如添加、编辑、列表、分页和排序)。此外,如果您想在打开添加/编辑弹出窗口之前或在实体成功保存后,或在检索数据后添加一些业务逻辑,请查看此部分 委托. 在我们下面的代码中,我们定义了 PostDataRetrievalDelegate
来对检索到的数据执行一些业务逻辑,即阻止用户选择某些符合特定业务条件的实体。
public class SuppliersViewModel : GenericCrudBase<Supplier>
{
public SuppliersViewModel(SuppliersSearchViewModel supplierSearch,ISuppliersService supplierService,
SuppliersAddEditViewModel suppliersAddEdit)
: base(supplierSearch, suppliersAddEdit)
{
PostDataRetrievalDelegate = (list) =>
{
supplierService.UpdateCanSelect(list);
};
}
}
现在,我们将创建资源字典 SuppliersTemplates.xaml 并添加两个 DataTemplate
来定义 SupplierSearch 和 SupplierAddEdit 的呈现。请查看下面的代码。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:supplierViewModels="clr-namespace:Northwind.Demo.ViewModel.Suppliers"
xmlns:supplierViews="clr-namespace:Northwind.Demo.Views.Suppliers">
<DataTemplate DataType="{x:Type supplierViewModels:SuppliersSearchViewModel}">
<supplierViews:SuppliersSearch/>
</DataTemplate>
<DataTemplate DataType="{x:Type supplierViewModels:SuppliersAddEditViewModel}">
<supplierViews:SuppliersAddEdit/>
</DataTemplate>
</ResourceDictionary>
然后,您应该将此资源字典引用添加到 App.xaml。
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/GenericCodes.CRUD.WPF;component/Resources/CRUDResources.xaml"/>
<ResourceDictionary Source="/Resources/SuppliersTemplates.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
步骤 3
IoC 容器注册
在此步骤中,您需要将项目的 DbContext
、IUnitofWork
、IRepository<T>
、IDialogService
和 ViewModels
注册到您的容器中。在我们的演示中,我们使用 Unity 容器。请查看下面的代码。
public class ViewModelLocator
{
static ViewModelLocator()
{
IUnityContainer container = new UnityContainer();
#region register DataContext & UnitOfWork
Database.SetInitializer<NorthwindContext>
(new System.Data.Entity.NullDatabaseInitializer<NorthwindContext>());
container.RegisterType<ApplicationDbContext, NorthwindContext>();
container.RegisterType<IUnitOfWork, UnitOfWork>();
#endregion
#region Register Repositories
container.RegisterType<IRepository<Supplier>, Repository<Supplier>>();
container.RegisterType<IRepository<Product>, Repository<Product>>();
container.RegisterType<IRepository<Category>, Repository<Category>>();
#endregion
#region Register App Services
container.RegisterType<IDialogService, DialogService>();
#endregion
#region Register Business Services
container.RegisterType<ISuppliersService, SuppliersService>();
container.RegisterType<IProductService, ProductService>();
//container.RegisterType<IService<Category>, Service<Category>>();
#endregion
#region Register ViewModels
container.RegisterType<MainViewModel>();
container.RegisterType<SuppliersViewModel>();
container.RegisterType<SuppliersSearchViewModel>();
container.RegisterType<SuppliersAddEditViewModel>();
container.RegisterType<ProductsListViewModel>();
container.RegisterType<ProductsSearchViewModel>();
container.RegisterType<ProductAddEditViewModel>();
#endregion
ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(container));
}
public SuppliersViewModel Suppliers
{
get
{
return ServiceLocator.Current.GetInstance<SuppliersViewModel>();
}
}
}
注意
您的 App DBContext 应继承自 ApplicationDbContext
。
public partial class NorthwindContext : ApplicationDbContext
{
public NorthwindContext()
: base("name=NorthwindContext")
{
}
public virtual DbSet<Supplier> Suppliers { get; set; }
..
..
..
}
结论
使用 WPF CrudControl 可以极大地提高直线型 CRUD 操作的效率。它只需要相对较少的编码工作,同时仍然可以自定义其行为。