【问题标题】:MVVM Implementation for WinForm applicationWinForm 应用程序的 MVVM 实现
【发布时间】:2012-06-14 06:57:08
【问题描述】:

我正在尝试为我的 WinForms 应用程序实现 MVVM(模型视图视图模型)模式。我正在使用 C# 2005。

我的应用程序有一个带有 2 个多行文本框和 3 个按钮的 MainForm(视图)。第一个文本框的目的是在单击按钮时显示应用程序正在执行的操作的运行评论。我继续向 TextBox 添加行以更新用户正在发生的事情。第二个文本框的目的是向用户更新任何错误情况、冲突、重复值;简而言之,用户需要查看的任何内容。它将每条消息分类为 INFO 或 WARNING 或 ERROR。 3 个按钮中的每一个都执行一个操作,并不断更新 2 个文本框。

我创建了一个 MainFormViewModel 类。

第一个问题: 当用户单击 MainForm 中的按钮时,我必须清除 2 个文本框的内容,并禁用该按钮,以便在第一次操作完成之前无法再次单击它。我应该直接在 MainForm 中更新这个文本框和按钮,还是应该以某种方式使用 MainFormViewModel?

第二个问题: 单击按钮调用 MainFormViewModel 类上的方法。在调用方法之前和调用方法之后,我想在第一个文本框中显示一条消息,例如“操作 A 开始/结束”。我通过调用具有 Log 方法的 Common 类来将消息记录到 TextBox 或文件或两者兼而有之。再次是否可以直接从 MainForm 执行此操作?我在事件处理程序的开始和结束处调用此日志记录方法。

第三个问题: 如何将错误消息从 ViewModel 传播回 View?我创建了一个自定义异常类“TbtException”。那么我是否必须在每个按钮中编写 2 个 catch 块,一个用于 TbtException,另一个用于遗传异常类?

谢谢。

【问题讨论】:

    标签: c# design-patterns


    【解决方案1】:

    您应该在视图中仅针对 ViewModel 对象的状态执行操作。例如。当您单击按钮时,您不应该假设视图模型正在计算,但您应该向视图模型添加一个状态,表明它正在做更长的事情,然后在视图中识别该状态。您不应随意禁用或启用视图中的按钮,但前提是存在需要更改这些按钮的状态。这可以有一个属性来指示当前选择了列表中的哪个项目,因此 UI 不会调用列表控件的 SelectedItem 成员,而是调用视图模型的成员。并且当用户点击remove时,view model会将选中的成员从list中移除,并且view会以事件的形式通过状态变化自动更新。

    这就是我所说的用于您的视图的视图模型。它通过视图可以绑定的可观察集合公开消息(即注册事件处理程序,因为在 WinForms 中不支持绑定)。文本框在任何时候都只呈现集合的内容。它具有清除视图可以调用的那些集合的操作。视图也可以调用底层模型的动作,但它只能通过视图模型更新!视图不应该为底层模型公开的事件注册任何事件处理程序。如果您想这样做,您应该在视图模型中连接该事件并将其公开给那里的视图。有时这可能感觉像是“只是另一个级别的间接”,这就是为什么它对于非常简单的应用程序(例如您的应用程序)来说可能是多余的。

    public class MainFormViewModel : INotifyPropertyChanged {
      private object syncObject = new object();
    
      private MainFormModel model;
      public virtual MainFormModel Model {
        get { return model; }
        set {
          bool changed = (model != value);
          if (changed && model != null) DeregisterModelEvents();
          model = value;
          if (changed) {
            OnPropertyChanged("Model");
            if (model != null) RegisterModelEvents();
          }
        }
      }
    
      private bool isCalculating;
      public bool IsCalculating {
        get { return isCalculating; }
        protected set {
          bool changed = (isCalculating != value);
          isCalculating = value;
          if (changed) OnPropertyChanged("IsCalculating");
        }
      }
    
      public ObservableCollection<string> Messages { get; private set; }
      public ObservableCollection<Exception> Exceptions { get; private set; }
    
      protected MainFormViewModel() {
        this.Messages = new ObservableCollection<string>();
        this.Exceptions = new ObservableCollection<string>();
      }
    
      public MainFormViewModel(MainFormModel model)
        : this() {
        Model = model;
      }
    
      protected virtual void RegisterModelEvents() {
        Model.NewMessage += new EventHandler<SomeEventArg>(Model_NewMessage);
        Model.ExceptionThrown += new EventHandler<OtherEventArg>(Model_ExceptionThrown);
      }
    
      protected virtual void DeregisterModelEvents() {
        Model.NewMessage -= new EventHandler<SomeEventArg>(Model_NewMessage);
        Model.ExceptionThrown -= new EventHandler<OtherEventArg>(Model_ExceptionThrown);
      }
    
      protected virtual void Model_NewMessage(object sender, SomeEventArg e) {
        Messages.Add(e.Message);
      }
    
      protected virtual void Model_ExceptionThrown(object sender, OtherEventArg e) {
        Exceptions.Add(e.Exception);
      }
    
      public virtual void ClearMessages() {
        lock (syncObject) {
          IsCalculating = true;
          try {
            Messages.Clear();
          } finally { IsCalculating = false; }
        }
      }
    
      public virtual void ClearExceptions() {
        lock (syncObject) {
          IsCalculating = true;
          try {
            Exceptions.Clear();
          } finally { IsCalculating = false; }
        }
      }
    
      public event PropertyChangedEventHandler PropertyChanged;
      protected virtual void OnPropetyChanged(string property) {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(property));
      }
    }
    

    编辑:关于异常处理

    我宁愿在 ViewModel 中捕获异常,也不愿在视图中捕获异常。视图模型更适合为显示做好准备。我不知道它在 WPF 中是如何工作的。我还没有在 WPF 中编写应用程序,我们还在做很多 WinForms。

    意见可能会有所不同,但我认为通用的 try/catch 子句并不是真正的异常处理。我认为您应该非常好地测试您的 UI,并且仅在必要时包含异常处理。这就是为什么您对视图模型进行单元测试并让用户测试视图的原因。但是如果你真的坚持原则,避免视图中的逻辑,你可以通过单元测试做很多事情。

    【讨论】:

    • 所以关于第一个问题,您是说视图应该调用 ViewModel 来更新状态属性,并且 2 个文本框应该挂钩到该属性并清除自己?关于第二个问题,我仍然不清楚文本框应该如何保持运行评论。似乎 ViewModel 应该继续在变量中编写运行评论,而 TextBox 应该挂钩到该变量并不断更新自己?对不起,但我是第一次实现这个,所以会感谢更多的帮助。请注意,正在运行的评论文本会很长。任何表单示例?
    • 关于错误处理,表单不会有try catch块吗?
    • 我想你已经很明白了。如果 ViewModel 应该抛出异常,表单可以有 try/catch 块。但无论如何,ViewModel 应该始终代表表单的当前状态。如果您仅在表单中存储评论或状态,您应该如何针对 ViewModel 编写单元测试?拥有 ViewModel 的优点是您可以对 UI 逻辑进行单元测试,而无需实际的 UI。因此,除了更新控件以显示 VM 的内容和调用 VM 的方法之外,您不希望 UI 中有任何逻辑。
    • 谢谢。我的 ViewModel 不应该抛出任何异常,但我在某处读到任何事件处理程序(如按钮单击处理程序)都应该为任何意外异常做好准备,因此应该有一个 try catch 块。是否有任何替代方法可以在所有事件处理程序方法(或它委托的 VM 方法)中编写重复的异常处理程序块。其次,根据 MVVM,视图是否不应该为 Method Entered 和 Exit 调用 Logger 方法?
    • 我更新了关于异常的答案,因为我错过了你的那个问题。记录是你的事,我对此没有任何建议。从我的角度来看,我想我回答了你的问题。您可以等待更多答案,设置赏金以获得更多出勤率,接受此答案,或者如果您认为它不是有效或好的答案,则将其保留。如果您还有其他问题,请打开一个新问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-22
    • 1970-01-01
    • 2018-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多