0

Note: Problem is identical to Bind Objects with generic list to wpf datagrid (unanswered) which asks for a solution involving CellTemplates and doesn't include any workarounds. I'm open to any solution, and I have a working (but non-ideal) solution.

The setup is that I have a List of Objects (Persons) that each contain a List of DataObjects.

class Person : List<DataObject>
{
    public string id { get; set; }
}
class DataObject
{
    public string columnName { get; set;}
    public string value { get; set;}
}

The columnNames are based on User Input, but every Person has the same columnNames in their List of DataObjects (i.e. they all have a firstName column, lastName column, dateOfBirth column, all with different values).

I'd like to show these values in a DataGrid format so that the values can be edited by the user.

What I'm currently doing is using a Grid (editGrid) and adding child TextBlocks to it for each column header, and then looping through the items to add TextBoxes for each cell. This works for small numbers, but when I have 1000s of People the program lags because of the sheer number of TextBoxes and TextBlocks being created.

List<People> items;
List<string> columnHeaders;
Grid editGrid;

// Generate column headers
editGrid.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
var columns = items.SelectMany(o => o.Select(a => a.columnName)).Distinct(StringComparer.OrdinalIgnoreCase);
foreach (string text in columns)
{
    TextBlock headerText = new TextBlock();
    headerText.Text = text;
    Grid.SetColumn(headerText, editGrid.ColumnDefinitions.Count());
    editGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto });

    columnHeaders.Add(text);
    editGrid.Children.Add(headerText);
 }

// Create rows
foreach (var item in items)
{
    foreach (var dataObject in item)
    {
        var columnNum = columnHeaders.IndexOf(dataObject.columnName);
        if (columnNum != -1)
        {
            TextBox valueBox = new TextBox();
            Binding bind = new Binding();
            bind.Source = dataObject;
            bind.Mode = BindingMode.TwoWay;
            bind.Path = new PropertyPath("value");
            BindingOperations.SetBinding(valueBox , TextBox.TextProperty, bind);

            Grid.SetColumn(valueBox, columnNum);
            Grid.SetRow(valueBox, editGrid.RowDefinitions.Count);
            editGrid.Children.Add(valueBox);
        }
    }
    editGrid.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
}
Daniel W
  • 3
  • 1

1 Answers1

1

Have you considered creating a DataGrid effect by using an ItemsControl? Something like:

<ItemsControl ItemsSource="{Binding}" Name="IC1"  VirtualizingStackPanel.IsVirtualizing="True" ScrollViewer.CanContentScroll="True">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.Template>
        <ControlTemplate>
            <Border
        BorderThickness="{TemplateBinding Border.BorderThickness}"
        Padding="{TemplateBinding Control.Padding}"
        BorderBrush="{TemplateBinding Border.BorderBrush}"
        Background="{TemplateBinding Panel.Background}"
        SnapsToDevicePixels="True">
                <ScrollViewer
                Padding="{TemplateBinding Control.Padding}"
                Focusable="False">
                    <ItemsPresenter
                    SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                </ScrollViewer>
            </Border>
        </ControlTemplate>
    </ItemsControl.Template>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*" />
                    <ColumnDefinition Width="9*" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock Text="ID" Grid.Column="0" Grid.Row="0"/>
                <TextBlock Grid.Column="0" Grid.Row="1" Text="{Binding Path=id}"/>
                <ItemsControl Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" ItemsSource="{Binding}" >
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <StackPanel Orientation="Horizontal"/>
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <StackPanel>
                                <TextBlock Text="{Binding Path=columnName}" Width="100" />
                                <TextBox Text="{Binding Path=value}" Width="100" />
                            </StackPanel>                                    
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

I populated with some test data (sorry, but in VB):

Property PersonList As New ObservableCollection(Of Person)

Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
    For x As Integer = 1 To 500
        Dim P1 As New Person With {.id = "SE" & x}
        P1.Add(New DataObject With {.columnName = "First Name", .value = "Simon"})
        P1.Add(New DataObject With {.columnName = "Last Name", .value = "Evans"})
        P1.Add(New DataObject With {.columnName = "DOB", .value = "03/03/1980"})
        Dim P2 As New Person With {.id = "RE" & x}
        P2.Add(New DataObject With {.columnName = "First Name", .value = "Ruth"})
        P2.Add(New DataObject With {.columnName = "Last Name", .value = "Evans"})
        P2.Add(New DataObject With {.columnName = "DOB", .value = "11/02/1979"})
        PersonList.Add(P1)
        PersonList.Add(P2)
    Next
    IC1.DataContext = PersonList
End Sub

That's 1,000 rows, but because the control uses virtulisation, there is no lag.

EDIT

No idea if the is the best way, but I would suggest adding an Int width property to your DataObject class and binding the width of the TextBlocks & TextBoxes in the second ItemsControl to this.

Then using the below code snippet to calculate the maximum width required (kudos to WPF equivalent to TextRenderer):

public static Size MeasureTextSize(string text, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, double fontSize)
{
    FormattedText ft = new FormattedText(text,
                                         CultureInfo.CurrentCulture,
                                         FlowDirection.LeftToRight,
                                         new Typeface(fontFamily, fontStyle, fontWeight, fontStretch),
                                         fontSize,
                                         Brushes.Black);
    return new Size(ft.Width, ft.Height);
}

(I used the font details for the Window, but you can, of course, specify different details if you are styling the text boxes.)

I then wrote this little bit to go through the data and set the widths (sorry, back to VB):

    If PersonList.Count > 0 Then
        Dim MaxLengths(PersonList(0).Count - 1) As Integer
        For i As Integer = 0 To MaxLengths.Count - 1
            MaxLengths(i) = 70 'Set Minimum width to accomodate Headers
        Next
        For Each P As Person In PersonList
            For i As Integer = 0 To P.Count - 1
                Dim BoxSize As Size = MeasureTextSize(P(i).value, FontFamily, FontStyle, FontWeight, FontStretch, FontSize)
                If BoxSize.Width > MaxLengths(i) Then MaxLengths(i) = BoxSize.Width + 6 'to allow for padding
            Next
        Next
        For Each P As Person In PersonList
            For i As Integer = 0 To P.Count - 1
                P(i).width = MaxLengths(i)
            Next
        Next
    End If
Simon Evans
  • 238
  • 2
  • 10
  • Thanks for this, it solves the lag entirely! I did have some issues though: it would put a column name above every entry instead of just one header, but that should be a relatively easy fix; I can't have the columns sized to the longest entry like a datagrid (width:auto makes the columns stop lining up between rows); and it assumes that the List in each Person are ordered the same, so I'll have to sort them before loading them into the control. Do you have any ideas for the width issue? – Daniel W Nov 09 '18 at 16:12
  • @DanielW - Yeah, the column headers I added as I misread your original post and thought the column name was specific to each person - just rip out the unnecessary controls from the DataTemplates. For the Width issue, I have addressed in an edit. – Simon Evans Nov 09 '18 at 19:32