Discover schema of data source

2019-08-03 17:24发布

问题:

Which interface(s) does a class need to implement for DataGrid to automatically discover its schema and generate columns?

Details

I'm trying to implement paging for DataGrid. The underlying data source is a DataTable. Binding DataGrid directly to the DataTable works fine and AutoGenerateColumns does its job.

Due to large size of incoming data and slow DataGrid response, I had to use row virtualization, which resulted in several (known) issues and I had to turn it off.

I'm now working on paging approach instead. Found a good implementation of PagingCollectionView and tried it. DataGrid correctly generates rows for the current page, but not columns. Looks like it is not able to read the schema of the underlying collection. (Note that the code at the link does not use AutoGenerateColumns and therefore does not suffer from the problem at hand).

I investigated further to see what special interfaces do DataTable and DataView implement to expose their schema. It appears (and I could be wrong) that ITypedList is the interface that I need to implement. I went on to implement this and am now stuck GetItemProperties(). There doesn't appear a way to construct PropertyDescriptors manually for each column of the DataTable. There is TypeDesriptor that can get all public properties of a given type, but that is obviously not going to work for DataTable columns.

So, what do I need to do to make my CollectionView class schema become discoverable for the DataGrid?

MCVE

Here is the minimum code that shows the problem with DataGrid:

XAML

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <Button Content="Select All" Click="SelectAll_Click" Grid.Row="0" />
    <Button Content="Deselect All" Click="Deselect_Click" Grid.Row="1" />

    <DataGrid x:Name="DG" AutoGenerateColumns="False" SelectionUnit="FullRow" Grid.Row="2">
      <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding ID}" />
        <DataGridTextColumn Binding="{Binding Name}" />
      </DataGrid.Columns>

      <DataGrid.RowStyle>
        <Style TargetType="DataGridRow">
        <Setter Property="IsSelected" Value="{Binding IsSelected, Mode  =TwoWay}" />
        </Style>
      </DataGrid.RowStyle>
    </DataGrid>
  </Grid>
</Window>

Code

using System.Data;

class MainWindow
{
  DataTable DT = new DataTable();

  MainWindow()
  {
    // This call is required by the designer.
    InitializeComponent();

    DT.Columns.Add(new DataColumn("ID", typeof(Int32)));
    DT.Columns.Add(new DataColumn("Name", typeof(string)));
    DT.Columns.Add(new DataColumn("IsSelected", typeof(bool)));

    for (index = 1; index <= 10000; index++) {
      var NR = DT.NewRow();
      NR["ID"] = index;
      NR["Name"] = System.Guid.NewGuid().ToString();
      DT.Rows.Add(NR);
    }

    DG.ItemsSource = DT.DefaultView;
  }

  private void SelectAll_Click(object sender, RoutedEventArgs e)
  {
    for (int i = 0; i < DT.Rows.Count; i++) {
      DT.Rows[i]["IsSelected"] = true;
    }
  }

  private void Deselect_Click(object sender, RoutedEventArgs e)
  {
    for (int i = 0; i < DT.Rows.Count; i++) {
      DT.Rows[i]["IsSelected"] = false;
    }
  }
}

Run the code and click Select All button. All the rows appear to be selected. Scroll down to the bottom of the DataGrid. Click Deselect All. Now scroll up slowly. You'll see that many of the rows are still selected. Go to your XAML and disable row virtualization for DataGrid. Selection will now work fine but it will take more time for the DataGrid to load.