728x90
반응형
728x170
■ 정렬과 필터링 가능한 데이터 가상화를 사용하는 방법을 보여준다.
[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
반응형
그리드형(광고전용)
'C# > WPF' 카테고리의 다른 글
[C#/WPF] ResourceDictionary 클래스 : 컴파일 및 동적 스키닝 사용하기 (0) | 2022.01.09 |
---|---|
[C#/WPF] ResourceDictionary 클래스 : 컴파일 및 정적 스키닝 사용하기 (0) | 2022.01.09 |
[C#/WPF] DoubleAnimation 클래스 : 화면을 반짝이는 애니메이션 사용하기 (0) | 2022.01.09 |
[C#/WPF] TextBox 클래스 : KeyDown 이벤트를 사용해 ENTER 키를 누르는 경우 포커스 해제하기 (0) | 2022.01.08 |
[C#/WPF] 마이크로폰 볼륨 설정하기 (0) | 2022.01.08 |
[C#/WPF] Page 엘리먼트 : WindowWidth/WindowHeight/WindowTitle 속성 사용하기 (0) | 2022.01.04 |
[C#/WPF] Application 클래스 : GetRemoteStream 정적 메소드를 사용해 원본 사이트 파일 XAML 페이지 로드하기 (0) | 2022.01.02 |
[C#/WPF] 원본 사이트 파일 사용하기 (0) | 2022.01.02 |
[C#/WPF] Frame 엘리먼트 : Source 속성을 사용해 컨텐트 XAML 페이지 로드하기 (0) | 2022.01.02 |
[C#/WPF] Frame 클래스 : Source 속성을 사용해 컨텐트 XAML 페이지 로드하기 (0) | 2022.01.02 |
댓글을 달아 주세요