Best practice for synchronizing a changing List of the model to a ObservableList of the ViewModel?
Asked Answered
P

2

11

I have an (external) model exposing a List that constantly changes (let's say every two seconds or so). A ViewModel knows that list registering for PropertyChange events. That ViewModel also provides an ObservableCollection to the UI for data binding.

+-----------------------------------------------+
|                                           View|
| +-----------+                                 |
| |Listbox    |                                 |
| +-----------+                                 |
+-----/\----------------------------------------+
      ||
      ||DataBinding
      ||
      ||
+-----||----------------------------------------+
|     ||                               ViewModel|
| +--------------------+         +-------------+|
| |ObservableCollection|<--------|ChangeHandler||
| +--------------------+    /    +-------------+|
|                          /           ^        |
+-------------------------/------------|--------+
                         /             |
                        /              |
           Synchronizing Lists         | PropertyChanged
                                       |
                                       |
+--------------------------------------|--------+
|                                  +-----+ Model|
|                                  |IList|      |
|                                  +-----+      |
|                                               |
+-----------------------------------------------+

In principle that works well, besides the updates conducted constantly. With every update the user looses his selection, i.e. all items will be deselected with every update. This is no wonder, as WPF's ListBox "sees" that a new list was assigned.

So, the thing must be that we do not assign a new ObservableCollection, but merge the content of the current ObservableCollection with the updated Model.List

Now my questions

  • Synchronizing Lists - Are there best practices (or frameworks) on how to do such a merge (Copy new items to ObservableCollection, Deleting missing ones, updating altered items)
  • Selected Item - How can I assure that the ListBox keeps the currently selected item (besides the case that item was removed)
Postdiluvian answered 26/2, 2013 at 14:58 Comment(1)
Thanks. I'm a very visual person and need some pictures. Swiftly done with Jave5Postdiluvian
A
3

You can either generate a new ObservableCollection from the updated model list or sync the current ObservableCollection with the model's one.

If you go for the second, one thing you may want to avoid is to fire CollectionChanged events for every synced item. Take a look at this ObservableCollection implementation which has the hability to defer the notifications.

As for keeping the current SelectedItem, if the instance of the ObservableCollection isn't changed (what is true, because we are syncing the collections) and the SelectedItem instance isn't removed, the listbox should hold the selection. However, I'm not certain if that is true if the NotifyCollectionChangedEventArgs.Action is "Reset". If that's the case, you can use the approach I use, which is to have both a colllection property and a SelectedItem property in the ViewModel. You bind the ViewModel's SelectedItem to the ListBox.SelectedItem in TwoWay mode. When you synchronize the Collections, you save the SelectedItem in a temp variable and then reapply it, if it wasn't removed, after the synchronization.

Ayers answered 26/2, 2013 at 15:55 Comment(2)
OK, thank you. You confirmed my worst imaginations. There is no automatic merge (providing my own Comparer). So it is time to write my own MergeableCollection with deferred event raising ;-)Postdiluvian
I'm currently working on a solution similar to what you're doing with your collection and I've been using AutoMapper to map between domain model and view model. If you do that you can write a merge converter that will automatically merge incoming models in the collection into your ViewModel's collection. I've found it quite nice and easy to use thus far. This question is similar to what I'm referring toVocalism
T
0

Just found an solution by René Bergelt which exactly handles the problem:

https://www.renebergelt.de/blog/2019/08/synchronizing-a-model-list-with-a-view-model-list/

/// <summary>
/// An observable collection which automatically syncs to the underlying models collection
/// </summary>
public class SyncCollection<TViewModel, TModel> : ObservableCollection<TViewModel>
{
    IList<TModel> modelCollection;
    Func<TViewModel, TModel> modelExtractorFunc;

    /// <summary>
    /// Creates a new instance of SyncCollection
    /// </summary>
    /// <param name="modelCollection">The list of Models to sync to</param>
    /// <param name="viewModelCreatorFunc">Creates a new ViewModel instance for the given Model</param>
    /// <param name="modelExtractorFunc">Returns the model which is wrapped by the given ViewModel</param>
    public SyncCollection(IList<TModel> modelCollection, Func<TModel, TViewModel> viewModelCreatorFunc, Func<TViewModel, TModel> modelExtractorFunc)
    {
        if (modelCollection == null)
            throw new ArgumentNullException("modelCollection");
        if (viewModelCreatorFunc == null)
            throw new ArgumentNullException("vmCreatorFunc");
        if (modelExtractorFunc == null)
            throw new ArgumentNullException("modelExtractorFunc");

        this.modelCollection = modelCollection;
        this.modelExtractorFunc = modelExtractorFunc;

        // create ViewModels for all Model items in the modelCollection
        foreach (var model in modelCollection)
            Add(viewModelCreatorFunc(model));

        CollectionChanged += SyncCollection_CollectionChanged;
    }

    private void SyncCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // update the modelCollection accordingly

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                for (int i = 0; i < e.NewItems.Count; i++)
                    modelCollection.Insert(i + e.NewStartingIndex, modelExtractorFunc((TViewModel)e.NewItems[i]));
                break;
            case NotifyCollectionChangedAction.Remove:
                // NOTE: currently this ignores the index (works when there are no duplicates in the list)
                foreach (var vm in e.OldItems.OfType<TViewModel>())
                    modelCollection.Remove(modelExtractorFunc(vm));
                break;
            case NotifyCollectionChangedAction.Replace:
                throw new NotImplementedException();
            case NotifyCollectionChangedAction.Move:
                throw new NotImplementedException();
            case NotifyCollectionChangedAction.Reset:
                modelCollection.Clear();
                foreach (var viewModel in this)
                    modelCollection.Add(modelExtractorFunc(viewModel));
                break;
        }
    }
}

Usage

// models
class Person
{
    public string Name { get; set; }

    public string PhoneNumber { get; set; }
}

class Contacts
{
    List<Person> People { get; } = new List<Person>();
}

// corresponding view models
class PersonViewModel : ViewModelBase
{
    public Person Model { get; }
}

class ContactsViewModel : ViewModelBase
{
    ObservableCollection<PersonViewModel> People { get; }
}

To synchronize changes to the ObservableCollection back we use the CollectionChanged event, catch the Models using the provided function from the affected ViewModels and carry out the same actions to the wrapped model list. For our sample classes provided earlier, we could then us eit like this:

 List<Person> list = new List<Person>() { ... };
 ObservableCollection<PersonViewModel> collection = 
    new SyncCollection<PersonViewModel, Person>(
    list, 
        (pmodel) => new PersonViewModel(pmodel),
        (pvm) => pvm.Model);

 // now all changes to collection are carried through to the model list
 // e.g. adding a new ViewModel will add the corresponding Model in the wrapped list, etc.

SyncCollection handles Model and ViewModel adding/removing inside CollectionChanged handler.

Topcoat answered 11/2, 2023 at 13:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.