How to improve performance of WPF Grid control (.NET 4.0/4.5)?

人走茶凉 提交于 2019-12-11 02:39:55

问题


Definition: Having 2D-array of string (about 10 columns, 1,600 rows, fixed length of 7-char) serving as a data source for WPF .NET 4.0 Grid control, the following code snippet has been used in order to populate the Grid with labels displaying values from array. Note: Grid was added to XAML and passed to the function PopulateGrid (see Listing 1.). The visual output is essentially a tabular data representation in read-only mode (no need for two-way binding).

Problem: Performance is a key issue. It took a mind-boggling 3...5 sec to complete this operation running on powerful Intel-i3/8GB-DDR3 PC; therefore, this WPF Grid performance is, IMHO, at least an order of magnitude slower than expected, based on comparison with similar controls/tasks in, e.g. regular WinForm data-aware controls, or even Excel worksheet.

Question 1: if there is a way to improve the performance of WPF Grid in scenario described above? Please direct your answer/potential improvement to the code snippet provided below in Listing 1 and Listing 2.

Question 1a: proposed solution could implement data binding to additional data-aware control, like for example DataGrid to DataTable. I've added string[,] to DataTable dt converter in Listing 2, so that additional control's DataContext (or ItemsSource, whatever) property could be bound to dt.DefaultView. So, in the simplest form, could you please provide a compact (desirably about couple lines of code as it was done in old-style data-aware controls) and efficient (performance-wise) solution on data-binding of WPF DataGrid to DataTable object ?

Many Thanks.

Listing 1. Procedure to populate WPF Grid GridOut from 2D string[,] Values

#region Populate grid with 2D-array values
/// <summary>
/// Populate grid with 2D-array values
/// </summary>
/// <param name="Values">string[,]</param>
/// <param name="GridOut">Grid</param>
private void PopulateGrid(string[,] Values, Grid GridOut)
{
    try
    {
        #region clear grid, then add ColumnDefinitions/RowsDefinitions

        GridOut.Children.Clear();
        GridOut.ColumnDefinitions.Clear();
        GridOut.RowDefinitions.Clear();

        // get column num
        int _columns = Values.GetUpperBound(1) + 1;

        // add ColumnDefinitions
        for (int i = 0; i < _columns; i++)
        {
            GridOut.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
        }

        // get rows num
        int _rows = Values.GetUpperBound(0) + 1;

        // add RowDefinitions
        for (int i = 0; i < _rows; i++)
        {
            GridOut.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
        }
        #endregion

        #region populate grid w/labels
        // populate grid w/labels
        for (int i = 0; i < _rows; i++)
        {
            for (int j = 0; j < _columns; j++)
            {
                // new Label control
                Label _lblValue = new Label();

                // assign value to Label
                _lblValue.Content = Values[i, j].ToString();

                // add Label to GRid
                GridOut.Children.Add(_lblValue);
                Grid.SetRow(_lblValue, i);
                Grid.SetColumn(_lblValue, j);
            }
        }
        #endregion
    }
    catch
    {
        GridOut.Children.Clear();
        GridOut.ColumnDefinitions.Clear();
        GridOut.RowDefinitions.Clear();
    }
}
#endregion

Listing 2. string[,] to DataTable conversion

#region internal: Convert string[,] to DataTable
/// <summary>
/// Convert string[,] to DataTable
/// </summary>
/// <param name="arrString">string[,]</param>
/// <returns>DataTable</returns>
internal static DataTable Array2DataTable(string[,] arrString)
{
    DataTable _dt = new DataTable();
    try
    {
        // get column num
        int _columns = arrString.GetUpperBound(1) + 1;

        // get rows num
        int _rows = arrString.GetUpperBound(0) + 1;

        // add columns to DataTable
        for (int i = 0; i < _columns; i++)
        {
            _dt.Columns.Add(i.ToString(), typeof(string));
        }

        // add rows to DataTable
        for (int i = 0; i < _rows; i++)
        {
            DataRow _dr = _dt.NewRow();
            for (int j = 0; j < _columns; j++)
            {
                _dr[j] = arrString[i,j];
            }
            _dt.Rows.Add(_dr);
        }
        return _dt;
    }
    catch { throw; }
}
#endregion

Note 2. It's recommended to replace Label control w/TextBlock using its Text property instead of Content as in case of Label. It will speed up the execution a little bit, plus the code snippet will be forward compatible with VS 2012 for Win 8, which doesn't include Label.

Note 3: So far I've tried binding DataGrid to DataTable (see XAML in Listing 3), but performance is very poor (grdOut is a nested Grid, that was used as a container for tabular data; _dataGrid is a data-aware object type of DataGrid).

Listing 3. DataGrid binding to DataTable: performance was poor, so I've removed that ScrollViewer and not it's running OK.

<ScrollViewer ScrollViewer.CanContentScroll="True" VerticalScrollBarVisibility="Auto" >
    <Grid Name="grdOut">
            <DataGrid AutoGenerateColumns="True" Name="_dataGrid" ItemsSource="{Binding Path=.}" />
    </Grid>
</ScrollViewer>

回答1:


Ok. Delete all your code and start all over.

This is my take on a "Dynamic Grid" of Labels with X number of rows and Y number of columns based off a 2D string array:

<Window x:Class="MiscSamples.LabelsGrid"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LabelsGrid" Height="300" Width="300">
    <DockPanel>

        <Button DockPanel.Dock="Top" Content="Fill" Click="Fill"/>

        <ItemsControl ItemsSource="{Binding Items}"
                      ScrollViewer.HorizontalScrollBarVisibility="Auto"
                      ScrollViewer.VerticalScrollBarVisibility="Auto"
                      ScrollViewer.CanContentScroll="true"
                      ScrollViewer.PanningMode="Both">
            <ItemsControl.Template>
                <ControlTemplate>
                    <ScrollViewer>
                        <ItemsPresenter/>
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <ItemsControl ItemsSource="{Binding Items}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Label Content="{Binding}"/>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <UniformGrid Rows="1"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                    </ItemsControl>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel VirtualizationMode="Recycling" IsVirtualizing="True"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </DockPanel>
</Window>

Code Behind:

public partial class LabelsGrid : Window
{
    private LabelsGridViewModel ViewModel { get; set; }

    public LabelsGrid()
    {
        InitializeComponent();
        DataContext = ViewModel = new LabelsGridViewModel();
    }

    private void Fill(object sender, RoutedEventArgs e)
    {
        var array = new string[1600,20];

        for (int i = 0; i < 1600; i++)
        {
            for (int j = 0; j < 20; j++)
            {
                array[i, j] = "Item" + i + "-" + j;
            }
        }

        ViewModel.PopulateGrid(array);
    }
}

ViewModel:

public class LabelsGridViewModel: PropertyChangedBase
{
    public ObservableCollection<LabelGridItem> Items { get; set; } 

    public LabelsGridViewModel()
    {
        Items = new ObservableCollection<LabelGridItem>();
    }

    public void PopulateGrid(string[,] values)
    {
        Items.Clear();

        var cols = values.GetUpperBound(1) + 1;
        int rows = values.GetUpperBound(0) + 1;

        for (int i = 0; i < rows; i++)
        {
            var item = new LabelGridItem();

            for (int j = 0; j < cols; j++)
            {
                item.Items.Add(values[i, j]);
            }

            Items.Add(item);
        }
    }
}

Data Item:

public class LabelGridItem: PropertyChangedBase
{
    public ObservableCollection<string> Items { get; set; }

    public LabelGridItem()
    {
        Items = new ObservableCollection<string>();
    }
}

PropertyChangedBase class (MVVM Helper)

public class PropertyChangedBase:INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        Application.Current.Dispatcher.BeginInvoke((Action) (() =>
                                                                 {
                                                                     PropertyChangedEventHandler handler = PropertyChanged;
                                                                     if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
                                                                 }));
    }
}

Result:

  • Performance is AWESOME. Notice I'm using 20 columns instead of the 10 you suggested. The Filling of the grid is IMMEDIATE when you click the button. I'm sure performance is much better than crappy dinosaur winforms due to Built-in UI Virtualization.

  • The UI is defined in XAML, as opposed to creating UI elements in procedural code, which is a bad practice.

  • The UI and data are kept separate, thus increasing maintainability and scalability and cleanliness.

  • Copy and paste my code in a File -> New -> WPF Application and see the results for yourself.

  • Also, keep in mind that if you're only going to display text, you'd better use a TextBlock instead of a Label, which is a much lightweight Text element.

  • WPF rocks, even if at edge cases it might present performance degradation, it's still 12837091723 better than anything currently in existence.

Edit:

I went ahead and added 0 zeros to the row count (160000). Performance is still acceptable. It took less than 1 second to populate the Grid.

Notice that the "Columns" are NOT being virtualized in my example. This can lead to performance issues if there's a big number of them, but that's not what you described.

Edit2:

Based on your comments and clarifications, I made a new example, this time based in a System.Data.DataTable. No ObservableCollections, no async stuff (there was nothing async in my previous example anyways). And just 10 columns. Horizontal Scrollbar was there due to the fact that the window was too small (Width="300") and was not enough to show the data. WPF is resolution independent, unlike dinosaur frameworks, and it shows scrollbars when needed, but also stretches the content to the available space (you can see this by resizing the window, etc).

I also put the array initializing code in the Window's constructor (to deal with the lack of INotifyPropertyChanged) so it's going to take a little bit more to load and show it, and I noticed this sample using System.Data.DataTable is slightly slower than the previous one.

However, I must warn you that Binding to Non-INotifyPropertyChanged objects may cause a Memory Leak.

Still, you will NOT be able to use a simple Grid control, because it does not do UI Virtualization. If you want a Virtualizing Grid, you will have to implement it yourself.

You will also NOT be able to use a winforms approach to this. It's simply irrelevant and useless in WPF.

    <ItemsControl ItemsSource="{Binding Rows}"
                  ScrollViewer.HorizontalScrollBarVisibility="Auto"
                  ScrollViewer.VerticalScrollBarVisibility="Auto"
                  ScrollViewer.CanContentScroll="true"
                  ScrollViewer.PanningMode="Both">
        <ItemsControl.Template>
            <ControlTemplate>
                <ScrollViewer>
                    <ItemsPresenter/>
                </ScrollViewer>
            </ControlTemplate>
        </ItemsControl.Template>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <ItemsControl ItemsSource="{Binding ItemArray}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding}"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <UniformGrid Rows="1"/>
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                </ItemsControl>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel VirtualizationMode="Recycling" IsVirtualizing="True"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>

Code Behind:

public partial class LabelsGrid : Window
{
    public LabelsGrid()
    {
        var array = new string[160000, 10];

        for (int i = 0; i < 160000; i++)
        {
            for (int j = 0; j < 10; j++)
            {
                array[i, j] = "Item" + i + "-" + j;
            }
        }

        DataContext = Array2DataTable(array);
        InitializeComponent();
    }

    internal static DataTable Array2DataTable(string[,] arrString)
    {
        //... Your same exact code here
    }
}

Bottom line is to do something in WPF you have to do it the WPF way. It's not just a UI framework, it's more of an Application Framework by itself.

Edit3:

<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding}"/>

 DataContext = Array2DataTable(array).DefaultView;

Works perfectly fine for me. Loading time is not noticeable with 160000 rows. What .Net framework version are you using?



来源:https://stackoverflow.com/questions/16659265/how-to-improve-performance-of-wpf-grid-control-net-4-0-4-5

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!