В модели представления ReactiveUI / WPF, как правильно планировать OAPH для наблюдаемых, заполненных другими потоками? - PullRequest
2 голосов
/ 26 октября 2019

У меня есть модели представления ReactiveUI, созданные в начальном потоке приложения WPF, где свойства представляют собой представления с поддержкой ObservableAsPropertyHelper для наблюдаемых объектов, передаваемых событиями из отдельного потока Task. Как правильно настроить планировщики OAPH, чтобы избежать проблем с многопоточностью, поддерживая тестируемость, не слишком загружая поток UI и не усложняя модели представления?

Чтобы дать более подробную информацию:недавно выбрал ReactiveUI в качестве основы для построения связующего кода MVVM в стороннем проекте WPF .NET Core 3, над которым я работал. Это клиент для системы воспроизведения аудио клиент / сервер, и большинство обновлений пользовательского интерфейса происходит от IObservable s, которые в конечном итоге присоединяются к событиям (используя FromEventPattern), которые запускаются в считывателе Task, вращающемся по TCP-соединению,(Является ли это хорошей идеей, я, вероятно, уйду к другому вопросу.)

Мои ViewModels, как правило, делают это:

  • Примите один из вышеперечисленных IObservables в конструкторе;
  • Привязать несколько преобразований LINQ наблюдаемого к свойствам, используя ObservableAsPropertyHelper, до .ToProperty;
  • Привязать еще несколько проекций этих свойств, используя комбинацию.WhenAnyValue и .ToProperty.

Поскольку я еще не купил систему привязки и активации ReactiveUI, ViewModels создаются (как я полагаю) в том же потоке, что и код App.xaml. и все их OAPH инициализируются в конструкторе. Все обращения к свойствам ViewModel осуществляются через односторонние привязки XAML.

Просто для пояснения, модели представлений находятся в одном проекте с ReactiveUI, но , а не ReactiveUI.WPF и т. Д. ;само приложение WPF имеет установленное ReactiveUI.WPF.

Я пробовал несколько различных подходов к подаче планировщиков на вызовы ToProperty и их планировщикам предоставления:

  1. Не перекрывает никаких планировщиков вообще. Сначала я подумал, что это будет правильно, так как документация по ObservableAsPropertyHelper не содержит никаких переопределений. Это завершается с ошибкой типа «невозможно изменить объект, потому что это не поток, который его создал», предположительно потому, что уведомления об изменении свойства происходят в потоке читателя Task.
  2. Переопределение планировщика OAPHприсоединение к считывателю Task, наблюдаемого с scheduler: RxApp.MainThreadScheduler в его ToProperty, после чтения в различных местах, что OAPH по умолчанию использует планировщик текущего потока. . Это исправило аварийные исключения, но, когда я попытался выполнить модульное тестирование модели представления, я начал получать странные расписания Heisenbugs, где изменения, передаваемые через наблюдаемое обновление сервера, не распространялись на WhenAnyValue производные свойства.
  3. Переопределение планировщика других OAPH тоже, что не помогло (но, возможно, немного смутило сбои). На этом этапе я был сбит с толку, потому что документация ReactiveUI предполагает, что RxApp.MainThreadScheduler должен быть незамедлительным, когда он обнаруживает модульные тесты , и поэтому все должно быть в одном потоке и (мы надеемся) работать последовательно.
  4. Нажав на scheduler в конструкторе модели представления ( в соответствии с этим конструктором из чужого проекта ), и, в частности, установив его в Immediate в модульном тесте (и RxApp.MainThreadSchedulerв другом месте).

Вот несколько упрощенная версия кода, с которым я работал, после шага 4:

// Represents the 'play/pause/stop' controls on an audio playback UI.
// The UI connects through an IObservable to a message feed coming from
// a playback server.
public class PlayerTransportViewModel : ReactiveObject
{
    // Holds the current 'state' of the audio being played (play/pause/stop). 
    private readonly ObservableAsPropertyHelper<PlaybackState> _state;
    public PlaybackState State => _state.Value;

    // For the view's convenience, we break out 'state' into three separate Booleans:
    private readonly ObservableAsPropertyHelper<bool> _isPlaying;
    private readonly ObservableAsPropertyHelper<bool> _isPaused;
    private readonly ObservableAsPropertyHelper<bool> _isStopped;
    public bool IsPlaying => _isPlaying.Value;
    // ... and so on for _isPaused and _isStopped.

    // The constructor then takes an observable over some raw feed of server messages.
    public PlayerTransportViewModel(IObservable<PlaybackEvent> events, IScheduler? scheduler)
    {
        // This glue *shouldn't* introduce asynchronicity into the observable.
        var states = SomeLinqGlueSelectingPlaybackStatesFrom(events);
        // This binding directly takes in an observable fed by a message decoding thread.
        // Is the explicit scheduler needed?
        _state = states.ToProperty(this, x => x.State, PlaybackState.Stopped, scheduler: scheduler);

        // These bindings are derived from State by WhenAny.
        // Is the explicit scheduler needed?
        _isPlaying = this.WhenAnyValue(x => x.State, x => x == PlaybackState.Playing).ToProperty(this, x => x.IsPlaying, scheduler: scheduler);
        // ... and so on for _isPaused and _isStopped.
    }
}

Этот код теперь работает нормально, ноЯ чувствую, что я обработал груз, не полностью понимая, каким должен быть правильный минимальный уровень вмешательства планировщика, или даже просто исправляю ли я проблемы в другом месте программы. (У меня есть некоторые странные экземпляры Heisenbug с другими частями того же проекта, но я еще не связал их с задаваемыми вопросами, так что это я начинаю с фундаментального пробела в моих знаниях).

...