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

14

我在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 ,因为它纯粹是用于传输。

问题

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

视图模型之间的通信

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

问题

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

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

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

然而,我阅读的有关MVVM的所有内容都指出模型是应用业务规则和行为的地方- 所有这些示例都在客户端上实现了模型。 理想情况下,我不希望在客户端和服务器上重复相同的检查,因此我想知道如何确保这种情况不会发生。

问题

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

我真的很想在我概述的所有领域中变得更好,并且提前感谢您的任何回答。

谢谢

Alex

编辑: 非常感谢大家的贡献,因为它们帮助我在某种程度上更加清晰地思考最佳方法。 所有答案都很好,但我决定接受Uri的答案,因为它最符合我现阶段的想法。 但是,我仍然不确定如何将DTO的ID转换为ItemsSource的SelectedItem,这是一个ViewModel列表。 我可以看到Converter可能有效,但我要尝试找到另一种解决方案。谢谢Alex

4个回答

5

以下是我对您问题的思考:

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

答案:我最喜欢的方法是包含。我同意DTO不应该有除了getter和setter之外的任何内容。保持它尽可能干净,因此它不应该触发INotifyPropertyChanged。我也认为View不应该直接访问对象模型(如果没有其他原因,您就无法获得属性更改的好处)。我的方法的缺点是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的ObservableCollection(而不是OrderLineDTO),所以包含关系不起作用。还要注意,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中发生的所有更改都将在视图模型中进行更改。通过ObservableCollection发送的通知,对订单行的添加和删除将自动反映在视图上。

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

答案: 在客户端和服务器上进行数据验证是没有问题的。避免重复代码-有一个单一的程序集(可能是定义DTO的程序集),执行验证,并在客户端部署此程序集。这样,您的应用程序将更具响应性,并减少服务器的工作量。

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

编辑: (回复Alex的评论):

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

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

@Udi,这基本上就是我所做的。但是,当涉及到下拉列表之类的东西时,我会有点不确定该如何处理。例如,OrderLineVM具有ProductId属性,在我的OrderLine表单上,我想显示该OrderLine的ProductCode。OrderLineVM是否包含产品列表,还是应该放在其他地方?谢谢Alex - lostinwpf
@lostinwpf:在我的回复正文中回答了你的问题。 - Uri London
我正在尝试您的建议。但是,我的ComboBox ItemsSource绑定到主窗体上ObservableCollection<ProductViewModel>属性。ProductViewModel包含Id、Code、Description。这意味着SelectedItem也需要是ProductViewModel类型。但是就OrderLineViewModel而言,我只需要一个int ProductId。因此,当我从磁盘加载时,我需要将ProductId转换为ComboBox中正确的项,并反之保存。感谢您迄今为止的帮助。Alex - lostinwpf
在这种情况下,您可以简单地实现一个从int到ProductViewModel的转换器(并从IValueConverter派生的对象)。然后,将转换器定义为资源,并在SelectedItem属性中引用该资源: SelectedItem = {Binding Path=productId,Converter={StaticResource cnv}} - Uri London

1

逐一回答您的问题...

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

为了避免编写大量相似的代码,即表示为模型对象和视图模型对象的相同领域对象,我尽可能使用代码生成。我喜欢使用XML描述我的模型,并使用T4模板进行代码生成。

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

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

希望这有所帮助。


谢谢你的回答,它们非常有用。我仍然不太确定关于1)的问题。这是否意味着我需要像 public class OrderViewModel{ Model = new OrderModel(new OrderDTO()) } 这样的东西,而在视图中直接绑定到 Model.OrderDetails?谢谢,Alex。 - lostinwpf

0

我有两年的WPF“富客户端”构建经验。

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

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

在视图模型之间通信

“最佳”做法(即几乎所有人都这样做)是使用弱事件模式来保持松散耦合。最著名的是PRISM库中的IEventAggregator,但还有其他实现。

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

将客户端视为网页:不要相信它。它是.NET代码,我们都知道它有多容易被黑客攻击。
这就是为什么您应该在WCF服务上实现安全检查。

希望对您有所帮助,

Bab。


0

我认为真正的问题是你想要多符合MVVM模式的标准?

MVVM的理念,以及类似的MVC和MVP模式,都是关注点分离。虽然我也曾经苦恼于这个话题,但我更深入地研究了该模式试图实现的目标,选择变得更加容易。

在MVVM中,你有三个关注点:视图(V)、模型(M)和视图模型(VM)。听起来很明显,对吧?但是问问自己每个关注点真正关心什么,如果我们开始混合关注点会发生什么——就像在其他地方混合关注点一样。我们的代码变得更难改变。

考虑这样一个情况,你让UI渗透到ViewModel中,通过公开使用UI类型的属性。这在处理对话框时很常见(MVVM中的主要头疼之源)。假设你正在使用第三方控件集开发应用程序,并且UI类型是他们的其中之一。现在,如果你交换控件集,你必须进行多次更改,而不仅仅是更改UI标记(或请设计师更改)。

(这让我印象深刻,因为我刚刚进行了这样的尝试,真正的MVVM应用程序很容易重新设计,而其他应用程序需要花费10-25倍的时间进行转换!)

这种情况同样影响了模式的“后端”。

模型的目的是将数据传输到/从您的应用程序使用的任何持久性机制中。这可以是Web服务、数据库、文本文件等。仅仅因为WCF添加了INotifyPropertyChanged等功能并不意味着推荐使用它们。请记住,微软公司是开发工具的企业。为了销售这些工具,它们需要在各种情况和层次中运行。例如,RIA服务非常适合快速而简单的应用程序,但在应用于实际解决方案时很快就会崩溃(至少在我的经验中是这样)。

那么,如果您确实使用包含模型,并且所有属性都委托给在ViewModel中保持状态的Model对象,而Model的性质发生了变化怎么办?或者Model不能满足您的所有需求。事实上,ViewModel应该是一个适配器,为UI提供其所需的操作。通常不应该存在1:1的关系,但我知道这种情况确实存在。

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

我的建议是跟随您的第一直觉,并使用AutoMapper将包含在您的模型对象中的数据映射到您的ViewModel中,反之亦然。 AutoMapper使处理可能遇到的阻抗不匹配问题变得非常容易,并为您提供一个单一的位置来进行更改,以应对合同的任何一侧发生变化的情况。

2

你所拥有的是一个对象模型,在这种情况下,拥有事件、回调等是完全合法的。我会说你的OrderViewModel包含了一组OrderLineViewModel对象。子对象可以包含对父对象(OrderViewModel)的引用,并从中获取所选客户。

3

首先,实现业务规则和验证的是ViewModel而不是Model。其次,这些规则存在是为了提供用户交互体验。无论您将哪些规则放入ViewModel中,您都应该始终在服务器上执行完整性检查,以确保用户被允许执行所请求的操作,并且数据对于持久性是有效的。

至于往返业务规则的问题,我会说不需要。我尽量在客户端应用程序中强制执行尽可能多的业务规则。一方面,它可以改善用户的体验,另一方面,它可以减少客户端所需的网络流量。我遵循的一个经验法则是,我从不允许用户持久化无效的对象。注意:不准确或不完整的数据与无效数据不同。无效数据会导致异常。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接