Dispatcher.CurrentDispatcher приводит к зависанию вызова ReactiveUI - PullRequest
0 голосов
/ 07 марта 2019

Вызов .Execute() для ReactiveCommand зависает или создает тупик в примере ниже. Почему это происходит, и как лучше всего этого избежать?

Ошибка возникает только при вызове Dispatcher.CurrentDispatcher. Очевидный ответ, чтобы не называть это, к сожалению, не вариант в более крупном проекте.

У меня в проекте есть пакеты nuget реактивные-ядро и реактивные-winforms, оба v7.4.0. Я запускаю тесты nunit из Visual Studio с Resharper.

Код - это тестовое устройство NUnit, обратите внимание, что TimeoutAfterAsync - это вспомогательный метод для отмены теста после определенного времени ожидания, поведение наблюдается без этой оболочки

[TestFixture]
public class ReactiveCommandTests
{
    private static async Task<bool> ExecuteCommand()
    {
        await Task.Delay(1000);
        return true;
    }

    public static ReactiveCommand<Unit, bool> Command = ReactiveCommand.CreateFromTask(ExecuteCommand);
    public static ReactiveCommand<Unit, bool> CommandOnTaskpoolScheduler = ReactiveCommand.CreateFromTask(ExecuteCommand, outputScheduler: RxApp.TaskpoolScheduler);
    public static ReactiveCommand<Unit, bool> CommandAfterDispatcherInvoked = ReactiveCommand.CreateFromTask(ExecuteCommand);



    [Test, Order(1)]
    public async Task Test()
    {
        //THIS WORKS
        try
        {
            await TimeoutAfterAsync(
                Command.Execute(),
                TimeSpan.FromSeconds(5),
                "control");
        }
        catch (TimeoutException)
        {
            Assert.Fail("Control case timed out (not expected)");
        }
    }

    [Test, Order(2)]
    public async Task Test_CreateCommandAfterDispatcherCall()
    {
        //This line causes unwanted behaviour
        var x = Dispatcher.CurrentDispatcher;

        //THIS FAILS
        try
        {
            await TimeoutAfterAsync(
                CommandAfterDispatcherInvoked.Execute(),
                TimeSpan.FromSeconds(5),
                "after dispatcher creation");
        }
        catch (TimeoutException)
        {
            Assert.Fail("Executing commandAfterDispatcherInvoked timed out (expected, but not understood");
        }
    }

    [Test, Order(3)]
    public async Task Test_CreateCommandWithThreadpoolScheduler()
    {
        //This line causes unwanted behaviour
        var x = Dispatcher.CurrentDispatcher;

        //THIS WORKS AGAIN (using ThreadpoolScheduler when creating ReactiveCommand)
        try
        {
            await TimeoutAfterAsync(
                CommandOnTaskpoolScheduler.Execute(),
                TimeSpan.FromSeconds(5),
                "after dispatcher creation, with thread pool");
        }
        catch (TimeoutException)
        {
            Assert.Fail("ThreadpoolScheduler case timed out (not expected)");
        }
    }

    private static async Task<TResult> TimeoutAfterAsync<TResult>(IObservable<TResult> observable,
        TimeSpan timeout,
        string context)
    {
        var task = observable .ToTask();
        var result = await Task.WhenAny(task, Task.Delay(timeout));
        if (result == task)
        {
            // Task completed within timeout.
            return task.GetAwaiter().GetResult();
        }
        else
        {
            // Task timed out.
            throw new TimeoutException(context);
        }
    }
}

1 Ответ

1 голос
/ 07 марта 2019

Dispatcher.CurrentDispatcher забавный;он создает диспетчер для текущего потока, если у него его еще нет!Это создает проблему для модульных тестов, поскольку новый диспетчер создается для потока пула потоков, который не является STA и не имеет обработчика сообщений.

Идеальное решение не звонить CurrentDispatcher.Когда-либо.Используйте await или IProgress<T> или (если необходимо) SynchronizationContext для передачи результатов / хода выполнения / событий в поток пользовательского интерфейса.Эти абстракции намного проще для создания тестовой среды.

Но сейчас вы можете использовать WpfContext , старый тип утилит, который был включен в ранние версии Async CTP.,WpfContext.Run примет делегата, создаст контекст диспетчера для текущего потока и выполнит делегат в этом контексте диспетчера, перекачивая его сообщения до завершения асинхронных операций.

...