UI creation in parallel

We have a WPF application that has a ListBox with VirtualizingStackPanel with caching. Not because it has many elements (usually less than 20, but maybe up to 100 or more in extreme cases), but because the elements take time to generate. Elements are actually UIElement objects. Thus, the application must dynamically generate UIElements.

The problem is that while virtualization seems to be working, the application is still responding slowly, and this is proof of a conceptual solution with minimal “noise”.

So, we realized that since the main problem is that we generate complex UIElement objects dynamically, we need to do this in parallel, that is, disable the stream. But we get an error that the code must be running in the STA stream:

The calling thread must be an STA because it requires many user interface components.

Does this mean that we cannot generate UIs (UIElement objects) in a thread other than the main thread of the WPF user interface?

Here's the corresponding code snippet from our proof of conceptual 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 the corresponding XAML fragment:

 <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 bind data to the user interface of properties 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 the final context, what our application does, a code presentation is created in which the list contains procedures that again contain a combination of structured content (for parameters and local variables, on the one hand, and operators and expressions on the other.)

In other words, our UIElement objects are too complex to create using only data binding.

Another thought we had was to use the “Async” settings in XAML, since it seems possible to create a “non-blocking interface”, but we were not able to implement this because we are getting the same error as above:

The calling thread must be an STA because it requires many user interface components.

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: 

Changes:

1) Added more XAML. 2) Added stacktrace.

+1
c # parallel-processing wpf task-parallel-library xaml
source share
4 answers

I have the same problem in a normal C # environment. I also tried many times. Are you calculating the size of the controls to pre-configure the size of the parent? I do this unfortunately.

You can also create a control that dynamically identifies your children. Thanks to this, you can create a UIElement adapter view. The adapter is created at startup and has all the information to create UIElements. The adapter can create the requested children on the STA stream on demand just in time. When scrolling up or down, you can create children in advance in the scroll direction. So you can start, for example. 5-10 user interface elements and then you calculate by scrolling more.

I know that this is not so pleasant, and it would be better if there is some kind of technology in the framework of this technology, but I have not found it yet.

You can look at these two things. One of them helped me a lot in managing. The other is still open since you need the .NET Framework 4.5:

  • SuspendLayout and ResumeLayout do not work very nicely. You can try the following:

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

+11
source share

You cannot modify elements in a user interface thread from another thread. It should work if you have a delegate in the user interface thread that handles the actual addition of the element to the user interface.

Edit:

From here :

There seem to be deeper issues using SynchronizationContext for streaming the user interface.

SynchronizationContext tied to COM + support and designed for cross-threading. In WPF, you cannot have a dispatcher that spans multiple threads, so a single SynchronizationContext cannot really cross-thread.

+2
source share

You tried:

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

Or, if you want to make asynchronous code in code, Dispatcher can help

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

Just checked your code below and I am not getting 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 ObservableBase does

+1
source share

If this is just a single row template, consider a ListView GridView.

As for dynamic content, instead of dynamic user interface elements, one user interface element is used, which displays formatted content (works, hyperlink, table).

Consider a FlowDocument for dynamic content.

Class flowdocument

FlowDocument can be created in the background.
Also see Priority Binding.
PriorityBinding Class

You can then display it using FlowDocumentScrollViewer or three other parameters.

I suspect that adding user interface elements dynamically disrupts virtualization because it cannot reuse user interface elements.

+1
source share

All Articles