在派生属性中使用调度程序以获得响应式UI的适当方法是什么?

时间:2017-07-17 14:08:24

标签: c# wpf asynchronous scheduler reactiveui

我很难找到适当的方法来安排长时间运行的反应性财产" getters"在我的ViewModel中。

This excerpt from Intro to RX直截了当地描述了我想要做的事情:

  
      
  • 回应某种用户操作
  •   
  • 在后台线程上工作
  •   
  • 将结果传递回UI线程
  •   
  • 更新用户界面
  •   

仅在这种情况下,除了用户交互之外,我想对其他属性的更改作出反应。

下面是我用来从原始属性获取派生属性的通用模板(在实际代码中,有级联派生属性的链)。

在Reactive ViewModel(继承自ReactiveObject)中,我已经拥有了一些来自其他属性的属性。例如,当Original更改时,Derived会重新计算。

    public TOriginal Original
    {
        get { return _original; }
        set { this.RaiseAndSetIfChanged(ref _original, value); }
    }
    TOriginal _original;


    public TDerived Derived { get { return _derived.Value; } }
    readonly ObservableAsPropertyHelper<double[,]> _derived;


    this.WhenAnyValue(x => x.Original)
        .Where(originalValue => originalValue != null)
        // ObserveOn? SubscribeOn? Which scheduler?
        .Select(derivedValue => LongRunningCalculation(originalValue))
        // Same thing here: ObserveOn? SubscribeOn? Which scheduler? 
        .ToProperty(this, x => x.Derived, out _derived); // should I use the `scheduler:` in this method?

我的问题是:我不知道这些不同的设计选择&#34;应该结合使用以获得我想要的响应式UI:

  • 使用哪种调度程序?我看过RxApp.TaskpoolSchedulerRxApp.MainThreadSchedulerNewThreadScheduler.Default和其他可能的例子。
  • 何时使用偶数SubscribeOn的{​​{1}} vs ObserveOnObserveOnDispatcher的{​​{1}}参数?
  • 订单有所不同吗?我已经在scheduler:运算符之前和之后放了重新调度方法,但我不太确定。我坦言,我甚至不确定ToProperty是否需要。
  • 我看过一些将Select设置为Select的示例,但我尝试过并且看起来并没什么区别,但同样,也许是因为其他因素。< / LI>
  • Binding.IsAsynctrue的概念是否相关?有没有办法在显示的代码中配置它们?
  • 我应该使用SynchronizationContext还是其他一些ReactiveUI类?

最令人伤脑筋的事实是,使用一些组合,计算工作正常,但阻止UI,而使用其他一些组合,值是异步计算的,UI有点较少被阻止,但有时 part 的派生值(例如,在项目集合中)不可用!

很抱歉,如果我问得太多,但我没有找到任何权威的预期方式来完成我在文档中所需的工作。

2 个答案:

答案 0 :(得分:2)

Select之前可能会阻止在TaskPoolScheduler上观察UI。 在ToProperty观察MainThreadScheduler之前。

  this.WhenAnyValue(x => x.Original)
        .Where(originalValue => originalValue != null)
        .ObserveOn(TaskPoolScheduler.Default)
        .Select(derivedValue => LongRunningCalculation(originalValue))
        .ObserveOn(RxApp.MainThreadScheduler)
        .ToProperty(this, x => x.Derived, out _derived); 

另外

人们对SubscribeOn实际做了什么非常困惑。有很多解释。例如,如此处的另一个答案所示

  

SubscribeOn将可观察链向上移动到顶部并确保observable在给定的调度程序上生成值

这不是真的。查看RX代码库中SubscribeOn的实现是有益的。你必须跳过几层抽象来实现目标,但最终你会发现。

public static IObservable<TSource> 
    SubscribeOn<TSource>
   ( IObservable<TSource> source
   , IScheduler scheduler
   )
{
  if (source == null)
    throw new ArgumentNullException("source");
  if (scheduler == null)
    throw new ArgumentNullException("scheduler");
  return (IObservable<TSource>) new AnonymousObservable<TSource>((Func<IObserver<TSource>, IDisposable>) (observer =>
  {
    SingleAssignmentDisposable assignmentDisposable = new SingleAssignmentDisposable();
    SerialDisposable d = new SerialDisposable();
    d.Disposable = (IDisposable) assignmentDisposable;
    assignmentDisposable.Disposable = scheduler.Schedule((Action) (() => d.Disposable = (IDisposable) new ScheduledDisposable(scheduler, source.SubscribeSafe<TSource>(observer))));
    return (IDisposable) d;
  }));
}

这样做的唯一方法是确保在指定的调度程序上调用Subscribe上的source方法,并确保同一Dispose返回的Subscribe方法在指定的调度程序上也调用了方法。这对下游代码的影响各不相同。

例如

using System;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace SubscribeOnVsObserveOn
{
    class Program
    {
        static readonly Subject<object> EventsSubject = new Subject<object>();

        private static readonly IObservable<object> Events = Observable.Create<object>
            ( observer =>
            {
                Info( "Subscribing"  );
                return EventsSubject.Subscribe( observer );
            } );

        public static void Info(string msg)
        {
            var currentThread = Thread.CurrentThread;
            var currentThreadName = string.IsNullOrWhiteSpace( currentThread.Name ) ? "<no name>" : currentThread.Name;
            Console.WriteLine
                ( $"Thread Id {currentThread.ManagedThreadId} {currentThreadName} - " + msg );
        }

        public static void  Foo()
        {
            Thread.CurrentThread.Name = "Main Thread";

            Info( "Starting"  );

            void OnNext(object o) => Info( $"Received {o}" );

            void Notify(object obj)
            {
                Info( $"Sending {obj}"  );
                EventsSubject.OnNext( obj );
            }

            void StartAndSend(object o, string threadName)
            {
                var thread = new Thread(Notify);
                thread.Name = threadName;
                thread.Start(o);
                thread.Join();
            }

            Notify(1);

            Console.WriteLine("=============================================" );
            Console.WriteLine("Subscribe Only" );
            Console.WriteLine("=============================================" );
            using (Events.Subscribe(OnNext))
            {
                Thread.Sleep( 200 );
                StartAndSend(2, "A");
                StartAndSend(3, "B");
            }

            Console.WriteLine("=============================================" );
            Console.WriteLine("Subscribe With SubscribeOn(CurrentThreadScheduler)" );
            Console.WriteLine("=============================================" );
            using (Events.SubscribeOn( CurrentThreadScheduler.Instance ).Subscribe(OnNext))
            {
                Thread.Sleep( 200 );
                StartAndSend(2, "A");
                StartAndSend(3, "B");
            }

            Console.WriteLine("=============================================" );
            Console.WriteLine("Subscribe With SubscribeOn(ThreadPool)" );
            Console.WriteLine("=============================================" );
            using (Events.SubscribeOn( ThreadPoolScheduler.Instance ).Subscribe(OnNext))
            {
                Thread.Sleep( 200 );
                StartAndSend(2, "A");
                StartAndSend(3, "B");
            }

            Console.WriteLine("=============================================" );
            Console.WriteLine("Subscribe With SubscribeOn(NewThread)" );
            Console.WriteLine("=============================================" );
            using (Events.SubscribeOn( NewThreadScheduler.Default ).Subscribe(OnNext))
            {
                Thread.Sleep( 200 );
                StartAndSend(2, "A");
                StartAndSend(3, "B");
            }

            Console.WriteLine("=============================================" );
            Console.WriteLine("Subscribe With SubscribeOn(NewThread) + ObserveOn" );
            Console.WriteLine("=============================================" );
            using (Events.SubscribeOn( NewThreadScheduler.Default ).ObserveOn(TaskPoolScheduler.Default  ).Subscribe(OnNext))
            {
                Thread.Sleep( 200 );
                StartAndSend(2, "A");
                StartAndSend(3, "B");
            }
        }




        static void Main(string[] args)
        {
            Foo();
            Console.WriteLine( "Press Any Key" );
            Console.ReadLine();
        }
    }
}

生成以下输出

Thread Id 1 Main Thread - Starting
Thread Id 1 Main Thread - Sending 1
=============================================
Subscribe Only
=============================================
Thread Id 1 Main Thread - Subscribing
Thread Id 4 A - Sending 2
Thread Id 4 A - Received 2
Thread Id 5 B - Sending 3
Thread Id 5 B - Received 3
=============================================
Subscribe With SubscribeOn(CurrentThreadScheduler)
=============================================
Thread Id 1 Main Thread - Subscribing
Thread Id 6 A - Sending 2
Thread Id 6 A - Received 2
Thread Id 7 B - Sending 3
Thread Id 7 B - Received 3
=============================================
Subscribe With SubscribeOn(ThreadPool)
=============================================
Thread Id 8 <no name> - Subscribing
Thread Id 10 A - Sending 2
Thread Id 10 A - Received 2
Thread Id 11 B - Sending 3
Thread Id 11 B - Received 3
=============================================
Subscribe With SubscribeOn(NewThread)
=============================================
Thread Id 12 <no name> - Subscribing
Thread Id 13 A - Sending 2
Thread Id 13 A - Received 2
Thread Id 14 B - Sending 3
Thread Id 14 B - Received 3
=============================================
Subscribe With SubscribeOn(NewThread) + ObserveOn
=============================================
Thread Id 16 <no name> - Subscribing
Thread Id 17 A - Sending 2
Thread Id 19 B - Sending 3
Thread Id 18 <no name> - Received 2
Thread Id 18 <no name> - Received 3
Press Any Key

值得注意的是,SubscribeOn既不能强制发送或接收事件在特定的调度程序上。它只能强制Subscribe方法在特定的调度程序上发生。 可能可能具有下游/上游效果。

答案 1 :(得分:1)

的调度程序

在Rx.NET中有一些调度程序,包括一个专用于WPF的调度程序。

  • TaskPoolScheduler在任务池上运行代码。这有点像在Task内运行代码。
  • NewThreadScheduler生成一个新线程来运行代码。通常不要使用此运算符,除非您知道“需要”它(您几乎从不这样做)
  • DispatcherScheduler在UI线程上运行代码。当您要在VM中设置属性时使用此选项

RxUI带来两个平台无关的调度程序抽象。无论你使用什么平台(WPF,UWP,Xamarin.iOS,Xamarin.Android)RxApp.MainThreadScheduler将始终引用UI线程调度程序,而RxApp.TaskPoolScheduler将引用类似于后台线程的东西

如果您想保持简单,只需使用RxApp调度程序;用于UI内容的RxApp.MainThreadScheduler和用于后台/重型内容的RxApp.TaskPoolScheduler

ObserveOn / SubscribeOn

名称SubscribeOn()有点令人困惑,因为它不会直接影响Subscribe()方法。 SubscribeOn()决定observable将从哪个调度程序开始;将在哪个调度程序上完成原始/第一个订阅(不是Subscribe()方法将在哪个调度程序上执行)。我喜欢认为SubsribeOn()将可观察链向上移动到顶部并确保observable在给定的调度程序上生成值。

有些运营商允许您指定应运行哪个调度程序。当他们这样做时,你应该总是喜欢传递一个调度程序,这样你就知道他们要去哪里工作并防止他们潜在地阻止他们(虽然他们不应该)。 SubsribeOn()对于不能让您指定调度程序的可观察对象来说是一种“黑客”。如果使用SubscribeOn(),但运算符指定了调度程序,则运算符调度程序将发出来自运算符的信号,而不是SubscribeOn()中指定的运算符调度程序。

ObserveOn()SubscribeOn()的功能大致相同,但它“从此时开始”。 ObserveOn()之后的运算符和代码将在给ObserveOn()的调度程序上执行。我喜欢认为ObserveOn()意味着“改变这个线程”。

做大量工作

如果你要做繁重的工作,把它放在一个函数中并调用该函数,就像你用LongRunningCalculation()所做的那样。您可以在ObserveOn(RxApp.TaskPoolScheduler)之前使用Select(),在ObserveOn(RxApp.MainThreadScheduler之后使用Observable.Start(),但我更喜欢将SelectMany()Observable.Start()结合使用。

对于函数,

Observable.Return()基本上是SelectMany():“将此函数的结果作为可观察对象给出。”您还可以指定它应该调用函数的调度程序。

await确保我们得到observable的结果,而不是observable本身。 (它有点像{oberables的WhenAnyValue():“在我们得到这个可观察的结果之前不要执行这个下一个运算符”)

派生属性

您正在执行派生属性。

使用ToProperty()获取属性和管道更改为INotifyPropertyChanged。你放在它们之间的运算符可以在后台线程上工作延迟派生属性的设置,但这就是我们public TOriginal Original { get { return _original; } set { this.RaiseAndSetIfChanged(ref _original, value); } } TOriginal _original; public TDerived Derived { get { return _derived.Value; } } readonly ObservableAsPropertyHelper<double[,]> _derived; _derived = this.WhenAnyValue(x => x.Original) .Where(originalValue => originalValue != null) // Sepcify the scheduler to the operator directly .SelectMany(originalValue => Observable.Start( () => LongRunningCalculation(originalValue), RxApp.TaskPoolScheduler)) .ObserveOn(RxApp.MainThreadScheduler) // I prefer this overload of ToProperty, which returns an ObservableAsPropertyHelper .ToProperty(this, x => x.Derived); 的原因。

我的选择

以下是我将如何实现您的具体示例:

-- ==============================
-- Create External Table Template
-- ==============================
USE <database_name, sysname, AdventureWorks>
GO

IF OBJECT_ID('<schema_name, sysname, dbo>.<table_name, sysname, sample_external_table>', 'U') IS NOT NULL
DROP EXTERNAL TABLE <schema_name, sysname, dbo>.<table_name, sysname, sample_external_table>
GO

 CREATE EXTERNAL TABLE <schema_name, sysname, dbo>.<table_name, sysname, sample_external_table>
(
<column1_name, sysname, c1> <column1_datatype, , int> <column1_nullability, , NOT NULL>,
<column2_name, sysname, c2> <column2_datatype, , char(10)> <column2_nullability, , NULL>,
<column3_name, sysname, c3> <column3_datatype, , datetime> <column3_nullability, , NULL>
)
WITH
(
LOCATION = N'<location, nvarchar(3000), sample_location>',
DATA_SOURCE = <data_source_name, sysname, sample_data_source>,
FILE_FORMAT = <file_format_name, sysname, sample_file_format>,
REJECT_TYPE = <reject_type, nvarchar(20), sample_reject_type>,
REJECT_VALUE = <reject_value, float, sample_reject_value>,
REJECT_SAMPLE_VALUE = <reject_sample_value, float, 
sample_reject_sample_value>
)
GO

我们有一个Slack团队为ReactiveUI欢迎您加入。您可以点击here

来要求邀请