Wpf MVVM:双向绑定到模型中的数组(不带INotifyPropertyChanged)

Wpf MVVM:双向绑定到模型中的数组(不带INotifyPropertyChanged),wpf,mvvm,data-binding,observablecollection,Wpf,Mvvm,Data Binding,Observablecollection,假设我的模型有一个属性,它是一个基元数组: class Model { public static int MaxValue { get { return 10; } } private int[] _array = {1,2,3,4}; public int[] MyArray { get { return _array; } set { for

假设我的模型有一个属性,它是一个基元数组:

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]);
       }
}
最后,我提供了一个ItemsControl,以便用户可以使用滑块设置每个值:

<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>
MyArray属性随后成为一个Wrap[],我们绑定到Path=Value。虽然以这种方式在模型中包装数组确实有点“凌乱”(注意:该模型也被其他WinForms应用程序使用),但它确实解决了问题,滑块现在可以更新数组

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

for( int i = 0; i < MyArray.Length; i++ )
    MyArray[i].Value = i;
但我不知道在更改MyArray值的情况下如何更新图像。我已经尝试订阅ObservaleCollection的CollectionChanged事件,但它只由按钮单击(对象替换)触发,而不是由滑块触发(因为滑块会更新集合中对象内的值)。我可以更改INotifyPropertyChanged中的包装对象并订阅ObservableCollection.PropertyChanged…但随后我会从模型中发送通知,这是(根据我所读的所有内容)在MVVM中不应该做的。因此,我要么无法在用户操纵滑块时刷新图像,要么必须基本上破坏MVVM——INotifyPropertyChanged、ObservableCollections和wrappers都在我的模型中。肯定有更干净的方法来解决这个问题…?

好问题

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


问题#2:我需要更多地查看您的代码,但一般来说,您只需要再次调用RaisePropertyChange(或其他,取决于您使用的框架),并为您的映像传递属性名称。

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

我的结局是这样的。TrulyObservableCollection来自(修改为report 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();
    }
}
类视图模型
{
模型_Model=新模型();
TrulyObservableCollection_myArrayLocal=null;
//将ItemsControl绑定到此:
公共TrulyObservableCollection MyArray
{
得到
{
//如果这是我们第一次获得此属性,请制作本地副本
//从模型
如果(_myArrayLocal==null)
{
_myArrayLocal=新的TrulyObservableCollection();
对于(var i=0;i<\u model.MyArray.Length;i++)
_添加(新的BindableUInt(_model.MyArray[i]);
_myArrayLocal.CollectionChanged+=myArrayLocal\u CollectionChanged;
}
返回_myArrayLocal;
}
设置
{
//如果我们正在设置,请将新(本地)阵列推回模型
_myArrayLocal=数值;
对于(变量i=0;i
感谢您周到的回答。我最终采取了一种不同的方法(见下文),但你的方法确实完美地解决了这个问题。认可的!
for( int i = 0; i < MyArray.Length; i++ )
    MyArray[i].Value = i;
   //MyArray is now ObservableCollection<Wrap>
   for( int i = 0; i < MyArray.Count; i++ )
        MyArray[i] = new Wrap() { Value = i };
class ViewModel
{
    public int SomeProperty
    {
      get { return _model.SomeProperty; }
      set { _model.SomeProperty = value; RefreshImage(); }
    }
}
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();
    }
}