2008. 12. 13. 16:34

[강좌] ListBox의 상속.


  사실 이 강좌의 제목은 ItemsControl의 상속이라고 해야 더 옳습니다. 하지만 ItemsControl를 상속받아서 제대로된 Control을 만든다는 것은 보다 험난한 길이기 때문에 그건 다음 강좌를 기약하고 그냥 ListBox를 통째로 상속받아서 ListBox에서(혹은 ItemsControl에서) 다행히 Protected override 메소드로 접근할 수 있는 메소드들만 건드려 보도록 하겠습니다. 

  사실 보통의 경우, 그냥 데이터만 바인딩만 하는 경우, 이런 경우가 왜 필요하느냐 물으실 수도 있겠지만 실제로 ListBox를 그대로 사용하는 경우는 저희 회사만 해도 한번도 없었다고 해도 과언이 아닙니다. 이왕 기본 컨트롤을 제공할 꺼면 좀더 리치하게 제공해줄 것이지. Asp.net 수준 정도로밖에 안만들어 놨기 때문에 그냥 갔다 쓰면 이게 RIA인지 그냥 WebPage인지 알 수가 없죠.. 아.. 잡소리가 길어졌군요..^^;;

  그럼 Rich 한 ListBox 를 위해 일단  기본 ListBox를 최대한 사용하는 방법을 알아보도록 하겠습니다 .그럼 먼저 제공해주는 override 메소드부터 살펴보죠. ListBox(ItemsControl)에서 새로 제공해주는 override 메소드는 다음과 같습니다.

  • protected virtual bool IsItemItsOwnContainerOverride(object item)
  • protected virtual DependencyObject GetContainerForItemOverride()
  • protected virtual void PrepareContainerForItemOverride(DependencyObject element, object item)
  • protected virtual void OnItemsChanged(NotifyCollectionChangedEventArgs e)
  • protected virtual void ClearContainerForItemOverride(DependencyObject element, object item)

5가지 모두 ListBox에 들어갈 Item에 관련된 메소드들이죠. 각각의 메소드들이 무슨 일을 하는지 알아보죠.

  1. IsItemItesOwnContainerOverride(object item)
      간단히 사용자가 넣어준 아이템이 Container를 가지고 있는지 확인합니다. base.IsItemItesOwnContainerOverride(item) 에서는 item이 ListBoxItem 인지 확인하고 ItemsControl을 바로 상속했을 경우에는 base에서 itemdl UIElement 인지 확인합니다.
       return 값이 false 인 경우 다음에 GetContainerForItemOverride 메소드가 호출되고 true일 경우 PrepareContainerForItemOverride 가 바로 호출 됩니다.
  2. GetContainerForItemOverride()
      Container를 반환해줍니다. base.GetContainerForItemOverride() 에서는 ListBoxItem의 새로운 인스턴스를 반환합니다. 이 때 ItemContainerStyle 이 null 이 아닐 때는 새로 생성되는 ListBoxItem에 ItemContainerStyle의 Style이 적용됩니다.
  3. PrepareContainerForItemOverride(DependencyObject element, object item)
      먼저 파라미터인 element에는 위의 GetContainerForItemOverride() 에서 반환된 ListBoxItem 이나 IsItemItesOwnContainerOverride(item) 의 결과가 true일 경우에는 사용자가 넣어준 ListBoxItem 혹은 ListBoxItem을 상속받은 객체가 들어오게 되어있습니다. 그리고 item 에는 사용자가 넣어준 데이타가 들어오게 되어있습니다. 
      일단 base.PrepareContainerForItemOverride(element, item)에서는 element에 ItemTemplate을 적용해주고 element가 ContentControl인 경우 Content에 item을 엮어주는 작업을 해줍니다. 그것과 함께 ListBox를 상속했을 경우에는(ItemsControl 상속의 경우 제외) 현재 SelectedIndex나 SelectedItem 에 따라 Selection 처리를 해줍니다.
  4. OnItemsChanged(NotifyCollectionChangedEventArgs e)
     
    이 메서드는 사용자가 ItemsSource 의 데이타를 INotifyCollectionChanged 인터페이스를 상속받은 데이타 클래스(ex. ObervableCollection<T>: 이 클래스의 사용방법은 차후에 설명하도록 하겠습니다. )로 했을 경우에만 들어옵니다. INotifyCollectionChanged의 CollectionChanged 이벤트가 발생하였을 경우에 들어옵니다.
  5. ClearContainerForItemOverride(DependencyObject element, object item)
      이 메서드는 생각보다 중요한 메서드입니다. 이 부분은 ListBox에서 Item을 제거할 때 들어옵니다.  실제로 기본 ListBox 에서 이부분에 구현된 코드는 없지만 만약 ListBox를 상속받아 PrepareContainerForItemOverride 함수에서 ListBoxItem 에 이벤트를 엮어주었다든지 List나 Dictionary를 따로 만들어 ListBoxItem 을 관리했다면 이 부분에서 해제시켜주거나 List에서 제외시켜주어야 합니다.

흠냐.. 어렵나요? 개발자는 코드로 말해야 하는 것을 길게 글로 써놓았으니 이해하기 힘드셨다고 해도 할 말이 없습니다. 그럼 이제 코드로 보여드리죠. 보여드릴 예제는 ListBoxItem 에 CheckBox가 추가되어 있는 경우입니다. CheckBox가 ListBox가 추가 되어서 Selected 된 것과 별도로 Checked 된 것인지 아닌지도 알아보고 싶은 것이지요.
(tip:UserControl이나 App.xaml에 Style이 있는 경우 Style에서 이벤트를 걸어도 그 이벤트가 비하인드 코드의 이벤트 핸들러로 들어옵니다. 이런 방법으로 구현을 할 수도 있겠지만 Style과 코드 사이에 의존성을 높여서 재사용성도 떨어지며 차후에 Style 변경시 문제가 생길 수 있어 되도록 사용하지 않는 것이 좋습니다. ) 

  그럼 ListBoxItem 을 먼저 Customizing을 해야 하겠군요. 먼저 CheckableListBoxItem이란 class를 선언하고 ListBoxItem을 상속받습니다.

    public class CheckableListBoxItem: ListBoxItem
    {
    }

그리고 여기서 기본 Style도 조금은 바꾸어 주어야 할 것이므로 DefaultStyle도 생성자에서 설정해주어야 합니다. 다음과 같이

        public CheckableListBoxItem()
        {
            DefaultStyleKey = typeof(CheckableListBoxItem);
        }

이 부분은 나중에 Control을 다 만든 다음에 자주 까먹을 수 있는 부분이니 무조건 처음부터 설정해주도록 합시다. (삽질 방지 습관화!!!) 

그 다음에 TemplatePart 에 CheckBox 하나를 추가해주도록 합시다. CheckBox 객체의 이름은 왠만하면 const 값으로 박아 놓고 쓰는게 좋겠죠. 그래서 다음과 같이 짜놓았습니다. 
(클래스 속성)

[TemplatePart(Name=CheckableListBoxItem.CheckBoxName, Type=(typeof(CheckBox)))]
(내부 코드)
CheckBox _checkBox;
internal const string CheckBoxName = "CheckBox";

그리고 OnApplyTemplate에서 Style에 들어있을 CheckBox를 찾아와야 하겠죠. 그리고 찾아온 CheckBox가 Checked 되었는지 안되었는지 확인하기 위해서 이벤트를 엮어줍니다.
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _checkBox = GetTemplateChild(CheckBoxName) as CheckBox;
            if (_checkBox != null)
            {
                _checkBox.Checked += new RoutedEventHandler(_checkBox_Checked);
                _checkBox.Unchecked += new RoutedEventHandler(_checkBox_Unchecked);
            }
        }

여기서 CheckableListBoxItem 의 Check 상태를 우리가 만들 CheckablelistBox 에 알려줄 수 있는 방법은 두가지가 있습니다. 한가지는 이벤트를 이용하는 것이고 다른 한가지는 ListBoxItem이 ListBox의 참조값을 받아서 ListBox의 메서드를 직접 호출 하는 방법입니다. 둘 다 장단점이 있는데, 전자는 ListBox가 ListBoxItem을 제거 할 때 이벤트도 함께 제거해주어야 메모리가 제대로 해제 될 수 있지만 후자보다 ListBoxItem이 독립적으로 사용이 가능하죠. 후자의 경우 메모리 해제 부분에 크게 신경쓰지 않아도 되지만 CheckableListBoxItem은 반드시 CheckableListBox 의 Item으로만 들어가야 한다는 단점이 있죠. 사실 ListBox와 ListBoxItem은 의존성이 아주 높은 관계이므로 후자로 구현해도 무방하고 실제 구현도 후자로 되어 있지만 일단 전자로 구현을 해보도록 하겠습니다. 

  위의 코드로 다음과 같은 이벤트와 이벤트 Fire 함수를 만들어 주고 Checked와 UnChecked  이벤트 핸들러에 함수를 추가해줍니다.

        public event RoutedEventHandler ItemChecked;
        public event RoutedEventHandler ItemUnchecked;

        void _checkBox_Unchecked(object sender, RoutedEventArgs e)
        {
            FireItemUnchecked(e);
        }
        void _checkBox_Checked(object sender, RoutedEventArgs e)
        {
            FireItemChecked(e);
        }

        internal void FireItemUnchecked(RoutedEventArgs e)
        {
            if (ItemUnchecked != null)
                ItemUnchecked(this, e);
        }
        internal void FireItemChecked(RoutedEventArgs e)
        {
            if (ItemChecked != null)
                ItemChecked(this, e);
        }

 자 그럼 앞에서 배운 override 함수를 활용하여 CheckableListBox를 만들어 보죠. 먼저 앞서 만든 것처럼 CheckableListBox를 만들고 DefaultStyle등을 만들어 줍니다. 그리고 맨먼저 ListBoxItem 이 아닌 CheckableListBox가 ListBoxItem으로 만들어지게 하기 위해 override 함수를 수정해주어야 합니다. 

  먼저 OnItemItsOwnContainerOverride 함수의 경우 ListBoxItem인지 체크해주는 함수입니다. 하지만 여기서는 CheckableListBoxItem이어야 하니.. CheckableListBoxItem이 아닌 경우 Container를 새로 만들어주어야 합니다. 
고로 다음과 같이 짜줍니다. 

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return (item is CheckableListBoxItem);
        }

간단하죠?^^ 다음으로 item이 ChekableListBoxItem이 아닌 경우 새로 Container를 만들어 줘야 하니 GetContainerForItemOverride 함수를 수정해주어야 합니다. 다음과 같이 짭니다. 
        protected override DependencyObject GetContainerForItemOverride()
        {
            CheckableListBoxItem item = new CheckableListBoxItem();
            if (this.ItemContainerStyle != null)
            {
                item.Style = this.ItemContainerStyle;
            }
            return item;
        }

  CheckableListBoxItem을 새로 만들어주고 ItemContainerStyle을 적용해주는 것이죠. 이렇게 하면 다음에 짜줄 PrepareContainerForItemOverride 함수의 DependencyObject 로 CheckableListBoxItem이 들어오는 것을 확인할 수 있습니다. 그러면 이제 PrepareContainerForItemOverride 를 수정해봅시다. 
        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            CheckableListBoxItem checkableItem = (element as CheckableListBoxItem);
            if (checkableItem != null)
            {
                checkableItem.ItemChecked += new RoutedEventHandler(checkableItem_ItemChecked);
                checkableItem.ItemUnchecked += new RoutedEventHandler(checkableItem_ItemUnchecked);
            }
        }
  PrepareContainerForItemOverride 함수의 base 부분에는 item에 data를 바인딩 시켜주고 Select에 관한 처리가 포함되어있으니 꼭 불러주도록 해야 합니다. 아니면 귀찮은 코드를 몇줄 더 짜야 겠죠.

그런데 여기서 Checked 된 것이 어떤 것인지 알기 위해서는 Check된 상태를 알려줄 Event와 Check된 객체를 담아둘 List가 하나 필요할 것입니다. 그리고 Check된 객체가 원래 어떤 아이템이었는지도 알아볼 수 있는 Dictionary도 하나 그래서 다음과 같은 Event와 Property 등을 추가합니다.
        public event RoutedEventHandler CheckedItemsChanged;
        public ObservableCollection<object> CheckedItems { get; private set; }
        private Dictionary<CheckableListBoxItem, object> _oDicCheckableListBoxItem;
        public CheckableListBox()
        {
            DefaultStyleKey = typeof(ListBox);
            CheckedItems = new ObservableCollection<object>();
_oDicCheckableListBoxItem = new Dictionary<CheckableListBoxItem, object>();
        }
그리고 Item을 제대로 얻기 위해 PrepareContainerForItemOverride 함수에 다음 함수 한 줄을 더 추가해줍니다.
      _oDicCheckableListBoxItem.Add(checkableItem, item);

그리고 아까 PrepareContainerForItemOverride함수에서 추가시켜주었던 EvnetHandler부분을 건드려 줍니다. 
        void checkableItem_ItemUnchecked(object sender, RoutedEventArgs e)
        {
            object item = _oDicCheckableListBoxItem[(sender as CheckableListBoxItem)];
            if (CheckedItems.Contains(item))
                CheckedItems.Remove(item);
            FireCheckItemsChanged();
        }
        void checkableItem_ItemChecked(object sender, RoutedEventArgs e)
        {
            CheckedItems.Add(_oDicCheckableListBoxItem[(sender as CheckableListBoxItem)]);
            FireCheckItemsChanged();
        }
        private void FireCheckItemsChanged()
        {
            if (CheckedItemsChanged != null)
                CheckedItemsChanged(this, null);
        }

자 그럼 이제 마지막(?) Themes/generic.xaml을 추가해주고 그 곳에 CheckBox가 추가된 CheckableListBoxItem Style을 넣어줍니다. 다음과 같이..
 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:MakeCustomListBox="clr-namespace:MakeCustomListBox"
             xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">   
    <Style TargetType="MakeCustomListBox:CheckableListBoxItem" >
        .
        .
        .
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="MakeCustomListBox:CheckableListBoxItem" >
                    <Grid Background="{TemplateBinding Background}">
                        .
                        .
                        .
                        <CheckBox x:Name="CheckBox" HorizontalAlignment="Left" Margin="{TemplateBinding Padding}">
                            <ContentPresenter x:Name="contentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" HorizontalAlignment="Left"   IsHitTestVisible="False"/>
                        </CheckBox>
                        .
                        .
                        .
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

    자 이제 완성입니다!! 그럼 테스트를 해볼까요. 간단히 xaml에는 CheckableListBox와 그냥 ListBox를 넣어주고 Add버튼과 DeleteButton을 넣어주었습니다. 그리고 코드는 다음과 같이..
        public Page()
        {
            InitializeComponent();
            myListBox.ItemsSource = new ObservableCollection<object>(){ "하나","둘","셋"};
            resultListBox.ItemsSource = myListBox.CheckedItems;
        }
        private void AddButton_Click(object sender, RoutedEventArgs e)
        {
            (myListBox.ItemsSource as IList).Add(new Color());
        }
        private void DeleteButton_Click(object sender, RoutedEventArgs e)
        {
            (myListBox.ItemsSource as IList).RemoveAt(0);
        }
  잘 작동하나요?.... 결과를 보면 Add 와 Delete도 잘 일어나고 Check 상태도 잘 들어오는 것을 알 수 있습니다. 

  그런데...!!!!  Check상태로 Delete를 누른 객체가 사라지지 않는다는 것을 알 수 있습니다.... 흠... 그렇습니다. 우리가 사용하지 않은 하나의 override 함수를 더 사용해야 합니다. 앞서 강조했던 ClearContainerForItemOverride 함수입니다. 이 함수에서 엮어주었던 이벤트를 해제시켜주고 참고하고 있던 리스트에서 삭제해주는 작업을 해주어야 합니다. 다음과 같이요.
        protected override void ClearContainerForItemOverride(DependencyObject element, object item)
        {
            base.ClearContainerForItemOverride(element, item);
            CheckableListBoxItem checkableItem = (element as CheckableListBoxItem);
            if (checkableItem != null)
            {
                object item2 = _oDicCheckableListBoxItem[checkableItem];
                if (CheckedItems.Contains(item2))
                {
                    CheckedItems.Remove(item2);
                }
                checkableItem.ItemChecked -= new RoutedEventHandler(checkableItem_ItemChecked);
                checkableItem.ItemUnchecked -= new RoutedEventHandler(checkableItem_ItemUnchecked);
                _oDicCheckableListBoxItem.Remove(checkableItem);
            }

        }

  
  급하게 ListBox를 상속받아서 Customizing 하다보면 항상 이 부분을 놓치기 쉽습니다. 이 부분이 구현이 안되면 생각보다 오작동하는 경우가 많기 때문에 꼭 짜주는 것이 좋습니다. 
 
  흠.. 그림 한장 없이 설명하다보니 이해가 쉽게 되지 않을 수도 있다는 생각이 듭니다. 그림을 넣기 좀 애매한 부분이 있어서.^^;;; 지금 여기서 설명한 method들은 모두 ItemsControl 에서 상속받은 method들이기 때문에 ListBox가 아니라 바로 ItemsControl 을 상속받아 Class를 만들 때에도 적용이 가능한 것들입니다. 이해를 돕기 위해 샘플프로젝트를 첨부합니다. 그럼 모두 삽질 금지!!^^

                                                                                                                            - smile -