UI的并行生成(Parallel Generation of UI)

2019-07-04 12:58发布

我们有一个具有与高速缓存的VirtualizingStackPanel一个ListBox WPF应用程序。 不是因为它有大量的许多元素(在极端情况下,通常低于20,但也许多达100个或更多),而是因为元素需要时间来产生。 元素实际上的UIElement对象。 因此,应用程序需要动态生成UI元素。

问题是,尽管虚拟化似乎工作,应用程序仍然慢慢的响应,这是概念的解决方案证明用最少的“噪音”。

因此,我们计算过,因为主要的问题是,我们动态地生成复杂的UIElement对象,我们需要做的是,在并行,即关闭线程。 但是,我们得到一个需要在STA线程中运行代码中的错误:

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

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

这是我们从概念解决方案的论证相关的代码片段:

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的:

<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。

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

在关闭情况下,让我们的应用程序的作用是创建代码视图,其中单是这又包含结构化内容的混合过程(参数一方面局部变量,语句和表达式的除外)。

换句话说我们的UIElement对象过于复杂,仅仅通过数据绑定创建。

我们有另外的想法是使用在XAML“异步”的设置,因为它似乎可能创造“无阻塞UI”,但我们一直没能实现这一点,因为我们得到了同样的错误如上:

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

堆栈跟踪:

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: 

编辑:

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

Answer 1:

我很痛苦在普通的C#环境相同的问题。 我也尝试过很多事情。 你计算的控件的大小调整父的大小提前? 我很遗憾这样做。

您也可创建控制动态嵌套你的孩子。 通过您可以创建类的一个的UIElement适配器。 该适配器在启动时创建的,并拥有所有信息来创建UI元素。 该适配器可以创建要求孩子们在STA线程只是时间的需求。 当向上或向下滚动,你可以事先在您滚动的方向创造孩子。 这样,您就可以与如5-10 UI元素开始,然后您可以通过更多的向上滚动计算。

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

您也可以看看这两件事情。 一个帮助我很多的响应控制。 另外仍然是开放的,因为你需要的.NET Framework 4.5:

  1. SuspendLayoutResumeLayout不工作很不错。 你可以试试这个:

     /// <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



Answer 2:

你无法从不同的线程改变UI线程上的项目。 如果你有它处理实际添加项目到UI在UI线程上的委托它应该工作。

编辑:

从这里 :

它似乎有使用的更深层次的问题SynchronizationContext的UI线程。

SynchronizationContext绑在与COM +支持,旨在跨线程。 在WPF你不能跨越多个线程调度,所以一个SynchronizationContext不能真正跨线程。



Answer 3:

如果它只是一个一行模板,然后考虑的ListView GridView控件。

至于动态内容,而动态然后UI元素使用显示格式化的内容(运行时,超链接,表)的单个UI元素。

考虑FlowDocument的动态内容。

FlowDocument的类

该FlowDocument的可以在后台创建。
另请参阅优先结合。
PriorityBinding类

然后,你可以用FlowDocumentScrollViewer或其他三个选项显示。

我怀疑将UI元素动态打破虚拟化,因为它不能再使用UI元素..



Answer 4:

你有没有尝试过:

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

或者,如果你的魔杖代码去异步的背后, Dispatcher可以帮助

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

刚刚测试你下面的代码,我没有错误:

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

但是我不知道是什么ObservableBase



文章来源: Parallel Generation of UI