从一个案例看MVC中DataContext和UpdateModel的工作原理(详解UpdateModel/SubmitChanges错误)

本文详细介绍了在ASP.NET MVC框架下,如何解决数据上下文(DataContext)与Repository之间的数据更新冲突问题。通过实例分析,阐述了如何正确使用UpdateModel和Save方法,以及如何在控制器中合理地管理Repository实例来避免冲突,最终实现数据的一致性和有效性更新。

昨天遇到一段棘手的程序,尝试了各种方法,忽而在SubmitChanges的时候没反应(无错误,也不更新),忽而发生ChangeConflict,经过几个小时,终于大致理清了思路,也顺便把DataContext/UpdateModel/SubmitChanges给搞得更明白了一些,特此分享。

 

先大致看看代码:

 

xxController

{

        AgileRepository _repAgile = new AgileRepository(); //这里边是SubmitChanges/DateContext/Tables等属性,可取出下面提到的story
        SFCRepository _repSFC = new SFCRepository(); //相同,可取出下面提到的UDCs

   

        [HttpPost]
        public ActionResult Edit(int id, FormCollection collection)
        {
            Story story = _repAgile.GetStoryAt(id);

            try
            {
                UpdateModel(story);
                foreach (var udc in story.UDCs)
                {
                    UpdateModel(udc, udc.ID.ToString("D6"));
                }
                _repAgile.Save();

                _repSFC.Save(); //这里也是
                ...
            }
}

 

之所以出现红色的_repSFC.Save(),是因为story的一个属性List<IUDC> UDCs,也是需要在这个页面被更新的内容(在View中有控件与其对应),而它的Get过程最初是:

class Story

{

        public List<IUDC> UDCs 
        {
            get
            {

               SFCRepository rep = new SFCRepository(); 
                return rep.GetUDCs().Where(...);
            }
            set { }
        }
}

这里蓝色的rep和前面红色的rep不是同一个,所以如果从蓝色rep的当中取出UDCs并进行UpdateModel,而对另外一个无关的红色rep保存,什么也不会发生;而如果两者都取出UDCs并都曾经被UpdateModel,在Save(内部执行了SubmitChanges)的时候,会报出confilict changes exception,这个Exception极其麻烦而且不透明Google/MSDN上能找到一些资料但很多不工作。

 

其实,全部解决方法的秘诀其实就是:让取出数据的那个Repository执行Save操作(或者从内部看,是让取出数据的DataContext执行SubmitChanges操作)

 

蓝色rep的是个局部变量,活不到Save的时候了,只能用红色的那个rep了。代码改为下面这个就好了:

xxController

{

 SFCRepository _repSFC = new SFCRepository(); //下面取数据/保存的都是它。

 

[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
    Story story = _repAgile.GetStoryAt(id);

    _repSFC.GetUDCsFor(story); //在这里边让_repSFC的DataContext取数据。

    try
    {
        UpdateModel(story);
        foreach (var udc in story.UDCs)
        {
             UpdateModel(udc, udc.ID.ToString("D6"));
        }
        _repAgile.Save();

        _repSFC.Save(); //这里会完成存储。
        ...
}

尽管能用了,但这段代码很不好,尤其GetUDCsFor,调用的位置很生硬,不好读也很容易出错。

最终还是这样最好:

 

外面:

            Story story = _repAgile.GetStoryAt(id);

            try
            {
                UpdateModel(story);
                foreach (var udc in story.UDCs)
                {
                    UpdateModel(udc, udc.ID.ToString("D6")); //Update的
                }
                story.SaveUDCs(); // 这个调用看着顺眼。
                _repAgile.Save();
                ...
            }
里边:

   public partial class Story : IUDCable, IItem
    {

        private SFCRepository _repSFC = new SFCRepository(); //这个方案里取数据/保存的都是它。_repSFC不再是个局部变量,生命周期正好和UDCs相同。
        public void SaveUDCs()
        {
            _repSFC.Save(); // 在这里保存
        }
        public List<IUDC> UDCs 
        {
            get
            {
                _repSFC.GetUDCOf(this, ref _udcs); //取数据。
                return _udcs;
            } 
        }
后面本来不应该再把_udcs传入GetUDCOf了,直接但因为别的IUDCable也要用到,所以封装了一个函数。

这个方案,取数据/存数据的都是Story内部的_repSFC,而且封装性更好,是最终结果。

 

最后重复一下在这种场景中发生问题时的解决方法的秘诀其实就是:让取出数据的那个Repository执行Save操作(或者从内部看,是让取出数据的DataContext执行SubmitChanges操作)

 

点击下载免费的敏捷开发教材:《火星人敏捷开发手册

 

<think>我们正在使用WPF Prism框架,问题是如何在ViewModel中通过DataContext绑定另一个ViewModel。在Prism中,通常不推荐直接在ViewModel中操作另一个ViewModel的DataContext,因为这样会破坏MVVM的分离原则。但是,如果确实需要,我们可以通过以下几种方式实现: 1. **使用Prism的事件聚合器(EventAggregator)**:这是一种松耦合的方式,ViewModel之间可以通过发布订阅事件来通信。 2. **使用共享服务**:创建一个服务类,其中包含需要共享的数据或功能,然后在需要的地方注入该服务。 3. **通过导航时传递参数**:如果是在导航到另一个视图时传递ViewModel,可以在导航过程中传递参数。 但是,问题中提到了通过DataContext绑定另一个ViewModel。在Prism中,视图的DataContext通常绑定到对应的ViewModel。如果我们想在当前ViewModel中获取另一个视图的DataContext(即另一个ViewModel),我们可以考虑以下方法: - 如果这两个视图在同一个视图容器中(比如同一个Window或UserControl),我们可以通过视图的依赖关系找到另一个视图,然后获取其DataContext。但是,这种方法需要小心,因为它会引入视图ViewModel之间的紧耦合。 另一种更符合Prism思想的方式是使用**区域管理器(RegionManager)**。我们可以将视图注入到区域中,然后通过区域获取视图实例,进而获取其DataContext。 例如: 1. 在某个区域(比如名为"SomeRegion")中显示一个视图(比如ViewA),该视图的DataContext是ViewModelA。 2. 在另一个ViewModel(比如ViewModelB)中,我们可以通过IRegionManager接口获取该区域中的视图,然后获取其DataContext。 但是,这种方法也需要谨慎使用,因为它也引入了ViewModel视图之间的依赖。 考虑到这些,我们可能更倾向于使用事件聚合器或共享服务。下面我们分别介绍: ### 方法1:使用事件聚合器(推荐) 事件聚合器允许ViewModel之间进行松耦合的通信。一个ViewModel发布事件,另一个ViewModel订阅该事件。 **步骤:** 1. 定义事件类,继承自PubSubEvent<T>,其中T是事件参数的类型(如果需要传递参数)。 2. 在需要接收事件的ViewModel中订阅该事件。 3. 在发送事件的ViewModel中发布该事件。 **示例:** 假设我们有两个ViewModel:ViewModelAViewModelB。ViewModelA需要通知ViewModelB并传递一些数据。 首先,定义事件类: ```csharp public class MyEvent : PubSubEvent<string> { } ``` 在ViewModelB中订阅事件(在构造函数中): ```csharp public ViewModelB(IEventAggregator eventAggregator) { eventAggregator.GetEvent<MyEvent>().Subscribe(HandleEvent); } private void HandleEvent(string parameter) { // 处理事件 } ``` 在ViewModelA中发布事件: ```csharp public ViewModelA(IEventAggregator eventAggregator) { this.eventAggregator = eventAggregator; } public void SomeMethod() { eventAggregator.GetEvent<MyEvent>().Publish("Hello from ViewModelA"); } ``` ### 方法2:使用共享服务 我们可以创建一个服务,该服务持有需要共享的ViewModel实例或数据。然后,将这个服务注入到需要的ViewModel中。 **示例:** 定义一个服务接口: ```csharp public interface ISharedService { ViewModelB VmB { get; set; } } ``` 实现该服务: ```csharp public class SharedService : ISharedService { public ViewModelB VmB { get; set; } } ``` 在Prism的容器中注册这个服务(在App.xaml.cs或模块中): ```csharp containerRegistry.RegisterSingleton<ISharedService, SharedService>(); ``` 在ViewModelB的构造函数中,将自身实例设置到服务中(注意:需要在ViewModelB被创建后设置): ```csharp public ViewModelB(ISharedService sharedService) { sharedService.VmB = this; } ``` 在ViewModelA中,就可以通过该服务访问ViewModelB: ```csharp public ViewModelA(ISharedService sharedService) { var vmB = sharedService.VmB; // 使用vmB } ``` 但是,这种方法需要注意生命周期问题,确保ViewModelB已经创建并设置到服务中。 ### 方法3:通过导航传递参数 如果是在导航过程中需要传递参数,可以使用Prism的导航服务。在导航时,可以传递一个NavigationParameters对象,其中包含参数。 **示例:** 在ViewModelA中导航: ```csharp _regionManager.RequestNavigate("RegionName", "ViewB", new NavigationParameters { { "key", "value" } }); ``` 在ViewModelB中,实现INavigationAware接口,在OnNavigatedTo方法中接收参数: ```csharp public void OnNavigatedTo(NavigationContext navigationContext) { var value = navigationContext.Parameters["key"] as string; } ``` ### 结论 在Prism框架中,ViewModel之间通常不直接引用彼此的实例,而是通过事件、服务或导航参数进行通信。这样保持了MVVM的松耦合特性。因此,我们应尽量避免在ViewModel中直接通过DataContext引用另一个ViewModel。 如果确实需要直接引用,可以考虑使用共享服务(如上所述)来间接持有ViewModel的引用。但请注意,这样做可能会增加组件间的耦合度,应谨慎使用。 ### 相关问题 1. 在WPF Prism中,如何实现ViewModel之间的通信? 2. Prism的事件聚合器(EventAggregator)是如何工作的? 3. 如何在Prism中注册使用共享服务? 4. Prism中的导航服务如何传递参数? 5. 在MVVM模式中,如何保持ViewModel的独立性?</think>在 WPF Prism 框架中,通过 `DataContext` 直接绑定另一个 ViewModel 通常不符合 MVVM 设计原则(会破坏解耦性),但可以通过以下方式间接实现交互: --- ### 推荐解决方案(符合 Prism 设计) #### 1. **使用依赖注入(推荐)** 通过 Prism 的容器注册 ViewModel,在需要时注入依赖: ```csharp // 注册 ViewModel(在 App/Module 中) containerRegistry.Register<OtherViewModel>(); // 在目标 ViewModel 中注入 public class CurrentViewModel { private readonly OtherViewModel _otherVM; public CurrentViewModel(OtherViewModel otherVM) { _otherVM = otherVM; // 直接获得实例 } } ``` #### 2. **通过事件聚合器通信(松耦合)** 使用 Prism 的 `IEventAggregator` 实现 ViewModel 间通信: ```csharp // 发送方 ViewModel eventAggregator.GetEvent<MyEvent>().Publish("Data"); // 接收方 ViewModel eventAggregator.GetEvent<MyEvent>().Subscribe(data => { // 处理数据 }); ``` #### 3. **共享服务层** 创建共享服务类,通过接口注入: ```csharp public interface IDataService { object SharedData { get; set; } } // 在 ViewModel 中注入服务 public class CurrentViewModel { public CurrentViewModel(IDataService dataService) { var data = dataService.SharedData; // 获取共享数据 } } ``` #### 4. **导航传参** 通过 Prism 导航传递参数: ```csharp // 发送参数 NavigationParameters parameters = new NavigationParameters { { "key", new OtherViewModel() } // 直接传递 ViewModel }; _regionManager.RequestNavigate("RegionName", "ViewName", parameters); // 接收方(需实现 INavigationAware) public void OnNavigatedTo(NavigationContext context) { var vm = context.Parameters["key"] as OtherViewModel; } ``` --- ### 注意事项 1. **避免直接绑定** MVVM 的核心是解耦,直接通过 `DataContext` 引用另一个 ViewModel 会导致: - 破坏 ViewModel 的独立性 - 增加测试复杂度 - 引发循环引用风险 2. **生命周期管理** 注入的 ViewModel 默认是瞬态实例,需通过 `RegisterSingleton` 控制生命周期: ```csharp containerRegistry.RegisterSingleton<OtherViewModel>(); ``` 3. **数据绑定替代方案** 如果需要在 View 中显示另一个 ViewModel,推荐使用 `Region`: ```xml <!-- 在 View 中声明区域 --> <ContentControl prism:RegionManager.RegionName="OtherRegion" /> ``` ```csharp // 动态注入 View-ViewModel _regionManager.RequestNavigate("OtherRegion", "OtherView"); ``` --- ### 总结 | **场景** | **推荐方案** | **优势** | |-------------------------|-----------------------|------------------------------| | 数据共享 | 事件聚合器/共享服务 | 完全解耦,可测试性高 | | 界面跳转传参 | 导航参数 | 符合 Prism 导航流程 | | 跨 ViewModel 方法调用 | 依赖注入 | 直接访问实例 | | 显示其他 View | Region 管理器 | 动态加载,符合 Prism 标准 | > 最佳实践:优先采用 **事件聚合器** 或 **共享服务** 实现 ViewModel 间通信,避免直接引用 DataContext[^2][^3]。 --- ### 相关问题 1. Prism 中如何实现 ViewModel 之间的松耦合通信? 2. WPF MVVM 模式下如何处理跨 ViewModel 的数据同步? 3. Prism 的事件聚合器(EventAggregator)有哪些使用场景? 4. 如何在 Prism 导航过程中传递复杂对象参数? 5. 依赖注入在 Prism 框架中是如何工作的?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值