Search code examples
wpfmvvmdata-bindingobservablecollectioncommunity-toolkit-mvvm

Update ObservableCollection item and related view, when the item implements an interface


As written everywhere (e.g. here, here, here, here...), reporting an ObservableCollection item property change to a view requires the item to implement INotifyPropertyChanged.
Using CommunityToolkit.Mvvm this can be done using an attribute:

public class MyViewModel : ObservableObject
{
    public ObservableCollection<MyItem> MyCollection { get; set; }
    //...
}

[INotifyPropertyChanged] // yay! No boilerplate code needed
public partial class MyItem
{
    public string MyProperty { get; set; }
}

If somewhere inside MyViewModel there is a change to the MyProperty of an item of MyCollection, the view will be updated. What if an interface comes into play?

public class MyViewModel : ObservableObject
{
    public ObservableCollection<IMyInterface> MyCollection { get; set; }
    //...
}

[INotifyPropertyChanged]
public partial class MyItem : IMyInterface // MyProperty is in IMyInterface too
{
    public string MyProperty { get; set; }
}

The view seems not to be updated anymore. I tried:

  1. Inheriting INotifyPropertyChanged in IMyInterface (that requires explicit implementation of the PropertyChanged event and OnPropertyMethod method in MyItem, which I don't want as otherwise I would have not used CommunityToolkit.Mvvm)
  2. Adding [INotifyPropertyChanged] to MyViewModel (expecting nothing but there was an answer somewhere telling that)

Is there an obvious, no-boilerplate solution that I'm missing here?

The view is updated if I do something like suggested here

var item = MyCollection[0];
item.MyProperty = "new value";
MyCollection[0] = item;

but I hope there's a better way.


Solution

  • I'm not sure this will solve your issue but I can see you're using the attribute incorrectly.

    Please see https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/inotifypropertychanged

    "These attributes are only meant to be used in cases where the target types cannot just inherit from the equivalent types (eg. from ObservableObject). If that is possible, inheriting is the recommended approach, as it will reduce the binary size by avoiding creating duplicated code into the final assembly."

    You're not inheriting from IMyInterface, you're implementing it.

    Your viewmodel should inherit ObservableObject

    Instead of

     [INotifyPropertyChanged]
     public partial class MyItem : IMyInterface
    

    You should have:

     public partial class MyItem : ObservableObject, IMyInterface
    

    Properties look like:

        [ObservableProperty]
        private List<FlatTransactionVM> transactions = new List<FlatTransactionVM>();
    

    That generates a public Transactions property.

    If you only have properties, you don't need that to be a partial class. That's necessary for relaycommand generation.

    EDIT

    This works for me:

    MainWindow

        <Window.DataContext>
            <local:MainWindowViewModel/>
        </Window.DataContext>
    <Grid>
        <ListBox ItemsSource="{Binding MyCollection}"
                 DisplayMemberPath="MyProperty"/>
    </Grid>
    

    MainWindowViewModel

        public partial class MainWindowViewModel : ObservableObject
        {
    
            [ObservableProperty]
            private ObservableCollection<IMyInterface> myCollection = new ObservableCollection<IMyInterface>();
            public MainWindowViewModel()
            {
               MyCollection = new ObservableCollection<IMyInterface>(
                  new List<MyItem>{
                      new MyItem{ MyProperty = "A" },
                      new MyItem { MyProperty = "B" },
                      new MyItem { MyProperty = "C" }
                  });
            }
        }
    

    MyItem

    public partial class MyItem : ObservableObject, IMyInterface
    {
        [ObservableProperty]
        private string myProperty;
    }
    

    Interface

    public interface IMyInterface
    {
        string MyProperty { get; set; }
    }
    

    Quick and dirty code in the view. This is purely to see what happens when one of the MyItems.MyProperty is set.

        private async void Window_ContentRendered(object sender, EventArgs e)
        {
            await Task.Delay(5000);
            var mw = this.DataContext as MainWindowViewModel;
            mw.MyCollection[1].MyProperty = "XXXXX";
        }
    

    After a short wait, I see the second item change to XXXXX as expected.