From e8c5322782937e4cd73ce456824a8b5eefbb9c0c Mon Sep 17 00:00:00 2001 From: Mark Kors Date: Mon, 15 Dec 2025 11:10:12 +0100 Subject: [PATCH] Student handleiding added --- Models/Product.cs | 33 +- STUDENT_HANDLEIDING.md | 1617 +++++++++++++++++++++++++++++++++++ ViewModels/MainViewModel.cs | 41 +- Views/MainWindow.xaml | 5 +- Views/MainWindow.xaml.cs | 9 +- 5 files changed, 1685 insertions(+), 20 deletions(-) create mode 100644 STUDENT_HANDLEIDING.md diff --git a/Models/Product.cs b/Models/Product.cs index 56bb692..1c91136 100644 --- a/Models/Product.cs +++ b/Models/Product.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; @@ -8,8 +10,35 @@ namespace MVVM_DEMO.Models { public class Product { - public string ProductName { get; set; } - public double Price { get; set; } + 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)); + } } } diff --git a/STUDENT_HANDLEIDING.md b/STUDENT_HANDLEIDING.md new file mode 100644 index 0000000..c341d43 --- /dev/null +++ b/STUDENT_HANDLEIDING.md @@ -0,0 +1,1617 @@ +# 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 + + + + + + + + +