并行生成UI

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

问题在于,即使虚拟化似乎有效,但应用程序仍然很难做出响应,这是一个概念validation解决方案,具有最小的“噪音”。

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

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

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

这是我们的概念validation解决方案中的相关代码片段:

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:

       <!---->                  

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

      

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

换句话说,我们的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.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)添加了堆栈跟踪。

我在正常的c#环境中遇到了同样的问题。 我也尝试过很多东西。 您是否计算了控件的大小以提前调整父级的大小? 我很遗憾地这样做。

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

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

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

  1. SuspendLayoutResumeLayout运行不太好。 你可以试试这个:

     ///  /// 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. ///  private const int WM_SETREDRAW = 11; ///  /// Suspends painting for the target control. Do NOT forget to call EndControlUpdate!!! ///  /// visual control 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); } ///  /// Resumes painting for the target control. Intended to be called following a call to BeginControlUpdate() ///  /// visual control 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

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

编辑:

从这里 :

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

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

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

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

考虑FlowDocument for Dynamic内容。

FlowDocument类

FlowDocument可以在后台创建。
另见优先级绑定。
PriorityBinding类

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

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

你有没有尝试过:

  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 myVar = new ObservableCollection(); public ObservableCollection 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; ///  /// Notifies the property changed. ///  /// The info. public void NotifyPropertyChanged(String info) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(info)); } } } 

但是我不知道ObservableBase在做什么