Debugging
I have changed the statement folder in the Parameters tab a hundred times and there was no problem but when I have made the last tutorial video there was a problem.
I have got the exception, which occurred because the Single method should have found exactly one match and it found none. Its reason was that AccountObject did not got any element at all.
I did not understand what went wrong. I just changed the Statement folder cell and pressed the Refresh button. The first clue was that the error did not happen always so I checked the Parallel stacks window.
It seems that two threads are running parallel. The first one is obvious the Refresh, the second is some kind of update.
The propagation of a data change
View to View model
If some value is changed in the View, the xaml file, then it propagates to the View modell. This is controlled be the UpdateSourceTrigger.
Binding="{Binding StatementFolder, UpdateSourceTrigger=LostFocus}"
LostFocus means that the change will go to the variable in the view model if the focus is moved away from the actual Control (Gui element). On the other hand, PropertyChanged means that the source variable is updated at every change (e.g. at every keystroke) even if the focus is not moved yet. For a TextBox the default behavior is LostFocus and I thought it is the same for a DataGridTextColumn. I tried and it was not the case.
A DataGridTextColumn has a unique mechanism, the source is updated when you leave the row, not just the cell. Here is the catch the Parameters tab has a single row so you can not leave it. But when the Refresh button is pressed the source is updated anyway and at the same time RefreshAsync method is started as well. So one thread updated the AccountObjects, the other tried to use it in an intermediate state.
I would have remembered if I have started two parallel threads. But explicitly I started just one thread, the other started implicitly.
It is very difficult to debug problems with parallel threads because the error does not occur deterministically but it depends on the timings.
The property setter of the source variable would be a nice place for a breakpoint so one can be sure when was the change. But I use Fody for INotifyPropertyChange so it was not an option for me.
Setting UpdateSourceTrigger=LostFocus solved the problem but I can not be 100% sure.
View model to database
DisplayedRows.CollectionChanged += DetachAttachPropertyChangedEventHandler;
private void DetachAttachPropertyChangedEventHandler(object sender,
NotifyCollectionChangedEventArgs e)
{
IsViewModellAndDbOutOfSync = true;
SetIsError();
if (e.OldItems is not null)
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= InvalidateViewModelAndRecalculateError;
if (e.NewItems is not null)
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += InvalidateViewModelAndRecalculateError;
}
The view model to database direction is also interesting. Detecting a change in a DataGrid is not easy. You need PropertyChanged for each row. If there is a row deletion or addition then it must be handled also.
public void InvalidateViewModelAndRecalculateError() { IsViewModellAndDbOutOfSync = true; SetIsError(); if (AutoRefreshDataGrid) Task.Run(CopyDataFromTableVmToDbAsync); }
If a row has changed then the IsViewModellAndDbOutOfSync is set to true. This variable is true if there was some change in the database and the view model should be refreshed or if the View changed the View model but the change did not go to the database yet. CopyDataFromTableVmToDbAsync does not change this property directly but calls indirectly CopyDataFromDbToTableVmAsync which sets this value to true.
Notice that Task.Run is used here. This command explicitly starts a parallel thread which can be dangerous.
Debugging is like air resistance, the bigger your code, the more difficult. Air resistance can be tricked by streamlined shape, in programming clean code can be your savior. Never underestimate how buggy, ineffective, dirty and hard to understand can be your own code you just wrote some years ago.
Update
Only one day has passed and the problem came back like a boomerang. Setting UpdateSourceTrigger=LostFocus was only a temporary fix. At least now I remember what is the cause of the problem.
private string statementFolder;
public string StatementFolder
{
get => statementFolder;
set => statementFolder = value;
}
I turned the StatementFolder into a full property and placed a breakpoint into the setter. I changed the value in the view and I did not hit the breakpoint. I tried to find some explanation on the net but non of them was believable. If you know the reason then let me know.
<DataGridTemplateColumn Header="{x:Static r:Resource.StatementFolder}" ToolTipService.ToolTip="{x:Static r:Resource.StatementFolderToolTip}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Style="{StaticResource TextAlignmentLeftElementStyle}" Text="{Binding StatementFolder}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding StatementFolder, UpdateSourceTrigger=LostFocus}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
My next idea was to use DataGridTemplateColumn instead of DataGridTextColumn. In a DataGridTemplateColumn I can use explicitly a TextBox with LostFocus. Now I hit the breakpoint at every time when I change the Statement folder. What is strange is that if I use Default instead of LostFocus then it does not work although the default for TextBox is LostFocus. Is it the final solution? Time will tell.