Pourquoi MVVM ?
MVVM (Model-View-ViewModel) sépare votre application en trois couches :
- Model : Données et logique métier
- View : Interface utilisateur (XAML)
- ViewModel : Pont entre Model et View, logique de présentation
Avantages : testabilité, maintenabilité, séparation claire des responsabilités.
Structure de projet recommandée
MonApp/
├── Models/
│ └── Product.cs
├── ViewModels/
│ ├── BaseViewModel.cs
│ ├── MainViewModel.cs
│ └── ProductViewModel.cs
├── Views/
│ ├── MainWindow.xaml
│ └── ProductView.xaml
├── Services/
│ └── IProductService.cs
├── Commands/
│ └── RelayCommand.cs
└── App.xaml
BaseViewModel
La classe de base pour tous vos ViewModels :
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
RelayCommand
Pour gérer les actions utilisateur :
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Predicate<object?>? _canExecute;
public RelayCommand(Action<object?> execute, Predicate<object?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
Exemple complet : ProductViewModel
public class ProductViewModel : BaseViewModel
{
private readonly IProductService _productService;
private string _name = string.Empty;
private decimal _price;
private bool _isLoading;
public ProductViewModel(IProductService productService)
{
_productService = productService;
SaveCommand = new RelayCommand(ExecuteSave, CanSave);
LoadCommand = new RelayCommand(async _ => await LoadAsync());
}
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public decimal Price
{
get => _price;
set => SetProperty(ref _price, value);
}
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public ICommand SaveCommand { get; }
public ICommand LoadCommand { get; }
private bool CanSave(object? _) => !string.IsNullOrEmpty(Name) && Price > 0;
private async void ExecuteSave(object? _)
{
IsLoading = true;
try
{
await _productService.SaveAsync(new Product { Name = Name, Price = Price });
}
finally
{
IsLoading = false;
}
}
private async Task LoadAsync()
{
IsLoading = true;
try
{
var product = await _productService.GetAsync();
Name = product.Name;
Price = product.Price;
}
finally
{
IsLoading = false;
}
}
}
La View (XAML)
<Window x:Class="MonApp.Views.ProductView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0"
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,10"/>
<TextBox Grid.Row="1"
Text="{Binding Price, StringFormat=N2}"
Margin="0,0,0,10"/>
<Button Grid.Row="2"
Content="Sauvegarder"
Command="{Binding SaveCommand}"
IsEnabled="{Binding IsLoading, Converter={StaticResource InverseBoolConverter}}"/>
</Grid>
</Window>
Dependency Injection
Avec Microsoft.Extensions.DependencyInjection :
public partial class App : Application
{
private readonly IServiceProvider _serviceProvider;
public App()
{
var services = new ServiceCollection();
ConfigureServices(services);
_serviceProvider = services.BuildServiceProvider();
}
private void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IProductService, ProductService>();
services.AddTransient<ProductViewModel>();
services.AddTransient<MainWindow>();
}
protected override void OnStartup(StartupEventArgs e)
{
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
}
Conseil : Utilisez CommunityToolkit.Mvvm pour éviter le boilerplate. Il génère automatiquement les notifications et commandes avec des attributs.
Conclusion
MVVM demande un investissement initial mais paie sur le long terme. Applications testables, maintenables, évolutives. C'est le standard pour le développement WPF professionnel.
Besoin d'aide sur votre projet WPF ? Contactez-moi.