XAML or HTML

022. 비헤이비어(CallMethodAction) #3

XAML 뽀개기

이전 포스트에서 살펴본 방법은 원하는 기능을 구현하는데 문제가 없었습니다. 하지만 기능을 필요로 하는 인터페이스를 마주 때마다 반복해야하는 비효율적인 문제가 예상되었습니다. 사실 문제를 해결하는 좋은 방법은 예 포스트들에서 이미 살펴보았습니다. 바로 Style(스타일), Template(템플릿) 관련 포스팅에서 말입니다.


관련 목차

 

018. 컨트롤템플릿(ControlTemplate) #1

019. 컨트롤템플릿(ControlTemplate) #2

 

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
<Window.Resources>
    <!-- ScrollViewer 기본 템플릿 -->
    <ControlTemplate x:Key="Template_ScrollViewer" TargetType="{x:Type ScrollViewer}">
        <Grid x:Name="Grid" Background="{TemplateBinding Background}">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
 
            <Rectangle x:Name="Corner" Grid.Column="1" Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Grid.Row="1"/>
            <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" CanHorizontallyScroll="False" CanVerticallyScroll="False" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="0" Margin="{TemplateBinding Padding}" Grid.Row="0"/>
            <ScrollBar x:Name="PART_VerticalScrollBar" AutomationProperties.AutomationId="VerticalScrollBar" Cursor="Arrow" Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Grid.Row="0" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}"/>
            <ScrollBar x:Name="PART_HorizontalScrollBar" AutomationProperties.AutomationId="HorizontalScrollBar" Cursor="Arrow" Grid.Column="0" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Orientation="Horizontal" Grid.Row="1" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportWidth}"/>
 
            <!-- 바로가기 버튼 -->
            <Button x:Name="PART_ScrollToHome"
                    Content="↑" FontWeight="Bold" FontSize="18"
                    HorizontalAlignment="Right" VerticalAlignment="Bottom"
                    Margin="5">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:CallMethodAction MethodName="ScrollToHome"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
        </Grid>
    </ControlTemplate>
</Window.Resources>
 
cs


ScrollViewer의 기본 템플릿을 구하는 방법은 예전 포스트 참고합니다. 조금 복잡해 보이지만 이번에 하고자 하는 기능 구현에만 집중합니다.

 

이전 포스트에서 ScrollViewer와 함께 Grid 묶여 있던 Button ScrollViewer의 기본 템플릿 안으로 옮기면 어떨까요? 만약 그렇게 한다면 템플릿은 리소스로 정의하기 때문에 해당 템플릿 리소스를 사용하는 ScrollViewer라면 해당 요소 기능을 자동으로 가지게 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
<Button x:Name="PART_ScrollToHome"
        Content="↑" FontWeight="Bold" FontSize="18"
        HorizontalAlignment="Right" VerticalAlignment="Bottom"
        Margin="5">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <ei:CallMethodAction TargetObject="{Binding  RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}" 
                                 MethodName="ScrollToHome"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
cs

 

여기서 주의할 점은 Button의 위치가 ScrollViewer 컨트롤의 밖에서 ScrollViewer의 템플릿 안으로 변경되었기 때문에 TargetObject 속성을 제대로 찾을 없게 된다는 점입니다.

 

1
TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}"
cs

 

첫번째AncestorType(원형이 되는 타입) 지정해 찾는 방법으로 간단히 바꿔 있습니다.

 

1
TargetObject="{Binding RelativeSource={RelativeSource TemplatedParent}}"
cs

 

두번째템플릿 부모를 찾는 방법도 있습니다. 예전 포스트에서도 다룬 적이 있으니 참고하세요.

 

1
2
3
4
5
6
7
8
9
10
11
<!-- ScrollViewer 템플릿 -->
<ScrollViewer Grid.Column="1" 
              Template="{DynamicResource Template_ScrollViewer}">
    <StackPanel Margin="5">
        <TextBlock Text="A"/>
                
        .....
 
        <TextBlock Text="Z"/>
    </StackPanel>
</ScrollViewer>
cs

 

 

앞에서 완료한 ScrollViewer의 템플릿을 ScrollViewer에 적용한 태스트해봅니다.

 

그런데 목록형 컨텐츠를 표현할 ScrollViewer만 단독으로 사용하는 경우는 드뭅니다. ItemsSource 속성, DataTemplate, 아이템을 선택했을 때 상태변경  여러 이유로 ListBox 자주 사용합니다. ListView, DataGrid 컨트롤 등도 마찬가지 입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- ListBox 기본 스타일 -->
<Style x:Key="Style_ListBox" TargetType="{x:Type ListBox}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBox}">
                <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="1" SnapsToDevicePixels="true">
                    <ScrollViewer Focusable="false" Padding="{TemplateBinding Padding}" 
                                  Template="{DynamicResource Template_ScrollViewer}">
                        <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                    </ScrollViewer>
                </Border>                    
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
 
cs

 

ListBox 기본 템플릿을 예전 포스트 참고해 스타일과 함께 리소스에 정의하고 이번에 살펴보고 있는 기능 구현에만 집중하기 위해 다른 Setter Trigger 정의들은 제거했습니다.


ListBox 기본 템플릿은 ScrollViewer를 포함하고 습니다. 이전과 동일한 방법으로 ScrollViewer 템플릿을 ScrollViewer의 템플릿으로 정의합니다.

 

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
<!-- ListBox 스타일-->
<ListBox Grid.Column="2" 
         Style="{DynamicResource Style_ListBox}">
    <sys:Char>A</sys:Char>
    <sys:Char>B</sys:Char>
    <sys:Char>C</sys:Char>
    <sys:Char>D</sys:Char>
    <sys:Char>E</sys:Char>
    <sys:Char>F</sys:Char>
    <sys:Char>G</sys:Char>
    <sys:Char>H</sys:Char>
    <sys:Char>I</sys:Char>
    <sys:Char>J</sys:Char>
    <sys:Char>K</sys:Char>
    <sys:Char>L</sys:Char>
    <sys:Char>M</sys:Char>
    <sys:Char>N</sys:Char>
    <sys:Char>O</sys:Char>
    <sys:Char>P</sys:Char>
    <sys:Char>Q</sys:Char>
    <sys:Char>R</sys:Char>
    <sys:Char>S</sys:Char>
    <sys:Char>T</sys:Char>
    <sys:Char>U</sys:Char>
    <sys:Char>V</sys:Char>
    <sys:Char>W</sys:Char>
    <sys:Char>X</sys:Char>
    <sys:Char>Y</sys:Char>
    <sys:Char>Z</sys:Char>
</ListBox>
cs

 

그런 다음 ListBox의 스타일에 정의만해주면 끝입니다

 


실행해서 태스트해봅니다. 더 많은 기본 비헤이비어에 대해서는 아래 MSDN 링크를 따라 참고하세요.


샘플 코드 : https://github.com/CharlesKwon/XamlSimplified


참고 : MSDN > Working with built-in behaviors


관련 목차

 

020. 비헤이비어(Behavior) #1

021. 비헤이비어(CallMethodAction) #2

022. 비헤이비어(CallMethodAction) #3


021. 비헤이비어(CallMethodAction) #2

XAML 뽀개기

사용자에게 좋은 인터페이스 경험을 제공하기 위해 계속해서 새로운 형태의 인터페이스가 유행 또는 제안되고 있는 같습니다. 중에서 가지를 CallMethodAction 비헤이비어를 이용해 구현해 봅시다비헤이비어 이름 그대로 메소드를 호출하는 기능을 하는 비헤이비어입니다.

 


예제로 사용할 것은 내용(Contents) 아주 길 때 흔히 제공되 인터페이스입니다. 스크롤을 하는 도중 다시 처음으로 돌아가려면 많은 스크롤 양에 불편함을 느낄 때가 종종 있습니다. 그럴 사용하면 알맞은 인터페이스입니다. 처음으로 바로가는 버튼을 제공하 아주 간단한 기능입니다.

 

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
<Grid>
    <ScrollViewer x:Name="Target_ScrollViewer">
        <StackPanel Margin="5">
            <TextBlock Text="A"/>
            <TextBlock Text="B"/>
            <TextBlock Text="C"/>
            <TextBlock Text="D"/>
            <TextBlock Text="E"/>
            <TextBlock Text="F"/>
            <TextBlock Text="G"/>
            <TextBlock Text="H"/>
            <TextBlock Text="I"/>
            <TextBlock Text="J"/>
            <TextBlock Text="K"/>
            <TextBlock Text="L"/>
            <TextBlock Text="M"/>
            <TextBlock Text="N"/>
            <TextBlock Text="O"/>
            <TextBlock Text="P"/>
            <TextBlock Text="Q"/>
            <TextBlock Text="R"/>
            <TextBlock Text="S"/>
            <TextBlock Text="T"/>
            <TextBlock Text="U"/>
            <TextBlock Text="V"/>
            <TextBlock Text="W"/>
            <TextBlock Text="X"/>
            <TextBlock Text="Y"/>
            <TextBlock Text="Z"/>
        </StackPanel>
    </ScrollViewer>
 
    <Button Content="↑" FontWeight="Bold" FontSize="18"
            HorizontalAlignment="Right" VerticalAlignment="Bottom"
            Margin="5,5,22,5">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="Click">
                <ei:CallMethodAction TargetObject="{Binding ElementName=Target_ScrollViewer}" 
                                     MethodName="ScrollToHome"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Button>
</Grid>
 
cs

 

ScrollViewer와 Button Grid 안에 구성했습니다. 그런 다음 이전 포스트에서 이야기 일반적인 방법으로 비헤이비어를 버튼에 설정합니다Grid 사용한 것은 별 다른 의미는 없고 정리하는 의미에서 그룹을 지은 것뿐 입니다

 

 

ScrollViewer 컨트롤은 스크롤과 관련된 함수(Method)를 포함하고 있습니다. 이런 정보를 바탕으로 비헤이비어에 여러 속성을 정의할 수 있습니다.

 

1
2
3
4
5
6
<i:Interaction.Triggers>
    <i:EventTrigger EventName="Click">
        <ei:CallMethodAction TargetObject="{Binding ElementName=Target_ScrollViewer}" 
                             MethodName="ScrollToHome"/>
    </i:EventTrigger>
</i:Interaction.Triggers>
cs

 


ElementName 문법을 이용해 TargetObject 속성을 정의하고 MethodName 속성에는 의도에 맞게 ScrollToHome 함수를 정의합니다. 트리거 버튼의 Click 이벤트로 되어 있어 앞서 정의한 액션이 실행되게 됩니다. 간단합니다.


이러한 비헤이비어의 속성 정의는 Blend for VS 속성 윈도우를 이용하면 XAML 문법에 익숙치 않아도 쉽게 정의할 있습니다.

 

그러면 기능을 필요로 하는 인터페이스마다 매번 버튼을 추가하고 하위에 비헤이비어를 정의해야할까요? 굉장히 비효율적일 같습니다. 다음 포스트에서 이를 해결해 봅시다.


샘플 코드 : https://github.com/CharlesKwon/XamlSimplified


참고 : MSDN > Working with built-in behaviors


관련 목차

 

020. 비헤이비어(Behavior) #1

021. 비헤이비어(CallMethodAction) #2

022. 비헤이비어(CallMethodAction) #3


020. 비헤이비어(Behavior) #1

XAML 뽀개기

Behavior 행동, 반응이란 뜻을 가지고 있습니다. 특정 상황, 조건(Trigger) 등에 따라 UI 요소에 어떤 영향을 줄  주로 사용됩니다. 사용자가 커스텀 비헤이비어를 만들어 기능을 확장할 수도 있습니다.


일단 비헤이비어를 사용하려면 System.Windows.Interactivity 어셈블리(dll) 참조가 필요합니다. 기본으로 제공되는 비헤이비어는 자주 사용되므로 Microsoft.Expression.Interactions 어셈블리도 함께 참조합니다.

 

 

Visual Studio 설치시 설정할 있는 옵션에서 SDK 제외되었다면 Blend for VS Assets(자산) 윈도우에서 기본으로 제공되는 비헤이비어가 보이지 않을 있습니다.

 

 

비헤이비어가 보이지 않는다면 Visual Studio Installer 이용해 SDK 설치합니다.

 

 

C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\.NETFramework\v4.5\Libraries\System.Windows.Interactivity.dll

 

이미 설치되어 있으면 경로를 참고해 수동으로 참조시켜도 됩니다.

 

 

참조 및 빌드를 후에는 기본으로 제공되는 비헤이비어를 Blend for VS에서 손쉽게 사용할 있습니다. 일반적인 사용 방법은 디자인뷰에 사용하길 원하는 UI 컨트롤 요소에 마우스로 drag & drop(드래그 드롭)하는 것입니다. 관련 어셈블리도 자동으로 참조됩니다.

 

1
2
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
cs

       

 방법을 이용하면 XAML 코드에 네임스페이스 등록도 자동으로 이루어 집니다. 여기서 접두사는 본인 취향에 맞게 수정해도 됩니다.

 

 

1
2
3
4
5
6
7
8
9
<Button Content="↑" FontWeight="Bold" FontSize="18"
        HorizontalAlignment="Right" VerticalAlignment="Bottom"
        Margin="5,5,22,5">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <ei:CallMethodAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
cs

 

비헤이비어는 드래그 앤 드롭하는 순간 UI 요소 하위에 자동으로 생성됩니다. 자동으로 생성된 XAML 코드를 살펴보면 Trigger(트리거) Action(액션)으로 구성되어 있습니다.

 

 

비헤이비어를 선택한 상태에서는 속성 윈도우에서 비헤이비어의 다양한 속성을 정의할 있습니다. 정의하지 않은 트리거의 Source 속성들은 트리 구조내 부모 요소 암시적으로 따릅니다.

 

 

트리거 타입은 새로 만들기 버튼을 눌러 변경할 있습니다.


다음 포스트에서는 나름 의미있는 예제를 들어 살펴보겠습니다.


샘플 코드 : https://github.com/CharlesKwon/XamlSimplified


참고 : MSDN > Working with built-in behaviors


관련 목차

 

020. 비헤이비어(Behavior) #1

021. 비헤이비어(CallMethodAction) #2

022. 비헤이비어(CallMethodAction) #3


019. 컨트롤템플릿(ControlTemplate) #2

XAML 뽀개기

이번 포스트에서는 템플릿 수정과 다양한 템플릿의 효율적인 사용방법에 대해 알아봅니다.

 

1
2
3
4
5
6
7
<HeaderedContentControl Header="재생" Grid.Column="1"
                        Style="{DynamicResource Style_HeaderedContentControl}">
    <StackPanel>
        <Button Content="시작"/>
        <Button Content="정지"/>
    </StackPanel>
</HeaderedContentControl>
cs


이전 포스트에서 Blend for VS [템플릿 편집 > 복사본 편집기능을 이용해 HeaderedContentControl의 기본 템플릿을 구했습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<Style x:Key="Style_HeaderedContentControl" TargetType="{x:Type HeaderedContentControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type HeaderedContentControl}">
                <Border Background="WhiteSmoke" 
                        BorderBrush="DarkGray" 
                        BorderThickness="1" 
                        Margin="5">
                    <StackPanel Background="WhiteSmoke" 
                                Margin="5">
                        <ContentPresenter ContentSource="Header" 
                                          TextBlock.FontWeight="Bold" 
                                          Margin="0,0,0,5"/>
                        <ContentPresenter />
                    </StackPanel>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
cs

 

이전 포스트에서 작성했던 스타일링 작업의 XAML 코드를 참고해 템플릿을 수정했습니다. HeaderedContentControl의 구조와 외형을 변경하는 작업입니다.

 

1
2
3
4
5
6
7
<HeaderedContentControl Header="재생"
                        Style="{DynamicResource Style_HeaderedContentControl}">
    <StackPanel>
        <Button Content="시작"/>
        <Button Content="정지"/>
    </StackPanel>
</HeaderedContentControl>
cs

           

HeaderedContentControl에 스타일을 각각 선언 수도 있지만 StackPanel 아래에 리소스로 스타일을 정의하면서 Key 이름을 생략하고 BasedOn 속성을 이용해 일괄적으로 하위 컨트롤에 영향을 미칠  있습니다

 

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
<StackPanel Grid.Column="1">
    <StackPanel.Resources>
        <Style TargetType="HeaderedContentControl"
               BasedOn="{StaticResource Style_HeaderedContentControl}"/>
    </StackPanel.Resources>
            
    <HeaderedContentControl Header="재생">
        <StackPanel>
            <Button Content="시작"/>
            <Button Content="정지"/>
        </StackPanel>
    </HeaderedContentControl>
    <HeaderedContentControl Header="트랙">
        <StackPanel>
            <Button Content="이전"/>
            <Button Content="다음"/>
        </StackPanel>
    </HeaderedContentControl>
    <HeaderedContentControl Header="소리">
        <StackPanel>
            <Button Content="작게"/>
            <Button Content="크게"/>
        </StackPanel>
    </HeaderedContentControl>
</StackPanel>
cs



  절약하고 깔끔한 코드를 얻게 되었습니다. 이전 포스트에서도 다룬 적이 있으니 참고하세요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<StackPanel.Resources>
    <ControlTemplate x:Key="Template_HeaderedContentControl" 
                     TargetType="{x:Type HeaderedContentControl}">
        <Border Background="WhiteSmoke" 
                BorderBrush="DarkGray" 
                BorderThickness="1" 
                Margin="5">
            <StackPanel Background="WhiteSmoke" 
                        Margin="5">
                <ContentPresenter ContentSource="Header" 
                                  TextBlock.FontWeight="Bold" 
                                  Margin="0,0,0,5"/>
                <ContentPresenter />
            </StackPanel>
        </Border>
    </ControlTemplate>
</StackPanel.Resources>
cs


1
2
3
4
5
6
7
<HeaderedContentControl Header="재생"
                        Template="{DynamicResource Template_HeaderedContentControl}">
    <StackPanel>
        <Button Content="시작"/>
        <Button Content="정지"/>
    </StackPanel>
</HeaderedContentControl>
cs


 

스타일이 아니더라도 템플릿을 변경할 있습니다. 말인 즉슨 스타일의 ControlTemplate을 단독으로도 사용할 있다는 것입니다. 리소스로 정의해 사용하는 방법 동일하기 때문에 Key 이름 선언이 필수입니다.

 

1
2
3
4
5
6
7
<!--HeaderedContentControl의 Content 템플릿-->
<DataTemplate x:Key="DataTemplate_Content">
    <StackPanel Orientation="Horizontal">
        <Button Content="이전" Width="50" Margin="0,0,5,0"/>
        <Button Content="다음" Width="50"/>
    </StackPanel>
</DataTemplate>
cs

 

1
2
3
4
5
6
7
8
<HeaderedContentControl Header="트랙"
                        Template="{DynamicResource Template_HeaderedContentControl}"
                        ContentTemplate="{DynamicResource DataTemplate_Content}">
    <!--<StackPanel>
        <Button Content="이전"/>    
        <Button Content="다음"/>        
    </StackPanel>-->
</HeaderedContentControl>
cs

 

 

Content만의 템플릿을 새로이 정의할 수도 있습니다. 이 템플릿은 DataTemplate이라고 합니다. DataTemplate은 이전 ControlTemplate의 하위 템플릿이라고 할  있습니다. DataTemplate 다른 포스트에서 다시 살펴보겠습니다.

 

1
2
3
4
5
6
7
<!--HeaderedContentControl의 Header 템플릿-->
<DataTemplate x:Key="DataTemplate_Header">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="※" Margin="0,0,5,0" Foreground="Red"/>
        <TextBlock Text="{Binding }" Foreground="Red"/>
    </StackPanel>
</DataTemplate>
cs

 

1
2
3
4
5
6
7
8
<HeaderedContentControl Header="소리" 
                        Template="{DynamicResource Template_HeaderedContentControl}"
                        HeaderTemplate="{DynamicResource DataTemplate_Header}">
    <StackPanel>
        <Button Content="작게"/>
        <Button Content="크게"/>
    </StackPanel>
</HeaderedContentControl>
cs

 

 

앞서 본 Content의 템플릿과 동일한 방법으로 Header만의 템플릿을 새로이 정의할 수도 있습니다.

 

Template, ContentTemplate, HeaderTemplate 모두 템플릿이라 있습니다. Template 기본 골격(외형)이라면 ContentTemplate, HeaderTemplate 아래 골격(외형)이라 있겠습니다. ContentPresenter의 Content 속성이 암시적 바인딩이 작용한 것과 같이 템플릿 속성도 암시적으로 바인딩됩니다.

 

지금까지 예로  템플릿 사용 방법들은 기본 골격은 같은데 특정 일부만 변경을 필요로 할  하위 템플릿을 새로이 정의해서 리소스(템플릿) 효율적으로 사용 가능케 합니다.


관련 목차


018. 컨트롤템플릿(ControlTemplate) #1

019. 컨트롤템플릿(ControlTemplate) #2

018. 컨트롤템플릿(ControlTemplate) #1

XAML 뽀개기

ControlTemplate 한마디로 정의하자면 외형바꾸기입니다. 모든 컨트롤은 ControlTemplate 가질 있습니다. 다시 말하자면 System.Windows.Controls.Control을 상속 받은 컨트롤은 템플릿을 가질 수 있습니다.

 

물론 아주 간단한 외형 변경은 Style 이용해 바꿀 수도 있습니다. 하지만 다른 요소들을 추가하는  복잡하고 섬세한 수준의 외형 변경은 한계가 있습니다. 다시 말해 버튼의 외형에 간단한 도형이라도 추가하려면 스타일을 이용해서는 불가능에 가깝습니다.

 

Button 외형바꾸기는 지겨우니깐 단계 복잡한 컨트롤을 예를 들어 살펴봅시다.

 

 

Header Content 그룹으로 묶여있는 구조입니다. 위와 같이 표현하는 방법에는 어떤 것들이 있을까요?

 

1
2
3
4
5
<StackPanel Margin="15,10">                
    <TextBlock Text="재생" FontWeight="Bold" Margin="5"/>                
    <Button Content="시작"/>                
    <Button Content="정지"/>            
</StackPanel>
cs

 

아무래도 위와 같은 XAML 코드가 가장 빠르고 적당할 같습니다.

 

 

만약 위와 같은 형식이 몇 번이고 반복된다면 어떨까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<StackPanel>
    <StackPanel Margin="15,10">
        <TextBlock Text="재생" FontWeight="Bold" Margin="5"/>
        <Button Content="시작"/>
        <Button Content="정지"/>
    </StackPanel>
    <StackPanel Margin="15,10">
        <TextBlock Text="트랙" FontWeight="Bold" Margin="5"/>
        <Button Content="이전"/>
        <Button Content="다음"/>
    </StackPanel>
    <StackPanel Margin="15,10">
        <TextBlock Text="소리" FontWeight="Bold" Margin="5"/>
        <Button Content="작게"/>
        <Button Content="크게"/>
    </StackPanel>
</StackPanel>
cs


StackPanel 각 그룹 요소를 감싼 후 반복해서 코드를 작성하면 되겠습니다. 하지만 여기에 디자인을 변경하기 위한 스타일링 작업이 추가된다면 어떨까요?

 

 

Border 요소를 추가하고 Background, BorderBrush, BorderThickness, Margin 속성 등을 이용해 아주 살짝 스타일을 다듬어주었습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<StackPanel>
    <Border Background="WhiteSmoke" BorderBrush="DarkGray" BorderThickness="1" Margin="5">
        <StackPanel Margin="5" Background="WhiteSmoke">
            <TextBlock Text="재생" FontWeight="Bold" Margin="0,0,0,5"/>
            <Button Content="시작"/>
            <Button Content="정지"/>
        </StackPanel>
    </Border>
    <Border Background="WhiteSmoke" BorderBrush="DarkGray" BorderThickness="1" Margin="5">
        <StackPanel Margin="5">
            <TextBlock Text="트랙" FontWeight="Bold" Margin="0,0,0,5"/>
            <Button Content="이전"/>
            <Button Content="다음"/>
        </StackPanel>
    </Border>
    <Border Background="WhiteSmoke" BorderBrush="DarkGray" BorderThickness="1" Margin="5">
        <StackPanel Margin="5">
            <TextBlock Text="소리" FontWeight="Bold" Margin="0,0,0,5"/>
            <Button Content="작게"/>
            <Button Content="크게"/>
        </StackPanel>
    </Border>
</StackPanel>
cs

 

아주 간단한 스타일 변경이지만 그룹 및 그룹 내 요소의 갯수만큼 반복해서 XAML 코드가 늘어납니다. 말인 즉슨 디자인이 변경될 경우 그룹 및 그룹 내 요소의 갯수만큼 코드를 변경해야 한다는 말과 같습니다. 자 그러면 그런 비효율적인 일 최소화할 있을까요? 하나하나씩 고쳐보겠습니다.



1
2
3
4
5
6
<HeaderedContentControl Header="재생" Grid.Column="1">
    <StackPanel>
        <Button Content="시작"/>
        <Button Content="정지"/>
    </StackPanel>
</HeaderedContentControl>
cs

 

먼저 HeaderedContentControl을 사용하면 Header Content가 짝으로 이루어진 그룹을 표현할 있습니다. 컨트롤 이름에서 어느 정도 짐작이 됩니다. 그런데 어떻게 이런 기본 구조가 표현되는지 궁금할겁니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<Style x:Key="Style_HeaderedContentControl" TargetType="{x:Type HeaderedContentControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type HeaderedContentControl}">
                <StackPanel>
                    <ContentPresenter ContentSource="Header"/>
                    <ContentPresenter />
                </StackPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
 
cs

 

Blend for VS [템플릿 편집 > 복사본 편집] 기능을 이용하면 아주 간단히 HeaderedContentControl의 기본 ControlTemplate 얻을 있습니다. 자동으로 생성된 코드를 살펴보면 템플릿은 스타일에 속한다고 볼  있습니다. 단독으로 사용될 수도 있습니다만 다음 기회에 살펴봅시다.

 

2개의 ContentPresenter Header Content 역할을 하는 것을 알  있습니다. 단순히 StackPanel 이용해 헤더와 컨텐츠를 세로로 정렬하고 있는 구조입니다. 이전 포스트에서 ContentPresenter 암시적으로 바인딩된다고 이야기했습니다. 그리고 Header 분리하기 위해 ContentSource 속성을 이용해 명시적으로 정의하고 있습니다. Content 속성에 TemplateBinding 문법을 이용해 명시적으로 정의할 수도 있습니다.

 

이렇게 ControlTemplate 컨트롤의 기본 구조 정의와 함께 외형을 정의하는 역할을 합니다. 다른 요소들을 추가하거나 다양한 속성들을 정의해  복잡하고 새로운 외형을 스타일링  있습니다. 다음 포스트에 이어 템플릿을 수정해 보도록 하겠습니다.


관련 목차


018. 컨트롤템플릿(ControlTemplate) #1

019. 컨트롤템플릿(ControlTemplate) #2

017. 두개의 콘텐츠 버튼(Two Contents Button) #3

XAML 뽀개기

AttachedProperty.cs : 결합 속성


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AP_TwoContentsButton
{
    #region SecondContent
 
    public static readonly DependencyProperty SecondContentProperty =
        DependencyProperty.RegisterAttached(
            "SecondContent",
            typeof(string),
            typeof(AP_TwoContentsButton),
            new PropertyMetadata(string.Empty));
 
    public static string GetSecondContent(DependencyObject dp)
    {
        return (string)dp.GetValue(SecondContentProperty);
    }
 
    public static void SetSecondContent(DependencyObject dp, string value)
    {
        dp.SetValue(SecondContentProperty, value);
    }
 
    #endregion
}
cs


이번엔 Attached Property 이용해 봅시다. 먼저 Dependency Property와는 달리 Button 컨트롤을 상속 받을 필요 없이 새로운 클래스를 만들었습니다. 이전 Class 명과 구분하기 위해 보기는 싫지만 AP_ 덧붙여 작성했습니다.


1
xmlns:atcp="clr-namespace:Sample.AttachedProperty"
cs


XAML 코드에서 결합 속성을 사용하기 위해 네임스페이스를 추가합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Button Content="Menu03" atcp:AP_TwoContentsButton.SecondContent="Lorem Ipsum is simply" Grid.Column="2">
    <Button.Template>
        <ControlTemplate TargetType="Button">
            <Border BorderBrush="{TemplateBinding BorderBrush}" 
                    BorderThickness="{TemplateBinding BorderThickness}" 
                    Background="{TemplateBinding Background}">
                <StackPanel HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                            VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                            Margin="5">
                    <ContentPresenter Content="{TemplateBinding Content}"/>
                    <TextBlock Text="{Binding (atcp:AP_TwoContentsButton.SecondContent), RelativeSource={RelativeSource TemplatedParent}}" TextWrapping="Wrap"/>
                </StackPanel>
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>
cs


기존 Button 컨트롤에 atcp:AP_TwoContentsButton.SecondContent 결 속성을 정의합니다. 결합 속성은 [정의타입].[속성명] 형태의 문법을 사용합니다.


1
<TextBlock Text="{Binding (atcp:AP_TwoContentsButton.SecondContent), RelativeSource={RelativeSource TemplatedParent}}" TextWrapping="Wrap"/>
cs


이전 의존 속성에서 사용한 TextBlock 코드를 재사용합니다. Binding 문법의 Path 속성만 추가한 결합속성에 알맞게 수정합니다. 간혹 Visual Studio에서 에러가 발생하는데 VS 끄고 다시 켜면 에러가 나지 않습니다. 생각에는 버그인 합니다.



동일한 표현이 가능합니다.



왼쪽이 결합 속성, 오른쪽이 의존 속성입니다. 결합 속성과 의존 속성을 속성 창에서 찾아 비교해보면 의존 속성만 노출되는 것을 알 수 있습니다. 당연한 결과라 생각할 수 있습니다. 결합 속성은 자신이 정의된 클래스가 아닌 다른 클래스에서 적용될 수 있다는 점이 다릅니다. 이전 결합 속성 포스트에서 레이아웃을 정의하는데 사용된다고 이야기했었습니다. 


결합 속성은 레이아웃 정의 뿐만 아니라 무한 확장 기능 구현에 큰 기능을 합니다. 좀 더 속 깊은 설명은 박문찬 MVP님의 블로그 포스트를 참고하시면 좋을 듯 합니다.


Dependency Property와 Attached Property의 차이점이 뭐지? #1

Dependency Property와 Attached Property의 차이점이 뭐지? #2


샘플 코드 : https://github.com/CharlesKwon/XamlSimplified


관련 목차


015. 두개의 컨텐츠 버튼(Two contents button) #1

016. 두개의 컨텐츠 버튼(Two contents button) #2

017. 두개의 컨텐츠 버튼(Two contents button) #3

016. 두개의 콘텐츠 버튼(Two Contents Button) #2

XAML 뽀개기

TwoContentsButton.cs : 의존 속성


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TwoContentsButton : Button
{
    #region SecondContent : 두번째 Content
 
    public string SecondContent
    {
        get { return (string)GetValue(SecondContentProperty); }
        set { SetValue(SecondContentProperty, value); }
    }
 
    public static readonly DependencyProperty SecondContentProperty =
        DependencyProperty.Register(
            "SecondContent"
            typeof(string), 
            typeof(TwoContentsButton), 
            new PropertyMetadata(string.Empty));
 
    #endregion
}
cs


Button 컨트롤을 상속 받아서 TwoContentsButton이라는 새로운 컨트롤을 만드는 방법입니다. 이전 포스트에서 알아본 Dependency Property 이용해 두번째 Content 속성을 추가할 있습니다.


1
xmlns:ctr="clr-namespace:Sample.Control"
cs


 컨트롤을 만든 것이므로 XAML에서 사용하려면 네임스페이스 추가가 필요합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<ctr:TwoContentsButton Content="Menu03" Margin="20" Grid.Column="2">
    <Button.Template>
        <ControlTemplate TargetType="Button">
            <Border BorderBrush="{TemplateBinding BorderBrush}" 
                    BorderThickness="{TemplateBinding BorderThickness}" 
                    Background="{TemplateBinding Background}">
                <StackPanel HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                            VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                            Margin="5">
                    <ContentPresenter Content="{TemplateBinding Content}"/>
                    <TextBlock Text=" " TextWrapping="Wrap"/>
                </StackPanel>
            </Border>
        </ControlTemplate>
    </Button.Template>
</ctr:TwoContentsButton>
cs


기존 버튼도  버튼에 맞춰 변경해야 합니다. 점점 수정해야 것들이 늘어납니다.



빌드 과정을 거친 후에는 IntelliSense 도움을 받아 SecondContent 속성을 정의할 있습니다


1
<TextBlock Text="{TemplateBinding SecondContent}" TextWrapping="Wrap"/>
cs


의존 속성을 정의한 후에는 두번째 Content 역할을 TextBlock 연결해줘야 겠습니다. 기존 ContentPresenter처럼 TemplateBinding 문법을 사용할 수는 없습니다. 빌드 자체가 되지 않습니다.


1
<TextBlock Text="{Binding SecondContent, RelativeSource={RelativeSource TemplatedParent}}" TextWrapping="Wrap"/>
cs


Binding 문법에서 템플릿 부모를 Source 지정하는 방법으로 SecondContent 속성에 접근할 있습니다.



샘플 코드 : https://github.com/CharlesKwon/XamlSimplified


관련 목차


015. 두개의 컨텐츠 버튼(Two contents button) #1

016. 두개의 컨텐츠 버튼(Two contents button) #2

017. 두개의 컨텐츠 버튼(Two contents button) #3