Наилучшая практика для синхронизации изменяющегося списка модели с ObservableList ViewModel?

У меня есть (внешняя) модель, выставляющая список, который постоянно меняется (скажем, каждые две секунды или около того). ViewModel знает, что список регистрируется для событий PropertyChange. Этот ViewModel также предоставляет ObservableCollection для пользовательского интерфейса для привязки данных.

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

В принципе это хорошо работает, кроме того, обновления проводятся постоянно. С каждым обновлением пользователь теряет свой выбор, то есть все элементы будут отменены при каждом обновлении. Это неудивительно, так как ListBox WPF "видит", что был назначен новый список.

Таким образом, дело в том, что мы не назначаем новую коллекцию ObservableCollection, а объединяем содержимое текущей коллекции ObservableCollection с обновленным Model.List

Теперь мои вопросы

  • Синхронизация списков. Существуют ли передовые практики (или структуры) по выполнению такого слияния (копирование новых элементов в ObservableCollection, удаление отсутствующих, обновление измененных элементов)
  • Выбранный элемент - Как я могу убедиться, что ListBox сохраняет текущий выбранный элемент (кроме случая, когда этот элемент был удален)

2 ответа

Решение

Вы можете сгенерировать новую коллекцию ObservableCollection из обновленного списка моделей или синхронизировать текущую коллекцию ObservableCollection с коллекцией модели.

Если вы перейдете ко второму, вы можете избежать одной вещи - запускать события CollectionChanged для каждого синхронизируемого элемента. Взгляните на эту реализацию ObservableCollection, которая может отложить уведомления.

Что касается сохранения текущего SelectedItem, если экземпляр ObservableCollection не изменен (что верно, потому что мы синхронизируем коллекции), а экземпляр SelectedItem не удален, список должен содержать выбор. Однако я не уверен, что это правда, если NotifyCollectionChangedEventArgs.Action имеет значение "Сброс". Если это так, вы можете использовать подход, который я использую, который должен иметь как свойство colllection, так и свойство SelectedItem в ViewModel. Вы привязываете SelectedItem ViewModel к ListBox.SelectedItem в двухстороннем режиме. Когда вы синхронизируете Коллекции, вы сохраняете SelectedItem во временной переменной, а затем повторно применяете его, если он не был удален, после синхронизации.

Только что нашел решение Рене Бергельта, которое точно решает проблему:

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;
        }
    }
}

Применение

      // 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; }
}

Чтобы синхронизировать изменения в ObservableCollection обратно, мы используем событие CollectionChanged, перехватываем модели с помощью предоставленной функции из затронутых ViewModels и выполняем те же действия со списком обернутых моделей. Для наших примеров классов, предоставленных ранее, мы могли бы использовать это следующим образом:

       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ручкиModelиViewModelдобавление/удаление внутри обработчика CollectionChanged.

Другие вопросы по тегам