Search code examples
c#mvvmdatagridprism

How to trigger a OnPropertyChanged event on a datagrid using MVVM/PRISM pattern


I have a DataGrid that is bound to a view model, that has an ItemSource of an ObservableCollection. The ObservableCollection in turn is bound to a SQL Server database via EF / DbContext.

I can add and delete rows currently using Prism DelegateCommand (inherits from ICommand) commands and delegate buttons.

What I want to do is edit the DataGrid, and when the row/cell loses focus automatically save the values back to the database.

I think I am going down the right track with this algorithm, but the PropertyChanged event is not being fired when the datagrid contents are being edited.

public class StudentEntity(
    string studentId,
    string firstName,
    string lastName,
    int cohortYear,
    string unitCode,
    string? email) : INotifyPropertyChanged
{
    [MaxLength(50)] public Guid Id { get; set; } = Guid.NewGuid();
    [MaxLength(10)] public string StudentId { get; set; } = studentId;
    [MaxLength(50)] public string FirstName { get; set; } = firstName;
    [MaxLength(50)] public string LastName { get; set; } = lastName;
    [MaxLength(100)] public string? Email { get; set; } = email;

    public int CohortYear { get; set; } = cohortYear;

    [MaxLength(10)] public string UnitCode { get; set; } = unitCode;

    public DateTime? CreatedDt { get; set; }= DateTime.UtcNow;

    public event PropertyChangedEventHandler? PropertyChanged;
        
    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            // this code does not execute when datagrid values are changed
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        
    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        // this code does not execute when datagrid values are changed
        if (EqualityComparer<T>.Default.Equals(field, value)) 
            return false;

        field = value;

        OnPropertyChanged(propertyName);
        return true;
    }
}

Excerpt from view model:

public class StudentsViewModel : AbstractViewModel // AbstractViewModel inherits from Prism :BindableBase
{
    public ObservableCollection<StudentEntity> StudentsObservableCollection;
    public DelegateCommand<StudentEntity> DeleteStudentCommand { get; }

    public StudentsViewModel()
    {
        StudentsObservableCollection.Clear();
        StudentsObservableCollection.CollectionChanged += StudentsObservableCollection_CollectionChanged;
        DeleteStudentCommand = new DelegateCommand<StudentEntity>(DeleteStudentDelegate);
    }

    private void StudentsObservableCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            var c = e.NewItems.Count;

            foreach (INotifyPropertyChanged added in e.NewItems)
            {
                // this code is triggered when new students added to the Students collection
                added.PropertyChanged += Student_PropertyChanged;
            }
        }
        
        if (e.OldItems == null) 
            return;

        foreach (INotifyPropertyChanged removed in e.OldItems)
        {
            removed.PropertyChanged -= Student_PropertyChanged;
        }
    }
    
    private void Student_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        // this code does not execute on datagrid change
        if (sender is not StudentEntity row)
        {
            return;
        }
        
        SaveData(row);
    }
    
    private async void SaveData(StudentEntity student)
    {
        // save data goes here.
    }
    
    private async void DeleteStudentDelegate(StudentEntity student)
    {
        // delete code goes here (works)
    }
}

The XAML:

<UserControl x:Class="ScriptGen.Views.Students.StudentDataGrid"
             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:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
             
  <d:UserControl.DataContext>
    <d:DesignInstance Type="viewModels:StudentsViewModel" />
  </d:UserControl.DataContext>
  <DataGrid
    CanUserResizeColumns="True"
    AutoGenerateColumns="False"
    Name="StudentsDataGrid"
    DataContext="{Binding }"
    ItemsSource="{Binding StudentsObservableCollection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <DataGrid.Columns>
      <DataGridTextColumn Header="Student ID" Binding="{Binding StudentId}" />
      <DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" />
      <DataGridTextColumn Header="Last Name" Binding="{Binding LastName}" />
      <DataGridTextColumn Header="Year" Binding="{Binding CohortYear}" />
      <DataGridTextColumn Header="Unit Code" Binding="{Binding UnitCode}" />
      <DataGridTextColumn Header="Email" Binding="{Binding Email}" />
      <DataGridTemplateColumn>
        <DataGridTemplateColumn.CellTemplate>
          <DataTemplate>
            <Button
              Cursor="Hand"
              Style="{StaticResource MaterialDesignFlatLightButton}"
              Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
                                              Path= DataContext.DeleteStudentCommand}"
              CommandParameter="{Binding SelectedItem, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}"
              ToolTip="Delete this student's record">
              <materialDesign:PackIcon Kind="Delete" />
            </Button>
          </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
      </DataGridTemplateColumn>
    </DataGrid.Columns>
  </DataGrid>
  </UserControl>

Solution

  • [MaxLength(50)] public string FirstName { get; set; } = firstName;

    I see noone calling SetField here and I wouldn't expect PropertyChanged to be raised.

    That being said, Entity Framework Core can use entities that implement INotifyPropertyChanged, have a look at the documentation over at microsoft:

    public class StudentEntity( string firstName ) : INotifyPropertyChanged
    {
        [MaxLength(50)] 
        public string FirstName 
        { 
            get => firstName; 
            set => SetField( ref firstName, value );
        };
    
        // TODO add more properties
    
        // TODO implement INotifyPropertyChanged
    }