C# Wpf mvvm 保持多个 ViewModels 与模型同步
C# Wpf mvvm keep multiple ViewModels with model sychronized
我有数据架构问题。我的目标应该是进行双向数据通信
在 ViewModel 和模型之间 类。我有一个 window 具有不同的用户控件。每个用户控件
有自己的数据,但它们之间共享一些属性。
对于每个 ViewModel,我实现了两个函数来同步模型和视图模型。
该模型应该保持更新,所以我在 属性Changed 事件中实现了方法调用 SyncModel。
到目前为止这还不太好,因为当我调用构造函数时,方法调用链是:
构造函数 -> SyncViewModel -> 属性 setter -> 属性已更改 -> SyncModel
这里有一些示例代码可以更好地理解我的问题:
public class SampleModel
{
public string Material { get; set; }
public double Weight { get; set; }
public double Length { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public object SharedProperty { get; set; }
}
public class SampleViewModelA : AbstractViewModel
{
public string Material
{
get
{
return _Material;
}
set
{
if (value != _Material)
{
_Material = value;
OnPropertyChanged(nameof(Material));
}
}
}
public double Weight
{
get
{
return _Weight;
}
set
{
if (value != _Weight)
{
_Weight = value;
OnPropertyChanged(nameof(Weight));
}
}
}
public object SharedProperty
{
get
{
return _SharedProperty;
}
set
{
if (value != _SharedProperty)
{
_SharedProperty = value;
OnPropertyChanged(nameof(SharedProperty));
}
}
}
public SampleViewModelA(SampleModel Instance) : base(Instance) { }
public override void SyncModel()
{
//If I wouldn't check here, it would loop:
//constructor -> SyncViewModel -> Property setter -> PropertyChanged -> SyncModel
if (Instance.Material == Material &&
Instance.Weight == Weight &&
Instance.SharedProperty == SharedProperty)
return;
Instance.Material = Material;
Instance.Weight = Weight;
Instance.SharedProperty = SharedProperty;
}
public override void SyncViewModel()
{
Material = Instance.Material;
Weight = Instance.Weight;
SharedProperty = Instance.SharedProperty;
}
private string _Material;
private double _Weight;
private object _SharedProperty;
}
public class SampleViewModelB : AbstractViewModel
{
//Same like SampleViewModelA with Properties Length, Width, Height AND SharedProperty
}
public abstract class AbstractViewModel : INotifyPropertyChanged
{
//All ViewModels hold the same Instance of the Model
public SampleModel Instance { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public AbstractViewModel(SampleModel Instance)
{
this.Instance = Instance;
SyncViewModel();
}
protected virtual void OnPropertyChanged(string PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
SyncModel();
}
public abstract void SyncModel();
public abstract void SyncViewModel();
}
真正的问题是,SharedProperty
需要在 SampleViewModelA
和 SampleViewModelB
之间更新。首先,我认为观察者模式可以帮助我,但 SharedProperties 是多种多样的,以使其与通用接口一起工作。然后我认为带有更改事件的数据控制器可以像这样帮助我
public class SampleDataController
{
public SampleModel Instance { get; set; }
public delegate void SynchronizeDelegate();
public event SynchronizeDelegate SynchronizeEvent;
public void SetSharedProperty(object NewValue)
{
if (Instance.SharedProperty != NewValue)
{
Instance.SharedProperty = NewValue;
SynchronizeEvent?.Invoke();
}
}
}
如果它这样做,我的 AbstractViewModel
将只与控制器而不是实例通信。 SyncModel
函数会调用 SetSharedProperty
之类的方法,而不是直接访问。
MainViewModel 代码可能如下所示。
public class SampleMainViewModel
{
public SampleViewModelA ViewModelA { get; set; }
public SampleViewModelB ViewModelB { get; set; }
public SampleDataController Controller { get; set; }
public SampleMainViewModel()
{
ViewModelA = new SampleViewModelA(Controller);
ViewModelB = new SampleViewModelB(Controller);
Controller.SynchronizeEvent += ViewModelA.SyncViewModel;
Controller.SynchronizeEvent += ViewModelB.SyncViewModel;
}
}
这会导致问题,SynchronizeEvent
调用的来源也是
订阅事件本身。这不会导致无限循环,因为我检查是否
值等于新状态,但对我来说似乎很难看。一定有更好的
远不止于此。
在我的项目中,我有 8 个 ViewModel 和多个模型 类,我需要在其中同步数据
不同的共享属性。
感谢您的帮助,并希望到目前为止问题是可以理解的。
您已经使用了由另一个视图模型 classes SampleViewModelA
和 SampleViewModelB
.
组成的 SampleMainViewModel
现在您所要做的就是在视图 models/views 之间移动 all shared 的属性(例如 SharedProperty
,还有 Material
和 Weight
) 组合 SampleMainViewModel
或共享 class。这样您的所有控件都可以绑定到相同的数据源。
此外 Model 之间的通信 --> View Model 应该只通过事件发生:Model 可以通过以下方式通知 View Model公开例如 DataChanged
事件。 Model 和 View Model 之间没有真正的双向 communication/dependency。这就是 MVVM 的主要特征:参与组件的单向依赖 - 通过实现事件、命令,尤其是利用数据绑定来实现。
以下示例显示了如何将控件绑定到共享属性和非共享属性(那些是专用视图模型 classes 的属性)。
MainWindow.xaml
<Window>
<Window.DataContext>
<SampleMainViewModel />
</Window.DataContext>
<StackPanel>
<UserControlA Material="{Binding Material}"
SharedProperty="{Binding SharedProperty}"
UnsharedPropertyA="{Binding ViewModelA.UnsharedPropertyA}" />
<UserControlB Material="{Binding Material}"
SharedProperty="{Binding SharedProperty}"
UnsharedPropertyB="{Binding ViewModelB.UnsharedPropertyB}" />
</StackPanel>
</Window>
SampleMainViewModel.cs
public class SampleMainViewModel : INotifyPropertyChanged
{
public SampleViewModelA ViewModelA { get; }
public SampleViewModelB ViewModelB { get; }
/* Properties must raise INotifyPropertyChanged.PropertyChanged */
public string Material { get; set; }
public double Weight { get; set; }
public object SharedProperty { get; set; }
// Example initialization
public SampleMainViewModel(SomeModelClass someModelClass)
{
this.ViewModelA = new SampleViewModelA();
this.ViewModelB = new SampleViewModelB();
this.Material = someModelClass.Material;
this.Weight = someModelClass.Weight;
this.SharedProperty = someModelClass.SharedProperty;
someModelClass.DataChanged += UpdateData_OnDataChanged;
}
private void UpdateData_OnDataChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Material = someModelClass.Material;
this.Weight = someModelClass.Weight;
this.SharedProperty = someModelClass.SharedProperty;
}
}
SampleViewModelA.cs
public class SampleViewModelA : INotifyPropertyChanged
{
public object UnsharedPropertyA { get; set; }
}
SampleViewModelB.cs
public class SampleViewModelB : INotifyPropertyChanged
{
public object UnsharedPropertyB { get; set; }
}
"Your suggestion cause the disadvantage, that my separated ViewModels
are no longer encapsulated. And if I would move all my code with
shared properties to the MainViewModel, the class would end up very
large"
针对您的评论:如果您坚持每个控件都有一个视图模型,包括复制属性等,那么您必须采取不同的方法。
此外,将 shared/duplicate 代码移出视图模型不会破坏封装 - 假设那些 classes 不包含仅重复的代码。
但请注意,不建议为每个控件创建一个视图模型。您有一个 view/page - 多个控件的集合 - 它具有定义的数据上下文。此视图中的所有控件共享相同的数据上下文 - 通常,因为视图与结构化上下文相关。
这就是默认情况下继承 FrameworkElement.DataContext
的原因。
每个控件都有一个视图模型会使事情变得过于复杂,并导致大量重复代码 - 而不仅仅是像您的示例中那样的重复属性。你会发现自己也在重复逻辑。谈到可测试性,如果你复制逻辑,你也会复制单元测试。
这是因为您正在处理相同的数据和相同的模型 classes.
您通常将重复代码提取到单独的 class 中,这是由依赖于此重复代码的类型引用的。考虑到这种“无重复代码”策略重构您的视图模型 classes 最终会将“共享”属性移动到单独的 class。由于我们讨论的是同一视图的数据上下文,因此这个单独的 class 将是分配给页面 DataContext
的视图模型 class。我想说你的方法恰恰相反:你复制代码(并将其称为封装)。如果 class 最终变得非常大,因为它包含很多属性,那么您可以检查您的 UI 设计 - 也许您应该将您的大页面拆分为更多内容更简洁的页面。这也可能会改善用户体验。
通常,拥有一个具有更多属性的视图模型并没有错。如果您的视图模型 class 也包含很多逻辑,您可以提取此逻辑以分离 classes.
你仍然可以使用上一个例子的模式,即监听模型的数据变化事件。
您要么实现一个非常通用的事件,如上面的 DataChanged
事件,要么实现几个更专业的事件,如 MaterialChanged
事件。还要确保将相同的模型实例注入每个视图模型。
下面的示例展示了如何让多个不同的视图模型 classes 公开相同的数据,其中所有这些视图模型 classes 通过观察它们的模型 classes 来更新自己:
MainWindow.xaml
<Window>
<Window.DataContext>
<SampleMainViewModel />
</Window.DataContext>
<StackPanel>
<UserControlA DataContext="{Binding ViewModelA}"
Material="{Binding Material}"
Weight="{Binding Weight}"
SharedProperty="{Binding SharedPropertyA}" />
<UserControlB DataContext="{Binding ViewModelB}"
Material="{Binding Material}"
Weight="{Binding Weight}"
SharedProperty="{Binding SharedPropertyB}" />
</StackPanel>
</Window>
SampleMainViewModel.cs
public class SampleMainViewModel : INotifyPropertyChanged
{
public SampleViewModelA ViewModelA { get; }
public SampleViewModelB ViewModelB { get; }
// Example initialization
public SampleMainViewModel()
{
var sharedModelClass = new SomeModelClass();
this.ViewModelA = new SampleViewModelA(sharedModelClass);
this.ViewModelB = new SampleViewModelB(sharedModelClass);
}
}
SampleViewModelA.cs
public class SampleViewModelA : INotifyPropertyChanged
{
/* Shared properties */
public string Material { get; set; }
public double Weight { get; set; }
public object SharedProperty { get; set; }
private SomeModelClass SomeModelClass { get; }
// Example initialization
public SampleViewModelA(SomeModelClass sharedModelClass)
{
this.SomeModelClass = sharedModelClass;
this.Material = this.SomeModelClass.Material;
this.Weight = this.SomeModelClass.Weight;
this.SharedProperty = this.SomeModelClass.SharedProperty;
// Listen to model changes
this.SomeModelClass.DataChanged += UpdateData_OnDataChanged;
this.SomeModelClass.MaterialChanged += OnModelMaterialChanged;
}
// Example command handler to send dat back to the model.
// This will trigger the model to raise corresponding data chnaged events
// to notify listening view model classes that new data is available.
private void ExecuteSaveDataCommand()
=> this.SomeModelClass.SaveData(this.Material, this.Weight);
private void OnModelMaterialChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Material = someModelClass.Material;
}
private void UpdateData_OnDataChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Weight = someModelClass.Weight;
this.SharedProperty = someModelClass.SharedProperty;
}
}
SampleViewModelB.cs
public class SampleViewModelB : INotifyPropertyChanged
{
/* Shared properties */
public string Material { get; set; }
public double Weight { get; set; }
public object SharedProperty { get; set; }
private SomeModelClass SomeModelClass { get; }
// Example initialization
public SampleViewModelB(SomeModelClass sharedModelClass)
{
this.SomeModelClass = sharedModelClass;
this.Material = this.SomeModelClass.Material;
this.Weight = this.SomeModelClass.Weight;
this.SharedProperty = this.SomeModelClass.SharedProperty;
// Listen to model changes
this.SomeModelClass.DataChanged += UpdateData_OnDataChanged;
this.SomeModelClass.MaterialChanged += OnModelMaterialChanged;
}
// Example command handler to send dat back to the model.
// This will trigger the model to raise corresponding data chnaged events
// to notify listening view model classes that new data is available.
private void ExecuteSaveDataCommand()
=> this.SomeModelClass.SaveData(this.Material, this.Weight);
private void OnModelMaterialChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Material = someModelClass.Material;
}
private void UpdateData_OnDataChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Weight = someModelClass.Weight;
this.SharedProperty = someModelClass.SharedProperty;
}
}
第一个解决方案(旨在消除重复代码)的一个变体是重构您的绑定源,公开共享属性,根据它们的职责将这些属性提取到新的 classes。
例如,您可以让 MainViewModel
公开一个封装 material 相关属性和逻辑的 MaterialViewModel
class。这样 MaterialViewModel
可以在全球范围内使用。
鉴于您遵循每个视图一个数据上下文 class 原则,您可以通过仅让特定页面的特定视图模型 class 公开 [=84] 来将共享属性的范围限制在特定页面=]相同 MaterialViewModel
实例:
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<StackPanel>
<MaterialControl DataContext="{Binding MaterialViewModel}"
Material="{Binding Material}"
Weight="{Binding Weight}" />
<UserControlB ... />
</StackPanel>
</Window>
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
// Since defined in 'MainViewModel' the properties of 'MaterialViewModel'
// are globally shared accross pages
public MaterialViewModel MaterialViewModel { get; }
/* View model classes per page */
public ViewModelPageA PageViewModelA { get; }
// If 'ViewModelPageB' would expose a 'MaterialViewModel',
// you can limit the visibility of 'MaterialViewModel' to the 'ViewModelPageB' DataContext exclusively
public ViewModelPageB PageViewModelB { get; }
// Example initialization
public SampleMainViewModel()
{
var sharedModelClass = new SomeModelClass();
this.MaterialViewModel = new MaterialViewModel(sharedModelClass);
this.ViewModelPageA = new ViewModelPageA(sharedModelClass);
// Introduce the MaterialViewModel to a page specific class
// to make the properties of 'MaterialViewModel' to be shared inside the page only
this.ViewModelPageB = new ViewModelPageB(sharedModelClass);
}
}
MaterialViewModel.cs
public class MaterialViewModel : INotifyPropertyChanged
{
public string Material { get; set; }
public double Weight { get; set; }
private SomeModelClass SomeModelClass { get; }
// Example initialization
public MaterialViewModel(SomeModelClass sharedModelClass)
{
this.SomeModelClass = sharedModelClass;
this.Material = this.SomeModelClass.Material;
this.Weight = this.SomeModelClass.Weight;
// Listen to model changes
this.SomeModelClass.MaterialDataChanged += OnModelMaterialChanged;
}
// Example command handler to send dat back to the model.
// This will also trigger the model to raise corresponding data chnaged events
// to notify listening view model classes that new data is available.
// It can make more sense to define such a command in the owning class,
// like SampleMainViewModel in this case.
private void ExecuteSaveDataCommand()
=> this.SomeModelClass.SaveData(this.Material, this.Weight);
private void UpdateData_MaterialChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Material = someModelClass.Material;
this.Weight = someModelClass.Weight;
}
}
我有数据架构问题。我的目标应该是进行双向数据通信 在 ViewModel 和模型之间 类。我有一个 window 具有不同的用户控件。每个用户控件 有自己的数据,但它们之间共享一些属性。 对于每个 ViewModel,我实现了两个函数来同步模型和视图模型。 该模型应该保持更新,所以我在 属性Changed 事件中实现了方法调用 SyncModel。 到目前为止这还不太好,因为当我调用构造函数时,方法调用链是: 构造函数 -> SyncViewModel -> 属性 setter -> 属性已更改 -> SyncModel
这里有一些示例代码可以更好地理解我的问题:
public class SampleModel
{
public string Material { get; set; }
public double Weight { get; set; }
public double Length { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public object SharedProperty { get; set; }
}
public class SampleViewModelA : AbstractViewModel
{
public string Material
{
get
{
return _Material;
}
set
{
if (value != _Material)
{
_Material = value;
OnPropertyChanged(nameof(Material));
}
}
}
public double Weight
{
get
{
return _Weight;
}
set
{
if (value != _Weight)
{
_Weight = value;
OnPropertyChanged(nameof(Weight));
}
}
}
public object SharedProperty
{
get
{
return _SharedProperty;
}
set
{
if (value != _SharedProperty)
{
_SharedProperty = value;
OnPropertyChanged(nameof(SharedProperty));
}
}
}
public SampleViewModelA(SampleModel Instance) : base(Instance) { }
public override void SyncModel()
{
//If I wouldn't check here, it would loop:
//constructor -> SyncViewModel -> Property setter -> PropertyChanged -> SyncModel
if (Instance.Material == Material &&
Instance.Weight == Weight &&
Instance.SharedProperty == SharedProperty)
return;
Instance.Material = Material;
Instance.Weight = Weight;
Instance.SharedProperty = SharedProperty;
}
public override void SyncViewModel()
{
Material = Instance.Material;
Weight = Instance.Weight;
SharedProperty = Instance.SharedProperty;
}
private string _Material;
private double _Weight;
private object _SharedProperty;
}
public class SampleViewModelB : AbstractViewModel
{
//Same like SampleViewModelA with Properties Length, Width, Height AND SharedProperty
}
public abstract class AbstractViewModel : INotifyPropertyChanged
{
//All ViewModels hold the same Instance of the Model
public SampleModel Instance { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public AbstractViewModel(SampleModel Instance)
{
this.Instance = Instance;
SyncViewModel();
}
protected virtual void OnPropertyChanged(string PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
SyncModel();
}
public abstract void SyncModel();
public abstract void SyncViewModel();
}
真正的问题是,SharedProperty
需要在 SampleViewModelA
和 SampleViewModelB
之间更新。首先,我认为观察者模式可以帮助我,但 SharedProperties 是多种多样的,以使其与通用接口一起工作。然后我认为带有更改事件的数据控制器可以像这样帮助我
public class SampleDataController
{
public SampleModel Instance { get; set; }
public delegate void SynchronizeDelegate();
public event SynchronizeDelegate SynchronizeEvent;
public void SetSharedProperty(object NewValue)
{
if (Instance.SharedProperty != NewValue)
{
Instance.SharedProperty = NewValue;
SynchronizeEvent?.Invoke();
}
}
}
如果它这样做,我的 AbstractViewModel
将只与控制器而不是实例通信。 SyncModel
函数会调用 SetSharedProperty
之类的方法,而不是直接访问。
MainViewModel 代码可能如下所示。
public class SampleMainViewModel
{
public SampleViewModelA ViewModelA { get; set; }
public SampleViewModelB ViewModelB { get; set; }
public SampleDataController Controller { get; set; }
public SampleMainViewModel()
{
ViewModelA = new SampleViewModelA(Controller);
ViewModelB = new SampleViewModelB(Controller);
Controller.SynchronizeEvent += ViewModelA.SyncViewModel;
Controller.SynchronizeEvent += ViewModelB.SyncViewModel;
}
}
这会导致问题,SynchronizeEvent
调用的来源也是
订阅事件本身。这不会导致无限循环,因为我检查是否
值等于新状态,但对我来说似乎很难看。一定有更好的
远不止于此。
在我的项目中,我有 8 个 ViewModel 和多个模型 类,我需要在其中同步数据 不同的共享属性。
感谢您的帮助,并希望到目前为止问题是可以理解的。
您已经使用了由另一个视图模型 classes SampleViewModelA
和 SampleViewModelB
.
组成的 SampleMainViewModel
现在您所要做的就是在视图 models/views 之间移动 all shared 的属性(例如 SharedProperty
,还有 Material
和 Weight
) 组合 SampleMainViewModel
或共享 class。这样您的所有控件都可以绑定到相同的数据源。
此外 Model 之间的通信 --> View Model 应该只通过事件发生:Model 可以通过以下方式通知 View Model公开例如 DataChanged
事件。 Model 和 View Model 之间没有真正的双向 communication/dependency。这就是 MVVM 的主要特征:参与组件的单向依赖 - 通过实现事件、命令,尤其是利用数据绑定来实现。
以下示例显示了如何将控件绑定到共享属性和非共享属性(那些是专用视图模型 classes 的属性)。
MainWindow.xaml
<Window>
<Window.DataContext>
<SampleMainViewModel />
</Window.DataContext>
<StackPanel>
<UserControlA Material="{Binding Material}"
SharedProperty="{Binding SharedProperty}"
UnsharedPropertyA="{Binding ViewModelA.UnsharedPropertyA}" />
<UserControlB Material="{Binding Material}"
SharedProperty="{Binding SharedProperty}"
UnsharedPropertyB="{Binding ViewModelB.UnsharedPropertyB}" />
</StackPanel>
</Window>
SampleMainViewModel.cs
public class SampleMainViewModel : INotifyPropertyChanged
{
public SampleViewModelA ViewModelA { get; }
public SampleViewModelB ViewModelB { get; }
/* Properties must raise INotifyPropertyChanged.PropertyChanged */
public string Material { get; set; }
public double Weight { get; set; }
public object SharedProperty { get; set; }
// Example initialization
public SampleMainViewModel(SomeModelClass someModelClass)
{
this.ViewModelA = new SampleViewModelA();
this.ViewModelB = new SampleViewModelB();
this.Material = someModelClass.Material;
this.Weight = someModelClass.Weight;
this.SharedProperty = someModelClass.SharedProperty;
someModelClass.DataChanged += UpdateData_OnDataChanged;
}
private void UpdateData_OnDataChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Material = someModelClass.Material;
this.Weight = someModelClass.Weight;
this.SharedProperty = someModelClass.SharedProperty;
}
}
SampleViewModelA.cs
public class SampleViewModelA : INotifyPropertyChanged
{
public object UnsharedPropertyA { get; set; }
}
SampleViewModelB.cs
public class SampleViewModelB : INotifyPropertyChanged
{
public object UnsharedPropertyB { get; set; }
}
"Your suggestion cause the disadvantage, that my separated ViewModels are no longer encapsulated. And if I would move all my code with shared properties to the MainViewModel, the class would end up very large"
针对您的评论:如果您坚持每个控件都有一个视图模型,包括复制属性等,那么您必须采取不同的方法。
此外,将 shared/duplicate 代码移出视图模型不会破坏封装 - 假设那些 classes 不包含仅重复的代码。
但请注意,不建议为每个控件创建一个视图模型。您有一个 view/page - 多个控件的集合 - 它具有定义的数据上下文。此视图中的所有控件共享相同的数据上下文 - 通常,因为视图与结构化上下文相关。
这就是默认情况下继承 FrameworkElement.DataContext
的原因。
每个控件都有一个视图模型会使事情变得过于复杂,并导致大量重复代码 - 而不仅仅是像您的示例中那样的重复属性。你会发现自己也在重复逻辑。谈到可测试性,如果你复制逻辑,你也会复制单元测试。 这是因为您正在处理相同的数据和相同的模型 classes.
您通常将重复代码提取到单独的 class 中,这是由依赖于此重复代码的类型引用的。考虑到这种“无重复代码”策略重构您的视图模型 classes 最终会将“共享”属性移动到单独的 class。由于我们讨论的是同一视图的数据上下文,因此这个单独的 class 将是分配给页面 DataContext
的视图模型 class。我想说你的方法恰恰相反:你复制代码(并将其称为封装)。如果 class 最终变得非常大,因为它包含很多属性,那么您可以检查您的 UI 设计 - 也许您应该将您的大页面拆分为更多内容更简洁的页面。这也可能会改善用户体验。
通常,拥有一个具有更多属性的视图模型并没有错。如果您的视图模型 class 也包含很多逻辑,您可以提取此逻辑以分离 classes.
你仍然可以使用上一个例子的模式,即监听模型的数据变化事件。
您要么实现一个非常通用的事件,如上面的 DataChanged
事件,要么实现几个更专业的事件,如 MaterialChanged
事件。还要确保将相同的模型实例注入每个视图模型。
下面的示例展示了如何让多个不同的视图模型 classes 公开相同的数据,其中所有这些视图模型 classes 通过观察它们的模型 classes 来更新自己:
MainWindow.xaml
<Window>
<Window.DataContext>
<SampleMainViewModel />
</Window.DataContext>
<StackPanel>
<UserControlA DataContext="{Binding ViewModelA}"
Material="{Binding Material}"
Weight="{Binding Weight}"
SharedProperty="{Binding SharedPropertyA}" />
<UserControlB DataContext="{Binding ViewModelB}"
Material="{Binding Material}"
Weight="{Binding Weight}"
SharedProperty="{Binding SharedPropertyB}" />
</StackPanel>
</Window>
SampleMainViewModel.cs
public class SampleMainViewModel : INotifyPropertyChanged
{
public SampleViewModelA ViewModelA { get; }
public SampleViewModelB ViewModelB { get; }
// Example initialization
public SampleMainViewModel()
{
var sharedModelClass = new SomeModelClass();
this.ViewModelA = new SampleViewModelA(sharedModelClass);
this.ViewModelB = new SampleViewModelB(sharedModelClass);
}
}
SampleViewModelA.cs
public class SampleViewModelA : INotifyPropertyChanged
{
/* Shared properties */
public string Material { get; set; }
public double Weight { get; set; }
public object SharedProperty { get; set; }
private SomeModelClass SomeModelClass { get; }
// Example initialization
public SampleViewModelA(SomeModelClass sharedModelClass)
{
this.SomeModelClass = sharedModelClass;
this.Material = this.SomeModelClass.Material;
this.Weight = this.SomeModelClass.Weight;
this.SharedProperty = this.SomeModelClass.SharedProperty;
// Listen to model changes
this.SomeModelClass.DataChanged += UpdateData_OnDataChanged;
this.SomeModelClass.MaterialChanged += OnModelMaterialChanged;
}
// Example command handler to send dat back to the model.
// This will trigger the model to raise corresponding data chnaged events
// to notify listening view model classes that new data is available.
private void ExecuteSaveDataCommand()
=> this.SomeModelClass.SaveData(this.Material, this.Weight);
private void OnModelMaterialChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Material = someModelClass.Material;
}
private void UpdateData_OnDataChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Weight = someModelClass.Weight;
this.SharedProperty = someModelClass.SharedProperty;
}
}
SampleViewModelB.cs
public class SampleViewModelB : INotifyPropertyChanged
{
/* Shared properties */
public string Material { get; set; }
public double Weight { get; set; }
public object SharedProperty { get; set; }
private SomeModelClass SomeModelClass { get; }
// Example initialization
public SampleViewModelB(SomeModelClass sharedModelClass)
{
this.SomeModelClass = sharedModelClass;
this.Material = this.SomeModelClass.Material;
this.Weight = this.SomeModelClass.Weight;
this.SharedProperty = this.SomeModelClass.SharedProperty;
// Listen to model changes
this.SomeModelClass.DataChanged += UpdateData_OnDataChanged;
this.SomeModelClass.MaterialChanged += OnModelMaterialChanged;
}
// Example command handler to send dat back to the model.
// This will trigger the model to raise corresponding data chnaged events
// to notify listening view model classes that new data is available.
private void ExecuteSaveDataCommand()
=> this.SomeModelClass.SaveData(this.Material, this.Weight);
private void OnModelMaterialChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Material = someModelClass.Material;
}
private void UpdateData_OnDataChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Weight = someModelClass.Weight;
this.SharedProperty = someModelClass.SharedProperty;
}
}
第一个解决方案(旨在消除重复代码)的一个变体是重构您的绑定源,公开共享属性,根据它们的职责将这些属性提取到新的 classes。
例如,您可以让 MainViewModel
公开一个封装 material 相关属性和逻辑的 MaterialViewModel
class。这样 MaterialViewModel
可以在全球范围内使用。
鉴于您遵循每个视图一个数据上下文 class 原则,您可以通过仅让特定页面的特定视图模型 class 公开 [=84] 来将共享属性的范围限制在特定页面=]相同 MaterialViewModel
实例:
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<StackPanel>
<MaterialControl DataContext="{Binding MaterialViewModel}"
Material="{Binding Material}"
Weight="{Binding Weight}" />
<UserControlB ... />
</StackPanel>
</Window>
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
// Since defined in 'MainViewModel' the properties of 'MaterialViewModel'
// are globally shared accross pages
public MaterialViewModel MaterialViewModel { get; }
/* View model classes per page */
public ViewModelPageA PageViewModelA { get; }
// If 'ViewModelPageB' would expose a 'MaterialViewModel',
// you can limit the visibility of 'MaterialViewModel' to the 'ViewModelPageB' DataContext exclusively
public ViewModelPageB PageViewModelB { get; }
// Example initialization
public SampleMainViewModel()
{
var sharedModelClass = new SomeModelClass();
this.MaterialViewModel = new MaterialViewModel(sharedModelClass);
this.ViewModelPageA = new ViewModelPageA(sharedModelClass);
// Introduce the MaterialViewModel to a page specific class
// to make the properties of 'MaterialViewModel' to be shared inside the page only
this.ViewModelPageB = new ViewModelPageB(sharedModelClass);
}
}
MaterialViewModel.cs
public class MaterialViewModel : INotifyPropertyChanged
{
public string Material { get; set; }
public double Weight { get; set; }
private SomeModelClass SomeModelClass { get; }
// Example initialization
public MaterialViewModel(SomeModelClass sharedModelClass)
{
this.SomeModelClass = sharedModelClass;
this.Material = this.SomeModelClass.Material;
this.Weight = this.SomeModelClass.Weight;
// Listen to model changes
this.SomeModelClass.MaterialDataChanged += OnModelMaterialChanged;
}
// Example command handler to send dat back to the model.
// This will also trigger the model to raise corresponding data chnaged events
// to notify listening view model classes that new data is available.
// It can make more sense to define such a command in the owning class,
// like SampleMainViewModel in this case.
private void ExecuteSaveDataCommand()
=> this.SomeModelClass.SaveData(this.Material, this.Weight);
private void UpdateData_MaterialChanged(object sender, EventArgs args)
{
var someModelClass = sender as SomeModelClass;
this.Material = someModelClass.Material;
this.Weight = someModelClass.Weight;
}
}