Introduction

MVVM패턴을 사용할 때 일반적으로 ListBox에 목록을 바인딩 하기 위해 ObservableCollection<T>를 ItemSource에 바인딩하는 방법을 사용합니다. ObservableCollection의 경우 INotifyCollectionChanged를 상속받아 구현되었기 때문에 목록이 변경되었을때 마다 ListBox에 변경을 알려 ListBox의 View와 ObservableCollection의 목록이 동일하게 유지될 수 있도록 돕습니다.

그리고 ListBox의 현재 선택된 항목을 ViewModel에서 제어하기 위해 ListBox의 SelectedItem속성에 ViewModel의 속성을 TwoWay로 바인딩합니다. 하나의 선택된 항목을 관리하기 위해서 SelectedItem을 이용할 수 있지만, 만약 ListBox에서 MultiSelect를 지원하는 경우 이슈가 발생합니다.

ListBox의 SelectedItems속성의 경우 읽기 전용이기 때문에 바인딩을 할수 없기 때문입니다. 때문에 ViewModel에서 View에 해당하는 ListBox를 가지고 있어야 하는데 View와 ViewModel을 분리하는 MVVM패턴에서 ViewModel에서 ListBox를 직접 조작하는 방법은 좋은 방법은 아닙니다. 이번시간에는 Multi Select를 지원하는 ListBox의 SelectedItems속성을 ViewModel에서 View에 종속적이지 않고 SelectedItems를 동기화 하는 방법에 대해 소개합니다.

Attached Property & Weak Event Pattern

위에서 언급했듯이 ListBox의 SelectedItems는 읽기 전용속성이기 때문에 직접적인 바인딩이 불가능합니다. 따라서 ListBox의 SelectedItems을 모니터링하고, 동기화할 목록 지정하기 위한 Attached Property를 아래와 같이 구현합니다.

그리고 ListBox의 SelectedItems와 대상목록을 동기화 하는 SelectedItemsHelper를 구현합니다. 그리고 INotifyCollectionChanged를 상속받아 구현된 SelectedItems와 대상목록의 CollectionChanged 이벤트를 WeakEvent패턴을 이용해 수신할수 있도록 SelectedItemsHelper에 IWeakEventListener를 상속받고 구현합니다.

※ WeakEvent 패턴이란 이벤트를 수신하거나 제거될때 발생할수 있는 메모리 누수문제등을 위해 설계된 패턴으로 명시적으로 이벤트 수신기와 이벤트 관리자 클래스를 제공하여 이벤트수신기에 대한 수명을 직접관리하고 구조적으로 이벤트 처리를 독립시킬수 있습니다. AttachedProperty를 이용해 이벤트 처리와 관련된 기능을 구현 할 때 이벤트 수신/해제가 빈번하게 발생해 메모리 누수가 발생할수 있어 WeakEvent 패턴이용해 명시적으로 관리하면 효과적입니다.

SelectedItemsHelper에는 위와 같이 ListBox의 SelectedItems와 대상목록 중 한곳이 갱신되었을때 갱신된 내용을 다른 목록에도 반영시키는 것으로 두 목록을 동기화 합니다. 이렇게 하면 ViewModel상의 목록과 ListBox의 SelectedItems가 서로 같은 데이터를 유지할 수 있습니다. 아래 동영상은 Attached Property와 SelectedItemsHelper를 이용해 구현한 데모 프로그램의 동영상입니다.

위 동영상에서 사용된 ListBox는 아래 코드와 같이 ListBoxHelper.SelectedItems을 ViewModel의 목록으로 바인딩합니다.

화면 아래쪽의 버튼을 클릭했을때 발생하는 ViewModel의 Command는 아래와 같이 임의의 항목을 SelectedNumbers에 등록하는 기능을 수행합니다. SelectedNumber는 ListBoxHelper.SelectedItems에 바인딩 되어 있기 때문에 SelectedNumber의 목록을 갱신하는 것만으로 ViewModel에서 View에 종속적이지 않는 상태로 ListBox의 SelectedItems를 갱신할 수 있습니다.

아래는 이번시간에 사용한 데모 프로그램 소스코드입니다.

 

신고

Introduction

Application을 개발할때 목록에 항목이 추가되거나, 제거 되었을때와 같은 동적인 변경내용을 수신기에 알리기 위해 일반적으로 목록객체에 INotifyCollectionChanged를 상속받아 구현하거나  INotifyCollectionChanged가 구현된 ObservableCollection<T>등을 사용합니다. 동적인 목록의 변경에 대하여 즉각 반응하기 때문에 매우 유용하게 사용될 수 있지만, 많은 양의 목록이 추가/제거 될경우 매번 목록의 변동이 있을대마다 CollectionChanged이벤트가 발생하기 때문에 퍼포먼스에 크게 영향을 미칠 수 있습니다. 이번시간에는 많은 양의 아이템을 INotifyCollectionChanged가 구현된 목록에 추가/제거할때 CollectionChanged가 발생하는것을 방지하고 처리가 모두 끝난뒤에 호출되도록 하는 방법에 대해 소개합니다.

List<T> VS ObservableCollection<T>

위 동영상은 INotifyCollectionChanged가 정의되지 않은 List<T>와 INotifyCollectionChanged가 정의된 ObservableCollection<T>를 이용해 ListBox의 Item목록을 변경 했을때의 퍼모먼스 비교입니다. 데모에서는 동일한 갯수의 목록(30만 건)을 목록에 추가하고 ListBox를 갱신하는 과정까지의 시간을 측정했습니다.

결과는 INotifyCollectionChanged를 정의하지 않은 List<T>가 INotifyCollectionChanged가 정의된 ObservableCollection<T>보다 10배정도 빠른 결과를 나타내고 있습니다. 이유는 ObservableCollection에서 항목이 추가될때마다 CollectionChanged이벤트가 발생하고 이를 수신하는 ListBox에서는 매 항목이 추가될때마다 View를 갱신하고 있지만, List<T>는 항목이 추가되더라도 View를 갱신하지 않기 때문입니다. List<T>는 View에 영향을 미치지않기 때문에 목록추가가 완료된뒤 View의 ItemsControl에 Refresh메서드를 호출하는것으로 View를 갱신할 수 있습니다.

하지만, List<T>를 이용했을때가 항상 성능이 뛰어난 것은 아닙니다. 위에서 언급했듯이 List<T>는 INotifyCollectionChanged가 구현되지 않았기 때문에 목록에 아이템을 추가할때마다 직접 View의 ItemsControl을 Refresh 메서드를 호출해줘야하는데, ItemsControl의 Refresh메서드를 호출하게 되면 View를 다시 그리기 때문에 이는 더큰 부작용을 일으킬수 있습니다. 

Create SuspendObservableCollection

가장 좋은 방법은 작은 건의 항목이 변경될때에는 INotifyCollectionChanged를 이용해 해당 항목이 추가/제거 되었음을 알리고, 많은 항목이 변경될때에는 CollectionChanged이벤트 발생을 일시적으로 중단하고, 모든 항목의 변경이 완료되었을때 CollectionChanged를 발생시키는 방법입니다. 하지만, ObservableCollection의 경우 CollectionChanged 이벤트에 대한 중지여부를 직접적으로는 제어 할 수 없기 때문에 ObservableCollection을 상속받아 CollectionChanged이벤트 발생여부를 제어 할 수 있는 SuspendObserableCollection을 구현할 수 있습니다. 아래는 SuspendObservableCollection의 소스코드입니다.


ObservableCollection의 OnCollectionChanged에서 내부적으로 발생시키는 CollectionChanged이벤트 호출을 제어하기 위해 IsSuspend 속성을 구현해 CollectionChanged 이벤트의 호출을 제어 했습니다. 실제 적용될 때에는 다음과 같이 사용할 수 있습니다.


이제 한두건 정도의 간단한 목록 변경에서는 INotifyCollectionChanged를 이용해 변경을 알리고 많은건의 목록변경에서는 IsSuspend와 Refresh메서드를 이용해 CollectionChanged이벤트를 제어 할 수 있습니다. 아래 동영상은 위에서 실시한 퍼포먼스 비교를SuspendObservalbleCollection에도 적용한 내용입니다.

Reflection for INotifyCollectionChanged

추가로, Reflection을 이용해 SuspendObservableCollection을 구현하지 않고 CollectionChnaged이벤트에 대한 제어를 수행할수 있습니다. 아래 코드는 Reflection을 이용한 CollectionChanged이벤트 제어를  Extension Method로 구현한 코드는 입니다.


CollectionChanged를 제어하기 위해서 PasueNotifyCollectionChanged메서드와 ResumeNotifyCollectionChaged를 이용합니다. 퍼포먼스는 SuspendObservableCollection보다 다소 느리지만, Type을 수정할 수 없는 상황이거나, INotifyCollectionChanged을 구현한 다른 Type의 Collection에서 유용하게 사용할 수 있습니다.

 

신고