property change event
event handling
observer pattern
property value monitoring
c# events

Raise an event whenever a property's value changed?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In C#, the standard way to raise an event when a property changes is to implement the INotifyPropertyChanged interface. This interface defines a PropertyChanged event that fires whenever a property's value is set to a new value. It is the foundation of data binding in WPF, WinForms, and MAUI. For custom events beyond INotifyPropertyChanged, you can define your own event delegates. The pattern involves comparing the old and new values in the property setter, updating the backing field, and then invoking the event.

INotifyPropertyChanged (Standard Pattern)

csharp
1using System.ComponentModel;
2using System.Runtime.CompilerServices;
3
4public class Person : INotifyPropertyChanged
5{
6    public event PropertyChangedEventHandler PropertyChanged;
7
8    private string _name;
9    public string Name
10    {
11        get => _name;
12        set
13        {
14            if (_name != value)
15            {
16                _name = value;
17                OnPropertyChanged();
18            }
19        }
20    }
21
22    private int _age;
23    public int Age
24    {
25        get => _age;
26        set
27        {
28            if (_age != value)
29            {
30                _age = value;
31                OnPropertyChanged();
32            }
33        }
34    }
35
36    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
37    {
38        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
39    }
40}
41
42// Usage
43var person = new Person();
44person.PropertyChanged += (sender, e) =>
45{
46    Console.WriteLine($"{e.PropertyName} changed");
47};
48
49person.Name = "Alice";  // Output: Name changed
50person.Age = 30;        // Output: Age changed
51person.Age = 30;        // No output — value didn't change

The [CallerMemberName] attribute automatically fills in the property name, eliminating magic strings. The if (_name != value) check prevents unnecessary event firing when the same value is assigned.

Generic SetProperty Helper

csharp
1using System.ComponentModel;
2using System.Runtime.CompilerServices;
3
4public class ObservableObject : INotifyPropertyChanged
5{
6    public event PropertyChangedEventHandler PropertyChanged;
7
8    protected bool SetProperty<T>(ref T field, T value,
9        [CallerMemberName] string propertyName = null)
10    {
11        if (EqualityComparer<T>.Default.Equals(field, value))
12            return false;
13
14        field = value;
15        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
16        return true;
17    }
18}
19
20public class Product : ObservableObject
21{
22    private string _name;
23    public string Name
24    {
25        get => _name;
26        set => SetProperty(ref _name, value);
27    }
28
29    private decimal _price;
30    public decimal Price
31    {
32        get => _price;
33        set => SetProperty(ref _price, value);
34    }
35}

The SetProperty helper reduces boilerplate to a single line per property. It returns true if the value changed, useful for triggering additional logic.

Custom Event with Old and New Values

csharp
1public class PropertyChangedEventArgs<T> : EventArgs
2{
3    public string PropertyName { get; }
4    public T OldValue { get; }
5    public T NewValue { get; }
6
7    public PropertyChangedEventArgs(string name, T oldValue, T newValue)
8    {
9        PropertyName = name;
10        OldValue = oldValue;
11        NewValue = newValue;
12    }
13}
14
15public class Settings
16{
17    public event EventHandler<PropertyChangedEventArgs<int>> VolumeChanged;
18
19    private int _volume;
20    public int Volume
21    {
22        get => _volume;
23        set
24        {
25            if (_volume != value)
26            {
27                var old = _volume;
28                _volume = value;
29                VolumeChanged?.Invoke(this,
30                    new PropertyChangedEventArgs<int>(nameof(Volume), old, value));
31            }
32        }
33    }
34}
35
36// Usage
37var settings = new Settings();
38settings.VolumeChanged += (s, e) =>
39{
40    Console.WriteLine($"Volume: {e.OldValue} -> {e.NewValue}");
41};
42settings.Volume = 80;  // Volume: 0 -> 80

Custom events can carry the old and new values, enabling undo/redo systems, validation, and logging.

WPF Data Binding Example

csharp
1// ViewModel
2public class MainViewModel : ObservableObject
3{
4    private string _message;
5    public string Message
6    {
7        get => _message;
8        set => SetProperty(ref _message, value);
9    }
10}
xml
<!-- XAML — TextBlock automatically updates when Message changes -->
<TextBlock Text="{Binding Message}" />
<TextBox Text="{Binding Message, UpdateSourceTrigger=PropertyChanged}" />

WPF's binding system listens for PropertyChanged events and updates the UI automatically. This is the primary use case for INotifyPropertyChanged.

CommunityToolkit.Mvvm (Source Generators)

csharp
1using CommunityToolkit.Mvvm.ComponentModel;
2
3// Source generators eliminate all boilerplate
4public partial class PersonViewModel : ObservableObject
5{
6    [ObservableProperty]
7    private string _name;
8
9    [ObservableProperty]
10    private int _age;
11
12    // The source generator creates:
13    // public string Name { get; set; }  with PropertyChanged notification
14    // public int Age { get; set; }      with PropertyChanged notification
15    // partial void OnNameChanging(string value)  — override hook
16    // partial void OnNameChanged(string value)   — override hook
17}

The MVVM Toolkit's [ObservableProperty] attribute uses source generators to create the full property with change notification at compile time. This is the modern approach for .NET 6+ projects.

JavaScript/TypeScript Equivalent

javascript
1// JavaScript Proxy-based property change detection
2function observable(target) {
3    const handlers = {};
4
5    return new Proxy(target, {
6        set(obj, prop, value) {
7            const oldValue = obj[prop];
8            obj[prop] = value;
9            if (oldValue !== value && handlers[prop]) {
10                handlers[prop].forEach(fn => fn(value, oldValue));
11            }
12            return true;
13        }
14    });
15}
16
17const person = observable({ name: "", age: 0 });
18
19// Subscribe to changes
20person.__handlers = { name: [] };
21person.__handlers.name.push((newVal, oldVal) => {
22    console.log(`Name: ${oldVal} -> ${newVal}`);
23});
typescript
1// TypeScript with decorators
2function notify(target: any, key: string) {
3    let value = target[key];
4    Object.defineProperty(target, key, {
5        get: () => value,
6        set: (newValue) => {
7            const oldValue = value;
8            value = newValue;
9            console.log(`${key}: ${oldValue} -> ${newValue}`);
10        }
11    });
12}

Common Pitfalls

  • Raising event before updating the backing field: If you fire PropertyChanged before assigning the new value to the field, event handlers that read the property will get the old value. Always update the field first, then raise the event.
  • Forgetting the equality check: Without if (_name != value), setting the same value triggers the event and causes unnecessary UI updates, which can lead to infinite loops in two-way data binding scenarios (setter fires event, binding sets value, setter fires event again).
  • Using string literals instead of nameof: OnPropertyChanged("Name") works but breaks silently if you rename the property. Use [CallerMemberName] or nameof(Name) so the compiler catches renames.
  • Not raising events for dependent properties: If FullName depends on FirstName and LastName, changing FirstName must also raise PropertyChanged for FullName. Missing this causes the UI to show stale computed values.
  • Thread safety with event invocation: PropertyChanged?.Invoke(...) is not thread-safe if subscribers are added/removed concurrently. In multi-threaded scenarios, capture the delegate in a local variable first: var handler = PropertyChanged; handler?.Invoke(...).

Summary

  • Implement INotifyPropertyChanged to raise events when properties change in C#
  • Use [CallerMemberName] to automatically pass the property name without magic strings
  • Create a SetProperty<T> helper method in a base class to reduce per-property boilerplate
  • Use CommunityToolkit.Mvvm with [ObservableProperty] for zero-boilerplate source-generated properties
  • Always check for value equality before raising the event to prevent infinite loops in data binding
  • Update the backing field before invoking the event so handlers read the new value

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.