简介
在我的WPF C#.NET应用程序中,我使用反应式扩展(Rx)来订阅事件,我经常需要从数据库重新加载一些内容来获取更新UI所需的值,因为事件对象通常只包含ID和一些元数据。
我使用Rx调度在后台加载数据并更新调度程序上的UI。我在混音和#34; Task.Run"在Rx序列内部(当使用" SelectMany&#34时;不再保证顺序,并且很难控制UnitTests中的调度)。另见:Executing TPL code in a reactive pipeline and controlling execution via test scheduler
我的问题
如果我关闭了我的应用程序(或关闭了一个标签页),我想取消订阅,然后等待数据库调用(从Rx" Select"调用),该调用仍然可以在"之后运行。 subscription.Dispose&#34 ;.到目前为止,我还没有找到任何好的实用工具或简单方法。
问题
是否有任何框架支持等待仍然在Rx链中运行的一切?
如果没有,您是否有任何好主意如何制作易于使用的实用程序?
有没有什么好的替代方法可以实现同样的目标?
示例
public async Task AwaitEverythingInARxChain()
{
// In real life this is a hot observable event sequence which never completes
IObservable<int> eventSource = Enumerable.Range(1, int.MaxValue).ToObservable();
IDisposable subscription = eventSource
// Load data in the background
.ObserveOn(Scheduler.Default)
.Select(id => LoadFromDatabase(id))
// Update UI on the dispatcher
.ObserveOn(DispatcherScheduler.Current)
.SubscribeOn(Scheduler.Default) // In real life the source produces the event values on a background thread.
.Subscribe(loadedData => UpdateUi(loadedData));
Thread.Sleep(TimeSpan.FromSeconds(10));
// In real life I want to cancel (unsubscribe) here because the user has closed the Application or closed the tab and return a task which completes when everything is done.
// Unsubscribe just guarantees that no "OnNext" is called anymore, but it doesn't wait until all operations in the sequence are finished (for example "LoadFromDatabase(id)" can still be runnig here.
subscription.Dispose();
await ?; // I need to await here, so that i can be sure that no "LoadFromDatabase(id)" is running anymore.
ShutDownDatabase();
}
我已经尝试过(并且没有工作)
更新:控制台输出示例和TakeUntil
public async Task Main()
{
Observable
.Timer(TimeSpan.FromSeconds(5.0))
.Subscribe(x =>
{
Console.WriteLine("Cancel started");
_shuttingDown.OnNext(Unit.Default);
});
await AwaitEverythingInARxChain();
Console.WriteLine("Cancel finished");
ShutDownDatabase();
Thread.Sleep(TimeSpan.FromSeconds(3));
}
private Subject<Unit> _shuttingDown = new Subject<Unit>();
public async Task AwaitEverythingInARxChain()
{
IObservable<int> eventSource = Observable.Range(0, 10);
await eventSource
.ObserveOn(Scheduler.Default)
.Select(id => LoadFromDatabase(id))
.ObserveOn(Scheduler.Default)
.TakeUntil(_shuttingDown)
.Do(loadedData => UpdateUi(loadedData));
}
public int LoadFromDatabase(int x)
{
Console.WriteLine("Start LoadFromDatabase: " + x);
Thread.Sleep(1000);
Console.WriteLine("Finished LoadFromDatabase: " + x);
return x;
}
public void UpdateUi(int x)
{
Console.WriteLine("UpdateUi: " + x);
}
public void ShutDownDatabase()
{
Console.WriteLine("ShutDownDatabase");
}
输出(实际):
Start LoadFromDatabase: 0
Finished LoadFromDatabase: 0
Start LoadFromDatabase: 1
UpdateUi: 0
Finished LoadFromDatabase: 1
Start LoadFromDatabase: 2
UpdateUi: 1
Finished LoadFromDatabase: 2
Start LoadFromDatabase: 3
UpdateUi: 2
Finished LoadFromDatabase: 3
Start LoadFromDatabase: 4
UpdateUi: 3
Cancel started
Cancel finished
ShutDownDatabase
Finished LoadFromDatabase: 4
Start LoadFromDatabase: 5
Finished LoadFromDatabase: 5
Start LoadFromDatabase: 6
Finished LoadFromDatabase: 6
Start LoadFromDatabase: 7
预期: 我想保证以下是最后的输出:
Cancel finished
ShutDownDatabase
答案 0 :(得分:3)
这比你想象的要容易。你可以await
观察到。所以只需这样做:
public async Task AwaitEverythingInARxChain()
{
IObservable<int> eventSource = Enumerable.Range(1, 10).ToObservable();
await eventSource
.ObserveOn(Scheduler.Default)
.Select(id => LoadFromDatabase(id))
.ObserveOn(DispatcherScheduler.Current)
.Do(loadedData => UpdateUi(loadedData), () => ShutDownDatabase());
}
在你的方法中有一点Console.WriteLine
个动作,并且在db调用中有一个小线程正在模拟网络延迟,我得到了这个输出:
LoadFromDatabase: 1 LoadFromDatabase: 2 UpdateUi: 1 LoadFromDatabase: 3 UpdateUi: 2 LoadFromDatabase: 4 UpdateUi: 3 LoadFromDatabase: 5 UpdateUi: 4 LoadFromDatabase: 6 UpdateUi: 5 LoadFromDatabase: 7 UpdateUi: 6 LoadFromDatabase: 8 UpdateUi: 7 LoadFromDatabase: 9 UpdateUi: 8 LoadFromDatabase: 10 UpdateUi: 9 UpdateUi: 10 ShutDownDatabase
如果您需要结束查询,只需创建一个shuttingDown
主题:
private Subject<Unit> _shuttingDown = new Subject<Unit>();
...然后像这样修改查询:
await eventSource
.ObserveOn(Scheduler.Default)
.Select(id => LoadFromDatabase(id))
.ObserveOn(DispatcherScheduler.Current)
.Do(
loadedData => UpdateUi(loadedData),
() => ShutDownDatabase())
.TakeUntil(_shuttingDown);
您只需要发出_shuttingDown.OnNext(Unit.Default);
来取消订阅可观察对象。
这是我完整的工作测试代码:
async Task Main()
{
Observable
.Timer(TimeSpan.FromSeconds(5.0))
.Subscribe(x => _shuttingDown.OnNext(Unit.Default));
await AwaitEverythingInARxChain();
}
private Subject<Unit> _shuttingDown = new Subject<Unit>();
public async Task AwaitEverythingInARxChain()
{
IObservable<int> eventSource = Observable.Range(0, 10);
await eventSource
.ObserveOn(Scheduler.Default)
.Select(id => LoadFromDatabase(id))
.ObserveOn(DispatcherScheduler.Current)
.Finally(() => ShutDownDatabase())
.TakeUntil(_shuttingDown)
.Do(loadedData => UpdateUi(loadedData));
}
public int LoadFromDatabase(int x)
{
Console.WriteLine("LoadFromDatabase: " + x);
Thread.Sleep(1000);
return x;
}
public void UpdateUi(int x)
{
Console.WriteLine("UpdateUi: " + x);
}
public void ShutDownDatabase()
{
Console.WriteLine("ShutDownDatabase");
}
我得到了这个输出:
LoadFromDatabase: 0 LoadFromDatabase: 1 UpdateUi: 0 LoadFromDatabase: 2 UpdateUi: 1 LoadFromDatabase: 3 UpdateUi: 2 LoadFromDatabase: 4 UpdateUi: 3 LoadFromDatabase: 5 UpdateUi: 4 ShutDownDatabase
请注意,observable会尝试在10秒内生成10个值,但会被OnNext
缩短。
答案 1 :(得分:3)
我终于找到了解决方案。 您可以使用TakeWhile来实现它。 TakeUntil不起作用,因为当第二个可观察序列产生第一个值时,主要的可观察序列立即完成。
以下是工作解决方案的示例:
public async Task Main_Solution()
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Observable
.Timer(TimeSpan.FromSeconds(4))
.Subscribe(x =>
{
Console.WriteLine("Cancel startedthread='{0}'", Thread.CurrentThread.ManagedThreadId);
cancellationTokenSource.Cancel();
});
await AwaitEverythingInARxChain(cancellationTokenSource.Token);
Console.WriteLine("Cancel finished thread='{0}'", Thread.CurrentThread.ManagedThreadId);
ShutDownDatabase();
Thread.Sleep(TimeSpan.FromSeconds(10));
}
public async Task AwaitEverythingInARxChain(CancellationToken token)
{
IObservable<int> eventSource = Observable.Range(0, 10);
await eventSource
.ObserveOn(Scheduler.Default)
.Select(id => LoadFromDatabase(id))
.TakeWhile(_ => !token.IsCancellationRequested)
.ObserveOn(Scheduler.Default) // Dispatcher in real life
.Do(loadedData => UpdateUi(loadedData)).LastOrDefaultAsync();
}
public int LoadFromDatabase(int x)
{
Console.WriteLine("Start LoadFromDatabase: {0} thread='{1}'", x, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromSeconds(3));
Console.WriteLine("Finished LoadFromDatabase: {0} thread='{1}'", x, Thread.CurrentThread.ManagedThreadId);
return x;
}
public void UpdateUi(int x)
{
Console.WriteLine("UpdateUi: '{0}' thread='{1}'", x, Thread.CurrentThread.ManagedThreadId);
}
public void ShutDownDatabase()
{
Console.WriteLine("ShutDownDatabase thread='{0}'", Thread.CurrentThread.ManagedThreadId);
}
输出:
Start LoadFromDatabase: 0 thread='9'
Finished LoadFromDatabase: 0 thread='9'
Start LoadFromDatabase: 1 thread='9'
UpdateUi: '0' thread='10'
Cancel startedthread='4'
Finished LoadFromDatabase: 1 thread='9'
Cancel finished thread='10'
ShutDownDatabase thread='10'
请注意&#34; ShutDownDatabase&#34;是最后一个输出(如预期的那样)。它一直等到&#34; LoadFromDatabase&#34;已完成第二个值,即使其生成的值未进一步处理。这正是我想要的。
答案 2 :(得分:0)
你需要等待一些东西。您无法等待订阅处理。最简单的方法是将处置逻辑转换为可观察对象的一部分:
var observable = eventSource
// Load data in the background
.ObserveOn(Scheduler.Default)
.Select(id => LoadFromDatabase(id))
.TakeUntil(Observable.Timer(TimeSpan.FromSeconds(10))) //This replaces your Thread.Sleep call
.Publish()
.RefCount();
var subscription = observable.ObserveOn(DispatcherScheduler.Current)
.Subscribe(loadedData => UpdateUi(loadedData));
//do whatever you want here.
await observable.LastOrDefault();