The C# features WPF and MVVM lean on, from properties, events, partial classes and interfaces to ObservableCollection, lambdas, async and the UI thread, with links to learn each one.

Bishal Biswas
Self Learner
Why this topic exists
A WPF app has two halves: XAML describes what the window looks like, and C# makes it do things. This course teaches XAML from zero, but it expects you to read C# without getting stuck.
Desktop apps use C# a little differently from web apps. A web request comes in, runs, and finishes. A WPF window stays open, waits for the user, and reacts to clicks and typing for as long as the app runs. That is why events, properties that announce changes and the UI thread matter so much more here.
This topic lists the C# features WPF uses, in the order you will meet them. For each one you get:
- a short explanation in plain words,
- a small piece of real WPF code that uses it,
- a link to the matching topic in the C# course for the full story.
The list has three groups:
- Know before you start. You will see these in the first few topics.
- Learn as you go. They appear later in the course.
- Can wait. Good C#, but you will not need it to build CoinJar.
If you already write C# comfortably, skim the samples and go on to Setting Up Visual Studio and the .NET SDK.
Know before you start
1. Classes, objects and properties
Your data lives in classes with properties. CoinJar, the expense tracker you build in this course, starts with this one:
public class Expense
{
public int Id { get; set; }
public string Title { get; set; } = "";
public decimal Amount { get; set; }
public DateTime Date { get; set; } = DateTime.Today;
}
WPF data binding only works with properties, never with plain fields. {Binding Title} in XAML reads the Title property, so you must know the difference between a field and a property.
You also need full properties, the long form with a private backing field. They let you run code whenever the value changes, which is exactly what WPF needs to update the screen:
private decimal _amount;
public decimal Amount
{
get => _amount;
set
{
_amount = value;
// tell the window that Amount changed (see section 5)
}
}
Learn more: Classes and Objects, Fields and Properties, Variables and Data Types.
2. Partial classes
Every window is split across two files: MainWindow.xaml and MainWindow.xaml.cs. The build turns the XAML into C# code, and the two halves are joined into one class. That only works because the class is marked partial:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent(); // created from the XAML half
}
}
partial comes back later with CommunityToolkit.Mvvm, where a code generator writes the other half of your view models for you. If you forget partial there, you get a build error.
Learn more: Classes and Objects, Source Generators (later, to see how the generated half is made). In this course: Understanding the Project Structure.
3. Inheritance
MainWindow : Window means your window inherits everything a WPF window already knows: a title bar, sizing, Show() and Close(). You will also inherit from UserControl, Application and, with the toolkit, ObservableObject.
public partial class App : Application { }
public partial class ExpenseCard : UserControl { }
You rarely build deep inheritance trees yourself, but you must understand what a base class gives you and how override works, because you will override methods such as OnStartup.
Learn more: Inheritance, Polymorphism, Access Modifiers.
4. Methods, events and event handlers
A desktop app spends its life waiting for something to happen. A button click is an event, and the method that runs is an event handler. You connect them in XAML (Click="AddButton_Click") or in C# with +=:
private void AddButton_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Expense added");
}
// The same connection, written in C#
AddButton.Click += AddButton_Click;
Every handler has the same shape: object sender (who raised the event) and an EventArgs type with the details. Events are built on delegates, so read about both.
Learn more: Events, Delegates, Methods and Functions. In this course: Code-Behind and Event Handlers.
5. Interfaces
WPF talks to your code through interfaces. Implement one, and WPF knows what your class can do. The three you will write most:
INotifyPropertyChanged: “I will tell you when a property changes”, so bound controls refresh.ICommand: “I am an action a button can run”, used in MVVM instead of click handlers.IValueConverter: “I can turn one value into another for display”, such as an amount into a colour.
public class Expense : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
Notice that this interface contains an event. Interfaces and events work together all the time in WPF.
Learn more: Interfaces, Abstraction. In this course: INotifyPropertyChanged, Commands and ICommand.
6. Nullable types and the ?. operator
New projects turn on nullable reference types, so the compiler warns you about values that might be null. In WPF you will see object? sender, event ... ? PropertyChanged and Expense? SelectedExpense (nothing is selected yet).
Expense? selected = ExpensesList.SelectedItem as Expense;
if (selected is null)
{
return;
}
?. calls a member only if the object is not null. PropertyChanged?.Invoke(...) is in almost every hand written view model.
Learn more: Variables and Data Types, Operators in C#, Classes and Objects.
7. Casting with as, is and (Type)
WPF hands you values typed as object: the sender of an event, SelectedItem of a list, the value in a converter. You must cast them to the real type before you can use them:
if (sender is Button button)
{
button.IsEnabled = false;
}
var expense = (Expense)ExpensesList.SelectedItem; // throws if it is not an Expense
Prefer is with a variable name (a type pattern): it checks and casts in one step and never throws.
Learn more: Type Conversion and Casting, Advanced Pattern Matching.
8. Generics and ObservableCollection<T>
A list on screen needs a collection behind it. List<T> works, but the screen will not notice when you add or remove an item. ObservableCollection<T> raises an event on every change, so the ListBox or DataGrid updates by itself:
public ObservableCollection<Expense> Expenses { get; } = new();
Expenses.Add(new Expense { Title = "Coffee", Amount = 3.50m }); // the list on screen grows
The <T> is generics: the same collection class, made for any type you name.
Learn more: Generics, List<T>, Dictionary<TKey, TValue>. In this course: Binding to Collections with ObservableCollection.
9. Lambda expressions
A lambda is a small inline function. WPF code uses them for commands, short event handlers and code that must run on the UI thread:
DeleteCommand = new RelayCommand(() => Expenses.Remove(SelectedExpense!),
() => SelectedExpense is not null);
SaveButton.Click += (sender, e) => Save();
The first lambda is what the button does. The second decides whether the button is enabled.
Learn more: Lambda Expressions, Delegates.
10. Attributes
Attributes are the labels in square brackets. With CommunityToolkit.Mvvm they replace pages of repeated code: [ObservableProperty] writes the change notification and [RelayCommand] turns a method into a command.
public partial class ExpenseListViewModel : ObservableObject
{
[ObservableProperty]
public partial string Title { get; set; } = "";
[RelayCommand]
private void Clear() => Title = ""; // becomes ClearCommand
}
Before the toolkit you will also meet [CallerMemberName], which fills in the property name for you in OnPropertyChanged().
Learn more: Attributes. In this course: MVVM with CommunityToolkit.Mvvm.
Learn as you go
11. async, await and the UI thread
WPF has one UI thread. If it is busy, the window freezes and Windows shows “Not Responding”. await lets slow work (files, databases, web calls) happen without blocking that thread, and then continues on the UI thread so you can update controls safely.
private async void LoadButton_Click(object sender, RoutedEventArgs e)
{
StatusText.Text = "Loading...";
var expenses = await _storage.LoadAsync(); // the window stays responsive
StatusText.Text = $"{expenses.Count} expenses";
}
Event handlers are the one place where async void is fine. Everywhere else, return Task. Code on a background thread must use the Dispatcher to touch controls.
Learn more: Async and Await, Multithreading. In this course: Async, Await and a Responsive UI, The Dispatcher and Threading.
12. LINQ
Totals, filters and sorting are LINQ one liners:
decimal total = Expenses.Sum(e => e.Amount);
var food = Expenses.Where(e => e.Category == "Food").OrderByDescending(e => e.Date);
Learn more: LINQ Fundamentals, LINQ Method Syntax. In this course: Sorting, Filtering and Grouping with CollectionView.
13. Static members, enums and nameof
Static members belong to the class itself, not to one object: Application.Current, MessageBox.Show() and the DependencyProperty fields you will register for custom controls. Enums name fixed choices like Visibility.Collapsed or MessageBoxResult.Yes. nameof(Amount) gives you the text "Amount" without typos.
var answer = MessageBox.Show("Delete this expense?", "CoinJar", MessageBoxButton.YesNo);
if (answer == MessageBoxResult.Yes) { /* delete it */ }
OnPropertyChanged(nameof(Total));
Learn more: Fields and Properties, Variables and Data Types. In this course: Dependency Properties.
14. Constructors and dependency injection
Later you will let the Generic Host create your windows and view models, passing in services through the constructor:
public partial class MainViewModel(IExpenseStorage storage) : ObservableObject
{
public Task SaveAsync(IEnumerable<Expense> items) => storage.SaveAsync(items);
}
That (IExpenseStorage storage) after the class name is a primary constructor.
Learn more: Constructors and Destructor, Dependency Injection Fundamentals. In this course: Dependency Injection with the Generic Host.
15. Exceptions, files and using
Desktop apps read and write files on the user’s PC, and things go wrong: the file is missing or locked. Wrap that work in try/catch, show a friendly message, and use using for streams and database contexts so they close properly.
try
{
await using var stream = File.OpenRead(path);
var items = await JsonSerializer.DeserializeAsync<List<Expense>>(stream);
}
catch (IOException ex)
{
MessageBox.Show($"Could not read your data: {ex.Message}");
}
Learn more: Exception Handling, File Handling, IDisposable and Resource Management. In this course: Saving Data to JSON Files, Error Handling and Logging.
16. Records (optional)
Records are handy for small messages that view models send to each other:
public record ExpenseAddedMessage(Expense Expense);
Learn more: Records. In this course: Messaging Between ViewModels.
Can wait
You can build all of CoinJar without these:
- Locks and low level threads. The Dispatcher and
asynccover what a typical WPF app needs. See Synchronization and Thread Safety when you share data between threads. - Writing your own source generators. The toolkit uses one, but you only use its attributes.
- Reflection, Span<T>, unsafe code and pointers. Advanced topics for library and performance work.
Your checklist
| C# concept | Where WPF uses it | C# topic |
|---|---|---|
| Properties and backing fields | Everything you bind to | Fields and Properties |
| Partial classes | MainWindow.xaml + .xaml.cs, the toolkit |
Classes and Objects |
| Inheritance | Window, UserControl, ObservableObject |
Inheritance |
| Events and delegates | Clicks, PropertyChanged |
Events |
| Interfaces | INotifyPropertyChanged, ICommand, converters |
Interfaces |
Nullable ? and ?. |
sender, selected items, events |
Operators in C# |
| Casting | sender, SelectedItem, converters |
Type Conversion and Casting |
| Generics | ObservableCollection<T> |
Generics |
| Lambdas | Commands, short handlers | Lambda Expressions |
| Attributes | [ObservableProperty], [RelayCommand] |
Attributes |
| async and the UI thread | Loading and saving without freezing | Async and Await |
If you can follow the view model in the code panel, you are ready for WPF. Building for the web instead? The MVC and Web API courses have their own version of this list.
Quick recap
- WPF apps stay open and react to the user, so events, change notification and the UI thread matter more than in web apps.
- Before you start, know properties and backing fields, partial classes, inheritance, events, interfaces, nullable types, casting, generics, lambdas and attributes.
- Learn
asyncwith the UI thread, LINQ, static members, dependency injection, exceptions and files as the course reaches them. - Low level threading, reflection and performance features can wait.
- Each concept links to a full topic in the C# course when you want to go deeper.
// ViewModels/ExpenseListViewModel.cs
// A hand written CoinJar view model that uses the "know before you start" C# features.
// The numbers match the sections of this topic.
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace CoinJar.ViewModels;
// 5. Interface: INotifyPropertyChanged lets bound controls refresh themselves
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged; // 4 + 6. a nullable event
// 10. [CallerMemberName] fills in the property name for us
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); // 6. ?. operator
}
// 3. Inheritance: Expense gets OnPropertyChanged from ViewModelBase
public class Expense : ViewModelBase
{
private string _title = "";
private decimal _amount;
public string Title
{
get => _title;
set { _title = value; OnPropertyChanged(); } // 1. full property
}
public decimal Amount
{
get => _amount;
set { _amount = value; OnPropertyChanged(); }
}
}
// 5. Interface: ICommand is what a Button's Command property calls
public class RelayCommand(Action execute, Func<bool>? canExecute = null) : ICommand
{
public event EventHandler? CanExecuteChanged; // 4. an event
public bool CanExecute(object? parameter) => canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => execute();
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public class ExpenseListViewModel : ViewModelBase
{
private Expense? _selectedExpense; // 6. nothing selected yet
// 8. Generics: ObservableCollection tells the ListBox about every Add and Remove
public ObservableCollection<Expense> Expenses { get; } = new();
public Expense? SelectedExpense
{
get => _selectedExpense;
set
{
_selectedExpense = value;
OnPropertyChanged();
DeleteCommand.RaiseCanExecuteChanged(); // re-check the button
}
}
public decimal Total => Expenses.Sum(e => e.Amount); // 12. LINQ
public RelayCommand AddCommand { get; }
public RelayCommand DeleteCommand { get; }
public ExpenseListViewModel()
{
// 9. Lambdas: what each button does, and when Delete is allowed
AddCommand = new RelayCommand(() =>
Expenses.Add(new Expense { Title = "Coffee", Amount = 3.50m })); // 1. object initializer
DeleteCommand = new RelayCommand(
() => Expenses.Remove(SelectedExpense!),
() => SelectedExpense is not null);
// 4. Subscribe to an event with +=, and use 13. nameof
Expenses.CollectionChanged += (sender, e) => OnPropertyChanged(nameof(Total));
}
}
// In MainWindow.xaml (the other half of the partial MainWindow class):
//
// <ListBox ItemsSource="{Binding Expenses}" DisplayMemberPath="Title"
// SelectedItem="{Binding SelectedExpense}" />
// <TextBlock Text="{Binding Total, StringFormat=Total: {0:C}}" />
// <Button Content="Add" Command="{Binding AddCommand}" />
// <Button Content="Delete" Command="{Binding DeleteCommand}" />
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready