Control.AddRange(...) is slow
Asked Answered
B

1

3

Project: I have a parent panel which holds a ComboBox and FlowLayoutPanel. The FlowLayoutPanel holds a variable number of child panels (a custom control that inherits from UserControl). Each child panel contains some labels, two ComboBoxes, a button, and a DataGridView with 3 ComboBox columns and a button column. The DataGridView may have 1-6 rows. The FlowLayoutPanel is populated with child panels when an item is selected from the ComboBox on the parent panel.

enter image description here

Problem: Populating the FlowLayoutPanel with about 50 child panels takes about 2.5 seconds. Specifically, I've determined that the call to FlowLayoutPanel.Controls.AddRange() is the culprit.

Relevant Code: I can't post all of my code here (too much code plus parts of it are confidential), but I'll do my best to explain what is happening.

Parent Panel:

private void displayInformation(Suite suite)
{
    this.SuspendLayout();

    // Get dependencies.
    List<SuiteRange> dependents = new List<SuiteRange>(suite.dependencies.Keys);
    dependents.Sort(SuiteRange.Compare);

    // Create a ChildPanel for each dependent.
    List<ChildPanel> rangePanels = new List<ChildPanel>();
    foreach (SuiteRange dependent in dependents)
    {
        ChildPanel sdp = new ChildPanel();
        sdp.initialize(initialSuite.name, dataAccess);
        sdp.displayInformation(dependent, suite.dependencies[dependent]);
        rangePanels.Add(sdp);
    }

    // Put the child panels in the FlowLayoutPanel.
    flpDependencyGroups.SuspendLayout();
    // Takes ~2.5 seconds
    flpDependencyGroups.Controls.AddRange(rangePanels.ToArray());
    flpDependencyGroups.ResumeLayout();

    // Takes ~0.5 seconds
    updateChildPanelSizes();

    this.ResumeLayout();
}

Things I've tried:

  • Call SuspendLayout() / ResumeLayout() on the parent panel and/or FlowLayoutPanel. Minimal performance increase (~0.2 seconds).
  • Use Control.FlatStyle.Flat on ComboBoxes, Buttons, and DataGridView columns. Minimal performance increase (~0.1 seconds).
  • Verified that none of my controls use a transparent background color.
  • Set ChildPanel.DoubleBuffered and ParentPanel.DoubleBuffered to true.
  • Remove the FlowLayoutPanel from its parent before calling AddRange() and re-adding it after.

Things that might be relevant:

  • The panels and controls use anchors (as opposed to autosize or dock).
  • My controls are manually populated and do not use the DataSource property.

EDIT: Solution:

@HighCore's answer is the correct solution. Unfortunately I won't be implementing it at this time (it could happen down the road) because I found a workaround. The workaround doesn't really solve the problem, just masks it, hence why I'm not posting this as an answer. I discovered that the form loads in half the time if the Dependencies tab isn't on top (i.e. the Product Lists tab is selected). This reduces loading time to about 1 second, which is acceptable. When data is being loaded and the Dependencies tab is on top, I switch to the Product Lists tab, throw up a dark grey box over the tab control that says "Loading..." in the middle, load the data, and then switch back to the Dependencies tab.

Thanks all for your comments and suggestions, it was greatly appreciated.

Bysshe answered 7/4, 2014 at 20:17 Comment(17)
solution: WPF. winforms doesn't support big UIsMila
Can you set WrapContents to false before you add the controls and reset it to true afterwards, so that, possibly, the layout is performed only once. (Shouldn't help as the xxRange() ought to imply it, but who knows..)Motherly
Probably wrong approach to design your UI, show a screen shot and we'll try to help you.Osvaldooswal
Such a busy UI is bound to be a bit sluggish in WinForms. While it may not be ideal, i.e. it's a workaround rather than a solution, you might try hiding or removing the FLP before adding the children and then showing or adding it again afterwards. Not sure whether that would help or not, hence the comment rather than an answer.Irritative
@TaW: WrapContents is already false.Bysshe
@Aybe: screenshot added.Bysshe
@jmcilhinney: I tried this and saw no change in the performance.Bysshe
Looking at the screenshot another approach came to my mind: Maybe you could let go of the scenario of creating all those visual objects beforehand and instead only create as many as your window layout can hold, 4 or 5. Then implement custom scrolling and either add new panels on the fly or replace the first batch with the new ones. The data should be held ready in objects..Motherly
And then there is the hybrid solution: Show one screenful in one go and then create the rest in a second longer call which would happen while the user has things to look at. 2.5 seconds of waiting is a lot but 2.5 seconds of looking a data is not..Motherly
I have created a rough mockup of your screenshot in WPF. With 10000 data items, response time is immediate due to WPF's built in UI Virtualization. You could integrate that in your existing winforms application via the ElementHost. Let me know if that is an option for you and I'll post an answer with full source code.Mila
It's an extreme solution but I agree with @HighCore, the move to WPF is worth it if you can afford it.Osvaldooswal
@Aybe why is it an "extreme" solution? I would say it's much less "extreme" that someone's recommended solution "buy Telerik which costs $1000 US dollars"Mila
@HighCore actually we don't really know if the OP can do that, he's the only judge about it. Now obviously yours is less extreme since its cost is 0$ :DOsvaldooswal
@HighCore Yes, I'm interested in seeing that mockup you mentioned. I'd prefer to keep everything in WinForms, but unless I find a solution in WinForms I may be forced to go with WPF (I'm not opposed to WPF, I just don't want to unnecessarily mix technologies).Bysshe
@Bysshe one thing I noticed is that you tagged your question [.net-2.0], my proposed solution would only work in .Net 3.0 and above, and will probably work much better in 4.0 and above.Mila
@TaW: Similar to your suggestions, I could page the controls in batches of 10 or so. Of course, that brings its own problems, but might be a last-ditch solution.Bysshe
@HighCore: Upgrading to .net 4.0 is a possibility.Bysshe
M
5

Posting this answer because the OP requested it:

This is how you'd do something like that in WPF:

<UserControl x:Class="WpfApplication7.ListBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <DockPanel>
        <Button Content="Load" Click="Load_Click" DockPanel.Dock="Top"/>

        <ListBox ItemsSource="{Binding}"
                 HorizontalContentAlignment="Stretch">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="LightGray" BorderThickness="1" Padding="5"
                            Background="#FFFAFAFA">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>

                            <Grid.ColumnDefinitions>
                                <ColumnDefinition/>
                                <ColumnDefinition/>
                                <ColumnDefinition/>
                                <ColumnDefinition/>
                            </Grid.ColumnDefinitions>

                            <TextBlock Text="Dependent Versions" FontWeight="Bold"
                                       Grid.ColumnSpan="2" HorizontalAlignment="Center"/>

                            <TextBlock Text="From:" FontWeight="Bold"
                                       Grid.Row="1" HorizontalAlignment="Center"/>

                            <TextBlock Text="To (exclusive):" FontWeight="Bold"
                                       Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center"/>

                            <ComboBox SelectedItem="{Binding From}"
                                      ItemsSource="{Binding FromOptions}"
                                      Grid.Row="2" Margin="5"/>

                            <ComboBox SelectedItem="{Binding To}"
                                      ItemsSource="{Binding ToOptions}"
                                      Grid.Row="2" Grid.Column="1" Margin="5"/>

                            <DataGrid ItemsSource="{Binding ChildItems}"
                                      AutoGenerateColumns="False" CanUserAddRows="False"
                                      Grid.Column="2" Grid.RowSpan="4">
                                <DataGrid.Columns>
                                    <DataGridTextColumn Header="XXXX" Binding="{Binding XXXX}"/>
                                    <DataGridTextColumn Header="Dependee From" Binding="{Binding DependeeFrom}"/>
                                    <DataGridTextColumn Header="Dependee To" Binding="{Binding DependeeTo}"/>
                                    <DataGridTemplateColumn Width="25">
                                        <DataGridTemplateColumn.CellTemplate>
                                            <DataTemplate>
                                                <Button Content="X"/>
                                            </DataTemplate>
                                        </DataGridTemplateColumn.CellTemplate>
                                    </DataGridTemplateColumn>

                                </DataGrid.Columns>
                            </DataGrid>

                            <Button Content="Delete"
                                    Grid.Column="3"
                                    HorizontalAlignment="Right" VerticalAlignment="Top"/>

                        </Grid>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </DockPanel>
</UserControl>

Code Behind (only boilerplate to support the example)

public partial class ListBoxSample : UserControl
{
    public ListBoxSample()
    {
        InitializeComponent();
    }

    public void LoadData()
    {
        Task.Factory.StartNew(() =>
        {
            var list = new List<DataItem>();

            for (int i = 0; i < 100000; i++)
            {
                var item = new DataItem()
                {
                    From = "1",
                    To = "2",
                    ChildItems =
                    {
                        new ChildItem()
                        {
                            DependeeFrom = i.ToString(),
                            DependeeTo = (i + 10).ToString(),
                            XXXX = "XXXX"
                        },
                        new ChildItem()
                        {
                            DependeeFrom = i.ToString(),
                            DependeeTo = (i + 10).ToString(),
                            XXXX = "XXXX"
                        },
                        new ChildItem()
                        {
                            DependeeFrom = i.ToString(),
                            DependeeTo = (i + 10).ToString(),
                            XXXX = "XXXX"
                        }
                    }
                };

                list.Add(item);
            }
            return list;

        }).ContinueWith(t =>
        {
            Dispatcher.Invoke((Action) (() => DataContext = t.Result));
        });
    }

    private void Load_Click(object sender, System.Windows.RoutedEventArgs e)
    {
        LoadData();
    }
}

Data Items:

public class DataItem
{
    public List<ChildItem> ChildItems { get; set; }

    public List<string> FromOptions { get; set; }

    public List<string> ToOptions { get; set; }

    public string From { get; set; }

    public string To { get; set; }

    public DataItem()
    {
        ChildItems = new List<ChildItem>();

        FromOptions = Enumerable.Range(0,10).Select(x => x.ToString()).ToList();
        ToOptions = Enumerable.Range(0, 10).Select(x => x.ToString()).ToList();
    }
}

public class ChildItem
{
    public string XXXX { get; set; }

    public string DependeeFrom { get; set; }

    public string DependeeTo { get; set; }
}

Then you put that in an existing winforms UI using an ElementHost:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        var elementHost = new ElementHost
        {
            Dock = DockStyle.Fill,
            Child = new ListBoxSample()
        };

        Controls.Add(elementHost);

    }
}

Result:

enter image description here

  • Notice that I added 100,000 records. Still, response time (both when scrolling and interacting with the UI) is immediate due to WPF's built in UI Virtualization.
  • Also notice that I'm using DataBinding which removes the need to manipulate UI elements in procedural code. This is important because the WPF Visual Tree is a complex structure, and DataBinding is the preferred approach in WPF always.
  • Also notice by resizing the form that the UI is completely resolution independent. You can customize it further by making the ComboBoxes fixed and having the DataGrid stretch to the remaining space. See WPF Layouts.
  • WPF Rocks. - see how much you can achieve with so little code, and without spending lots of $$$ in third party controls. You should really forget winforms forever.
  • You will need to target .Net 3.0 at a minimum, but 4.0/4.5 is highly recommended because WPF had several issues in earlier versions, which were fixed in 4.0.
  • Make sure you reference PresentationCore.dll, PresentationFramework.dll, WindowsBase.dll, System.Xaml.dll and WindowsFormsIntegration.dll, all of which belong to the .Net Framework itself (no 3rd parties)
Mila answered 11/4, 2014 at 21:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.