Search code examples
c#dapper

ObservableCollection does not update => DataGrid UI does not show everything


I have a problem with some code i wrote. I am trying to build a Applicant-Administration program. I have a SQL-Server on which is my Database.

At the start of the program, a DataGrid is bound to a ObservableCollection which fires a SQL SELECT via Dapper.

If I insert a new Applicant with another SQL query, the DataGrid does not show this new Applicant.

I debugged through my program to see whats going wrong. I think it is the ObservableCollection on which I bind.

Code of DataGrid

<DataGrid x:Name="DgInformation" ItemsSource="{Binding AllData,Mode=TwoWay}"
                  SelectedItem="{Binding SelectedApplicantModel, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}"
                  AutoGenerateColumns="False"
                  Grid.Row="2"
                  MaxHeight="870"
                  IsSynchronizedWithCurrentItem="True"
                  IsReadOnly="True"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  HorizontalAlignment="Center"
                  Margin="10,5,10,5"
                  Background="#F5F5F5"
                  AlternatingRowBackground="#eeeeee" AlternationCount="2">
            <DataGrid.RowStyle>
                <Style TargetType="{x:Type DataGridRow}">
                    <EventSetter Event="MouseDoubleClick" Handler="DG_Information_MouseDoubleClick" />
                    <EventSetter Event="KeyDown" Handler="DG_Information_KeyDown" />
                </Style>
            </DataGrid.RowStyle>
            <DataGrid.Columns>
                <materialDesign:MaterialDataGridTextColumn Header="Vorname" Width="110"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding FirstName,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <materialDesign:MaterialDataGridTextColumn Header="Nachname" Width="110"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding LastName,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <materialDesign:MaterialDataGridTextColumn Header="Geburtstag" Width="110"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding Birthday,Mode=TwoWay, StringFormat=\{0:dd.MM.yy\}, UpdateSourceTrigger=PropertyChanged}" />
                <materialDesign:MaterialDataGridTextColumn Header="Schulabschluss" Width="140"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding Graduation.Graduation,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <materialDesign:MaterialDataGridTextColumn Header="PLZ" Width="90"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding PostCode,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <materialDesign:MaterialDataGridTextColumn Header="Ort"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding Address.HomePlace, Mode =TwoWay,UpdateSourceTrigger=PropertyChanged}" />
                <materialDesign:MaterialDataGridTextColumn Header="Geschlecht"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding Gender.Gender,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <DataGridHyperlinkColumn Header="E-Mail"
                                         EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                         Binding="{Binding EMail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                    <DataGridHyperlinkColumn.ElementStyle>
                        <Style>
                            <EventSetter Event="Hyperlink.Click" Handler="DgHyperlinkClick" />
                        </Style>
                    </DataGridHyperlinkColumn.ElementStyle>
                </DataGridHyperlinkColumn>
                <materialDesign:MaterialDataGridTextColumn Header="Stelle"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding Job,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
                <materialDesign:MaterialDataGridTextColumn Header="Bewertung"
                                                           EditingElementStyle="{StaticResource MaterialDesignDataGridTextColumnPopupEditingStyle}"
                                                           Binding="{Binding Grade.Grade, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            </DataGrid.Columns>
        </DataGrid>

My ViewModel

public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private ObservableCollection<ApplicantModel> _allData;

        public BaseViewModel()
        {
            AllData = ApplicantService.GetInstance().GetAll();
        }

        public ObservableCollection<ApplicantModel> AllData
        {
            get { return _allData; }
            set
            {
                _allData = value;
                OnPropertyChanged();
            }
        }

My Service Class

private readonly IApplicantRepository _repository;

        public ApplicantService()
        {
            _repository = new ApplicantRepository();
        }

        public ObservableCollection<ApplicantModel> GetAll()
        {
            return _repository.GetAll();
        }

And my Repository

public ObservableCollection<ApplicantModel> GetAll()
        {
                var result =
                    Db.Query<ApplicantModel, AddressModel, GraduationModel, GenderModel, GradeModel, ApplicantModel>(
                        "SELECT a.ID, a.FirstName, a.LastName, a.Birthday, a.EMail, a.Job, ad.PostCode, ad.HomePlace, g.Graduation, g.ID, ge.Gender, ge.ID, Grade.Grade FROM dbo.Applicant As a INNER JOIN Address As ad ON ad.PostCode = a.PostCode INNER JOIN Graduation As g ON g.ID = a.GraduationID INNER JOIN Gender As ge On ge.ID = a.GenderID INNER JOIN Grade ON Grade.Grade = a.GradeID",
                        _mapSelectGetAll, splitOn: "HomePlace,Graduation,Gender,Grade");
                Logger.LogInfo("Function GetAll executed in ApplicantRepository!");

                ObservableCollection<ApplicantModel> obsCollection = new ObservableCollection<ApplicantModel>(result);
                return obsCollection;
        }

        private ApplicantModel _mapSelectGetAll(ApplicantModel applicant, AddressModel address, GraduationModel graduation, GenderModel gender, GradeModel grade)
        {
            applicant.Graduation = graduation;
            applicant.Address = address;
            applicant.Gender = gender;
            applicant.Grade = grade;

            return applicant;
        }

I hope anyone can show me, where the mistake is.


Solution

  • Your analysis is spot on :) It is indeed a problem with your binding to AllData, mainly because you allow it to be reassigned in a setter.

    Your DataGrid is really only listening to the changes of your ObservableCollection (or rather, handling the INotifyCollectionChanged interface. It doesn't know that your parent object has changed the collection itself (even though you fire a PropertyChanged event)

    Ideally, you should auto-initialize observable collections, and not allow any outside setting of it. Let it handle addition/removal/resetting through the CollectionChanged.

    The good thing is that you could now write your program in an easier way (no need to pass around ObservableCollection<T>)

    So your viewmodel property would go to:

    public IList<ApplicantModel> AllData { get; } = new ObservableCollection<ApplicationModel>();
    

    because ObservableCollection<T> inherits from IList<T> there is no problem in dumming it down. The DataGrid will still detect the INotifyCollectionChanged interface.

    This does mean however that you can no longer assign a new collection to the property, but that's in essence a good thing :) Also, the AllData property will never be null, so that's another good thing.

    However, you now need to rewrite the logic of updating your AllData property on your ViewModel

    So, in case you still want to auto-fill it in the constructor, you can change the viewmodel property and constructor to:

    public BaseViewModel() {
        AllData = new ObservableCollection<ApplicantModel>( ApplicantService.GetInstance().GetAll() );
    } 
    
    public IList<ApplicantModel> AllData { get; }
    

    And no need for the private backing field, you now have a readonly auto-property instead, and the constructor utilizes the IEnumerable<T> constructor parameter for the ObservableCollection<T>

    Then, you should change your service class, there is really no reason for it to return a full blown ObservableCollection<ApplicantModel>(), just let it return IEnumerable<ApplicantModel> or at most a IReadOnlyList<ApplicantModel>.

    This change then also needs to go to your Repository.

    This then eventually leaves you to handle deletions and updates of data. If you are working with some local database, just acting upon save/delete requests would be fine, if you work with a central database, it might be more involving :)

    Hopefully it's not to much text :)