wpf 并行生成 UI

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13711812/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 06:28:01  来源:igfitidea点击:

Parallel Generation of UI

c#wpfxamlparallel-processingtask-parallel-library

提问by Bent Rasmussen

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.

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

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

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:

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

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

调用线程必须是 STA,因为许多 UI 组件都需要它。

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

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

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

    // ...
}

and here is a relevant piece of XAML:

这是一段相关的 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>

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

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

<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.)

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

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

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

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:

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

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

调用线程必须是 STA,因为许多 UI 组件都需要它。

Stacktrace:

堆栈跟踪:

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:\Users\Christian\Desktop\WPF4.5_VirtualizingStackPanelNewFeatures\WPF4.5_VirtualizingStackPanelNewFeatures\Person.cs:line 84
       at WPF4._5_VirtualizingStackPanelNewFeatures.Person.<ParallelGenerateUI>b__2() in c:\Users\Christian\Desktop\WPF4.5_VirtualizingStackPanelNewFeatures\WPF4.5_VirtualizingStackPanelNewFeatures\Person.cs:line 68
       at System.Threading.Tasks.Task`1.InnerInvoke()
       at System.Threading.Tasks.Task.Execute()
  InnerException: 

Edits:

编辑:

1) Added more XAML. 2) Added stacktrace.

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

回答by Patrick

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.

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

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.

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

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.

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

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:

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

  1. SuspendLayoutand ResumeLayoutdon'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();
    }
    
  2. Dispatcher.Yield

  1. SuspendLayout并且ResumeLayout不要操作得很好。你可以试试这个:

    /// <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();
    }
    
  2. Dispatcher.Yield

回答by N_A

You can't change items on the UI thread from a different thread. It should work if you have a delegate on the UI thread which handles actually adding the item to the UI.

您不能从不同的线程更改 UI 线程上的项目。如果您在 UI 线程上有一个委托来处理实际将项目添加到 UI,它应该可以工作。

Edit:

编辑:

From here:

这里

It appears there are deeper issues with using the SynchronizationContextfor UI threading.

使用SynchronizationContextfor UI 线程似乎存在更深层次的问题。

SynchronizationContextis tied in with the COM+ support and is designed to cross threads. In WPF you cannot have a Dispatcher that spans multiple threads, so one SynchronizationContextcannot really cross threads.

SynchronizationContext与 COM+ 支持捆绑在一起,旨在跨线程。在 WPF 中,您不能拥有跨多个线程的 Dispatcher,因此SynchronizationContext不能真正跨线程。

回答by paparazzo

If it is just a one row template then consider ListView GridView.

如果它只是一个单行模板,那么请考虑 ListView GridView。

As for dynamic content rather then dynamic UI elements use a single UI element that displays formatted content (runs, hyperlink, table).

至于动态内容而不是动态 UI 元素,则使用单个 UI 元素来显示格式化的内容(运行、超链接、表格)。

Consider FlowDocument for Dynamic content.

考虑用于动态内容的 FlowDocument。

FlowDocument Class

流文档类

The FlowDocument can be created in background.
Also see priority binding.
PriorityBinding Class

FlowDocument 可以在后台创建。
另请参阅优先绑定。
优先绑定类

Then you can display it with FlowDocumentScrollViewer or three other options.

然后您可以使用 FlowDocumentScrollViewer 或其他三个选项显示它。

I suspect adding UI elements dynamically breaks virtualization as it cannot reuse UI elements..

我怀疑动态添加 UI 元素会破坏虚拟化,因为它无法重用 UI 元素。

回答by sa_ddam213

Have you tried:

你有没有尝试过:

 ItemsSource="{Binding Persons, IsAsync=True}"

Or if you wand to go async in code behind, Dispatchercan help

或者,如果您想在后面的代码中进行异步,Dispatcher可以提供帮助

private void ParallelGenerateUI()
{
    Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)delegate()
    {
       _UI = GenerateUI();
        RaisePropertyChanged("UI");
    });
}

Just tested you code below and I get no errors:

刚刚测试了你下面的代码,我没有收到任何错误:

public partial class MainWindow : Window 
{

    public MainWindow()
    {
        InitializeComponent();
        for (int i = 0; i < 10000; i++)
        {
            Persons.Add(new Person());
        }
    }

    private ObservableCollection<Person> myVar = new ObservableCollection<Person>();
    public ObservableCollection<Person> Persons
    {
        get { return myVar; }
        set { myVar= value; }
    }
}

  public class Person : INotifyPropertyChanged
{
    // ...

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

    private void ParallelGenerateUI()
    {
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)delegate()
        {

            _UI = GenerateUI();
            NotifyPropertyChanged("UI");
        });

    }

    private UIElement GenerateUI()
    {
        Random rnd = new Random();

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

    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Notifies the property changed.
    /// </summary>
    /// <param name="info">The info.</param>
    public void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

However I do not know what ObservableBaseis doing

但是我不知道ObservableBase在做什么