C#Wpf mvvm保持多个ViewModel与模型同步



我有数据体系结构问题。我的目标应该是实现双向数据通信在ViewModels和Model类之间。我有一个带有不同用户控件的窗口。每个用户控件拥有自己的数据,但这些数据之间共享一些属性。对于每个ViewModel,我实现了两个同步模型和ViewModel的功能。模型应该保持更新,所以我在PropertyChanged事件中实现了方法调用SyncModel。到目前为止,这还不太好,因为当我调用构造函数时,方法调用链是:构造函数->SyncViewModel->属性设置器->属性已更改->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需要在SampleViewModelASampleViewModelB之间进行更新。起初,我认为观察者模式可以帮助我,但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个ViewModels和多个模型类,我需要用不同的共享属性。

我很感谢你的帮助,希望到目前为止问题是可以理解的。

您已经使用了由其他视图模型类SampleViewModelASampleViewModelB组成的SampleMainViewModel
现在,您所要做的就是将视图模型/视图(如SharedProperty,但也包括MaterialWeight)之间共享的属性全部移动到组合的SampleMainViewModel或一般的共享类。这样,所有控件都可以绑定到同一个数据源。

此外,模型-->视图模型只能通过事件发生:模型可以通过公开例如DataChanged事件来通知视图模型。模型视图模型VVM的主要特征:参与组件的单向依赖性——通过实现事件、命令,尤其是通过利用数据绑定来实现。

下面的示例显示了如何将控件绑定到共享属性和非共享属性(即专用视图模型类的属性)。

主窗口.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; }
}

";你的建议造成了缺点,我的分离ViewModels不再被封装。如果我能移动我所有的代码共享属性到MainViewModel,该类最终会非常"大";

要解决您的意见:如果您坚持每个控件都有一个视图模型,包括复制属性等,那么您必须采取不同的方法
此外,将共享/重复代码从视图模型中移出并不会破坏封装—假设这些类不仅包含重复代码。

但请注意,不建议每个控件都有一个视图模型。您有一个视图/页面-多个控件的聚合-它有一个定义的数据上下文。此视图中的所有控件共享相同的数据上下文,因为视图通常与结构化上下文相关
这就是默认继承FrameworkElement.DataContext的原因。

每个控件都有一个视图模型会使事情变得过于复杂,并导致大量重复的代码,而不仅仅是像您的示例中那样重复的属性。你会发现自己也在重复逻辑。说到可测试性,如果您复制逻辑,您也将复制单元测试。这是因为您正在处理相同的数据和相同的模型类。

通常将重复代码提取到一个单独的类中,该类由依赖于此重复代码的类型引用。用这个"重构"视图模型类;无重复代码";考虑到的政策最终会推动";共享的";属性转换为单独的类。由于我们讨论的是同一视图的数据上下文,因此这个单独的类将是分配给页面的DataContext的视图模型类。我想说的是,您的方法恰恰相反:您复制代码(并称之为封装)。如果这个类因为包含很多属性而变得很大,那么你可以回顾一下你的UI设计——也许你应该把你的大页面拆分成更多内容更简洁的页面。这可能也会改善用户体验。

一般来说,拥有更多属性的视图模型没有什么错。如果视图模型类也包含大量逻辑,则可以提取这些逻辑来分离类。

您仍然可以使用上一个示例的模式,即侦听模型的数据更改事件。

要么实现一个非常通用的事件,如上面的DataChanged事件,要么实现几个更专门的事件,例如MaterialChanged事件。还要确保将相同的模型实例注入到每个视图模型中
以下示例显示了如何使用多个不同的视图模型类来公开相同的数据,其中所有这些视图模型类都通过观察其模型类来更新自己:

主窗口.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;
}
}

第一个解决方案(旨在消除重复代码)的一个变体是重构绑定源,根据共享属性的职责将这些属性提取到新的类中,从而公开共享属性。

例如,您可以让MainViewModel公开一个MaterialViewModel类,该类封装与材质相关的属性和逻辑。通过这种方式,MaterialViewModel可以全局可用
假设您遵循每个视图一个数据上下文类的原则,您可以通过仅让共享属性的特定视图模型类公开相同的MaterialViewModel实例来将共享属性的范围限制为特定页面:

主窗口.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;
}
}

最新更新