In an MVVM architecture, models should be kept free of UI-related logic. They represent pure data that is retrieved from memory via a service. I see a challenge when the user interface needs to respond to changes in the internal state of a model, but the model itself should not implement notification mechanisms such as INotifyPropertyChanged or other UI framework-specific behaviors.

Example

Consider a mail application. A folder contains many messages. The FolderViewModel exposes an observable collection of these messages to the view:

public class FolderViewModel(IService service) : ObservableObject
{
    public ObservableCollection<Message> Messages { get; } = service.LoadMessagesForFolder(/*...*/);
}

Each Message is a plain domain model:

public class Message
{
    public string Subject { get; set; }
    public bool IsRead { get; set; }
    public bool IsFlagged { get; set; }
}

From the UI, I can add or remove messages easily via commands on the FolderViewModel. However, when interacting with a message, the following two problems arise:

  1. The user interface is not updated when a message is changed: Since Message is a POCO, it does not implement INotifyPropertyChanged; changes such as marking as read or toggling a flag are not propagated to the user interface.
  2. Commands cannot be bound clean and easily to the parent of the list view item (e.g., in WinUI): The command for a Delete button on each message item must be bound to itself.

Attempted solution

My workaround has been to create an item-viewmodel that wraps the model, exposes observable properties for the UI, and forwards commands:

public partial class MessageItemViewModel(Message model) : ObservableObject
{
    private readonly Message _model = model;

    [ObservableProperty]
    public partial bool IsRead { get; set; } = model.IsRead; 

    [ObservableProperty]
    public partial bool IsFlagged { get; set; } = model.IsFlagged; 

    // commands for setting flag, etc.
}

Although this works, it raises several serious concerns:

  1. Every relevant model property now exists twice (once in the model, once in the wrapper) which leads to code duplication.
  2. Large collections of wrapped items increase memory usage.
  3. Wrappers often require services, but must also be created manually because they have their model as a parameter. This does not allow services to be injected via the DI container.
  4. When item view models subscribe to external events or domain changes, there is a potential risk of leaks.

The question

What is the recommended MVVM approach to reflect changes in model state in the user interface? Is there a clean pattern commonly used in MVVM applications to handle this scenario?

5 Replies 5

Just a note...

This statement is not accurate:

In an MVVM architecture, models should be kept free of UI-related logic.

Generally, this is impossible, because the UI can be, and usually is, an essential part of the overall application logic. The right approach is to isolate and abstract out the UI implementation. Can you see the difference? The criterion for this abstraction is met when you can easily replace the View with something completely different, using a different UI framework, and so on. But not to separat the logic itself.

Now, to discuss the particular architecture issue, including one of the most important issues, code duplication, you have to provide an abstract description of your project goals and most basic requirements, importantly, not based on your existing code.

Your existing code can be discussed separately, when we have the understanding what the application can do.

I think an easy solution is just to implement INotifyPropertyChanged in your Message class. That way you do not mix any UI related things into your Message class. Implementing INotifyPropertyChanged just means, that your class says: I just changed that property, if anybody is intereseted. If someone is interested (like your UI), it is fine. If nobody is interested, this is also fine. Implementing INotifyPropertyChanged does not prevent you using that class in other contextes (where no UI is interested in a change of a property).

Every relevant model property now exists twice (once in the model, once in the wrapper) which leads to code duplication.

Some duplication is inherent in MVVM and related patterns. This is not considered a problem since the context is different. While properties may be dulicated, rules and logic that govern that data should not be duplicated. Data may or may not be duplicated depending on the model of the UI, but the trend is to apply changed immediately, without any separate save-step.

Large collections of wrapped items increase memory usage.

WPF and most other UI frameworks provide virtualization that decouples UI objects from the model. There are several variants of virtualization, but some just need a total count of items up front, and let you create any objects on demand. Even if you create a wrapper for each item I would not expect this to cause memory problems in anything but the most extreme cases.

Wrappers often require services, but must also be created manually because they have their model as a parameter. This does not allow services to be injected via the DI container.

I don't really understand what this means. Services, in the way I consider them, process model objects in some way, and should not have any model objects as constructor parameters, unless it is a default object or something like that.

When item view models subscribe to external events or domain changes, there is a potential risk of leaks.

I'm not sure what you mean by "leaks", but I would argue that any events should be in the model itself. I.e. the model reacts to some external event, updates itself, and raises an event to inform anyone else that it has been changed. There is still a significant risk of circular updates if you are not careful however.

A pattern I like is to create a generic object that contains a value and an event that is raised when that value changes. Something like:

public class ChangeProperty<T>(T internalValue)
{
    private T internalValue = internalValue;
    public T Value
    {
        get => internalValue;
        set {
            if (!EqualityComparer<T>.Default.Equals(internalValue, value))
            {
                internalValue = value;
                Changed?.Invoke(this, value);
            }
        }
    }
    public event EventHandler<T> Changed;
}

Note that this only really works with immutable types. You can extend this patter in several ways, like allowing values to be mapped, synchronized etc. This makes it very easy to share a value between multiple views, and the equality check usually take care of any circular updates. But debugging this kind of code can be difficult since you tend to just have a bunch of anonymous functions in the call stack.

You use "interfaces" to hook models and view "code behind". The interface only includes elements that would be valid in say a "Standard library". When that won't do, you use a "converter"; e.g. a boolean to visibility converter (one way or two way); since "visibility" is a UI enum. BTW, MS is "slanting" away from MVVM due to its impact on "performance" (compiler limitations and "indirection"). Part of that pattern includes "publishing events"; that the view and or model can subscribe to; e.g. "Submit happened"; "Data arrived"; "Setting changed". The "sender" can be the model itself; or the view "interface".

If you own (can modify) the Message class, you can simply make it an ObservableObject. Being an ObservableObject doesn't necessarily mean it is UI related. It can also be in a pure C# class library.

Your Reply

By clicking “Post Your Reply”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.