如何等待 IObservable 完成,而不会使用反应式扩展阻止 UI
本文关键字:反应式 UI 扩展 完成 等待 何等待 IObservable | 更新日期: 2023-09-27 18:36:52
我正在尝试更新一段代码,以便它模拟模态对话框,而反应式扩展感觉像是一个合适的工具,但我无法让它工作。
目前,代码如下所示:
public bool ShowConfirmationDialogs(IEnumerable<ItemType> items)
{
bool canContinue = true;
foreach (var item in items)
{
Dialog dialog = new Dialog();
dialog.Prepare(item); // Prepares a "dialog" specific to each item
IObservable<bool> o = Service.ShowDialog(dialog, result =>
{
// do stuff with result that may impact next iteration, e.g.
canContinue = !result.Condition;
});
// tried using the following line to wait for observable to complete
// but it blocks the UI thread
o.FirstOrDefault();
if (!canContinue)
break;
}
if (!canContinue)
{
// do something that changes current object's state
}
return canContinue;
}
到目前为止,lambda 表达式中的代码用于在关闭 ShowDialog
显示的"对话框"时执行操作。对ShowDialog
的调用是非阻塞的,用于返回void
。
ShowDialog
幕后发生的事情是将对象添加到ObservableCollection
,以便将其显示在屏幕上。
我修改了ShowDialog
,以便它将返回一个IObservable<bool>
,该在对话框关闭时调用其OnCompleted
订阅者。这行得通。我使用以下代码对其进行了测试:
o.Subscribe(b => Console.WriteLine(b), () => Console.WriteLine("Completed"));
当对话框关闭时,我可以看到字符串"Completed"
。我的问题是上面的行是非阻塞的,所以我可能会显示几个对话框,这是我不想做的。
我尝试了以下方法:
o.FirstOrDefault();
假设程序会在那里等待,直到可观察量发送某些内容或完成。该程序会阻止所有内容,但它也会冻结 UI,这意味着我永远不会看到我的对话框,我永远无法关闭它,因此可观察量永远不会完成。
我使用 ObserveOn
和 SubscribeOn
尝试了几种变体,试图让 UI 线程完成其工作,但没有运气。任何想法将不胜感激,我的主要目标是保持代码看起来是顺序的,有点像使用 Window.ShowDialog
时一样。
总结一下:(并在评论中回答克里斯)
问题是ShowDialog
是非阻塞的,如上所述,预期的行为与使用时相同 Window.ShowDialog
.现在,我要么不能阻止 - 但随后循环继续并得到几个对话框 - 或者我可以阻止(使用 FirstOrDefault
),但它也会阻止 UI,这会阻止我关闭对话框以完成可观察量。
更多解释:(对于谜语)
当我调用ShowDialog
时,将显示一个模式控件(从某种意义上说,它阻止用户访问应用程序的其余部分),但对该方法的调用是非阻塞的,因此执行会立即继续。在我的示例中,由于循环,这可能会显示多个对话框。该方法是非阻塞的,因为它所做的只是将对象添加到集合中,我无法更改此行为。
但是,希望使用 Rx,我这样做是为了ShowDialog
将返回一个IObservable
。所以现在该方法立即返回,但我有一个对象,一旦关闭ShowDialog
操作显示的控件,它将调用任何观察者OnCompleted
。我为此使用Subject
,以防万一。
我现在想要的是等待这个返回的IObservable
完成,然后再继续,从而模拟阻塞调用。 FirstOrDefault
成功地完成了等待部分,但不幸的是,它还阻塞了 UI 线程,阻止控件实际显示,从而阻止用户关闭它,从而阻止IObservable
完成。
我知道我的想法不会太远,因为我可以通过在 x 秒后自动关闭对话框来让事情顺利进行。我现在需要的只是"等待"部分不阻止 UI,以便用户可以关闭控件而不是计时器。
我找到了解决问题的方法,所以如果您有兴趣,我会分享它。
经过一些重构后,我重命名了我在问题中使用的服务方法,并创建了一个新方法。它的界面看起来像这样:
public interface IDialogService
{
/// <summary>
/// Displays the specified dialog.
/// </summary>
/// <remarks>This method is non-blocking. If you need to access the return value of the dialog, you can either
/// provide a callback method or subscribe to the <see cref="T:System.IObservable{bool?}" /> that is returned.</remarks>
/// <param name="dialog">The dialog to display.</param>
/// <param name="callback">The callback to be called when the dialog closes.</param>
/// <returns>An <see cref="T:System.IObservable{bool?}" /> that broadcasts the value returned by the dialog
/// to any observers.</returns>
IObservable<bool?> Show(Dialog dialog, Action<bool?> callback = null);
/// <summary>
/// Displays the specified dialog. This method waits for the dialog to close before it returns.
/// </summary>
/// <remarks>This method waits for the dialog to close before it returns. If you need to show a dialog and
/// return immediately, use <see cref="M:Show"/>.</remarks>
/// <param name="dialog">The dialog to display.</param>
/// <returns>The value returned by the dialog.</returns>
bool? ShowDialog(Dialog dialog);
}
解决我问题的部分是ShowDialog
的实现:
public bool? ShowDialog(Dialog dialog)
{
// This will hold the result returned by the dialog
bool? result = null;
// We show a dialog using the method that returns an IObservable
var subject = this.Show(dialog);
// but we have to wait for it to close on another thread, otherwise we'll block the UI
// we do this by preparing a new DispatcherFrame that exits when we get a value
// back from the dialog
DispatcherFrame frame = new DispatcherFrame();
// So start observing on a new thread. The Start method will return immediately.
new Thread((ThreadStart)(() =>
{
// This line will block on the new thread until the subject sends an OnNext or an OnComplete
result = subject.FirstOrDefault();
// once we get the result from the dialog, we can tell the frame to stop
frame.Continue = false;
})).Start();
// This gets executed immediately after Thread.Start
// The Dispatcher will now wait for the frame to stop before continuing
// but since we are not blocking the current frame, the UI is still responsive
Dispatcher.PushFrame(frame);
return result;
}
我认为注释应该足以理解代码,我现在可以这样使用:
public bool? ShowConfirmationDialogs(IEnumerable<ItemType> items)
{
bool canContinue = true;
foreach (var item in items)
{
Dialog dialog = new Dialog();
dialog.Prepare(item); // Prepares a "dialog" specific to each item
bool? result = Service.ShowDialog(dialog);
canContinue = result.HasValue && result.Value;
if (!canContinue)
break;
}
if (!canContinue)
{
// do something that changes current object's state
}
return canContinue;
}
我很想听听其他用户可能提出的任何评论或替代方案。
您的代码是可观察序列和可枚举序列的奇怪混合。您还尝试从使用可观察量的函数返回bool
,因此您强制采用要求阻止操作的不良做法。
你最好尝试让所有内容都保持可观察性。这是最佳实践方法。
此外,在ShowDialog
函数中也没有明确使用每个item
。
这是我目前能提供的最好的,不知道更多。试试这个:
public IObservable<bool> ShowConfirmationDialogs(IEnumerable items)
{
var query =
from item in items.OfType<SOMEBASETYPE>().ToObservable()
from result in Service.ShowDialog(item =>
{
// do stuff with result that may impact next iteration of foreach
})
select new
{
Item = item,
Result = result,
};
return query.TakeWhile(x => x.Result == true);
}
调用代码应在 UI 线程上观察。
让我知道这是否有帮助。