3

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.

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:

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

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

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:

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

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

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:

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

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.

Dave New
  • 38,496
  • 59
  • 215
  • 394
Bent Rasmussen
  • 5,538
  • 9
  • 44
  • 63
  • In the sample you are creating a TextBlock. In real life what are the complex UIElements? – paparazzo Dec 04 '12 at 21:35
  • In real-life there is a mixture of Grid, TextBlock with colored code, Runs, Spans, HyperLinks, etc. (To make a code viwer) But even with the simple proof-of-concept solution where we just use TextBlock with Run's we can't get it to run fast. We have also looked at the Avalon control but it is line-based and therefore not well-suited for our application since we have a semi-structured mixture of controls (Grid's, TextBlock's, etc.) – Bent Rasmussen Dec 04 '12 at 21:43
  • 1
    I am working on an answer were I suggest a different approach. – paparazzo Dec 04 '12 at 21:45
  • Cool, Blam! Thanks for your effort. :-) – Bent Rasmussen Dec 04 '12 at 21:48

4 Answers4

23

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.

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.

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:

  1. SuspendLayout and ResumeLayout don'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

bluish
  • 26,356
  • 27
  • 122
  • 180
Patrick
  • 907
  • 9
  • 22
  • 2
    Thumbs up for the awesome and clean implementation (pinvoke-free!) I think that the call to Invalidate() could be omitted since Refresh() makes sure to call invalidate internally anyways. Feel free to correct me if I'm wrong though. – XDS Jul 16 '16 at 16:57
  • 4
    What a legend. You're invited to my wedding if you want – Jack Nov 27 '18 at 04:36
  • BeginControlUpdate / EndControlUpdate fixed a nasty issue I had with adding new controls into a tab page not docking/filling properly. Thank you very much. – Coxy May 27 '19 at 01:43
  • Great piece of code thanks! I'd advise nesting this code into a IDisposable class so EndControlUpdate() is implicitly called in a finally block https://stackoverflow.com/a/20703701/27194 – Patrick from NDepend team Dec 03 '19 at 07:11
2

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.

Edit:

From here:

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

SynchronizationContext is 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 SynchronizationContext cannot really cross threads.

Community
  • 1
  • 1
N_A
  • 19,799
  • 4
  • 52
  • 98
  • 1
    We are only attempting to generate the items on a separate thread, we haven't actually added them to the list off-thread. But it looks like it's just not possible to parallelize UIElement *generation*. – Bent Rasmussen Dec 04 '12 at 21:21
  • From the msdn page for `Task.ContinueWith()`: "Creates a continuation that executes asynchronously when the target Task completes." ContinueWith is async and thus not suitable for calling things which change the UI. http://msdn.microsoft.com/en-us/library/dd321405.aspx – N_A Dec 04 '12 at 21:25
  • Yes, but there is an overload where you can control on which SynchronizationContext the code is executed: http://msdn.microsoft.com/en-us/library/dd321307.aspx – Bent Rasmussen Dec 04 '12 at 21:32
  • See my edit. `SynchronizationContext` is not useful for UI threading. – N_A Dec 04 '12 at 21:47
1

Have you tried:

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

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

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 ObservableBase is doing

sa_ddam213
  • 42,848
  • 7
  • 101
  • 110
1

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

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

Consider FlowDocument for Dynamic content.

FlowDocument Class

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

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

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

paparazzo
  • 44,497
  • 23
  • 105
  • 176
  • PriorityBinding is interesting, we've looked at that previously but the async aspect will probably cause our code to fail again because the UIElement is still generated off-thread - in both the slow and fast path. FlowDocument might be an option, not sure. If we can create it on a background thread then it's certainly a solution to the STA problem. Then we just need to assure that we can use controls analogous to the Grid to layout things like parameters and locals and also create the Grid in parallel so that we don't end up with the same problem again due to e.g. Grid having a STA dependency. – Bent Rasmussen Dec 04 '12 at 22:06
  • Actually, if you mean having one row for the procedure header and one row for the FlowDocument (slow-to-generate) body, then this could be a viable option. Of course not as clean as having one element per procedure but not much less so. – Bent Rasmussen Dec 04 '12 at 22:19
  • Not sure what you mean by create the "Grid in parallel". Since FlowDocument is content (not UI) can create it in the background. I do exactly that on some results of 1000 rows with one property a complex FlowDocument. – paparazzo Dec 04 '12 at 22:20
  • Okay, it just quacks and walks like a duck (UI) ;-) .. The point was that if we had one element per procedure, then we would need to be able to add tabular data to the FlowDocument which of course requires the object representing the tabular data (procedure header) to be able to be constructed in another thread. If it's possible to add a Grid to a FlowDocument, then that means that we can't have Grid throw a STA error in its constructor just because it was called from another thread. But as I wrote above we could split each procedure into 2 elements (header and body). – Bent Rasmussen Dec 04 '12 at 22:25
  • We'll make some experiments first. For now, thanks for your help. – Bent Rasmussen Dec 04 '12 at 22:30
  • You don't add a Grid as in a the UI element to the FlowDocument. FlowDocument has a table http://msdn.microsoft.com/en-us/library/ms747133.aspx – paparazzo Dec 04 '12 at 22:31