WCF 示例 – 第十三章 – 业务域 – 父子关系






4.87/5 (13投票s)
跨应用层开发父/子关系——以及AutoMapper、WPF自定义转换器和WPF关闭。
![]() |
![]() |
|
第十二章 | 第十四章 |
系列文章
“WCF示例”系列文章介绍了如何使用WCF进行通信和NHibernate进行持久化来设计和开发WPF客户端。该系列介绍描述了文章的范围并高度概括地讨论了架构解决方案。该系列的源代码可在CodePlex上找到。
章节概述
在本系列文章中,我们已经涵盖了最重要的基础设施组件;然而,我们的业务领域只包含一个实体,这无助于解释在跨不同应用层设计父子关系时如何解决一些常见场景。在本章中,我们将向模型中引入一个新实体,以便我们可以描述如何解决上述情况。
在本章末尾的附录部分,我们还将讨论以下主题:
- 如何在Visual Studio中执行应用程序
- 本章新增的几个WPF方面:自定义WPF转换器和显式应用程序关闭
- AutoMapper
模型实体概述
到目前为止,我们的模型由一个类组成:Customer
。我们正在添加一个名为Address
的新实体,这是一个包含客户地址详细信息的简单实体
public class Address :EntityBase
{
01 protected Address(){}
02 public virtual Customer Customer { get; private set; }
public virtual string Street { get; private set; }
public virtual string City { get; private set; }
public virtual string PostCode { get; private set; }
public virtual string Country { get; private set; }
03 public static Address Create(IRepositoryLocator locator, AddressDto operation)
{
var customer = locator.GetById<Customer>(operation.CustomerId);
var instance = new Address
{
...
};
locator.Save(instance);
return instance;
}
public virtual void Update(IRepositoryLocator locator, AddressDto operation)
{
UpdateValidate(locator, operation);
...
locator.Update(this);
}
private void UpdateValidate(IRepositoryLocator locator, AddressDto operation)
{
return;
}
}
该实体引用了`Customer`(第02行),因此我们将拥有一个一对多关系。与`Customer`类一样,我们隐藏了构造函数(第01行),因此当需要新实例时,需要调用`Create`静态方法(第03行)。
为了适应新的Address
类,Customer
类需要进行一些重构;有几个重要的点是,Customer
类将负责Address
实例的创建和删除,并且地址集合不会直接暴露以确保集合得到良好管理;另请参阅如何因为NHibernate而需要使用ISet
01 public virtual ReadOnlyCollection<Address> Addresses()
{
if (AddressSet == null) return null;
return new ReadOnlyCollection<Address>(AddressSet.ToArray());
}
02 public virtual Address AddAddress(IRepositoryLocator locator,
AddressDto operation)
{
AddAddressValidate(locator, operation);
var address = Address.Create(locator, operation);
AddressSet.Add(address);
return address;
}
03 public virtual void DeleteAddress(IRepositoryLocator locator, long addressId)
{
DeleteAddressValidate(locator, addressId);
var address = AddressSet.Single(a => a.Id.Equals(addressId));
AddressSet.Remove(address);
locator.Remove(address);
}
相反,集合通过将其克隆到`ReadOnlyCollection`中暴露(第01行)。如果需要向客户添加新地址,则必须使用`AddAddress`方法(第02行);删除地址时也适用(第03行)。
由于上述更改,领域模型如下所示:
需要将以下更改添加到NHibernate映射文件
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="eDirectory.Domain"
namespace="eDirectory.Domain.Entities">
<class name="Customer" table="Customer">
...
01 <set name ="AddressSet" fetch="subselect">
<key column="Customer_ID"
foreign-key="FK_Customer_Address"
not-null="true" />
<one-to-many class="Address"/>
</set>
</class>
<class name="Address" table="Address">
<id name="Id" type="Int64" unsaved-value="0">
<generator class="native" />
</id>
02 <many-to-one name="Customer" class="Customer"
column="Customer_ID" not-null="true"/>
<property name="Street" length="50" not-null="true" />
<property name="City" length="50" not-null="true" />
<property name="PostCode" length="10" not-null="true" />
<property name="Country" length="50" not-null="true" />
</class>
</hibernate-mapping>
在`Customer`映射中,私有`AddressSet`集合被声明为`Address`实例的一对多集合;我们指定`Address`表中的`Customer_ID`字段用作链接(第01行)。在`Address`映射部分,我们还声明`Customer`引用使用相同的列名(第02行)。这种方法允许从子级导航回父级。
让我们演示一下将更改传播到数据库是多么容易;如果我们创建一个新测试
并且配置设置为使用NHibernate模式运行测试,那么测试将为我们生成新的schema,这不是很好吗?只需记住更改测试的_App.config_文件
您可能需要打开数据库连接以查看新架构
新地址服务
我们计划修改用户界面,以便可以使用以下屏幕
我们需要提供一个新服务,以便创建、检索和更新一个Address
实例
添加新服务需要以下步骤:
- 将新接口添加到`IContractLocator`
- 接口的三个实现需要更新
- 添加三个新的
AddressServiceProxy
、AddressServiceAdapter
和AddressWcfService
类
上述类的实现很简单,因为它们实际上与Customer
服务的实现非常相似;您可以获取源代码以了解更多详细信息。
在服务器端,我们需要修改eDirectory.WcfService以将新的Address
服务添加到端点列表中
<configuration>
...
<system.serviceModel>
<services>
...
<service name="eDirectory.WcfService.AddressWcfService"
behaviorConfiguration="eDirectory.WcfServiceBehaviour">
<endpoint address="AddressServices" binding="basicHttpBinding"
bindingConfiguration="eDirectoryBasicHttpEndpointBinding"
contract="eDirectory.Common.ServiceContract.IAddressService" />
</service>
</services>
...
</system.serviceModel>
...
</configuration>
客户端
除了实现新的`AddressServiceAdapter`和`AddressServiceProxy`类之外
我们添加了一堆新的视图以及它们各自的模型和视图模型
在模型类中,需要提及的是`AgendaModel`
class AgendaModel
{
public IList<CustomerDto> CustomerList { get; set; }
public CustomerDto SelectedCustomer { get; set; }
public AddressDto SelectedAddress { get; set; }
}
请注意,该模型为选定的网格行提供了类占位符;这两种方式都适用,非常好。在视图中唯一要做的就是正确设置绑定
这可能不明显,但当从服务器检索客户列表时,每个客户DTO都包含一个地址集合。您可以实现一种更“啰嗦”的设计,即仅在选择客户时才检索地址集合。此外,`Address`类中的`Customer`引用在DTO实现中转换为存储`CustomerId`;如果您不采用这种方法,您的DTO序列化至少会是一场噩梦
`AgendaViewModel`还有一个有趣的方面,那就是我们使用`RelayCommand`类管理操作按钮的方式。在这种情况下,如果客户实例包含地址,用户需要删除所有地址,然后客户的“删除”按钮才能启用。这通过在`RelayCommand`构造函数中使用上述选定的持有者实现谓词来轻松实现
private RelayCommand DeleteCustomerCommandInstance;
public RelayCommand DeleteCustomerCommand
{
get
{
if (DeleteCustomerCommandInstance != null)
return DeleteCustomerCommandInstance;
DeleteCustomerCommandInstance =
new RelayCommand(a => DeleteCustomer(Model.SelectedCustomer.Id),
p => Model.SelectedCustomer != null &&
Model.SelectedCustomer.Addresses.Count == 0);
return DeleteCustomerCommandInstance;
}
}
XAML声明简直是小菜一碟
另一个已实现但我们之前没有机会看到的方面是:ViewModel和服务如何使用选定的客户DTO来增强用户体验;例如,当创建新的客户实例时,我们需要确保一旦用户回到主屏幕,新客户实例就是网格中选定的那个。我们通过以下方式解决此要求
public RelayCommand CreateCustomerCommand
{
get
{
if (CreateCustomerCmdInstance != null)
return CreateCustomerCmdInstance;
01 CreateCustomerCmdInstance =
new RelayCommand(a => OpenCustomerDetail(null));
return CreateCustomerCmdInstance;
}
}
private void OpenCustomerDetail(CustomerDto customerDto)
{
var customerDetailViewModel = new CustomerDetailViewModel(customerDto);
02 var result = customerDetailViewModel.ShowDialog();
03 if (result.HasValue && result.Value)
Model.SelectedCustomer = customerDetailViewModel.Model.Customer;
04 Refresh();
}
private void Refresh()
{
long? customerId = Model !=null && Model.SelectedCustomer != null ?
Model.SelectedCustomer.Id : (long?) null;
long? addressId = Model != null && Model.SelectedAddress != null ?
Model.SelectedAddress.Id : (long?)null;
var result = CustomerServiceInstance.FindAll();
Model = new AgendaModel { CustomerList = result.Customers };
if(customerId.HasValue)
{
05 Model.SelectedCustomer =
Model.CustomerList.FirstOrDefault(c => c.Id.Equals(customerId));
...
}
RaisePropertyChanged(() => Model);
}
上面有一些代码,请稍等片刻;`CreateCustomerCommand`委托给`OpenCustomerDetail`方法(第01行),该方法调用客户详细信息屏幕,如果创建了新的客户实例,它会设置模型中的`SelectedCustomer`属性(第02和03行)。然后调用`Refresh`方法,该方法调用`CustomerServiceInstance.FindAll()`并将在调用服务之前`Model.SelectedCustomer`的值(第05行)设置为它。
章节总结
父子关系在所有应用程序中都很常见;在本章中,我们讨论了在所有应用程序层中实现这些关系是多么相对容易。我们讨论了如何建模我们的实体以使集合得到良好管理。总之,父级完全负责子实例的创建和删除。这是一个很好的例子,说明我们的实体如何从简单的CRUD数据类演变为实现业务行为的更复杂的实体。
我们还讨论了NHibernate的实现,以及在项目当前阶段,创建自动管理新数据库模式的新测试是多么容易,这证明了一个非常有价值的方面。我们还涵盖了一些MVVM技术,以利用客户端的一些常见场景,例如使用`RelayCommand`上的谓词启用/禁用操作按钮;再次证明,通过为XAML视图提供丰富的模型实现,可以获得巨大的价值,从而由于XAML绑定功能而减少了代码隐藏的数量。
在下一章中,我们将讨论如何轻松地将我们的应用程序部署到Microsoft Azure。
附录
运行应用程序
对于那些刚接触本系列或不确定如何运行eDirectory应用程序的人,以下部分描述了快速运行应用程序的步骤。eDirectory是一个可以使用NHibernate存储库实现在内存中或针对SQL Server数据库运行的应用程序。在这里,我们讨论如何以非常简单的方式运行客户端:进程内内存模式。
首先,您需要验证客户端的`App.Config`是否已正确设置,以便`SpringConfigFile`设置为使用`InMemoryConfiguration.xml`文件
确保`eDirectory.WPF`应用程序设置为启动项
在Visual Studio中将配置更改为内存实例
现在您只需启动应用程序:F5 或 CTRL+F5
WCF 的一些亮点
本章在WPF方面做了几件事,值得简要讨论。WCF默认在创建的第一个视图关闭时终止客户端应用程序。在此版本的eDirectory中,需要询问用户哪个视图必须打开。一旦用户按下“确定”按钮,原始屏幕必须关闭;如果不做任何操作,应用程序将在此时终止。阻止此行为的一个简单方法是向WPF指示应用程序本身将负责其关闭
public partial class App : Application
{
public App()
{
01 ShutdownMode = ShutdownMode.OnExplicitShutdown;
}
private void BootStrapper(object sender, StartupEventArgs e)
{
var boot = new eDirectoryBootStrapper();
boot.Run();
02 Shutdown();
}
}
当`App`实例被创建时,会指示应用程序将手动关闭(第01行),这在`Run`方法返回后发生(第02行)。
第二个亮点是一个自定义枚举转换器,由`Selector`视图使用,它允许将单选按钮与特定的枚举值匹配。该转换器是
public class EnumMatchToBooleanConverter : IValueConverter
{
01 public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null || parameter == null) return false;
string checkValue = value.ToString();
string targetValue = parameter.ToString();
return checkValue.Equals(targetValue,
StringComparison.InvariantCultureIgnoreCase);
}
02 public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null || parameter == null) return null;
bool useValue = (bool)value;
string targetValue = parameter.ToString();
return useValue ? Enum.Parse(targetType, targetValue) : null;
}
}
`Convert`方法用于判断给定枚举值是否应设置单选按钮;该方法假定如果参数与传递的值匹配,则应设置单选按钮。如果未设置单选按钮,`ConvertBack`返回`null`;如果已设置,则返回XAML中设置的枚举值。
XAML如下所示
转换器被声明为名为`enumConverter`的资源,然后在单选按钮声明中使用;每个都分配了一个枚举值;`CurrentOption`是ViewModel上声明的`ViewTypeEnum`属性,它无需任何其他额外代码即可正确设置。棒极了!
AutoMapper
在本章中,我们决定引入AutoMapper。这是一个对象到对象的映射器,非常适合在处理领域实体和DTO时使用。您可以查看CodePlex项目以获取更多详细信息。
使用 AutoMapper 相当容易。首先,我们创建映射,然后安装它们,之后就可以使用这些映射了。在 eDirectory.Domain 项目中,添加了一个新类来声明映射
定义了两个映射,其中从`Customer`到`CustomerDto`的映射比较有趣。它将DTO的`Addresses`集合映射到一个函数,该函数委托给另一个AutoMapper映射,将实体中的`Addresses`集合映射到`AddressDto`实例的集合。
然后,当WCF服务启动时,将调用静态的`Install`方法
您还可以利用Spring.Net的功能,只需在配置文件中声明该类即可初始化静态方法;这是我们在内存模式下执行应用程序时使用的方法;这是Spring.Net功能的另一个很好的例子
在`Customer`服务实现中可以找到eDirectory解决方案如何使用AutoMapper映射的示例