[WPF] 在单元测试中使用 Prism 的 EventAggregator,订阅到 ThreadOption.UIThread 会报错

  • A+
所属分类:.NET技术
摘要

上面是一段使用了 Prism 的单元测试,它主要的逻辑是在 EventAggregator 中订阅了 TestEvent,当接收到消息后在 UI 线程上执行后续的逻辑。这种代码在正常程序中没有问题,但在单元测试中会报错:


1. 问题

[TestClass] public class UnitTest1 {     [TestMethod]     public void TestMethod1()     {         ContainerLocator.Container.Resolve<TestViewModel>();     } }  public class TestViewModel {     public TestViewModel(IEventAggregator eventAggregator)     {         var testEvent = eventAggregator.GetEvent<TestEvent>();         testEvent.Subscribe(() => { }, ThreadOption.UIThread);     } }  public class TestEvent : PubSubEvent {  } 

上面是一段使用了 Prism 的单元测试,它主要的逻辑是在 EventAggregator 中订阅了 TestEvent,当接收到消息后在 UI 线程上执行后续的逻辑。这种代码在正常程序中没有问题,但在单元测试中会报错:

System.InvalidOperationException: To use the UIThread option for subscribing, the EventAggregator must be constructed on the UI thread.

2. 原因

翻翻源码,可以发现这个 Exception 在 PubSubEventSubscribe 函数中抛出:

switch (threadOption) {     case ThreadOption.PublisherThread:         subscription = new EventSubscription(actionReference);         break;     case ThreadOption.BackgroundThread:         subscription = new BackgroundEventSubscription(actionReference);         break;     case ThreadOption.UIThread:         if (SynchronizationContext == null) throw new InvalidOperationException(Resources.EventAggregatorNotConstructedOnUIThread);         subscription = new DispatcherEventSubscription(actionReference, SynchronizationContext);         break;     default:         subscription = new EventSubscription(actionReference);         break; 

SynchronizationContext 为 null 时就会判断当前不在 UI 线程,然后抛出 Exception。而 SynchronizationContext 又是在 EventAggregator 中赋值:

private readonly SynchronizationContext syncContext = SynchronizationContext.Current;  public TEventType GetEvent<TEventType>() where TEventType : EventBase, new() {     lock (events)     {         EventBase existingEvent = null;          if (!events.TryGetValue(typeof(TEventType), out existingEvent))         {             TEventType newEvent = new TEventType();             newEvent.SynchronizationContext = syncContext;             events[typeof(TEventType)] = newEvent;              return newEvent;         }         else         {             return (TEventType)existingEvent;         }     } } 

问题就出在 SynchronizationContext.Current 这里。这个属性用于获取当前线程的同步上下文。不是每一个线程都有一个 SynchronizationContext 对象。一个总是有 SynchronizationContext 对象的是UI线程。由于单元测试并不是运行在 UI 线程,所以这个属性在单元测试中一直为 null。

3. 解决方案

现在我们知道问题原因了,解决方案也很简单,只要自定义一个 EventAggregator,源码全部照抄,但是把这句:

private readonly SynchronizationContext syncContext = SynchronizationContext.Current; 

替换成这句:

private readonly SynchronizationContext syncContext = new SynchronizationContext(); 

就不会出现 PubSubEvent 中 SynchronizationContext 等于 null 的情况了。然后再把这个类注册到容器中作为 IEventAggregator:

ContainerLocator.Current.RegisterSingleton<IEventAggregator, MyEventAggregator>(); 

4. 最后

根据单元测试项目的结构,容器的初始化会有不同的方式,如果想尽量模仿 PrismApplication 的话可以参考 PrismApplicationBasePrismInitializationExtensions 写一个初始化类,大概差不多这样(简化了部分代码):

[TestClass] public abstract class TestInitializerBase {     public void Initialize()     {         ContainerLocator.SetContainerExtension(() => new UnityContainerExtension());         ContainerExtension = ContainerLocator.Current;          ContainerExtension.RegisterSingleton<IDialogService, DialogService>();         ContainerExtension.RegisterSingleton<IModuleInitializer, ModuleInitializer>();         ContainerExtension.RegisterSingleton<IModuleManager, ModuleManager>();         ContainerExtension.RegisterSingleton<RegionAdapterMappings>();         ContainerExtension.RegisterSingleton<IRegionManager, RegionManager>();         ContainerExtension.RegisterSingleton<IRegionNavigationContentLoader, RegionNavigationContentLoader>();          ContainerExtension.RegisterSingleton<IEventAggregator, EventAggregator>();          ContainerExtension.RegisterSingleton<IRegionViewRegistry, RegionViewRegistry>();         ContainerExtension.RegisterSingleton<IRegionBehaviorFactory, RegionBehaviorFactory>();         ContainerExtension.Register<IRegionNavigationJournalEntry, RegionNavigationJournalEntry>();         ContainerExtension.Register<IRegionNavigationJournal, RegionNavigationJournal>();         ContainerExtension.Register<IRegionNavigationService, RegionNavigationService>();                 RegisterRequiredTypes(ContainerExtension);      }      public IContainerExtension ContainerExtension { get; private set; }      protected abstract void RegisterRequiredTypes(IContainerRegistry containerRegistry); }  public class TestInitializer : TestInitializerBase {     [AssemblyInitialize]     public static void InitializeAseemble(TestContext testContext)     {         var testInitializer = new TestInitializer();         testInitializer.Initialize();     }      protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry)     {         containerRegistry.RegisterSingleton<IEventAggregator, MyEventAggregator>();     } } 

这样在 TestInitializer 中可以注册各种方便单元测试的伪对象。