Files
mvvm_demo/STUDENT_HANDLEIDING.md
2025-12-15 11:10:12 +01:00

41 KiB

WPF MVVM Tutorial - Stap voor Stap Handleiding

Inhoudsopgave

  1. Introductie
  2. Vereisten
  3. Wat is MVVM?
  4. Stap 1: Project Opzetten
  5. Stap 2: Projectstructuur Aanmaken
  6. Stap 3: Het Model Bouwen
  7. Stap 4: RelayCommand Implementeren
  8. Stap 5: Het ViewModel Bouwen
  9. Stap 6: De View Bouwen
  10. Stap 7: Testen en Uitvoeren
  11. Concepten Uitgelegd
  12. 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:

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

private string _productName;
private decimal _productPrice;
  • Backing fields voor de properties
  • Conventie: underscore prefix voor private fields

Properties met OnPropertyChanged

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

public event PropertyChangedEventHandler PropertyChanged;
  • Event dat wordt afgevuurd wanneer een property verandert
  • De UI kan zich abonneren op dit event

OnPropertyChanged Method

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:

using System;
using System.Windows.Input;

namespace MVVM_DEMO.Commands
{
    /// <summary>
    /// 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'.
    /// </summary>
    public class RelayCommand : ICommand
    {
        private readonly Action<object?> _execute;
        private readonly Func<object?, bool>? _canExecute;

        /// <summary>
        /// Occurs when changes occur that affect whether or not the command should execute.
        /// </summary>
        public event EventHandler? CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        /// <summary>
        /// Creates a new command that can always execute.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        public RelayCommand(Action<object?> execute) : this(execute, null)
        {
        }

        /// <summary>
        /// Creates a new command.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        /// <param name="canExecute">The execution status logic.</param>
        public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        /// <summary>
        /// Determines whether this command can execute in its current state.
        /// </summary>
        /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
        /// <returns>true if this command can be executed; otherwise, false.</returns>
        public bool CanExecute(object? parameter)
        {
            return _canExecute == null || _canExecute(parameter);
        }

        /// <summary>
        /// Executes the command.
        /// </summary>
        /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
        public void Execute(object? parameter)
        {
            _execute(parameter);
        }
    }
}

4.3 Code Uitleg

ICommand Interface

public class RelayCommand : ICommand
  • ICommand: WPF interface voor commando's
  • Vereist: Execute(), CanExecute(), en CanExecuteChanged event

Private Fields

private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
  • _execute: De method die wordt uitgevoerd wanneer het commando wordt aangeroepen
  • _canExecute: Optionele method die bepaalt of het commando kan worden uitgevoerd
  • Action<object?>: Delegate voor een method zonder return waarde
  • Func<object?, bool>: Delegate voor een method die een boolean retourneert

CanExecuteChanged Event

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

public RelayCommand(Action<object?> execute) : this(execute, null)
{
}

public RelayCommand(Action<object?> execute, Func<object?, bool>? 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

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

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:

// SLECHT: Code-behind (niet MVVM)
private void Button_Click(object sender, RoutedEventArgs e)
{
    // Logic hier
}

Met RelayCommand:

// 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:

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

private ObservableCollection<Product> _products;
private Product _selectedProduct;
  • _products: Backing field voor de productenlijst
  • _selectedProduct: Backing field voor het geselecteerde product

INotifyPropertyChanged

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

public MainViewModel()
{
    _products = new ObservableCollection<Product>();
    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

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

public ObservableCollection<Product> 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

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

public ICommand AddProductCommand { get; set; }
  • ICommand: Interface voor commando's in WPF
  • Kan gebonden worden aan buttons in XAML

ExecuteAddProduct Method

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

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:

private bool CanExecuteAddProduct(object? parameter)
{
    return Products.Count < 10; // Max 10 producten
}

5.7 Waarom ObservableCollection?

Vergelijk met een normale List:

// SLECHT: Normale List
List<Product> products = new List<Product>();
products.Add(new Product()); // UI wordt NIET geüpdatet!

// GOED: ObservableCollection
ObservableCollection<Product> products = new ObservableCollection<Product>();
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:

<Window x:Class="MVVM_DEMO.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:MVVM_DEMO"
        xmlns:vm="clr-namespace:MVVM_DEMO.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="800">
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>

    <Grid>
        <StackPanel>
            <ComboBox x:Name="comboBox"
                      Width="200"
                      Height="30"
                      Margin="10"
                      VerticalAlignment="Top"
                      ItemsSource="{Binding Products}"
                      SelectedItem="{Binding SelectedProduct, Mode=TwoWay}"
                      DisplayMemberPath="ProductName" />
            <Button x:Name="btnAddProduct"
                    Content="Add Product"
                    Width="100px"
                    Height="25px"
                    Command="{Binding AddProductCommand}" />
            <Label Content="Selected Product Details"
                   FontWeight="Bold"
                   FontSize="16"
                   Margin="10" />
            <TextBox x:Name="txtProductName"
                     Width="200"
                     Height="30"
                     Text="{Binding SelectedProduct.ProductName, UpdateSourceTrigger=PropertyChanged}"
                     />
            <TextBox x:Name="txtProductPrice"
                     Width="200"
                     Height="30"
                     Text="{Binding SelectedProduct.ProductPrice, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
    </Grid>
</Window>

6.2 XAML Code Uitleg - Deel 1: Namespaces en Window

Namespaces

xmlns:vm="clr-namespace:MVVM_DEMO.ViewModels"
  • xmlns: XML namespace declaratie
  • vm: Prefix voor ons ViewModel namespace
  • clr-namespace: .NET namespace
  • Nu kunnen we vm:MainViewModel gebruiken in XAML

DataContext

<Window.DataContext>
    <vm:MainViewModel />
</Window.DataContext>
  • DataContext: De data bron voor alle bindings in deze Window
  • Maakt een nieuwe instantie van MainViewModel aan
  • Alle child controls erven deze DataContext

Alternatief (in code-behind):

// In MainWindow.xaml.cs
public MainWindow()
{
    InitializeComponent();
    DataContext = new MainViewModel();
}

6.3 XAML Code Uitleg - Deel 2: Layout

Grid en StackPanel

<Grid>
    <StackPanel>
        <!-- Controls hier -->
    </StackPanel>
</Grid>
  • Grid: Basis layout container
  • StackPanel: Stapelt child controls verticaal (standaard)

6.4 XAML Code Uitleg - Deel 3: ComboBox

<ComboBox x:Name="comboBox"
          Width="200"
          Height="30"
          Margin="10"
          VerticalAlignment="Top"
          ItemsSource="{Binding Products}"
          SelectedItem="{Binding SelectedProduct, Mode=TwoWay}"
          DisplayMemberPath="ProductName" />

Properties Uitleg:

  • x:Name: Identificatie naam (voor code-behind toegang)
  • Width/Height: Afmetingen in pixels
  • Margin: Ruimte rondom (hier: 10 pixels aan alle kanten)
  • VerticalAlignment: Verticale uitlijning

Data Binding Properties:

  • ItemsSource: {Binding Products}

    • Bindt aan de Products property in het ViewModel
    • Toont alle producten in de lijst
  • SelectedItem: {Binding SelectedProduct, Mode=TwoWay}

    • Bindt aan de SelectedProduct property
    • Mode=TwoWay: Wijzigingen gaan beide kanten op
      • View → ViewModel: Gebruiker selecteert een product
      • ViewModel → View: Code wijzigt SelectedProduct
  • DisplayMemberPath: "ProductName"

    • Welke property van Product wordt getoond in de ComboBox
    • Toont de naam, niet het hele object

Binding Modes Uitgelegd:

OneWay:     ViewModel → View (default voor meeste properties)
TwoWay:     ViewModel ↔ View (default voor user input controls)
OneTime:    ViewModel → View (alleen bij initialisatie)
OneWayToSource: View → ViewModel

6.5 XAML Code Uitleg - Deel 4: Button

<Button x:Name="btnAddProduct"
        Content="Add Product"
        Width="100px"
        Height="25px"
        Command="{Binding AddProductCommand}" />

Properties Uitleg:

  • Content: De tekst op de button
  • Command: {Binding AddProductCommand}
    • Bindt aan het AddProductCommand in het ViewModel
    • Geen Click event in code-behind nodig!
    • Bij klik wordt Execute van het commando aangeroepen

Verschil met code-behind:

<!-- SLECHT: Code-behind -->
<Button Click="Button_Click" />

<!-- GOED: MVVM met Command -->
<Button Command="{Binding AddProductCommand}" />

6.6 XAML Code Uitleg - Deel 5: TextBoxes

Product Naam TextBox

<TextBox x:Name="txtProductName"
         Width="200"
         Height="30"
         Text="{Binding SelectedProduct.ProductName, UpdateSourceTrigger=PropertyChanged}" />

Binding Uitleg:

  • Text: {Binding SelectedProduct.ProductName}

    • Nested binding: Bindt aan een property van een property
    • SelectedProduct is een property van MainViewModel
    • ProductName is een property van het Product object
  • UpdateSourceTrigger=PropertyChanged

    • PropertyChanged: Update bij elke toetsaanslag
    • Default zou zijn: Update bij focus verlies
    • Geeft real-time updates

UpdateSourceTrigger Opties:

PropertyChanged: Bij elke wijziging (real-time)
LostFocus:      Bij verlies van focus (default)
Explicit:       Alleen bij expliciete aanroep

Product Prijs TextBox

<TextBox x:Name="txtProductPrice"
         Width="200"
         Height="30"
         Text="{Binding SelectedProduct.ProductPrice, UpdateSourceTrigger=PropertyChanged}" />
  • Zelfde concept als ProductName
  • Bindt aan ProductPrice property
  • Automatische conversie van decimal naar string en vice versa

6.7 MainWindow.xaml.cs Aanpassen

Open Views/MainWindow.xaml.cs en vervang de inhoud met:

using MVVM_DEMO.Models;
using MVVM_DEMO.ViewModels;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace MVVM_DEMO
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            // ViewModel is already set in XAML via Window.DataContext
            comboBox.SelectionChanged += ComboBox_SelectionChanged;
            // initial selection
            if (comboBox.Items.Count > 0)
            {
                comboBox.SelectedIndex = 0;
            }
        }

        private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // display selected product details
            if (comboBox.SelectedItem != null && DataContext is MainViewModel viewModel)
            {
                /*viewModel.productName = ((Product)comboBox.SelectedItem).ProductName;
                viewModel.productPrice = (int)((Product)comboBox.SelectedItem).ProductPrice;
                viewModel.OnPropertyChanged(nameof(viewModel.productName));
                viewModel.OnPropertyChanged(nameof(viewModel.productPrice));*/

            }
        }

        // btnAddProduct_Click removed - now using Command binding in XAML
    }
}

Code-behind Uitleg:

  • Minimale code-behind: Dit is het doel van MVVM!
  • SelectionChanged event: Zorgt voor initiële selectie
  • Commented code: Oude manier (niet meer nodig dankzij binding)
  • Opmerking over Command: Button gebruikt nu Command binding

Belangrijk: In pure MVVM zou zelfs de SelectionChanged event handler vermeden worden, maar voor beginners is dit een acceptabele compromise.


Stap 7: Testen en Uitvoeren

7.1 Build het Project

  1. Druk op Ctrl + Shift + B of
  2. Menu: Build > Build Solution
  3. Controleer de Output window voor errors

7.2 Project Uitvoeren

  1. Druk op F5 of
  2. Klik op de groene Start button
  3. De applicatie zou moeten opstarten

7.3 Functionaliteit Testen

Test 1: Producten Bekijken

  1. Open de ComboBox dropdown
  2. Je zou 3 producten moeten zien:
    • Product 1
    • Product 2
    • Product 3

Test 2: Product Selecteren

  1. Selecteer een product uit de ComboBox
  2. De TextBoxes zouden automatisch moeten updaten met:
    • Product naam
    • Product prijs

Test 3: Product Toevoegen

  1. Klik op de "Add Product" button
  2. Een nieuw product wordt toegevoegd aan de lijst
  3. Het nieuwe product verschijnt in de ComboBox
  4. De naam is "Product 4" (of hoger)
  5. De prijs is willekeurig tussen 10 en 100

Test 4: Live Editing

  1. Selecteer een product
  2. Typ in de Product Name TextBox
  3. Wijzig de naam (bijv. "Laptop")
  4. Open de ComboBox weer
  5. De naam zou onmiddellijk geüpdatet moeten zijn!

Test 5: Prijs Editing

  1. Selecteer een product
  2. Wijzig de prijs in de Product Price TextBox
  3. Selecteer een ander product en terug
  4. De prijs zou behouden moeten blijven

7.4 Troubleshooting

Probleem: Applicatie start niet

  • Oplossing: Controleer of er build errors zijn
  • Check: Error List window (View > Error List)

Probleem: ComboBox is leeg

  • Oplossing:
    • Controleer of LoadData() wordt aangeroepen in constructor
    • Controleer ItemsSource binding in XAML
    • Zet een breakpoint in LoadData() method

Probleem: TextBoxes updaten niet

  • Oplossing:
    • Controleer SelectedProduct binding
    • Controleer of OnPropertyChanged() wordt aangeroepen
    • Controleer UpdateSourceTrigger=PropertyChanged

Probleem: Button doet niets

  • Oplossing:
    • Controleer Command binding
    • Zet breakpoint in ExecuteAddProduct method
    • Controleer of AddProductCommand wordt geïnitialiseerd

Concepten Uitgelegd

1. Data Binding

Wat is het? Data Binding is het automatisch synchroniseren van data tussen View en ViewModel.

Syntax:

<TextBox Text="{Binding PropertyName}" />

Hoe werkt het?

  1. WPF maakt een binding object
  2. Binding abonneert zich op PropertyChanged event
  3. Bij wijziging update WPF de UI automatisch
  4. Bij user input update WPF het ViewModel (TwoWay)

Binding Path:

<!-- Simple binding -->
<TextBox Text="{Binding ProductName}" />

<!-- Nested binding -->
<TextBox Text="{Binding SelectedProduct.ProductName}" />

<!-- Collection binding -->
<ComboBox ItemsSource="{Binding Products}" />

2. INotifyPropertyChanged

Waarom nodig? WPF moet weten wanneer data verandert om de UI te updaten.

Implementatie Pattern:

// 1. Implementeer interface
public class MyClass : INotifyPropertyChanged
{
    // 2. Declareer event
    public event PropertyChangedEventHandler PropertyChanged;

    // 3. Helper method
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // 4. Gebruik in properties
    private string _myProperty;
    public string MyProperty
    {
        get => _myProperty;
        set
        {
            _myProperty = value;
            OnPropertyChanged(); // Automatisch "MyProperty" als naam
        }
    }
}

CallerMemberName Attribute:

// Zonder CallerMemberName
OnPropertyChanged("MyProperty"); // Foutgevoelig!

// Met CallerMemberName
OnPropertyChanged(); // Automatisch de juiste naam

3. ObservableCollection

Verschil met List:

// List<T>
List<Product> products = new List<Product>();
products.Add(new Product()); // UI update: ❌

// ObservableCollection<T>
ObservableCollection<Product> products = new ObservableCollection<Product>();
products.Add(new Product()); // UI update: ✅

Events:

  • CollectionChanged: Fired bij Add, Remove, Clear, Replace
  • Automatisch gedetecteerd door WPF bindings

Wanneer gebruiken?:

  • Voor collections die gebonden zijn aan de UI
  • Niet voor interne data (gebruik List)
  • Niet voor read-only data (gebruik IEnumerable)

4. ICommand Pattern

Waarom Commands?

  • Scheiding van UI en logica
  • Testbaar zonder UI
  • Automatische enable/disable logica
  • Herbruikbaar

ICommand Interface:

public interface ICommand
{
    event EventHandler CanExecuteChanged;
    bool CanExecute(object parameter);
    void Execute(object parameter);
}

RelayCommand Gebruik:

// In ViewModel
public ICommand MyCommand { get; set; }

// Constructor
MyCommand = new RelayCommand(ExecuteMethod, CanExecuteMethod);

// Execute method
private void ExecuteMethod(object? parameter)
{
    // Doe iets
}

// CanExecute method
private bool CanExecuteMethod(object? parameter)
{
    return true; // of false om button te disablen
}

XAML Binding:

<Button Command="{Binding MyCommand}" />

5. DataContext

Wat is het? De data bron voor alle bindings in een control en zijn children.

Inheritance:

<Window> <!-- DataContext = MainViewModel -->
    <StackPanel> <!-- Inherits: MainViewModel -->
        <TextBox Text="{Binding ProductName}" /> <!-- Binds to MainViewModel.ProductName -->
    </StackPanel>
</Window>

Manieren om te zetten:

<!-- In XAML -->
<Window.DataContext>
    <vm:MainViewModel />
</Window.DataContext>

<!-- In code-behind -->
DataContext = new MainViewModel();

6. Binding Modes

OneWay (default voor de meeste properties):

ViewModel → View
<TextBlock Text="{Binding ProductName, Mode=OneWay}" />
  • View update bij ViewModel wijziging
  • ViewModel update NIET bij View wijziging

TwoWay (default voor input controls):

ViewModel ↔ View
<TextBox Text="{Binding ProductName, Mode=TwoWay}" />
  • Wijzigingen gaan beide kanten op
  • Gebruikelijk voor user input

OneTime:

ViewModel → View (only once)
  • Alleen bij initialisatie
  • Geen updates daarna
  • Performance voordeel

OneWayToSource:

View → ViewModel
  • Omgekeerde van OneWay
  • Zeldzaam gebruikt

7. UpdateSourceTrigger

Bepaalt wanneer de binding wordt geüpdatet.

PropertyChanged:

<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
  • Bij elke toetsaanslag
  • Real-time updates
  • Hogere load

LostFocus (default):

<TextBox Text="{Binding Name, UpdateSourceTrigger=LostFocus}" />
  • Bij verlies van focus
  • Minder updates
  • Betere performance

Explicit:

<TextBox Text="{Binding Name, UpdateSourceTrigger=Explicit}" />
  • Alleen bij expliciete aanroep
BindingExpression binding = txtBox.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();

Veelgemaakte Fouten

Fout 1: Vergeten INotifyPropertyChanged te implementeren

Symptoom: UI update niet bij data wijziging

Fout Code:

public class Product
{
    public string Name { get; set; } // ❌
}

Correcte Code:

public class Product : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged(); // ✅
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Fout 2: List gebruiken in plaats van ObservableCollection

Symptoom: UI update niet bij Add/Remove

Fout Code:

public List<Product> Products { get; set; } // ❌

Correcte Code:

public ObservableCollection<Product> Products { get; set; } // ✅

Fout 3: Binding Mode vergeten bij TwoWay binding

Symptoom: Wijzigingen in View komen niet in ViewModel

Fout Code:

<ComboBox SelectedItem="{Binding SelectedProduct}" /> <!-- ❌ -->

Correcte Code:

<ComboBox SelectedItem="{Binding SelectedProduct, Mode=TwoWay}" /> <!-- ✅ -->

Note: Voor de meeste input controls (TextBox, ComboBox, CheckBox) is TwoWay de default. Maar het is beter om expliciet te zijn!

Fout 4: OnPropertyChanged vergeten aan te roepen

Symptoom: UI update niet

Fout Code:

public string Name
{
    get => _name;
    set
    {
        _name = value; // ❌ Geen OnPropertyChanged
    }
}

Correcte Code:

public string Name
{
    get => _name;
    set
    {
        _name = value;
        OnPropertyChanged(); // ✅
    }
}

Fout 5: DataContext niet zetten

Symptoom: Bindings werken niet, data is null

Fout Code:

<Window>
    <!-- ❌ Geen DataContext! -->
    <TextBox Text="{Binding ProductName}" />
</Window>

Correcte Code:

<Window>
    <Window.DataContext>
        <vm:MainViewModel /> <!-- ✅ -->
    </Window.DataContext>
    <TextBox Text="{Binding ProductName}" />
</Window>

Fout 6: Command niet initialiseren

Symptoom: Button doet niets bij klik

Fout Code:

public ICommand MyCommand { get; set; } // ❌ null!

public MainViewModel()
{
    // Vergeten te initialiseren
}

Correcte Code:

public ICommand MyCommand { get; set; }

public MainViewModel()
{
    MyCommand = new RelayCommand(ExecuteMethod); // ✅
}

Fout 7: Property naam typo in CallerMemberName

Symptoom: Verkeerde property wordt geüpdatet

Fout Code:

public string Name
{
    get => _name;
    set
    {
        _name = value;
        OnPropertyChanged("Naam"); // ❌ Verkeerde naam!
    }
}

Correcte Code:

public string Name
{
    get => _name;
    set
    {
        _name = value;
        OnPropertyChanged(); // ✅ Automatisch juiste naam
    }
}

Fout 8: Namespace vergeten in XAML

Symptoom: ViewModel niet gevonden

Fout Code:

<Window>
    <Window.DataContext>
        <MainViewModel /> <!-- ❌ Namespace ontbreekt! -->
    </Window.DataContext>
</Window>

Correcte Code:

<Window xmlns:vm="clr-namespace:MVVM_DEMO.ViewModels"> <!-- ✅ Namespace declaratie -->
    <Window.DataContext>
        <vm:MainViewModel /> <!-- ✅ Met prefix -->
    </Window.DataContext>
</Window>

Fout 9: UpdateSourceTrigger vergeten voor real-time updates

Symptoom: TextBox update pas bij focus verlies

Fout Code:

<TextBox Text="{Binding ProductName}" /> <!-- ❌ -->

Correcte Code (voor real-time):

<TextBox Text="{Binding ProductName, UpdateSourceTrigger=PropertyChanged}" /> <!-- ✅ -->

Fout 10: Null reference bij nested binding

Symptoom: Crash bij opstarten of null reference exception

Fout Code:

<TextBox Text="{Binding SelectedProduct.ProductName}" />
public Product SelectedProduct { get; set; } // null bij opstarten! ❌

Oplossing 1: Initialiseer met default waarde

public Product SelectedProduct { get; set; } = new Product(); // ✅

Oplossing 2: Gebruik FallbackValue in binding

<TextBox Text="{Binding SelectedProduct.ProductName, FallbackValue=''}" /> <!-- ✅ -->

Oplossing 3: Initiële selectie in code

if (comboBox.Items.Count > 0)
{
    comboBox.SelectedIndex = 0; // ✅
}

Uitbreidingsopdrachten

Nu je de basis begrijpt, probeer deze uitbreidingen:

Opdracht 1: Delete Product Command

Voeg een "Delete Product" button toe:

  1. Maak een DeleteProductCommand
  2. Implementeer ExecuteDeleteProduct method
  3. Implementeer CanExecuteDeleteProduct (alleen als er een product geselecteerd is)
  4. Voeg button toe aan XAML

Hint:

private bool CanExecuteDeleteProduct(object? parameter)
{
    return SelectedProduct != null;
}

Opdracht 2: Edit Product Command

Maak een dialoog om producten te bewerken:

  1. Maak een nieuw Window EditProductWindow.xaml
  2. Maak een EditProductViewModel
  3. Gebruik ShowDialog() om het window te tonen
  4. Sla wijzigingen op

Opdracht 3: Value Converter

Maak een value converter voor prijs formatting:

  1. Maak een class in ValueConverters folder
  2. Implementeer IValueConverter
  3. Converteer decimal naar string met currency symbool
  4. Gebruik in binding: {Binding Price, Converter={StaticResource PriceConverter}}

Voorbeeld:

public class PriceConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is decimal price)
        {
            return $"€ {price:F2}";
        }
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Opdracht 4: Input Validatie

Voeg validatie toe voor product naam en prijs:

  1. Implementeer IDataErrorInfo in Product class
  2. Valideer in property setters
  3. Toon error messages in UI
  4. Disable "Add Product" bij ongeldige input

Opdracht 5: Persistentie

Sla producten op in een file:

  1. Maak een ProductRepository class
  2. Implementeer SaveToFile() en LoadFromFile()
  3. Gebruik JSON serialization
  4. Laad data bij opstarten
  5. Sla op bij wijzigingen

Samenvatting

Je hebt nu een volledige WPF MVVM applicatie gebouwd! Je hebt geleerd:

Patronen en Concepten:

  • MVVM architectuur pattern
  • INotifyPropertyChanged voor property change notification
  • ICommand pattern voor button logic
  • Data Binding (OneWay, TwoWay)
  • ObservableCollection voor automatische UI updates

Implementatie Details:

  • Model class met property notification
  • ViewModel met commands en observable collections
  • View met XAML data binding
  • RelayCommand voor commando implementatie
  • Separation of concerns

Best Practices:

  • Minimale code-behind
  • Testbare code
  • Herbruikbare componenten
  • Clean architecture

Volgende stappen:

  1. Experimenteer met de uitbreidingsopdrachten
  2. Lees de officiële Microsoft WPF documentatie
  3. Bouw je eigen MVVM applicatie
  4. Onderzoek MVVM frameworks zoals Prism of MVVM Light

Succes met je verdere MVVM ontwikkeling!