Чтобы найти, где ошибка в программе - используйте отладку.
Сразу скажу, задача получилась нетривиальная, и букв в этом ответе будет много. Пишу с нуля, потому что мой подход к решению несовместим с подходом автора вопроса.
Приложение создано на базе WPF .NET Core 3.1, но разницы с .NET Framework быть не должно.
Пример привожу не для боевого проекта, а в целях обучения, чтобы можно было понять, как же именно победить DataGrid и сделать её матрицей, хотя сама DataGrid предназначена для работы со списком объектов, а не с двумерными массивами.
За основу для данных взял класс System.Data.DataTable, обычно его используют для получения и сохранения данных в БД SQL-типа. Так же он поддерживает динамическое обновление отображения данных в DataGrid, а это именно то, что нужно.
Для реализации примера я использовал шаблон программирования MVVM и следующее техническое задание (сам его себе придумал):
- Реализовать отображение матрицы в
DataGrid, то есть двумерного числового массива
- Заголовки колонок и строк таблицы должны быть пронумерованы
- [Ответ на главный вопрос] Данные вводиться будут прямо в таблицу
- Чтобы не усложнять пример, пусть будет тип данных для каждой ячейки
int
- Реализовать управление размером таблицы с минимальным размером 1x1
- Реализовать метод очистки таблицы для демонстрации команды, изменяющей таблицу
- Реализовать метод подсчета суммы всех ячеек для демонстрации способа использования данных
Почему MVVM? Да потому что для реализации этого ТЗ методом Winforms с обработчиками событий, пришлось бы написать раз в 5 больше кода и воевать с попутными багами и странностями поведения контролов. WPF заточен под MVVM и нативно поддерживает такой подход, это удобно. Например, я написал 0 строк кода для валидации ячеек таблицы и текстовых полей (а валидация есть!), а кнопки сами дизаблятся, когда команду нельзя использовать. Другими словами - тратил время на написание полезного кода, а не костылей и обвязок.
Для реализации примера по шаблону MVVM потребуется 2 вспомогательных класса, которые просто надо добавить в проект один раз "Правый клик на проекте -> Добавить -> Класс". Эти классы никогда не будут изменяться.
Первый из двух позволит сообщать интерфейсу о том, что какое-то свойство изменилось посредством вызова события PropertyChanged, которое в свою очередь я буду вызывать методом OnPropertyChanged().
NotifyPropertyChanged.cs
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Так как в MVVM вообще не используются обработчики событий (за ненадобностью), нужно что-то, что можно вызывать когда например нажата кнопка - команду.
Второй вспомогательный класс как раз предназначен для удобного использования команд. Предлагаю на данном этапе не вникать в его код, а тоже просто добавить в проект.
RelayCommand.cs
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
Со вспомогательными классами покончено. Далее, нужно создать основной и самый нужный класс MainViewModel. Он и будет содержать нужные нам свойства и методы, и к нему будет подключен интерфейс приложения.
Привожу сразу полный код класса, в комментариях укажу возможно непонятные моменты, но кода не так много, думаю, разобраться вполне реально самостоятельно.
MainViewModel.cs
public class MainViewModel : NotifyPropertyChanged
{
private DataTable _data;
private ICommand _addRowCommand;
private ICommand _addColumnCommand;
private ICommand _removeRowCommand;
private ICommand _removeColumnCommand;
private ICommand _setSizeCommand;
private ICommand _cleanCommand;
private ICommand _calcSumCommand;
private int _rowsCount;
private int _columnsCount;
private long _sum;
public DataTable Data // контейнер для матрицы, DataGrid привязана сюда
{
get => _data;
set
{
_data = value;
OnPropertyChanged();
}
}
public int RowsCount // тексбокс Строки привязан сюда
{
get => _rowsCount;
set
{
_rowsCount = value;
OnPropertyChanged();
}
}
public int ColumnsCount // тексбокс Колонки привязан сюда
{
get => _columnsCount;
set
{
_columnsCount = value;
OnPropertyChanged();
}
}
public long Sum // число суммы ячеек привязано сюда
{
get => _sum;
set
{
_sum = value;
OnPropertyChanged();
}
}
private void AddRows(int count) // добавить строки
{
for (int i = 1; i <= count; i++)
Data.Rows.Add();
RowsCount = Data.Rows.Count;
}
private void AddColumns(int count) // добавить колонки
{
for (int i = 1; i <= count; i++)
{
Data.Columns.Add(new DataColumn(Data.Columns.Count.ToString(), typeof(int))
{
AllowDBNull = false,
DefaultValue = 0
});
}
Data = Data.Copy(); // здесь и далее - хак, способ полностью перерисовать таблицу DataGrid, так как она не поддерживает динамическое изменение колонок.
ColumnsCount = Data.Columns.Count;
}
private void RemoveRows(int count) // удалить строки (с конца)
{
for (int i = 1; i <= count && Data.Rows.Count > 1; i++)
Data.Rows.RemoveAt(Data.Rows.Count - 1);
RowsCount = Data.Rows.Count;
}
private void RemoveColumns(int count) // удалить колонки (с конца)
{
for (int i = 1; i <= count && Data.Columns.Count > 1; i++)
Data.Columns.RemoveAt(Data.Columns.Count - 1);
Data = Data.Copy();
ColumnsCount = Data.Columns.Count;
}
// что делают команды, можно будет увидеть по биндингам на них у кнопок в xaml
public ICommand AddRowCommand => _addRowCommand ?? (_addRowCommand = new RelayCommand(parameter =>
{
AddRows(1);
}));
public ICommand AddColumnCommand => _addColumnCommand ?? (_addColumnCommand = new RelayCommand(parameter =>
{
AddColumns(1);
}));
public ICommand RemoveRowCommand => _removeRowCommand ?? (_removeRowCommand = new RelayCommand(parameter =>
{
RemoveRows(1);
}, parameter => Data.Rows.Count > 1));
public ICommand RemoveColumnCommand => _removeColumnCommand ?? (_removeColumnCommand = new RelayCommand(parameter =>
{
RemoveColumns(1);
}, parameter => Data.Columns.Count > 1));
public ICommand SetSizeCommand => _setSizeCommand ?? (_setSizeCommand = new RelayCommand(parameter =>
{
if (RowsCount > Data.Rows.Count)
AddRows(RowsCount - Data.Rows.Count);
if (RowsCount < Data.Rows.Count)
RemoveRows(Data.Rows.Count - RowsCount);
if (ColumnsCount > Data.Columns.Count)
AddColumns(ColumnsCount - Data.Columns.Count);
if (ColumnsCount < Data.Columns.Count)
RemoveColumns(Data.Columns.Count - ColumnsCount);
}));
public ICommand CleanCommand => _cleanCommand ?? (_cleanCommand = new RelayCommand(parameter =>
{
for (int i = 0; i < Data.Rows.Count; i++)
for (int j = 0; j < Data.Columns.Count; j++)
Data.Rows[i][j] = 0;
Data = Data.Copy();
}));
public ICommand CalcSumCommand => _calcSumCommand ?? (_calcSumCommand = new RelayCommand(parameter =>
{
long sum = 0;
for (int i = 0; i < Data.Rows.Count; i++)
for (int j = 0; j < Data.Columns.Count; j++)
sum += (int)Data.Rows[i][j];
Sum = sum;
}));
public MainViewModel() // конструктор, выполняется 1 раз при запуске программы
{
Data = new DataTable();
AddColumns(1);
AddRows(1);
}
}
И чтобы не заканчивать на полуслове, вот сразу полный код разметки. Обратите внимание, я нигде не использую именование x:Name. Оно без надобности, когда используются биндинги Binding. Грубо говоря, я сообщаю контролам, где брать данные, и они берут, сами, и сами обновляются.
MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/><!-- здесь поключается MainViewModel к интерфейсу -->
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<WrapPanel Orientation="Vertical">
<TextBlock Text="Строки Колонки" Margin="5,5,5,0"/>
<StackPanel Orientation="Horizontal">
<TextBox Width="30" Margin="5" MaxLength="3" Text="{Binding RowsCount, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="×" VerticalAlignment="Center" FontSize="16" Margin="0,0,0,4"/>
<TextBox Width="30" Margin="5" MaxLength="3" Text="{Binding ColumnsCount, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="Применить" Margin="5" Command="{Binding SetSizeCommand}"/>
</StackPanel>
<Button Margin="5" Content="Добавить строку" Command="{Binding AddRowCommand}"/>
<Button Margin="5" Content="Удалить строку" Command="{Binding RemoveRowCommand}"/>
<Button Margin="5" Content="Добавить колонку" Command="{Binding AddColumnCommand}"/>
<Button Margin="5" Content="Удалить колонку" Command="{Binding RemoveColumnCommand}"/>
<Button Margin="5" Content="Очистить" Command="{Binding CleanCommand}"/>
<Separator Margin="5"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Сумма всех ячеек:" Margin="5"/>
<TextBlock Margin="0,5" FontWeight="Bold" Text="{Binding Sum}"/>
</StackPanel>
<Button Content="Посчитать сумму" Margin="5" Command="{Binding CalcSumCommand}"/>
</WrapPanel>
<DataGrid Margin="5" Grid.Column="1" ItemsSource="{Binding Data}" SelectionMode="Single" SelectionUnit="Cell" AlternationCount="9999" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserSortColumns="False">
<DataGrid.RowHeaderTemplate>
<ItemContainerTemplate>
<!-- AlternationIndex - это хак, использование не по назначению,
но здесь это самый простой способ пронумеровать строки без кучи кода -->
<TextBlock Text="{Binding AlternationIndex,RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</ItemContainerTemplate>
</DataGrid.RowHeaderTemplate>
</DataGrid>
</Grid>
</Window>
В итоге, кода получилось не так много, и вроде все работает. Если есть дополнительные вопросы - задавайте в комментариях к этому ответу, помогу чем смогу.

Ну и традиционное, показываю для новичков в WPF MVVM, что же у меня в коде класса окна. А там ничего. :)
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}