在可单元测试的 MVVM 代码中使用 Dispatcher

发布于 2024-10-14 08:34:26 字数 1841 浏览 8 评论 0原文

我有一个 MVVM-lite 应用程序,我希望对其进行单元测试。该模型使用 System.Timers.Timer,因此更新事件最终在后台工作线程上结束。这个单元测试很好,但在运行时抛出 System.NotSupportedException“这种类型的 CollectionView 不支持从与 Dispatcher 线程不同的线程更改其 SourceCollection。”我曾希望 MVVM-lite 类 Threading.DispatcherHelper 能够解决问题,但调用 DispatcherHelper.CheckBeginInvokeOnUI 会导致我的单元测试失败。这是我在视图模型中最终得到的代码,

private void locationChangedHandler(object src, LocationChangedEventArgs e)
{
    if (e.LocationName != this.CurrentPlaceName)
    {
        this.CurrentPlaceName = e.LocationName;
        List<FileInfo> filesTaggedForHere = Tagger.FilesWithTag(this.CurrentPlaceName);

        //This nextline fixes the threading error, but breaks it for unit tests
        //GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(delegate { updateFilesIntendedForHere(filesTaggedForHere); });

        if (Application.Current != null)
        {
            this.dispatcher.Invoke(new Action(delegate { updateFilesIntendedForHere(filesTaggedForHere); }));
        }
        else
        {
            updateFilesIntendedForHere(filesTaggedForHere);
        }
    }
}
private void updateFilesIntendedForHere(List<FileInfo> filesTaggedForHereIn)
{
    this.FilesIntendedForHere.Clear();
    foreach (FileInfo file in filesTaggedForHereIn)
    {
        if (!this.FilesIntendedForHere.Contains(file))
        {
            this.FilesIntendedForHere.Add(file);
        }
    }
}

我在 http://kentb.blogspot.com/2009/04/mvvm-infrastruct-viewmodel.html 但是在单元测试期间对 Dispatcher.CurrentDispatcher 上的 Invoke 的调用未能运行,因此失败。这就是为什么如果运行是在测试而不是应用程序中,我会直接调用帮助器方法。

这是不对的——ViewModel 不应该关心它是从哪里调用的。谁能明白为什么 Kent Boogaart 的调度程序方法和 MVVM-lite DispatcherHelper.CheckBeginInvokeOnUI 在我的单元测试中都不起作用?

I've an MVVM-lite application that I'd like to have unit testable. The model uses a System.Timers.Timer and so the update event ends up on a background worker thread. This unit-tested fine, but at runtime threw the System.NotSupportedException "This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread." I had hoped that the MVVM-lite class Threading.DispatcherHelper would fix things but calling DispatcherHelper.CheckBeginInvokeOnUI results in my unit test failing. Here's the code I've ended up with in the view model

private void locationChangedHandler(object src, LocationChangedEventArgs e)
{
    if (e.LocationName != this.CurrentPlaceName)
    {
        this.CurrentPlaceName = e.LocationName;
        List<FileInfo> filesTaggedForHere = Tagger.FilesWithTag(this.CurrentPlaceName);

        //This nextline fixes the threading error, but breaks it for unit tests
        //GalaSoft.MvvmLight.Threading.DispatcherHelper.CheckBeginInvokeOnUI(delegate { updateFilesIntendedForHere(filesTaggedForHere); });

        if (Application.Current != null)
        {
            this.dispatcher.Invoke(new Action(delegate { updateFilesIntendedForHere(filesTaggedForHere); }));
        }
        else
        {
            updateFilesIntendedForHere(filesTaggedForHere);
        }
    }
}
private void updateFilesIntendedForHere(List<FileInfo> filesTaggedForHereIn)
{
    this.FilesIntendedForHere.Clear();
    foreach (FileInfo file in filesTaggedForHereIn)
    {
        if (!this.FilesIntendedForHere.Contains(file))
        {
            this.FilesIntendedForHere.Add(file);
        }
    }
}

I did try the trick in http://kentb.blogspot.com/2009/04/mvvm-infrastructure-viewmodel.html but the call to Invoke on the Dispatcher.CurrentDispatcher failed to run during the unit test and so it failed. That's why I'm calling the helper method directly if the run is in a test and not an application.

This can't be right - the ViewModel shouldn't care where it's being called from. Can anyone see why neither Kent Boogaart's dispatcher method nor the MVVM-lite DispatcherHelper.CheckBeginInvokeOnUI work in my unit test?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

双马尾 2024-10-21 08:34:26

我这样做:

class MyViewModel() {
    private readonly SynchronizationContext _syncContext;

    public MyViewModel() {
        _syncContext = SynchronizationContext.Current; // or use DI
    )

    ...

    public void SomeTimerEvent() {
        _syncContext.Post(_ => UpdateUi(), null);
    }
}

默认上下文将是测试中的线程池和 UI 中的调度程序。如果您想要一些其他行为,您还可以轻松创建自己的测试上下文。

I do it like this:

class MyViewModel() {
    private readonly SynchronizationContext _syncContext;

    public MyViewModel() {
        _syncContext = SynchronizationContext.Current; // or use DI
    )

    ...

    public void SomeTimerEvent() {
        _syncContext.Post(_ => UpdateUi(), null);
    }
}

The default context will be threadpool in your test and dispatcher in UI. You can also easily create your own test context if you want some other behavior.

向地狱狂奔 2024-10-21 08:34:26

我不相信 MVVM-lite 中有一个简单的答案。您有调用 DispatcherHelper.CheckBeginInvokeOnUI 的正确解决方案。但是,当单元测试运行时,UI不存在,DispatcherHelper将无法正常运行。

我使用 ReactiveUI。它的 DispatcherHelper.CheckBeginInvokeOnUI (RxApp.DeferredScheduler) 版本将检查以确定它是否正在单元测试中运行。如果是,它将在当前线程上运行,而不是尝试编组到不存在的 UI 线程。您也许可以使用此代码在 DispatcherHelper 中构建您自己的检查。相关代码位于 RxApp.cs 方法 InUnitTestRunner() (第 196 行)。这是相当老套的,但它有效,我不认为有更好的方法。

I do not believe there is an easy answer to this in MVVM-lite. You have the right solution of calling DispatcherHelper.CheckBeginInvokeOnUI. But, when the unit test is being run, the UI does not exist, and the DispatcherHelper won't run properly.

I use ReactiveUI. It's version of DispatcherHelper.CheckBeginInvokeOnUI (RxApp.DeferredScheduler) will check to determine if it is being run in a unit test. If it is, it will run on the current thread instead of trying to marshal to the non-existent UI thread. You may be able to use this code to build your own check into DispatcherHelper. The relevant code is in RxApp.cs method InUnitTestRunner() (line 196). It is pretty hacky, but it works and I don't think there is a better way.

梦魇绽荼蘼 2024-10-21 08:34:26

我只是在 ViewModelUnitTestBase 中调用 Initialize 方法,它工作正常。
确保 DispatcherHelper.UIDispatcher 不为 null。

public abstract class ViewModelUnitTestBase<T> where T : ViewModelBase
{
    private T _viewModel = default(T);
    public T ViewModel
    {
        get { return _viewModel; }
        set { _viewModel = value; }
    }

    static ViewModelUnitTestBase()
    {
        DispatcherHelper.Initialize();
    }
}

I just call the Initialize method in my ViewModelUnitTestBase and it works fine.
Make sure that the DispatcherHelper.UIDispatcher is not null.

public abstract class ViewModelUnitTestBase<T> where T : ViewModelBase
{
    private T _viewModel = default(T);
    public T ViewModel
    {
        get { return _viewModel; }
        set { _viewModel = value; }
    }

    static ViewModelUnitTestBase()
    {
        DispatcherHelper.Initialize();
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文