3

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>
Alexander Bell
  • 7,842
  • 3
  • 26
  • 42
  • You don't have a performance problem. You have a design problem. You should definitely NOT be doing this in WPF. Use an `ItemsControl` which has built-in UI Virtualization. Post a screenshot of what you need and I can tell you the proper way to do it in WPF. And please, forget the winforms mentality. – Federico Berasategui May 20 '13 at 22:42
  • @HighCore: Thanks for your response. I pretty much understand that there are many other ways to implement this functionality with additional controls, e.g. ListView, or DataGrid, etc. In my question it was specifically stated, that I am looking for the solution pertinent just to Grid control usage. I would consider solutions involving any additional controls only if all other options pertinent to just WPF Grid are found insufficient. Best regards, – Alexander Bell May 20 '13 at 22:56
  • Again, `Grid` is a `Layout`-related UI element. I has NOTHING to do with displaying DATA. I'm preparing an example that uses an `ItemsControl` with dynamically defined columns and stuff. – Federico Berasategui May 20 '13 at 23:04
  • @HighCore: Thanks again. As FYI, I've seen a lot of critical comments regarding performance of other WPF data-aware controls (like ListView or DataGrid), and the key issue was to implement UI Virtualization, which (presumably) gives a huge performance boost. If this indeed is a critical point, then please elaborate on it providing detailed solution, or just link to the existing one. Regards, – Alexander Bell May 20 '13 at 23:09

1 Answers1

6

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:

enter image description here

  • 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?

Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • Thanks again for your input, though it's not what I am looking for. Let me be clear on this: the entire idea is to minimize the surface area of the technology involved, thus no observable collections, no async stuff (blocking execution is just fine), no horizontal scroll bars (as I stated, it must be 10 columns or less). To simplify the task, I've added just ONE additional data object: DataTable dt, so the control to be used to display the tabular data just has to be bound to dt.DefaultView. I posted the code snippet for string[,] to DataTable conversion. Regards, – Alexander Bell May 21 '13 at 00:51
  • @AlexBell What do you mean by `"minimize the surface area of the technology involved"?? WPF does not accept (nor care about) other technologies' ways. If you will do something in WPF, it's *the WPF way or the highway"*. Otherwise you'll suffer a lot, and will not achieve anything useful. WPF doesn't really care about "what you're looking for"... think about it a second time. – Federico Berasategui May 21 '13 at 01:51
  • @AlexBell To be clear on this, the WPF way is actually the XAML way. This means that everything I'm talking about here also applies to all the other XAML-based technologies (WinRT, Silverlight, Windows Phone, etc). The code you posted originally (which creates `Label` elements in procedural code) resembles the old, unmaintainable winforms approach. WPF does not support that. – Federico Berasategui May 21 '13 at 01:54
  • TextBlock vs. Label is a minor/side issue (Label control included in VS 2010, so it's not exactly (quote) "the old, unmaintainable winforms approach"... btw, that old-style approach worked pretty good for couple decades, and data binding could be completed in two lines of code with performance significantly better than what's seen in WPF data-aware controls, with associated tons of code instead of just two data-binding lines as it was done before. Anyway, a practical solution regarding WPF DataGrid to DataView binding with decent performance would be appreciated. Rgds, – Alexander Bell May 21 '13 at 02:12
  • @AlexBell the winforms approach "worked well" because developers got used to write (and thus read) shitty code. I can't stand any winforms code without feeling dizzy and feeling like vomiting. And winforms databinding story is a joke. "Data Aware Controls" - I don't know what that is, EVERYTHING is "Data Aware" in WPF. Even `Borders`, `Brushes`, `Transforms`, `ItemsControls`, You clearly have no idea what you're talking about. Also, winforms worked well for windows 95, but it's 2013, we can't afford to do windows-95 like applications anymore. – Federico Berasategui May 21 '13 at 02:47
  • @AlexBell btw, I have seen NO examples of MVVM winforms around. Have you? Non-MVVM UI = shitty code. Period. No way around it. Therefore the only way to do non-shitty UI are XAML-based techs. – Federico Berasategui May 21 '13 at 02:49
  • @AlexBell I wasn't talking about `Labels` vs `TextBlocks`. I was talking about creating UI elements in code, which is the crappiest approach I've ever seen. – Federico Berasategui May 21 '13 at 02:50
  • Thanks for this new edit, I am looking at it. Can you plz explain where is DataGrid control and its binding portion? Also, could you please simplify your code assuming that everything elese is already done (that Grid with Labels or TextBlocks is actually a nested grid in a main one), so for the sake of simplicity, can you just focus on what I've asked, namely: DataGrid binding to DataTable? Thanks and regards, – Alexander Bell May 21 '13 at 02:58
  • @AlexBell you have thousands of `DataGrid with DataTable` examples around. Just google it. Your original question was something different, so that's why I gave a suitable answer to your original question, where you said you were NOT wanting to use a DataGrid. – Federico Berasategui May 21 '13 at 02:59
  • As a matter of fact, I did that data binding like (XAML): and put a line _dataGrid.DataContext = _dt.DefaultView; in code behind. It works, but performance is terrible. Rgds, – Alexander Bell May 21 '13 at 03:01
  • Your edit 3 is essentially identical to what I was using to bind DataGrid except for that ScrollViewer (a leftover from the previous solution w/TextBlocks or Labels). I just removed that ScrollViewer and performance is OK. I will accept and upvote your answer. Thanks and regards, – Alexander Bell May 21 '13 at 03:16
  • @AlexBell =P. What scrollviewer? there's no ScrollViewer in my edit3. I was recording my desktop to send you a video of the app running here. – Federico Berasategui May 21 '13 at 03:18
  • There was a ScrollViewer in my original solution :-) (see that Listing 3). Thanks and regards, – Alexander Bell May 21 '13 at 03:20
  • @AlexBell DUDE.. That is the root of all evil!! That's why you were getting bad performance... because putting a ScrollViewer there disables UI Virtualization!! The scrollviewer is an "infinite container" (I mean it doesn't limit the size of it's contents). Therefore ALL DataGrid Rows where being rendered! – Federico Berasategui May 21 '13 at 03:22
  • Thanks a bunch, that's good to know! As I said, it was a leftover from the previous Grid-Labels solutions. Best rgds, – Alexander Bell May 21 '13 at 03:35