【问题标题】:Wait until UIElement is drawn (without updatelayout)等到 UIElement 被绘制(没有 updatelayout)
【发布时间】:2013-03-24 12:20:14
【问题描述】:

如果单击按钮,我有一个大约需要 5 秒才能完成的功能。如果单击按钮,我想显示某种通知以指示正在处理按钮单击,例如

<Button Click="OnButtonClick" Content="Process Input" />

<Border x:Name="NotificationBorder" Opacity="0" IsHitTestVisible="False" 
        Width="500" Height="100" Background="White">
    <TextBlock Text="Your input is being processed" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>

然后在按钮上的代码隐藏中单击:

private void OnButtonClick(Object sender, RoutedEventArgs e)
{
    DoubleAnimation da = new DoubleAnimation
        {
            From = 5,
            To = 0,
            Duration = TimeSpan.FromSeconds(2.5),
        };

    // Making border visible in hopes that it's drawn before animation kicks in
    NotificationBorder.Opacity = 1;
    da.Completed += (o, args) => NotificationBorder.Opacity = 0;

    NotificationBorder.UpdateLayout(); //Doesn't do anything
    UpdateLayout(); // Doesn't do anything

    NotificationBorder.BeginAnimation(OpacityProperty, da);

    // Simulate calculationheavy functioncall
    Thread.Sleep(5000);
}

不知何故,UpdateLayout() 的渲染速度不够快,只有在 Thread.Sleep 的 5 秒结束后才会显示通知。

Dispatcher.Invoke((Action)(() =&gt; NotificationBorder.Opacity = 1), DispatcherPriority.Render); 也不起作用。

此外,我不能让Thread.Sleep 在单独的工作线程中运行 - 在实际应用程序中,它需要从 Dispatcher 拥有的对象中读取数据并(重新)构建部分 UI。

有没有办法在调用Thread.Sleep() 之前使其可见?

【问题讨论】:

  • 你试过 Dispatcher.BeginInvoke((Action)(() => Thread.Sleep(5000)) 吗?
  • 您没有看到更新,因为您的模拟正在休眠 UI 线程。如果您的长时间运行的进程也在主线程中,您将看到相同的行为。
  • 您需要在单独的线程中异步执行长时间运行的函数。您可以使用BackgroundWorker 来执行此任务。
  • 为什么要将NotificationBorder 的不透明度从 5 设置为 0?不透明度是一个介于 0 和 1 之间的值。
  • @Clemens:这样不透明度是可见的,并且仅在动画的最后 20% 中淡出(我没有费心寻找“更好”的替代方案,如果有的话?)。 BackgroundWorker 可以访问 Dispatcher 拥有的对象吗? IE。从数据网格中恢复项目?

标签: c# .net wpf uielement


【解决方案1】:

问题是主线程(UI 线程)上的任何长时间活动都会导致 UI 冻结,因此您将看不到任何动画。

您需要在单独的线程中进行计算。我需要访问 Dispatcher 拥有的对象,您可以使用 uiObject.Dispatcher.BeginInvoke 或 Invoke。 BackgroundWorker 可以帮助您执行一些长计算并订阅事件 Completed,该事件将由 BackgroundWorker 在 UI 线程上自动触发。

如果您对 UI 冻结没问题,请使用 Dispatcher.BeginInvoke(DispatcherPriority.Background,...) 更改某些属性和其他内容重新安排,例如

NotificationBorder.Opacity = 1; Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)(()=>Thread.Sleep(5000)));

【讨论】:

  • 我不认为 OP 真的会在这里冻结 UI,因为即使由于优先级更改而显示初始边框,动画也不会在 UI 线程运行时运行被屏蔽了。
  • 当然,这就是我写“如果你对 UI 冻结没问题”的原因
【解决方案2】:

如果您希望 UI 更新,您需要将长时间运行的操作更改为异步操作,这意味着将 UI 依赖项与任何逻辑或可能 IO 绑定操作一直占用的时间正确解耦。如果您不这样做,那么在您的长时间运行的操作中发生的任何 UI 更新实际上都不会反映在 UI 中,直到它完成为止。

如果您使用 .NET 4.5,以下是我的首选模式:

    public async Task DoComplexStuff()
    {
        //Grab values from UI objects that must be accessed on UI thread

        //Run some calculations in the background that don't depend on the UI
        var result = await Task.Run((Func<string>)DoComplexStuffInBackground);

        //Update UI based on results

        //Possibly do more stuff in the background, etc.
    }

    private string DoComplexStuffInBackground()
    {
        return "Stuff";
    }

这与旧式 BackgroundWorker 方法相比有些“倒退”,但我发现它往往会产生更清晰的逻辑。您无需使用显式调用 UI 更新来编写后台代码,而是通过 Task.Run() 围绕显式后台工作调用编写 UI 代码。这也倾向于导致以 UI 为中心的代码和底层逻辑代码之间的自然分离,因为您需要将逻辑重构为可以以这种方式调用的方法。无论如何,这对于长期可维护性来说是个好主意。

【讨论】:

    【解决方案3】:

    因此您可以将两个操作(UI 和您的计算)分派到调度程序中,它们将在一行中执行

    Dispatcher.BeginInvoke(do ui things);
    Dispatcher.BeginInvoke(do your logic things);
    

    但你最好将你的计算移到后台线程中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-10-17
      • 2011-05-23
      • 1970-01-01
      • 2018-10-19
      • 1970-01-01
      相关资源
      最近更新 更多