■ 정렬과 필터링 가능한 데이터 가상화를 사용하는 방법을 보여준다.
[TestLibrary 프로젝트]
▶ DataWrapper.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
<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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
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 } } |