在我的应用程序中,我有一个类型为Task<bool>
的异步Save()方法,如果保存成功,通过bool将发出信号。所有类型的东西都可以在Save()中发生,它通过处理异常的另一层调用,显示可能的对话框等,但这并不重要,我关心的只是bool结果。
现在我必须在非异步方法中调用此方法(这是从使用过的框架中覆盖,所以我不能只是让它异步)
代码看起来有点像:
public override void SynchronousMethodFromFramework()
{
bool result = false;
Task.Run(async () => result = await Save());
return result;
}
问题是,在Save完成之前返回结果(因此始终为false)。 怎么解决这个问题?我已经尝试过Task.WaitAll(),。Result,.ConfigureAwaiter(false),但我所做的一切似乎都完全冻结了我的应用程序。
更多信息:
使用的WPF框架是Caliburn.Micro。我的MainviewModel是Conductor<IScreen>.Collection.OneActive
,它在Tabcontrol中传导多个视图模型。每个ViewModel都是某种编辑屏幕。
当最终用户关闭应用程序时(通过右上方的红色X),我想迭代所有选项卡以查看它们是否有挂起的更改。
mainviewmodel的代码:
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);
}
我的&#34;编辑&#34; -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);
}
&#34; CanClose&#34;是CM提供的非异步框架方法......
答案 0 :(得分:1)
正确的解决方案是让SynchronousMethodFromFramework
异步,方法是让它返回Task
或使用类似deferrals的内容(正如我在博客中描述的那样) 。所以大多数让让你的需求知道给框架作者。
与此同时,您可以使用one of the hacks from my article on async
brownfield development来解决问题。
最简单的解决方案是使您的Save
方法成为纯粹的后台方法 - 毕竟,如果它作为后台任务运行,它不应该“进入”具有任何更新的UI线程。如果您的代码在几个地方调用Save
- 一次用于“常规”保存,然后另一个“仅限同步最后机会”保存 - 那么您可以使用IProgress<T>
更新UI而不是直接访问ViewModel属性和/或使用Dispatcher
。如果您的代码仅在此处调用Save
,则只需完全删除所有UI更新。
如果您可以使Save
成为真正的后台操作,那么您可以阻止:
return Task.Run(() => Save()).GetAwaiter().GetResult();
但是如果你不能这样做(我假设你已经考虑过并拒绝了它),那么你可以调用一些严肃的黑暗魔法来根据你的意愿弯曲运行时间。也就是说,使用嵌套消息循环(Lucifer Enterprises的注册商标)。
Developer General的警告:嵌套的消息循环是邪恶的。邪恶,邪恶,邪恶!副作用包括精神错乱和死亡。这段代码的未来维护者很可能会变得暴力并且追捕你。
自从我在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();
}
答案 1 :(得分:0)
谢谢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方法重新路由(注意这个覆盖的“异步” - 谢谢你的提示)
然后在进行选项卡的MainViewmodel中,我自己的异步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();
}