c# – WPF(MVVM) + Entity Framework: GUI update with navigation property change

Question:

Let's say in an MVVM application there are two classes in the model:

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Book> Books { get; set; }
}

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int AuthorId { get; set; }
    public virtual Author Author { get; set; }
}

There is a class in the ViewModel, in which some author with several books, loaded from the database using the EntityFramework, is passed as a parameter:

public class AuthorsViewModel: ViewModelBase
{

    public AuthorsViewModel()
        : this(null)
    {  

    }

    public AuthorsViewModel(Author author)
    {
        _author = author;
    }  

    Author _author;
    public Author CurrentAuthor
    {
        get
        {
            if (_author == null)
            {
                _author = new Author();
            }
            return _author;
        }
        set
        {
            _author = value;
            RaisePropertyChanged("CurrentAuthor");
        }
    }

    Book _selectedBook;
    public Book SelectedBook
    {
        get
        {
            if (_selectedBook == null)
            {
                _selectedBook = new Book();
            }
            return _selectedBook;
        }
        set
        {
            _selectedBook = value;
            RaisePropertyChanged("SelectedBook");
        }
    }   

    public RelayCommand RemoveBookCommand
    {
        get
        {
            return new RelayCommand(RemoveBook);
        }
    }
    void RemoveBook()
    {
        CurrentAuthor.Books.Remove(SelectedBook);
    }      
}

And finally, in the View there is a window in which this author is described in detail:

<Window x:Class="{КЛАСС ОКНА}"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="{НАШ NAMESPACE}"
        xmlns:vm="clr-namespace:{ТАМ ЛЕЖИТ НАШ КЛАСС ViewModel}"
        mc:Ignorable="d"
        Title="" Height="246" Width="300">
    <Window.DataContext>
        <vm:AuthorsViewModel/>
    </Window.DataContext>
    <Grid>
        <TextBlock Text="Author:"/>
        <TextBox 
            Text="{Binding Path=CurrentAuthor.Name}" 
            Width="150"
            Height="23"
            Margin="40,0,0,0" 
            HorizontalAlignment="Left" 
            VerticalAlignment="Top" 
            BorderBrush="Black"/>
        <ListView
            BorderBrush="Black"
            Width="250"
            Height="150"
            ItemsSource="{Binding Path=CurrentAuthor.Books}"
            SelectedItem="{Binding Path=SelectedBook}">
            <ListView.View>
                <GridView>
                    <GridViewColumn 
                        Header="Books"
                        DisplayMemberBinding="{Binding Path=Title}"/>
                </GridView>
            </ListView.View>
        </ListView>
        <Button
            Content="Remove book"
            Height="25"
            Width="50"
            VerticalAlignment="Bottom"
            Margin="5"
            Command="{Binding Path=RemoveBookCommand}"/> 
    </Grid>
</Window>

All this seems to work, but only when you click on the delete button, the current book is really removed from the entity, but it still continues to be displayed in the window. How do I update the UI when the CurrentAuthor.Books navigation property changes?

PS: The example is completely abstract, but the code as a whole repeats the logic of my application, so if there are comments, I will be glad to hear.

Answer:

In order to use the proxy Collection<T> you still have to make adjustments to the model. Your book collection must implement the IList<Book> interface, like so:

public virtual List<Book> Books { get; set; } = new List<Book>();

We will not make any further changes to the model.

In order for your UI to receive notifications when the contents of the book collection change – deletion, insertion, etc., we need to implement the INotifyCollectionChanged interface. Its implementation is not trivial, but luckily there is (at least) one implementation example in the standard library, and that is the ObservableCollection<T> class: https://referencesource.microsoft.com/#system/compmod/system/collections/objectmodel/ observablecollection.cs
As you can see in the source code, the class inherits from the Collection<T> proxy collection, but, for unknown reasons, the developers have hidden the proxy functionality from us.
Well, I just copied the ObservableCollection<T> implementation and wrote my own instead of the existing constructors:

public class ProxyObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
    public ProxyObservableCollection() : base() { }

    public ProxyObservableCollection(IEnumerable<T> collection)
    {
        if (collection == null)
            throw new ArgumentNullException("collection");

        CopyFrom(collection);
    }

    public ProxyObservableCollection(IList<T> list) : base(list) { }

Of particular interest here is the last constructor – it is just (unlike the copy constructor) that creates a proxy for the passed IList<T> .

Now you can use it. Add to VM:

ProxyObservableCollection<Book> _booksOfCurrentAuthor;
public ProxyObservableCollection<Book> BooksOfCurrentAuthor
{
    get
    {
        return _booksOfCurrentAuthor;
    }
    private set
    {
        _booksOfCurrentAuthor = value;
        RaisePropertyChanged(nameof(BooksOfCurrentAuthor));
    }
}

Add to the CurrentAuthor property:

public Author CurrentAuthor
{
    get
    {
        ...
    }
    set
    {
        _author = value;
        BooksOfCurrentAuthor = new ProxyObservableCollection(CurrentAuthor.Books);
        RaisePropertyChanged("CurrentAuthor");
    }

Now a copy of the collection will not be created, but at the same time, all changes in the collection of books will notify the UI:

    <ListView
        ...
        ItemsSource="{Binding Path=BooksOfCurrentAuthor}"

Just in case, here is the full code ProxyObservableCollection<T> :

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;

namespace MyNamespace
{
    public class ProxyObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
    {
        public ProxyObservableCollection() : base() { }

        public ProxyObservableCollection(IEnumerable<T> collection)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");

            CopyFrom(collection);
        }

        public ProxyObservableCollection(IList<T> list) : base(list) { }

        private void CopyFrom(IEnumerable<T> collection)
        {
            IList<T> items = Items;
            if (collection != null && items != null)
            {
                using (IEnumerator<T> enumerator = collection.GetEnumerator())
                {
                    while (enumerator.MoveNext())
                    {
                        items.Add(enumerator.Current);
                    }
                }
            }
        }

        public void Move(int oldIndex, int newIndex)
        {
            MoveItem(oldIndex, newIndex);
        }

        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add
            {
                PropertyChanged += value;
            }
            remove
            {
                PropertyChanged -= value;
            }
        }

        public virtual event NotifyCollectionChangedEventHandler CollectionChanged;

        protected override void ClearItems()
        {
            CheckReentrancy();
            base.ClearItems();
            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnCollectionReset();
        }

        protected override void RemoveItem(int index)
        {
            CheckReentrancy();
            T removedItem = this[index];

            base.RemoveItem(index);

            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, index);
        }

        protected override void InsertItem(int index, T item)
        {
            CheckReentrancy();
            base.InsertItem(index, item);

            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
        }

        protected override void SetItem(int index, T item)
        {
            CheckReentrancy();
            T originalItem = this[index];
            base.SetItem(index, item);

            OnPropertyChanged(IndexerName);
            OnCollectionChanged(NotifyCollectionChangedAction.Replace, originalItem, item, index);
        }

        protected virtual void MoveItem(int oldIndex, int newIndex)
        {
            CheckReentrancy();

            T removedItem = this[oldIndex];

            base.RemoveItem(oldIndex);
            base.InsertItem(newIndex, removedItem);

            OnPropertyChanged(IndexerName);
            OnCollectionChanged(NotifyCollectionChangedAction.Move, removedItem, newIndex, oldIndex);
        }

        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
            => PropertyChanged?.Invoke(this, e);

        protected virtual event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (CollectionChanged != null)
            {
                using (BlockReentrancy())
                {
                    CollectionChanged(this, e);
                }
            }
        }

        protected IDisposable BlockReentrancy()
        {
            _monitor.Enter();
            return _monitor;
        }

        protected void CheckReentrancy()
        {
            if (_monitor.Busy)
            {
                if ((CollectionChanged != null) && (CollectionChanged.GetInvocationList().Length > 1))
                    throw new InvalidOperationException();
            }
        }

        private void OnPropertyChanged(string propertyName)
            => OnPropertyChanged(new PropertyChangedEventArgs(propertyName));

        private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index)
            => OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));

        private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index, int oldIndex)
            => OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index, oldIndex));

        private void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index)
            => OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));

        private void OnCollectionReset()
            => OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

        private class SimpleMonitor : IDisposable
        {
            public void Enter()
            {
                ++_busyCount;
            }

            public void Dispose()
            {
                --_busyCount;
            }

            public bool Busy { get { return _busyCount > 0; } }

            int _busyCount;
        }

        private const string CountString = "Count";

        private const string IndexerName = "Item[]";

        private SimpleMonitor _monitor = new SimpleMonitor();
    }
}
Scroll to Top