내용 보기

작성자

관리자 (IP : 172.17.0.1)

날짜

2020-07-10 02:57

제목

[WPF] DataGrid, ListView 컨트롤 스크롤링시 데이터 갱신 02


DataGrid, ListView 같은 Collection 타입의 데이터를 바인딩하여 표시 될때 인터페이스 IList의 인덱서를 통해 데이터를 표시 하게 되어 있다고 관련 글을 작성한 적이 있었다.

http://arooong.synology.me:5008/List/ContentsView/144

위 글과 비슷한 IList및 IList를 상속 받아 구현된 Collection타입 클래스를 이용하여 DataGrid 및 ListView 컨트롤에서 스크롤링 시 보이는 영역에서 부터 특정 인덱스 까지의 데이터를 비동기로 요청하여 표시 하는 클래스를 기술 할려고 한다.

로직은 이렇다.

  1. DataGrid및 ListView컨트롤에 보여질 모든 데이터의 Key를 받아 온다.
  2. 받아온 모든 데이터 Key만 DataGrid및 ListView컨트롤에 표시 한다.
    (바인딩 처리가 되어 있어 직접 구현한 Collection타입 클래스에 Key데이터 Add시 자동으로 표시.)
  3. DataGrid및 ListView컨트롤에서 화면에 보이는 영역의 IList 인덱서 요청
  4. 보이는 영역의 인덱스 부터 지정된 인덱스 까지의 데이터 요청 후 표시
    (예: 데이터 100건씩 요청시 현재 보이는 영역 인덱스가 10일 경우 10 ~ 110 만큼 데이터 요청)
    이미 요청이 완료된 데이터 일 경우 캐시된 데이터 바로 표시
  5. DataGrid및 ListView컨트롤에서 화면에 보이는 영역의 인덱스 만큼 [03번 ~ 04번 반복]

요약 하자면 화면에 보이는 영역의 데이터를 컨트롤이 요청할때 캐시에 요청이 완료된 데이터가 있다면 해당 인덱스에 맞는 데이터를 return하고 캐시에 요청된 데이터가 없는 경우 새로 지정된 개수만큼 데이터를 비동기로 요청하여 캐시에 보관 한다.만약 이미 로드된 데이터중에서 변경(삭제, 수정, 추가)이 일어 났을 경우 해당 컨트롤에게 변경 통보를 알려 주어야 하는데 이때는 INotifyCollectionChanged 인터페이스를 사용하여 리스트의 데이터가 어떻게 변경 되었는지 통보 할 수 있다.

INotifyCollectionChanged 맴버중 NotifyCollectionChangedEventHandler 이벤트가 있다.
이 이벤트는 NotifyCollectionChangedEventArgs 클래스를 파라메터로 두고 있어 NotifyCollectionChangedEventArgs 클래스의 NotifyCollectionChangedAction Enum 속성을 통해서 List의 데이터가 추가 되었는지 제거 되었는지 새로 로딩해야 할 것인지를 바인딩된 각 컨트롤에 변경 통보를 할 수 있다.

컨트롤은 NotifyCollectionChangedEventHandler 이벤트를 받으면 해당 Action에 맞게 다시 현재 보이는 영역의 데이터를 IList 인덱서를 통해 요청하게 된다.

다음은 위 로직에 따른 직접 구현한 Collection타입 클래스 코드다.

MirrorCollection.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Threading;
 
namespace IntegratedManagementConsole.Commons
{
    public enum SyncMode { Default, Sequential };
 
    /// 
    /// Data Grid, ListView 컨트롤 등에 바인딩 처리시 화면에 보이는 영역만큼 지정된 개수로 서버에 데이터를 요청하여 표시해 주는 Collection 클래스
    /// 
    /// 
    public class MirrorCollection<T> : INotifyCollectionChanged, IList, IList<T> where T : classnew()
    {
        public delegate void InsertKeysTaskEndEventHandler(object sender);
        public delegate void ResolveFetchDataTaskEventHandler<T>(object sender, ResolveTaskEventArgs<T> e);
        public delegate void DataLoadedEventHandler(object sender, DataLoadedEventArgs e);
 
        #region Event Fields
        /// 
        /// Insert Keys 작업이 완료 된 후 이벤트 발생
        /// 
        public event InsertKeysTaskEndEventHandler InsertKeysTaskEnd;
        /// 
        /// 데이터 요청 Task 이벤트 발생
        /// 
        public event ResolveFetchDataTaskEventHandler<T> ResolveFetchDataTask;
        /// 
        /// 데이터가 모두 로드 되었을때 이벤트 발생
        /// 
        public event DataLoadedEventHandler DataLoaded;
        #endregion
 
        #region Member Fields
        /// 
        /// 전체 데이터
        /// Key : 데이터 관리 Key
        /// Value : 데이터
        /// 
        private Dictionary<object, Wrapper> _innerStorage;
        /// 
        /// 전체 데이터의 Key List
        /// 
        private List<object> _innerKeyStorage;
        private int _maxConcurrentRequests;
        private int _batchAmount;
 
        private List<Wrapper> _pendingRequests;
        private List<Wrapper> _requestedObjects;
        private int _currentRequestsNumber;
        #endregion
 
        #region Constructor
        /// 
        /// MirrorCollection 인스턴스를 기본 값으로 초기화 합니다.
        /// 
        /// 데이터의 Key 속성 이름
        public MirrorCollection(string keyPropertyName)
        {
            KeyPropertyName = keyPropertyName;
            CollectionSyncMode = SyncMode.Default;
            BatchAmount = 10;
            UseBatchReplace = true;
            UseAsyncCollectionChanged = false;
            MaxConcurrentRequests = Environment.ProcessorCount;
 
            _innerStorage = new Dictionary<object, Wrapper>();
            _innerKeyStorage = new List<object>();
            _pendingRequests = new List<Wrapper>();
            _requestedObjects = new List<Wrapper>();
        }
 
        /// 
        /// MirrorCollection 인스턴스를 초기화 합니다.
        /// 
        /// 데이터의 Key 속성 이름
        /// 데이터 요청시 진행 모드
        public MirrorCollection(string keyPropertyName, SyncMode collectionSyncMode)
            : this(keyPropertyName)
        {
            CollectionSyncMode = collectionSyncMode;
        }
 
        /// 
        /// MirrorCollection 인스턴스를 초기화 합니다.
        /// 
        /// 데이터의 Key 속성 이름
        /// 데이터 요청시 진행 모드
        /// 한번 요청시 받아올 데이터 개수
        /// 최대 동시 요청할 수 있는 수
        public MirrorCollection(string keyPropertyName, SyncMode collectionSyncMode, int batchAmount, int maxConcurrentRequests)
            : this(keyPropertyName, collectionSyncMode)
        {
            BatchAmount = batchAmount;
            MaxConcurrentRequests = maxConcurrentRequests;
        }
        #endregion
 
        #region Properties
        /// 
        /// 데이터의 Key 속성 이름
        /// 
        public string KeyPropertyName { get; private set; }
        /// 
        /// 데이터 요청시 진행 모드
        /// 
        public SyncMode CollectionSyncMode { get; set; }
        public bool UseBatchReplace { get; set; }
        /// 
        /// 비동기로 데이터 리스트 변경 작업 반영 여부
        /// 
        public bool UseAsyncCollectionChanged { get; set; }
 
        /// 
        /// 한번 요청시 받아올 데이터 개수
        /// 
        public int BatchAmount
        {
            get { return _batchAmount; }
            set
            {
                _batchAmount = value;
                CheckBatchAmount();
            }
        }
 
        /// 
        /// 최대 동시 요청할 수 있는 수
        /// 
        public int MaxConcurrentRequests
        {
            get { return _maxConcurrentRequests; }
            set
            {
                _maxConcurrentRequests = value;
                CheckMaxConcurrentRequests();
            }
        }
        #endregion
 
        private void CheckBatchAmount()
        {
            if (BatchAmount < 1)
                BatchAmount = 1;
        }
 
        private void CheckMaxConcurrentRequests()
        {
            if (MaxConcurrentRequests == -1)
                return;
 
            if (MaxConcurrentRequests < 2)
                MaxConcurrentRequests = 2;
        }
 
        private void CheckKey(object key)
        {
            if (key == null || _innerStorage.ContainsKey(key))
                throw new NotSupportedException("Null and duplicate keys aren't supported");
        }
 
        private object GetItemKey(T item)
        {
            if (item == null)
                return null;
 
            return PropertyHelper<T>.GetPropertyValue(item, KeyPropertyName);
        }
 
        protected bool IsRequested(Wrapper wrapper)
        {
            return _pendingRequests.Contains(wrapper) || _requestedObjects.Contains(wrapper);
        }
 
        protected async void PerformBatchTaskAsync(Task<List<T>> batchTask)
        {
#if DEBUG
            System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch();
            st.Start();
#endif
 
            //System.Threading.Thread.Sleep(10);
            _currentRequestsNumber++;
            List<T> results = await batchTask;
            _currentRequestsNumber--;
 
            List<Wrapper> loadedWrappers = new List<Wrapper>();
            List<T> newDataObjects = new List<T>();
            List<T> oldDataObjects = new List<T>();
            int startingIndex = -1;
 
            foreach (T loadedItem in results)
            {
                Wrapper loadedItemWrapper = _innerStorage[GetItemKey(loadedItem)];
 
                if (startingIndex == -1 && UseBatchReplace)
                {
                    startingIndex = _innerKeyStorage.IndexOf(loadedItemWrapper.Key);
                }
                oldDataObjects.Add(loadedItemWrapper.DataObject);
                newDataObjects.Add(loadedItem);
 
                loadedItemWrapper.DataObject = loadedItem;
                loadedItemWrapper.IsLoaded = true;
 
                loadedWrappers.Add(loadedItemWrapper);
 
                if (!UseBatchReplace)
                {
                    int replaceIndex = _innerKeyStorage.IndexOf(loadedItemWrapper.Key);
                    this.RaiseCollectionChangedReplace(newDataObjects.Last(), oldDataObjects.Last(), replaceIndex);
                }
            }
 
            // 지정 인덱스 크기 만큼 데이터를 받아 왔다면 컨트롤에 데이터 리스트 변경 통보 이벤트 발생
            if (UseBatchReplace)
                this.RaiseCollectionChangedReplace(newDataObjects, oldDataObjects, startingIndex);
 
            this.CompleteBatchRequest(loadedWrappers);
 
#if DEBUG
            st.Stop();
            System.Diagnostics.Trace.WriteLine("PerformBatchTaskAsync : " + st.ElapsedMilliseconds);
            App.Log.Debug("PerformBatchTaskAsync : " + st.ElapsedMilliseconds);
#endif
        }
 
        protected void StartBatchRequest(Wrapper startWrapper)
        {
            int startIndex = _innerKeyStorage.IndexOf(startWrapper.Key);
            List<Wrapper> objectWrappersToRequest = new List<Wrapper>();
 
            for (int i = startIndex; i < Math.Min(startIndex + BatchAmount, _innerStorage.Count); i++)
            {
                Wrapper wrapper = _innerStorage[_innerKeyStorage[i]];
                if (!IsRequested(wrapper))
                {
                    objectWrappersToRequest.Add(wrapper);
                }
            }
 
            if (CollectionSyncMode == SyncMode.Sequential && _requestedObjects.Count > 0)
            {
                _pendingRequests.AddRange(objectWrappersToRequest);
                return;
            }
 
            if (CollectionSyncMode == SyncMode.Default && MaxConcurrentRequests != -1 && _currentRequestsNumber > MaxConcurrentRequests)
            {
                _pendingRequests.AddRange(objectWrappersToRequest);
                return;
            }
 
            ResolveTaskEventArgs<T> args = new ResolveTaskEventArgs<T>(objectWrappersToRequest.Select(w => w.Key).ToList());
            this.RaiseFetchDataTask(args);
 
            if (args.FetchDataTask != null)
            {
                _requestedObjects.AddRange(objectWrappersToRequest);
                this.PerformBatchTaskAsync(args.FetchDataTask);
 
                /*
                Task.Run(() =>
                {
                    try
                    {
                        this.PerformBatchTaskAsync(args.FetchDataTask);
                    }
                    catch (System.NotSupportedException notSupportedEx)
                    {
                        //
                    }
                    catch (Exception ex)
                    {
                        App.Log.Error(ex);
                    }
                });
                 * */
            }
        }
 
        protected void CompleteBatchRequest(IList<Wrapper> loadedObjectWrappers)
        {
            // _requestedObjects의 아이템을 조건에 맞는게 제거할 경우
            // loadedObjectWrappers 리스트에 포함 되어 있는지 체크하는 것 은
            // 대량의 데이터일 경우 CPU 오버헤드가 너무 크기 때문에 단순히 IsLoaded속성으로 체크 하도록 변경할 필요가 있다.  [2015. 10. 19 엄태영]
 
            // 대용량의 데이터를 표시 할 경우 아래 조건은 CPU 오버헤드가 높고 속도가 너무 느림.
            // 기본 데이터량 표시 용도 조건
            //_requestedObjects.RemoveAll(w => loadedObjectWrappers.Contains(w));
 
            // 대용량의 데이터를 표시 할 경우 아래 조건으로 비교
            _requestedObjects.RemoveAll(w => w.IsLoaded);
 
            this.RaiseDataLoaded(loadedObjectWrappers.Select(w => w.Key).ToList());
 
            if (_pendingRequests.Count > 0)
            {
                Wrapper firstPendingWrapper = _pendingRequests[0];
                for (int i = Math.Min(BatchAmount, _pendingRequests.Count) - 1; i >= 0; i--)
                    _pendingRequests.RemoveAt(i);
 
                this.StartBatchRequest(firstPendingWrapper);
            }
        }
 
        /// 
        /// Insert Keys 작업이 완료 된 후 이벤트 발생
        /// 
        /// 
        protected void RaiseInsertKeysTaskEnd()
        {
            if (InsertKeysTaskEnd != null)
                InsertKeysTaskEnd(this);
        }
 
        protected void RaiseDataLoaded(IList<object> loadedKeys)
        {
            if (DataLoaded != null)
                DataLoaded(thisnew DataLoadedEventArgs(loadedKeys));
        }
 
        /// 
        /// 데이터 요청 Task 이벤트 발생
        /// 
        /// 
        protected void RaiseFetchDataTask(ResolveTaskEventArgs<T> args)
        {
            if (ResolveFetchDataTask != null)
                ResolveFetchDataTask(this, args);
        }
 
        /// 
        /// 데이터의 전체 키 추가
        /// 
        /// 총 키 개수 (전체 데이터 개수)
        /// 데이터 카를 받아오는 메서드
        public void AddKeys(int count, Func<intobject> GetKeyValue)
        {
            this.InsertKeys(_innerKeyStorage.Count, count, GetKeyValue);
        }
 
        public void InsertKeys(int index, int count, Func<intobject> GetKeyValue)
        {
            Task.Factory.StartNew((arg) =>
            {
                Tuple<int, Func<intobject>int> argTuple = (Tuple<int, Func<intobject>int>)arg;
                int itemCount = argTuple.Item1;
                Func<intobject> getKeyValueFunc = argTuple.Item2;
                int insertIndex = argTuple.Item3;
 
                for (int i = 0; i < itemCount; i++)
                {
                    object key = getKeyValueFunc(i);
                    this.CheckKey(key);
 
                    Wrapper newObjectWrapper = new Wrapper(key, KeyPropertyName);
                    _innerStorage.Add(newObjectWrapper.Key, newObjectWrapper);
                    _innerKeyStorage.Insert(insertIndex + i, newObjectWrapper.Key);
 
                    this.RaiseCollectionChangedAdd(null, insertIndex + i);
                }
            }, Tuple.Create(count, GetKeyValue, index))
            .ContinueWith((tsk) =>
            {
                if (tsk.IsCompleted)
                {
                    this.RaiseInsertKeysTaskEnd();
                }
            });
        }
 
        public void Refresh(T item, bool isLoaded)
        {
            Wrapper wrapper = _innerStorage[GetItemKey(item)];
            if (wrapper != null)
            {
                wrapper.IsLoaded = isLoaded;
                RaiseCollectionChangedReplace(item, item, _innerKeyStorage.IndexOf(wrapper.Key));
            }
        }
        
        #region INotifyCollectionChanged Implementation
        public virtual event NotifyCollectionChangedEventHandler CollectionChanged;
 
        private async void RaiseCollectionChangedAsync(NotifyCollectionChangedEventArgs args)
        {
            Dispatcher dispatcher = null;
            if (Application.Current != null)
                dispatcher = Application.Current.Dispatcher;
 
            if (dispatcher != null && !dispatcher.CheckAccess())
            {
                await Task.Factory.FromAsync<object, NotifyCollectionChangedEventArgs>(CollectionChanged.BeginInvoke, CollectionChanged.EndInvoke, this, args, new object());
            }
            else
            {
                await dispatcher.BeginInvoke(new Action(() =>
                {
                    Task.Factory.FromAsync<object, NotifyCollectionChangedEventArgs>(CollectionChanged.BeginInvoke, CollectionChanged.EndInvoke, this, args, new object());
                }));
            }
        }
 
        protected virtual void RaiseCollectionChanged(NotifyCollectionChangedEventArgs args)
        {
            try
            {
                if (CollectionChanged == null)
                    return;
 
                Dispatcher dispatcher = null;
                if (Application.Current != null)
                    dispatcher = Application.Current.Dispatcher;
 
                if (dispatcher != null && !dispatcher.CheckAccess())
                {
                    Action<object, NotifyCollectionChangedEventArgs> dispatcherAction = new Action<object, NotifyCollectionChangedEventArgs>((sender, e) => { CollectionChanged(sender, e); });
                    if (UseAsyncCollectionChanged)
                    {
                        dispatcher.BeginInvoke(dispatcherAction, this, args);
                    }
                    else
                    {
                        dispatcher.Invoke(dispatcherAction, this, args);
                    }
 
                    return;
                }
 
                if (UseAsyncCollectionChanged)
                {
                    this.RaiseCollectionChangedAsync(args);
                }
                else
                {
                    foreach (NotifyCollectionChangedEventHandler handler in CollectionChanged.GetInvocationList())
                    {
                        // 2015. 10. 15 엄태영
                        // NotifyCollectionChangedEventHandler를 참조한 개체가 CollectionView 타입일 경우 Refresh() 메서드를 호출해 준다.
                        // CollectionView 개체는 NotifyCollectionChangedEventHandler 이벤트를 지원하지 않아 NotSupportedException가 발생된다.
                        if (handler.Target is CollectionView && handler.Target != null)
                        {
                            ((CollectionView)handler.Target).Refresh();
                        }
                        else
                        {
                            handler(this, args);
                        }
                    }
 
                    //this.CollectionChanged(this, args);
                }
            }
            catch (NotSupportedException notSupportedEx)
            {
                App.Log.Write("MirrorCollection NotSupportedException 오류 발생!");
                App.Log.Error(notSupportedEx);
            }
            catch (Exception ex)
            {
                App.Log.Error(ex);
            }
        }
 
        protected void RaiseCollectionChangedAdd(object newItem, int itemIndex)
        {
            this.RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItem, itemIndex));
        }
 
        protected void RaiseCollectionChangedRemove(object oldItem, int itemIndex)
        {
            this.RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItem, itemIndex));
        }
 
        protected void RaiseCollectionChangedReplace(object newItem, object oldItem, int itemIndex)
        {
            this.RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, itemIndex));
        }
 
        protected void RaiseCollectionChangedReplace(IList newItems, IList oldItems, int startingIndex)
        {
            this.RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItems, oldItems, startingIndex));
        }
 
        protected void RaiseCollectionChangedReset()
        {
            this.RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
        #endregion
 
        #region IList<T> Implementation
        public int IndexOf(T item)
        {
            object key = GetItemKey(item);
            if (key == null)
                return -1;
 
            return _innerKeyStorage.IndexOf(key);
        }
 
        public void Insert(int index, T item)
        {
            if (item == null)
                return;
 
            object key = GetItemKey(item);
            this.CheckKey(key);
 
            Wrapper wrapper = new Wrapper(key, KeyPropertyName);
            wrapper.DataObject = item;
            wrapper.IsLoaded = true;
 
            _innerKeyStorage.Insert(index, key);
            _innerStorage.Add(key, wrapper);
 
            this.RaiseCollectionChangedAdd(wrapper.DataObject, index);
        }
 
        public void RemoveAt(int index)
        {
            object key = _innerKeyStorage[index];
            T item = _innerStorage[key].DataObject;
            _innerStorage.Remove(key);
            _innerKeyStorage.RemoveAt(index);
 
            this.RaiseCollectionChangedRemove(item, index);
        }
 
        public T this[int index]
        {
            get
            {
                // DataGrid 또는 ListView 컨트롤에서 화면에 보이는 영역의 데이터를 표시 할때
                // ※ 바인딩 된 컨트롤에서 IList의 IList.this[int index]인덱서를 자동 호출하게 된다.
                Wrapper wrapper = _innerStorage[_innerKeyStorage[index]];
                // 데이터가 로드 되지 않은 데이터 일 경우 데이터 요청
                if (!wrapper.IsLoaded && !this.IsRequested(wrapper))
                    StartBatchRequest(wrapper);
 
                return wrapper.DataObject;
            }
            set
            {
                Wrapper wrapper = _innerStorage[_innerKeyStorage[index]];
                wrapper.Key = GetItemKey(value);
                wrapper.IsLoaded = true;
 
                if (this.IsRequested(wrapper))
                {
                    _requestedObjects.Remove(wrapper);
                    _pendingRequests.Remove(wrapper);
                }
                wrapper.DataObject = value;
            }
        }
        #endregion
 
        #region ICollection<T> Implementation
        public void Add(T item)
        {
            this.Insert(_innerKeyStorage.Count, item);
        }
 
        public void Clear()
        {
            _innerStorage.Clear();
            _innerKeyStorage.Clear();
 
            this.RaiseCollectionChangedReset();
        }
 
        public bool Contains(T item)
        {
            object key = GetItemKey(item);
            if (key == null)
                return false;
 
            return _innerStorage.ContainsKey(key);
        }
 
        public void CopyTo(T[] array, int arrayIndex)
        {
            _innerStorage.Select(kvp => kvp.Value.DataObject).ToList().CopyTo(array, arrayIndex);
        }
 
        public bool Remove(T item)
        {
            int itemIndex = IndexOf(item);
            if (itemIndex == -1)
                return false;
 
            this.RemoveAt(itemIndex);
            return true;
        }
 
        public int Count
        {
            get { return _innerStorage.Count; }
        }
 
        public bool IsReadOnly
        {
            get { return false; }
        }
        #endregion
 
        #region IList Implementation
        int IList.Add(object value)
        {
            this.Add((T)value);
            return Count - 1;
        }
 
        void IList.Clear()
        {
            this.Clear();
        }
 
        bool IList.Contains(object value)
        {
            return this.Contains((T)value);
        }
 
        int IList.IndexOf(object value)
        {
            return IndexOf((T)value);
        }
 
        void IList.Insert(int index, object value)
        {
            this.Insert(index, (T)value);
        }
 
        bool IList.IsFixedSize
        {
            get { return false; }
        }
 
        bool IList.IsReadOnly
        {
            get { return false; }
        }
 
        void IList.Remove(object value)
        {
            this.Remove((T)value);
        }
 
        void IList.RemoveAt(int index)
        {
            this.RemoveAt(index);
        }
 
        object IList.this[int index]
        {
            get { return this[index]; }
            set { this[index] = (T)value; }
        }
        #endregion
 
        #region ICollection Implementation
        void ICollection.CopyTo(Array array, int index)
        {
            ((ICollection)_innerStorage.Select(kvp => kvp.Value.DataObject).ToList()).CopyTo(array, index);
        }
 
        int ICollection.Count
        {
            get { return Count; }
        }
 
        bool ICollection.IsSynchronized
        {
            get { return ((ICollection)_innerKeyStorage).IsSynchronized; }
        }
 
        object ICollection.SyncRoot
        {
            get { return ((ICollection)_innerKeyStorage).SyncRoot; }
        }
        #endregion
 
        #region IEnumerable<T> Members
        public IEnumerator<T> GetEnumerator()
        {
            return new MirrorCollectionEnumerator(this);
        }
        #endregion
 
        #region IEnumerable Implementation
        IEnumerator IEnumerable.GetEnumerator()
        {
            return new MirrorCollectionEnumerator(this);
        }
        #endregion
 
 
        protected class Wrapper
        {
            private T _dataObject;
            private string _keyPropertyName;
 
            public Wrapper(object key, string keyPropertyName)
            {
                Key = key;
                this._keyPropertyName = keyPropertyName;
            }
 
            public object Key { get; set; }
            public bool IsLoaded { get; set; }
 
            public T DataObject
            {
                get
                {
                    if (_dataObject == null)
                    {
                        //_dataObject = Activator.CreateInstance();
                        _dataObject = new T();
                        PropertyHelper<T>.SetPropertyValue(_dataObject, _keyPropertyName, Key);
                    }
                    return _dataObject;
                }
                set { _dataObject = value; }
            }
 
        }
 
        protected class MirrorCollectionEnumerator : IEnumerator<T>, IEnumerator
        {
            private MirrorCollection<T> _owner;
            private int _index;
            private T _current;
 
            public MirrorCollectionEnumerator(MirrorCollection<T> owner)
            {
                _owner = owner;
                _index = -1;
                _current = default(T);
            }
 
            #region IEnumerator<T> Implementation
            public T Current
            {
                get { return _current; }
            }
            #endregion
 
            #region IEnumerator Implementation
            object IEnumerator.Current
            {
                get { return _current; }
            }
 
            public bool MoveNext()
            {
                _index++;
                if (_index < _owner.Count)
                {
                    _current = _owner[_index];
                    return true;
                }
 
                _current = default(T);
                return false;
            }
 
            public void Reset()
            {
                _index = -1;
                _current = default(T);
            }
            #endregion
 
            #region IDisposable Implementation
            public void Dispose()
            {
                _current = default(T);
                _owner = null;
            }
            #endregion
        }
    }
 
    public class ResolveTaskEventArgs<T> : EventArgs
    {
        public ResolveTaskEventArgs(IList<object> keyValues)
        {
            Keys = keyValues;
        }
 
        public IList<object> Keys { get; private set; }
        public Task<List<T>> FetchDataTask { get; set; }
    }
 
    public class DataLoadedEventArgs : EventArgs
    {
        public DataLoadedEventArgs(IList<object> keyValues)
        {
            Keys = keyValues;
        }
 
        public IList<object> Keys { get; private set; }
    }
 
    public static class PropertyHelper<T> where T : class
    {
        private static Type _lastType;
        private static PropertyInfo _propertyInfoCache;
 
        private static void CheckPropertyInfoCache(string propertyName)
        {
            if (_lastType != typeof(T) || _propertyInfoCache == null || _propertyInfoCache.Name != propertyName)
            {
                _propertyInfoCache = typeof(T).GetRuntimeProperty(propertyName);
                _lastType = typeof(T);
            }
        }
 
        public static object GetPropertyValue(T dataObject, string propertyName)
        {
            CheckPropertyInfoCache(propertyName);
 
            if (_propertyInfoCache == null)
                return default(T);
 
            return _propertyInfoCache.GetValue(dataObject);
        }
 
        public static void SetPropertyValue(T dataObject, string propertyName, object value)
        {
            CheckPropertyInfoCache(propertyName);
 
            if (_propertyInfoCache != null)
                _propertyInfoCache.SetValue(dataObject, value);
        }
    }
}
cs

위 MirrorCollection 사용

PolicyTemplateModel.cs

using IntegratedManagementConsole.Commons;
using System;
using System.ComponentModel;
 
namespace IntegratedManagementConsole.Windows.TestWindow
{
    public class PolicyTemplateModel : ModelBase
    {
        private int _userNum;
        private string _userID;
        private string _userName;
        private bool _userChecked;
 
        public PolicyTemplateModel()
        {
            base.AddProperty<int, PolicyTemplateModel>("UserNum");
            base.AddProperty<string, PolicyTemplateModel>("UserID");
            base.AddProperty<string, PolicyTemplateModel>("UserName");
            base.AddProperty<bool, PolicyTemplateModel>("UserChecked");
        }
 
        public int UserNum
        {
            get { return _userNum; }
            set
            {
                _userNum = value;
                base.SetPropertyValue<int>("UserNum", value);
            }
        }
 
        public string UserID
        {
            get { return _userID; }
            set
            {
                _userID = value;
                base.SetPropertyValue<string>("UserID", value);
            }
        }
 
        public string UserName
        {
            get { return _userName; }
            set
            {
                _userName = value;
                base.SetPropertyValue<string>("UserName", value);
            }
        }
 
        public bool UserChecked
        {
            get { return _userChecked; }
            set
            {
                _userChecked = value;
                base.SetPropertyValue<bool>("UserChecked", value);
            }
        }
    }
}
cs

TestWindow.xaml

<ListView x:Name="xList1" Grid.Row="0" Grid.Column="0" Util:GridViewSort.AutoSort="True" ItemSource="{Binding MC}" ScrollViewer.IsDeferredScrollingEnabled="True">
                <ListView.ItemContainerStyle>
                    <Style BasedOn="{StaticResource sListViewItem}" TargetType="ListViewItem">
                        
                    Style>
                ListView.ItemContainerStyle>
                <ListView.View>
                    <GridView>
                        <GridViewColumn Width="100" Header="UserNum">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding UserNum}"/>
                                DataTemplate>
                            GridViewColumn.CellTemplate>
                        GridViewColumn>
                        <GridViewColumn Width="100" Header="UserID">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding UserID}"/>
                                DataTemplate>
                            GridViewColumn.CellTemplate>
                        GridViewColumn>
                        <GridViewColumn Width="110" Header="UserName">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding UserName}"/>
                                DataTemplate>
                            GridViewColumn.CellTemplate>
                        GridViewColumn>
 
                        <GridViewColumn Width="110" Header="UserChecked">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <CheckBox IsChecked="{Binding UserChecked}" />
                                DataTemplate>
                            GridViewColumn.CellTemplate>
                        GridViewColumn>
                    GridView>
                ListView.View>
ListView>
cs

TestWindowViewModel.cs

#region Member Fields
private MirrorCollection<PolicyTemplateModel> _mc;
#endregion  // Member Fields
 
#region Constructor
public TestWindowViewModel()
{
            if (DesignerProperties.GetIsInDesignMode(new DependencyObject())) return;
 
            ManagersCaller<CDTManager>.Instance.CreateTargetSystem("192.168.1.38");
            this.CreateBigDataInfo(100);
}
#endregion  // Constructor
 
#region Properties
public MirrorCollection<PolicyTemplateModel> MC
{
            get { return _mc; }
            set
            {
                _mc = value;
                this.OnPropertyChanged("MC");
            }
}
#endregion  // Properties
 
/// <summary>
/// MirrorCollection 초기화
/// summary>
/// <param name="batchAmount">한번에 데이터를 요청할 개수(컨트롤의 보여지는 영역에서 부터 요청할 개수)param>
private void CreateBigDataInfo(int batchAmount)
{
            // 01. 모든 키 요청
            List<Int64> allKeyList = ManagersCaller<CDTManager>.Instance.GetGetPolicyTemplateIDList();
 
            MirrorCollection<PolicyTemplateModel> mc = new MirrorCollection<PolicyTemplateModel>("UserNum", SyncMode.Default, batchAmount, 20);
            mc.InsertKeysTaskEnd += this.mc_InsertKeysTaskEnd;
            // 전체 데이터의 개수와 데이터 Key 추가
            mc.AddKeys(allKeyList.Count, (index) => allKeyList[index]);
            mc.ResolveFetchDataTask += this.OnDaysInfoResolveFetchDataTask;
            mc.UseAsyncCollectionChanged = false;
}
 
/// <summary>
/// 모든 키 추가 작업 완료 이벤트 핸들러
/// summary>
/// <param name="sender">param>
private void mc_InsertKeysTaskEnd(object sender)
{
            // 모든 키 추가가 완료된 후 ListView컨트롤에 바인딩 데이터 표시
            MirrorCollection<PolicyTemplateModel> mc = sender as MirrorCollection<PolicyTemplateModel>;
            if (mc != null)
                MC = mc;
}
 
/// <summary>
/// 데이터 요청 Task 이벤트 핸들러
/// summary>
/// <param name="sender">param>
/// <param name="e">param>
private void OnDaysInfoResolveFetchDataTask(object sender, ResolveTaskEventArgs<PolicyTemplateModel> e)
{
            FuncPolicyTemplateModel>> action = delegate()
            {
                return this.GetData(e.Keys);
            };
 
            TaskPolicyTemplateModel>> asyncGetData = TaskPolicyTemplateModel>>.Factory.StartNew(action);
            e.FetchDataTask = asyncGetData;
}
 
/// <summary>
/// 정해진 데이터의 키 만큼 데이터 요청
/// summary>
/// <param name="requestKeyList">param>
/// <returns>returns>
private List<PolicyTemplateModel> GetData(IList<object> requestKeyList)
{
            //System.Threading.Thread.Sleep(5000);
 
            // 02. 해당 키 범위 만큼 데이터 요청
            List<PolicyTemplateModel> SPolicyTemplateList = ManagersCaller<CDTManager>.Instance.GetPolicyTemplateListByIDRange(requestKeyList.Cast<int>().Min(), requestKeyList.Cast<int>().Max());
 
            return SPolicyTemplateList;
}
cs

위 처럼 직접 구현한 MirrorCollection 클래스를 DatasGrid나 ListView컨트롤에 바인딩 하여 화면에 보이는 영역만 데이터를 비동기적으로 호출하여 표시 할 수 있다.

ListView에 바인딩 처리한 속성 초기화를 InsertKeysTaskEnd 이벤트를 통해 처리 하는 것을 볼 수 있다.
왜 굳이 이벤트를 통해 바인딩된 속성을 초기화 하는 이유는 다음과 같다.

public event InsertKeysTaskEndEventHandler 이벤트는 MirrorCollection 에 모든 데이터의 Key가 추가 완료 된 경우 이벤트가 발생 된다.

데이터 Key는 public void AddKeys(int count, Func<int, object> GetKeyValue) 메서드를 통해서 처리 되는데
void AddKeys 메서드를 보면 Key가 추가 될때 마다 NotifyCollectionChangedEventHandler 이벤트를 Add 액션으로 발생시키고 있다.

이 작업들이 모두 비동기로 처리 되고 있으며, 비동기로 처리가 되다 보니 동시에 ListView 컨트롤에 ItemSource가 바인딩 되어 있기 때문에 IList의 인덱서를 요청하게 된다.
이때 ListView 컨트롤이 호출하는 IList의 인덱서 보다 먼저 NotifyCollectionChangedEventHandler 이벤트가 발생 되다 보니 간혈적으로 InvalidOperationException Exception이 발생하게 된다. (바인딩할 데이터의 양이 많다면 100% 발생한다.)
그렇기 때문에 일단 모든 데이터 Key 추가 작업이 완료된 후에 바인딩 처리를 해주는 것 이다.

출처1

출처2