并行生成 UI [英] Parallel Generation of UI

查看:16
本文介绍了并行生成 UI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们有一个 WPF 应用程序,它有一个带有缓存的 VirtualizingStackPanel 的 ListBox.不是因为它有大量的元素(通常少于 20,但在极端情况下可能高达 100 或更多),而是因为元素需要时间来生成.这些元素实际上是 UIElement 对象.所以应用程序需要动态生成UIElements.

We have a WPF application that has a ListBox with a VirtualizingStackPanel with caching. Not because it has massively many elements (typically less than 20 but perhaps up to 100 or more in extreme cases) but because elements take time to generate. The elements are in fact UIElement objects. So the application dynamically needs to generate UIElements.

问题是,即使虚拟化看起来可以工作,应用程序的响应速度仍然很慢,这是具有最小噪音"的概念验证解决方案.

The problem is that even though the virtualization appears to work, the application is still slow to become responsive, and this is in a proof of concept solution with minimal "noise".

因此我们认为,由于主要问题是我们动态生成复杂的 UIElement 对象,因此我们需要并行执行此操作,即线程外.但是我们得到一个错误,代码需要在 STA 线程上运行:

So we figured that since the main problem is that we generate complex UIElement objects dynamically, we need to do that in parallel, i.e. off-thread. But we get an error that the code needs to be run on a STA thread:

调用线程必须是 STA,因为很多 UI 组件都需要这个.

The calling thread must be STA, because many UI components require this.

这是否意味着我们不能在 WPF 主 UI 线程以外的线程上生成 UI(UIElement 对象)?

Does this mean that we cannot generate UI (UIElement objects) on thread other than the WPF main UI thread?

以下是我们的概念验证解决方案中的相关代码片段:

Here's a relevant code fragment from our proof of concept solution:

public class Person : ObservableBase
{
    // ...

    UIElement _UI;
    public UIElement UI
    {
        get
        {
            if (_UI == null)
            {
                ParallelGenerateUI();
            }
            return _UI;
        }
    }

    private void ParallelGenerateUI()
    {
        var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

        Task.Factory.StartNew(() => GenerateUI())
        .ContinueWith(t =>
        {
            _UI = t.Result;
            RaisePropertyChanged("UI");
        }, scheduler);
    }

    private UIElement GenerateUI()
    {
        var tb = new TextBlock();
        tb.Width = 800.0;
        tb.TextWrapping = TextWrapping.Wrap;
        var n = rnd.Next(10, 5000);
        for (int i = 0; i < n; i++)
        {
            tb.Inlines.Add(new Run("A line of text. "));
        }
        return tb;
    }

    // ...
}

这里是一段相关的 XAML:

and here is a relevant piece of XAML:

<DataTemplate x:Key="PersonDataTemplate" DataType="{x:Type local:Person}">
    <Grid>
        <Border Margin="4" BorderBrush="Black" BorderThickness="1" MinHeight="40" CornerRadius="3" Padding="3">

            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <!--<RowDefinition />-->
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <TextBlock Text="Name : " Grid.Row="0" FontWeight="Bold" HorizontalAlignment="Right" />
                <TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding Name}" />
                <TextBlock Text=" - Age : " Grid.Column="2" Grid.Row="0" FontWeight="Bold"
                        HorizontalAlignment="Right" />
                <TextBlock Grid.Column="3" Grid.Row="0" Text="{Binding Age}" />
                <ContentControl Grid.Column="4" Grid.Row="0" Content="{Binding Path=UI}" />

            </Grid>
        </Border>
    </Grid>
</DataTemplate>

如您所见,我们将数据绑定到 UIElement 类型的属性 UI.

As you can see we databind to a property UI of type UIElement.

<ListBox x:Name="listbox" ItemsSource="{Binding Persons}" Background="LightBlue"
    ItemTemplate="{StaticResource PersonDataTemplate}"
    ItemContainerStyle="{StaticResource ListBoxItemStyle}" 
    VirtualizingPanel.IsVirtualizing="True"
    VirtualizingPanel.IsVirtualizingWhenGrouping="True" 
    VirtualizingStackPanel.ScrollUnit="Pixel"  
    VirtualizingStackPanel.CacheLength="10,10"
    VirtualizingStackPanel.CacheLengthUnit="Item"
>
    <ListBox.GroupStyle>
        <GroupStyle HeaderTemplate="{StaticResource GroupHeaderTemplate}" />
    </ListBox.GroupStyle>

</ListBox>

在结束语境中,我们的应用程序所做的是创建一个代码视图,其中的程序列表再次包含结构化内容的混合(一方面是参数和局部变量,另一方面是语句和表达式.)

In closing context, what our application does is create a code view where the list is of procedures which again contain a mix of structured content (for parameters and local variables on one hand and statements and expressions on the other.)

换句话说,我们的 UIElement 对象过于复杂,无法单独通过数据绑定来创建.

In other words our UIElement objects are too complex to create via databinding alone.

我们的另一个想法是在 XAML 中使用异步"设置,因为它似乎可以创建非阻塞 UI",但我们无法实现这一点,因为我们遇到了与上述相同的错误:

Another thought we had was to use "Async" settings in the XAML as it appears possible to create "non-blocking UI" but we have not been able to implement this because we get the same error as above:

调用线程必须是 STA,因为很多 UI 组件都需要这个.

The calling thread must be STA, because many UI components require this.

堆栈跟踪:

System.InvalidOperationException was unhandled by user code
  HResult=-2146233079
  Message=The calling thread must be STA, because many UI components require this.
  Source=PresentationCore
  StackTrace:
       at System.Windows.Input.InputManager..ctor()
       at System.Windows.Input.InputManager.GetCurrentInputManagerImpl()
       at System.Windows.Input.KeyboardNavigation..ctor()
       at System.Windows.FrameworkElement.FrameworkServices..ctor()
       at System.Windows.FrameworkElement.EnsureFrameworkServices()
       at System.Windows.FrameworkElement..ctor()
       at System.Windows.Controls.TextBlock..ctor()
       at WPF4._5_VirtualizingStackPanelNewFeatures.Person.GenerateUI() in c:UsersChristianDesktopWPF4.5_VirtualizingStackPanelNewFeaturesWPF4.5_VirtualizingStackPanelNewFeaturesPerson.cs:line 84
       at WPF4._5_VirtualizingStackPanelNewFeatures.Person.<ParallelGenerateUI>b__2() in c:UsersChristianDesktopWPF4.5_VirtualizingStackPanelNewFeaturesWPF4.5_VirtualizingStackPanelNewFeaturesPerson.cs:line 68
       at System.Threading.Tasks.Task`1.InnerInvoke()
       at System.Threading.Tasks.Task.Execute()
  InnerException: 

1) 添加了更多 XAML.2) 添加了堆栈跟踪.

1) Added more XAML. 2) Added stacktrace.

推荐答案

我在普通的 c# 环境中遇到了同样的问题.我也尝试了很多东西.是否提前计算了控件的大小来调整父级的大小?不幸的是,我正在这样做.

I am suffering the same problem in normal c# environment. I also tried lots of things. Do you calculate the size of controls to adjust the size of the parent in advance? I am doing this unfortunately.

您还可以创建一个动态嵌套子项的控件.通过它,您可以创建一种 UIElement 适配器.适配器在开始时创建,并具有创建 UIElements 的所有信息.适配器可以根据需要及时在 STA 线程上创建请求的子项.向上或向下滚动时,您可以在滚动方向上提前创建子项.这样你就可以从例如开始5-10 个 UI 元素,然后通过向上滚动更多来计算.

You may also create a control nesting your children dynamically. By that you can create kind of an UIElement Adapter. The adapter is created at the start time and has all information to create the UIElements. The adapter could create requested children on STA thread on demand just in time. When scrolling up or down you may create children in advance in the direction you are scrolling. This way you can start with e.g. 5-10 UI elements and then you calculate by scrolling up more.

我知道这不是那么好,如果框架内有一些技术提供类似的东西,它会更好,但我还没有找到.

I know this is not so nice and it would be better, if there is some technology within the framework providing something like this, but I did not found it yet.

你也可以看看这两件事.一个帮助我在控制响应.另一个仍然打开,因为您需要 .NET Framework 4.5:

You may look also at those two things. One helped me much in control responsive. The other is still open, since you need .NET Framework 4.5:

  1. SuspendLayoutResumeLayout 的操作不是很好.你可以试试这个:

  1. SuspendLayout and ResumeLayout don't operate very nice. You may try this:

/// <summary>
/// An application sends the WM_SETREDRAW message to a window to allow changes in that 
/// window to be redrawn or to prevent changes in that window from being redrawn.
/// </summary>
private const int WM_SETREDRAW = 11; 

/// <summary>
/// Suspends painting for the target control. Do NOT forget to call EndControlUpdate!!!
/// </summary>
/// <param name="control">visual control</param>
public static void BeginControlUpdate(Control control)
{
    Message msgSuspendUpdate = Message.Create(control.Handle, WM_SETREDRAW, IntPtr.Zero,
          IntPtr.Zero);

    NativeWindow window = NativeWindow.FromHandle(control.Handle);
    window.DefWndProc(ref msgSuspendUpdate);
}

/// <summary>
/// Resumes painting for the target control. Intended to be called following a call to BeginControlUpdate()
/// </summary>
/// <param name="control">visual control</param>
public static void EndControlUpdate(Control control)
{
    // Create a C "true" boolean as an IntPtr
    IntPtr wparam = new IntPtr(1);
    Message msgResumeUpdate = Message.Create(control.Handle, WM_SETREDRAW, wparam,
          IntPtr.Zero);

    NativeWindow window = NativeWindow.FromHandle(control.Handle);
    window.DefWndProc(ref msgResumeUpdate);
    control.Invalidate();
    control.Refresh();
}

  • Dispatcher.Yield

    这篇关于并行生成 UI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆