Search code examples
c#wpfmvvmdatagridobservablecollection

MVVM WPF - How to update DataGrid bound to ObservableCollection


What I'm trying to do:

I have a WPF app, linked to a SQL-server. I am using the MVVM-light package (I do actually have Prism.Core installed, but I'm not sure if I'm using it or not.... new to MVVM).

There's a DataGrid, bound to an ObservableCollection. I have been trying to implement the PropertyChangedEventHandler, but I can't seem to get it to work.

I have a Delete button bound, and I am able to remove rows, but when I re-open the form, the changes does not carry over.

I tried to change the binding-mode for the DataGrid from OneWay to TwoWay. With OneWay, the changes does not carry over when I re-open the form. With TwoWay, I get this error message when opening the child form (which contains the DataGrid):

System.InvalidOperationException: 'A TwoWay or OneWayToSource binding cannot work on the read->only property 'licenseHolders' of type 'Ridel.Hub.ViewModel.LicenseHoldersViewModel'.'

So, If I then add a set; to my public ObservableCollection<LicenseHolders> licenseHolders { get; }, the program runs, but the previous problem persists, like it did when there was a OneWay mode configuration on the DataGrid.

What do I need to do to get this to work without communicating directly with the Sql-server, which would defy the whole point of using this methodology in the first place?

ViewModel:

public class LicenseHoldersViewModel : INotifyPropertyChanged {

    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<LicenseHolders> licenseHolders { get; }
        = new ObservableCollection<LicenseHolders>();

    public LicenseHoldersViewModel() {

        FillDataGridLicenseHolders();
    }

    private void FillDataGridLicenseHolders() {

        try {

            using (SqlConnection sqlCon = new(ConnectionString.connectionString))
            using (SqlCommand sqlCmd = new("select * from tblLicenseHolder", sqlCon))
            using (SqlDataAdapter sqlDaAd = new(sqlCmd))
            using (DataSet ds = new()) {

                sqlCon.Open();
                sqlDaAd.Fill(ds, "tblLicenseHolder");

                foreach (DataRow dr in ds.Tables[0].Rows) {

                    licenseHolders.Add(new LicenseHolders {

                        ID = Convert.ToInt32(dr[0].ToString()),
                        Foretaksnavn = dr[1].ToString(),
                        Foretaksnummer = dr[2].ToString(),
                        Adresse = dr[3].ToString(),
                        Postnummer = (int)dr[4],
                        Poststed = dr[5].ToString(),
                        BIC = dr[6].ToString(),
                        IBAN = dr[7].ToString(),
                        //Profilbilde ???
                        Kontaktperson = dr[8].ToString(),
                        Epost = dr[9].ToString(),
                        Tlf = dr[10].ToString()
                    });
                }
            }

        } catch (Exception ex) {

            MessageBox.Show(ex.Message, "Message", MessageBoxButton.OK, MessageBoxImage.Information);
        }
    }

    private RelayCommand<LicenseHolders> _removeLicenseHoldersCommand;

    public RelayCommand<LicenseHolders> RemoveLicenseHoldersCommand => _removeLicenseHoldersCommand
        ??= new RelayCommand<LicenseHolders>(RemoveLicenseHolderExecute, RemoveLicenseHolderCanExecute);

    private bool RemoveLicenseHolderCanExecute(LicenseHolders myLicenseHolder) {

        // Checking for the removeable licenseholder in the collection
        return licenseHolders.Contains(myLicenseHolder);
    }

    private void RemoveLicenseHolderExecute(LicenseHolders myLicenseHolder) {

        licenseHolders.Remove(myLicenseHolder);
    }

    private void OnPropertyChanged(string myLicenseHolder) {

        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(myLicenseHolder));
    }
}

Model

public class LicenseHolders {

    public int ID { get; set; }
    public string Foretaksnavn { get; set; }
    public string Foretaksnummer { get; set; }
    public string Adresse { get; set; }
    public int Postnummer { get; set; }
    public string Poststed { get; set; }
    public string BIC { get; set; }
    public string IBAN { get; set; }
    public string Kontaktperson { get; set; }
    public string Epost { get; set; }
    public string Tlf { get; set; }

}

Code-behind

public partial class Personell : Window {

        LicenseHoldersViewModel licenseHoldersViewModel;

        public Personell() {

            InitializeComponent();
            btnLogOut.Content = UserInfo.UserName;

            licenseHoldersViewModel = new LicenseHoldersViewModel();
            base.DataContext = licenseHoldersViewModel;
        }

XAML

<Window x:Class="Ridel.Hub.Personell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Ridel.Hub" xmlns:viewmodel="clr-namespace:Ridel.Hub.ViewModel" d:DataContext="{d:DesignInstance Type=viewmodel:LicenseHoldersViewModel}"
        mc:Ignorable="d"
        
    <Canvas Margin="10,10,10,10">
    <!-- I click this button when I want to delete a DataGrid row -->
        <Button Style="{DynamicResource ButtonWithRoundCornersGreen}" FontSize="22" x:Name="btnDelete" Content="Delete license holder" Width="187" Height="47" 
                Background="#48bb88" Foreground="White" Canvas.Left="547" Canvas.Top="668" IsEnabled="False" 
                Command="{Binding RemoveLicenseHoldersCommand}" CommandParameter="{Binding SelectedItem, ElementName=dgLicenseHolder}"/>

        <DataGrid             
            x:Name="dgLicenseHolder"
            CanUserAddRows="False"
            ItemsSource="{Binding licenseHolders, Mode=TwoWay}"
            Height="557" 
            Width="505" 
            ColumnWidth="*"
            Canvas.Top="158" 
            FontSize="20"
            IsReadOnly="True"
            SelectionMode="Single"
            AutoGenerateColumns="False"
            CanUserDeleteRows="False"          
            SelectionChanged="dgLicenseHolder_SelectionChanged" Canvas.Left="31" >

            <DataGrid.Columns>
                <DataGridTextColumn Header="ID"             Binding="{Binding Path='ID'}" IsReadOnly="True" Visibility="Collapsed"/>
                <DataGridTextColumn Header="Foretaksnavn"   Binding="{Binding Path='Foretaksnavn'}" IsReadOnly="True" Visibility="Visible"/>
                <DataGridTextColumn Header="Foretaksnummer" Binding="{Binding Path='Foretaksnummer'}" IsReadOnly="True" Visibility="Collapsed"/>
                <DataGridTextColumn Header="Adresse"        Binding="{Binding Path='Adresse'}" IsReadOnly="True" Visibility="Collapsed"/>
                <DataGridTextColumn Header="Postnummer"     Binding="{Binding Path='Postnummer'}" IsReadOnly="True" Visibility="Collapsed"/>
                <DataGridTextColumn Header="Poststed"       Binding="{Binding Path='Poststed'}" IsReadOnly="True" Visibility="Visible"/>
                <DataGridTextColumn Header="BIC"            Binding="{Binding Path='BIC'}" IsReadOnly="True" Visibility="Collapsed"/>
                <DataGridTextColumn Header="IBAN"           Binding="{Binding Path='IBAN'}" IsReadOnly="True" Visibility="Collapsed"/>
                <DataGridTextColumn Header="Kontaktperson"  Binding="{Binding Path='Kontaktperson'}" IsReadOnly="True" Visibility="Collapsed"/>
                <DataGridTextColumn Header="Epost"          Binding="{Binding Path='Epost'}" IsReadOnly="True" Visibility="Collapsed"/>
                <DataGridTextColumn Header="Tlf"            Binding="{Binding Path='Tlf'}" IsReadOnly="True" Visibility="Collapsed"/>
            </DataGrid.Columns>
            
        </DataGrid>
    </Canvas>
</Window>

Solution

  • I thought the deletion of rows would be reflected on the server, because of how the ObservableCollection was bound.

    This is not true.

    ObservableCollection only provides notification of its change.
    That is, if you added an item to the ObservableCollection, then it will automatically appear in the DataGrid. If you use the collection without notification (for example, List), then this will not happen. And you will have to come up with some additional techniques in order to trigger the update of the view.

    ObservableCollection "does not know" how its elements are created, how they are related to the data source (database, file or other source).
    In your previous topic, in the code of my example, I showed the bool RemoveFromBD(LicenseHolders license) method just to write the code that removes the license from the database into it.

    I don’t know ADO well and I don’t know the name of the table fields in your database at all, but perhaps this implementation of the method will be correct:

            private bool RemoveFromBD(LicenseHolders license)
            {
                string sql = string.Format("Delete from tblLicenseHolder where ID = '{0}'", license.ID);
                using (SqlConnection sqlCon = new SqlConnection(ConnectionString.connectionString))
                using (SqlCommand cmd = new SqlCommand(sql, sqlCon))
                {
                    try
                    {
                        sqlCon.Open();
                        cmd.ExecuteNonQuery();
                        return true;
                    }
                    catch
                    {
                        return false;
                    }
                }
            }