Я написал класс C #, который находится между пользовательским интерфейсом приложения WPF и системой обмена сообщениями Async. Сейчас я пишу модульные тесты для этого класса и сталкиваюсь с проблемами с диспетчером. Следующий метод принадлежит к тестируемому мной классу и создает обработчик подписки. Поэтому я вызываю этот метод Set_Listing_Records_ResponseHandler из Модульного теста - Метод тестирования.

public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
    // Subscribe to Query Response Channel and Wire up Handler for Query Response
    await this.ConnectAsync();
    return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
        {
            try
            {
                ...
            }
            catch (Exception e)
            {
                ...
            }
        }));
    }));
}

Поток выполнения возвращается к строке Application.Current.Dispatcher ...., но затем выдает ошибку:

Object reference not set to an instance of an object.

Когда я отлаживаю, я вижу, что Application.Current имеет значение null.

Я немного поискал и нашел несколько примеров использования диспетчера в модульном тесте - метод тестирования, и я попробовал некоторые из них, и они предотвращают ошибку, но код в диспетчере никогда не запускается.

Мне не удалось найти никаких примеров, когда в методе, который вызывает метод тестирования, используется диспетчер.

Я работаю в .NET 4.5.2 на машине с Windows 10.

Будем очень благодарны любой помощи.

Спасибо за ваше время.

0
user2109254 26 Ноя 2016 в 14:50

2 ответа

Лучший ответ

Спасибо всем, кто ответил, мы очень ценим ваш отзыв.

Вот что я в итоге сделал:

Я создал частную переменную в своем классе UICommsManager:

private SynchronizationContext _MessageHandlerContext = null;

И инициализировал это в конструкторе UICommsManager. Затем я обновил свои обработчики сообщений, чтобы использовать новый _MessageHandlerContext вместо Dispatcher:

public async Task<bool> Set_Listing_Records_ResponseHandler(string responseChannelSuffix, Action<List<AIDataSetListItem>> successHandler, Action<Exception> errorHandler)
{
    // Subscribe to Query Response Channel and Wire up Handler for Query Response
    await this.ConnectAsync();
    return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
        _MessageHandlerContext.Post(delegate {
            try
            {
                ...
            }
            catch (Exception e)
            {
                ...
            }
        }, null);
    }));
}

При использовании из пользовательского интерфейса класс UICommsManager получает SynchronizationContext.Current, переданный в конструктор.

Я обновил свой модульный тест, добавив следующую частную переменную:

private SynchronizationContext _Context = null;

И следующий метод, который его инициализирует:

#region Test Class Initialize

[TestInitialize]
public void TestInit()
{
    SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
    _Context = SynchronizationContext.Current;
}

#endregion

А затем UICommsManager получает переменную _Context, переданную в его конструктор из метода тестирования.

Теперь он работает в обоих сценариях, когда вызывается WPF и когда вызывается модульным тестом.

0
user2109254 28 Ноя 2016 в 05:56

Правильный современный код никогда не будет использовать Dispatcher. Он привязывает вас к конкретному пользовательскому интерфейсу и затрудняет модульное тестирование (как вы обнаружили).

Лучший подход - неявно захватить контекст и возобновить его, используя await. Например, если вы знаете, что Set_Listing_Records_ResponseHandler всегда будет вызываться из потока пользовательского интерфейса, вы можете сделать что-то вроде этого:

public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
  // Subscribe to Query Response Channel and Wire up Handler for Query Response
  await this.ConnectAsync();
  var tcs = new TaskCompletionSource<FayeMessage>();
  await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(
      (client, message) => tcs.TrySetResult(message)));
  var message = await tcs.Task;
  // We are already back on the UI thread here; no need for Dispatcher.
  try
  {
    ...
  }
  catch (Exception e)
  {
    ...
  }
}

Однако если вы имеете дело с системой типа событий, в которой уведомления могут приходить неожиданно, вы не всегда можете использовать неявный захват await. В этом случае следующий лучший подход состоит в том, чтобы захватить текущий SynchronizationContext в какой-то момент (например, при создании объекта) и поставить свою работу в очередь к нему, а не напрямую диспетчеру. Например.,

private readonly SynchronizationContext _context;
public Constructor() // Called from UI thread
{
  _context = SynchronizationContext.Current;
}
public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
  // Subscribe to Query Response Channel and Wire up Handler for Query Response
  await this.ConnectAsync();
  return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
    _context.Post(new SendOrPostCallback(() =>
    {
        try
        {
            ...
        }
        catch (Exception e)
        {
            ...
        }
    }, null));
  }));
}

Однако, если вы чувствуете, что должны использовать диспетчер (или просто не хотите очищать код прямо сейчас), вы можете использовать WpfContext, который предоставляет реализацию Dispatcher для модульного тестирования. Обратите внимание, что вы по-прежнему не можете использовать Application.Current.Dispatcher (если вы не используете MSFakes) - вместо этого в какой-то момент вам придется захватить диспетчер.

0
Stephen Cleary 28 Ноя 2016 в 00:40