'ListBox'에 해당되는 글 6건

  1. 2008.12.13 [강좌] ListBox의 상속. 1
  2. 2008.12.10 [강좌] 1. Listbox의 기본적인 사용.(2) Item Look 변경.
  3. 2008.12.10 [강좌] 1. Listbox의 기본적인 사용.(1) 아이템 셋팅.
  4. 2008.12.09 [강좌] ListBox 를 사용하자!!
  5. 2008.12.02 ListBox의 Select된 객체 해제하기.(Select취소하기) 1
  6. 2008.11.05 Wheel 지원 리스트 박스
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 -

2008. 12. 10. 11:09

[강좌] 1. Listbox의 기본적인 사용.(2) Item Look 변경.

Item 의 스타일을 변경해주는 방법은 2가지가 있습니다.

바로 ItemContainerStyle 을 바꾸어 주는 방법과 ItemTemplate을 바꾸는 방법이지요.

ItemTemplate 을 바꾸는 방법은 스캇 구슬리 강좌 에 소개 되어있는데요. 기본적으로 Container 안에 내용물을 바꾸는 것으로 볼 수 있죠. 
 
그럼 ItemContainerStyle부터 살펴 봅시다. ItemContainderStyle은 Style을 Value 값으로 받습니다. 아시는 분은 알겠지만 Style은 TargetType 이 필요하죠. 여기서 TargetType은 ListBoxItem 입니다. 결국 ItemContainerStyle은 ListBoxItem의 스타일을 정해주는 프로퍼티라는 것이지요.

 그럼 ListBoxItem의 DefaultStyle을 볼까요?
 
<Style TargetType="ListBoxItem" >
  <Setter Property="Padding" Value="3" />
  <Setter Property="HorizontalContentAlignment" Value="Left" />
  <Setter Property="VerticalContentAlignment" Value="Top" />
  <Setter Property="Background" Value="Transparent" />
  <Setter Property="BorderThickness" Value="1" />
  <Setter Property="TabNavigation" Value="Local" />
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="ListBoxItem" xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
        <Grid Background="{TemplateBinding Background}">
          <vsm:VisualStateManager.VisualStateGroups>
            <vsm:VisualStateGroup x:Name="CommonStates" >
              <vsm:VisualState x:Name="Normal" />
              <vsm:VisualState x:Name="MouseOver">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.TargetProperty="Opacity" Duration="0" To=".35" />
                </Storyboard>
              </vsm:VisualState>
              <vsm:VisualState x:Name="Disabled">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" Duration="0" To=".55" />
                </Storyboard>
              </vsm:VisualState>
            </vsm:VisualStateGroup>
            <vsm:VisualStateGroup x:Name="SelectionStates" >
              <vsm:VisualState x:Name="Unselected" />
              <vsm:VisualState x:Name="Selected">
                <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="fillColor2" Storyboard.TargetProperty="Opacity" Duration="0" To=".75" />
                </Storyboard>
              </vsm:VisualState>
            </vsm:VisualStateGroup>
            <vsm:VisualStateGroup x:Name="FocusStates" >
              <vsm:VisualState x:Name="Focused">
                <Storyboard>
                  <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Visibility" Duration="0">
                    <DiscreteObjectKeyFrame KeyTime="0">
                      <DiscreteObjectKeyFrame.Value>
                        <Visibility>Visible</Visibility>
                      </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                  </ObjectAnimationUsingKeyFrames>
                </Storyboard>
              </vsm:VisualState>
              <vsm:VisualState x:Name="Unfocused" />
            </vsm:VisualStateGroup>
          </vsm:VisualStateManager.VisualStateGroups>
          <Rectangle x:Name="fillColor" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"  />
          <Rectangle x:Name="fillColor2" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"  />
          <ContentPresenter x:Name="contentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" HorizontalAlignment="Left" Margin="{TemplateBinding Padding}"  />
          <Rectangle x:Name="FocusVisualElement" Stroke="#FF6DBDD1" StrokeThickness="1" Visibility="Collapsed" RadiusX="1" RadiusY="1"  />
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

너무 길죠?^^ 제가 알기 쉽게 Template부분만 간단히 정리해보죠.

      <ControlTemplate TargetType="ListBoxItem">
        <Grid>
          <vsm:VisualStateManager.VisualStateGroups/>
          <ContentPresenter />
        </Grid>
      </ControlTemplate>

 간단하죠?^^ 간단히 말하면 ListBoxItem은 하나의 ContentPresenter를 가지고 있는
ContentControl 이죠. VisualStateManager는 Item 상태를 결정 해주는 것이고요.

 사실 ContentPresenter이 없어도 됩니다. 다만 ItemTemplate이 무용지물이 되어버리죠.구지 ItemsTemplate을 쓰지 않고 Container로 모두 끝내도 된다면 상관없겠죠.

 그리고 특히 Select나 MouseOver에 대해 특별한 애니메이션이 필요하다면 꼭 ItemContainerStyle을 다시 설정해주어야 하겠죠.  또 ItemTemplate은 위의 ListBoxItem Style 중에 <ContentPresenter /> 부분을 대체하는 부분이기 때문에 ListBoxItem의 전체적인 디자인을 바꾸지는 못하겠죠.

결국 간단히

ItemContainerStyle은 ListBoxItem의 Style을 바꾸는 것이고
ItemTemplate은 ListBoxItem의 ContentPresenter 부분을 바꾸는 것입니다.


(이렇게 간단한 걸.. 이렇게.. 길게 어렵게 설명한 것인가....--;;;)

아직 이해를 못하신 분들을 위해 두가지 예제를 보여드리죠.

다음의 경우를 살펴봅시다.

        <ListBox Width="60" x:Name="ItemTemplateSettingListBox">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Ellipse Width="5" Height="5" Margin="5" Fill="Black"/>
                        <TextBlock Text="{Binding }"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>


이 후 비하인드 코드에서 다음과 같이 셋팅해줍시다.
 ItemTemplateSettingListBox.ItemsSource = new List<string>() { "하나", "둘", "셋", "넷" };

결과는 다음과 같죠.
ListBoxItem 안에들어가는 내용물 디자인이 바뀌긴 했지만 MouseOver나 Select시 State 변화는 똑같은 걸 알 수 있죠.

  ItemsContainerStyle 을 바꾸어야 하는데 이것은 조금더 복잡하기 때문에 Blend를 사용하는 것이 좋을 것같습니다. Blend에서 ListBox를 선택한 후 ItemsContainerStyle을 바꿀려고 보니 아래 그림과 같이 메뉴가 없는 걸 알 수 있습니다.

ItemTemplate과 다음장에 배울 ItemsPanel은 수정할 수 있는데 ItemsContainerStyle은 찾아볼 수가 없죠. 오른쪽 프로퍼티 창에는 존재하지만 GUI로 작업할 방법이 없는 듯 싶은데요..그래서 여기선 한가지 우회 방법을 써야만 합니다.
바로 ListBoxItem을 하나 만들어서 그 Style을 만들고 ItemsContainerStyle에 적용하는 것입니다.

  왼쪽 Control 메뉴를 클릭하면 선택할 수 있는 모든 컨트롤이 나옵니다.(아니 "Show All" 이 Check 되어있을 경우만요..)


ListBoxItem을 클릭한 후에 ListBoxItem을 적당한 크기로 생성을 합니다. 그리고 ListBoxItem 의 Style을 조정해 주면 됩니다.

Style을 변경하는 방법은 다른 강좌에서 설명하기로 하고 여기서는 간단히 제가 임의로 수정하도록 하겠습니다.
Style을 다 만들고 나면 오른쪽 Resource Tab메뉴에 다음과 같이 Style이 생겨 있음을 볼 수 있습니다.
그러면 이 등록된 Style을 ListBox의 ItemsContainerStyle에 적용하기만 하면 됩니다. 적용방법은 간단하죠. ListBox선택후 오른쪽 속성창에서 다음 그림에서 보이는 것처럼 설정해주시면 됩니다.
이제 적용된 결과를 테스트 해보시면 ListBox의 Item들이 전혀 다르게 Style이 적용된다는 것을 알 수 있을 겁니다.

그럼 다음에는 ItemsPanel 에 대해 설명하죠. 다음 장에는 좀더 자세히 설명을 해야겠다는 반성을.....--;;

                                                                                                                                           - smile -

p.s. 역시 샘플 프로젝트를 첨부합니다.


 
2008. 12. 10. 09:00

[강좌] 1. Listbox의 기본적인 사용.(1) 아이템 셋팅.

 일단 대략적인 사용법은 아래의 강좌를 참고 하시길 바랍니다.

스캇 구슬리의 영문 강좌
http://weblogs.asp.net/scottgu/pages/silverlight-tutorial-part-5-using-the-listbox-and-databinding-to-display-list-data.aspx

번역.
  http://hoons.kr/board.aspx?Name=sivlerlighttip&board_idx=457368&page=1&Mode=2&BoardIdx=11410


그럼 전 따분한 이론은 별로 좋아하지 않으니 바로 실전으로 들어가보겠습니다.

ListBox의 프로퍼티나 메소드에 대한 설명은 강좌 중간, 중간에 설명하도록 하겠습니다.

그럼 일단 ListBox를 만들어 보죠.

사용자 삽입 이미지

일단 테두리만 보일 뿐 아무것도 보이지 않는군요. 일단 여기에 Source를 셋팅 시켜야 겠죠.
이부분은 코드단에서 해야 하죠. 그럼 Page.xaml.cs 화일로 돌아가서 Item을 셋팅시켜주고 오겠습니다.

  여기서 Item을 셋팅 시켜주는 방법은 2가지가 있는데요 하나씩 알아보도록 하죠.

  먼저  ListBoxItems 에 직접 셋팅해주는 방법입니다. 다음과 같죠.
사용자 삽입 이미지

코드로는 아래와 같죠.
myList.Items.Add(myRect);
myList.Items.Add(myEllipse);


이렇게 아이템을 셋팅했을 때의 좋은 점은 소수의 아이템을 셋팅 시 간편하고 추가시킨 UIElement를 MyList.Items Collection을 통해 직접 가지고 올 수 있다는 것입니다.

그런데 이렇게 Item을 셋팅시켰을 때는 한가지 문제점이 생깁니다. 한번 Item으로 셋팅된 Item은 다시는 다른 곳에 셋팅시킬 수 없다는 것입니다. 이것은 버그성 같기도 하지만 어쨋든 다음과 같은 코드는 에러를 수반합니다.

사용자 삽입 이미지

 이에 대한 것은 다른 글에 포스팅을 이미 했으니 참고 하시길 바랍니다.

http://error1001.com/16

  두번째 방법은 위의 방법보다 정상적인 방법이라고 하겠습니다. 바로 ItemsSource 에 DataClass 의 Collection 을 셋팅하는 방법입니다.

  간단히 정리하면...

  MyListBox.ItemsSource = collection;

  뭐 이렇게 된다는 것이죠.

  여기서 약간은 복잡한 루틴이 들어가는데..  먼저 다음과 같은 코드를 써보죠.
 
  MyList.ItemsSource = new List<int>() { 1,2,3,4,5 };

  결과는 다음과 같습니다.

사용자 삽입 이미지

 내부적으로 이루어지는 코드를 살펴 보면 다음과 같습니다. (아래는 beta1때 공개된 Mix Control 소스입니다. 현재버전과는 조금 상이한 부분이 있습니다. 하지만 내부적인 구현은 비슷합니다.)

        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            ListBoxItem listBoxItem = element as ListBoxItem;
            Debug.Assert(null != listBoxItem);

            listBoxItem.ParentListBox = this;

            bool setContent = true;
            if (listBoxItem != item)
            {
                if (null != ItemTemplate)
                {
                    listBoxItem.ContentTemplate = ItemTemplate;
                }
                else if (!string.IsNullOrEmpty(DisplayMemberPath))
                {
                    Binding binding = new Binding(DisplayMemberPath);
                    binding.Converter = new DisplayMemberValueConverter();
                    listBoxItem.SetBinding(ContentControl.ContentProperty, binding);
                    setContent = false;
                }

                listBoxItem.Item = item;
                if (setContent)
                {
                    listBoxItem.Content = item;
                }
                ObjectToListBoxItem[item] = listBoxItem;
            }
            // Apply ItemContainerStyle
            if ((null != ItemContainerStyle) && (null == listBoxItem.Style))
            {
                listBoxItem.Style = ItemContainerStyle;
            }
                                                        .
                                                        .
                                                        .

여기서 PrepareContainerForItemOverride 함수는 ItemsControl의 Virtual 함수인데

Listbox에 들어갈 Item들을 준비해주는 함수입니다. 아이템의 생성은 다른 부분에서

일어나고 여기서는 이미 생성된 아이템의 스타일이나 프로퍼티 값등을 설정해주는

부분입니다.

  여기서 DependencyObject 로 들어오는 element가 바로 ListBoxItem 에 해당하는

UI 객체에 해당하고 object로 들어오는 item은 바로 사용자가 셋팅해준 collection의

한 item 이 됩니다.

여기서 보면 listBoxItem.Content = item; 이렇게 셋팅해주는 부분이 있는데 이 부분에서

item이 UIElement가 아닐 경우에는 object의 ToString 값이 화면에 표시가 됩니다.


  이부분은 앞서 설명했듯이. ListBoxItem 도 하나의 ContentControl로써 ContentPresenter를 포함하고 있습니다.
ContentControl은 Content로 UIElement가 들어올 경우에는 PlaceHolder 역할을 합니다. 하지만 다른 경우에는 TextBlock으로 대체하게 됩니다. 

  여기서 DataBinding 을 좀더 잘 활용하기 위해서는  ListBox의 ItemTemplate과 ItemContainerStyle 속성을 활용하여야 하는데 이 이야기는 다음강좌에 하도록 하죠. 일단은 의도와는 다르게 벌써 글이 길어져서..^^

                                                                                              - smile -




 


 

 
2008. 12. 9. 08:58

[강좌] ListBox 를 사용하자!!

대략적인 강좌 진행은 다음같이 할 예정입니다.

 1. ListBox 의 기본 적인 사용법

 2. ListBox 의 확장.(ListBox 상속 받아 쓰기)

 3. Listbox Clone 만들기.

 4. RichListBox 만들기.

 
 이 중 1, 2, 3 번은 확정이고 4번은 고민중입니다. 아직 구현된 component가 제대로 작동하는지 충분히 테스트 해보지 못했기 때문입니다. 그럼 이제 시작해보죠.


                                                                                             - smile -
2008. 12. 2. 12:27

ListBox의 Select된 객체 해제하기.(Select취소하기)


ListBox는 많은 프로젝트에서 가장 많이 사용하면서도 쓰기 어려운 컨트롤중에 하나죠. 여기서 가끔 사용하게 되는 것이 이미 Select된 객체를 취소시키는 것입니다. 코드로 Select를 하는 방법은 두가지가 있죠. 하나는 SelectedItem을 이용하는 방법이고 하나는 SelectedIndex를 사용하는 방법입니다. 

  SelectedIndex는 선택된 객체의 순서를 반환해주고 SelectedItem은 선택된 객체의 Binding된 Data 값을 반환해주죠.
셋팅을 해줄 때도 역시 선택할 객체의 Index값을 SelectedIndex에 넣어주거나 우리가 선택하고 싶은 Data를 SelectedItem에 셋팅해줌으로써 Select된 객체를 바꿀 수 있습니다. 

  그리고 선택이 되지 않은 초기 값은 SelctedIndex 는 -1 이며 SelectedItem 은 null 값이 됩니다. 

  그러면 반대로 선택을 해제 시키려면 SelectedIndex 에 -1값을 넣어주거나 SelectedItem에 null값을 집어넣어주면 되겠죠...


   그런데 문제는 이것이 잘 안먹는다는데 있습니다. 간단하게 테스트를 해보죠.


 Xaml 코드에서 ListBox를 하나 생성해준 뒤 이름을 그냥 "list" 라고 넣어두었습니다.

그리고 코드는 다음과 같습니다.


 public Page()
        {
            InitializeComponent();
            list.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);
        }
        void list_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
           list.SelectedItem = null; // 혹은 list.SelectedIndex = -1;
        }

선택하자 마자. Select를 풀어주자는 것이죠. 그런데 결과를 보면 절대로 Select는 풀리지 않죠.. Select가 풀리지 않으니 한번 선택한 객체를 다시 선택 이벤트를 받는 것은 불가능해지는 것이죠... --;; 어쩐다..

해결 방법은 간단합니다. " list.SelectedItem = null;" 요 부분을 다음과 같이 바꿔주면 정상 작동합니다.


if (e.AddedItems.Count != 0)
            list.Dispatcher.BeginInvoke(() => { list.SelectedItem = null; });


내부적인 작동은 알수 없지만 추측해보자면.. SelectionChanged 이벤트가 일어나는 타이밍의 문제가 아닐까 싶습니다.

SelectionChanged가 일어났을때는 아직 SelectedIndex나 SelectedItem 설정에 대한 로직이 아직 진행중인 상태인 거죠.  로직이 완전히 끝났을 때 다시 SelectedIndex를 설정해주어야만 정상작동하게 되는 것이죠. Dispatcher는 현재 UI스레드 작업이 완료되면 그 다음 작업을 실행시켜주는 것이니 현재는 아마 처음 Selection 에대한 작업이 실행될 것이고 이 작업이 끝나면 자동적으로 SelectedIndex나 SelectedItem설정에 대한 로직도 끝나 있는 것이죠. 그리고 그 후에 Disptcher에 등록시켜둔 list.SelectedItem = null 이라는 명령을 수행하게 되면 정상작동하게 되는게 아닐까 합니다.

  순전히 추측일뿐 정확한 이야기는 아닐 수 있습니다. 확인 방법은 ListBox 의 내부 코드를 뜯어보는 수밖에..(사실 예전에 뜯어봤는데 지금 다시 뜯어보기 귀찮아서..--;;)

  일단 간단히 SelectionChanged에서 Select 된 객체를 바꿔주거나 해제시켜주고 싶을 때는 Dispatcher를 사용하면 된다는 것입니다. 

  여기서 좀더 흥미로운 실험을 더 해보도록 하겠습니다. 신기하게도 제가 이사실을 발견하고 Gilbert에게 이 사실에 대해 알려주었을 때 Gilbert군은 그냥 SelectedItem 에 null 값을 넣어주면 아이템이 해제된다고 하더군요.. 그래서 Gilbert가 구현한 코드를 보았습니다. 신기하게도 Dispatcher를 사용하지 않고 정상작동되더군요.

  Gilbert가 구현한 부분은 여러개의 리스트 박스가 있어 여러개의 리스트 박스 중 한개의 리스트 박스에만 Selected Item이 존재하도록 하는 것이었습니다. 그래서 하나의 리스트 박스에서 SelectionChanged이벤트로 Selection이 일어났을 때 다른 ListBox객체들의 SelectedItem 값을 null로 만들어주어서 다른 리스트 박스의 선택값을 해제시켜주는 것이죠.

  Gibert군의 코드에서 이건 정말 잘 작동하였습니다. 그래서 저는 다시 테스트를 해보기로 했습니다. 이번에는 ListBox를 하나더 추가하고 이름을 "list2"라고 지어줬습니다.

   그리고 아래와 같이 코드를 구성했습니다.


  public Page()
        {
            InitializeComponent();
            list.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);
            list2.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list2.SelectionChanged += new SelectionChangedEventHandler(list2_SelectionChanged);
        }
        void list2_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
              list.SelectedItem = null;
        }
        void list_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
              list2.SelectedItem = null;
        }

결과는... 일단 처음에 list에 3을 클릭하고 list2의 4를 클릭했을 때 분명히 list의 3이 선택해제가 되었습니다. 오.. 이것은 되는 구나 했지만... 그다음에 바로 오작동이 시작되었습니다. 다시 list의 3을 클릭했을 때 list2의 4는 선택해제가 되지 않았습니다. 뿐만 아니라 그 이후에 list의 3을 클릭해도 list2의 4를 클릭해도 전혀 SelectionChaged 이벤트가 들어오지 않더군요..

  이 오작동을 가지고 Gilbert가 실제로 구현해 놓은 부분에 가서 확인을 해보았습니다. 그런데 정말 신기하게도 이런 오작동도 발견되지 않더군요. 그래서 다음처럼 구현을 해보았습니다.

public Page()
        {
            InitializeComponent();
            list.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);
            list2.ItemsSource = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
            list2.SelectionChanged += new SelectionChangedEventHandler(list_SelectionChanged);
        }
        void list_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count == 0)
                return;
            if (sender == list)
            {
                    list2.SelectedItem = null; 
            }
            else
            {

                    list.SelectedItem = null; 
            }
        }

list와 list2에서 같은 EventHandler를 사용하는 것이죠. 이렇게 하는 것이 무슨 차이점이 있는지 알 수 없지만 Gilbert군이 구현해놓은 코드에는 이런식으로 구현이 되어있었습니다.

  결과는.... 정말 잘 작동합니다.... 

  이게 잘 작동하는 이유는 정말 알 수가 없군요...


결국 정리하자면 이렇습니다.

SelectionChaged 이벤트에서 listBox의 Selection을 바꿔주고 싶을 때는 왠만하면 Dispatcher를 쓰자!!!!


그럼 모두 삽질 덜하시길...^^

                                                                                                                      - smile -
2008. 11. 5. 14:52

Wheel 지원 리스트 박스


간단하게 Wheel이 지원되는 리스트 박스 만드는 방법을 알려드리도록 하죠.^^

먼저 Wheel 을 지원 받을 수 있도록 아래 포스트에 가서 Wheel을 지원할 수 있게 하는 class를 다운 받습니다.
http://cafe.naver.com/mssilverlight/693

아니면 새로 짜도 상관은 없습니다.

그리고 아래와 같은 클래스를 만듭니다.

using System;
using System.Windows;
using System.Windows.Controls;
using HugeFlow.Interface;

namespace HugeFlow.Controls
{
    [TemplatePart(Name = WheelListBox.ElementScrollViewerName, Type = typeof(ScrollViewer))]
    public class WheelListBox : ListBox
    {
        #region ScrollOffset
        ///  
        /// Gets or sets the ScrollOffset possible Value of the double object.
        ///  
        public double ScrollOffset
        {
            get { return (double)GetValue(ScrollOffsetProperty); }
            set { SetValue(ScrollOffsetProperty, value); }
        }

        ///  
        /// Identifies the ScrollOffset dependency property.
        ///  
        public static readonly DependencyProperty ScrollOffsetProperty =
                    DependencyProperty.Register(
                          "ScrollOffset",
                          typeof(double),
                          typeof(WheelListBox),
                          null);
        #endregion ScrollOffset

        public WheelListBox() : base()
        {
            DefaultStyleKey = typeof(ListBox);
            ScrollOffset = 20;
            (new MouseWheelHelper(this)).WheelScroll += new EventHandler<MouseWheelEventArgs>(Wheel_Moved);
        }

        void Wheel_Moved(object sender, MouseWheelEventArgs e)
        {
            e.Handled = true;

            double tempOffset = ElementScrollViewer.VerticalOffset - ScrollOffset * e.Delta;

            if (tempOffset < 0)
                tempOffset = 0;
            else if (tempOffset > ElementScrollViewer.ScrollableHeight)
                tempOffset = ElementScrollViewer.ScrollableHeight;

            ElementScrollViewer.ScrollToVerticalOffset(tempOffset);
        }

        ///  
        /// Identifies the optional ScrollViewer element from the template.
        /// 
        internal ScrollViewer ElementScrollViewer { get; set; }
        private const string ElementScrollViewerName = "ScrollViewer";

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            ElementScrollViewer = GetTemplateChild(ElementScrollViewerName) as ScrollViewer;
        }
    }
}

 

어째 간단하게 보이실런지..^^ 쉽게 갔다 쓰는 용으로 프로젝트도 하나 만들어 봤습니다. 첨부합니다. 그럼 유용하게 사용하시길.^^

- smile -