Home > Software design >  When I try to cast the DataGrid.ItemsSource to the DataView for the second time, I get an error (&qu
When I try to cast the DataGrid.ItemsSource to the DataView for the second time, I get an error (&qu

Time:07-18

I want the rows that the user selects to be deleted.

As far as I know, the DataGrid.ItemsSource cannot be removed directly using the DataGrid.Items.Remove() method, so I must convert the DataGrid.ItemsSource to a DataTable and use the DataGrid.ItemsSource = DataTable.DefaultView property. To convert DataGrid.ItemsSource to DataTable, I tried the following solutions in various situations, and each has advantages and disadvantages (I spent several months researching and testing):

First solution (I made this by myself):
XAML:

    <DataGrid x:Name="BookDataGrid" EnableRowVirtualization="True" VirtualizingPanel.ScrollUnit="Pixel" CanUserAddRows="False" BeginningEdit="BookDataGrid_BeginningEdit" RowEditEnding="BookDataGrid_RowEditEnding" HeadersVisibility="Column" AutoGenerateColumns="False" ItemsSource="{Binding}" HorizontalAlignment="Left" VerticalAlignment="Top" Height="386" Width="486" Margin="0">
        <DataGrid.Columns>
            <DataGridTextColumn x:Name="BookName" Binding="{Binding BookName}" Width="SizeToHeader">
                <DataGridTextColumn.EditingElementStyle>
                    <Style TargetType="TextBox">
                        <Setter Property="AcceptsReturn" Value="True"/>
                        <Setter Property="ContextMenu" Value="{StaticResource CustomContextMenu}"/>
                        <Setter Property="TextWrapping" Value="WrapWithOverflow"/>
                    </Style>
                </DataGridTextColumn.EditingElementStyle>
            </DataGridTextColumn>
            <DataGridTemplateColumn x:Name="BookImage" Width="SizeToHeader">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Image x:Name="BookImg" Source="{Binding BookImage}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>

C#:

    public byte[] ImageSourceToBytes(BitmapEncoder BitEncoder, ImageSource ImgSource)
    {
        byte[] Bytes = null;
        switch ((ImgSource as BitmapSource) != null)
        {
            case true:
                BitEncoder.Frames.Add(BitmapFrame.Create((ImgSource as BitmapSource)));
                using (var Stream = new System.IO.MemoryStream())
                {
                    BitEncoder.Save(Stream);
                    Bytes = Stream.ToArray();
                }
                break;
        }
        return Bytes;
    }
    public DataTable DataGridToDataTable(DataGrid DG, DataTable DT, byte NumberOfColumns, byte VisualColumnIndex, string ControlName)
    {
        for (int i = 0; i < DG.Items.Count; i  )
        {
            DT.Rows.Add(DG.Items[i]);
        }
        for (int i = 0; i < DG.Items.Count; i  )
        {
            for (byte j = 0; j < NumberOfColumns; j  )
            {
                switch (j == VisualColumnIndex)
                {
                    case true:
                        FrameworkElement FE = DG.Columns[j].GetCellContent((DataGridRow)DG.ItemContainerGenerator.ContainerFromIndex(i));
                        Image Img = new Image() { Source = ((((DataGridTemplateColumn)DG.Columns[j]).CellTemplate.FindName(ControlName, FE) as Image).Source) };
                        DT.Rows[i][j] = ImageSourceToBytes(new PngBitmapEncoder(), Img.Source);
                        break;
                    default:
                        DG.ScrollIntoView((DataRowView)DG.Items[i]);
                        DT.Rows[i][j] = ((DG.Columns[j].GetCellContent(((DataGridRow)DG.ItemContainerGenerator.ContainerFromIndex(i)))) as TextBlock).Text;
                        break;
                }
            }
        }
        return DT;
    }

Advantage: Even if one of the columns is of the Image type, this approach operates without an error.

Disadvantage: The method DG.ScrollIntoView((DataRowView)DG.Items[i]) must be used when the EnableRowVirtualization attribute is set to True; otherwise, a null error will occur. For large numbers of rows, this approach is incredibly sluggish (e.g., if we have 20,000 rows, it could take an hour or more).

Second solution (I made this by myself):

XAML:

    <DataGrid x:Name="BookDataGrid" EnableRowVirtualization="False" CanUserAddRows="False" BeginningEdit="BookDataGrid_BeginningEdit" RowEditEnding="BookDataGrid_RowEditEnding" HeadersVisibility="Column" AutoGenerateColumns="False" ItemsSource="{Binding}" HorizontalAlignment="Left" VerticalAlignment="Top" Height="386" Width="486" Margin="0">
        <DataGrid.Columns>
            <DataGridTextColumn x:Name="BookName" Binding="{Binding BookName}" Width="SizeToHeader">
                <DataGridTextColumn.EditingElementStyle>
                    <Style TargetType="TextBox">
                        <Setter Property="AcceptsReturn" Value="True"/>
                        <Setter Property="ContextMenu" Value="{StaticResource CustomContextMenu}"/>
                        <Setter Property="TextWrapping" Value="WrapWithOverflow"/>
                    </Style>
                </DataGridTextColumn.EditingElementStyle>
            </DataGridTextColumn>
            <DataGridTemplateColumn x:Name="BookImage" Width="SizeToHeader">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Image x:Name="BookImg" Source="{Binding BookImage}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>

C#:

    public DataTable DataGridToDataTable(DataGrid DG, DataTable DT, byte NumberOfColumns, byte VisualColumnIndex, string ControlName)
    {
        for (int i = 0; i < DG.Items.Count; i  )
        {
            DT.Rows.Add(DG.Items[i]);
        }
        for (int i = 0; i < DG.Items.Count; i  )
        {
            for (byte j = 0; j < NumberOfColumns; j  )
            {
                switch (j == VisualColumnIndex)
                {
                    case true:
                        FrameworkElement FE = DG.Columns[j].GetCellContent((DataGridRow)DG.ItemContainerGenerator.ContainerFromIndex(i));
                        Image Img = new Image() { Source = ((((DataGridTemplateColumn)DG.Columns[j]).CellTemplate.FindName(ControlName, FE) as Image).Source) };
                        DT.Rows[i][j] = ImageSourceToBytes(new PngBitmapEncoder(), Img.Source);
                        break;
                    default:
                        DT.Rows[i][j] = ((DG.Columns[j].GetCellContent(((DataGridRow)DG.ItemContainerGenerator.ContainerFromIndex(i)))) as TextBlock).Text;
                        break;
                }
            }
        }
        return DT;
    }

Advantage: This solution converts DataGrid.ItemsSource to DataTable faster than the first because EnableRowVirtualization is equal to False in this case.

Disadvantage: This solution consumes a lot of memory; for example, if we have 100,000 rows and the database table is 2GB in size, it will consume 2GB of RAM and a RAM space error may occur. After-running-the-App

Third solution:

XAML:

    <DataGrid x:Name="BookDataGrid" EnableRowVirtualization="True" VirtualizingPanel.ScrollUnit="Pixel" CanUserAddRows="False" BeginningEdit="BookDataGrid_BeginningEdit" RowEditEnding="BookDataGrid_RowEditEnding" HeadersVisibility="Column" AutoGenerateColumns="False" ItemsSource="{Binding}" HorizontalAlignment="Left" VerticalAlignment="Top" Height="386" Width="486" Margin="0">
        <DataGrid.Columns>
            <DataGridTextColumn x:Name="BookName" Binding="{Binding BookName}" Width="SizeToHeader">
                <DataGridTextColumn.EditingElementStyle>
                    <Style TargetType="TextBox">
                        <Setter Property="AcceptsReturn" Value="True"/>
                        <Setter Property="ContextMenu" Value="{StaticResource CustomContextMenu}"/>
                        <Setter Property="TextWrapping" Value="WrapWithOverflow"/>
                    </Style>
                </DataGridTextColumn.EditingElementStyle>
            </DataGridTextColumn>
            <DataGridTemplateColumn x:Name="BookImage" Width="SizeToHeader">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Image x:Name="BookImg" Source="{Binding BookImage}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>

C#:

    uint[] BookCodeSelectedItems = null; //I need this for further calculations
    private void DataGridDeleteMenu_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        switch (BookDataGrid.SelectedItems.Count > 0)
        {
            case true:
                List<object> DefaultRow = new List<object>();
                DataTable BDT = ((DataView)BookDataGrid.ItemsSource).ToTable(); //The first time the event is executed, no error occurs, but the second time the error occurs on this line
                for (int i = 0; i < BookDataGrid.Items.Count; i  )
                {
                    DefaultRow.Add(BookDataGrid.Items[i]);
                }
                BookCodeSelectedItems = new uint[BookDataGrid.SelectedItems.Count];
                for (int i = 0; i < BookDataGrid.SelectedItems.Count; i  )
                {
                    BookCodeSelectedItems[i] = uint.Parse(BDT.Rows[i][3].ToString());
                    DefaultRow.Remove(BookDataGrid.SelectedItems[i]);
                }
                BookDataGrid.ItemsSource = DefaultRow;
                break;
        }
    }

Advantage: The DataGrid.ItemsSource is rapidly changed to DataTable in this approach when the DataGridDeleteMenu PreviewMouseLeftButtonDown event is fired for the first time.

Disadvantage: However, when the event is re-run, a System.InvalidCastException: 'Unable to cast object of type 'System.Collections.Generic.List 1[System.Object]' to type 'System.Data.DataView'.' error occurs.

I tested the following code (I put a breakpoint):

var dataType = BookDataGrid.ItemsSource.GetType().BaseType;

Result:

For the first time:

Value= {Name = "MarshalByValueComponent" FullName = "System.ComponentModel.MarshalByValueComponent"}

Type= System.Type {System.RuntimeType}

For the second time:

Value= {Name = "Object" FullName = "System.Object"}

Type= System.Type {System.RuntimeType}

It seems that only the value has changed.

But why?

And what is the solution?

Error

I use the following tools:

XAML:

<Window.Resources>
    <local:DatabaseDataSet x:Key="Database_DataSet"/>
    <CollectionViewSource x:Key="BookTableViewSource" Source="{Binding BookTable, Source={StaticResource Database_DataSet}}"/>
    <CollectionViewSource x:Key="MemberTableViewSource" Source="{Binding MemberTable, Source={StaticResource Database_DataSet}}"/>
</Window.Resources>
<Grid DataContext="{StaticResource BookTableViewSource}" Width="486" Height="386">
    <DataGrid x:Name="BookDataGrid" HeadersVisibility="Column" EnableRowVirtualization="True" VirtualizingPanel.ScrollUnit="Pixel" CanUserAddRows="False" AutoGenerateColumns="False" ItemsSource="{Binding}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="486" Height="386" Margin="0">
        <DataGrid.Columns>
            <DataGridTextColumn x:Name="BookName" Binding="{Binding BookName}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Publisher" Binding="{Binding Publisher}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Category" Binding="{Binding Category}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="BookCode" Binding="{Binding BookCode}" IsReadOnly="True" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Inventory" Binding="{Binding Inventory}" IsReadOnly="True" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="ReleaseDate" Binding="{Binding ReleaseDate}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="DateTaken" Binding="{Binding DateTaken}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="ReturnDate" Binding="{Binding ReturnDate}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="RecipientName" Binding="{Binding RecipientName}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Language" Binding="{Binding BookLanguage}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Length" Binding="{Binding Length}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Form" Binding="{Binding Form}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Translator" Binding="{Binding Translator}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Narrator" Binding="{Binding Narrator}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="ISBN" Binding="{Binding ISBN}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Location" Binding="{Binding Location}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Price" Binding="{Binding Price}" Width="SizeToHeader"/>
            <DataGridTemplateColumn x:Name="BookImage" Width="SizeToHeader">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Image x:Name="BookImg" Source="{Binding BookImage}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

C#:

    public void BookDatagridRefresh()
    {
        DatabaseDataSet Database_DataSet = ((DatabaseDataSet)TryFindResource("Database_DataSet"));
        DatabaseDataSetTableAdapters.BookTableTableAdapter BookTable_TableAdapter = new DatabaseDataSetTableAdapters.BookTableTableAdapter();
        BookTable_TableAdapter.Fill(Database_DataSet.BookTable);
        BookDataGrid.ItemsSource = Database_DataSet.Tables["BookTable"].DefaultView;
    }

Visual Studio 2017 .NET Framework 4.5.2 WPF

Binding

Thank you for your attention.

CodePudding user response:

Finally, I found a solution that does not have the above problems.

I used the CollectionViewSource class to convert.

I tested this solution in different conditions and with the full capacity of MS Access Database.

The conditions are as follows:

Number of records = 180,478

Database size = 2,097,144 KB (database size without any rows = 604 KB)

Size of each row = (Database size - database size without any rows) / Number of records => about 11.616595 KB (Of course, if I haven't made a mistake)

Hardware and software used in the test = Acer Aspire 5750G Laptop (Core i5 2nd Gen/4 GB RAM/Win7-x64), Visual Studio 2017, .NET Framework 4.5.2, WPF

WPF:

<Window.Resources>
    <local:DatabaseDataSet x:Key="Database_DataSet"/>
    <CollectionViewSource x:Key="BookTableViewSource" Source="{Binding BookTable, Source={StaticResource Database_DataSet}}"/>
    <CollectionViewSource x:Key="MemberTableViewSource" Source="{Binding MemberTable, Source={StaticResource Database_DataSet}}"/>
</Window.Resources>
<Grid DataContext="{StaticResource BookTableViewSource}" Width="486" Height="386">
    <DataGrid x:Name="BookDataGrid" HeadersVisibility="Column" EnableRowVirtualization="True" VirtualizingPanel.ScrollUnit="Pixel" CanUserAddRows="False" AutoGenerateColumns="False" ItemsSource="{Binding}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="486" Height="386" Margin="0">
        <DataGrid.Columns>
            <DataGridTextColumn x:Name="BookName" Binding="{Binding BookName}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Publisher" Binding="{Binding Publisher}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Category" Binding="{Binding Category}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="BookCode" Binding="{Binding BookCode}" IsReadOnly="True" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Inventory" Binding="{Binding Inventory}" IsReadOnly="True" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="ReleaseDate" Binding="{Binding ReleaseDate}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="DateTaken" Binding="{Binding DateTaken}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="ReturnDate" Binding="{Binding ReturnDate}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="RecipientName" Binding="{Binding RecipientName}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Language" Binding="{Binding BookLanguage}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Length" Binding="{Binding Length}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Form" Binding="{Binding Form}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Translator" Binding="{Binding Translator}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Narrator" Binding="{Binding Narrator}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="ISBN" Binding="{Binding ISBN}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Location" Binding="{Binding Location}" Width="SizeToHeader"/>
            <DataGridTextColumn x:Name="Price" Binding="{Binding Price}" Width="SizeToHeader"/>
            <DataGridTemplateColumn x:Name="BookImage" Width="SizeToHeader">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Image x:Name="BookImg" Source="{Binding BookImage}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

C#:

In this case (180,478 rows), I tested the execution speeds of ObservableCollection<> and List<>, and it appeared that List<> was faster by roughly 0.5 seconds. Additionally, I tested the AddRange function of the List<> and it appeared to be one second slower.

    int SelectedCount = 0;
    string[] ISBN_Value = null;
    uint[] BookCodeSelectedItems = null;
    CollectionViewSource BookCollectionViewSource = new CollectionViewSource();
    private void DataGridDeleteMenu_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        switch (BookDataGrid.SelectedItems.Count > 0)
        {
            case false:
                MessageWindow MW = new MessageWindow();
                MW.YesButton.Visibility = Visibility.Hidden;
                MW.NoButton.Visibility = Visibility.Hidden;
                MW.MessageLabel.Margin = new Thickness(0, 110, 0, 0);
                MW.MessageLabel.HorizontalAlignment = HorizontalAlignment.Center;
                MW.Image.Source = GetImageFromBytes(System.IO.File.ReadAllBytes(System.Windows.Forms.Application.StartupPath   @"\Images\Warning.bin"));
                MW.MessageTextBlock.Text = "No rows selected";
                MW.OKButton.Content = "OK";
                MW.ShowDialog();
                break;
            default:
                var stopwatch = new System.Diagnostics.Stopwatch();
                stopwatch.Start();
                List<object> Row = new List<object>();
                //Additionally, I tested the "AddRange" function of the "List<>" and it appeared to be one second slower. I mean "Row.AddRange(BookDataGrid.Items.Cast<object>().ToList());"
                for (int i = 0; i < BookDataGrid.Items.Count; i  )
                {
                    Row.Add(BookDataGrid.Items[i]);
                }
                DataView DV = BookCollectionViewSource.Source as DataView;
                SelectedCount = BookDataGrid.SelectedItems.Count; //I need this for further calculations
                ISBN_Value = new string[BookDataGrid.SelectedItems.Count]; //I need this for further calculations
                BookCodeSelectedItems = new uint[BookDataGrid.SelectedItems.Count]; //I need this for further calculations
                for (int i = 0; i < BookDataGrid.SelectedItems.Count; i  )
                {
                    BookCodeSelectedItems[i] = uint.Parse(DV.Table.Rows[i][3].ToString());
                    ISBN_Value[i] = DV.Table.Rows[i][14].ToString();
                    Row.Remove(BookDataGrid.SelectedItems[i]);
                }
                BookDataGrid.ItemsSource = Row;
                stopwatch.Stop();
                MessageBox.Show("TotalSeconds: "   stopwatch.Elapsed.TotalSeconds.ToString());
                break;
        }
    }

Output (if all rows are removed):

Output (all rows)

Output (if rows are removed out of order):

Output (out of order)

I hope this is the most comprehensive solution for converting DataGrid.ItemsSource to DataTable, DataView or another type of data source.

Thank you for your attention.

  • Related