在运行测试时,Dispatcher.Invoke(Action) 会阻塞

编程语言 2026-07-10

编辑
尽管我很想把这个问题解决掉,但推荐的解决方案对我不起作用。相信我,我已经试过了。我的测试一直在转圈,一点反应也没有。我用过推荐的Push Frame方法,但它仍然不起作用。这意味着,测试现在在PushFrame调用处挂起。
Here is how my test looks like:

    [Fact]
    public void OpenPersonDetailsAndChangeTheirNameTest()
    {
        //A
        var person = new Mock<IPersonViewModel>();
        var pocoPersonMock = new Mock<POCO_Person>();
        var windowServiceMoq = new Mock<IWindowService>();
        var viewModelRepositoryMock = new Mock<IViewModelRepository>();
        var containerRepositoryMock = new Mock<IContainerRepository>();
        var providerMock = new Mock<IContainerProvider>();
        var dispatcherServiceMock = new Mock<IDispatcherService>();
        var designerServiceMock = new Mock<IDesignerService>();
        var dispatcher = Dispatcher.CurrentDispatcher;
        var dispatcherFrame = new DispatcherFrame();
        RestMoq = new Mock<Models.Generated.Models>("", new System.Net.Http.HttpClient());

        containerRepositoryMock.Setup(c => c.GetContainer()).Returns(providerMock.Object);
        providerMock.Setup(p => p.Resolve(typeof(IWindowService))).Returns(windowServiceMoq.Object);
        providerMock.Setup(p => p.Resolve(typeof(IViewModelRepository))).Returns(viewModelRepositoryMock.Object);
        providerMock.Setup(p => p.Resolve(typeof(IPersonViewModel))).Returns(person.Object);
        viewModelRepositoryMock.Setup(v => v.GetViewModel<IPersonViewModel>()).Returns(person.Object);
        windowServiceMoq.Setup(s => s.Show(It.IsAny<IBaseViewModel>())).Verifiable();
        designerServiceMock.Setup(d => d.InDesignMode()).Returns(true);
        person.Setup(p => p.FirstName).Returns("Test First Name");
        RestMoq
            .Setup(s => s.PeopleAllAsync())
            .ReturnsAsync(
            () =>
                new List<POCO_Person>
                {
                    pocoPersonMock.Object
                }
            );
        dispatcherServiceMock.Setup(d => d.GetDispatcher()).Returns(dispatcher);

        var sut = new MainViewModel(RestMoq.Object, windowServiceMoq.Object, dispatcherServiceMock.Object, designerServiceMock.Object, viewModelRepositoryMock.Object, containerRepositoryMock.Object);

        //A
        //populate people
        if (sut.PopulatePeopleCommand.CanExecute(null))
        {
            sut.PopulatePeopleCommand.Execute(null);
        }

        Dispatcher.PushFrame(dispatcherFrame);
        sut.SelectedPerson = sut.People.FirstOrDefault();

        if (sut.OpenPersonDetailsCommand.CanExecute(person.Object))
        {
            sut.OpenPersonDetailsCommand.Execute(person.Object);
        }

        //A
        windowServiceMoq.Verify(s => s.Show(It.IsAny<IBaseViewModel>()), Times.Once());            
        providerMock.Verify(p => p.Resolve(typeof(IWindowService)), Times.AtLeastOnce());
    }

我正在开发一个WPF应用,使用.NET 8.0,并且正在为它编写测试。应用和测试项目都使用.NET 8.0。现在在调度程序(Dispatcher)挂起并最终在运行测试时任务被取消时,我遇到了一个问题。我有一个IoC并且使用Prism注入我的服务。在我的BaseViewModel中,我有一个在UI线程上运行代码的方法,恰当地命名为 UI(),它看起来像这样:

        public void UI(Action action)
        {
            Dispatcher.Invoke(action);
        }

无论是在调试还是发布模式下运行应用,这个方法都能工作。这里是调度程序是如何创建的:

        public Dispatcher GetDispatcher()
        {
            return Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
        }

如你所见,这个实现考虑了应用不存在的情况,我以为找到了一个可行的方案。然而,当我用它来填充一个可观察集合时,在测试中却会一直挂起,而在运行或调试时却没问题。我束手无策,想知道在使用非应用调度程序时是否需要改为Push Frames。这里是它在命令中的使用方式:

        private async void PopulatePeople()
        {
            Progress = 0;
            IsBusy = true;
            await Task.Run(async () =>
            {
                ShowToastMessage("About to get a load of records");
                var restPeople = await peopleRestService.PeopleAllAsync();
                ShowToastMessage("Got a load of records");

                List<IBaseViewModel> localList = new List<IBaseViewModel>();

                var pvm = Container.Resolve<IPersonViewModel>();
                pvm.Person = pe;
                pvm.Title = pe.Name;
                pvm.RestPeople = new ObservableCollection<RestPerson>(restPeople.Select(p => new RestPerson(p)));

                localList.Add(pvm);
                GenerateExpensiveViewModels(numberOfPeople, maxVmIndex, vmIndex, localList, restPeople);
                ShowToastMessage("Got them all!");
                UI(() => People = new ObservableCollection<IBaseViewModel>(localList));// <--- this line is causing issue
            });
            IsBusy = false;
        }

我看不出这为什么会不起作用,但话又说回来,我对Dispatcher的了解也不够深入,无法猜测原因。任何建议都非常感谢,谢谢
编辑2
以下是我的DelegateCommand的实现方式:

public class DelegateCommand : IDelegateCommand
{
    public event EventHandler CanExecuteChanged;

    Action _execute;
    Func<bool> _canExecute;

    public DelegateCommand(Action action, Func<bool> canExecute = null)
    {
        _execute = action;
        _canExecute = canExecute ?? (() => true);
    }

    public bool CanExecute(object parameter)
    {
        return _canExecute.Invoke();
    }

    public void Execute(object parameter)
    {
        _execute?.Invoke();
    }

    public void Refresh()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

以及接口:

public interface IDelegateCommand : ICommand
{
    void Refresh();
}

解决方案

经过一番排查并试过Dispatcher.Yield() 等方法后,仍然没有效果,也就是执行了代码,但在调用UI(Action) 方法后从不返回。
这让我思考,主应用是基于Dispatcher的,它会旋转并向消息泵推送消息,但只要应用在运行,它就永远不会真正退出。由于我的测试在UI上没有任何展示,调度程序就像平常一样工作,呆在那里自旋。因此现在我要么重复应用程序的做法,要么在完成后弃用调度程序。这并不容易,因为调度程序就放在那里,只要我的调用是同步的,它们就永远不会被执行。
一个任务来救场。当我启动测试时,我也会启动一个任务,它会自旋并检查我的测试条件。在这种情况下,是视图模型中的People集合。看起来大致是这样的:

//create frame for our dispatcher
var dispatcherFrame = new DispatcherFrame(true);  
//create a task to monitor criteria
var task = Task.Factory.StartNew(() => DispatcherFrame_Continue(sut, dispatcherFrame));
//... test code in here

//method for breaking out of dispatcher loop
void DispatcherFrame_Continue(MainViewModel vm, DispatcherFrame frame)
{
    bool breakOut = false;
    while (!breakOut)
    {
        if (vm.People != null && vm.People.Count > 0)
        {
              breakOut = true;
        }
        Thread.Sleep(200);
    }

    frame.Continue = false;
}

这个方案对我而言效果最佳,因为在使用调度程序时不需要对现有代码做修改,但会让测试变得有点冗长和复杂。因为这些条件会随着测试而变化,方法还得针对每个测试进行适配。也许有更好的解决方案,但我目前找不到。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章