【问题标题】:MVVM - From Model to ViewModel leaving Model untouched [closed]MVVM - 从模型到 ViewModel 保持模型不变[关闭]
【发布时间】:2020-10-29 16:19:14
【问题描述】:

假设我有一个代表我的模型的第三方后端 [C# .NET Standard] 库;该库纯粹用作数据处理后端工具。 显然没有 GUI,只提供了一些公共只读寄存器,以允许其他父/拥有代码观察这些寄存器的状态。举个简单的例子:

public class MyModel
{
    public int MyVariable { get; private set; }
    public List<int> MyCollection { get; private set; }
    ...
}

现在我想提供一个 WPF 应用程序作为视图面板/仪表板,以便用户可以在监视器上查看实时情况;我选择了 MVVM 方法。

第一个问题是:假设 Model 应该保持不变,而不使其适应 ViewModel 是否是正确的设计方法(忘记 Model 是我实际上不拥有的代码的第三方库)?如何有效且正确地编写 ViewModel 以便它可以从 Model 中提取/检索这些寄存器? 我经历了一个基于时间的更新选项(每秒从 Model 重新读取这些寄存器),老实说,这似乎有点低效;我不知道如何从 ViewModel 端拦截模型变量更改或模型集合更改。

然后,让我们暂时放弃我没有库代码的约束,并考虑我可以修改模型(再次,它是一个正确的设计吗?)。我会让MyModel 实现INotifyPropertyChanged 并将List&lt;int&gt; MyCollection 更改为ObservableCollection&lt;int&gt; MyCollection,以便我可以拦截数据更改并绑定以在屏幕上更新。但是当CollectionChanged 事件被触发时,我陷入了Dispatcher 问题。 由于 Model 无法访问 Dispatcher (.NET Standard),我如何执行集合更新?如何抑制 Model 触发的 CollectionChanged 并从 Dispatcher 中的 ViewModel 触发它? 我已经通过Dispatcher 转发解决方案,但我再次认为这是错误的,没有正确编码:

public delegate void DispatcherService(Action action);
public class MyModel
{
    public int MyVariable { get; private set; }
    public List<int> MyList { get; private set; }
    public event DispatcherService MyDispatcherService;
    public void AddElementToMyList(int element)
    {
        MyDispatcherService?.Invoke(() => MyList.Add(element));
    }
}

public class MyViewModel
{
    private MyModel _myModel;
    public int MyModelMyVariable { get { return _myModel.MyVariable; } }
    public List<int> MyModelMyCollection { get { return _myModel.MyCollection; } }
    public MyViewModel(MyModel myModel)
    {
        _myModel = myModel;
        _myModel.MyDispatcherService += ((action) => { Application.Current.Dispatcher.Invoke(action); });
    }
}

【问题讨论】:

  • 有什么办法可以说明寄存器列表中的某些内容发生了变化吗?正如所描述的,tgese 听起来像是你只能轮询的东西。这些整数从秒到秒有多少变化?巴秒处理多少?您的视图是否会变得模糊,或者某个项目会在一秒钟内发生变化,还是什么?

标签: c# wpf mvvm binding dispatcher


【解决方案1】:

通常您的模型类不需要实现INotifyPropertyChagedINotifyCollectionChanged。这些接口旨在由视图模型类实现,以便为视图提供非常通用的通知。模型应该公开专门的事件以通知视图模型有关数据更改。

由于库是第 3 方,您只能按照 API 允许的方式使用它。当库公开动态数据时,它很可能会公开您的视图模型可以订阅的相关事件。
否则,您的视图模型将不得不轮询库以获取数据更改(在这种情况下,您可能会寻找更重要的替代库)。

在 MVVM 中,模型不应该处理任何DispatcherDispatcher 是一个与 UI 相关的概念。它处理 UI 对象的 UI 线程关联 (DispatcherObject)。它还负责管理 UI 线程的作业队列。
模型组件既没有与 UI 相关的对象,也不依赖于 UI 或 UI 线程。

在您描述的场景中,正确的方法是从您的视图模型订阅模型事件。然后填充一个ObservableCollection,视图模型将其公开给视图以进行数据绑定。

根据 MVVM,模型永远不会访问视图模型。因此,只有视图模型必须对CollectionChanged 事件或对与 UI 线程相关联的对象的任何其他访问进行编组(例如,如果访问发生在与 UI 线程不同的线程中) )。

您更正后的示例可能如下所示:

public class MyModel
{
  public int Property { get; private set; }
  public List<int> DataCollection { get; private set; }
  public event EventHandler DataIsReady;

  public void AddElementToDataCollectionOnBackgroundThread(int element)
  {
    Task.Run(()  
    {
      this.DataCollection.Add(element);         
      DataIsReady?.Invoke(this, EventArgs.Empty);
    }
  }
}

public class MyViewModel : INotifyPropertyChanegd
{
  private MyModel Model { get; };
 
  private int viewModelProperty;
  public int ViewModelProperty
  {
    get => return this.viewModelProperty;
    set 
    {
      this.viewModelProperty = value;
      OnPropertyChanged();
    }
  }

  public ObservaleCollection<int> ViewItemsSource { get }

  public MyViewModel(MyModel model)
  {
    this.Model = model;
    this.Model.DataIsReady += OnModelDataIsReady;
  } 

  private void OnModelDataIsReady(object sender, EventArgs e)
  {
    // Since the event was raised on a background thread,
    // the view model is responsible to marshal the collection change to the UI thread
    Application.Current.Dispatcher.InvokeAsync(
      () =>
      {
        this.ViewItemsSource.Clear();
        this.Model.DataCollection.ForEach(this.ViewItemsSource.Add));
      });

    // PropertyChanged is automatically marshalled 
    // to the UI thread by the framework --> no dispatcher needed
    this.ViewModelProperty = this.Model.Property;
  }
}

【讨论】:

  • 您的代码对我来说似乎非常清晰流畅,我只想澄清一点:每次DataIsReady 事件都对ViewItemsSource 执行clear&refill 是否可以?被炒鱿鱼?这是否会出现性能问题(在很长的列表或更新率很高的列表的情况下)?
  • 是的,clear&refill 会降低性能,尤其是当控件绑定到此集合时。控件必须再次呈现以显示更改。出于这个原因,您总是努力使用 UI 虚拟化,可能与数据虚拟化结合使用。这样您就可以控制集合更改将对 UI 产生的影响。如果修改后的集合不是视图的绑定源,则可以认为在大多数情况下对性能的影响可以忽略不计。
  • 在这种情况下(集合不作为视图的数据源),您可以在后台线程上刷新集合(这可能会引入并发问题)。对于大型集合,您可以引入更细粒度的事件逻辑。但如前所述,大型收藏大多不是必需的,因为用户只能查看非常有限数量的项目。应使用数据虚拟化来减少集合大小。这只是一个例子,主要是展示调度员在为INotifyCollectionChanged工作。
  • 是的,UI 虚拟化听起来是个不错的选择
  • 最后澄清一点:ViewModel 属性(那些必需/绑定到 View 的属性)引用 ViewModel “镜像变量/字段”而不是引用在 Model 中定义的那些(根据您的示例,ViewModelProperty 引用 viewModelProperty 而不是 Model.PropertyViewItemsSource 引用自身Model.DataCollection)?一方面这在我看来是代码重复,另一方面在我看来这是很好的解耦
【解决方案2】:

您的模型完全可以封装一些库对象。在一个完美的世界中,这些库对象将功能如此全面,以至于它们将支持诸如INotifyPropertyChanged 之类的观察模式。然后它们可以在各种环境中使用,例如直接作为 MVVM 的模型。在您的情况下,由于库对象不支持可观察性,因此创建提供该可观察性的包装类可能是有意义的。否则,您最终会在视图模型中对此进行补偿(这也是完全可以接受的,尽管我希望在定义架构边界时我的范围非常干净)。

至于调度程序,我的直觉是模型不应该表现出任何线程关联性——如果某个线程上的模型更新了,通知就会在同一个线程上发出。由于视图本质上是线程绑定的,因此编组通知的责任要么在视图模型中,要么直接在视图中。当然,您可以随时编组更新模型的调用,以便通知仅发生在 UI 线程上。

【讨论】:

  • 关于第一项,您建议采用什么方法来提供可观察性:基于时间的更新还是其他方式?关于第二项,当您声明“如果某个线程上的模型更新,通知会在同一个线程上发出”时我很好,但是我找不到通过Dispatcher 正确处理这种情况的方法,你能提供一个快速简单的例子?
  • @GiacomoPirinoli 取决于。这些对象是在您的控制之下,还是在您的控制之外被修改?如果在外面,唯一的选择是轮询(基于时间的更新)。如果它在您的控制范围内,我绝不建议投票。您可以改为创建包装类,或者——在最坏的情况下——直接在视图模型中的对象之间进行方法调用或触发事件。
  • @GiacomoPirinoli 如果我没记错的话,WPF 会自动将 IPropertyChanged/ICollectionChanged 通知发送到 UI 线程。你看到了什么不同的东西吗?如果您正在编写的代码正在接收通知(而不是由 WPF 绑定接收),那么您必须在接收站点进行编组。
  • 好吧,如果我只是在我的模型库中执行更新(例如:public void Add(T item))而不进行任何特定处理,CollectionChanged 事件会被触发,然后我会得到 Exception 'This type of CollectionView不支持从不同于 Dispatcher 线程的线程更改其 SourceCollection。'
  • 模型是您的数据浮出水面。我会说模型对象实现inotifypropertychanged 远非完美。它们不应该直接呈现给视图。这是视图模型的目的。图案中有三层而不是两层。
猜你喜欢
  • 1970-01-01
  • 2011-08-14
  • 1970-01-01
  • 2016-08-23
  • 1970-01-01
  • 1970-01-01
  • 2015-08-24
  • 2020-07-20
  • 2013-12-09
相关资源
最近更新 更多