1.개요
프로젝트 중 나중에 개인적으로도 사용할 만한 Custom Control 이 있어서 따로 정리하게 되었다.
물론 실제 프로젝트에 적용된 코드와 50% 이상 다르니.. 안심하고 ^^;
2. 요구사항
UX 팀에서 Microsoft Outlook 2008의 좌측 메뉴처럼 접혔다 펼쳐지는 메뉴를 원했다. 거기다가 펼쳐진 메뉴 이외의 메뉴들은 맨 하단으로 내려가게 되고, 전체 메뉴의 Height 크기는 변하지 않게 해달라고 하였다. 그리고 펼쳐지는 메뉴는 무조건 하나.
일반적으로 Expand 메뉴를 생각하면 StackPanel에 Expander Control을 여러 개 쌓고, Expander.Content의 Height 만큼 길이 제약이 없을테지만, 전체 컨트롤의 Height 크기만큼 유지 되어야 하는게 까다로웠다.
3. 개발
펼쳐지는 메뉴가 무조건 하나이기에 ListBox의 Single Select가 먼저 떠올랐다. Selector.IsSelected 프로퍼티에 Expander Control의 IsExpand 프로퍼티를 바인딩 시키면 하나만 펼쳐지기 때문이다.
3.1. ListBox Customizing
3.1.1. ListBoxItemContainerStyle
ListBox에 바인딩 될 메뉴는 ListBoxItem 으로 컨테이너가 생성되므로 Container의 Template를 수정한다.
<ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <Expander Header="{Binding Header}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true" IsExpanded="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}"> <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </Expander> </ControlTemplate> </Setter.Value> </Setter> </Style> </ListBox.ItemContainerStyle>
ControlTemplate에 Expander 를 추가하고 Expander.Content에는 ContentPresenter를 두어 별도의 Template 설정을 가능하도록 한다.
그리고 Expander의 IsExpanded 프로퍼티에 ListBoxItem의 IsSelected 프로퍼티를 바인딩한다.
또한 Expander 의 Header 프로퍼티에 적당한 값이 들어가도록 바인딩한다. 나는 ExpandableMenu를 위하여 별도의 Class를 바인딩할 계획이므로 Header라는 프로퍼티에 바인딩하였다.
3.1.2. ItemTemplate
ListBox의 ItemTemplate부분에는 실제로 SubMenu 영역이 들어가게 될테지만, 여기서는 간단하게 알아볼 수 있을 정도로만 TextBlock 하나만 놓도록 하겠다.
<ListBox.ItemTemplate> <DataTemplate> <TextBlock Margin="10" Text="{Binding Content}"/> </DataTemplate> </ListBox.ItemTemplate>
3.2. Resize Expander.Content.Height
ListBoxItem이 선택되었을때, Expander 가 Expand 되는 것까지 확인하였다. 이제 Expander.Content 의 Height 크기를 조절해야 한다.
Code behind 로 가서 ListBox의 이것저것을 손보아야 한다.
ItemsControl.ItemContainerGenerator 를 이용하면 ListBoxItemContainer의 여러가지 상황에 따라 제어할 수 있다.
이 중 StatusChanged Event를 이용하여 컨테이너가 생성되어졌을때 Visual Parent인 TreeView의 ActualHeight를 구하고, 펼쳐지지 않은 Expander들의 Height를 구하여 ListBox에서 비어 있는 영역의 Height 를 Expander.Content의 Height로 설정한다.
아래는 주요 코드이다.
protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged; } private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e) { try { var actualHeight = ActualHeight; if (ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated) { var itemElements = Items.OfType<object>(); var firstOfvalidListBoxItems = (from item in itemElements let l = ItemContainerGenerator.ContainerFromItem(item) as ListBoxItem where l.IsSelected == false && l.ActualHeight != 0 // 선택되면 Fill 되므로 선택되지 않은 ListBoxItem select l).FirstOrDefault(); if (firstOfvalidListBoxItems == null) return; var collapsedHeight = firstOfvalidListBoxItems.ActualHeight * Items.Count; foreach (var listBoxItem in itemElements) { var container = ItemContainerGenerator.ContainerFromItem(listBoxItem) as ListBoxItem; Expander listBoxItemContainerExpander = VisualTreeHelper.GetChild(container, 0) as Expander; if (listBoxItemContainerExpander == null) break; var element = listBoxItemContainerExpander.Content as FrameworkElement; if (element == null) break; var elementHeight = (actualHeight - collapsedHeight); if (elementHeight >= 0) { element.Height = elementHeight; } } } } catch (Exception ex) { Debug.WriteLine(ex); } }
3.3. ExpandableMenuItem
뭐 특별한 거 없이 원하는 Header (Menu Name), Content (Sub-Menu Name)을 설정하기 위해 별도의 클래스를 만들어서 ListBox.ItemsSource에 바인딩하였다.
public class ExpandableMenuItem { #region Properties public object Header { get; set; } public object Content { get; set; } public bool IsExpanded { get; set; } #endregion }
4. 정리
몇줄 안되는 코드로 멋진 컨트롤이 완성되었다. 여기에 추가적으로 블랜더가 스타일 좀 입히면 Telerik Control 부럽지 않는 Custom Control이 된다. 디자인 감각이 뛰어난 개발자라면 스스로 해보길 바란다. (난 디자인 감각이 없나봐 ㅠ)
전체 소스는 Github에 공개 하였다. 참고하길 바란다.