# WPF MVVM Tutorial - Stap voor Stap Handleiding ## Inhoudsopgave 1. [Introductie](#introductie) 2. [Vereisten](#vereisten) 3. [Wat is MVVM?](#wat-is-mvvm) 4. [Stap 1: Project Opzetten](#stap-1-project-opzetten) 5. [Stap 2: Projectstructuur Aanmaken](#stap-2-projectstructuur-aanmaken) 6. [Stap 3: Het Model Bouwen](#stap-3-het-model-bouwen) 7. [Stap 4: RelayCommand Implementeren](#stap-4-relaycommand-implementeren) 8. [Stap 5: Het ViewModel Bouwen](#stap-5-het-viewmodel-bouwen) 9. [Stap 6: De View Bouwen](#stap-6-de-view-bouwen) 10. [Stap 7: Testen en Uitvoeren](#stap-7-testen-en-uitvoeren) 11. [Concepten Uitgelegd](#concepten-uitgelegd) 12. [Veelgemaakte Fouten](#veelgemaakte-fouten) --- ## Introductie Deze handleiding leidt je stap voor stap door het bouwen van een WPF-applicatie met het MVVM (Model-View-ViewModel) ontwerppatroon. Je bouwt een eenvoudige productenbeheer applicatie waarbij je leert over: - Data Binding - Observable Collections - Property Change Notifications - ICommand Pattern - Separation of Concerns **Eindresultaat**: Een werkende applicatie waar je producten kunt bekijken, selecteren en toevoegen met automatische UI-updates. --- ## Vereisten ### Software - Visual Studio 2022 (Community, Professional of Enterprise) - .NET 8.0 SDK ### Kennis - Basis kennis van C# - Object-georiënteerd programmeren - Basiskennis van XAML (niet vereist maar handig) --- ## Wat is MVVM? MVVM staat voor **Model-View-ViewModel** en is een ontwerppatroon dat helpt bij het scheiden van: - **Model**: De data en business logica - **View**: De gebruikersinterface (XAML) - **ViewModel**: De tussenpersoon die data voorbereidt voor de View ### Voordelen van MVVM: - **Scheiding van zorgen**: UI-code gescheiden van business logica - **Testbaarheid**: ViewModels kunnen eenvoudig worden getest zonder UI - **Herbruikbaarheid**: Models en ViewModels zijn herbruikbaar - **Data Binding**: Automatische synchronisatie tussen UI en data ### Diagram: ``` View (XAML) <------ Data Binding ------> ViewModel | | Model ``` --- ## Stap 1: Project Opzetten ### 1.1 Nieuw Project Aanmaken 1. Open Visual Studio 2022 2. Klik op **"Create a new project"** 3. Zoek naar **"WPF Application"** (niet WPF App (.NET Framework)!) 4. Selecteer **"WPF Application"** en klik **Next** ### 1.2 Project Configureren 1. **Project name**: `MVVM_DEMO` 2. **Location**: Kies een geschikte locatie 3. **Solution name**: `MVVM_DEMO` 4. Klik **Next** ### 1.3 Framework Selecteren 1. Selecteer **.NET 8.0** als framework 2. Klik **Create** ### 1.4 Wat Krijg Je? Visual Studio creëert automatisch: - `App.xaml` en `App.xaml.cs` - De applicatie startpunt - `MainWindow.xaml` en `MainWindow.xaml.cs` - Het hoofdvenster - `MVVM_DEMO.csproj` - Project configuratie --- ## Stap 2: Projectstructuur Aanmaken Een goede mappenstructuur is essentieel voor overzichtelijke MVVM-applicaties. ### 2.1 Mappen Aanmaken In de **Solution Explorer**: 1. **Rechtsklik** op het project `MVVM_DEMO` 2. Selecteer **Add > New Folder** 3. Maak de volgende mappen aan: - `Models` - `ViewModels` - `Views` - `Commands` - `ValueConverters` (voorlopig leeg, voor toekomstige uitbreidingen) ### 2.2 MainWindow.xaml Verplaatsen 1. **Sleep** `MainWindow.xaml` naar de `Views` map 2. Visual Studio vraagt of je namespace references wilt updaten - klik **Yes** ### 2.3 Projectstructuur Controleren Je projectstructuur zou er nu zo uit moeten zien: ``` MVVM_DEMO/ ├── Commands/ ├── Models/ ├── ViewModels/ ├── Views/ │ ├── MainWindow.xaml │ └── MainWindow.xaml.cs ├── ValueConverters/ ├── App.xaml └── MVVM_DEMO.csproj ``` --- ## Stap 3: Het Model Bouwen Het **Model** bevat de data structuur. We gaan een `Product` class maken. ### 3.1 Product Class Aanmaken 1. **Rechtsklik** op de `Models` map 2. Selecteer **Add > Class...** 3. Naam: `Product.cs` 4. Klik **Add** ### 3.2 Product Class Implementeren Open `Product.cs` en vervang de inhoud met: ```csharp using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; namespace MVVM_DEMO.Models { public class Product { private string _productName; private decimal _productPrice; public string ProductName { get => _productName; set { _productName = value; OnPropertyChanged(); } } public decimal ProductPrice { get => _productPrice; set { _productPrice = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } } ``` ### 3.3 Code Uitleg #### Private Fields ```csharp private string _productName; private decimal _productPrice; ``` - **Backing fields** voor de properties - Conventie: underscore prefix voor private fields #### Properties met OnPropertyChanged ```csharp public string ProductName { get => _productName; set { _productName = value; OnPropertyChanged(); } } ``` - **get**: Retourneert de waarde van het private field - **set**: Zet de nieuwe waarde EN roept `OnPropertyChanged()` aan - Dit zorgt ervoor dat de UI wordt genotificeerd bij wijzigingen #### INotifyPropertyChanged Event ```csharp public event PropertyChangedEventHandler PropertyChanged; ``` - Event dat wordt afgevuurd wanneer een property verandert - De UI kan zich abonneren op dit event #### OnPropertyChanged Method ```csharp protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } ``` - **[CallerMemberName]**: Automatisch de naam van de aanroepende property - **PropertyChanged?.Invoke**: Veilig aanroepen (alleen als er listeners zijn) - **PropertyChangedEventArgs**: Bevat de naam van de gewijzigde property ### 3.4 Waarom INotifyPropertyChanged? Zonder `INotifyPropertyChanged` weet de UI niet wanneer data verandert. Met dit pattern: 1. Property wordt gewijzigd 2. `OnPropertyChanged()` wordt aangeroepen 3. Event wordt afgevuurd 4. UI luistert naar het event 5. UI update zichzelf automatisch --- ## Stap 4: RelayCommand Implementeren **Commands** zijn de MVVM-manier om button clicks en andere UI-acties af te handelen zonder code-behind. ### 4.1 RelayCommand Class Aanmaken 1. **Rechtsklik** op de `Commands` map 2. Selecteer **Add > Class...** 3. Naam: `RelayCommand.cs` 4. Klik **Add** ### 4.2 RelayCommand Implementeren Open `RelayCommand.cs` en vervang de inhoud met: ```csharp using System; using System.Windows.Input; namespace MVVM_DEMO.Commands { /// /// A command whose sole purpose is to relay its functionality to other objects by invoking delegates. /// The default return value for the CanExecute method is 'true'. /// public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func? _canExecute; /// /// Occurs when changes occur that affect whether or not the command should execute. /// public event EventHandler? CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } /// /// Creates a new command that can always execute. /// /// The execution logic. public RelayCommand(Action execute) : this(execute, null) { } /// /// Creates a new command. /// /// The execution logic. /// The execution status logic. public RelayCommand(Action execute, Func? canExecute) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } /// /// Determines whether this command can execute in its current state. /// /// Data used by the command. If the command does not require data to be passed, this object can be set to null. /// true if this command can be executed; otherwise, false. public bool CanExecute(object? parameter) { return _canExecute == null || _canExecute(parameter); } /// /// Executes the command. /// /// Data used by the command. If the command does not require data to be passed, this object can be set to null. public void Execute(object? parameter) { _execute(parameter); } } } ``` ### 4.3 Code Uitleg #### ICommand Interface ```csharp public class RelayCommand : ICommand ``` - **ICommand**: WPF interface voor commando's - Vereist: `Execute()`, `CanExecute()`, en `CanExecuteChanged` event #### Private Fields ```csharp private readonly Action _execute; private readonly Func? _canExecute; ``` - **_execute**: De method die wordt uitgevoerd wanneer het commando wordt aangeroepen - **_canExecute**: Optionele method die bepaalt of het commando kan worden uitgevoerd - **Action**: Delegate voor een method zonder return waarde - **Func**: Delegate voor een method die een boolean retourneert #### CanExecuteChanged Event ```csharp public event EventHandler? CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } ``` - Koppelt aan WPF's `CommandManager` - WPF controleert automatisch of commando's kunnen worden uitgevoerd - Buttons worden automatisch disabled als `CanExecute` false retourneert #### Constructors ```csharp public RelayCommand(Action execute) : this(execute, null) { } public RelayCommand(Action execute, Func? canExecute) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } ``` - **Eerste constructor**: Voor commando's die altijd kunnen worden uitgevoerd - **Tweede constructor**: Voor commando's met validatie logica - **Null check**: Gooit exception als `execute` null is #### Execute Method ```csharp public void Execute(object? parameter) { _execute(parameter); } ``` - Roept de `_execute` delegate aan - Wordt aangeroepen wanneer de gebruiker de actie triggert (bijv. button click) #### CanExecute Method ```csharp public bool CanExecute(object? parameter) { return _canExecute == null || _canExecute(parameter); } ``` - Als `_canExecute` null is: altijd true (commando kan altijd) - Anders: roep `_canExecute` aan en retourneer het resultaat ### 4.4 Waarom RelayCommand? Zonder commando's zou je code-behind nodig hebben: ```csharp // SLECHT: Code-behind (niet MVVM) private void Button_Click(object sender, RoutedEventArgs e) { // Logic hier } ``` Met RelayCommand: ```csharp // GOED: MVVM met Commands public ICommand AddProductCommand { get; set; } AddProductCommand = new RelayCommand(ExecuteAddProduct, CanExecuteAddProduct); ``` **Voordelen**: - Testbaar (zonder UI) - Herbruikbaar - Separation of concerns - Automatische enable/disable logica --- ## Stap 5: Het ViewModel Bouwen Het **ViewModel** is het hart van MVVM - het verbindt de data (Model) met de UI (View). ### 5.1 MainViewModel Class Aanmaken 1. **Rechtsklik** op de `ViewModels` map 2. Selecteer **Add > Class...** 3. Naam: `MainViewModel.cs` 4. Klik **Add** ### 5.2 MainViewModel Implementeren Open `MainViewModel.cs` en vervang de inhoud met: ```csharp using MVVM_DEMO.Commands; using MVVM_DEMO.Models; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using System.Windows.Input; namespace MVVM_DEMO.ViewModels { public class MainViewModel : INotifyPropertyChanged { private ObservableCollection _products; private Product _selectedProduct; public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // constructor public MainViewModel() { _products = new ObservableCollection(); LoadData(); // Initialize commands AddProductCommand = new RelayCommand(ExecuteAddProduct, CanExecuteAddProduct); } // read data private void LoadData() { _products.Add(new Product { ProductName = "Product 1",ProductPrice = 10 }); _products.Add(new Product { ProductName = "Product 2", ProductPrice = 20 }); _products.Add(new Product { ProductName = "Product 3", ProductPrice = 30 }); Products = _products; } // properties public ObservableCollection Products { get => _products; set { _products = value; OnPropertyChanged(); } } public Product SelectedProduct { get => _selectedProduct; set { _selectedProduct = value; OnPropertyChanged(); } } // Commands public ICommand AddProductCommand { get; set; } // Command methods private void ExecuteAddProduct(object? parameter) { Random random = new Random(); int randomPrice = random.Next(10, 100); Products.Add(new Product { ProductName = $"Product {Products.Count + 1}", ProductPrice = randomPrice }); } private bool CanExecuteAddProduct(object? parameter) { // You can add validation logic here // For now, always allow adding products return true; } } } ``` ### 5.3 Code Uitleg - Deel 1: Fields en INotifyPropertyChanged #### Private Fields ```csharp private ObservableCollection _products; private Product _selectedProduct; ``` - **_products**: Backing field voor de productenlijst - **_selectedProduct**: Backing field voor het geselecteerde product #### INotifyPropertyChanged ```csharp public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } ``` - Zelfde pattern als in het Model - Notificeert de UI over property wijzigingen ### 5.4 Code Uitleg - Deel 2: Constructor en Data Loading #### Constructor ```csharp public MainViewModel() { _products = new ObservableCollection(); LoadData(); // Initialize commands AddProductCommand = new RelayCommand(ExecuteAddProduct, CanExecuteAddProduct); } ``` - Initialiseert de `ObservableCollection` - Laadt initiële data via `LoadData()` - Maakt het `AddProductCommand` aan met execute en canExecute methods #### LoadData Method ```csharp private void LoadData() { _products.Add(new Product { ProductName = "Product 1", ProductPrice = 10 }); _products.Add(new Product { ProductName = "Product 2", ProductPrice = 20 }); _products.Add(new Product { ProductName = "Product 3", ProductPrice = 30 }); Products = _products; } ``` - Voegt 3 testproducten toe - In een echte applicatie zou dit data uit een database kunnen halen - **Object initializer syntax**: `new Product { PropertyName = value }` ### 5.5 Code Uitleg - Deel 3: Properties #### Products Property ```csharp public ObservableCollection Products { get => _products; set { _products = value; OnPropertyChanged(); } } ``` - **ObservableCollection**: Speciale collectie die automatisch de UI notificeert bij Add/Remove - Publieke property voor data binding vanuit XAML #### SelectedProduct Property ```csharp public Product SelectedProduct { get => _selectedProduct; set { _selectedProduct = value; OnPropertyChanged(); } } ``` - Houdt bij welk product momenteel geselecteerd is - Bij wijziging wordt de UI automatisch geüpdatet ### 5.6 Code Uitleg - Deel 4: Command Implementation #### Command Property ```csharp public ICommand AddProductCommand { get; set; } ``` - **ICommand**: Interface voor commando's in WPF - Kan gebonden worden aan buttons in XAML #### ExecuteAddProduct Method ```csharp private void ExecuteAddProduct(object? parameter) { Random random = new Random(); int randomPrice = random.Next(10, 100); Products.Add(new Product { ProductName = $"Product {Products.Count + 1}", ProductPrice = randomPrice }); } ``` - **Wordt aangeroepen** wanneer de button wordt geklikt - **Random prijs** tussen 10 en 100 - **String interpolation**: `$"Product {Products.Count + 1}"` - **ObservableCollection.Add**: Automatische UI update #### CanExecuteAddProduct Method ```csharp private bool CanExecuteAddProduct(object? parameter) { return true; } ``` - Bepaalt of het commando kan worden uitgevoerd - `true`: button is enabled - `false`: button is disabled - Hier altijd `true`, maar je zou validatie kunnen toevoegen **Voorbeeld met validatie**: ```csharp private bool CanExecuteAddProduct(object? parameter) { return Products.Count < 10; // Max 10 producten } ``` ### 5.7 Waarom ObservableCollection? Vergelijk met een normale List: ```csharp // SLECHT: Normale List List products = new List(); products.Add(new Product()); // UI wordt NIET geüpdatet! // GOED: ObservableCollection ObservableCollection products = new ObservableCollection(); products.Add(new Product()); // UI wordt WEL geüpdatet! ``` **ObservableCollection** implementeert `INotifyCollectionChanged` en update de UI automatisch bij: - Add - Remove - Clear - Replace --- ## Stap 6: De View Bouwen De **View** is de gebruikersinterface in XAML. We gaan `MainWindow.xaml` aanpassen. ### 6.1 MainWindow.xaml Aanpassen Open `Views/MainWindow.xaml` en vervang de inhoud met: ```xml