MOO WebTech
.NET MAUItheory-practiceintermediate

L15 — .NET MAUI pt.3: Data Binding & MVVM

Stop writing code-behind for everything. We introduce data binding, INotifyPropertyChanged, ViewModels, Commands, and ObservableCollection — the MVVM pattern that scales.

80 min15.05.2026L15

🎯Learning Objectives

  • Explain what MVVM is and why XAML apps are built around it
  • Bind a Label/Entry to a property via {Binding PropertyName}
  • Implement INotifyPropertyChanged so the UI updates when properties change
  • Trigger logic from a button using ICommand instead of a Clicked event
  • Display a list of items using CollectionView and ObservableCollection<T>

📖Theory

1. The Problem With Code-Behind

In L14 every button click was handled in the page's .xaml.cs:

C#
private void OnSaveClicked(object sender, EventArgs e)
{
    string name = NameEntry.Text;
    // ... validate, save, update labels ...
}

That works for three controls. With thirty, the code-behind becomes a tangle of UI references. You can't unit-test it — tests can't touch NameEntry without launching the UI. And every screen change forces a C# edit.

2. MVVM — Separating UI From Logic

MVVM stands for Model–View–ViewModel. The idea:

  • Model — plain C# classes that represent your data (e.g. Student, Product). No UI knowledge.
  • View — the XAML file. Purely describes what's on screen.
  • ViewModel — a C# class that holds the state the View needs and the commands the View can trigger. The View binds to the ViewModel.
Code
User types in Entry    → bound to ViewModel.Name       (View updates ViewModel)
User taps Save button  → triggers ViewModel.SaveCommand (View asks ViewModel to act)
ViewModel changes data → ViewModel notifies → View     (ViewModel updates View)

The View and the ViewModel don't reference each other by name. They're glued together by data binding.

3. Your First Binding

Here's a minimal ViewModel:

C#
public class MainViewModel
{
    public string Greeting { get; set; } = "Hello from the ViewModel!";
}

In MainPage.xaml.cs, attach the ViewModel as the page's BindingContext:

C#
public MainPage()
{
    InitializeComponent();
    BindingContext = new MainViewModel();
}

In MainPage.xaml, bind a Label to Greeting:

XML
<Label Text="{Binding Greeting}" FontSize="24" />

{Binding Greeting} means: "find a property called Greeting on the current BindingContext and show its value."

Run it. The label says "Hello from the ViewModel!". No x:Name, no code-behind assignment.

4. Two-Way Binding — Entry That Talks Back

For an Entry, you want binding to work in both directions:

  • ViewModel → View (show initial value)
  • View → ViewModel (when user types, update the property)

That's called two-way binding and is the default for Entry.Text:

XML
<Entry Text="{Binding Name}" Placeholder="Your name" />
C#
public class MainViewModel
{
    public string Name { get; set; } = "";
}

Now whatever the user types is automatically stored in ViewModel.Name. No OnTextChanged, no manual read.

5. INotifyPropertyChanged — Push Updates to the UI

So far binding works in one direction: the ViewModel starts with a value and the View shows it. But if the ViewModel changes the value later, the View doesn't know.

The fix is the INotifyPropertyChanged interface. The ViewModel fires an event saying "property X changed!" and the binding layer re-reads it.

C#
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class MainViewModel : INotifyPropertyChanged
{
    private string _name = "";

    public string Name
    {
        get => _name;
        set
        {
            if (_name == value) return;
            _name = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

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

The [CallerMemberName] attribute fills in the property name for you — so you just write OnPropertyChanged() without an argument.

Most teams put this boilerplate in a BaseViewModel class and have every ViewModel inherit from it:

C#
public abstract class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? name = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        return true;
    }
}

public class MainViewModel : BaseViewModel
{
    private string _name = "";
    public string Name
    {
        get => _name;
        set => SetField(ref _name, value);
    }
}

6. Commands — Buttons Without Clicked

A ViewModel shouldn't see Button or EventArgs. Instead it exposes commandsICommand objects that the XAML can invoke.

C#
public class MainViewModel : BaseViewModel
{
    private string _name = "";
    public string Name
    {
        get => _name;
        set { SetField(ref _name, value); SaveCommand.ChangeCanExecute(); }
    }

    private string _status = "";
    public string Status
    {
        get => _status;
        set => SetField(ref _status, value);
    }

    public Command SaveCommand { get; }

    public MainViewModel()
    {
        SaveCommand = new Command(
            execute: () => Status = $"Saved '{Name}'!",
            canExecute: () => !string.IsNullOrWhiteSpace(Name)
        );
    }
}

In XAML:

XML
<VerticalStackLayout Padding="20" Spacing="10">
    <Entry Text="{Binding Name}" Placeholder="Enter a name" />
    <Button Text="Save" Command="{Binding SaveCommand}" />
    <Label Text="{Binding Status}" />
</VerticalStackLayout>

canExecute is automatic: when it returns false, MAUI greys out the button.

7. Lists — CollectionView + ObservableCollection<T>

To show a list that updates when items are added or removed, bind a CollectionView to an ObservableCollection<T>.

C#
public class StudentsViewModel : BaseViewModel
{
    public ObservableCollection<Student> Students { get; } = new();

    public Command AddStudentCommand { get; }

    private string _newName = "";
    public string NewName
    {
        get => _newName;
        set => SetField(ref _newName, value);
    }

    public StudentsViewModel()
    {
        AddStudentCommand = new Command(() =>
        {
            if (string.IsNullOrWhiteSpace(NewName)) return;
            Students.Add(new Student { Name = NewName });
            NewName = "";
        });
    }
}
XML
<VerticalStackLayout Padding="20" Spacing="10">

    <HorizontalStackLayout Spacing="10">
        <Entry Text="{Binding NewName}" Placeholder="Student name" WidthRequest="220" />
        <Button Text="Add" Command="{Binding AddStudentCommand}" />
    </HorizontalStackLayout>

    <CollectionView ItemsSource="{Binding Students}">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Grid Padding="10" ColumnDefinitions="*,Auto">
                    <Label Text="{Binding Name}" Grid.Column="0" />
                    <Label Text="👤"           Grid.Column="1" />
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>

</VerticalStackLayout>

Two bindings with different meanings:

  • ItemsSource="{Binding Students}" — bound against the page's ViewModel.
  • Text="{Binding Name}" inside the DataTemplate — bound against one Student item. Inside a template, BindingContext changes to the current item.

8. When to Use Code-Behind vs MVVM

MVVM is the default in production MAUI apps. But it's not a religion. Use code-behind when:

  • You need one-off UI tricks (focus a specific entry, scroll to a position)
  • The logic is purely visual (animations, purely-UI state toggles)
  • A tiny prototype where MVVM scaffolding costs more than it saves

Everything data-related — forms, lists, save/load, validation — belongs in a ViewModel.

💻Code Examples

Example A — Full greeting app with MVVM

BaseViewModel.cs:

C#
public abstract class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? name = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        return true;
    }
}

MainViewModel.cs:

C#
public class MainViewModel : BaseViewModel
{
    private string _name = "";
    private string _greeting = "";

    public string Name
    {
        get => _name;
        set { if (SetField(ref _name, value)) GreetCommand.ChangeCanExecute(); }
    }

    public string Greeting
    {
        get => _greeting;
        set => SetField(ref _greeting, value);
    }

    public Command GreetCommand { get; }

    public MainViewModel()
    {
        GreetCommand = new Command(
            execute: () => Greeting = $"Hello, {Name}! ({Name.Length} letters)",
            canExecute: () => !string.IsNullOrWhiteSpace(Name)
        );
    }
}

MainPage.xaml:

XML
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="HelloMvvm.MainPage"
             Title="Greet">

    <VerticalStackLayout Padding="20" Spacing="10" VerticalOptions="Center">
        <Entry Text="{Binding Name}" Placeholder="Your name" />
        <Button Text="Greet" Command="{Binding GreetCommand}" />
        <Label Text="{Binding Greeting}" FontSize="24" HorizontalOptions="Center"/>
    </VerticalStackLayout>

</ContentPage>

MainPage.xaml.cs:

C#
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        BindingContext = new MainViewModel();
    }
}

Notice: code-behind is now three lines. All logic is in the ViewModel, which is a pure C# class with no UI types.

✏️Practice Tasks

Task 1MVVM counter
EASY — IN CLASS

Refactor the counter app from L13 to MVVM:

  • Create CounterViewModel : BaseViewModel with:
    • int Count property (with change notification)
    • string Display property that returns $"Taps: {Count}"
    • IncrementCommand that does Count++ and re-raises Display
    • ResetCommand that sets Count = 0
  • In XAML, bind a label to Display and two buttons to the commands
  • Set BindingContext = new CounterViewModel() in the page constructor
💡 Hint
When Count changes, Display also needs to re-fire its own PropertyChanged — because bindings don't know that Display depends on Count. Call OnPropertyChanged(nameof(Display)) from within the Count setter, or just have Display as a read-only computed property and manually notify it from commands.
Task 2To-do list
MEDIUM — HOMEWORK

Build a simple to-do app using MVVM + ObservableCollection:

  • TodoItem model: string Title, bool IsDone
  • TodosViewModel with:
    • ObservableCollection<TodoItem> Items
    • string NewTitle (two-way bound to an Entry)
    • AddCommand that adds new TodoItem { Title = NewTitle } and clears NewTitle
  • CollectionView with an ItemTemplate showing a CheckBox (bound to IsDone) and a Label (bound to Title)
💡 Hint
For the CheckBox + Label row, use a Grid with ColumnDefinitions="Auto,*". Two-way binding on CheckBox: IsChecked="{Binding IsDone}". The Label binds against the item, so Text="{Binding Title}" refers to TodoItem.Title, not the ViewModel.
Task 3Student GPA tracker
HARD — HOMEWORK

Build a form + list:

  • Model: Student { string Name; double Gpa; }
  • ViewModel with:
    • ObservableCollection<Student> Students
    • NewName, NewGpa entries (bind as string for Gpa and TryParse when adding)
    • AddCommand that validates Name is non-empty and Gpa is 0.0–4.0; otherwise sets an ErrorMessage property
    • TopStudent computed property that returns the student with the highest GPA (or "—" if list is empty)

UI:

  • Grid form at the top (Name, Gpa, Add button, error label)
  • CollectionView of students showing name and GPA
  • A "Top student" label below the list bound to TopStudent
💡 Hint
TopStudent has to re-compute whenever the collection changes. Subscribe to Students.CollectionChanged in the ViewModel constructor and call OnPropertyChanged(nameof(TopStudent)) inside. This is the same "notify dependent property" trick as in Task 1.

⚠️Common Mistakes

Forgetting to set BindingContext

If no BindingContext is assigned, all {Binding ...} expressions silently resolve to nothing. Nothing shows, nothing crashes. Check that BindingContext = new MainViewModel(); runs in the page constructor.

Using auto-properties without INotifyPropertyChanged

A plain public string Name { get; set; } won't notify the UI when changed from code. The UI shows the initial value and never updates. You need a backing field and a SetField call in the setter.

Using List<T> instead of ObservableCollection<T>

A plain List<T> doesn't raise change notifications. Adding an item won't refresh the CollectionView. Always use ObservableCollection<T> for list properties bound to the UI.

Mixing commands and Clicked on the same button

If you set both Command="..." and Clicked="...", MAUI runs both, which usually isn't what you want. Pick one: Command for MVVM, Clicked for code-behind.

Referencing UI controls inside the ViewModel

If your ViewModel imports Microsoft.Maui.Controls or references Entry, something went wrong — the ViewModel is supposed to be UI-agnostic. Undo it and bind via properties instead.

🎓Instructor Notes

⚡ How to run this lesson (~80 min)

  • [5 min] Motivation. Show the L14 calculator. Ask: "Where does the logic live?" Point at the code-behind. "What happens when we want to test CalculateBmi?" → can't without the UI. Introduce MVVM.
  • [10 min] Diagram on the board. Draw View ↔ ViewModel ↔ Model. Emphasise: bindings glue View to VM; VM doesn't know about View.
  • [15 min] Live code: first binding. Build the tiny greeting example. Use one-way first (Label), then two-way (Entry). Turn off binding → show UI stops working. Restore.
  • [15 min] Live code: INotifyPropertyChanged. This is the part that confuses students most. Walk through BaseViewModel line by line. Then show what happens without OnPropertyChanged.
  • [10 min] Commands. Replace a Clicked handler with a Command. Show canExecute greying the button out live.
  • [5 min] ObservableCollection + CollectionView. Build the student list from section 7 on the projector.
  • [15 min] Task 1 in class. The trap: computed property (Display) not updating when Count changes. Have the OnPropertyChanged(nameof(Display)) fix ready.
  • [5 min] Wrap up + assign homework. Preview L16: connecting the same ViewModel to a real PostgreSQL database. Assign Tasks 2 and 3.

💬 Discussion questions

  • "If the View doesn't reference the ViewModel by name, how does {Binding Name} know where to look?"
  • "What's the benefit of putting logic in a ViewModel instead of code-behind?"
  • "Why does changing a List<T> not update the UI, but changing an ObservableCollection<T> does?"