programing

MVVM 동기 컬렉션

fastcode 2023. 4. 15. 09:33
반응형

MVVM 동기 컬렉션

C# 및 WPF에서 Model 객체의 컬렉션과 일치하는 Model View 객체의 컬렉션을 동기화하는 표준화된 방법이 있습니까?저는 다음 두 컬렉션을 동기화할 수 있는 클래스를 찾고 있습니다. 사과가 몇 개만 있고 모든 것을 기억할 수 있다고 가정하면요.

Apple 컬렉션에 Apple을 추가할 경우 Apple Model View를 Apple Model Views 컬렉션에 추가하고 싶습니다.각 컬렉션의 Collection Changed 이벤트를 들으면서 나만의 글을 쓸 수 있었습니다.이것은 저보다 똑똑한 사람이 그것을 하기 위한 "올바른 방법"을 정의한 일반적인 시나리오처럼 보입니다.

public class BasketModel
{
    public ObservableCollection<Apple> Apples { get; }
}

public class BasketModelView
{
    public ObservableCollection<AppleModelView> AppleModelViews { get; }
}

나는 느슨하게 구성된 자동 업데이트 컬렉션을 사용한다.

public class BasketModelView
{
    private readonly Lazy<ObservableCollection<AppleModelView>> _appleViews;

    public BasketModelView(BasketModel basket)
    {
        Func<AppleModel, AppleModelView> viewModelCreator = model => new AppleModelView(model);
        Func<ObservableCollection<AppleModelView>> collectionCreator =
            () => new ObservableViewModelCollection<AppleModelView, AppleModel>(basket.Apples, viewModelCreator);

        _appleViews = new Lazy<ObservableCollection<AppleModelView>>(collectionCreator);
    }

    public ObservableCollection<AppleModelView> Apples
    {
        get
        {
            return _appleViews.Value;
        }
    }
}

사용법ObservableViewModelCollection<TViewModel, TModel>:

namespace Client.UI
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.Diagnostics.Contracts;
    using System.Linq;

    public class ObservableViewModelCollection<TViewModel, TModel> : ObservableCollection<TViewModel>
    {
        private readonly ObservableCollection<TModel> _source;
        private readonly Func<TModel, TViewModel> _viewModelFactory;

        public ObservableViewModelCollection(ObservableCollection<TModel> source, Func<TModel, TViewModel> viewModelFactory)
            : base(source.Select(model => viewModelFactory(model)))
        {
            Contract.Requires(source != null);
            Contract.Requires(viewModelFactory != null);

            this._source = source;
            this._viewModelFactory = viewModelFactory;
            this._source.CollectionChanged += OnSourceCollectionChanged;
        }

        protected virtual TViewModel CreateViewModel(TModel model)
        {
            return _viewModelFactory(model);
        }

        private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
            case NotifyCollectionChangedAction.Add:
                for (int i = 0; i < e.NewItems.Count; i++)
                {
                    this.Insert(e.NewStartingIndex + i, CreateViewModel((TModel)e.NewItems[i]));
                }
                break;

            case NotifyCollectionChangedAction.Move:
                if (e.OldItems.Count == 1)
                {
                    this.Move(e.OldStartingIndex, e.NewStartingIndex);
                }
                else
                {
                    List<TViewModel> items = this.Skip(e.OldStartingIndex).Take(e.OldItems.Count).ToList();
                    for (int i = 0; i < e.OldItems.Count; i++)
                        this.RemoveAt(e.OldStartingIndex);

                    for (int i = 0; i < items.Count; i++)
                        this.Insert(e.NewStartingIndex + i, items[i]);
                }
                break;

            case NotifyCollectionChangedAction.Remove:
                for (int i = 0; i < e.OldItems.Count; i++)
                    this.RemoveAt(e.OldStartingIndex);
                break;

            case NotifyCollectionChangedAction.Replace:
                // remove
                for (int i = 0; i < e.OldItems.Count; i++)
                    this.RemoveAt(e.OldStartingIndex);

                // add
                goto case NotifyCollectionChangedAction.Add;

            case NotifyCollectionChangedAction.Reset:
                Clear();
                for (int i = 0; i < e.NewItems.Count; i++)
                    this.Add(CreateViewModel((TModel)e.NewItems[i]));
                break;

            default:
                break;
            }
        }
    }
}

고객님의 요구 사항을 정확히 이해하지 못할 수도 있지만, 비슷한 상황에 대처한 방법은 ObservableCollection에서 CollectionChanged 이벤트를 사용하고 필요에 따라 뷰 모델을 작성/파기하는 것입니다.

void OnApplesCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{    
  // Only add/remove items if already populated. 
  if (!IsPopulated)
    return;

  Apple apple;

  switch (e.Action)
  {
    case NotifyCollectionChangedAction.Add:
      apple = e.NewItems[0] as Apple;
      if (apple != null)
        AddViewModel(asset);
      break;
    case NotifyCollectionChangedAction.Remove:
      apple = e.OldItems[0] as Apple;
      if (apple != null)
        RemoveViewModel(apple);
      break;
  }

}

ListView에서 많은 항목을 추가하거나 제거할 때 성능 문제가 발생할 수 있습니다.

이 문제를 해결하려면 Observable Collection을 Add Range, Remove Range, BinaryInsert 메서드로 확장하고 수집이 변경되었음을 알리는 이벤트를 추가합니다.확장 Collection View와 함께컬렉션이 변경되면 일시적으로 소스의 연결이 끊어지는 소스는 정상적으로 작동합니다.

HTH,

데니스

우선, 저는 이것을 할 수 있는 하나의 "올바른 방법"이 있다고 생각하지 않습니다.그것은 전적으로 당신의 신청에 달려있다.올바른 방법은 더 많고 덜 올바른 방법은 더 많다.

그렇다고는 해도, 왜 이 컬렉션을 「일치」로 할 필요가 있는지 의문입니다.어떤 시나리오로 인해 동기화되지 않을 수 있습니까?M-V-VM에 관한 Josh Smith의 MSDN 기사에 있는 샘플 코드를 보면 모델이 생성될 때마다 View Models와 동기화되는 경우가 대부분입니다.다음과 같이 합니다.

void CreateNewCustomer()
{
    Customer newCustomer = Customer.CreateNewCustomer();
    CustomerViewModel workspace = new CustomerViewModel(newCustomer, _customerRepository);
    this.Workspaces.Add(workspace);
    this.SetActiveWorkspace(workspace);
}

궁금한 게 있는데, 어떤 이유로 이 파일을 만들 수 없게 되었나요?AppleModelView every every every every every every를 만들 때마다Apple당신의 질문을 오해하지 않았다면, 그것이 이 컬렉션을 "동기화"시키는 가장 쉬운 방법인 것 같습니다.

예(및 설명)는 http://blog.lexique-du-net.com/index.php?post/2010/03/02/M-V-VM-How-to-keep-collections-of-ViewModel-and-Model-in-sync에서도 찾을 수 있습니다.

도움이 되었으면 좋겠다

네, 저는 이 답변에 열광하고 있습니다.그래서 저는 CTor 주입을 지원하기 위해 추가한 추상적인 팩토리를 공유해야 했습니다.

using System;
using System.Collections.ObjectModel;

namespace MVVM
{
    public class ObservableVMCollectionFactory<TModel, TViewModel>
        : IVMCollectionFactory<TModel, TViewModel>
        where TModel : class
        where TViewModel : class
    {
        private readonly IVMFactory<TModel, TViewModel> _factory;

        public ObservableVMCollectionFactory( IVMFactory<TModel, TViewModel> factory )
        {
            this._factory = factory.CheckForNull();
        }

        public ObservableCollection<TViewModel> CreateVMCollectionFrom( ObservableCollection<TModel> models )
        {
            Func<TModel, TViewModel> viewModelCreator = model => this._factory.CreateVMFrom(model);
            return new ObservableVMCollection<TViewModel, TModel>(models, viewModelCreator);
        }
    }
}

이를 바탕으로 구축되는 것은 다음과 같습니다.

using System.Collections.ObjectModel;

namespace MVVM
{
    public interface IVMCollectionFactory<TModel, TViewModel>
        where TModel : class
        where TViewModel : class
    {
        ObservableCollection<TViewModel> CreateVMCollectionFrom( ObservableCollection<TModel> models );
    }
}

그리고 이것은:

namespace MVVM
{
    public interface IVMFactory<TModel, TViewModel>
    {
        TViewModel CreateVMFrom( TModel model );
    }
}

다음으로 완전성을 확인하기 위한 늘체커를 나타냅니다.

namespace System
{
    public static class Exceptions
    {
        /// <summary>
        /// Checks for null.
        /// </summary>
        /// <param name="thing">The thing.</param>
        /// <param name="message">The message.</param>
        public static T CheckForNull<T>( this T thing, string message )
        {
            if ( thing == null ) throw new NullReferenceException(message);
            return thing;
        }

        /// <summary>
        /// Checks for null.
        /// </summary>
        /// <param name="thing">The thing.</param>
        public static T CheckForNull<T>( this T thing )
        {
            if ( thing == null ) throw new NullReferenceException();
            return thing;
        }
    }
}

Sam Harwell의 솔루션은 이미 상당히 우수하지만, 두 가지 문제가 있습니다.

  1. this._source.CollectionChanged += OnSourceCollectionChanged은 없습니다. 대대등등등 is is is is is is is is 。this._source.CollectionChanged -= OnSourceCollectionChanged가 없습니다.
  2. 가 에 된 뷰 의 이벤트에 viewModelFactory을 준비할 수 ).생성된 뷰 모델을 "파괴"용으로 준비할 수 없습니다.)

따라서 Sam Harwell 접근법의 단점을 모두 수정하는 솔루션을 제안합니다.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics.Contracts;
using System.Linq;

namespace Helpers
{
    public class ObservableViewModelCollection<TViewModel, TModel> : ObservableCollection<TViewModel>
    {
        private readonly Func<TModel, TViewModel> _viewModelFactory;
        private readonly Action<TViewModel> _viewModelRemoveHandler;
        private ObservableCollection<TModel> _source;

        public ObservableViewModelCollection(Func<TModel, TViewModel> viewModelFactory, Action<TViewModel> viewModelRemoveHandler = null)
        {
            Contract.Requires(viewModelFactory != null);

            _viewModelFactory = viewModelFactory;
            _viewModelRemoveHandler = viewModelRemoveHandler;
        }

        public ObservableCollection<TModel> Source
        {
            get { return _source; }
            set
            {
                if (_source == value)
                    return;

                this.ClearWithHandling();

                if (_source != null)
                    _source.CollectionChanged -= OnSourceCollectionChanged;

                _source = value;

                if (_source != null)
                {
                    foreach (var model in _source)
                    {
                        this.Add(CreateViewModel(model));
                    }
                    _source.CollectionChanged += OnSourceCollectionChanged;
                }
            }
        }

        private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    for (int i = 0; i < e.NewItems.Count; i++)
                    {
                        this.Insert(e.NewStartingIndex + i, CreateViewModel((TModel)e.NewItems[i]));
                    }
                    break;

                case NotifyCollectionChangedAction.Move:
                    if (e.OldItems.Count == 1)
                    {
                        this.Move(e.OldStartingIndex, e.NewStartingIndex);
                    }
                    else
                    {
                        List<TViewModel> items = this.Skip(e.OldStartingIndex).Take(e.OldItems.Count).ToList();
                        for (int i = 0; i < e.OldItems.Count; i++)
                            this.RemoveAt(e.OldStartingIndex);

                        for (int i = 0; i < items.Count; i++)
                            this.Insert(e.NewStartingIndex + i, items[i]);
                    }
                    break;

                case NotifyCollectionChangedAction.Remove:
                    for (int i = 0; i < e.OldItems.Count; i++)
                        this.RemoveAtWithHandling(e.OldStartingIndex);
                    break;

                case NotifyCollectionChangedAction.Replace:
                    // remove
                    for (int i = 0; i < e.OldItems.Count; i++)
                        this.RemoveAtWithHandling(e.OldStartingIndex);

                    // add
                    goto case NotifyCollectionChangedAction.Add;

                case NotifyCollectionChangedAction.Reset:
                    this.ClearWithHandling();
                    if (e.NewItems == null)
                        break;
                    for (int i = 0; i < e.NewItems.Count; i++)
                        this.Add(CreateViewModel((TModel)e.NewItems[i]));
                    break;

                default:
                    break;
            }
        }

        private void RemoveAtWithHandling(int index)
        {
            _viewModelRemoveHandler?.Invoke(this[index]);
            this.RemoveAt(index);
        }

        private void ClearWithHandling()
        {
            if (_viewModelRemoveHandler != null)
            {
                foreach (var item in this)
                {
                    _viewModelRemoveHandler(item);
                }
            }

            this.Clear();
        }

        private TViewModel CreateViewModel(TModel model)
        {
            return _viewModelFactory(model);
        }
    }
}

두 중 첫 하려면 단순히 ' 쪽으로'를 설정하면 .SourceCollectionChanged이벤트 핸들러

중 두 문제를 '어느 정도'를 됩니다.viewModelRemoveHandler예를 들어 오브젝트에 연결된 이벤트 핸들러를 제거하여 "파괴를 위해 오브젝트를 삭제"할 수 있습니다.

MVVM을 사용하여 실행 취소/재설정을 제공합니다. 제2부: 리스트의 표시모듈링' 기사에서는MirrorCollection<V, D>뷰 모델 및 모델 컬렉션 동기화를 실현하기 위한 클래스입니다.

기타 참고 자료

  1. 원래 링크(현재는 사용할 수 없습니다):변경 알림 blog블로그 아카이브 using MVVM을 사용하여 실행 취소/재설정을 제공합니다. 제2부: 리스트의 표시 모델링

View Model에 있는 비즈니스 오브젝트 컬렉션을 래핑하기 위한 도우미 클래스를 작성했습니다.

280Z28의 솔루션이 정말 마음에 들어요.한마디만 할게요.Notify Collection Changed Action마다 루프를 실행해야 합니까?액션의 문서에는 "하나 이상의 항목"이 기재되어 있는 것은 알고 있습니다만, Observable Collection 자체는 범위 추가 및 삭제를 지원하지 않기 때문에 이러한 일은 결코 일어나지 않을 것이라고 생각합니다.

수집을 기본값으로 리셋하거나 목표값과 일치시키는 것은 자주 경험하는 일입니다.

나는 Miscilanious 메서드에 대한 작은 도우미 클래스를 썼다.

public static class Misc
    {
        public static void SyncCollection<TCol,TEnum>(ICollection<TCol> collection,IEnumerable<TEnum> source, Func<TCol,TEnum,bool> comparer, Func<TEnum, TCol> converter )
        {
            var missing = collection.Where(c => !source.Any(s => comparer(c, s))).ToArray();
            var added = source.Where(s => !collection.Any(c => comparer(c, s))).ToArray();

            foreach (var item in missing)
            {
                collection.Remove(item);
            }
            foreach (var item in added)
            {
                collection.Add(converter(item));
            }
        }
        public static void SyncCollection<T>(ICollection<T> collection, IEnumerable<T> source, EqualityComparer<T> comparer)
        {
            var missing = collection.Where(c=>!source.Any(s=>comparer.Equals(c,s))).ToArray();
            var added = source.Where(s => !collection.Any(c => comparer.Equals(c, s))).ToArray();

            foreach (var item in missing)
            {
                collection.Remove(item);
            }
            foreach (var item in added)
            {
                collection.Add(item);
            }
        }
        public static void SyncCollection<T>(ICollection<T> collection, IEnumerable<T> source)
        {
            SyncCollection(collection,source, EqualityComparer<T>.Default);
        }
    }

제 요구의 대부분을 커버하고 있습니다.첫 번째는 아마도 당신의 변환 타입으로 가장 적합할 것입니다.

주의: 컬렉션 내의 요소만 동기화하며 내부 값은 동기화하지 않습니다.

이것은 Sam Harwell의 대답의 약간의 변형입니다.IReadOnlyCollection<>그리고.INotifyCollectionChanged유전하는 대신ObservableCollection<>직접적으로.이로 인해 소비자가 컬렉션을 수정할 수 없게 됩니다.이 시나리오에서는 일반적으로 이 컬렉션을 수정하는 것은 바람직하지 않습니다.

또한 이 구현에서는 소스 컬렉션이 미러링된 컬렉션과 동시에 폐기되지 않은 경우 메모리 누수를 방지하기 위해 이벤트핸들러를 소스 컬렉션에 부가합니다.

/// <summary>
/// A collection that mirrors an <see cref="ObservableCollection{T}"/> source collection 
/// with a transform function to create it's own elements.
/// </summary>
/// <typeparam name="TSource">The type of elements in the source collection.</typeparam>
/// <typeparam name="TDest">The type of elements in this collection.</typeparam>
public class MappedObservableCollection<TSource, TDest>
    : IReadOnlyCollection<TDest>, INotifyCollectionChanged
{
    /// <inheritdoc/>
    public int Count => _mappedCollection.Count;

    /// <inheritdoc/>
    public event NotifyCollectionChangedEventHandler CollectionChanged {
        add { _mappedCollection.CollectionChanged += value; }
        remove { _mappedCollection.CollectionChanged -= value; }
    }

    private readonly Func<TSource, TDest> _elementMapper;
    private readonly ObservableCollection<TDest> _mappedCollection;

    /// <summary>
    /// Initializes a new instance of the <see cref="MappedObservableCollection{TSource, TDest}"/> class.
    /// </summary>
    /// <param name="sourceCollection">The source collection whose elements should be mapped into this collection.</param>
    /// <param name="elementMapper">Function to map elements from the source collection to this collection.</param>
    public MappedObservableCollection(ObservableCollection<TSource> sourceCollection, Func<TSource, TDest> elementMapper)
    {
        if (sourceCollection == null) throw new ArgumentNullException(nameof(sourceCollection));
        _mappedCollection = new ObservableCollection<TDest>(sourceCollection.Select(elementMapper));

        _elementMapper = elementMapper ?? throw new ArgumentNullException(nameof(elementMapper));

        // Update the mapped collection whenever the source collection changes
        // NOTE: Use the weak event pattern here to avoid a memory leak
        // See: https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/weak-event-patterns
        CollectionChangedEventManager.AddHandler(sourceCollection, OnSourceCollectionChanged);
    }

    /// <inheritdoc/>
    IEnumerator<TDest> IEnumerable<TDest>.GetEnumerator()
        => _mappedCollection.GetEnumerator();

    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator()
        => _mappedCollection.GetEnumerator();

    /// <summary>
    /// Mirror a change event in the source collection into the internal mapped collection.
    /// </summary>
    private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action) {
            case NotifyCollectionChangedAction.Add:
                InsertItems(e.NewItems, e.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Remove:
                RemoveItems(e.OldItems, e.OldStartingIndex);
                break;
            case NotifyCollectionChangedAction.Replace:
                RemoveItems(e.OldItems, e.OldStartingIndex);
                InsertItems(e.NewItems, e.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Reset:
                _mappedCollection.Clear();
                InsertItems(e.NewItems, 0);
                break;
            case NotifyCollectionChangedAction.Move:
                if (e.OldItems.Count == 1) {
                    _mappedCollection.Move(e.OldStartingIndex, e.NewStartingIndex);
                } else {
                    RemoveItems(e.OldItems, e.OldStartingIndex);

                    var movedItems = _mappedCollection.Skip(e.OldStartingIndex).Take(e.OldItems.Count).GetEnumerator();
                    for (int i = 0; i < e.OldItems.Count; i++) {
                        _mappedCollection.Insert(e.NewStartingIndex + i, movedItems.Current);
                        movedItems.MoveNext();
                    }
                }

                break;
        }
    }

    private void InsertItems(IList newItems, int newStartingIndex)
    {
        for (int i = 0; i < newItems.Count; i++)
            _mappedCollection.Insert(newStartingIndex + i, _elementMapper((TSource)newItems[i]));
    }

    private void RemoveItems(IList oldItems, int oldStartingIndex)
    {
        for (int i = 0; i < oldItems.Count; i++)
            _mappedCollection.RemoveAt(oldStartingIndex);
    }
}

언급URL : https://stackoverflow.com/questions/1256793/mvvm-sync-collections

반응형