限制事件和锁定方法

时间:2014-02-18 16:47:15

标签: winrt-xaml system.reactive

让我假装我有这样的事情:

<TextBox Text="{Binding Text, Mode=TwoWay}" />

这样的事情:

public class MyViewModel : INotifyPropertyChanged
{
    public MyViewModel()
    {
        // run DoWork() when this.Text changes
        Observable.FromEventPattern<PropertyChangedEventArgs>(this, "PropertyChanged")
            .Where(x => x.EventArgs.PropertyName.Equals("Text"))
            .Subscribe(async x => await DoWork());
    }

    private async Task DoWork()
    {
        await Task.Delay(this.Text.Length * 100);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private string _Text = "Hello World";
    public string Text
    {
        get { return _Text; }
        set
        {
            _Text = value;
            if (this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs("Text"));
        }
    }
}

在这种情况下,用户可能非常快速地打字。我需要:

  1. DoWork()在DoWork()已经运行时不能运行

  2. 用户可以键入spurts,更改,暂停,更改

  3. 每次更改都不需要DoWork(),只需要最后一次更改

  4. 不需要比1秒更频繁地调用DoWork()

  5. DoWork()不能等到最后一次更改,如果突发是&gt; 1秒

  6. 系统空闲时不应调用DoWork()

  7. DoWork()的持续时间因此而变化。文字

  8. 问题不在于Rx能否做到这一点。我知道它可以。什么是正确的语法?

5 个答案:

答案 0 :(得分:3)

你可能会惊讶于它作为纯粹的RX解决方案有多难。它与提交限制搜索以响应文本框更改的类似(以及典型的Rx 101示例)略有不同 - 在这种情况下,可以触发并发搜索,取消除最新搜索之外的所有搜索。

在这种情况下,一旦DoWork()关闭并运行,就无法替换或中断。

问题在于Rx流向一个方向流动而不能“倒退” - 因此事件会与慢速消费者排队。在Rx中,由于消费者放慢而导致事件丢失是非常困难的。

在一个新的(可能受限制的)事件到来时DoWork()可被取消和替换的世界会更容易。

首先,我提出一个纯粹的Rx解决方案。然后在最后,一个更简单的方法,其中缓慢的消费者由Rx之外的调度机制处理。

对于纯方法,您需要使用此辅助扩展方法来删除针对慢速消费者which you can read about here排队的事件:

public static IObservable<T> ObserveLatestOn<T>(
    this IObservable<T> source, IScheduler scheduler)
{
    return Observable.Create<T>(observer =>
    {
        Notification<T> outsideNotification = null;
        var gate = new object();
        bool active = false;

        var cancelable = new MultipleAssignmentDisposable();
        var disposable = source.Materialize().Subscribe(thisNotification =>
        {
            bool wasNotAlreadyActive;
            lock (gate)
            {
                wasNotAlreadyActive = !active;
                active = true;
                outsideNotification = thisNotification;
            }

            if (wasNotAlreadyActive)
            {
                cancelable.Disposable = scheduler.Schedule(self =>
                {
                    Notification<T> localNotification = null;
                    lock (gate)
                    {
                        localNotification = outsideNotification;
                        outsideNotification = null;
                    }
                    localNotification.Accept(observer);
                    bool hasPendingNotification = false;
                    lock (gate)
                    {
                        hasPendingNotification = active = (outsideNotification != null);
                    }
                    if (hasPendingNotification)
                    {
                        self();
                    }
                });
            }
        });
        return new CompositeDisposable(disposable, cancelable);
    });
}

有了这个,您可以执行以下操作:

// run DoWork() when this.Text changes
Observable.FromEventPattern<PropertyChangedEventArgs>(this, "PropertyChanged")
          .Where(x => x.EventArgs.PropertyName.Equals("Text"))
          .Sample(TimeSpan.FromSeconds(1)) // get the latest event in each second
          .ObservableLatestOn(Scheduler.Default) // drop all but the latest event
          .Subscribe(x => DoWork().Wait()); // block to avoid overlap

说明

说实话,你可能最好避开这里的纯Rx解决方案,而不是直接从订阅者那里调用DoWork()。我会用一个调用来自Subscribe方法的中间调度机制来包装它,如果它已经在运行,它会处理不调用它 - 代码将更容易维护。

编辑:

在考虑了几天之后,我没有做到比其他一些答案更好 - 我会留下上面的兴趣,但我想我喜欢Filip Skakun接近最好。

答案 1 :(得分:3)

虽然我有点同意詹姆斯世界,但我认为如果我们只使用一些可变状态,你可以做得更好。如果DoWork看起来像这样:

AsyncSubject<Unit> doingWork;
public IObservable<Unit> DoWork()
{
    if (doingWork != null) return doingWork;

    doingWork = Observable.Start(() => {
        // XXX: Do work
        Thread.Sleep(1000);

        // We only kick off this 1sec timeout *after* we finish doing work
        Observable.Timer(TimeSpan.FromSeconds(1.0), DispatcherScheduler.Instance)
            .Subscribe(_ => doingWork = null);
    });

    return doingWork;
}

现在,DoWork自动退出自动化,我们可以摆脱这种等待订阅的愚蠢行为;我们将油门设置为250毫秒,快速但不太快。

这最初似乎违反了上面的要求#5,但是我们已经确保任何过快调用DoWork的人只能得到之前运行的结果 - 效果将是多次调用DoWork,但不一定任何事情。这确保了,如果我们没有工作,我们将不会在用户停止输入后延迟1秒,如Throttle(1.seconds)

    Observable.FromEventPattern<PropertyChangedEventArgs>(this, "PropertyChanged")
        .Where(x => x.EventArgs.PropertyName.Equals("Text"))
        .Throttle(TimeSpan.FromMilliseconds(250), DispatcherScheduler.Instance)
        .SelectMany(_ => DoWork())
        .Catch<Unit, Exception>(ex => {
            Console.WriteLine("Oh Crap, DoWork failed: {0}", ex);
            return Observable.Empty<Unit>();
        })
        .Subscribe(_ => Console.WriteLine("Did work"));

答案 2 :(得分:3)

我认为解决问题的一种更简单且可重用的方法实际上可能是基于异步/等待而不是基于RX。查看我获得的单线程EventThrottler类实现作为'Is there such a synchronization tool as “single-item-sized async task buffer”?' question的答案。有了它,您可以简单地重写DoWork()方法:

private void DoWork()
{
    EventThrottler.Default.Run(async () =>
    {
        await Task.Delay(1000);
        //do other stuff
    });
}

并在每次文字更改时调用它。不需要RX。此外,如果您已经在使用WinRT XAML Toolkit - 该类为in there

以下是作为快速参考的throttler类代码的副本:

public class EventThrottler
{
    private Func<Task> next = null;
    private bool isRunning = false;

    public async void Run(Func<Task> action)
    {
        if (isRunning)
            next = action;
        else
        {
            isRunning = true;

            try
            {
                await action();

                while (next != null)
                {
                    var nextCopy = next;
                    next = null;
                    await nextCopy();
                }
            }
            finally
            {
                isRunning = false;
            }
        }
    }

    private static Lazy<EventThrottler> defaultInstance =
        new Lazy<EventThrottler>(() => new EventThrottler());
    public static EventThrottler Default
    {
        get { return defaultInstance.Value; }
    }
}

答案 3 :(得分:2)

这是我所拥有的(代码经过测试,顺便说一句)。它基于事件限制扩展I created a few years ago。我认为它的好名字是Ouroboros。 关于它的主要问题是与使用Throttle时相反,如果冷却时间已经过去,它会立即开始工作。

public static IObservable<TResult> CombineVeryLatest<TLeft, TRight, TResult>(
    this IObservable<TLeft> leftSource,
    IObservable<TRight> rightSource, 
    Func<TLeft, TRight, TResult> selector)
{
    return Observable.Defer(() =>
    {
        int l = -1, r = -1;
        return Observable.CombineLatest(
            leftSource.Select(Tuple.Create<TLeft, int>),
            rightSource.Select(Tuple.Create<TRight, int>),
                (x, y) => new { x, y })
            .Where(t => t.x.Item2 != l && t.y.Item2 != r)
            .Do(t => { l = t.x.Item2; r = t.y.Item2; })
            .Select(t => selector(t.x.Item1, t.y.Item1));
    });
}

public static IObservable<TWork> WorkSequencer<T, TWork>(
    this IObservable<T> source, Func<Task<TWork>> work)
{
    return source.Publish(src =>
    {
        var fire = new Subject<T>();
        var fireCompleted = fire.SelectMany(x => work()).Publish();
        fireCompleted.Connect();
        var whenCanFire = fireCompleted.StartWith(default(TWork));

        var subscription = src
            .CombineVeryLatest(whenCanFire, (x, flag) => x)
            .Subscribe(fire);

        return fireCompleted.Finally(subscription.Dispose);
    });
}

然后用法是:

    private int _counter;

    public MainWindow()
    {
        InitializeComponent();
        var clicks = Observable
            .FromEventPattern(TestBn, "Click")
            .Do(_ =>
            {
                Console.WriteLine("click");
                _counter++;
            });
        clicks.WorkSequencer(DoWork).Subscribe();
    }

    private async Task<int> DoWork()
    {
        var workNumber = _counter;
        Console.WriteLine("Work Start " + workNumber);
        await Task.WhenAll(Task.Delay(_counter*100), Task.Delay(1000));
        Console.WriteLine("Work Done " + workNumber);
        return _counter;
    }

答案 4 :(得分:2)

我有一些名为SubscribeWithoutOverlap的组合器,我在UI中用于此目的。丢弃所有传入事件,除了最后一个事件被丢弃,直到工作完成。工作完成后,将要求事件缓冲区进行下一个事件。

    /// <summary>
    /// Subscribe to the observable whilst discarding all events that are
    /// recieved whilst the action is being processed. Can be
    /// used to improve resposiveness of UI's for example 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source"></param>
    /// <param name="action"></param>
    /// <param name="scheduler"></param>
    /// <returns></returns>
    public static IDisposable SubscribeWithoutOverlap<T>
    (this IObservable<T> source, Action<T> action, IScheduler scheduler = null)
    {
        var sampler = new Subject<Unit>();
        scheduler = scheduler ?? Scheduler.Default;
        var p = source.Replay(1);

        var subscription = sampler.Select(x=>p.Take(1))
            .Switch()
            .ObserveOn(scheduler)
            .Subscribe(l =>
            {
                action(l);
                sampler.OnNext(Unit.Default);
            });

        var connection = p.Connect();
        sampler.OnNext(Unit.Default);

        return new CompositeDisposable(connection, subscription);
    }

    public static IDisposable SubscribeWithoutOverlap<T>
    (this IObservable<T> source, Func<T,Task> action, IScheduler scheduler = null)
    {
        var sampler = new Subject<Unit>();
        scheduler = scheduler ?? Scheduler.Default;
        var p = source.Replay(1);

        var subscription = sampler.Select(x=>p.Take(1))
            .Switch()
            .ObserveOn(scheduler)
            .Subscribe(async l =>
            {
                await action(l);
                sampler.OnNext(Unit.Default);
            });

        var connection = p.Connect();
        sampler.OnNext(Unit.Default);

        return new CompositeDisposable(connection, subscription);
    }

所以以下内容应符合您的要求。

IObservable<string> source;

source
   .Throttle(TimeSpan.FromMilliSeconds(100))
   .Merge(source.Sample(TimeSpan.FromSeconds(1))
   .SubscribeWithoutOverlap(DoWork)

请注意Throttle和Sample的混合,以获得问题中要求的两种计时行为。

关于其他一些答案。如果您发现自己将复杂的RX逻辑放入业务逻辑中,则将其提取到具有明确目的的自定义组合器中。稍后当你试图了解它的作用时,你会感谢自己。