在 WPF 智能客户端应用程序中使用 WCF DTO 时理解 MVVM 使用的问题

Posted

技术标签:

【中文标题】在 WPF 智能客户端应用程序中使用 WCF DTO 时理解 MVVM 使用的问题【英文标题】:Problems understanding use of MVVM when using WCF DTO's in a WPF smart client app 【发布时间】:2012-02-16 10:06:30 【问题描述】:

在该领域几乎没有经验,我正在编写一个 WPF 智能客户端应用程序,使用 MVVM 与 WCF 后端通信,并且我真的很难从所有信息中做出正确的决定。这让我想到了一系列问题,我希望在这方面更有经验的人可以在这里解决这些问题。

例如,其中一个屏幕将允许输入订单并向订单添加订单行。

模型用什么?

在 WCF 服务上,我有以下简化的 DTO:

public OrderDTO

   string orderDetails  get; set; 
   List<OrderLineDTO> OrderLines  get; set; 


public OrderLineDTO

   int customerId  get; set; 
   int productId  get; set; 
   double quantity  get; set; 

还有一个 WCF 服务,其方法如下:

public OrderService Order

    CreateOrderResponse CreateOrder(OrderDTO order) 

然后,在我的 WPF 智能客户端中,我引用了 DTO,但显然它没有实现 INotifyPropertyChanged,因为它纯粹用于传输。

问题

推荐的方法是将这些 DTO 转换为使用 Automapper 或类似工具实现 INotifyPropertyChanged 的模型吗?还是应该直接在 ViewModel 中将 DTO 用作模型?

视图模型之间的通信

目前,我有一个带有 2 个选项卡(OrderOrderLines)的订单视图,其中 ViewModel 为 OrderViewModelOrderLineViewModel。在订单选项卡上,我有一个 ComboBox 包含客户 ID 和名称。当我在OrderView 上选择客户时,我需要告知OrderLineView 已选择客户,以便ComboBox 仅显示属于该客户的产品。

问题

在这种情况下,OrderViewModel 如何与OrderLineViewModel 通信?

添加订单行并应用逻辑/业务规则

由于服务器级应用程序将被多个客户端(例如 PC、移动设备)使用。我想确保在服务器级应用程序中应用所有业务规则。例如,添加订单行时。如果是某种产品类型,则只有在客户具有某种认证的情况下才能添加。

然而,我所读到的关于 MVVM 的所有内容都表明,模型是应用业务规则和行为的对象——所有这些示例都在客户端实现了模型。理想情况下,我不想在客户端和服务器上重复相同的检查,所以我想知道如何确保不会发生这种情况。

问题

您是否允许用户添加 invalid 行,将请求发送到服务器,让服务器应用相关规则并返回响应?还是在将请求发送到服务器之前以某种方式在智能客户端应用程序中应用逻辑?

我真的希望在我在这里概述的所有领域都变得更好,并提前感谢您的任何回复。

谢谢

亚历克斯

编辑: 感谢大家的贡献,因为它帮助我在最佳前进方向方面变得更加清晰。所有的答案都很好,但我决定接受 Uri 的答案,因为它最符合我现阶段的想法。但是,我仍然不确定处理从 DTO 的 Id 到 ItemsSource 中的 SelectedItem 的转换的最佳方法,ItemsSource 是 ViewModel 的列表。我可以看到转换器可能会工作,但我会尝试寻找另一种解决方案。谢谢亚历克斯

【问题讨论】:

【参考方案1】:

以下是我对您的问题的看法:

问题: 推荐的方法是将这些 DTO 转换为使用 Automapper 或类似方法实现 INotifyPropertyChanged 的​​模型吗?还是应该直接在viewmodel中使用DTO作为模型?

答案:我最喜欢的方法是遏制。我同意你的看法,DTO 不应该有任何东西,只有 getter 和 setter。尽可能保持干净,因此不应触发 INotifyPropertyChanged。我也不认为视图应该直接访问对象模型(如果没有其他原因,您没有更改属性的好处)。我的方法的缺点是 ViewModel 中有一些额外的代码,但我认为这是值得的。

public class VmBase : INotifyPropertyChanged 

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void raise( string propName )
    
        if( PropertyChanged ) 
            PropertyChanged( this, new PropertyChangedEventArgs(propName) );
        
    


public class OrderLineVm : VmBase 
    private OrderLineDTO orderLine;

    public OrderLineVm( OrderLineDTO ol ) 
        orderLine = ol;
    

    public OrderLineVm( ) 
        orderLine = new OrderLineDTO();
    

    int customerId 
        get  return orderLine.customerId; 
        set  orderLine.customerId=value; raise("customerId"); 
    

    int productId 
        get  return orderLine.productId; 
        set  orderLine.productId=value; raise("productId"); 
    

    double quantity 
       ...
    

通过垃圾回收的奇迹,OrderLineDTO 将只被创建一次(当它来自服务器时)并且只要需要它就可以存活。有两种公共构造函数:一种带有 DTO(通常,当对象来自服务器时),另一种在客户端上创建。

对于 OrderVm,这有点复杂,因为您希望有一个 OrderLineVm(与 OrderLineDTO)的 ObservableCollection(与 List),因此包含不起作用。另请注意,orderLines 只有一个 getter(您可以从中添加和删除订单行,但您不会更改整个列表。在构建期间分配一次)。

public class OrderVm : VmBase 
    private string _orderDetails;
    public string orderDetails 
        get  return _orderDetails;
        set  _orderDetails=value; raise("orderDetails"); 
    

    private ObservableCollection<OrderLineVm> _orderLines;
    public ObservableCollection<OrderLineVm> orderLines  
        get  return _orderLines; 
    

问题: 在这种情况下 OrderViewModel 将如何与 OrderLineViewModel 通信?

回答:如果需要交流,确实应该以最简单的方式进行。两个视图模型类位于同一层。 OrderVm 引用了一个 OrderLineVm 列表,如果您需要从 OrderLineVm 类进行通信来订购,只需保留一个引用即可。

但是,我强烈认为不需要沟通。一旦视图被适当地绑定,我认为没有理由进行这种通信。绑定的 Mode 属性应该是“双向”,所以 UI 中的所有更改都会在 View Model 中更改。由于从 ObservableCollection 发送的通知,订单行列表的添加、删除将自动反映在视图上。

问题:您是否允许用户添加无效行发送请求到服务器让服务器应用相关规则并返回响应?还是在将请求发送到服务器之前以某种方式在智能客户端应用程序中应用逻辑?

答案:除了服务器之外,在客户端进行数据验证并没有什么问题。避免重复代码 - 拥有一个执行验证的程序集(可能是定义 DTO 的程序集),并在客户端中部署此程序集。这样,您的应用程序的响应速度就会更快,并且您将减少服务器上的工作量。

显然,您需要在服务器上进行数据验证(出于安全原因和竞争冲突)。即使客户端验证通过,但服务器返回错误时,您必须处理这种情况。

编辑:(跟进 Alex 的评论):

显示一个下拉列表:我认为您混淆的根源在于实际上有两个独立的 ItemsSource(因此有两个单独的数据上下文):有一个订单行列表,每个订单行中嵌入的是ProductID,即填充组合框的项目。只有 SelectedItem 是 ProductLine 的属性。通常,可能的 ProductID 列表对于应用程序(或订单)应该是全局的。您将拥有整个表单的 ProductID 属性,并为其命名(例如 x:Key 或 x:Name)。然后,在 ComboBox 元素中只引用这个列表:

<ComboBox ItemsSource="Binding Source=StaticResource ProductIDs"
          SelectedItem="Binding Path=productId"
          />

【讨论】:

@Udi,这几乎就是我所做的。但是,我确实有点不确定如何处理下拉列表之类的东西。例如,OrderLineVM 有一个 ProductId 属性,我想在我的 OrderLine 表单上显示该 OrderLine 的 ProductCode。 OrderLineVM 是否包含产品列表或是否属于其他地方?谢谢亚历克斯 @lostinwpf:在我的回复正文中回答您的问题 我正在尝试您的建议。但是,我的组合框 ItemsSource 绑定到主窗体上的 ObservableCollection 属性。 ProductViewModel 包含 ID、代码、描述。这意味着 SelectedItem 也需要是 ProductViewModel 类型。但是就 OrderLineViewModel 而言,我只需要一个 int ProductId。因此,当我从磁盘加载时,我需要将 ProductId 转换为 ComboBox 中的正确项目并保存反向。感谢您一直以来的帮助。亚历克斯 在这种情况下,您可以简单地实现一个从 int 到 ProductViewModel 的转换器(以及从 IValueConverter 派生的对象)。然后,将转换器定义为资源,并在 SelectedItem 属性中引用该资源:SelectedItem = Binding Path=productId,Converter=StaticResource cnv【参考方案2】:

依次回答你的问题……

1) 如果您的属性在更改时不需要通知 UI,则无需使用INotifyPropertyChanged - 我认为您可以将模型直接绑定到视图。如果它不添加任何额外的功能,则无需添加额外的层。但是,在大多数应用程序中,您将希望通过 UI 更改模型对象状态。在这种情况下,您需要添加实现INotifyPropertyChanged 的视图模型对象。您可以制作一个适应模型的视图模型,即将属性委托给底层模型,或者将模型对象状态复制到等效的视图模型。

为了避免编写大量非常相似的代码,即表示为模型对象和视图模型对象的相同域对象,我尝试尽可能使用代码生成。我喜欢使用 XML 来描述我的模型,以及用于 codegen 的 T4 模板。

2) OrderViewModel 应该如何与OrderLineViewModel 通信?直接地!这些概念听起来非常紧密相关,我猜一个订单有多个订单行?在这种情况下,只需让每个视图模型引用另一个。如果两者在您的域中紧密耦合,则无需花哨的中介。

3) 好问题!我同意服务器应该应用验证。是否在客户端中复制某些此类验证取决于您的要求。如果您与服务器的通信快速且频繁,则您可以通过在用户编辑订单时与服务器通信并在订单从一个字段到另一个字段进行验证时提供良好的用户体验。然而,在许多情况下这是不切实际的。在客户端应用程序中应用简单验证是很常见的,但允许服务器进行更复杂的检查,例如检查唯一性等...

希望对您有所帮助。

【讨论】:

感谢您的回答,它们非常有用。关于1),我仍然不太确定。这是否意味着我需要public class OrderViewModel Model = new OrderModel(new OrderDTO()) 和 OrderModel 之类的东西,并且在视图中您直接绑定到 Model.OrderDetails?谢谢亚历克斯【参考方案3】:

我有 2 年构建“富客户端”(在 WPF 中)的经验。

然后在我的 WPF 智能客户端中,我引用了 DTO,但很明显 它没有实现 INotifyPropertyChanged,因为它纯粹是为了 运输。

错误 默认情况下,WCF 将在每个 DTO 上自动实现 INPC。 最好将 DTO 用作简单视图的 ViewModel,而对于更复杂的视图,使用组合“模式”。

视图模型之间的通信

“最佳”实践(阅读:几乎每个人都在做的事情)是使用弱事件模式,以保持松散耦合。最著名的一个是来自 PRISM 库的 IEventAggregator,但那里有几种实现。

添加订单行并应用逻辑/业务规则

把客户端想象成一个网页:不要相信它。它是 .NET 代码,我们都知道入侵是多么容易。 因此,您应该在 WCF 服务上实施安全检查。

HTH,

巴布。

【讨论】:

【参考方案4】:

我相信真正的问题是你希望对 MVVM 模式有多真实?

MVVM 以及 MVC 和 MVP 等类似模式背后的理念是关注点分离。虽然我也为这个话题辛勤工作,但我仔细研究了该模式试图完成的工作,并且选择变得更容易了。

使用 MVVM,您需要考虑三个问题:视图 (V)、模型 (M) 和视图模型 (VM)。看起来很明显,对吧?但是问问自己每个人真正关心的是什么,如果我们开始混合关注点会发生什么——就像我们在其他地方混合关注点时会发生的那样。我们的代码变得更难更改。

考虑到这一点,考虑通过公开使用 UI 类型的属性让 UI 潜入 ViewModel 的情况。这在处理对话(MVVM 中令人头疼的主要来源)时很常见。假设您正在使用第 3 方控件集开发应用程序,并且 UI 类型是他们的其中之一。现在,如果您交换控件集而不是仅仅更改 UI 标记(或让设计人员这样做),则必须进行多项更改。

(这让我记忆犹新,因为我刚刚进行了这样的努力,真正的 MVVM 应用程序可以快速重新设计,而其他应用程序的转换时间是其他应用程序的 10-25 倍!)

同样的场景也会影响模式的“后端”。

模型的目的是将数据传输到/从您在应用程序中使用的任何持久性机制。这可能是 Web 服务、数据库、文本文件等。仅仅因为 WCF 添加了 INotifyPropertyChanged 等功能并不意味着建议使用它们。请记住,Microsoft 从事开发工具的业务。为了销售这些工具,他们需要在各种情况和级别下工作。例如,RIA 服务非常适合快速而肮脏的应用程序,但在应用于实际解决方案时会很快崩溃(至少根据我的经验)。

那么,如果您确实使用了包含模型并且将所有属性委托给在 ViewModel 中保持状态的 Model 对象,并且 Model 的性质发生了变化,会发生什么情况?或者模型并不能满足您的所有需求。事实上,ViewModel 应该是一个适配器,它为 UI 提供了它需要操作的东西。与模型应该很少有 1:1 的关系,但我知道它会发生。

如果您在 6 个月后决定使用 REST 服务而不是 WCF,会发生什么情况?现在您的模型中没有 INPC 支持,因为您没有处理自动生成的代理类。虽然不像 UI 变化那样有形,但同样的想法也适用于此,这就是模式将模型分开的原因。

我的建议是您的第一直觉是使用 AutoMapper 将模型对象中包含的数据映射到您的 ViewModel 中,反之亦然。 AutoMapper 可以非常轻松地处理您可能面临的阻抗不匹配问题,并在合同的一方或另一方发生变化时为您提供一个单一的地方进行更改。

2

您拥有的是一个对象模型,在这种情况下,具有事件、回调等是完全合法的。我会说您的 OrderViewModel 包含 OrderLineViewModel 对象的集合。子对象可以包含对父对象 (OrderViewModel) 的引用,并从那里拉取选定的客户。

3

首先,实现业务规则和验证的是 ViewModel,而不是 Model。其次,此类规则旨在为用户提供交互式体验。无论您在 ViewModel 中设置什么规则,您都应该始终在服务器上执行完整性检查,以确保允许用户执行请求的操作并且数据对于持久性有效。

关于往返业务规则的问题,我会说不。我尝试在客户端应用程序中执行尽可能多的业务规则。一、提升了用户体验,减少了客户端所需的网络流量。我遵循的一条经验法则是,我绝不允许用户保留无效对象。注意:不准确或不完整的数据不等同于无效。无效数据导致异常。

【讨论】:

以上是关于在 WPF 智能客户端应用程序中使用 WCF DTO 时理解 MVVM 使用的问题的主要内容,如果未能解决你的问题,请参考以下文章

如何从同一 WPF 应用程序托管的 WCF 服务调用 WPF 应用程序中的方法?

什么是 WPF 和 WCF 应用程序的用户身份验证/授权的“最佳实践”?

客户端 WPF 应用程序如何使用 Azure 上的 WCF 服务进行身份验证?

WPF MVVM WCF 客户端/服务器架构

结合 WPF + WCF + 实体框架的好例子 [关闭]

如何处理 SilverLight 4 和 WPF 应用程序的 WCF 身份验证