C# MVVM menu & datagrid

.xaml


<Window x:Class="WPFTutoMVVM_MenuAndDataGrid.MainWindow"
        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:WPFTutoMVVM_MenuAndDataGrid"
        xmlns:vm="clr-namespace:WPFTutoMVVM_MenuAndDataGrid.src.Relay"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    


    <Grid>
        <Menu DockPanel.Dock="Top" x:Name="MainMenu" DataContext="{Binding MenuVM}">
            <MenuItem Header="_Fichier">
                <MenuItem Header="_Nouveau patient" x:Name="miNewPatient" Command="{Binding NewPatientCommand}" InputGestureText="Ctrl+N"/>
                <MenuItem Header="_Ouvrir..." x:Name="miOpen" InputGestureText="Ctrl+O"/>
                <Separator/>
                <MenuItem Header="_Exporter" x:Name="miExport"/>
                <MenuItem Header="_Initialiser" x:Name="miInitialize" Command="{Binding InitializeDatabaseCommand}"/>
                <Separator/>
                <MenuItem Header="_Quitter" x:Name="miExit" InputGestureText="Alt+F4"/>
            </MenuItem>

            <MenuItem Header="_Édition">
                <MenuItem Header="_Copier" x:Name="miCopy" InputGestureText="Ctrl+C"/>
                <MenuItem Header="_Coller" x:Name="miPaste" InputGestureText="Ctrl+V"/>
            </MenuItem>

            <MenuItem Header="_Patients">
                <MenuItem Header="_Liste des patients" x:Name="miListPatients"/>
                <MenuItem Header="_Rechercher..." x:Name="miSearchPatients" InputGestureText="Ctrl+F"/>
            </MenuItem>

            <MenuItem Header="_Aide">
                <MenuItem Header="_Documentation" x:Name="miDocs"/>
                <MenuItem Header="À _propos" x:Name="miAbout"/>
            </MenuItem>
        </Menu>
        
        <DataGrid ItemsSource="{Binding PatientVM.Patients}"
                  SelectedItem="{Binding PatientVM.SelectedPatient, Mode=TwoWay}"
                  AutoGenerateColumns="False"
                  IsReadOnly="False"
                  CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  Margin="20,30,0,8"
                  SelectionMode="Single"
                  EnableRowVirtualization="True"
                  EnableColumnVirtualization="True" Width="600" Height="300" MaxHeight="300" VerticalAlignment="Top" HorizontalAlignment="Left">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Nom" Binding="{Binding LastName, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Width="*" MinWidth="150" />
                <DataGridTextColumn Header="Prénom" Binding="{Binding FirstName, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Width="*" MinWidth="150"/>
                <DataGridTextColumn Header="Naissance"
                                    Binding="{Binding BirthDate, StringFormat=\{0:yyyy-MM-dd\}, ValidatesOnDataErrors=True}"
                                    Width="140"/>
                <DataGridCheckBoxColumn Header="Actif" Binding="{Binding Active}" Width="80"/>

                
                <DataGridTemplateColumn Header="Actions" Width="160">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
                                <Button Content="Voir"
                                        Command="{Binding DataContext.PatientVM.ViewCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
                                        CommandParameter="{Binding}"/>
                                <Button Content="Supprimer"
                                        Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
                                        CommandParameter="{Binding}"/>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>

    </Grid>
</Window>

  
  

.cs


 public partial class MainWindow : Window
 {
     public MainWindow()
     {
         InitializeComponent();
         DataContext = new MainViewModel(); 
     }
 }
  

MainViewModel.cs


 internal class MainViewModel
 {
     public MenuViewModel MenuVM { get; } = new();
     public DataGridViewModel PatientVM { get; } = new();
 }

MenuViewModel.cs


internal class MenuViewModel
{
    public ICommand NewPatientCommand { get; }
    public ICommand InitializeDatabaseCommand { get; }

    public MenuViewModel()
    {
        NewPatientCommand = new RelayCommand(_ => NewPatient(), _ => true);
        InitializeDatabaseCommand = new RelayCommand(InitializeDatabase, _ => true);
    }

    private void NewPatient()
    {
        // MessageBox.Show("Nouveau patient");
        // var window = new CreatePractitioner.CreatePractitionerWindow();
        // window.ShowDialog();
    }

    private void InitializeDatabase(object? choice)
    {
        // MedicalManagementDatabaseInit.Initialize();
        MessageBox.Show("Initialisation de la base de données OK", "Info");
    }
}

DataGridViewModel.cs


internal class DataGridViewModel : INotifyPropertyChanged
{
    public ObservableCollection<PatientDto> Patients { get; } = new();

    private readonly ICollectionView _patientsView;
    public ICollectionView PatientsView => _patientsView;

    private PatientDto? _selectedPatient;
    public PatientDto? SelectedPatient
    {
        get => _selectedPatient;
        set
        {
            if (_selectedPatient != value)
            {
                _selectedPatient = value;
                OnPropertyChanged(nameof(SelectedPatient));
                DeleteCommandAsCommand?.RaiseCanExecuteChanged();
                ViewCommandAsCommand?.RaiseCanExecuteChanged();
                Console.WriteLine($"SelectedPatient changed: {SelectedPatient?.FirstName}");
            }
        }
    }

    // Champ de filtre
    private string _filterText = string.Empty;
    public string FilterText
    {
        get => _filterText;
        set
        {
            if (_filterText != value)
            {
                _filterText = value;
                OnPropertyChanged(nameof(FilterText));
                _patientsView.Refresh();
            }
        }
    }

    // Commands
    public ICommand LoadCommand { get; }
    public ICommand LoadAsyncCommand { get; }
    public ICommand AddCommand { get; }
    public ICommand DeleteCommand { get; }
    public ICommand ViewCommand { get; }

    // Pour RaiseCanExecuteChanged pratique
    private RelayCommand? DeleteCommandAsCommand => DeleteCommand as RelayCommand;
    private RelayCommand? ViewCommandAsCommand => ViewCommand as RelayCommand;

    public DataGridViewModel()
    {
        // Préparer la vue (tri + filtre)
        _patientsView = CollectionViewSource.GetDefaultView(Patients);
        _patientsView.Filter = FilterPredicate;
        _patientsView.SortDescriptions.Add(new SortDescription(nameof(PatientDto.FirstName), ListSortDirection.Ascending));

        // Commands
        LoadCommand = new RelayCommand(_ => Load());
        ViewCommand = new RelayCommand(_ => View(), _ => SelectedPatient is not null);

        // Données de démonstration au démarrage            
        // _ = SeedDemoDataAsync();
        Console.WriteLine("PatientViewModel: initializing data...");
        _ = InitializeAsync();
        Console.WriteLine("PatientViewModel: initializing data quit");
    }

    private bool FilterPredicate(object obj)
    {
        if (obj is not PatientDto p) return false;
        if (string.IsNullOrWhiteSpace(FilterText)) return true;

        return p.FirstName.Contains(FilterText, StringComparison.CurrentCultureIgnoreCase)
            /*|| p.Id.ToString().Contains(FilterText, StringComparison.CurrentCultureIgnoreCase)*/;
    }

    private async Task SeedDemoDataAsync()
    {            
        for (var i = 0; i < 100; i++)
        {
            var firstname = $"firstname {(i + 1):D4}";
            var lastname = $"lastname {(i + 1):D4}";
            var birthdate = new DateTime(1990, 6, 1 + i % 12);
            Patients.Add(new PatientDto()
            {
                FirstName = firstname,
                LastName = lastname,
                BirthDate = birthdate
            }
            );
        }
    }


    private async Task InitializeAsync()
    {
        // Laisse l'UI respirer
        await Task.Yield();

        try
        {
            await SeedDemoDataAsync();
        }
        catch (Exception ex)
        {
            // TODO: log/afficher l'erreur
            // _logger.LogError(ex, "Erreur pendant SeedDemoDataAsync");
        }
    }


    // Chargement sync (ex. depuis une source)
    private void Load()
    {
        Patients.Clear();
        foreach (var p in Enumerable.Range(1, 5).Select(i =>
                     new PatientDto { FirstName = $"Patient {i}" }))
        {
            Patients.Add(p);
        }
    }


    private void View()
    {
        // Dans MVVM pur, on lèverait un Event ou on utiliserait un service de navigation.
        // Ici, le code-behind (MainWindow) peut écouter une notification via event ou Message.
        // OnRequestInfo?.Invoke(this, $"Dossier de: {SelectedPatient?.FirstName})";// (ID {SelectedPatient?.Id})";
        Console.WriteLine($"View patient: {SelectedPatient?.FirstName}");
    }

    // Événement pour que la vue affiche des infos (MessageBox) sans casser MVVM
    public event EventHandler<string>? OnRequestInfo;

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged(string? prop = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}

Relay Command


  internal class RelayCommand : ICommand
 {
     private readonly Action<object?> _execute;
     private readonly Func<object?, bool>? _canExecute;

     public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
     {
         _execute = execute ?? throw new ArgumentNullException(nameof(execute));
         _canExecute = canExecute;
     }

     public event EventHandler? CanExecuteChanged;
     public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
     public void Execute(object? parameter) => _execute(parameter);

     public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);

 }