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();
        }