如何用Model中的List直接更新ViewModel里的ObservableCollection?
正如众多开发者在MVVM架构中所了解的,不应让View与Model直接耦合。因此我的View绑定到ViewModel中的ObservableCollection,而该集合需要通过Model中的List来更新。
我尝试的方案是在CanvasModel中创建CanvasObject,添加到List后,通过事件将引用传递给ViewModel,再由ViewModel更新ObservableCollection。以下是我的实现代码及说明:
View 代码
<ItemsControl ItemsSource="{Binding CanvasObjects}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas Width="1000" Height="1000" Background="GhostWhite" MouseDown="Canvas_MouseDown"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl>
为处理鼠标点击,我添加了MouseDown事件:
public partial class CanvasView : UserControl { dynamic viewModel; public CanvasView() { InitializeComponent(); Loaded += InitializeViewModel; } private void InitializeViewModel(object sender, RoutedEventArgs e) { viewModel = DataContext; Loaded -= InitializeViewModel; } private void Canvas_MouseDown(object sender, MouseButtonEventArgs e) { viewModel.MouseDownEventHandler(sender, e); } }
ViewModel 代码
internal class CanvasViewModel : ViewModelBase, IMouseEventController { private readonly ToolPanelDispatcher pToolPanelDispatcher; private readonly CanvasDispatcher pCanvasDispatcher; private readonly CanvasObjectDispatcher pCanvasObjectDispatcher; private CanvasModel pCanvasModel; private ToolMode pToolMode; private Point pCurrentMousePointerCoordinates; private Point pPreviousMousePointerCoordinates; public ObservableCollection<CanvasObject> CanvasObjects { get; set; } public CanvasViewModel(ToolPanelDispatcher toolPanelDispatcher, CanvasDispatcher canvasDispatcher, CanvasObjectDispatcher canvasObjectDispatcher) { pToolPanelDispatcher = toolPanelDispatcher; pCanvasDispatcher = canvasDispatcher; pCanvasObjectDispatcher = canvasObjectDispatcher; // Injects CanvasDispatcher and CanvasObjectDispatcher into CanvasModel pCanvasModel = new CanvasModel(pCanvasDispatcher, pCanvasObjectDispatcher); CanvasObjects = new ObservableCollection<CanvasObject>(); // Subscribes/listens for when ToolMode changes on the ToolPanelView pToolPanelDispatcher.ToolModeChanged += ToolModeChangedEventHandler; pCanvasDispatcher.CanvasObjectAdded += CanvasObjectAddedEventHandler; } private void ToolModeChangedEventHandler(ToolMode toolMode) { pToolMode = toolMode; // Relays current tool mode to CanvasObjects pCanvasDispatcher.PublishToolModeChanged(pToolMode); } private void CanvasObjectAddedEventHandler(CanvasObject canvasObject) { CanvasObjects.Add(canvasObject); } public void MouseDownEventHandler(object sender, MouseButtonEventArgs e) { pCurrentMousePointerCoordinates = e.GetPosition(sender as Canvas); switch (pToolMode) { case ToolMode.Node: pCanvasModel.DrawCanvasObject(CanvasObjectType.Node, pCurrentMousePointerCoordinates); break; } pPreviousMousePointerCoordinates = pCurrentMousePointerCoordinates; } }
Model 代码
internal class CanvasModel { private readonly List<CanvasObject> pCanvasObjects; private readonly CanvasDispatcher pCanvasDispatcher; private readonly CanvasObjectDispatcher pCanvasObjectDispatcher; public CanvasModel(CanvasDispatcher canvasDispatcher, CanvasObjectDispatcher canvasObjectDispatcher) { pCanvasObjects = new List<CanvasObject>(); pCanvasDispatcher = canvasDispatcher; pCanvasObjectDispatcher = canvasObjectDispatcher; } public void DrawCanvasObject(CanvasObjectType canvasObjectType, Point origin) { var canvasObject = new Node(origin, pCanvasDispatcher, pCanvasObjectDispatcher); pCanvasObjects.Add(canvasObject); pCanvasDispatcher.OnCanvasObjectAdded(canvasObject); } }
事件基础模式:
public delegate void CanvasObjectAddedEventHandler(CanvasObject canvasObject); public event CanvasObjectAddedEventHandler? CanvasObjectAdded; public void OnCanvasObjectAdded(CanvasObject canvasObject) { CanvasObjectAdded?.Invoke(canvasObject); }
目前我仅能想到通过事件传递CanvasObject引用,让ViewModel监听并更新ObservableCollection的方式。请问这是唯一可行方案吗?是否有更高效直接的方式用List更新ObservableCollection?希望得到最佳实践相关的建议。
你的事件驱动方案符合MVVM解耦原则,但并非唯一可行方案,以下几种方式可根据场景选择:
1. 简化事件传递逻辑
当前方案通过CanvasDispatcher中转事件,可直接让CanvasModel自身暴露事件,减少中间层冗余:
internal class CanvasModel { private readonly List<CanvasObject> pCanvasObjects; public event Action<CanvasObject>? CanvasObjectAdded; public CanvasModel() { pCanvasObjects = new List<CanvasObject>(); } public void DrawCanvasObject(CanvasObjectType canvasObjectType, Point origin) { var canvasObject = new Node(origin); pCanvasObjects.Add(canvasObject); CanvasObjectAdded?.Invoke(canvasObject); } }
ViewModel直接订阅Model的事件:
public CanvasViewModel() { pCanvasModel = new CanvasModel(); pCanvasModel.CanvasObjectAdded += obj => CanvasObjects.Add(obj); }
2. 让Model维护ObservableCollection(需谨慎)
如果Model不需要跨平台复用(比如不兼容非WPF环境),可直接让Model使用ObservableCollection<CanvasObject>替代List<CanvasObject>,ViewModel直接暴露该集合给View绑定:
internal class CanvasModel { public ObservableCollection<CanvasObject> CanvasObjects { get; } = new(); public void DrawCanvasObject(CanvasObjectType canvasObjectType, Point origin) { var canvasObject = new Node(origin); CanvasObjects.Add(canvasObject); } }
ViewModel中:
public ObservableCollection<CanvasObject> CanvasObjects => pCanvasModel.CanvasObjects;
这种方式最直接,但要注意Model不能依赖WPF专属类型,否则会限制其复用性。
3. 使用CollectionViewSource做中间层
如果需要对集合做过滤、排序等视图级操作,可用CollectionViewSource包装Model的List,ViewModel监听Model的集合变更事件后刷新视图:
internal class CanvasViewModel : ViewModelBase { private readonly CanvasModel pCanvasModel; public ICollectionView CanvasObjectsView { get; } public CanvasViewModel() { pCanvasModel = new CanvasModel(); var cvs = new CollectionViewSource { Source = pCanvasModel.CanvasObjects }; CanvasObjectsView = cvs.View; pCanvasModel.CanvasObjectAdded += _ => CanvasObjectsView.Refresh(); } }
View绑定到CanvasObjectsView即可,适合需要复杂视图逻辑的场景。
最佳实践建议
- 优先保持Model纯净性:如果Model需要跨平台复用,避免依赖WPF类型(如
ObservableCollection),事件驱动是更稳妥的选择,可简化中间Dispatcher层。 - 坚持View通过ViewModel转发事件:当前View不直接操作Model的做法符合MVVM,继续保持。
- 考虑批量变更优化:未来若有批量添加/删除对象的需求,可在Model中暴露批量变更事件,ViewModel一次性更新
ObservableCollection,减少UI刷新次数。 - 可选弱事件模式:若ViewModel与Model生命周期不一致,为避免内存泄漏,可用
WeakEventManager替代直接事件订阅。
内容的提问来源于stack exchange,提问作者Experiment-626

