我有以下设置
IObservable<Data> source = ...;
source
.Select(data=>VeryExpensiveOperation(data))
.Subscribe(data=>Console.WriteLine(data));
通常,事件会在合理的时间范围内分开。
想象一下,用户更新表单中的文本框。我们的VeryExpensiveOperation
可能需要5秒才能完成,而它需要一小时玻璃
显示在屏幕上。
但是,如果在5秒钟内用户再次更新文本框
我想要取消当前的VeryExpensiveOperation
在新的开始之前。
我会想象像
这样的场景source
.SelectWithCancel((data, cancelToken)=>VeryExpensiveOperation(data, token))
.Subscribe(data=>Console.WriteLine(data));
因此每次调用lambda时都会使用cancelToken调用
用于管理取消Task
。但是现在我们正在混合Task,CancelationToken和RX。
不太确定如何将它们整合在一起。任何建议。
奖励积分,用于确定如何使用XUnit测试运营商:)
第一次尝试
public static IObservable<U> SelectWithCancelation<T, U>( this IObservable<T> This, Func<CancellationToken, T, Task<U>> fn )
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
return This
.ObserveOn(Scheduler.Default)
.Select(v=>{
tokenSource.Cancel();
tokenSource=new CancellationTokenSource();
return new {tokenSource.Token, v};
})
.SelectMany(o=>Observable.FromAsync(()=>fn(o.Token, o.v)));
}
尚未测试。我希望一个没有完成的任务生成一个IObservable,它完成时不会触发任何OnNext
个事件。
答案 0 :(得分:7)
您必须将VeryExpensiveOperation
建模为可取消的异步事物。 Task
或IObservable
。我认为这是一项CancellationToken
:
Task<TResult> VeryExpensiveOperationAsync<TSource, TResult>(TSource item, CancellationToken token);
然后你这样做:
source
.Select(item => Observable.DeferAsync(async token =>
{
// do not yield the observable until after the operation is completed
// (ie do not just do VeryExpensiveOperation(...).ToObservable())
// because DeferAsync() will dispose of the token source as soon
// as you provide the observable (instead of when the observable completes)
var result = await VeryExpensiveOperationAsync(item, token);
return Observable.Return(result);
})
.Switch();
Select
只会创建一个延迟的observable,当订阅时,它将创建一个令牌并启动操作。如果在操作完成之前取消订阅了observable,则令牌将被取消。
Switch
订阅了来自Select
的每个新的observable,取消订阅它所订阅的先前的观察结果。
这有你想要的效果。
P.S。这很容易测试。只需提供一个模拟源和一个使用单元测试提供的VeryExpensiveOperation
的模拟TaskCompletetionSource
,这样单元测试就可以准确控制何时生成新的源项目以及何时完成任务。像这样:
void SomeTest()
{
// create a test source where the values are how long
// the mock operation should wait to do its work.
var source = _testScheduler.CreateColdObservable<int>(...);
// records the actions (whether they completed or canceled)
List<bool> mockActionsCompleted = new List<bool>();
var resultStream = source.SelectWithCancellation((token, delay) =>
{
var tcs = new TaskCompletionSource<string>();
var tokenRegistration = new SingleAssignmentDisposable();
// schedule an action to complete the task
var d = _testScheduler.ScheduleRelative(delay, () =>
{
mockActionsCompleted.Add(true);
tcs.SetResult("done " + delay);
// stop listening to the token
tokenRegistration.Dispose();
});
// listen to the token and cancel the task if the token signals
tokenRegistration.Disposable = token.Register(() =>
{
mockActionsCompleted.Add(false);
tcs.TrySetCancelled();
// cancel the scheduled task
d.Dispose();
});
return tcs.Task;
});
// subscribe to resultStream
// start the scheduler
// assert the mockActionsCompleted has the correct sequence
// assert the results observed were what you expected.
}
由于动态安排的新操作,您可能会因使用testScheduler.Start()
而遇到问题。使用testScheduler.AdvanceBy(1)
的while循环可能会更好。
答案 1 :(得分:0)
为什么不使用Throttle?
http://rxwiki.wikidot.com/101samples#toc30
Throttle会停止事件流,直到在指定的时间段内不再生成任何事件。例如,如果将文本框的TextChanged事件限制为.5秒,则在用户停止键入.5秒之前不会传递任何事件。这在您不希望在每次击键后开始新搜索但希望等到用户暂停的搜索框中非常有用。
SearchTextChangedObservable = Observable.FromEventPattern<TextChangedEventArgs>(this.textBox, "TextChanged");
_currentSubscription = SearchTextChangedObservable.Throttle(TimeSpan.FromSeconds(.5)).ObserveOnDispatcher