VSLive! Blog

Industry Insights, Information, and Developer News

Blog archive

.NET MAUI ViewModel Testing: Getting Property Change Notifications Right

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.

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.

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:

  • Multiple property notifications from a single change
  • The order of notifications
  • Computed properties that depend on other properties
  • Complex scenarios with cascading updates

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:

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:

  • Performance Matters More: Mobile devices have limited resources, so unnecessary property change notifications can impact performance. The SettingSameValue_ShouldNotRaiseNotification test above ensures you're not raising events unnecessarily.
  • Computed Properties Are Common: MAUI apps often have complex UI states that depend on multiple properties. Make sure you test all the cascading notifications.
  • Binding Failures Are Silent: Unlike desktop apps where you might see binding errors in the output window, MAUI binding failures can be completely silent. Good tests are your safety net.
  • Cross-Platform Considerations: Different platforms might handle binding differently. While your ViewModel tests run on the build server, they help ensure consistent behavior across iOS, Android, and Windows.

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:

  • Touch interfaces make it harder to debug when buttons don't enable/disable properly
  • Navigation patterns mean users might not notice state inconsistencies immediately
  • Platform differences can mask binding issues during development
  • Release builds make debugging binding problems much harder

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.

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

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 08/19/2025


Keep Up-to-Date with Visual Studio Live!

Email Address*Country*
Please type the letters/numbers you see above.