使用非贫血领域模型与Wpf MVVM

6
我正在使用MVVM来实现基于WPF的应用程序UI。我有一个ViewModel,它包装每个可编辑的Model,可以进行编辑。VM包含处理错误通知,"is dirty"管理等所有逻辑。这种设计很好地支持了简单领域模型对象的CRUD场景,这些对象是无效的,也就是说没有包含任何逻辑。
现在,我面临着一个更棘手的问题,因为我有一个包含逻辑的领域模型,而且该逻辑可以改变领域模型的内部状态。
是否有人已经面对过这种情况?如果有,你有什么建议来正确处理这个问题?
Riana
3个回答

9
以下是我的常规做法:
1. ViewModel 层由属于此层的类型组成,这意味着我不会直接在 ViewModel 中使用业务对象。我将业务对象映射到 ViewModel 对象,这些对象可能与原始对象的形状相同或不同,但减去了行为。可以认为这违反了“不要重复自己”的原则,但这样做可以遵循单一职责原则。在我看来,SRP 通常应优先于 DRY。ViewModel 的存在是为了服务于视图,而模型的存在是为了服务于业务规则/行为。
2. 我创建了一个门面/服务层,该层以 ViewModel 作为参数并返回 ViewModel,但将 ViewModel 映射到其对应的业务对象版本。这样,非贫血对象就不会对 ViewModel 引入非视图逻辑。
依赖关系如下: ViewModel <--> 门面/服务层 --> 业务对象
如果您想充分发挥 MVVM 的潜力,我认为记住以下内容很重要:ViewModel 是视图的模型/抽象,而不是呈现给视图的模型。

嗨,丹尼尔,感谢你的回答。我的应用程序几乎完全按照你描述的方式构建,只是我使用另一种机制将模型与视图模型进行映射。现在假设您的模型包含更改其属性之一状态的行为,并且该行为应从UI(单击按钮或其他任何内容)触发。您如何处理这种情况,因为这正是我问题的关键点?您是否也在VM中重现了逻辑?谢谢。 - Riana
4
假设我们有一个"计算总价"按钮和一个OrderViewModel。点击按钮会触发CalculateOrder命令,该命令会调用ViewModel上的CalculateOrder方法,然后调用_facade.CalculateTotal(orderViewModel)。Facade会将所有内容映射到业务对象,计算出总价,然后返回更新后的ViewModel。 - Daniel Auger
1
谢谢你的建议,Daniel。对我来说非常有用!实际上,从那个架构来看,你的Facade将负责在域模型上执行行为,跟踪域模型状态变化,然后更新VM以显示域模型的新状态,是吗? - Riana
1
有趣的观点:“ViewModel 是视图的模型/抽象,而不是呈现给视图的模型。” 然而,这与大多数人似乎实现 MVVM 的方式非常不同。 你有任何参考资料可以进一步阐明这一点吗? - jpierson

2
尝试使用命令模式。您的屏幕应该被设计成执行实体上的操作(命令),而不是编辑实体。如果您在设计屏幕时遵循这个原则,那么您的ViewModel将具有应映射到命令对象的属性。然后,该命令将被发送到领域模型的(远程)facade。用于显示数据的ViewModel可以直接映射到数据库(完全绕过领域模型),因此您不需要在领域模型类中放置糟糕的getter。

感谢Szymon的回答。您是在描述CQRS架构,对吗?实际上,我曾考虑过它,但因为担心它会给我的简单小应用程序带来很多开销而放弃了。然而,我怀疑使用这种模型来编辑领域模型的命令可能是目前解决我的问题的最佳方法,我实际上正在尝试深入研究这种方式。 - Riana

1
如果领域模型不是贫血的,您将需要使用事件来将模型内部的更改传达回ViewModel。这样,您就不必担心跟踪哪些操作可能会使VM与模型不同步。
以下是一个简单的示例:
首先,是一个示例模型:
public class NonAnemicModel
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            if (_name == value)
                return;

            _name = value;
            OnNameChanged(EventArgs.Empty);
        }
    }

    public event EventHandler NameChanged;
    protected virtual void OnNameChanged(EventArgs e)
    {
        if (NameChanged != null)
            NameChanged(this, e);
    }

    public void PerformNameCalculation(int chars)
    {
        //example of a complex logic that inadvertently changes the name
        this.Name = new String('Z', chars); //makes a name of Z's
    }
}

这里是一个示例 ViewModel:

public class MyViewModel : INotifyPropertyChanged
{
    private NonAnemicModel _model;

    public NonAnemicModel Model 
    {
        get { return _model; }
        set
        {
            _model = value;
            _model.NameChanged += (sender, args) => NotifyPropertyChanged("UserName");
        }
    }

    public string UserName 
    {
        get { return this.Model.Name; }
        set { this.Model.Name = value; }
    }

    //this command would call out to the PerformNameCalculation method on the Model.
    public ICommand PerformNameCalculation { get; private set; }
}

请注意,当模型中的名称更改时,将引发PropertyChanged事件。这样,无论是使用了UserName setter还是PerformNameCalculation命令,ViewModel都保持同步。这样做的一个很大的缺点是您必须向您的模型添加更多的事件,但我发现在长期运行中拥有这些事件通常非常有帮助。只需小心事件的内存泄漏!

+1 是因为我认为这是创建 MVVM 场景模型对象的通用做法。虽然在很多 DDD 概念中,它会与之相冲突,但总体上,该模型能够直接绑定,但我们仍然创建 ViewModel 来对其进行抽象,以提供额外的视图特定行为、形状和相关数据。 - jpierson

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