Return 来自同步方法中的异步方法的结果
Return result from Async Method within a Sync method
在我的应用程序中,我有一个 Task<bool>
类型的异步 Save() 方法,如果保存成功,它会通过 bool 发出信号。 Save() 中可能发生各种事情,它通过处理异常的另一层调用,显示可能的对话框等,但这没关系,我只关心 bool 结果。
现在我必须从非异步方法中调用此方法(这是对已用框架的覆盖,因此我不能只将其设为异步)
代码看起来有点像:
public override void SynchronousMethodFromFramework()
{
bool result = false;
Task.Run(async () => result = await Save());
return result;
}
问题是,结果在保存完成之前返回(因此始终为 false)。
它如何解决这个问题?我已经尝试了 Task.WaitAll()、.Result、.ConfigureAwaiter(false),但我所做的一切似乎都完全冻结了我的应用程序。
更多信息:
使用的WPF框架是Caliburn.Micro。我的 MainviewModel 是一个 Conductor<IScreen>.Collection.OneActive
,它在 Tabcontrol 中执行多个视图模型。每个 ViewModel 都是某种编辑屏幕。
当最终用户关闭应用程序时(通过右上角的红色 X ),我想遍历所有选项卡以查看它们是否有待处理的更改。
主视图模型代码:
public override void CanClose(Action<bool> callback)
{
//for each tab, go to it and try to close it.
//If pending changes and close is not succeeded (eg, user cancels), abort aplication close
bool canclose = false;
Action<bool> result = b => canclose = b;
for (int i = Items.Count - 1; i >= 0; i--)
{
var screen = Items[i];
screen.CanClose(result);
if (!canclose)
{
callback(false);
return;
}
}
callback(true);
}
我的 "Edit"-ViewModels 中的代码:
private async Task<bool> SavePendingChanges()
{
if (!Entity.HasDirtyContents())
return true;
bool? dialogResult = DialogProvider.ShowMessageBox("Save changes",
"There are pending changes, do you want to save them ?", MsgBoxButton.YesNoCancel);
if (dialogResult == null)
return false;//user cancelled
if (dialogResult == false)
return true;//user doesn't want to save, but continue
//try to save; if save failed => return false
return await (Save());
}
public override void CanClose(Action<bool> callback)
{
var task = SavePendingChanges();
task.Wait();
bool result = task.Result;
callback(result);
}
"CanClose"是CM提供的非异步框架方法...
正确的 解决方案是使 SynchronousMethodFromFramework
异步,通过 return Task
或使用类似 deferrals(正如我在我的博客上所描述的)。所以绝对让框架作者知道你的需求。
同时,您可以使用 one of the hacks from my article on async
brownfield development.
破解它
最简单的解决方案是让您的 Save
方法成为纯粹的后台方法 - 毕竟,如果它 运行 作为后台任务,它不应该 "reaching into" UI 线程有任何更新。如果您的代码在几个地方调用 Save
- 一次用于 "regular" 保存,然后另一个 "last chance synchronous only" 保存 - 那么您可以使用 IProgress<T>
更新 UI 而不是使用 Dispatcher
直接访问 ViewModel 属性 and/or。如果您的代码仅在此处调用 Save
,则完全删除所有 UI 更新。
如果你能让 Save
成为真正的后台操作,那么你就可以阻止:
return Task.Run(() => Save()).GetAwaiter().GetResult();
但是,如果您做不到(我假设您已经考虑过并拒绝了),那么您可以调用一些严肃的黑魔法来让运行时间屈从于您的意愿。即,使用 Nested Message Loop(Lucifer Enterprises 的注册商标)。
Developer General's warning: Nested message loops are evil. Evil, evil, evil! Side effects include insanity and death. It is entirely likely that a future maintainer of this code will become violent and hunt you down.
自从我在 WPF 中完成嵌套消息循环以来已经有一段时间了,但我相信这应该可以解决问题:
private async Task<bool> EvilSaveAsync(DispatcherFrame frame)
{
try
{
return await Task.Run(() => Save());
}
finally
{
frame.Continue = false;
}
}
public override void SynchronousMethodFromFramework()
{
var frame = new DispatcherFrame();
var task = EvilSaveAsync(frame);
Dispatcher.PushFrame(frame);
return task.GetAwaiter().GetResult();
}
谢谢 Stephen Cleary 的回答,但与此同时,我也找到了一个似乎适用于我尝试过的所有场景的解决方案(打开多个选项卡,有些没有变化,有些有更改,而其他人则通过触发他们自己的工作流的乐观并发异常进行更改,打开对话框来解决这些问题)。所有方案都有效,所以我想知道这是否是一个好的解决方案,或者我是否忽略了某些事情或做了一些可怕的黑客攻击?
我的做法:
由于问题是 CM 框架中的 CanClose 非同步方法(实际上,问题是回调操作),我在自己的 ViewModelbase 中将其重新路由为:
public abstract class ViewModelBase : IScreen
{
//sealed so the users of the viewmodelbase cannot accidentally override it
public async override sealed void CanClose(Action<bool> callback)
{
callback(await CanClose());
}
//default implementation is always closable
public async virtual Task<bool> CanClose()
{
return true;
}
}
因此,框架中的所有 "CanClose" 现在都通过我自己的 Task 类型的异步 CanClose 方法重新路由(注意此覆盖上的 "async" - 感谢您的提示)
然后在执行选项卡的 MainView 模型中,我自己的异步 CanClose 被覆盖为:
public async override Task<bool> CanClose()
{
for (int i = Items.Count - 1; i >= 0; i--)
{
var screen = Items[i] as ViewModelBase;
var canclose = await screen.CanClose();
if (!canclose) return false;
Items.Remove(screen);//remove it from the tabs so it's not visible anymore
}
return true;
}
最终,在我的 EditViewmodels 中,我将 CanClose 覆盖为:
public override async Task<bool> CanClose()
{
return await SavePendingChanges();
}
public async Task<bool> SavePendingChanges()
{
if (!Entity.HasDirtyContents())
return true;
bool? dialogResult = DialogProvider.ShowMessageBox("Save changes",
"There are pending changes, do you want to save them ?", MsgBoxButton.YesNoCancel);
if (dialogResult == null)
return false;//user cancelled
if (dialogResult == false)
return true;//user doesn't want to save, but continue
//try to save; if save failed => return false
return await Save();
}
在我的应用程序中,我有一个 Task<bool>
类型的异步 Save() 方法,如果保存成功,它会通过 bool 发出信号。 Save() 中可能发生各种事情,它通过处理异常的另一层调用,显示可能的对话框等,但这没关系,我只关心 bool 结果。
现在我必须从非异步方法中调用此方法(这是对已用框架的覆盖,因此我不能只将其设为异步)
代码看起来有点像:
public override void SynchronousMethodFromFramework()
{
bool result = false;
Task.Run(async () => result = await Save());
return result;
}
问题是,结果在保存完成之前返回(因此始终为 false)。 它如何解决这个问题?我已经尝试了 Task.WaitAll()、.Result、.ConfigureAwaiter(false),但我所做的一切似乎都完全冻结了我的应用程序。
更多信息:
使用的WPF框架是Caliburn.Micro。我的 MainviewModel 是一个 Conductor<IScreen>.Collection.OneActive
,它在 Tabcontrol 中执行多个视图模型。每个 ViewModel 都是某种编辑屏幕。
当最终用户关闭应用程序时(通过右上角的红色 X ),我想遍历所有选项卡以查看它们是否有待处理的更改。
主视图模型代码:
public override void CanClose(Action<bool> callback)
{
//for each tab, go to it and try to close it.
//If pending changes and close is not succeeded (eg, user cancels), abort aplication close
bool canclose = false;
Action<bool> result = b => canclose = b;
for (int i = Items.Count - 1; i >= 0; i--)
{
var screen = Items[i];
screen.CanClose(result);
if (!canclose)
{
callback(false);
return;
}
}
callback(true);
}
我的 "Edit"-ViewModels 中的代码:
private async Task<bool> SavePendingChanges()
{
if (!Entity.HasDirtyContents())
return true;
bool? dialogResult = DialogProvider.ShowMessageBox("Save changes",
"There are pending changes, do you want to save them ?", MsgBoxButton.YesNoCancel);
if (dialogResult == null)
return false;//user cancelled
if (dialogResult == false)
return true;//user doesn't want to save, but continue
//try to save; if save failed => return false
return await (Save());
}
public override void CanClose(Action<bool> callback)
{
var task = SavePendingChanges();
task.Wait();
bool result = task.Result;
callback(result);
}
"CanClose"是CM提供的非异步框架方法...
正确的 解决方案是使 SynchronousMethodFromFramework
异步,通过 return Task
或使用类似 deferrals(正如我在我的博客上所描述的)。所以绝对让框架作者知道你的需求。
同时,您可以使用 one of the hacks from my article on async
brownfield development.
最简单的解决方案是让您的 Save
方法成为纯粹的后台方法 - 毕竟,如果它 运行 作为后台任务,它不应该 "reaching into" UI 线程有任何更新。如果您的代码在几个地方调用 Save
- 一次用于 "regular" 保存,然后另一个 "last chance synchronous only" 保存 - 那么您可以使用 IProgress<T>
更新 UI 而不是使用 Dispatcher
直接访问 ViewModel 属性 and/or。如果您的代码仅在此处调用 Save
,则完全删除所有 UI 更新。
如果你能让 Save
成为真正的后台操作,那么你就可以阻止:
return Task.Run(() => Save()).GetAwaiter().GetResult();
但是,如果您做不到(我假设您已经考虑过并拒绝了),那么您可以调用一些严肃的黑魔法来让运行时间屈从于您的意愿。即,使用 Nested Message Loop(Lucifer Enterprises 的注册商标)。
Developer General's warning: Nested message loops are evil. Evil, evil, evil! Side effects include insanity and death. It is entirely likely that a future maintainer of this code will become violent and hunt you down.
自从我在 WPF 中完成嵌套消息循环以来已经有一段时间了,但我相信这应该可以解决问题:
private async Task<bool> EvilSaveAsync(DispatcherFrame frame)
{
try
{
return await Task.Run(() => Save());
}
finally
{
frame.Continue = false;
}
}
public override void SynchronousMethodFromFramework()
{
var frame = new DispatcherFrame();
var task = EvilSaveAsync(frame);
Dispatcher.PushFrame(frame);
return task.GetAwaiter().GetResult();
}
谢谢 Stephen Cleary 的回答,但与此同时,我也找到了一个似乎适用于我尝试过的所有场景的解决方案(打开多个选项卡,有些没有变化,有些有更改,而其他人则通过触发他们自己的工作流的乐观并发异常进行更改,打开对话框来解决这些问题)。所有方案都有效,所以我想知道这是否是一个好的解决方案,或者我是否忽略了某些事情或做了一些可怕的黑客攻击?
我的做法:
由于问题是 CM 框架中的 CanClose 非同步方法(实际上,问题是回调操作),我在自己的 ViewModelbase 中将其重新路由为:
public abstract class ViewModelBase : IScreen
{
//sealed so the users of the viewmodelbase cannot accidentally override it
public async override sealed void CanClose(Action<bool> callback)
{
callback(await CanClose());
}
//default implementation is always closable
public async virtual Task<bool> CanClose()
{
return true;
}
}
因此,框架中的所有 "CanClose" 现在都通过我自己的 Task 类型的异步 CanClose 方法重新路由(注意此覆盖上的 "async" - 感谢您的提示)
然后在执行选项卡的 MainView 模型中,我自己的异步 CanClose 被覆盖为:
public async override Task<bool> CanClose()
{
for (int i = Items.Count - 1; i >= 0; i--)
{
var screen = Items[i] as ViewModelBase;
var canclose = await screen.CanClose();
if (!canclose) return false;
Items.Remove(screen);//remove it from the tabs so it's not visible anymore
}
return true;
}
最终,在我的 EditViewmodels 中,我将 CanClose 覆盖为:
public override async Task<bool> CanClose()
{
return await SavePendingChanges();
}
public async Task<bool> SavePendingChanges()
{
if (!Entity.HasDirtyContents())
return true;
bool? dialogResult = DialogProvider.ShowMessageBox("Save changes",
"There are pending changes, do you want to save them ?", MsgBoxButton.YesNoCancel);
if (dialogResult == null)
return false;//user cancelled
if (dialogResult == false)
return true;//user doesn't want to save, but continue
//try to save; if save failed => return false
return await Save();
}