MVVM:双向绑定到模型中的数组(w/o INotifyPropertyChanged)
MVVM: Two-Way Binding To Array In The Model (w/o INotifyPropertyChanged)
假设我的模型有一个 属性 这是一个基元数组:
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 包装这个 属性 这个供视图使用:
class ViewModel
{
Model _model = new Model();
public int[] MyArray
{
get { return _model.MyArray; }
set { _model.MyArray = value; }
}
}
最后,我展示了一个 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>
在这个简单的示例中,TwoWay 绑定将不起作用 - 滑块不会更新属性。这通过将每个数组元素包装在一个对象中来解决,例如:
class Wrap
{
public int Value{ get; set; }
}
MyArray 属性然后变成 Wrap[],我们绑定到 Path=Value。虽然以这种方式在模型中包装数组确实感觉有点"messy"(注意:该模型也被其他 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 中,模型 类 不应该与通知有任何关系 - 应该完全由虚拟机。通常这样的讨论围绕 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 和包装器。当然必须有更清洁的方法来解决这个问题...?
好问题。
问题 #1。我一直在 SO 上看到的一件事是人们声称模型不应该执行 INPC。我是一个绝对的 MVVM 纯粹主义者,以至于我参与了非常大的全栈应用程序并坚持(成功地)没有一行代码隐藏。这是一个连 Josh Smith 都认为过于极端的立场,但即使是像我这样的铁杆纯粹主义者也承认,将此功能添加到模型层是实际必要的。 INPC 不是特定于视图的概念;视图及其相应的视图模型在设计上是瞬态的。这意味着您有两种选择之一:要么几乎完全在视图模型层中复制模型层(当您的时间和预算紧张时容易出错且完全不切实际),或者为模型层提供一种机制对视图模型进行更改通知。如果您确实选择了第二种方法,那么您到底为什么要自己动手而不是使用 WPF 中已经内置的久经考验的机制呢?每一个物有所值的现代 ORM 都允许您 substitute in proxy objects with INPC, in cases of code-first design you can even add it at compile time automatically with a post-processing step.
问题 #2:我需要查看更多您的代码,但通常您只需要再次调用 RaisePropertyChange(或其他任何内容,具体取决于您使用的框架)并传入属性 为你的形象。
为了避免更改我的模型(以及依赖它的所有其他项目),我最终通过在我的 VM 中存储一个重复的集合来解决这个问题,并对模型进行 pushing/pulling 更改。它可能不是那么优雅,但它很好地完成了工作:我可以从滑块、按钮更新数组,并在每次更新后刷新图像——同时将模型的数组保留为常规的旧基元。
我最终得到了类似下面的结果。 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();
}
}
假设我的模型有一个 属性 这是一个基元数组:
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 包装这个 属性 这个供视图使用:
class ViewModel
{
Model _model = new Model();
public int[] MyArray
{
get { return _model.MyArray; }
set { _model.MyArray = value; }
}
}
最后,我展示了一个 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>
在这个简单的示例中,TwoWay 绑定将不起作用 - 滑块不会更新属性。这通过将每个数组元素包装在一个对象中来解决,例如:
class Wrap
{
public int Value{ get; set; }
}
MyArray 属性然后变成 Wrap[],我们绑定到 Path=Value。虽然以这种方式在模型中包装数组确实感觉有点"messy"(注意:该模型也被其他 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 中,模型 类 不应该与通知有任何关系 - 应该完全由虚拟机。通常这样的讨论围绕 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 和包装器。当然必须有更清洁的方法来解决这个问题...?
好问题。
问题 #1。我一直在 SO 上看到的一件事是人们声称模型不应该执行 INPC。我是一个绝对的 MVVM 纯粹主义者,以至于我参与了非常大的全栈应用程序并坚持(成功地)没有一行代码隐藏。这是一个连 Josh Smith 都认为过于极端的立场,但即使是像我这样的铁杆纯粹主义者也承认,将此功能添加到模型层是实际必要的。 INPC 不是特定于视图的概念;视图及其相应的视图模型在设计上是瞬态的。这意味着您有两种选择之一:要么几乎完全在视图模型层中复制模型层(当您的时间和预算紧张时容易出错且完全不切实际),或者为模型层提供一种机制对视图模型进行更改通知。如果您确实选择了第二种方法,那么您到底为什么要自己动手而不是使用 WPF 中已经内置的久经考验的机制呢?每一个物有所值的现代 ORM 都允许您 substitute in proxy objects with INPC, in cases of code-first design you can even add it at compile time automatically with a post-processing step.
问题 #2:我需要查看更多您的代码,但通常您只需要再次调用 RaisePropertyChange(或其他任何内容,具体取决于您使用的框架)并传入属性 为你的形象。
为了避免更改我的模型(以及依赖它的所有其他项目),我最终通过在我的 VM 中存储一个重复的集合来解决这个问题,并对模型进行 pushing/pulling 更改。它可能不是那么优雅,但它很好地完成了工作:我可以从滑块、按钮更新数组,并在每次更新后刷新图像——同时将模型的数组保留为常规的旧基元。
我最终得到了类似下面的结果。 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();
}
}