using System; using System.Diagnostics; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using ReactiveUI; using ReactiveUI.Fody.Helpers; namespace MyTests; [TestClass] public class ObserveOnTests { /// /// The scenario is that there is a business object which updates its properties /// on background threads, a view model which observes the business object's /// properties, and a view which binds to the view model. The view must be /// updated on the UI thread only. /// The business object has two properties, C1 and C2. The view model observes /// C1 on the UI thread. If it also observes C2, but on a background thread, /// the observer for C1 will sometimes run on a background thread. /// [TestMethod] //[Ignore("Characterization test")] [DataRow(true, true)] [DataRow(true, false)] [DataRow(false, false)] public async Task MultipleObserverThreadConfusion(bool observeC2, bool observeC2OnMainThread) { // For convenience, construct a thread to be the "UI thread" and point a scheduler at it. Thread mainThread = null; using var scheduler = new EventLoopScheduler(ts => mainThread = new Thread(ts) { Name = "EventThread" }); RxApp.MainThreadScheduler = scheduler; var businessObject = new BusinessObject(); var viewModel = new ViewModel(businessObject, observeC2, observeC2OnMainThread); var observedCount = 0; scheduler.Schedule(() => { viewModel.WhenAnyValue(vm => vm.Counter1) .Subscribe(count => { if (mainThread != Thread.CurrentThread) Debugger.Break(); Assert.AreSame(mainThread, Thread.CurrentThread, $"Counter1 was updated to {count} on a background thread."); observedCount = count; }); }); const int finalCount = 10000; await Task.Run(() => businessObject.CountTo(finalCount)); while (observedCount < finalCount) await Task.Delay(10); } private class ViewModel : ReactiveObject { public ViewModel(BusinessObject component, bool observeC2, bool observeC2OnMainThread) { Component = component; _counter1 = this.WhenAnyValue(vm => vm.Component.Counter) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, vm => vm.Counter1); if (observeC2) _counter2 = this.WhenAnyValue(vm => vm.Component.Counter) .ObserveOn(observeC2OnMainThread ? RxApp.MainThreadScheduler : CurrentThreadScheduler.Instance) .ToProperty(this, vm => vm.Counter2); else _counter2 = ObservableAsPropertyHelper.Default(); } public int Counter1 => _counter1.Value; private readonly ObservableAsPropertyHelper _counter1; public int Counter2 => _counter2.Value; private readonly ObservableAsPropertyHelper _counter2; private BusinessObject Component { get; } } private class BusinessObject : ReactiveObject { [Reactive] public int Counter { get; private set; } // Repeated updates to perform on a background thread. public void CountTo(int stopCount) { while (Counter < stopCount) { Counter++; } } } }