【问题标题】:How to postpone execution until some event happens?如何推迟执行直到某些事件发生?
【发布时间】:2015-04-29 18:33:10
【问题描述】:

我有一个 WebBrowser 控件,它有 InvokeScript 方法,你应该只在 WebBrowser 加载后调用它。

所以我尝试过这样的事情:

private readonly ManualResetEventSlim browserLoaded = new ManualResetEventSlim(false);    

private void BrowserLoaded(object sender, NavigationEventArgs navigationEventArgs)
{                   
        browserLoaded.Set();
}

private async Task<object> InvokeScript(string invoke, object[] parameters = null)
{
        return await Task.Factory
            .StartNew(() =>
                      {
                          if (!browserLoaded.Wait(TimeSpan.FromSeconds(10)))
                          {
                              throw new Exception("Timeout for waiting browser to load.");
                          }
                      })
            .ContinueWith(task => parameters == null
                ? browser.InvokeScript(invoke)
                : browser.InvokeScript(invoke, parameters), TaskScheduler.FromCurrentSynchronizationContext());
}          

对我来说它看起来不是很好,但是在异步调用时可以正常工作。 出现问题,当我尝试同步读取结果值时 - 应用程序只是挂起:

private string GetEnteredText() 
{
     return (string)InvokeScript("getEnteredText").Result;
}

我知道,我应该一直异步,但我想知道如何处理属性:

public override string UserText
{
    get
    {
        return GetEnteredText();
    }
    set
    {
        SetEnteredText(value);
    }
}

或者在这种情况下异步是错误的方式?

更新

属性是浏览器页面中的输入字段值和 WPF 中的视图模型之间的“粘合剂”,所以我看不出将其作为单独方法的好方法,特别是因为它是更大框架的一部分(注意覆盖它的关键字)。 加载浏览器控件后,执行逻辑不会花费很长时间,我猜不到 10 毫秒,这就是为什么在这种情况下我可以使用同步执行。而且通常浏览器控件加载速度足够快,这里延迟的唯一原因是确保在加载之前不调用 InvokeScript,而不是因为它需要很长时间或smth。

【问题讨论】:

  • 如果可能的话,我会尽量避免在我的 getter 和 setter 中使用逻辑。在 getter 中使用异步方法会使事情陷入困境 - 它可能现在、一分钟或一小时等返回。为了避免您在上面描述的情况,我通常会尝试触发任何必要的逻辑 before 我需要来自 getter 的数据。您能否提供更多有关使用 UserText 属性的上下文,我会看看我是否可以弄清楚在哪里调用 SetEnteredText 方法?
  • Here 您将了解如何使用async/await 处理onload 事件。 This 也可能会引起您的兴趣。
  • @goobering - 我已经解释了用法。

标签: c# wpf async-await


【解决方案1】:

应用只是挂起

我们会的,你自己说的。你知道为什么会这样。您正在使用Task.Result 阻止通话,这就是导致死锁的原因。

或者在这种情况下异步是错误的方式?

我们没有异步属性。我们没有它们的原因是属性本质上不是异步的。斯蒂芬·克利里describes it nicely

如果您的“属性”每次都需要异步评估 它被访问了,那么你真的在谈论异步 操作。最好的解决方案是将属性更改为异步 方法。从语义上讲,它不应该是一个属性。

而不是属性,使其成为一个异步方法,您可以正确地await

关于使用ManualResetEvent,我会改用TaskCompletionSource&lt;bool&gt;。它与 TPL 一起“更好”地工作:

private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
private void BrowserLoaded(object sender, NavigationEventArgs navigationEventArgs)
{                   
    tcs.TrySetResult(true);
}

private async Task<object> InvokeScript(string invoke, object[] parameters = null)
{
    var timeoutTask = Task.Delay(TimeSpan.FromSeconds(10));
    if (timeoutTask == await Task.WhenAny(tcs.Task, timeoutTask))
    {
        // You've timed out;
    }

    return Task.Run(() =>
    {
         parameters == null ? browser.InvokeScript(invoke)
                            : browser.InvokeScript(invoke, parameters)
    });
}

我还看到您在后续文章中使用了TaskScheduler.FromCurrentSynchronizationContext()。如果您需要在 UI 线程上执行此操作,则根本没有使用线程池线程的意义。

@Noseratio 补充说 WebBrowser 是一个 STA 对象。他的回答here 可能会有所帮助。

【讨论】:

  • Yuval,这里的问题是你不能真正使用Task.Run中的browser,因为WebBrowser是一个STA对象,只能从创建它的线程访问。也许,OP 正在寻找类似this 的东西。
  • @Noseratio 谢谢,我不知道,因为我没有深入了解他正在使用的委托的执行语义。
  • @Noseratio 编辑澄清:)
  • 我猜有死锁,但不知道为什么,尝试调试并没有透露太多。我会仔细查看@Noseratio 的答案,看看它是否符合我的需求。
  • @Giedrius 你遇到了死锁,因为你被Task.Result 阻止了。我以为你明白了。
猜你喜欢
  • 2012-08-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-09
  • 2016-02-06
  • 1970-01-01
  • 2014-08-03
  • 2019-10-16
相关资源
最近更新 更多