Industry Insights, Information, and Developer News
If you've built any .NET MAUI applications, you know that ViewModels are the heart of your UI logic. And if you've tried to write unit tests for those ViewModels, you've probably discovered that testing INotifyPropertyChanged implementations can be... frustrating.
INotifyPropertyChanged
I was recently working on a MAUI app where data binding seemed to work inconsistently. Sometimes the UI would update when the ViewModel changed, sometimes it wouldn't. The problem? The ViewModel wasn't raising property change notifications correctly, and they had no tests to catch these issues.
Let me show you a reliable pattern for testing ViewModel property change notifications that will save you hours of debugging binding issues in your MAUI apps.
The Problem: Invisible Binding Failures Consider this typical MAUI scenario - a user profile form with some basic validation logic:
public class UserProfileViewModel : INotifyPropertyChanged { private string _name; private int _age; public string Name { get => _name; set { if (_name != value) { _name = value; OnPropertyChanged(nameof(Name)); } } } public int Age { get => _age; set { if (_age != value) { _age = value; OnPropertyChanged(nameof(Age)); // Oops - forgot to notify IsAdult! } } } public bool IsAdult => Age >= 18; public bool CanSubmit => !string.IsNullOrEmpty(Name) && IsAdult; public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
This ViewModel looks reasonable, but it has subtle bugs. When Age changes, IsAdult might change too, but we're not notifying the UI about that. Same with CanSubmit - it depends on both Name and IsAdult, but we're not raising notifications for it either.
Age
IsAdult
CanSubmit
Name
In a MAUI app, this means your submit button might stay disabled even when the form becomes valid, or validation indicators might not update properly. These binding issues are notoriously hard to debug because they work sometimes but fail in edge cases.
The Traditional Testing Approach (And Why It's Painful) Most developers try to test property change notifications like this:
[Test] public void ChangingAge_ShouldUpdateIsAdult() { var viewModel = new UserProfileViewModel(); var propertyChangedRaised = false; string changedPropertyName = null; viewModel.PropertyChanged += (sender, e) => { propertyChangedRaised = true; changedPropertyName = e.PropertyName; }; viewModel.Age = 25; Assert.IsTrue(propertyChangedRaised); Assert.AreEqual("Age", changedPropertyName); // But what about IsAdult? This test can't check for multiple notifications! }
This approach falls apart quickly when you need to test:
A Better Way: The NotifyPropertyChangedTester Here's a helper class that makes testing property change notifications much more reliable:
using System.ComponentModel; public class NotifyPropertyChangedTester { public NotifyPropertyChangedTester(INotifyPropertyChanged viewModel) { if (viewModel == null) throw new ArgumentNullException(nameof(viewModel)); Changes = new List<string>(); viewModel.PropertyChanged += OnPropertyChangedEvent; } private void OnPropertyChangedEvent(object? sender, PropertyChangedEventArgs e) { if (string.IsNullOrEmpty(e.PropertyName)) throw new InvalidOperationException("PropertyName was null or empty"); Changes.Add(e.PropertyName); } public List<string> Changes { get; private set; } public void AssertChange(int index, string expectedProperty) { Assert.That(Changes, Is.Not.Null); Assert.That(index < Changes.Count, $"Expected at least {index + 1} changes."); Assert.That(Changes[index], Is.EqualTo(expectedProperty)); } public void AssertChange(string expectedProperty) { Assert.That(Changes, Does.Contain(expectedProperty), $"Expected a change notification for '{expectedProperty}'."); } public void AssertChangeCount(int expectedCount) { Assert.That(Changes.Count, Is.EqualTo(expectedCount), $"Expected {expectedCount} property changes, but got {Changes.Count}"); } }
This helper captures all property change notifications in order, making it easy to verify complex scenarios.
Testing Real MAUI Scenarios Now we can write comprehensive tests for our ViewModel. Let's fix our UserProfileViewModel first by adding the missing notifications:
UserProfileViewModel
public class UserProfileViewModel : INotifyPropertyChanged { private string _name; private int _age; public string Name { get => _name; set { if (_name != value) { _name = value; OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(CanSubmit)); // Added! } } } public int Age { get => _age; set { if (_age != value) { _age = value; OnPropertyChanged(nameof(Age)); OnPropertyChanged(nameof(IsAdult)); // Added! OnPropertyChanged(nameof(CanSubmit)); // Added! } } } public bool IsAdult => Age >= 18; public bool CanSubmit => !string.IsNullOrEmpty(Name) && IsAdult; public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
Now we can write thorough tests:
[TestFixture] public class UserProfileViewModelTests { [Test] public void ChangingName_ShouldNotifyNameAndCanSubmit() { var viewModel = new UserProfileViewModel { Age = 25 }; // Start as adult var tester = new NotifyPropertyChangedTester(viewModel); viewModel.Name = "Alice"; tester.AssertChangeCount(2); tester.AssertChange(0, "Name"); tester.AssertChange(1, "CanSubmit"); } [Test] public void ChangingAge_ShouldNotifyAllDependentProperties() { var viewModel = new UserProfileViewModel { Name = "Bob" }; var tester = new NotifyPropertyChangedTester(viewModel); viewModel.Age = 25; tester.AssertChangeCount(3); tester.AssertChange(0, "Age"); tester.AssertChange(1, "IsAdult"); tester.AssertChange(2, "CanSubmit"); } [Test] public void ChangingFromAdultToMinor_ShouldUpdateCanSubmit() { var viewModel = new UserProfileViewModel { Name = "Charlie", Age = 20 }; var tester = new NotifyPropertyChangedTester(viewModel); viewModel.Age = 16; // Now a minor tester.AssertChange("Age"); tester.AssertChange("IsAdult"); tester.AssertChange("CanSubmit"); // Verify the actual values too Assert.That(viewModel.IsAdult, Is.False); Assert.That(viewModel.CanSubmit, Is.False); } [Test] public void SettingSameValue_ShouldNotRaiseNotification() { var viewModel = new UserProfileViewModel { Name = "David" }; var tester = new NotifyPropertyChangedTester(viewModel); viewModel.Name = "David"; // Same value tester.AssertChangeCount(0); } }
MAUI-Specific Testing Considerations When testing ViewModels for MAUI applications, there are a few additional things to keep in mind:
SettingSameValue_ShouldNotRaiseNotification
Advanced Testing Scenarios You can extend this pattern for more complex scenarios:
[Test] public void LoadingData_ShouldNotifyInCorrectOrder() { var viewModel = new UserProfileViewModel(); var tester = new NotifyPropertyChangedTester(viewModel); // Simulate loading user data viewModel.IsLoading = true; viewModel.Name = "Loading User"; viewModel.Age = 30; viewModel.IsLoading = false; // Verify the notifications happened in the right order tester.AssertChange(0, "IsLoading"); tester.AssertChange(1, "Name"); tester.AssertChange(2, "CanSubmit"); tester.AssertChange(3, "Age"); tester.AssertChange(4, "IsAdult"); tester.AssertChange(5, "CanSubmit"); // Second notification for CanSubmit tester.AssertChange(6, "IsLoading"); }
Why This Matters for MAUI In traditional desktop applications, you might get away with sloppy property change notifications because binding failures are often more obvious. But in MAUI apps:
Having comprehensive ViewModel tests gives you confidence that your data binding will work correctly across all platforms and scenarios.
Summary Testing INotifyPropertyChanged implementations doesn't have to be painful. The NotifyPropertyChangedTester helper class makes it easy to verify that your ViewModels raise the right notifications in the right order.
NotifyPropertyChangedTester
For MAUI applications especially, these tests are crucial because binding failures can be silent and platform-specific. By testing your property change notifications thoroughly, you'll catch binding issues during development instead of discovering them in production.
The next time you're building a MAUI ViewModel, set up these tests from the beginning. Your future self (and your users) will thank you when the data binding just works.
About the Author
Posted by Benjamin Day on 08/19/2025