3

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

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

所以我们认为,由于主要问题是我们动态生成复杂的 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) 添加了堆栈跟踪。

4

4 回答 4

21

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

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

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

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

  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

于 2013-02-22T08:51:19.683 回答
2

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

编辑:

这里

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

SynchronizationContext与 COM+ 支持相关联,旨在跨线程。在 WPF 中,您不能拥有跨越多个线程的 Dispatcher,因此SynchronizationContext不能真正跨线程。

于 2012-12-04T21:06:30.583 回答
1

你有没有尝试过:

 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在做什么

于 2012-12-04T21:06:00.617 回答
1

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

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

考虑动态内容的 FlowDocument。

流文档类

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

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

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

于 2012-12-04T21:54:09.243 回答