ARTICLE AD BOX
I'm building a recipe manager application in WPF using the MVVM pattern for a school project. The app lets you add new recipes via textboxes and displays them in a listbox.
Each recipe has three fields: Name, Kategorie and Kalorien. The project is split into three separate layers following standard MVVM — Model, ViewModel and View as separate projects in the same solution.
The basic functionality works fine. Recipes load on startup, the listbox displays them correctly, and clicking the add button creates a new recipe and appends it to the list. The input textboxes also clear themselves after the button is clicked, which tells me the PropertyChanged notifications are working correctly for the input properties in VMMainWindow.
The problem starts when I select an item from the listbox and try to display its details in textblocks below. The textblocks are bound to SelectedRezept.Name, SelectedRezept.Kategorie and SelectedRezept.Kalorien.
On first selection, they show the correct values, which means the binding path itself is correct and SelectedRezept is being set properly.
However when a method runs that modifies one of those properties of the selected VMRezept object, the textblock doesn't get updated. I can confirm in the debugger that the underlying value changes — the UI just doesn't pick it up.
The display only refreshes if I click away to a different item and then reselect the original one, which suggests WPF is not being notified that the property changed.
I noticed that my setters in VMRezept don't call PropertyChanged at all — they just pass the value straight through to the model. I have an OnPropertyChanged helper method defined in VMRezept but I never actually call it anywhere. I'm not sure if this is the root cause or if the real issue is that PropertyChanged on a child view model object doesn't automatically bubble up to the parent VMMainWindow where the binding lives.
I tried firing PropertyChanged for SelectedRezept again from VMMainWindow after the method call and that does make the textblocks update, but it feels wrong to notify that the entire selected object changed when really only one property inside it changed.
My questions are
Should every setter in VMRezept be calling PropertyChanged, even though the values come from the model layer?
Is manually re-firing PropertyChanged for SelectedRezept from the parent the standard approach, or is there a cleaner pattern for this?
Does PropertyChanged on a child view model ever automatically propagate to bindings in the parent, or does WPF only listen to the object it is directly bound to?
My code:
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ViewModel { public class VMMainWindow : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; ObservableCollection<VMRezept> rezepteListe; VMRezept aktRezept; private UserCommand buttonCommand; private string neuerName; public string NeuerName { get => neuerName; set { neuerName = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NeuerName))); } } private string neueKategorie; public string NeueKategorie { get => neueKategorie; set { neueKategorie = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NeueKategorie))); } } private int neueKalorien; public int NeueKalorien { get => neueKalorien; set { neueKalorien = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NeueKalorien))); } } public ObservableCollection<VMRezept> RezepteListe { get => rezepteListe; set => rezepteListe = value; } public VMRezept AktRezept { get => aktRezept; set => aktRezept = value; } public UserCommand ButtonCommand { get => buttonCommand; set => buttonCommand = value; } public VMMainWindow() { rezepteListe = new ObservableCollection<VMRezept>(); Fuellen(); this.ButtonCommand = new UserCommand(new Action<object>(VMMWNeuesRezept)); } private void VMMWNeuesRezept(object obj) { VMRezept neuesRezept = new VMRezept(NeuerName, NeueKategorie, NeueKalorien); rezepteListe.Add(neuesRezept); NeuerName = ""; NeueKategorie = ""; NeueKalorien = 0; } public void Fuellen() { RezepteListe.Add(new VMRezept("Pasta Aglio e Olio", "vegetarisch", 400)); RezepteListe.Add(new VMRezept("Pfannkuchen", "Dessert", 450)); RezepteListe.Add(new VMRezept("Gulasch", "Hauptgericht", 940)); } } } <Window x:Class="Kochbuch.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:Kochbuch" xmlns:vm="clr-namespace:ViewModel;assembly=ViewModel" mc:Ignorable="d" Title="MainWindow" Height="500" Width="800"> <Window.DataContext> <vm:VMMainWindow></vm:VMMainWindow> </Window.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition Height="10"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition MinHeight="60"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition Width="2*"></ColumnDefinition> </Grid.ColumnDefinitions> <Label Content="Kochbuch" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" HorizontalAlignment="Center" FontSize="40" FontWeight="Bold"/> <Label Content="gespeicherte Rezepte" Grid.Row="1" Grid.Column="0" HorizontalAlignment="Right" FontSize="25" Background="Bisque"></Label> <StackPanel Grid.Row="1" Grid.Column="1" Orientation="Vertical"> <Label Content="Rezept" Grid.Column="0" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" FontWeight="Bold"/> <ComboBox x:Name="comboRezepte" ItemsSource="{Binding RezepteListe}" Grid.Column="1" Grid.Row="1" Height="Auto" MinHeight="50" FontSize="20"/> </StackPanel> <StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal"> <Label Content="Name" Grid.Column="0" Grid.Row="2" VerticalAlignment="Center" FontSize="20" Width="160"/> <TextBox Grid.Column="1" Grid.Row="2" Text="{Binding ElementName=comboRezepte, Path=SelectedItem.Name}" FontSize="20"></TextBox> </StackPanel> <StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal"> <Label Content="Kategorie" Grid.Column="0" Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Width="160"/> <TextBox Grid.Column="1" Text="{Binding ElementName=comboRezepte, Path=SelectedItem.Kategorie}" Grid.Row="3" FontSize="20"></TextBox> </StackPanel> <StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal"> <Label Content="Kalorien/Portion" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Grid.Column="0" Grid.Row="4" Width="160"/> <TextBox Grid.Column="1" Text="{Binding ElementName=comboRezepte, Path=SelectedItem.Kalorien}" Grid.Row="4" FontSize="20" ></TextBox> </StackPanel> <Label Grid.Column="0" Grid.Row="5" Grid.ColumnSpan="2" Background="Brown" /> <Label Content="neues Rezept" Grid.Row="6" Grid.Column="0" FontSize="25" Background="Bisque"></Label> <Label Content="Neuer Name" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Grid.Column="0" Grid.Row="7"/> <TextBox Grid.Column="1" Text="{Binding NeuerName}" Grid.Row="7" FontSize="20" ></TextBox> <Label Content="Neue Kategorie" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Grid.Column="0" Grid.Row="8"/> <TextBox Grid.Column="1" Text="{Binding NeueKategorie}" Grid.Row="8" FontSize="20" ></TextBox> <Label Content="Kalorien/Portion" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="20" Grid.Column="0" Grid.Row="9"/> <TextBox Grid.Column="1" Text="{Binding NeueKalorien}" Grid.Row="9" FontSize="20" ></TextBox> <Button Grid.Row="10" Grid.ColumnSpan="2" Command="{Binding ButtonCommand}" Content="Neues Rezept aufnehmen" FontSize="20" ></Button> </Grid> </Window>