첨부 실행 코드는 나눔고딕코딩 폰트를 사용합니다.
728x90
반응형
728x170

■ 정렬과 필터링 가능한 데이터 가상화를 사용하는 방법을 보여준다.

TestSolution.zip
0.03MB

[TestLibrary 프로젝트]

 

▶ DataWrapper.cs

using System.ComponentModel;

namespace TestLibrary
{
    /// <summary>
    /// 데이터 래퍼
    /// </summary>
    /// <typeparam name="TItem">항목 타입</typeparam>
    public class DataWrapper<TItem> : INotifyPropertyChanged where TItem : class
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 속성 변경시 - 속성 변경시

        /// <summary>
        /// 속성 변경시
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 인덱스
        /// </summary>
        private int index;

        /// <summary>
        /// 데이터
        /// </summary>
        private TItem item;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 인덱스 - Index

        /// <summary>
        /// 인덱스
        /// </summary>
        public int Index
        {
            get
            {
                return this.index;
            }
        }

        #endregion
        #region 항목 번호 - ItemNumber

        /// <summary>
        /// 항목 번호
        /// </summary>
        public int ItemNumber
        {
            get
            {
                return this.index + 1;
            }
        }

        #endregion
        #region 로딩 여부 - IsLoading

        /// <summary>
        /// 로딩 여부
        /// </summary>
        public bool IsLoading
        {
            get
            {
                return this.item == null;
            }
        }

        #endregion
        #region 항목 - Item

        /// <summary>
        /// 항목
        /// </summary>
        public TItem Item
        {
            get
            {
                return this.item;
            }
            internal set
            {
                this.item = value;

                this.FirePropertyChangedEvent("Item");
                this.FirePropertyChangedEvent("IsLoading");
            }
        }

        #endregion
        #region 사용 여부 - InUse

        /// <summary>
        /// 사용 여부
        /// </summary>
        public bool InUse
        {
            get
            {
                return PropertyChanged != null;
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - DataWrapper(index)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="index">인덱스</param>
        public DataWrapper(int index)
        {
            this.index = index;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 속성 변경시 이벤트 발생시키기 - FirePropertyChangedEvent(propertyName)

        /// <summary>
        /// 속성 변경시 이벤트 발생시키기
        /// </summary>
        /// <param name="propertyName">속성명</param>
        private void FirePropertyChangedEvent(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }
}

 

▶ DataPage.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace TestLibrary
{
    /// <summary>
    /// 데이터 페이지
    /// </summary>
    /// <typeparam name="TItem">항목 타입</typeparam>
    public class DataPage<TItem> where TItem : class
    {
        #region 항목 리스트 - ItemList

        /// <summary>
        /// 항목 리스트
        /// </summary>
        public IList<DataWrapper<TItem>> ItemList { get; private set; }

        #endregion
        #region 최근 사용 시간 - LastUsedTime

        /// <summary>
        /// 최근 사용 시간
        /// </summary>
        public DateTime LastUsedTime { get; set; }

        #endregion
        #region 사용 여부 - InUse

        /// <summary>
        /// 사용 여부
        /// </summary>
        public bool InUse
        {
            get
            {
                return ItemList.Any(wrapper => wrapper.InUse);
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - DataPage(firstIndex, pageLength)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="firstIndex">첫번째 인덱스</param>
        /// <param name="pageLength">페이지 길이</param>
        public DataPage(int firstIndex, int pageLength)
        {
            ItemList = new List<DataWrapper<TItem>>(pageLength);

            for(int i = 0; i < pageLength; i++)
            {
                ItemList.Add(new DataWrapper<TItem>(firstIndex + i));
            }

            LastUsedTime = DateTime.Now;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 만들기 - Populate(itemList)

        /// <summary>
        /// 만들기
        /// </summary>
        /// <param name="itemList">항목 리스트</param>
        public void Populate(IList<TItem> itemList)
        {
            int index = 0;

            int i;

            for(i = 0; i < itemList.Count && i < ItemList.Count; i++)
            {
                ItemList[i].Item = itemList[i];

                index = ItemList[i].Index;
            }

            while(i < itemList.Count)
            {
                index++;

                ItemList.Add
                (
                    new DataWrapper<TItem>(index)
                    {
                        Item = itemList[i]
                    }
                );

                i++;
            }

            while(i < ItemList.Count)
            {
                ItemList.RemoveAt(ItemList.Count - 1);
            }
        }

        #endregion
    }
}

 

▶ IItemProvider.cs

using System.Collections.Generic;

namespace TestLibrary
{
    /// <summary>
    /// 항목 공급자 인터페이스
    /// </summary>
    /// <typeparam name="TItem">항목 타입</typeparam>
    public interface IItemProvider<TItem>
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method

        #region 카운트 가져오기 - FetchCount()

        /// <summary>
        /// 카운트 가져오기
        /// </summary>
        /// <returns>항목 수</returns>
        /// <remarks>이용 가능한 전체 항목 수를 가져온다.</remarks>
        int FetchCount();

        #endregion
        #region 범위 가져오기 - FetchRange(startIndex, pageCount, overallCount)

        /// <summary>
        /// 범위 가져오기
        /// </summary>
        /// <param name="startIndex">시작 인덱스</param>
        /// <param name="pageCount">페이지 수</param>
        /// <param name="overallCount">전체 수</param>
        /// <returns>항목 리스트</returns>
        IList<TItem> FetchRange(int startIndex, int pageCount, out int overallCount);

        #endregion
    }
}

 

▶ VirtualizingCollection.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;

namespace TestLibrary
{
    /// <summary>
    /// 가상화 컬렉션
    /// </summary>
    /// <typeparam name="TItem">항목 타입</typeparam>
    public class VirtualizingCollection<TItem> : IList<DataWrapper<TItem>>, IList where TItem : class
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 항목 공급자
        /// </summary>
        private readonly IItemProvider<TItem> itemProvider;

        /// <summary>
        /// 페이지 크기
        /// </summary>
        private readonly int pageSize = 100;

        /// <summary>
        /// 페이지 타임아웃
        /// </summary>
        private readonly long pageTimeout = 10000;

        /// <summary>
        /// 카운트
        /// </summary>
        private int count = -1;

        /// <summary>
        /// 데이터 페이지 딕셔너리
        /// </summary>
        private Dictionary<int, DataPage<TItem>> pageDictionary = new Dictionary<int, DataPage<TItem>>();

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 항목 공급자 - ItemProvider


        /// <summary>
        /// 항목 공급자
        /// </summary>
        public IItemProvider<TItem> ItemProvider
        {
            get
            {
                return this.itemProvider;
            }
        }

        #endregion
        #region 페이지 크기 - PageSize

        /// <summary>
        /// 페이지 크기
        /// </summary>
        public int PageSize
        {
            get
            {
                return this.pageSize;
            }
        }

        #endregion
        #region 페이지 타임아웃 - PageTimeout

        /// <summary>
        /// 페이지 타임아웃
        /// </summary>
        public long PageTimeout
        {
            get
            {
                return this.pageTimeout;
            }
        }

        #endregion

        #region 카운트 - Count

        /// <summary>
        /// 카운트
        /// </summary>
        public int Count
        {
            get
            {
                if(this.count == -1)
                {
                    this.count = 0;

                    LoadCount();
                }

                return this.count;
            }
            protected set
            {
                this.count = value;
            }
        }

        #endregion
        #region 인덱서 - this[index]

        /// <summary>
        /// 인덱서
        /// </summary>
        /// <param name="index">인덱스</param>
        /// <returns>데이터 래퍼</returns>
        public DataWrapper<TItem> this[int index]
        {
            get
            {
                int pageIndex  = index / PageSize;
                int pageOffset = index % PageSize;

                RequestPage(pageIndex);

                if(pageOffset > PageSize / 2 && pageIndex < Count / PageSize)
                {
                    RequestPage(pageIndex + 1);
                }

                if(pageOffset < PageSize / 2 && pageIndex > 0)
                {
                    RequestPage(pageIndex - 1);
                }

                CleanUpPageDictionary();

                return this.pageDictionary[pageIndex].ItemList[pageOffset];
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        #endregion
        #region 인덱서 - IList.this[index]

        /// <summary>
        /// 인덱서
        /// </summary>
        /// <param name="index">인덱스</param>
        /// <returns>객체</returns>
        object IList.this[int index]
        {
            get
            {
                return this[index];
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        #endregion

        #region 동기화 루트 - SyncRoot

        /// <summary>
        /// 동기화 루트
        /// </summary>
        public object SyncRoot
        {
            get
            {
                return this;
            }
        }

        #endregion
        #region 동기화 여부 - IsSynchronized

        /// <summary>
        /// 동기화 여부
        /// </summary>
        public bool IsSynchronized
        {
            get
            {
                return false;
            }
        }

        #endregion
        #region 읽기 전용 여부 - IsReadOnly

        /// <summary>
        /// 읽기 전용 여부
        /// </summary>
        public bool IsReadOnly
        {
            get
            {
                return true;
            }
        }

        #endregion
        #region 고정 크기 여부 - IsFixedSize

        /// <summary>
        /// 고정 크기 여부
        /// </summary>
        public bool IsFixedSize
        {
            get
            {
                return false;
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - VirtualizingCollection(itemProvider, pageSize, pageTimeout)

        /// <summary>
        ///  생성자
        /// </summary>
        /// <param name="itemProvider">항목 공급자</param>
        /// <param name="pageSize">페이지 크기</param>
        /// <param name="pageTimeout">페이지 타임아웃</param>
        public VirtualizingCollection(IItemProvider<TItem> itemProvider, int pageSize, int pageTimeout)
        {
            this.itemProvider = itemProvider;
            this.pageSize     = pageSize;
            this.pageTimeout  = pageTimeout;
        }

        #endregion
        #region 생성자 - VirtualizingCollection(itemProvider, pageSize)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="itemProvider">항목 공급자</param>
        /// <param name="pageSize">페이지 크기</param>
        public VirtualizingCollection(IItemProvider<TItem> itemProvider, int pageSize)
        {
            this.itemProvider = itemProvider;
            this.pageSize     = pageSize;
        }

        #endregion
        #region 생성자 - VirtualizingCollection(itemProvider)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="itemProvider">항목 공급자</param>
        public VirtualizingCollection(IItemProvider<TItem> itemProvider)
        {
            this.itemProvider = itemProvider;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 열거자 구하기 - GetEnumerator()

        /// <summary>
        /// 열거자 구하기
        /// </summary>
        /// <returns>데이터 래퍼 열거자</returns>
        public IEnumerator<DataWrapper<TItem>> GetEnumerator()
        {
            for(int i = 0; i < Count; i++)
            {
                yield return this[i];
            }
        }

        #endregion
        #region 열거자 구하기 - IEnumerable.GetEnumerator()

        /// <summary>
        /// 열거자 구하기
        /// </summary>
        /// <returns>열거자</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion
        #region 추가하기 - Add(wrapper)

        /// <summary>
        /// 추가하기
        /// </summary>
        /// <param name="wrapper">데이터 래퍼</param>
        public void Add(DataWrapper<TItem> wrapper)
        {
            throw new NotSupportedException();
        }

        #endregion
        #region 추가하기 - IList.Add(wrapper)

        /// <summary>
        /// 추가하기
        /// </summary>
        /// <param name="wrapper">데이터 래퍼</param>
        /// <returns>인덱스</returns>
        int IList.Add(object wrapper)
        {
            throw new NotSupportedException();
        }

        #endregion
        #region 포함 여부 구하기 - Contains(wrapper)

        /// <summary>
        /// 포함 여부 구하기
        /// </summary>
        /// <param name="wrapper">데이터 래퍼</param>
        /// <returns>포함 여부</returns>
        public bool Contains(DataWrapper<TItem> wrapper)
        {
            foreach(DataPage<TItem> page in this.pageDictionary.Values)
            {
                if(page.ItemList.Contains(wrapper))
                {
                    return true;
                }
            }

            return false;
        }

        #endregion
        #region 포함 여부 구하기 - IList.Contains(wrapper)

        /// <summary>
        /// 포함 여부 구하기
        /// </summary>
        /// <param name="wrapper">데이터 래퍼</param>
        /// <returns>포함 여부</returns>
        bool IList.Contains(object wrapper)
        {
            return Contains((DataWrapper<TItem>)wrapper);
        }

        #endregion
        #region 지우기 - Clear()

        /// <summary>
        /// 지우기
        /// </summary>
        public void Clear()
        {
            throw new NotSupportedException();
        }

        #endregion
        #region 인덱스 구하기 - IndexOf(wrapper)

        /// <summary>
        /// 인덱스 구하기
        /// </summary>
        /// <param name="wrapper">데이터 래퍼</param>
        /// <returns>인덱스</returns>
        public int IndexOf(DataWrapper<TItem> wrapper)
        {
            foreach(KeyValuePair<int, DataPage<TItem>> keyValuePair in this.pageDictionary)
            {
                int indexWithinPage = keyValuePair.Value.ItemList.IndexOf(wrapper);

                if(indexWithinPage != -1)
                {
                    return PageSize * keyValuePair.Key + indexWithinPage;
                }
            }

            return -1;
        }

        #endregion
        #region 인덱스 구하기 - IList.IndexOf(wrapper)

        /// <summary>
        /// 인덱스 구하기
        /// </summary>
        /// <param name="wrapper">데이터 래퍼</param>
        /// <returns>인덱스</returns>
        int IList.IndexOf(object wrapper)
        {
            return IndexOf((DataWrapper<TItem>)wrapper);
        }

        #endregion
        #region 삽입하기 - Insert(index, wrapper)

        /// <summary>
        /// 삽입하기
        /// </summary>
        /// <param name="index">인덱스</param>
        /// <param name="wrapper">데이터 래퍼</param>
        public void Insert(int index, DataWrapper<TItem> wrapper)
        {
            throw new NotSupportedException();
        }

        #endregion
        #region 삽입하기 - IList.Insert(index, wrapper)

        /// <summary>
        /// 삽입하기
        /// </summary>
        /// <param name="index">인덱스</param>
        /// <param name="wrapper">데이터 래퍼</param>
        void IList.Insert(int index, object wrapper)
        {
            Insert(index, (DataWrapper<TItem>)wrapper);
        }

        #endregion
        #region 제거하기 - RemoveAt(index)

        /// <summary>
        /// 제거하기
        /// </summary>
        /// <param name="index">인덱스</param>
        public void RemoveAt(int index)
        {
            throw new NotSupportedException();
        }

        #endregion
        #region 제거하기 - Remove(wrapper)

        /// <summary>
        /// 제거하기
        /// </summary>
        /// <param name="wrapper">데이터 래퍼</param>
        /// <returns>처리 결과</returns>
        public bool Remove(DataWrapper<TItem> wrapper)
        {
            throw new NotSupportedException();
        }

        #endregion
        #region 제거하기 - IList.Remove(wrapper)

        /// <summary>
        /// 제거하기
        /// </summary>
        /// <param name="wrapper">데이터 래퍼</param>
        void IList.Remove(object wrapper)
        {
            throw new NotSupportedException();
        }

        #endregion
        #region 복사하기 - CopyTo(itemArray, arrayIndex)

        /// <summary>
        /// 복사하기
        /// </summary>
        /// <param name="itemArray">항목 배열</param>
        /// <param name="arrayIndex">배열 인덱스</param>
        public void CopyTo(DataWrapper<TItem>[] itemArray, int arrayIndex)
        {
            throw new NotSupportedException();
        }

        #endregion
        #region 복사하기 - ICollection.CopyTo(array, index)

        /// <summary>
        /// 복사하기
        /// </summary>
        /// <param name="array">배열</param>
        /// <param name="index">인덱스</param>
        void ICollection.CopyTo(Array array, int index)
        {
            throw new NotSupportedException();
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 카운트 가져오기 - FetchCount()

        /// <summary>
        /// 카운트 가져오기
        /// </summary>
        /// <returns>카운트</returns>
        protected int FetchCount()
        {
            return ItemProvider.FetchCount();
        }

        #endregion
        #region 카운트 로드하기 - LoadCount()

        /// <summary>
        /// 카운트 로드하기
        /// </summary>
        protected virtual void LoadCount()
        {
            Count = FetchCount();
        }

        #endregion
        #region 페이지 만들기 - PopulatePage(pageIndex, itemList)

        /// <summary>
        /// 페이지 만들기
        /// </summary>
        /// <param name="pageIndex">페이지 인덱스</param>
        /// <param name="itemList">항목 리스트</param>
        protected virtual void PopulatePage(int pageIndex, IList<TItem> itemList)
        {
            DataPage<TItem> page;

            if(this.pageDictionary.TryGetValue(pageIndex, out page))
            {
                page.Populate(itemList);
            }
        }

        #endregion
        #region 페이지 가져오기 - FetchPage(pageIndex, pageLength, count)

        /// <summary>
        /// 페이지 가져오기
        /// </summary>
        /// <param name="pageIndex">페이지 인덱스</param>
        /// <param name="pageLength">페이지 길이</param>
        /// <param name="count">카운트</param>
        /// <returns>원본 항목 리스트</returns>
        protected IList<TItem> FetchPage(int pageIndex, int pageLength, out int count)
        {
            return ItemProvider.FetchRange(pageIndex * PageSize, pageLength, out count);
        }

        #endregion
        #region 페이지 로드하기 - LoadPage(pageIndex, pageLength)

        /// <summary>
        /// 페이지 로드하기
        /// </summary>
        /// <param name="pageIndex">페이지 인덱스</param>
        /// <param name="pageLength">페이지 길이</param>
        protected virtual void LoadPage(int pageIndex, int pageLength)
        {
            int count = 0;

            PopulatePage(pageIndex, FetchPage(pageIndex, pageLength, out count));

            Count = count;
        }

        #endregion
        #region 페이지 요청하기 - RequestPage(pageIndex)

        /// <summary>
        /// 페이지 요청하기
        /// </summary>
        /// <param name="pageIndex">페이지 인덱스</param>
        protected virtual void RequestPage(int pageIndex)
        {
            if(!this.pageDictionary.ContainsKey(pageIndex))
            {
                int pageLength = Math.Min(this.PageSize, this.Count - pageIndex * this.PageSize);

                DataPage<TItem> dataPage = new DataPage<TItem>(pageIndex * this.PageSize, pageLength);

                this.pageDictionary.Add(pageIndex, dataPage);

                LoadPage(pageIndex, pageLength);
            }
            else
            {
                this.pageDictionary[pageIndex].LastUsedTime = DateTime.Now;
            }
        }

        #endregion
        #region 캐시 비우기 - EmptyCache()

        /// <summary>
        /// 캐시 비우기
        /// </summary>
        protected void EmptyCache()
        {
            this.pageDictionary = new Dictionary<int, DataPage<TItem>>();
        }

        #endregion
        #region 페이지 딕셔너리 정리하기 - CleanUpPageDictionary()

        /// <summary>
        /// 페이지 딕셔너리 정리하기
        /// </summary>
        private void CleanUpPageDictionary()
        {
            int[] keyArray = this.pageDictionary.Keys.ToArray();

            foreach(int key in keyArray)
            {
                if(key != 0 && (DateTime.Now - this.pageDictionary[key].LastUsedTime).TotalMilliseconds > PageTimeout)
                {
                    bool removePage = true;

                    DataPage<TItem> page;

                    if(this.pageDictionary.TryGetValue(key, out page))
                    {
                        removePage = !page.InUse;
                    }

                    if(removePage)
                    {
                        this.pageDictionary.Remove(key);
                    }
                }
            }
        }

        #endregion
    }
}

 

▶ AsyncVirtualizingCollection.cs

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

namespace TestLibrary
{
    /// <summary>
    /// 비동기 가상화 컬렉션
    /// </summary>
    /// <typeparam name="TItem">항목 타입</typeparam>
    public class AsyncVirtualizingCollection<TItem> : VirtualizingCollection<TItem>, INotifyCollectionChanged, INotifyPropertyChanged where TItem : class
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 컬렉션 변경시 - CollectionChanged

        /// <summary>
        /// 컬렉션 변경시
        /// </summary>
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        #endregion
        #region 속성 변경시 - PropertyChanged

        /// <summary>
        /// 속성 변경시
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 동기화 컨텍스트
        /// </summary>
        private readonly SynchronizationContext synchronizationContext;

        /// <summary>
        /// 로딩 여부
        /// </summary>
        private bool isLoading;

        /// <summary>
        /// 초기화 여부
        /// </summary>
        private bool isInitializing;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 동기화 컨텍스트 - SynchronizationContext

        /// <summary>
        /// 동기화 컨텍스트
        /// </summary>
        protected SynchronizationContext SynchronizationContext
        {
            get
            {
                return this.synchronizationContext;
            }
        }

        #endregion
        #region 로딩 여부 - IsLoading

        /// <summary>
        /// 로딩 여부
        /// </summary>
        public bool IsLoading
        {
            get
            {
                return this.isLoading;
            }
            set
            {
                if(value != this.isLoading)
                {
                    this.isLoading = value;

                    FirePropertyChangedEvent("IsLoading");
                }
            }
        }

        #endregion
        #region 초기화 여부 - IsInitializing

        /// <summary>
        /// 초기화 여부
        /// </summary>
        public bool IsInitializing
        {
            get
            {
                return this.isInitializing;
            }
            set
            {
                if(value != this.isInitializing)
                {
                    this.isInitializing = value;

                    FirePropertyChangedEvent("IsInitializing");
                }
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - AsyncVirtualizingCollection(itemProvider, pageSize, pageTimeout)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="itemProvider">항목 공급자</param>
        /// <param name="pageSize">페이지 크기</param>
        /// <param name="pageTimeout">페이지 타임아웃</param>
        public AsyncVirtualizingCollection(IItemProvider<TItem> itemProvider, int pageSize, int pageTimeout) : base(itemProvider, pageSize, pageTimeout)
        {
            this.synchronizationContext = SynchronizationContext.Current;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 컬렉션 변경시 이벤트 발생시키기 - FireCollectionChangedEvent(e)

        /// <summary>
        /// 컬렉션 변경시 이벤트 발생시키기
        /// </summary>
        /// <param name="e">이벤트 인자</param>
        protected virtual void FireCollectionChangedEvent(NotifyCollectionChangedEventArgs e)
        {
            CollectionChanged?.Invoke(this, e);
        }

        #endregion
        #region 속성 변경시 이벤트 발생시키기 - FirePropertyChangedEvent(e)

        /// <summary>
        /// 속성 변경시 이벤트 발생시키기
        /// </summary>
        /// <param name="e">이벤트 인자</param>
        protected virtual void FirePropertyChangedEvent(PropertyChangedEventArgs e)
        {
            PropertyChanged?.Invoke(this, e);
        }

        #endregion

        #region 카운트 로드하기 - LoadCount()

        /// <summary>
        /// 카운트 로드하기
        /// </summary>
        protected override void LoadCount()
        {
            if(Count == 0)
            {
                IsInitializing = true;
            }

            ThreadPool.QueueUserWorkItem(LoadCount);
        }

        #endregion
        #region 카운트 로드 완료시 처리하기 - ProcessLoadCountCompleted(argument)

        /// <summary>
        /// 카운트 로드 완료시 처리하기
        /// </summary>
        /// <param name="argument">인자</param>
        protected virtual void ProcessLoadCountCompleted(object argument)
        {
            int newCount = (int)argument;

            SetNewCount(newCount);

            IsInitializing = false;
        }

        #endregion
        #region 페이지 로드하기 - LoadPage(pageIndex, pageLength)

        /// <summary>
        /// 페이지 로드하기
        /// </summary>
        /// <param name="pageIndex">페이지 인덱스</param>
        /// <param name="pageLength">페이지 길이</param>
        protected override void LoadPage(int pageIndex, int pageLength)
        {
            IsLoading = true;

            ThreadPool.QueueUserWorkItem(LoadPage, new int[] { pageIndex, pageLength });
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 컬렉션 리셋하기 - ResetCollectionReset()

        /// <summary>
        /// 컬렉션 리셋하기
        /// </summary>
        private void ResetCollectionReset()
        {
            NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);

            FireCollectionChangedEvent(e);
        }

        #endregion
        #region 속성 변경시 이벤트 발생시키기 - FirePropertyChangedEvent(propertyName)

        /// <summary>
        /// 속성 변경시 이벤트 발생시키기
        /// </summary>
        /// <param name="propertyName">속성명</param>
        private void FirePropertyChangedEvent(string propertyName)
        {
            PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);

            FirePropertyChangedEvent(e);
        }

        #endregion

        #region 카운트 로드하기 - LoadCount(argument)

        /// <summary>
        /// 카운트 로드하기
        /// </summary>
        /// <param name="argument">인자</param>
        private void LoadCount(object argument)
        {
            int count = FetchCount();

            SynchronizationContext.Send(ProcessLoadCountCompleted, count);
        }

        #endregion
        #region 신규 카운트 설정하기 - SetNewCount(newCount)

        /// <summary>
        /// 신규 카운트 설정하기
        /// </summary>
        /// <param name="newCount">신규 카운트</param>
        private void SetNewCount(int newCount)
        {
            if(newCount != Count)
            {
                Count = newCount;

                EmptyCache();

                ResetCollectionReset();
            }
        }

        #endregion
        #region 페이지 로드 완료시 처리하기 - ProcessLoadPageCompleted(state)

        /// <summary>
        /// 페이지 로드 완료시 처리하기
        /// </summary>
        /// <param name="state">상태</param>
        private void ProcessLoadPageCompleted(object state)
        {
            object[] argumentArray = (object[])state;

            int pageIndex = (int)argumentArray[0];

            IList<TItem> itemList = (IList<TItem>)argumentArray[1];

            int newCount = (int)argumentArray[2];

            SetNewCount(newCount);

            PopulatePage(pageIndex, itemList);

            IsLoading = false;
        }

        #endregion
        #region 페이지 로드하기 - LoadPage(state)

        /// <summary>
        /// 페이지 로드하기
        /// </summary>
        /// <param name="state">상태</param>
        private void LoadPage(object state)
        {
            int[] argumentArray = (int[])state;

            int pageIndex    = argumentArray[0];
            int pageLength   = argumentArray[1];
            int overallCount = 0;

            IList<TItem> itemList = FetchPage(pageIndex, pageLength, out overallCount);

            SynchronizationContext.Send(ProcessLoadPageCompleted, new object[] { pageIndex, itemList, overallCount });
        }

        #endregion
    }
}

 

[TestProject 프로젝트]

 

▶ USRegion.cs

namespace TestProject
{
    /// <summary>
    /// US 지역
    /// </summary>
    public enum USRegion
    {
        /// <summary>
        /// 북동부
        /// </summary>
        NorthEast,

        /// <summary>
        /// 남동부
        /// </summary>
        SouthEast,

        /// <summary>
        /// 중서부
        /// </summary>
        MiddleWest,

        /// <summary>
        /// 남서부
        /// </summary>
        SouthWest,

        /// <summary>
        /// 서부
        /// </summary>
        West
    }
}

 

▶ DateToStringConverter.cs

using System;
using System.Globalization;
using System.Windows.Data;

namespace TestProject
{
    /// <summary>
    /// 날짜->문자열 변환자
    /// </summary>
    public class DateToStringConverter : IValueConverter
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 변환하기 - Convert(sourceValue, targetType, parameter, cultureInfo)

        /// <summary>
        /// 변환하기
        /// </summary>
        /// <param name="sourceValue">소스 값</param>
        /// <param name="targetType">타겟 타입</param>
        /// <param name="parameter">매개 변수</param>
        /// <param name="cultureInfo">문화 정보</param>
        /// <returns>변환 값</returns>
        public object Convert(object sourceValue, Type targetType, object parameter, CultureInfo cultureInfo)
        {
            DateTime date = (DateTime)sourceValue;

            return string.Format("{0:d}", date);
        }

        #endregion
        #region 역변환하기 - ConvertBack(sourceValue, targetType, parameter, cultureInfo)

        /// <summary>
        /// 역변환하기
        /// </summary>
        /// <param name="sourceValue">소스 값</param>
        /// <param name="targetType">타겟 타입</param>
        /// <param name="parameter">매개 변수</param>
        /// <param name="cultureInfo">문화 정보</param>
        /// <returns>역변환 값</returns>
        public object ConvertBack(object sourceValue, Type targetType, object parameter, CultureInfo cultureInfo)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

 

▶ DoubleToStringConverter.cs

using System;
using System.Globalization;
using System.Windows.Data;

namespace TestProject
{
    /// <summary>
    /// 배정도 실수 -> 문자열 변환자
    /// </summary>
    public class DoubleToStringConverter : IValueConverter
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 변환하기 - Convert(sourceValue, targetType, parameter, cultureInfo)

        /// <summary>
        /// 변환하기
        /// </summary>
        /// <param name="sourceValue">소스 값</param>
        /// <param name="targetType">타겟 타입</param>
        /// <param name="parameter">매개 변수</param>
        /// <param name="cultureInfo">문화 정보</param>
        /// <returns>변환 값</returns>
        public object Convert(object sourceValue, Type targetType, object parameter, CultureInfo cultureInfo)
        {
            double value = (double)sourceValue;

            return string.Format("{0:c}", value);
        }

        #endregion
        #region 역변환하기 - ConvertBack(sourceValue, targetType, parameter, cultureInfo)

        /// <summary>
        /// 역변환하기
        /// </summary>
        /// <param name="sourceValue">소스 값</param>
        /// <param name="targetType">타겟 타입</param>
        /// <param name="parameter">매개 변수</param>
        /// <param name="cultureInfo">문화 정보</param>
        /// <returns>역변환 값</returns>
        public object ConvertBack(object sourceValue, Type targetType, object parameter, CultureInfo cultureInfo)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

 

▶ IntegerToRegionConverter.cs

using System;
using System.Globalization;
using System.Windows.Data;

namespace TestProject
{
    /// <summary>
    /// 정수 -> 영역 변환자
    /// </summary>
    public class IntegerToRegionConverter : IValueConverter
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 변환하기 - Convert(sourceValue, targetType, parameter, cultureInfo)

        /// <summary>
        /// 변환하기
        /// </summary>
        /// <param name="sourceValue">소스 값</param>
        /// <param name="targetType">타겟 타입</param>
        /// <param name="parameter">매개 변수</param>
        /// <param name="cultureInfo">문화 정보</param>
        /// <returns>변환 값</returns>
        public object Convert(object sourceValue, Type targetType, object parameter, CultureInfo cultureInfo)
        {
            int value;

            bool result = int.TryParse(sourceValue.ToString(), out value);

            if(result)
            {
                USRegion region = (USRegion)(int)sourceValue;

                return region.ToString();
            }

            return string.Empty;
        }

        #endregion
        #region 역변환하기 - ConvertBack(sourceValue, targetType, parameter, cultureInfo)

        /// <summary>
        /// 역변환하기
        /// </summary>
        /// <param name="sourceValue">소스 값</param>
        /// <param name="targetType">타겟 타입</param>
        /// <param name="parameter">매개 변수</param>
        /// <param name="cultureInfo">문화 정보</param>
        /// <returns>역변환 값</returns>
        public object ConvertBack(object sourceValue, Type targetType, object parameter, CultureInfo cultureInfo)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

 

▶ DateRangePicker.xaml

<UserControl x:Class="TestProject.DateRangePicker"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <DatePicker Name="fromDatePicker" Grid.Column="0"
            Margin="10 0 0 0 " />
        <TextBlock Grid.Column="1"
            Margin="10 0 0 0 "
            VerticalAlignment="Center"
            Text="~" />
        <DatePicker Name="toDatePicker" Grid.Column="2"
            Margin="10 0 0 0" />
    </Grid>
</UserControl>

 

▶ DateRangePicker.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace TestProject
{
    /// <summary>
    /// 일자 범위 선택기
    /// </summary>
    public partial class DateRangePicker : UserControl
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 시작일 변경시 - FromDateChanged

        /// <summary>
        /// 시작일 변경시
        /// </summary>
        public event EventHandler FromDateChanged;

        #endregion
        #region 종료일 변경시 - ToDateChanged

        /// <summary>
        /// 종료일 변경시
        /// </summary>
        public event EventHandler ToDateChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Dependency Property
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 시작일 속성 - FromDateProperty

        /// <summary>
        /// 시작일 속성
        /// </summary>
        public static readonly DependencyProperty FromDateProperty = DependencyProperty.Register
        (
            "FromDate",
            typeof(Nullable<DateTime>),
            typeof(DateRangePicker),
            new PropertyMetadata(FromDatePropertyChangedCallback)
        );

        #endregion
        #region 종료일 속성 - ToDateProperty

        /// <summary>
        /// 종료일 속성
        /// </summary>
        public static readonly DependencyProperty ToDateProperty = DependencyProperty.Register
        (
            "DateTo",
            typeof(Nullable<DateTime>),
            typeof(DateRangePicker),
            new PropertyMetadata(ToDatePropertyChangedCallback)
        );

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 시작일 - FromDate

        /// <summary>
        /// 시작일
        /// </summary>
        public Nullable<DateTime> FromDate
        {
            get
            {
                return (Nullable<DateTime>)GetValue(FromDateProperty);
            }
            set
            {
                SetValue(FromDateProperty, value);
            }
        }

        #endregion
        #region 종료일 - ToDate

        /// <summary>
        /// 종료일
        /// </summary>
        public Nullable<DateTime> ToDate
        {
            get
            {
                return (Nullable<DateTime>)GetValue(ToDateProperty);
            }
            set
            {
                SetValue(ToDateProperty, value);
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - DateRangePicker()

        /// <summary>
        /// 생성자
        /// </summary>
        public DateRangePicker()
        {
            InitializeComponent();

            this.fromDatePicker.BlackoutDates.Add
            (
                new CalendarDateRange
                (
                    DateTime.Today.AddDays(1),
                    DateTime.MaxValue
                )
            );

            this.fromDatePicker.SetBinding
            (
                DatePicker.SelectedDateProperty,
                new Binding("FromDate")
                {
                    Source = this,
                    Mode   = BindingMode.TwoWay
                }
            );

            this.toDatePicker.BlackoutDates.Add
            (
                new CalendarDateRange
                (
                    DateTime.Today.AddDays(1),
                    DateTime.MaxValue
                )
            );

            this.toDatePicker.SetBinding
            (
                DatePicker.SelectedDateProperty,
                new Binding("ToDate")
                {
                    Source = this,
                    Mode   = BindingMode.TwoWay
                }
            );
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region 시작일 속성 변경시 콜백 처리하기 - FromDatePropertyChangedCallback(d, e)

        /// <summary>
        /// 시작일 속성 변경시 콜백 처리하기
        /// </summary>
        /// <param name="d">의존 객체</param>
        /// <param name="e">이벤트 인자</param>
        private static void FromDatePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            DateRangePicker dateRangePicker = (DateRangePicker)d;

            dateRangePicker.ProcessFromDateChanged();
        }

        #endregion
        #region 종료일 속성 변경시 콜백 처리하기 - ToDatePropertyChangedCallback(d, e)

        /// <summary>
        /// 종료일 속성 변경시 콜백 처리하기
        /// </summary>
        /// <param name="d">의존 객체</param>
        /// <param name="e">이벤트 인자</param>
        private static void ToDatePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            DateRangePicker dateRangePicker = (DateRangePicker)d;

            dateRangePicker.FireDateChancedEvent(dateRangePicker.ToDateChanged);
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Instance
        //////////////////////////////////////////////////////////////////////////////// Private

        #region 시작일 변경시 처리하기 - ProcessFromDateChanged()

        /// <summary>
        /// 시작일 변경시 처리하기
        /// </summary>
        private void ProcessFromDateChanged()
        {
            this.toDatePicker.BlackoutDates.Clear();

            if(FromDate.HasValue)
            {
                DateTime fromDate = FromDate.Value;

                if(ToDate.HasValue)
                {
                    DateTime toDate = ToDate.Value;

                    if(toDate <= fromDate)
                    {
                        ToDate = null;
                    }
                }

                this.toDatePicker.BlackoutDates.Add
                (
                    new CalendarDateRange
                    (
                        DateTime.MinValue,
                        fromDate
                    )
                );
            }

            this.toDatePicker.BlackoutDates.Add
            (
                new CalendarDateRange
                (
                    DateTime.Today.AddDays(1),
                    DateTime.MaxValue
                )
            );

            this.FireDateChancedEvent(FromDateChanged);
        }

        #endregion
        #region 일자 변경 이벤트 발생시키기 - FireDateChancedEvent(handler)

        /// <summary>
        /// 일자 변경 이벤트 발생시키기
        /// </summary>
        /// <param name="handler">이벤트 핸들러</param>
        private void FireDateChancedEvent(EventHandler handler)
        {
            handler?.Invoke(this, EventArgs.Empty);
        }

        #endregion
    }
}

 

▶ DataGridHelper.cs

using System.Windows.Controls;
using System.Windows.Data;

namespace TestProject
{
    /// <summary>
    /// 데이터 그리드 헬퍼
    /// </summary>
    public static class DataGridHelper
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        #region 정렬 멤버 경로 구하기 - GetSortMemberPath(column)

        /// <summary>
        /// 정렬 멤버 경로 구하기
        /// </summary>
        /// <param name="column">컬럼</param>
        /// <returns>정렬 멤버 경로</returns>
        public static string GetSortMemberPath(DataGridColumn column)
        {
            string sortPropertyName = column.SortMemberPath;

            if(string.IsNullOrEmpty(sortPropertyName))
            {
                DataGridBoundColumn boundColumn = column as DataGridBoundColumn;

                if(boundColumn != null)
                {
                    Binding binding = boundColumn.Binding as Binding;

                    if(binding != null)
                    {
                        if(!string.IsNullOrEmpty(binding.XPath))
                        {
                            sortPropertyName = binding.XPath;
                        }
                        else if(binding.Path != null)
                        {
                            sortPropertyName = binding.Path.Path;
                        }
                    }
                }
            }

            return sortPropertyName;
        }

        #endregion
    }
}

 

▶ Customer.cs

using System;
using System.ComponentModel;

namespace TestProject
{
    /// <summary>
    /// 고객
    /// </summary>
    public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Event
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 속성 변경 전 - PropertyChanging

        /// <summary>
        /// 속성 변경 전
        /// </summary>
        public event PropertyChangingEventHandler PropertyChanging;

        #endregion
        #region 속성 변경 후 - PropertyChanged

        /// <summary>
        /// 속성 변경 후
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 빈 속성 변경 전 이벤트 인자
        /// </summary>
        private static PropertyChangingEventArgs _emptyPropertyChangingEventArgs = new PropertyChangingEventArgs(string.Empty);

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Instance
        //////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// ID
        /// </summary>
        private int id;

        /// <summary>
        /// 이름
        /// </summary>
        private string firstName;

        /// <summary>
        /// 성
        /// </summary>
        private string lastName;

        /// <summary>
        /// 가입일
        /// </summary>
        private System.DateTime customerSince;

        /// <summary>
        /// 지역 호출 지불액
        /// </summary>
        private double amountPaidLocalCalls;

        /// <summary>
        /// 국내 호출 지불액
        /// </summary>
        private double amountPaidNationalCalls;

        /// <summary>
        /// 해외 호출 지불액
        /// </summary>
        private double amountPaidInternationalCalls;

        /// <summary>
        /// 계획상 패밀리 멤버 수
        /// </summary>
        private int numberFamilyMembersInPlan;

        /// <summary>
        /// 선호 프로그램 가입 여부
        /// </summary>
        private bool joinedPreferredProgram;

        /// <summary>
        /// 지역
        /// </summary>
        private int region;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region ID - Id

        /// <summary>
        /// ID
        /// </summary>
        public int Id
        {
            get
            {
                return this.id;
            }
            set
            {
                if(this.id != value)
                {
                    FirePropertyChangingEvent();

                    this.id = value;

                    FirePropertyChangedEvent("Id");
                }
            }
        }

        #endregion
        #region 이름 - FirstName

        /// <summary>
        /// 이름
        /// </summary>
        public string FirstName
        {
            get
            {
                return this.firstName;
            }
            set
            {
                if(this.firstName != value)
                {
                    FirePropertyChangingEvent();

                    this.firstName = value;

                    FirePropertyChangedEvent("FirstName");
                }
            }
        }

        #endregion
        #region 성 - LastName

        /// <summary>
        /// 성
        /// </summary>
        public string LastName
        {
            get
            {
                return this.lastName;
            }
            set
            {
                if(this.lastName != value)
                {
                    FirePropertyChangingEvent();

                    this.lastName = value;

                    FirePropertyChangedEvent("LastName");
                }
            }
        }

        #endregion
        #region 가입일 - CustomerSince

        /// <summary>
        /// 가입일
        /// </summary>
        public DateTime CustomerSince
        {
            get
            {
                return this.customerSince;
            }
            set
            {
                if(this.customerSince != value)
                {
                    FirePropertyChangingEvent();

                    this.customerSince = value;

                    FirePropertyChangedEvent("CustomerSince");
                }
            }
        }

        #endregion
        #region 지역 호출 지불액 - AmountPaidLocalCalls

        /// <summary>
        /// 지역 호출 지불액
        /// </summary>
        public double AmountPaidLocalCalls
        {
            get
            {
                return this.amountPaidLocalCalls;
            }
            set
            {
                if(this.amountPaidLocalCalls != value)
                {
                    FirePropertyChangingEvent();

                    this.amountPaidLocalCalls = value;

                    FirePropertyChangedEvent("AmountPaidLocalCalls");
                }
            }
        }

        #endregion
        #region 국내 호출 지불액 - AmountPaidNationalCalls

        /// <summary>
        /// 국내 호출 지불액
        /// </summary>
        public double AmountPaidNationalCalls
        {
            get
            {
                return this.amountPaidNationalCalls;
            }
            set
            {
                if(this.amountPaidNationalCalls != value)
                {
                    FirePropertyChangingEvent();

                    this.amountPaidNationalCalls = value;

                    FirePropertyChangedEvent("AmountPaidNationalCalls");
                }
            }
        }

        #endregion
        #region 해외 호출 지불액 - AmountPaidInternationalCalls

        /// <summary>
        /// 해외 호출 지불액
        /// </summary>
        public double AmountPaidInternationalCalls
        {
            get
            {
                return this.amountPaidInternationalCalls;
            }
            set
            {
                if(this.amountPaidInternationalCalls != value)
                {
                    FirePropertyChangingEvent();

                    this.amountPaidInternationalCalls = value;

                    FirePropertyChangedEvent("AmountPaidInternationalCalls");
                }
            }
        }

        #endregion
        #region 계획상 패밀리 멤버 수 - NumberFamilyMembersInPlan

        /// <summary>
        /// 계획상 패밀리 멤버 수
        /// </summary>
        public int NumberFamilyMembersInPlan
        {
            get
            {
                return this.numberFamilyMembersInPlan;
            }
            set
            {
                if((this.numberFamilyMembersInPlan != value))
                {
                    FirePropertyChangingEvent();

                    this.numberFamilyMembersInPlan = value;

                    FirePropertyChangedEvent("NumberFamilyMembersInPlan");
                }
            }
        }

        #endregion
        #region 선호 프로그램 가입 여부 - JoinedPreferredProgram

        /// <summary>
        /// 선호 프로그램 가입 여부
        /// </summary>
        public bool JoinedPreferredProgram
        {
            get
            {
                return this.joinedPreferredProgram;
            }
            set
            {
                if(this.joinedPreferredProgram != value)
                {
                    FirePropertyChangingEvent();

                    this.joinedPreferredProgram = value;

                    FirePropertyChangedEvent("JoinedPreferredProgram");
                }
            }
        }

        #endregion
        #region 지역 - Region

        /// <summary>
        /// 지역
        /// </summary>
        public int Region
        {
            get
            {
                return this.region;
            }
            set
            {
                if(this.region != value)
                {
                    FirePropertyChangingEvent();

                    this.region = value;

                    FirePropertyChangedEvent("Region");
                }
            }
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - Customer()

        /// <summary>
        /// 생성자
        /// </summary>
        public Customer()
        {
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Protected

        #region 속성 변경 전 이벤트 발생시키기 - FirePropertyChangingEvent()

        /// <summary>
        /// 속성 변경 전 이벤트 발생시키기
        /// </summary>
        protected virtual void FirePropertyChangingEvent()
        {
            PropertyChanging?.Invoke(this, _emptyPropertyChangingEventArgs);
        }

        #endregion
        #region 속성 변경 후 이벤트 발생시키기 - FirePropertyChangedEvent(propertyName)

        /// <summary>
        /// 속성 변경 후 이벤트 발생시키기
        /// </summary>
        /// <param name="propertyName">속성명</param>
        protected virtual void FirePropertyChangedEvent(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion
    }
}

 

▶ CustomerProvider.cs

using System;
using System.Collections.Generic;
using System.Linq;

using TestLibrary;

namespace TestProject
{
    /// <summary>
    /// 고객 공급자
    /// </summary>
    public class CustomerProvider : IItemProvider<Customer>
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 고객 리스트
        /// </summary>
        private readonly List<Customer> customerList;

        /// <summary>
        /// 시작일
        /// </summary>
        private readonly DateTime? startDate;

        /// <summary>
        /// 종료일
        /// </summary>
        private readonly DateTime? endDate;

        /// <summary>
        /// 정렬 필드
        /// </summary>
        private readonly string sortField;

        /// <summary>
        /// 카운트
        /// </summary>
        private int count;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - CustomerProvider()

        /// <summary>
        /// 생성자
        /// </summary>
        public CustomerProvider()
        {
            this.startDate = DateTime.Today.AddYears(-100);
            this.endDate   = DateTime.Today.AddYears(100);
            this.sortField = "CustomerSince DESC";

            this.customerList = new List<Customer>();

            for(int i = 0; i < 1000000; i++)
            {
                this.customerList.Add
                (
                    new Customer
                    {
                        AmountPaidInternationalCalls = i % 100,
                        AmountPaidLocalCalls         = i % 100,
                        AmountPaidNationalCalls      = i % 100,
                        CustomerSince                = this.startDate.Value.AddDays(i),
                        FirstName                    = string.Format("Customer {0}", i),
                        Id                           = i,
                        LastName                     = string.Format("LastName {0}", i),
                        JoinedPreferredProgram       = i % 2 == 0,
                        NumberFamilyMembersInPlan    = i % 4,
                        Region                       = i % 100,
                    }
                );
            }
        }

        #endregion
        #region 생성자 - CustomerProvider(startDate, endDate, sortField)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="startDate">시작일</param>
        /// <param name="endDate">종료일</param>
        /// <param name="sortField">정렬 필드</param>
        public CustomerProvider(DateTime? startDate, DateTime? endDate, string sortField) : this()
        {
            if(startDate != null)
            {
                this.startDate = startDate;
            }

            if(endDate != null)
            {
                this.endDate = endDate;
            }

            this.sortField = sortField;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 카운트 가져오기 - FetchCount()

        /// <summary>
        /// 카운트 가져오기
        /// </summary>
        /// <returns>카운트</returns>
        public int FetchCount()
        {
            this.count = this.customerList.Count(e => e.CustomerSince >= this.startDate && e.CustomerSince <= this.endDate);

            return this.count;
        }

        #endregion
        #region 범위 가져오기 - FetchRange(startIndex, pageCount, overallCount)

        /// <summary>
        /// 범위 가져오기
        /// </summary>
        /// <param name="startIndex">시작 인덱스</param>
        /// <param name="pageCount">페이지 카운트</param>
        /// <param name="overallCount">전체 카운트</param>
        /// <returns>고객 리스트</returns>
        public IList<Customer> FetchRange(int startIndex, int pageCount, out int overallCount)
        {
            // 이 경우 데이터베이스의 데이터가 변경되지 않는다고 가정하기 때문에 카운트를 다시 가져오지 않아도 된다.
            overallCount = this.count;

            if(this.sortField.Contains("DESC"))
            {
                return this.customerList.Where(e => e.CustomerSince >= this.startDate && e.CustomerSince <= this.endDate)
                                        .OrderBy(e => e.FirstName)
                                        .Skip(startIndex)
                                        .Take(pageCount).ToList();
            }
            else
            {
                return this.customerList.Where(e => e.CustomerSince >= this.startDate && e.CustomerSince <= this.endDate)
                                        .OrderByDescending(e => e.FirstName)
                                        .Skip(startIndex)
                                        .Take(pageCount).ToList();
            }
        }

        #endregion
    }
}

 

▶ CustomSortDescription.cs

using System.ComponentModel;
using System.Windows.Controls;

namespace TestProject
{
    /// <summary>
    /// 커스텀 정렬 설명
    /// </summary>
    public class CustomSortDescription
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 속성명 - PropertyName

        /// <summary>
        /// 속성명
        /// </summary>
        public string PropertyName { get; set; }

        #endregion
        #region 리스트 정렬 방향 - Direction

        /// <summary>
        /// 리스트 정렬 방향
        /// </summary>
        public ListSortDirection Direction { get; set; }

        #endregion
        #region 데이터 그리드 컬럼 - Column

        /// <summary>
        /// 데이터 그리드 컬럼
        /// </summary>
        public DataGridColumn Column { get; set; }

        #endregion
    }
}

 

▶ MainWindow.xaml

<Window x:Class="TestProject.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestProject"
    Width="800"
    Height="600"
    Title="정렬과 필터링 가능한 데이터 가상화 사용하기"
    FontFamily="나눔고딕코딩"
    FontSize="16">
    <Window.Resources>
        <local:DateToStringConverter x:Key="DateToStringConverterKey" />
        <local:DoubleToStringConverter x:Key="DoubleToStringConverterKey" />
        <local:IntegerToRegionConverter x:Key="IntegerToRegionConverterKey" />
        <Style x:Key="TitleTextBlockStyleKey" TargetType="TextBlock" >
            <Setter Property="FontWeight" Value="Bold" />
        </Style>
        <Style x:Key="DataGridStyleKey" TargetType="{x:Type DataGrid}">
            <Setter Property="BorderThickness"               Value="1"         />
            <Setter Property="BorderBrush"                   Value="#ff688caf" />
            <Setter Property="Background"                    Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"     />
            <Setter Property="Foreground"                    Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
            <Setter Property="ScrollViewer.CanContentScroll" Value="True"                />
            <Setter Property="RowDetailsVisibilityMode"      Value="VisibleWhenSelected" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type DataGrid}">
                        <Grid>
                            <Border
                                BorderThickness="{TemplateBinding BorderThickness}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                Background="{TemplateBinding Background}"
                                Padding="{TemplateBinding Padding}"
                                SnapsToDevicePixels="True">
                                <ScrollViewer Name="DG_ScrollViewer"
                                    Focusable="False">
                                    <ScrollViewer.Template>
                                        <ControlTemplate TargetType="{x:Type ScrollViewer}">
                                            <Grid>
                                                <Grid.RowDefinitions>
                                                    <RowDefinition Height="Auto" />
                                                    <RowDefinition Height="*"    />
                                                    <RowDefinition Height="Auto" />
                                                </Grid.RowDefinitions>
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="Auto" />
                                                    <ColumnDefinition Width="*"    />
                                                    <ColumnDefinition Width="Auto" />
                                                </Grid.ColumnDefinitions>
                                                <Button
                                                    Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource FindAncestor,
                                                        AncestorLevel=1, AncestorType={x:Type DataGrid}}}"
                                                    Focusable="False">
                                                    <Button.Visibility>
                                                        <Binding
                                                            RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type DataGrid}}"
                                                            Path="HeadersVisibility">
                                                            <Binding.ConverterParameter>
                                                                <DataGridHeadersVisibility>All</DataGridHeadersVisibility>
                                                            </Binding.ConverterParameter>
                                                        </Binding>
                                                    </Button.Visibility>
                                                    <Button.Template>
                                                        <ControlTemplate TargetType="{x:Type Button}">
                                                            <Grid>
                                                                <Rectangle Name="Border"
                                                                    Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
                                                                    SnapsToDevicePixels="True" />
                                                                <Polygon Name="Arrow"
                                                                    HorizontalAlignment="Right"
                                                                    VerticalAlignment="Bottom"
                                                                    Margin="8 8 3 3"
                                                                    Fill="Black"
                                                                    Stretch="Uniform"
                                                                    Opacity="0.15"
                                                                    Points="0 10 10 10 10 0" />
                                                            </Grid>
                                                            <ControlTemplate.Triggers>
                                                                <Trigger Property="IsMouseOver" Value="True">
                                                                    <Setter
                                                                        TargetName="Border"
                                                                        Property="Stroke"
                                                                        Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" />
                                                                </Trigger>
                                                                <Trigger Property="IsPressed" Value="True">
                                                                    <Setter
                                                                        TargetName="Border"
                                                                        Property="Fill"
                                                                        Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" />
                                                                </Trigger>
                                                                <Trigger Property="IsEnabled" Value="False">
                                                                    <Setter
                                                                        TargetName="Arrow"
                                                                        Property="Visibility"
                                                                        Value="Collapsed" />
                                                                </Trigger>
                                                            </ControlTemplate.Triggers>
                                                        </ControlTemplate>
                                                    </Button.Template>
                                                    <Button.Command>
                                                        <RoutedCommand />
                                                    </Button.Command>
                                                </Button>
                                                <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1">
                                                    <DataGridColumnHeadersPresenter.Visibility>
                                                        <Binding
                                                            RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type DataGrid}}"
                                                            Path="HeadersVisibility">
                                                            <Binding.ConverterParameter>
                                                                <DataGridHeadersVisibility>Column</DataGridHeadersVisibility>
                                                            </Binding.ConverterParameter>
                                                        </Binding>
                                                    </DataGridColumnHeadersPresenter.Visibility>
                                                </DataGridColumnHeadersPresenter>
                                                <ScrollContentPresenter Name="PART_ScrollContentPresenter" Grid.Row="1" Grid.ColumnSpan="2"
                                                    CanHorizontallyScroll="False"
                                                    CanVerticallyScroll="False"
                                                    CanContentScroll="{TemplateBinding CanContentScroll}"
                                                    ContentStringFormat="{TemplateBinding ContentStringFormat}"
                                                    ContentTemplate="{TemplateBinding ContentTemplate}"
                                                    Content="{TemplateBinding Content}" />
                                                <ScrollBar Name="PART_VerticalScrollBar" Grid.Row="1" Grid.Column="2"
                                                    Orientation="Vertical"
                                                    ViewportSize="{TemplateBinding ViewportHeight}"
                                                    Maximum="{TemplateBinding ScrollableHeight}"
                                                    Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
                                                    Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" />
                                                <Grid Grid.Column="1" Grid.Row="2">
                                                    <Grid.ColumnDefinitions>
                                                        <ColumnDefinition Width="{Binding NonFrozenColumnsViewportHorizontalOffset,
                                                            RelativeSource={RelativeSource FindAncestor, AncestorLevel=1,
                                                            AncestorType={x:Type DataGrid}}}" />
                                                        <ColumnDefinition Width="*" />
                                                    </Grid.ColumnDefinitions>
                                                    <ScrollBar Name="PART_HorizontalScrollBar" Grid.Column="1"
                                                        Orientation="Horizontal"
                                                        ViewportSize="{TemplateBinding ViewportWidth}"
                                                        Maximum="{TemplateBinding ScrollableWidth}"
                                                        Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"
                                                        Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" />
                                                </Grid>
                                            </Grid>
                                        </ControlTemplate>
                                    </ScrollViewer.Template>
                                    <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                                </ScrollViewer>
                            </Border>
                            <Grid Name="InitializingGrid"
                                Background="White"
                                Opacity="0.5"
                                Cursor="Wait"
                                Visibility="Collapsed">
                                <TextBlock
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Center"
                                    Text="Initializing..." />
                            </Grid>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding Path=IsInitializing}" Value="True">
                                <Setter Property="Visibility" Value="Visible" TargetName="InitializingGrid"/>
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="IsGrouping" Value="True">
                    <Setter Property="ScrollViewer.CanContentScroll" Value="False" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <Style x:Key="DataGridRowStyleKey" TargetType="{x:Type DataGridRow}">
            <Setter Property="Background"               Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" />
            <Setter Property="SnapsToDevicePixels"      Value="True"     />
            <Setter Property="Validation.ErrorTemplate" Value="{x:Null}" />
            <Setter Property="ValidationErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <TextBlock
                            VerticalAlignment="Center"
                            Margin="2 0 0 0"
                            Foreground="Red"
                            Text="!">
                            <Run Text="!" />
                        </TextBlock>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type DataGridRow}">
                        <Grid>
                            <Border Name="DGR_Border"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                Background="{TemplateBinding Background}"
                                SnapsToDevicePixels="True">
                                <SelectiveScrollingGrid>
                                    <SelectiveScrollingGrid.RowDefinitions>
                                        <RowDefinition Height="*"    />
                                        <RowDefinition Height="Auto" />
                                    </SelectiveScrollingGrid.RowDefinitions>
                                    <SelectiveScrollingGrid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*"    />
                                    </SelectiveScrollingGrid.ColumnDefinitions>
                                    <DataGridCellsPresenter Grid.Column="1"
                                        SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                        ItemsPanel="{TemplateBinding ItemsPanel}" />
                                    <DataGridDetailsPresenter Grid.Row="1" Grid.Column="1"
                                        Visibility="{TemplateBinding DetailsVisibility}">
                                        <SelectiveScrollingGrid.SelectiveScrollingOrientation>
                                            <Binding
                                                RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type DataGrid}}"
                                                Path="AreRowDetailsFrozen">
                                                <Binding.ConverterParameter>
                                                    <SelectiveScrollingOrientation>Vertical</SelectiveScrollingOrientation>
                                                </Binding.ConverterParameter>
                                            </Binding>
                                        </SelectiveScrollingGrid.SelectiveScrollingOrientation>
                                    </DataGridDetailsPresenter>
                                    <DataGridRowHeader Grid.RowSpan="2"
                                        SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical">
                                        <DataGridRowHeader.Visibility>
                                            <Binding
                                                RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type DataGrid}}"
                                                Path="HeadersVisibility">
                                                <Binding.ConverterParameter>
                                                    <DataGridHeadersVisibility>Row</DataGridHeadersVisibility>
                                                </Binding.ConverterParameter>
                                            </Binding>
                                        </DataGridRowHeader.Visibility>
                                    </DataGridRowHeader>
                                </SelectiveScrollingGrid>
                            </Border>
                            <StackPanel Name="Loading"
                                Background="Transparent"
                                Cursor="Wait"
                                Visibility="Collapsed">
                                <Rectangle
                                    Height="2"
                                    Fill="White" />
                                <StackPanel
                                    Margin="5 0 0 0"
                                    Orientation="Horizontal">
                                    <TextBlock Text="항목 로딩중 " />
                                    <TextBlock Text="{Binding ItemNumber}" />
                                    <TextBlock Text="..." />
                                </StackPanel>
                            </StackPanel>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding IsLoading}" Value="True">
                                <Setter TargetName="Loading"    Property="Visibility" Value="Visible"   />
                                <Setter TargetName="DGR_Border" Property="Visibility" Value="Collapsed" />
                            </DataTrigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*"    />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock
                    Style="{StaticResource TitleTextBlockStyleKey}"
                    VerticalAlignment="Center"
                    Text="고객 가입일" />
                <local:DateRangePicker x:Name="dateRangePicker"
                    Margin="10 0 0 0"
                    FromDateChanged="dateRangePicker_DateChanged"
                    ToDateChanged="dateRangePicker_DateChanged" />
            </StackPanel>
        </StackPanel>
        <DataGrid Name="dataGrid" Grid.Row="1"
            Style="{StaticResource DataGridStyleKey}"
            RowStyle="{StaticResource DataGridRowStyleKey}"
            Margin="0 10 0 0"
            AutoGenerateColumns="False"
            IsSynchronizedWithCurrentItem="True"
            VirtualizingStackPanel.VirtualizationMode="Recycling"
            EnableColumnVirtualization="True"
            EnableRowVirtualization="True"
            IsReadOnly="True"
            ItemsSource="{Binding}"
            Sorting="dataGrid_Sorting"
            SelectedIndex="0">
            <DataGrid.Columns>
                <DataGridTextColumn Header="이름"
                    Binding="{Binding Item.FirstName}" />
                <DataGridTextColumn Header="성"
                    Binding="{Binding Item.LastName}" />
                <DataGridTextColumn Header="가입일"
                    Binding="{Binding Item.CustomerSince, Converter={StaticResource DateToStringConverterKey}}" />
                <DataGridTextColumn Header="지역 호출"
                    Binding="{Binding Item.AmountPaidLocalCalls, Converter={StaticResource DoubleToStringConverterKey}}" />
                <DataGridTextColumn Header="국내 호출"
                    Binding="{Binding Item.AmountPaidNationalCalls, Converter={StaticResource DoubleToStringConverterKey}}" />
                <DataGridTextColumn Header="해외 호출"
                    Binding="{Binding Item.AmountPaidInternationalCalls, Converter={StaticResource DoubleToStringConverterKey}}"/>
                <DataGridTextColumn Header="멤버 수"
                    Binding="{Binding Item.NumberFamilyMembersInPlan}" />
                <DataGridCheckBoxColumn Header="선호"
                    Binding="{Binding Item.JoinedPreferredProgram}" />
                <DataGridTextColumn Header="지역"
                    Binding="{Binding Item.Region, Converter={StaticResource IntegerToRegionConverterKey}}" />
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

 

▶ MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

using TestLibrary;

namespace TestProject
{
    /// <summary>
    /// 메인 윈도우
    /// </summary>
    public partial class MainWindow : Window
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 커스텀 정렬 설명 리스트
        /// </summary>
        private readonly List<CustomSortDescription> descriptionList;

        /// <summary>
        /// 고객 공급자
        /// </summary>
        private CustomerProvider customerProvider;

        /// <summary>
        /// 페이지 크기
        /// </summary>
        private int pageSize = 100;

        /// <summary>
        /// 페이지 타임아웃
        /// </summary>
        private int pageTimeout = 5000;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Consturctor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - MainWindow()

        /// <summary>
        /// 생성자
        /// </summary>
        public MainWindow()
        {
            PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Off;

            InitializeComponent();

            string defaultSortColumnName = "CustomerSince";

            DataGridColumn defaultSortColumn = dataGrid.Columns.Single(dgc => GetColumnSortMemberPath(dgc) == defaultSortColumnName);

            this.descriptionList = new List<CustomSortDescription>
            {
                new CustomSortDescription
                {
                    PropertyName = defaultSortColumnName,
                    Direction    = ListSortDirection.Descending,
                    Column       = defaultSortColumn
                }
            };


            RefreshData();
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Private
        //////////////////////////////////////////////////////////////////////////////// Event

        #region 일자 범위 선택기 일자 변경시 처리하기 - dateRangePicker_DateChanged(sender, e)

        /// <summary>
        /// 일자 범위 선택기 일자 변경시 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void dateRangePicker_DateChanged(object sender, EventArgs e)
        {
            RefreshData();
        }

        #endregion
        #region 데이터 그리드 정렬 처리하기 - dataGrid_Sorting(sender, e)

        /// <summary>
        /// 데이터 그리드 정렬 처리하기
        /// </summary>
        /// <param name="sender">이벤트 발생자</param>
        /// <param name="e">이벤트 인자</param>
        private void dataGrid_Sorting(object sender, DataGridSortingEventArgs e)
        {
            ApplySortColumn(e.Column);

            e.Handled = true;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Function

        #region 컬럼 정렬 멤버 경로 구하기 - GetColumnSortMemberPath(column)

        /// <summary>
        /// 컬럼 정렬 멤버 경로 구하기
        /// </summary>
        /// <param name="column">컬럼</param>
        /// <returns>컬럼 정렬 멤버 경로</returns>
        private string GetColumnSortMemberPath(DataGridColumn column)
        {
            string prefixToRemove = "Item.";
            string fullSortColumn = DataGridHelper.GetSortMemberPath(column);
            string sortColumn     = fullSortColumn.Substring(prefixToRemove.Length);

            return sortColumn;
        }

        #endregion
        #region 현재 정렬 문자열 구하기 - GetCurrentSortString()

        /// <summary>
        /// 현재 정렬 문자열 구하기
        /// </summary>
        /// <returns>현재 정렬 문자열</returns>
        private string GetCurrentSortString()
        {
            StringBuilder stringBuilder = new StringBuilder();

            string separator = string.Empty;

            foreach(CustomSortDescription description in this.descriptionList)
            {
                stringBuilder.Append(separator);
                stringBuilder.Append(description.PropertyName);

                if(description.Direction == ListSortDirection.Descending)
                {
                    stringBuilder = stringBuilder.Append(" DESC");
                }

                separator = ", ";
            }

            return stringBuilder.ToString();
        }

        #endregion
        #region 컬럼 정렬 방향 업데이트하기 - UpdateColumnSortDirection()

        /// <summary>
        /// 컬럼 정렬 방향 업데이트하기
        /// </summary>
        private void UpdateColumnSortDirection()
        {
            foreach(CustomSortDescription description in this.descriptionList)
            {
                description.Column.SortDirection = description.Direction;
            }
        }

        #endregion
        #region 데이터 갱신하기 - RefreshData()

        /// <summary>
        /// 데이터 갱신하기
        /// </summary>
        private void RefreshData()
        {
            string sortString = GetCurrentSortString();

            this.customerProvider = new CustomerProvider
            (
                this.dateRangePicker.FromDate,
                this.dateRangePicker.ToDate,
                sortString
            );

            AsyncVirtualizingCollection<Customer> collection = new AsyncVirtualizingCollection<Customer>
            (
                this.customerProvider,
                this.pageSize,
                this.pageTimeout
            );

            DataContext = collection;

            UpdateColumnSortDirection();

            this.dataGrid.SelectedIndex = 0;
        }

        #endregion
        #region 정렬 컬럼 적용하기 - ApplySortColumn(column)

        /// <summary>
        /// 정렬 컬럼 적용하기
        /// </summary>
        /// <param name="column">컬럼</param>
        private void ApplySortColumn(DataGridColumn column)
        {
            string sortColumn = GetColumnSortMemberPath(column);

            CustomSortDescription existingDescription = this.descriptionList.SingleOrDefault(sd => sd.PropertyName == sortColumn);

            if(existingDescription == null)
            {
                existingDescription = new CustomSortDescription
                {
                    PropertyName = sortColumn,
                    Direction    = ListSortDirection.Ascending,
                    Column       = column
                };

                this.descriptionList.Add(existingDescription);
            }
            else
            {
                existingDescription.Direction = (existingDescription.Direction == ListSortDirection.Ascending) ? ListSortDirection.Descending : ListSortDirection.Ascending;
            }

            bool isShiftPressed = (Keyboard.Modifiers & ModifierKeys.Shift) != 0;

            if(!isShiftPressed)
            {
                for(int i = this.descriptionList.Count - 1; i >= 0; i--)
                {
                    CustomSortDescription description = this.descriptionList[i];

                    if(description.PropertyName != sortColumn)
                    {
                        this.descriptionList.RemoveAt(i);
                    }
                }
            }

            RefreshData();
        }

        #endregion
    }
}
728x90
반응형
그리드형(광고전용)
Posted by icodebroker

댓글을 달아 주세요