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 在 PubSubEvent 的 Subscribe 函式中拋出:
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 的話可以參考 PrismApplicationBase 和 PrismInitializationExtensions 寫一個初始化類,大概差不多這樣(簡化了部分代碼):
[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 中可以注冊各種方便單元測驗的偽物件,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/251022.html
標籤:WPF
