1

I'm writing an app in WPF, trying to use the MVVM-design pattern (which is new to me). I have a DataGrid bound to an ObservableCollection.

What I'm trying to achieve:

Delete the currently selected DataGrid-row using a 'Delete'-button. I've tried a plethora of forum-posts and videos to find a solution. The solution has probably stared me right in the face several times, but at this point I'm more confused than I was when I first started.

Any assistance would be appreciated.

ViewModel (updated with working code, thanks to EldHasp):

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Windows;
using GalaSoft.MvvmLight.CommandWpf;
using Ridel.Hub.Model;

namespace Ridel.Hub.ViewModel {

    public class LicenseHoldersViewModel : INotifyPropertyChanged {

        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");

                    //if (licenseHolders == null)
                        //licenseHolders = new ObservableCollection<LicenseHolders>();

                    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
            ?? (_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);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string propertyName) {

            if (PropertyChanged != null) {

                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Model:

public class LicenseHolders {

    public string example { get; set; }
    // more of the same...
}

XAML:

<Button Style="{DynamicResource ButtonWithRoundCornersGreen}" 
    FontSize="22" x:Name="btnDelete" Content="Delete" 
    Width="187" Height="47" Background="#48bb88" Foreground="White" 
    Canvas.Left="547" Canvas.Top="668" IsEnabled="False" Click="btnDelete_Click"/>

<DataGrid             
    x:Name="dgLicenseHolder"
    CanUserAddRows="False"
    ItemsSource="{Binding licenseHolders}"
    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="Example" Binding="{Binding Path='Example'}" IsReadOnly="True" Visibility="Collapsed"/>
        // more of the same..
    </DataGrid.Columns>            
</DataGrid>

Code-behind:

public partial class Personell : Window {

    LicenseHoldersViewModel licenseHoldersViewModel;

    public Personell() {

        InitializeComponent();

        licenseHoldersViewModel = new LicenseHoldersViewModel();
        base.DataContext = licenseHoldersViewModel;
    }
}
Ole M
  • 317
  • 1
  • 17

2 Answers2

1

Since you are using MVVM, you should not use button event Click handler to manipulate ViewModel data.
To do this, you must create a command-property in the ViewModel and bind it to the buttons.
The command parameter bind either to SelectedItem (if the Single mode is selected and only one selected row is deleted), or to SelectedItems (if the Extended mode is selected and multiple rows are deleted).

I don't know which implementations of INotifyPropertyChanged and ICommand you are using.
Therefore, I am writing an example based on my own implementation of the BaseInpc and RelayCommand classes.

    public class LicensesViewModel : BaseInpc
    {
        // A collection-property of the ObservableCollection type is best done «ReadOnly».
        public ObservableCollection<LicenseHolders> LicensesHolders { get; }
            = new ObservableCollection<LicenseHolders>();
        public void FillDataGrid()
        {

            //---------------
            //---------------
            //---------------

                //if (licenseHolders == null)
                //    licenseHolders = new ObservableCollection<LicenseHolders>();

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

                    LicensesHolders.Add(new LicenseHolders
                    {

                        example = dr[0].ToString(),
                        // more of the same...

                    });
                }
            }
        }


        private RelayCommand _removeLicenseCommand;
        public RelayCommand RemoveLicenseCommand => _removeLicenseCommand
            ?? (_removeLicenseCommand = new RelayCommand<LicenseHolders>(RemoveLicenseExecute, RemoveLicenseCanExecute));

        private bool RemoveLicenseCanExecute(LicenseHolders license)
        {
            // Checking for the Removable License in the Collection
            return LicensesHolders.Contains(license);
        }

        private void RemoveLicenseExecute(LicenseHolders license)
        {
            // If necessary, the code for removing the license from the database is placed at the beginning of the method.
            // For example, calling some method for this, returning true if the deletion is successful.
            var result = RemoveFromBD(license);

            //It is then removed from the collection.
            if (result)
                LicensesHolders.Remove(license);
        }

        private bool RemoveFromBD(LicenseHolders license)
        {
            throw new NotImplementedException();
        }
    }
<Button Style="{DynamicResource ButtonWithRoundCornersGreen}" 
    FontSize="22" x:Name="btnDelete" Content="Delete" 
    Width="187" Height="47" Background="#48bb88" Foreground="White" 
    Canvas.Left="547" Canvas.Top="668" IsEnabled="False"
    Command="{Binding RemoveLicenseCommand}"
    CommandParameter="{Binding SelectedItem, ElementName=dgLicenseHolder}"/>

An implementation option for the multi-selection mode:

        private RelayCommand _removeLicensesCommand;
        public RelayCommand RemoveLicensesCommand => _removeLicensesCommand
            ?? (_removeLicensesCommand = new RelayCommand<IList>(RemoveLicensesExecute, RemoveLicensesCanExecute));

        private bool RemoveLicensesCanExecute(IList licenses)
        {
            // Checking for the Removable License in the Collection
            return licenses.OfType<LicenseHolders>().Any(license => LicensesHolders.Contains(license));
        }

        private void RemoveLicensesExecute(IList licenses)
        {
            foreach (var license in licenses.OfType<LicenseHolders>().ToArray())
            {
                var result = RemoveFromBD(license);

                if (result)
                    LicensesHolders.Remove(license);
            }
        }
<Button Style="{DynamicResource ButtonWithRoundCornersGreen}" 
    FontSize="22" x:Name="btnDelete" Content="Delete" 
    Width="187" Height="47" Background="#48bb88" Foreground="White" 
    Canvas.Left="547" Canvas.Top="668" IsEnabled="False"
    Command="{Binding RemoveLicensesCommand}"
    CommandParameter="{Binding SelectedItems, ElementName=dgLicenseHolder}"/>

Update! (when trying to implement the solution of 'EldHasp' Error message

The error is due to using MVVMLight.
You initially did not indicate that you are using this package.
In it, the RelayCommand is not the base for the RelayCommand<T>.
Therefore, the type of the property and variable must match the type of the used constructor.

This is how you need it for MVVMLight:

    private RelayCommand<LicenseHolders> _removeLicenseHoldersCommand;

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

Additional advice on assigning DataContext.

Assigning it in Code Behind is not handled by XAML Designer at design time. Therefore, you do not see during development how your UI elements will look when data is transferred to them.
This is extremely inconvenient.
To solve this, you need to set the Development Time DataContext.
But in simple cases, you just need to set the DataContext in XAML.

Clear Code Behind:

public partial class Personell : Window 
{
    public Personell() => InitializeComponent();
}

Add to XAML:

<Window.DataContext>
  <local:LicenseHoldersViewModel/>
</Window.DataContext>
EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • Thank you for the informative post, the solution looks elegant, and I am trying to implement it. `public RelayCommand RemoveLicenseHolderCommand => _removeLicenseHoldersCommand ?? (_removeLicenseHoldersCommand = new RelayCommand(RemoveLicenseHolderExecute, RemoveLicenseHolderCanExecute);` The above gives me an exception; CS0029, the types cannot be implicitly converted. – Ole M Jun 15 '21 at 09:03
  • 1
    When does this error occur? When compiled or executed? – EldHasp Jun 15 '21 at 09:26
  • Compiling, red squiggly line :-( – Ole M Jun 15 '21 at 09:54
  • 1
    Probably copied incorrectly, or got confused in variant codes. Show the complete code how you applied my example. And preferably, a screen of the error. – EldHasp Jun 15 '21 at 09:59
  • I updated my original post with the error message, as well as all my ViewModel-code. Many thanks! – Ole M Jun 15 '21 at 10:21
  • 1
    I have updated my answer. Read his supplement for the cause of the error. – EldHasp Jun 15 '21 at 10:42
  • 1
    Also read my additional recommendation for assigning a DataContext value. – EldHasp Jun 15 '21 at 10:58
  • Superb, that did the trick. I apologize for not given adequate information from the start. That being said, another issue has appeared. When I run my program and try to delete a row in the DataGrid, it gives me the following exception: System.NotImplementedException: 'The method or operation is not implemented.' If I put a break-point at that line that caused the error: (`var result = RemoveFromDB(myLicenseHolder`); It gives me a CS0136: .."the name is used in an enclosing local scope to define a local or parameter". – Ole M Jun 15 '21 at 11:07
  • 1
    I haven't implemented the RemoveFromDB method. It is just shown as an example. I don't even know if you need to synchronize the deletion from the DataGrid with the deletion from the DB. If necessary, remove the `throw new NotImplementedException();` in the body of the method. And add your code for interacting with the database. – EldHasp Jun 15 '21 at 11:11
  • I believe as I researched this, the whole point would be that you did not need to communicate with the `Sql-server` at all, but simply remove from the `Observable Collection`. Right? How would I modify your code to allow only this option? Seems I can't remove or change any of the code without creating many discrepancies. Man, this WVVM thing is starting to seem overly complex. I really need to take a few steps back and re-research the whole concept. I will look at the documentation you posted. Spasiba! – Ole M Jun 15 '21 at 11:45
  • 1
    Leave only the line in the command method: `LicensesHolders.Remove(license);` Delete or comment out the previous two rows. – EldHasp Jun 15 '21 at 11:52
  • It's been a long day, that finally did the trick. THANK YOU!! for your patience. Marking your reply as the answer. MVVM sure is a beautiful monstrosity. – Ole M Jun 15 '21 at 11:57
  • 1
    You just don't know the patterns well. Not just MVVM. If you knew MVC, MVP, then it would be much easier for you to understand MVVM. And WPF also has built-in tools for bindings, data contexts, so a large solution in WPF is much easier to implement than in Forms or the Console. Try to understand, figure it out. And, over time, you yourself will wonder what is so difficult in this you have seen before. – EldHasp Jun 15 '21 at 12:23
0

You can use Prism. Intall package Prism.Core then Install-Package Microsoft.Xaml.Behaviors.Wpf -Version 1.1.31 packages in your project, in your xaml declare namespace as - xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

<Button Content="Button">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="Click">
                <i:InvokeCommandAction Command="{Binding MyCommand }" 
                            CommandParameter="{Binding PassRow/IDHere}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Button>

In Your ViewModel -

public class ViewModel
{
    public ICommand MyCommand { get; set; }

    public ViewModel()
    {
        MyCommand = new DelegateCommand<object>(OnRowDeleted);
    }

    public void OnRowDeleted(object o)
    {
        // remove the value from your observablecollection here using LINQ
    }
}
Aadittya
  • 59
  • 6