WPF DataGrid Multithreading Crash

2019-08-25 11:34发布

问题:

I have a View with a DataGrid in it. A ViewModel as DataContext where i can access a DataTable in a background object. The background object has to work with the DataTable and keep it updated. The user has also be allowed to make changes to that DataTable.

If i create a copy of the DataTable it stops crashing but the user is obviousely not working on the data.

If i leave access open for the user the program crashed inevitabely.

Here is a short program that will crash:

app.cs

public partial class App : Application
{
    public App()
    {
        SomeBackgroundThing background = new SomeBackgroundThing();
        MainWindowViewModel viewmodel = new MainWindowViewModel(background);
        MainWindowView view = new MainWindowView(viewmodel);
        view.Show();
    }
}

main xaml

<Window x:Class="NullPointerDataGrid.MainWindowView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid Name="datagrid" ItemsSource="{Binding Path=table, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
    </Grid>
</Window>

and the program code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Data;
using System.Diagnostics;
using System.Timers;
using System.ComponentModel;

namespace NullPointerDataGrid
{
    public partial class MainWindowView : Window
    {
        public MainWindowView(MainWindowViewModel model)
        {
            DataContext = model;
            InitializeComponent();
            datagrid.Loaded += new RoutedEventHandler(ScrollToBottom);
            datagrid.AutoGeneratedColumns += new EventHandler(StarSizeLastRow);

        }

    void ScrollToBottom(object sender, RoutedEventArgs e)
    {
        Debug.WriteLine("TableGrid_ScrollToBottom");
        if (datagrid.Items.Count > 0)
        {
            var border = VisualTreeHelper.GetChild(datagrid, 0) as Decorator;
            if (border != null)
            {
                var scroll = border.Child as ScrollViewer;
                if (scroll != null) scroll.ScrollToEnd();
            }
        }

    }

    void StarSizeLastRow(object sender, EventArgs e)
    {
        Debug.WriteLine("TableGrid_StarSizeLastColumn");
        try
        {
            datagrid.Columns[datagrid.Columns.Count - 1].Width = new DataGridLength(1, DataGridLengthUnitType.Star);
        }
        catch { }
    }

    }

public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private SomeBackgroundThing thing;
    public DataTable table
    {
        get
        {
            lock (thing.table)
            {
                //DataTable wpfcopy = thing.table.Copy();
                return thing.table;
            };
        }
        set
        {
            Debug.Write("This never happens");
        }
    }

    public MainWindowViewModel(SomeBackgroundThing thing)
    {
        this.thing = thing;
        thing.Changed += new EventHandler(thing_Changed);
    }

    void thing_Changed(object sender, EventArgs e)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("table"));
        }
    }
}

public class SomeBackgroundThing : IDisposable
{
    public DataTable table;
    private DataTable tablecopy;
    private System.Timers.Timer timer, slowrowchanger;

    public event EventHandler Changed = new EventHandler((o, e) => { ;});
    protected void CallChanged(object sender, EventArgs e)
    {
        Changed(sender, e);
    }

    public SomeBackgroundThing()
    {
        CreateTable();
        UpdateB(this, null);
        tablecopy = table.Copy();
        InitAndStartTimer(1);
    }

    #region timer

    private void UpdateA()
    {
        Boolean haschanged = false;
        DataTable newcopy = table.Copy(); ;
        if (newcopy.Rows.Count != tablecopy.Rows.Count)
        {
            Debug.WriteLine("Different ammount of rows");
            haschanged = true;
        }
        else if (newcopy.Columns.Count != tablecopy.Columns.Count)
        {
            Debug.WriteLine("Different ammount of columns");
            haschanged = true;
        }
        else
        {
            for (int i = 0; i < newcopy.Rows.Count; i++)
            {
                for (int j = 0; j < newcopy.Columns.Count; j++)
                {
                    if (newcopy.Rows[i][j].ToString() != tablecopy.Rows[i][j].ToString())
                    {
                        Debug.WriteLine(String.Format(
                            "Element [{0}/{1}]: {2} is different from {3}",
                            i, j, newcopy.Rows[i][j], tablecopy.Rows[i][j]
                            ));
                        haschanged = true;
                    }
                    if (haschanged) break;
                }
                if (haschanged) break;
            }
        }
        if (haschanged)
        {
            tablecopy = newcopy;
        }
    }

    private void InitAndStartTimer(int interval)
    {
        timer = new System.Timers.Timer();
        timer.Interval = interval;
        timer.AutoReset = true;
        timer.Elapsed += new ElapsedEventHandler((s, e) =>
        {
            UpdateA();
        });
        timer.Enabled = true;

        slowrowchanger = new System.Timers.Timer();
        slowrowchanger.Interval = 3000;
        slowrowchanger.AutoReset = true;
        slowrowchanger.Elapsed += new ElapsedEventHandler((s, e) =>
        {
            UpdateB(null, null);
        });
        slowrowchanger.Enabled = true;

    }

    public void Dispose()
    {
        timer.Enabled = false;
        slowrowchanger.Enabled = false;
        timer.Dispose();
        slowrowchanger.Dispose();
    }

    #endregion

    #region editlastrow

    void UpdateB(object sender, EventArgs e)
    {
        Random rnd = new Random();
        List<String> cells = new List<string>{
                "The SAME", 
                rnd.Next(0,100).ToString(), 
                rnd.ToString(), 
                rnd.NextDouble().ToString()};
        lock (table)
        {
            OverwriteOrAppendLastRow(ref table, cells);
            table.AcceptChanges();
        }
        CallChanged(this, null);
    }

    private void OverwriteOrAppendLastRow(ref DataTable table, List<string> newrow)
    {
        if (table.Rows.Count == 0) CreteEmptyRow(ref table);
        if (newrow[0].ToString() != table.Rows[table.Rows.Count - 1][0].ToString())
        {
            Debug.WriteLine(String.Format("Creating row because '{0}' is different from '{1}'", newrow[0], table.Rows[table.Rows.Count - 1][0]));
            CreteEmptyRow(ref table);
        }
        OverwriteLastRow(ref table, newrow);
    }

    private void OverwriteLastRow(ref DataTable table, List<string> newrow)
    {
        for (int i = 0; i < newrow.Count() && i < table.Columns.Count; i++)
        {
            table.Rows[table.Rows.Count - 1][i] = newrow[i];
        }
    }

    private void CreteEmptyRow(ref DataTable table)
    {
        table.Rows.Add(new String[table.Columns.Count]);
    }

    #endregion

    private void CreateTable()
    {
        table = new DataTable();
        table.Columns.Add("FirstCell", typeof(String));
        table.Columns.Add("BananaCell", typeof(String));
        table.Columns.Add("CherryCell", typeof(String));
        table.Columns.Add("Blue", typeof(String));
        Random rnd = new Random();
        for (int i = 0; i < 145; i++)
        {
            table.Rows.Add(new String[]{
                rnd.Next().ToString(), 
                rnd.Next(0,i+1).ToString(), 
                rnd.ToString(), 
                rnd.NextDouble().ToString()});
        }
    }

}

}

How can i stop this multithread crashing?


EDIT:

I don't know if there are more than one reasons for this code to crash. But i did my best to gather some information about one reason to crash:

Nullpointer exception in App.g.cs - the autogenerated portion. The Debugger wont step into it - so i cant say anything about the line it crashes in.

Here is the Exception Detail, sorry for the German.

System.NullReferenceException wurde nicht behandelt.
  Message=Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.
  Source=PresentationFramework
  InnerException: 

The Stacktrace only shows "Externer Code" so no stack to trace.

The thing is that WPF crashes - my code can handle it... somehow i need to capsule WPF so it wont crash, one way to do that is to copy the DataTable - but then i loose the ability to write back that table since its setter is not called when something has gotten edited.

EDIT #2:

I recreated this example to show the error i have in another program and i just found out that what crashes is actually related with the scrollbar. If i change the ammount of displayed data to a low number so that there is no scrollbar, the code will not crash.

回答1:

The following change to the viewmodel solves the problem.

I am now using a copy for wpf to work on and only note the canges should they occur. This code has an issue with the poorly refined change mechanism - but that is beyond the scope of this question.

public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private SomeBackgroundThing thing;

    private DataTable wpftable;
    public DataTable table
    {
        get
        {
            lock (wpftable)
            {
                return wpftable;
            }

        }

        set
        {
            lock (wpftable)
            {
                wpftable = value;
            }
        }
    }

    public MainWindowViewModel(SomeBackgroundThing thing)
    {
        wpftable = thing.table.Copy();
        this.thing = thing;
        thing.Changed += new EventHandler(thing_Changed);
    }

    void thing_Changed(object sender, EventArgs e)
    {
        if (PropertyChanged != null)
        {
            DataTable wpftablecopy = wpftable.Copy();
            DataTable thintablecopy = thing.table.Copy();
            int rowcount = wpftablecopy.Rows.Count;
            for (int col = 0; col < 4; col++)
            {
                for (int row = 0; row < rowcount; row++)
                {
                    if (wpftablecopy.Rows[row][col] != thintablecopy.Rows[row][col])
                        wpftable.Rows[row][col] = thintablecopy.Rows[row][col];
                }
            }
            PropertyChanged(this, new PropertyChangedEventArgs("table"));
        }
    }
}