【问题标题】:MVVM: Two-Way Binding To Array In The Model (w/o INotifyPropertyChanged)MVVM:双向绑定到模型中的数组(没有 INotifyPropertyChanged)
【发布时间】:2016-01-30 12:36:38
【问题描述】:

假设我的模型有一个原始数组的属性:

class Model
{
    public static int MaxValue { get { return 10; } }

    private int[] _array = {1,2,3,4};
    public int[] MyArray
    {
       get
       { 
          return _array; 
       }
       set
       {
           for (int i = 0; i < Math.Min(value.Length, _array.Length); i++)
               _array[i] = Math.Min(MaxValue, value[i]);
       }
}

ViewModel 然后包装这个属性 this 供 View 使用:

class ViewModel
{
    Model _model = new Model();
    public int[] MyArray
    {
        get { return _model.MyArray; }
        set { _model.MyArray = value; }
    }
}

最后,我展示了一个 ItemsControl,以便用户可以使用 Slider 设置每个值:

<ItemsControl ItemsSource="{Binding MyArray}" >
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <Slider Value="{Binding Path=., Mode=TwoWay}" Minimum="0" Maximum="{Binding Source={x:Static Model.MaxValue}, Mode=OneWay}" />
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

在这个简单的示例中,TwoWay 绑定不起作用 - 滑块不会更新属性。这可以通过将每个数组元素包装在一个对象中来解决,例如:

class Wrap
{
    public int Value{ get; set; }
}

然后 MyArray 属性变成 Wrap[],我们绑定到 Path=Value。虽然以这种方式在模型中包装数组确实感觉有点“混乱”(注意:其他 WinForms 应用程序也使用该模型),但它确实解决了问题,并且滑块现在可以更新数组。

接下来,让我们添加一个按钮来重置数组值。点击时:

for( int i = 0; i < MyArray.Length; i++ )
    MyArray[i].Value = i;

出现了另一个问题:单击按钮后,滑块不反映新值。这可以通过将数组更改为 ObservableCollection 并使用对象替换更新值来解决:

   //MyArray is now ObservableCollection<Wrap>
   for( int i = 0; i < MyArray.Count; i++ )
        MyArray[i] = new Wrap() { Value = i };

第一个问题:我的理解是,在 MVVM 中,Model 类不应该与通知有任何关系——这应该完全由 VM 处理。通常这样的讨论围绕 INotifyPropertyChanged,但在我看来,将数组更改为 ObservableCollection 似乎真的开始弄乱模型,以便......好吧......通知。这不会被认为是不好的做法吗?所有其他使用该模型的(WinForms)应用程序现在都必须更新以理解这个古怪的对象包装基元集合,只是为了使其与数据绑定一起工作。是否有其他方法可以解决这个问题?

第二个问题:我真正的问题来自另一个要求。在 ViewModel 中,数组值(和其他属性)实际上是用来生成图像的——每次更新属性时,都需要刷新图像。对于我的其他(非数组)属性,我这样做:

class ViewModel
{
    public int SomeProperty
    {
      get { return _model.SomeProperty; }
      set { _model.SomeProperty = value; RefreshImage(); }
    }
}

但我不知道在更改 MyArray 值的情况下如何更新图像。我尝试订阅 ObservableCollection 的 CollectionChanged 事件,但它仅由按钮单击(对象替换)触发,而不是由滑块触发(因为它们更新集合中对象内的值)。我可以制作 Wrap 对象 INotifyPropertyChanged 并订阅 ObservableCollection.PropertyChanged ......但是我会从模型中发送通知,这(根据我读过的所有内容)你不应该在 MVVM 中做。因此,当用户操作滑块时,我要么无法刷新图像,要么不得不基本上破坏 MVVM——在我的模型中使用 INotifyPropertyChanged、ObservableCollections 和包装器。当然必须有一种更清洁的方法来解决这个问题......?

【问题讨论】:

    标签: wpf mvvm data-binding observablecollection


    【解决方案1】:

    为了避免更改我的模型(以及依赖它的所有其他项目),我最终通过在我的 VM 中存储一个重复的集合,并将更改推/拉到模型中来解决这个问题。它可能不是那么优雅,但它很好地完成了工作:我可以从滑块、按钮更新数组,并在每次更新后刷新图像——同时将模型的数组保留为常规的旧原语。

    我最终得到了以下内容。 TrulyObservableCollection 来自here(修改为报告 NotifyCollectionChangedAction.Replace 而不是 Reset),而 BindableUint 只是实现 INotifyPropertyChanged 的​​包装器。

    class ViewModel
    {
        Model _model = new Model();
        TrulyObservableCollection<BindableUInt> _myArrayLocal = null;
    
        //Bind the ItemsControl to this:
        public TrulyObservableCollection<BindableUInt> MyArray
        {
            get
            {
                //If this is the first time we're GETTING this property, make a local copy
                //from the Model
                if (_myArrayLocal == null)
                {
                    _myArrayLocal = new TrulyObservableCollection<BindableUInt>();
                    for (var i = 0; i < _model.MyArray.Length; i++)
                        _myArrayLocal.Add(new BindableUInt(_model.MyArray[i]));
    
                    _myArrayLocal.CollectionChanged += myArrayLocal_CollectionChanged;
                }
    
                return _myArrayLocal;
            }
            set 
            {
                //If we're SETTING, push the new (local) array back to the model
                _myArrayLocal = value;
                for (var i = 0; i < value.Count; i++) 
                    _model.MyArray[i] = value[i].Value;
    
                RefreshImage();
            }
        }
    
        /// <summary>
        /// Called when one of the items in the local collection changes;
        /// push the changed item back to the model, & update the display.
        /// </summary>
        private void myArrayLocal_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            //Sanity checks here
            int changedIndex = e.NewStartingIndex;
            if (_model.MyArray[changedIndex] != _myArrayLocal[changedIndex].Value)
            {
                _model.MyArray[changedIndex] = _myArrayLocal[changedIndex].Value;
                RefreshImage();
            }
        }
    
        /// <summary>
        /// When using the button to modify the array, make sure we pull the
        /// updated copy back to the VM
        /// </summary>
        private void ButtonClick()
        {
            _model.ResetMyArray();
            for (var i = 0; i < _model.MyArray.Length; i++)
                _myArrayLocal[i].Value = _model.MyArray[i];
            RefreshImage();
        }
    }
    

    【讨论】:

      【解决方案2】:

      好问题。

      问题 #1。我在 SO 上一直看到的一件事是人们声称模型不应该做 INPC。我是一个绝对的 MVVM 纯粹主义者,以至于我参与了非常大的全栈应用程序并且(成功地)坚持没有一行代码隐藏。这是一个甚至 Josh Smith 都认为过于极端的立场,但即使是像我这样的铁杆纯粹主义者也承认,将这个功能添加到模型层是一种实际的必要性。 INPC 不是特定于视图的概念;视图及其相应的视图模型在本质上是设计瞬态的。这意味着您有两种选择之一:或者您几乎完全在视图模型层中复制模型层(当您的日程安排和预算紧张时容易出错并且完全不切实际),或者您为模型层提供一种机制对视图模型进行更改通知。如果您确实选择了第二种方法,那么您究竟为什么要推出自己的方法,而不是使用 WPF 中已经内置的久经考验的机制呢?每个现代的 ORM 都可以使用 substitute in proxy objects with INPC,在代码优先设计的情况下,您甚至可以在编译时使用 a post-processing step 自动添加它。

      问题 #2:我需要查看更多您的代码,但通常您只需要再次调用 RaisePropertyChange(或其他任何内容,具体取决于您使用的框架)你的形象。

      【讨论】:

      • 感谢您的周到回答。我最终采取了一种不同的方法(见下文),但你的方法绝对完美地解决了这个问题。接受!
      猜你喜欢
      • 2011-04-12
      • 1970-01-01
      • 1970-01-01
      • 2018-07-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多