У меня есть модели представления ReactiveUI, созданные в начальном потоке приложения WPF, где свойства представляют собой представления с поддержкой ObservableAsPropertyHelper
для наблюдаемых объектов, передаваемых событиями из отдельного потока Task
. Как правильно настроить планировщики OAPH, чтобы избежать проблем с многопоточностью, поддерживая тестируемость, не слишком загружая поток UI и не усложняя модели представления?
Чтобы дать более подробную информацию:недавно выбрал ReactiveUI в качестве основы для построения связующего кода MVVM в стороннем проекте WPF .NET Core 3, над которым я работал. Это клиент для системы воспроизведения аудио клиент / сервер, и большинство обновлений пользовательского интерфейса происходит от IObservable
s, которые в конечном итоге присоединяются к событиям (используя FromEventPattern
), которые запускаются в считывателе Task
, вращающемся по TCP-соединению,(Является ли это хорошей идеей, я, вероятно, уйду к другому вопросу.)
Мои ViewModels, как правило, делают это:
- Примите один из вышеперечисленных
IObservable
s в конструкторе; - Привязать несколько преобразований LINQ наблюдаемого к свойствам, используя
ObservableAsPropertyHelper
, до .ToProperty
; - Привязать еще несколько проекций этих свойств, используя комбинацию
.WhenAnyValue
и .ToProperty
.
Поскольку я еще не купил систему привязки и активации ReactiveUI, ViewModels создаются (как я полагаю) в том же потоке, что и код App.xaml
. и все их OAPH инициализируются в конструкторе. Все обращения к свойствам ViewModel осуществляются через односторонние привязки XAML.
Просто для пояснения, модели представлений находятся в одном проекте с ReactiveUI
, но , а не ReactiveUI.WPF
и т. Д. ;само приложение WPF имеет установленное ReactiveUI.WPF
.
Я пробовал несколько различных подходов к подаче планировщиков на вызовы ToProperty
и их планировщикам предоставления:
- Не перекрывает никаких планировщиков вообще. Сначала я подумал, что это будет правильно, так как документация по
ObservableAsPropertyHelper
не содержит никаких переопределений. Это завершается с ошибкой типа «невозможно изменить объект, потому что это не поток, который его создал», предположительно потому, что уведомления об изменении свойства происходят в потоке читателя Task
. - Переопределение планировщика OAPHприсоединение к считывателю
Task
, наблюдаемого с scheduler: RxApp.MainThreadScheduler
в его ToProperty
, после чтения в различных местах, что OAPH по умолчанию использует планировщик текущего потока. . Это исправило аварийные исключения, но, когда я попытался выполнить модульное тестирование модели представления, я начал получать странные расписания Heisenbugs, где изменения, передаваемые через наблюдаемое обновление сервера, не распространялись на WhenAnyValue
производные свойства. - Переопределение планировщика других OAPH тоже, что не помогло (но, возможно, немного смутило сбои). На этом этапе я был сбит с толку, потому что документация ReactiveUI предполагает, что
RxApp.MainThreadScheduler
должен быть незамедлительным, когда он обнаруживает модульные тесты , и поэтому все должно быть в одном потоке и (мы надеемся) работать последовательно. - Нажав на
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 с другими частями того же проекта, но я еще не связал их с задаваемыми вопросами, так что это я начинаю с фундаментального пробела в моих знаниях).