在 XAML 中使用 EntitySpaces 业务对象
探讨如何在 WPF 应用程序中使用 EntitySpaces 业务对象框架。

目录
引言
开发一个有效的业务层可能是任何数据库驱动应用程序中最具挑战性的任务。当然,万事皆有挑战,但业务层就像一对互相憎恨的夫妇的共同朋友:它是所有人依赖以维系联系、保持和平与和谐的纽带。业务层接口需要以一种简洁、有效且灵活的方式来表示数据库。业务层在处理来自数据库的实体组合以及内存中新创建的实体(没有数据库分配的 ID/默认值或计算列)时,也必须保持一致的行为。随着 UI 技术和业务层技术之间的数据绑定方法变得越来越复杂,如果您选择的技术不兼容,可能会非常痛苦。
在我的团队的项目早期,我们非常倾向于使用 Windows Presentation Foundation (WPF) 和 XAML 来创建我们的用户界面。为了继续使用 WPF/XAML,我们需要找到一个能够与其数据绑定方法论良好配合的业务层框架。我的首选是 dOOdads。很快就明显看出 d00dads 与 XAML 的配合并不好,原因不值得赘述。在研究了其他几个框架之后,我们对 EntitySpaces (ES) 试用版进行了大量工作,并发现它与 XAML 的配合效果相当好。以下是我们喜欢 ES 的一些原因:
- 丰富且直观 - EntitySpaces 按您想要的方式实现您想要的功能;几乎所有我想要的东西都在里面,而且易于使用。
- 动态查询 - ES 提供了一种在代码中创建动态、类型安全查询的方法,而不是使用自定义存储过程或编写临时查询。
- 简单的代码生成 - 重新生成所有业务对象很简单,并且使用部分类,因此生成的代码不会与用户代码重叠。
- 分层属性 - 除了外键 ID 属性外,还会为外键引用的对象生成类型化引用属性。
- 可空属性 - 生成的属性使用可空类型,因此我们可以区分
null
值与空值/0
值。 - 自定义集合 - 这些集合很方便,因为它们提供了一种实现自定义集合级代码的方法,同时仍提供通用的集合功能。
- 良好的支持 - 免费软件很好,但仅支持可能就值回票价了——ES 团队非常迅速地响应了一些阻止性问题。
并非一切都完美,但我们与 ES 和 XAML 的配合相当不错。EntitySpaces 团队解决了几个问题,我们找到了其他问题的解决方法。本文的目的是带您了解我们的探索之旅,并向您展示如何避免陷阱。我希望它能帮助您在选择演示和业务层技术时做出明智的决定。
先决条件
在阅读本文之前,您应该熟悉 Windows Presentation Foundation 和 EntitySpaces 这两种技术。本文并非旨在成为这两种技术之一的教程,而是演示如何将它们结合使用。如果您不熟悉 EntitySpaces 框架,建议您先阅读 EntitySpaces 网站上的文档,熟悉该框架。我建议从“入门”PDF 或“演练”视频演示开始。在本文中,我将只介绍对使您的业务实体与 WPF 数据绑定协同工作至关重要的具体步骤。至于 WPF,您至少需要具备 XAML 和数据绑定的基本知识才能理解本文涵盖的主题。
要使用 EntitySpaces 作为您的业务层来开发自己的应用程序,您需要 下载 MyGeneration 和 EntitySpaces 试用版。两者都是免费下载,但 EntitySpaces 试用版的使用期限为 45 天。
运行示例应用程序
在运行本文的示例应用程序之前,您需要执行四项操作:
- 下载 EntitySpaces 试用版。
- 在您的 SQL Server 上安装 Northwind 数据库(Northwind 创建脚本包含在 zip 文件中)。
- 编辑 ESN.WindowsClient.exe.config 中的连接字符串以连接到您的数据库。
- 将以下引用添加到两个项目中:
EntitySpaces.Core
EntitySpaces.Interfaces
EntitySpaces.SqlClientProvider
EntitySpaces.Loader
完成这些步骤后,构建解决方案即可运行。
ESN 业务层设置
在 XAML 中使用 EntitySpaces 的第一个重要步骤在于代码生成。WPF 通过 INotifyPropertyChanged
接口支持动态数据绑定,因此请确保在生成业务实体时选择“支持 INotifyPropertyChanged”选项。如果没有此选项,您仍然可以绑定到 EntitySpaces 实体,但它们不会反映底层业务层中发生的变化。
您可以在下方看到我用于为该项目生成业务对象的选项:


这是生成的 Customers
类的样子:


生成的类在创建后即可使用。您可以使用 ES 自定义主模板(具有类似的设置)来创建单独的文件(部分类)来包含用户创建的代码,但这并非必需。在此项目中,我只将自定义代码添加到了一个类(稍后介绍)。
开发 ESN 用户界面
EntitySpaces Northwind 客户编辑器界面的用户界面分为三个区域:
- 一个网格,用于查看所有客户数据。
- 一个网格,用于编辑选定客户的数据。
- 一个区域,用于将客户与客户类型(人口统计信息)匹配。
客户视图网格非常简单。以下是我用于定义它的 XAML:
查看业务实体中的数据
<ListView Grid.Row="0" Grid.ColumnSpan="3" Name="CustomersListBox"
SelectionMode="Single" IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding ElementName=ThisWindow, Path=Customers}"
ToolTip="Press Ctrl + S to save changes.">
<ListView.View>
<GridView>
<GridViewColumn Header="Company Name"
DisplayMemberBinding="{Binding CompanyName}"/>
<GridViewColumn Header="Contact Name"
DisplayMemberBinding="{Binding ContactName}"/>
<GridViewColumn Header="Contact Title"
DisplayMemberBinding="{Binding ContactTitle}"/>
<GridViewColumn Header="Address" DisplayMemberBinding="{Binding Address}"/>
<GridViewColumn Header="City" DisplayMemberBinding="{Binding City}"/>
<GridViewColumn Header="Region" DisplayMemberBinding="{Binding Region}"/>
<GridViewColumn Header="Postal Code" DisplayMemberBinding="{Binding PostalCode}"/>
<GridViewColumn Header="Country" DisplayMemberBinding="{Binding Country}"/>
<GridViewColumn Header="Phone" DisplayMemberBinding="{Binding Phone}"/>
<GridViewColumn Header="Fax" DisplayMemberBinding="{Binding Fax}"/>
<GridViewColumn CellTemplate="{StaticResource DeleteCustomerTemplate}"/>
</GridView>
</ListView.View>
</ListView>
您需要认识到的第一件事是,此 ListView
中项目的源是一个 EntitySpaces 集合(CustomersCollection
)。绑定中的 ElementName
指的是窗口本身(声明为 Name="ThisWindow"
),因此 Customers
属性可以在 MainWindow.xaml 的代码隐藏文件(MainWindow.xaml.cs)中找到。这是属性定义:
private Biz.CustomersCollection _customers = null;
public Biz.CustomersCollection Customers
{
get
{
if (_customers == null)
ResetCustomers();
return _customers;
}
}
void ResetCustomers()
{
_customers = new Biz.CustomersCollection();
_customers.Query.OrderBy(_customers.Query.ContactName.Ascending);
_customers.Query.Load();
}
使用 EntitySpaces 从数据库动态加载数据非常容易。由于此处未指定 where
子句,因此 _customers
集合将填充 Customers
表中的所有记录(按 ContactName
排序)。重要的是,此类集合应定义为 Window/Control 本身的实例变量。在一个标签式应用程序中,我最初使用了跨多个标签共享的 static
属性来提高效率,但这导致了一些奇怪的行为,即引用该集合的不同控件会共享同一个选定项(在一个中更改会影响其他控件)。
设置好数据源后,定义列就非常简单了。每一列都绑定到业务实体类(Customers
)上的 public
属性。任何 public
属性都可以作为 XAML 控件的绑定源,但 INotifyPropertyChanged
接口以及每个属性上的正确实现对于在底层数据更改时自动更新绑定至关重要。我在生成业务对象时使用的选项为所有基于数据库列的属性创建了属性更改通知,并且可以轻松地在用户创建的代码文件中为自己的属性添加 PropertyChanged
支持。
customers grid
中最后有趣的一点是使用 CellTemplate
而非 DisplayMemberBinding
的列。它引用的 static
资源可以在 Window.Resources
部分找到。这是一个相当简单的模板,但目的是演示如何使用模板以更灵活的方式显示数据或提供交互,使用更复杂的控件。您也可以同样轻松地使用 TextBoxes
和/或 ComboBoxes
对所有列进行模板化,以便在 grid
中进行内联编辑,但我希望在此示例中保持简单。
编辑器控件
忽略不重要的布局和标签控件,允许您编辑客户数据的 TextBox
与查看器 grid
一样简单。
<TextBox Grid.Row="0" Grid.Column="2"
Text="{Binding CompanyName, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="2" Grid.Column="2"
Text="{Binding ContactName, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="4" Grid.Column="2"
Text="{Binding ContactTitle, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="6" Grid.Column="2"
Text="{Binding Address, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="8" Grid.Column="2"
Text="{Binding City, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="10" Grid.Column="2"
Text="{Binding Region, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="12" Grid.Column="2"
Text="{Binding PostalCode, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="14" Grid.Column="2"
Text="{Binding Country, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="16" Grid.Column="2"
Text="{Binding Phone, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="18" Grid.Column="2"
Text="{Binding Fax, UpdateSourceTrigger=PropertyChanged}"/>
默认情况下,Binding
到 TextBox.Text
是双向的,因此每当文本更改时,binding
都会将文本写回 Binding
源(从而更新 GridView
)。我唯一需要从默认设置更改的是 UpdateSourceTrigger
属性。没有此设置,更改将不会传递到源,直到 TextBox
失去焦点,因此当用户键入时,GridView
将与文本不同步。虽然此示例仅使用 TextBoxes
和 string
属性,但绑定到生成的类对于其他控件和数据类型同样有效。但是,在某些情况下,您可能需要使用转换器将源数据转换为您要设置的属性的类型。嗯,这听起来像一个过渡……
处理多对多关系
EntitySpaces 模板根据外键引用的类型(一对一、一对多、多对多)生成不同的属性定义。对于 Northwind Customers
表,ES 会生成两个属性和两个方法来管理与 CustomerDemographics
的多对多关系。
// Properties
CustomerCustomerDemoCollection CustomerCustomerDemoCollectionByCustomerID
CustomerDemographicsCollection UpToCustomerDemographicsCollection
// Methods
void AssociateCustomerDemographicsCollection(CustomerDemographics)
void DissociateCustomerDemographicsCollection(CustomerDemographics)
这些属性分别允许您访问链接表中的关联记录集合以及相关的 Demographics
集合。您可以通过 CustomerCustomerDemoCollectionByCustomerID
手动创建和删除关联,或者使用提供的 Associate
和 Dissociate
方法。保存 Customer
时,对关联的任何更改都会保存到数据库。那么,如何将所有这些组合起来创建一个有效的用户界面呢?好问题。让我们深入探讨一下。
首先,我们需要显示所有可用的 CustomerDemographics
,以便用户可以选择要与选定的 Customer
关联的项。然后,我们需要显示每个人口统计信息是否已关联。我决定通过 CheckBox
列表来表示这一点。WPF 实际上没有预定义的 CheckBoxList
控件,但没关系。我们可以使用一个通用的 ItemsControl
配合模板来显示 CheckBoxes
。
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding ElementName=ThisWindow, Path=Demographics}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Margin="4,2,10,2" Content="{Binding CustomerDesc}"
Click="CustomerType_Click">
<CheckBox.IsChecked>
<MultiBinding Mode="OneWay"
Converter="{StaticResource CustomerDemographicsCheckConverter}">
<MultiBinding.Bindings>
<Binding ElementName="CustomersListBox" Path="SelectedItem"/>
<Binding Path="."/><!-- The current Data Context (a CustomerDemographic)-->
</MultiBinding.Bindings>
</MultiBinding>
</CheckBox.IsChecked>
</CheckBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
首先:ItemsSource
。这绑定到一个与我创建的 Customers
集合类似的属性。接下来,ItemsPanel
定义控件内项目的布局。最重要的一部分是定义每个项目内容的模板。由于 ItemsSource
是 CustomerDemographicsCollection
,因此每个项目的 DataContext
将是 CustomerDemographics
对象,因此内部的所有绑定都将基于该源。例如,Content="{Binding CustomerDesc}
是将每个 CheckBox
的文本设置为每个人口统计信息的描述。
IsChecked
状态是最棘手的部分。首先,它需要是一个 MultiBinding
,因为我需要同时知道当前的 customer
和 demographic
才能确定它们是否已关联。其次,我需要一个自定义的 MultiValueConverter
将绑定中的值转换为布尔型的选中状态。最后,我必须处理选中状态的变化,并将这些变化体现在业务对象中。值转换器支持双向转换,但很难将布尔值(选中状态)转换为 Customer
和 CustomerDemographic
对象。我决定保持简单,使用单向 binding
并在 CheckBox
上处理 Click
事件。CheckBox
还有 Checked
和 Unchecked
事件,但这些事件在框架设置 IsChecked
状态之前触发,所以行不通。为什么不行?因为绑定是单向的,WPF 会在复选框被点击后将 CheckBox
的 IsChecked
状态设置为一个字面值。每当 databound
属性被设置为字面值时,它将覆盖绑定。因此,在用户点击 CheckBox
后,其选中状态将保持不变,即使我选择了另一个 customer
。我的解决方案是在每次 CheckBox
单击后重置 binding
(_checkBoxBinding
是一个等同于 XAML 文件中声明的绑定的 binding
对象)。
void CustomerType_Click(object sender, RoutedEventArgs e)
{
Biz.Customers currentCustomer = this.CustomersListBox.SelectedItem as Biz.Customers;
if (currentCustomer == null)
return;
CheckBox check = sender as CheckBox;
if (check == null)
return;
Biz.CustomerDemographics currentDemographic =
check.DataContext as Biz.CustomerDemographics;
if (currentDemographic == null)
return;
// This will be after the check status changed
if (check.IsChecked ?? false)
currentCustomer.AssociateDemographic(currentDemographic);
//currentCustomer.AssociateCustomerDemographicsCollection(currentDemographic);
else
currentCustomer.DissociateDemographic(currentDemographic);
//currentCustomer.DissociateCustomerDemographicsCollection(currentDemographic);
check.SetBinding(CheckBox.IsCheckedProperty, this._checkBoxBinding);
}
您可能会注意到此 Click
处理程序中的另一个奇怪之处:我没有使用生成的 Associate
和 Dissociate
方法。这是因为这些方法引起的变化无法通过业务对象的任何 public
属性来访问。如果您只想关联某个项并保存它,它们工作正常,但如果您想绑定到最新的内存中关联,那就无能为力了。因此,我没有使用生成的关联方法,而是创建了我自己的版本,它们只将记录添加到链接表中,然后我在 binding
的 converter
中检查该表以获取关联。以下是这些方法的模样:
public void AssociateDemographic(CustomerDemographics demographic)
{
CustomerCustomerDemo link = this.CustomerCustomerDemoCollectionByCustomerID.AddNew();
link.CustomerID = this.CustomerID;
link.UpToCustomerDemographicsByCustomerTypeID = demographic;
}
public void DissociateDemographic(CustomerDemographics demographic)
{
foreach (CustomerCustomerDemo link in
this.CustomerCustomerDemoCollectionByCustomerID)
if (link.CustomerID == this.CustomerID &&
(object.ReferenceEquals(link.UpToCustomerDemographicsByCustomerTypeID,
demographic) ||
link.CustomerTypeID == demographic.CustomerTypeID))
link.MarkAsDeleted();
}
我对这种方法唯一不喜欢的是,每次用户单击 CheckBox
时都会创建和删除记录。如果数据库中已经存在一条记录,它可能会被删除并替换为新记录,所以链接集合在这方面不是很智能。至少在单击保存时一切都能正常工作;不需要手动操作。
EntitySpaces 集合的困惑
在使用 EntitySpaces 时,您可能还会遇到另一个问题。虽然单个实体可以支持 INotifyPropertyChanged
,但尚不支持 INotifyCollectionChanged
(这是 .NET 3.0 的新特性)。WPF 控件主要使用它来跟踪集合中的更改。您可能会认为,由于它们不实现此接口,ES 集合将不会向 databound
控件提供正确的更改通知,并且您将无法在 UI 中看到集合中的更改。但事实并非如此。我不确定原因,但在大多数情况下它们确实有效。控件将 ES 集合与 List
或 ObservableCollection
区别对待的唯一一次是在集合最初为空时。如果您向集合加载了 1 条或多条记录并进行绑定,那么在添加和删除记录时一切都会按预期更新,但如果集合在首次绑定时为空,则 UI 控件在添加和删除记录时会表现得非常奇怪。我不确定这是 EntitySpaces 的问题还是 WPF 的问题,但幸运的是有一个解决方法。您可能已经注意到 Demographics
属性定义中的这种奇怪现象:
void ResetDemographics()
{
_demographics = new Biz.CustomerDemographicsCollection();
_demographics.LoadAll();
_demographics.DetachEntity(_demographics.AddNew());
//_demographics.AddNew().MarkAsDeleted(); <-- this works too
}
由于问题发生在绑定到初始为空的集合时,我的一个同事发现,如果您在初始化集合后简单地添加一条记录,然后立即删除它,那么一切都会正常工作。
保存及其他
我最后想提的是一个很有趣的缺点,因为我在尝试实现一个功能时发现了两个缺点。我想要做的是在 Customer
网格中添加一个带有“保存”按钮的列,允许用户单独保存用户。以下是该列的模板样式:
<DataTemplate x:Key="SaveCustomerTemplate">
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding es.IsAdded}" Value="True">
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
<DataTrigger Binding="{Binding es.IsModified}" Value="True">
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</DataTemplate>
我在此设置中发现的第一个问题是,即使在更改了一些客户数据后,保存按钮也永远不会启用。这是因为为数据库列生成的属性实现了 PropertyChanged
通知,但 EntitySpaces 的核心类(例如实体 es
属性中引用的类)则没有(es
对象存储有关实体 RowState
的信息)。由于 es.IsAdded
和 es.IsModified
属性不触发属性更改事件,因此按钮的 IsEnabled
状态永远不会改变。我已将此问题报告给 EntitySpaces 团队。他们过去对错误报告和功能请求响应迅速,所以我预计这将在他们的下一个版本中得到解决。
第二个关于独立保存按钮的问题是我在最初产生这个想法时就应该意识到的。EntitySpaces 不支持保存集合中的单个实体。它实际上会抛出异常。据我所知,这是出于效率原因。显然,一次性保存整个集合的更改效率更高,但某些用户可能出于合法业务原因需要保存集合内的单个对象。此外,一些内置的 EntitySpaces 框架行为可能会导致此异常。在处理此示例应用程序时,实际上发生了这种情况:
- 用户创建一个新的
demographic
。 - 用户将该
demographic
与第一个customer
关联。 - 用户保存
customers
集合。 - 当第一个
customer
保存时,它会保存新的关联,从而保存新的demographic
。 - 由于
demographic
属于一个集合,因此会抛出异常,并且保存被中止。
在这种情况下,我通过先保存整个 Demographics
集合来解决问题,但这并不总是这么容易。我认为如果 ES 允许用户独立于其所在集合来保存实体,那会好得多。
结论
这就是我对将 EntitySpaces 业务对象框架与 Windows Presentation Foundation 结合使用的分析的总结。总的来说,我认为 EntitySpaces 是一个很棒的框架,但还有改进的空间。如果非要量化的话,我认为 ES 大约有 90% 已经为 XAML 做好了准备。在大多数情况下,您可能会对 ES 对象和 XAML 的功能感到满意,但与任何技术(包括 XAML 本身)一样,也有一些陷阱需要您去克服。如果您想了解更多关于 EntitySpaces(无论是与 WPF 一起使用还是不一起使用)的信息或获得帮助,我建议您访问他们的网站和 支持论坛。
许可说明
与 EntitySpaces 相关的代码不受任何特定许可的约束。本文中的所有其他代码均受以下许可约束。