XAML or HTML

015. 두개의 콘텐츠 버튼(Two Contents Button) #1

XAML 뽀개기


위와 같이 2개의 Content가 사용되는 버튼 컨트롤은 실제 프로젝트에서 흔히 사용됩니다. 기본으로 제공되는 버튼 컨트롤은 Content 속성이 하나 밖에 제공되지 않지만 Object 타입이기 때문에 아래와 같은 방법으로 흔히 표현할 수 있습니다.


1
2
3
4
5
6
7
8
<Button>
    <Button.Content>
        <StackPanel Margin="5">
            <TextBlock Text="Menu01" FontWeight="Bold"/>
            <TextBlock Text="Lorem Ipsum is simply" TextWrapping="Wrap"/>
        </StackPanel>
    </Button.Content>
</Button>
cs


1
2
3
4
5
6
<Button>
    <StackPanel Margin="5">
        <TextBlock Text="Menu01" FontWeight="Bold"/>
        <TextBlock Text="Lorem Ipsum is simply" TextWrapping="Wrap"/>
    </StackPanel>
</Button>
cs


가장 간단한 방법은 위와 같지 않을까 생각됩니다. 하지만 버튼 컨트롤 자체의 Style과 Template에 영향을 받을 수 없는 한계가 있습니다. 물론 TextBlock 자체에 Style를 입힐 수는 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<Button Content="Menu01">
    <Button.Template>
        <ControlTemplate TargetType="Button">
            <Border BorderBrush="{TemplateBinding BorderBrush}" 
                    BorderThickness="{TemplateBinding BorderThickness}" 
                    Background="{TemplateBinding Background}" 
                    SnapsToDevicePixels="True">
                <ContentPresenter x:Name="contentPresenter" 
                    ContentTemplate="{TemplateBinding ContentTemplate}" 
                    Content="{TemplateBinding Content}" 
                    ContentStringFormat="{TemplateBinding ContentStringFormat}" 
                    Focusable="False" 
                    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                    Margin="{TemplateBinding Padding}" 
                    RecognizesAccessKey="True" 
                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>
cs


기본으로 제공되는 버튼의 템플릿은 위와 같습니다. 약간 복잡해 보입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Button Content="Menu02" Grid.Column="1">
    <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}"/>
                </StackPanel>
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>
cs


이번 포스트 주제에만 집중할 수 있도록 예제 코드를 간략히 수정했습니다. 2개의 콘텐츠를 세로로 정렬하기 위해 StackPanel로 ContentPresenter를 감싸주었습니다.


Content="{TemplateBinding Content}" 코드도 생략할 수있습니다. ContentPresenter의 Content는 암시적으로 바인딩되기 때문에 가능합니다. 이 예제에서는 일부러 생락하지 않습니다. ContentTemplate과 ContentPresenter는 다른 포스트에서 다룰 예정입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Button Content="Menu02" Tag="Lorem Ipsum is simply" Grid.Column="1">
    <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="{TemplateBinding Tag}" TextWrapping="Wrap"/>
                </StackPanel>
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>
cs


위 코드는 FrameworkElement를 상속 받은 모든 컨트롤들이 공통적으로 가지고 있는 Tag 속성을 이용하는 예제 코드입니다. 두번째 Content를 추가하기 위해 ContentPresenter를 하나 더 추가해도 되지만 TextWrapping 속성을 이용해 줄내림 효과를 바로 얻기 위해 TextBlock 컨트롤을 추가했습니다. 그런 후 Tag 속성과 TextBlock 컨트롤의 Text 속성을 TemplateBinding 문법을 이용해 결합했습니다.



이런 간단한 방법으로도 2개의 컨텐츠를 표현할 수 있습니다. 하지만 Tag 속성은 의미적(Semantic)으로 적합하지 않고 일부 개발하는 과정에서 다른 용도로 사용될 수 있는 가능성이 있으므로 이렇게 사용하는 것은 프로토타입니나 샘플 코드에서나 적합할 것입니다.


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


관련 목차


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

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

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

014. 결합 속성(Attached property)

XAML 뽀개기

AttachedProperty.cs : 의존 속성

 

샘플 프로젝트에 AttachedProperty.cs 파일을 추가하고 SetFontPanel 클래스를 작성합니다.


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
#region ChildrenFontSize
 
public static readonly DependencyProperty ChildrenFontSizeProperty =
    DependencyProperty.RegisterAttached(
        "ChildrenFontSize",
        typeof(double),
        typeof(SetFontPanel),
        new PropertyMetadata((double)12, OnChildrenFontSizePropertyChanged));
 
public static double GetChildrenFontSize(DependencyObject dp)
{
    return (double)dp.GetValue(ChildrenFontSizeProperty);
}
 
public static void SetChildrenFontSize(DependencyObject dp, double value)
{
    dp.SetValue(ChildrenFontSizeProperty, value);
}
 
private static void OnChildrenFontSizePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    var panel = sender as Panel;
    if (panel == nullreturn;
 
    SetFontProperties(panel);
}
 
#endregion
 
cs


ChildrenFontSize라는 Attached Property 작성했습니다. Attached property Dependency property입니다. Dependency property Register 함수 정의하고 Attached property RegisterAttached 함수 정의하는 차이가 있습니다. Get Set 함수를 갖고 있는 점도 특징입니다. 자세한 내용은 박문찬 MVP님의 블로그를 참고하세요.

 

추가로 PropertyChanged 이벤트가 발생할 때마다 SetFontProperties 함수가 호출되도록 했습니다.

 

#region ChildrenForeground

 

.....

 

#endregion

 

ChildrenForeground라는 결합 속성도 추가했지만 반복되는 내용이므로 설명은 생략합니다.

FontFamily, FontWeight, FontStyle, FontStretch 사용하고자하는 결합 속성들이 추가될 있습니다.

 

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
#region Attach
 
public static readonly DependencyProperty AttachProperty =
    DependencyProperty.RegisterAttached(
        "Attach",
        typeof(bool),
        typeof(SetFontPanel),
        new PropertyMetadata(false, OnAttachPropertyChanged));
 
public static bool GetAttach(DependencyObject dp)
{
    return (bool)dp.GetValue(AttachProperty);
}
 
public static void SetAttach(DependencyObject dp, bool value)
{
    dp.SetValue(AttachProperty, value);
}
 
private static void OnAttachPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    var panel = sender as Panel;
    if (panel == nullreturn;
 
    if ((bool)e.NewValue)
    {
        panel.Loaded += panel_Loaded;
    }
    else
    {
        panel.Loaded -= panel_Loaded;
    }
}
 
#endregion
 
cs


Loaded 이벤트일 한번만 SetFontProperties 함수를 호출하기 위해서 Attach라는 bool형의 결합 속성도 추가했습니다.

 

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
#region Method
 
private static void SetFontProperties(Panel panel)
{
    foreach (var child in panel.Children)
    {
        if (child == nullreturn;
 
        if (child is TextBlock)
        {
            var txt = child as TextBlock;
            if (txt == nullreturn;
 
            txt.FontSize = GetChildrenFontSize(panel);
            txt.Foreground = GetChildrenForeground(panel);
            // FontFamily
            // FontWeight
            // FontStyle
            // FontStretch
        }
        else if (child is Control)
        {
            var ctr = child as Control;
            if (ctr == nullreturn;
 
            ctr.FontSize = GetChildrenFontSize(panel);
            ctr.Foreground = GetChildrenForeground(panel);
            // FontFamily
            // FontWeight
            // FontStyle
            // FontStretch
        }
        //else if (또 다른 컨트롤을 추가)
        //{
 
        //}
    }
}
 
#endregion
cs

 

Panel 안에 들어간 컨트롤들을 모두 찾아 앞에서 추가한 결합 속성들을 알맞은 속성에 대입해주는 함수를 작성했습니다. 파라미터를 Panel 지정해 WrapPanel, UniformGrid Panel 상속 받은 레이아웃 패널이라면 범용적으로 사용 가능하도록 했습니다.

 

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

 

앞에서 작성한 결합 속성을 XAML에서 사용하기 위해 네임스페이스를 선언합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Attached property -->
<StackPanel x:Name="spParent" 
            HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="1"
            atcp:SetFontPanel.ChildrenFontSize="15" 
            atcp:SetFontPanel.ChildrenForeground="Red" 
            atcp:SetFontPanel.Attach="True">
            <!-- [Prefix].[ClassName].[AttachedProperty] -->
    <Label Content="Attached property"/>
    <TextBlock Text="Lorem Ipsum is simply "/>
    <TextBlock Text="dummy text of the"/>
    <TextBlock Text="printing and typesetting "/>
    <TextBlock Text="industry."/>
</StackPanel>
cs

 

결합 속성을 사용하는 방법은 위와 같습니다. "[접두사].[클래스명].[결합속성명]"


 

부모가 자식들을 일괄적으로 Font 관련 속성들을 정의할 있게 되었습니다. 결합 속성이 일반 속성과 다른 점은 직접 사용하는 속성이 아니라 다른 요소가 사용하도록 하는 점입니다. 이 예제에서는 자식들이 그 결합 속성을 사용하도록 했습니다. 


이 예제에서 추가한 결합 속성은 사용자가 정의한 것 입니다. 사실 우리는 이미 많은 결합 속성을 사용하고 있습니다. 예를 들어 보겠습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
 
    <TextBlock Grid.Column="1"/>
</Grid>
 
<Canvas>
    <TextBlock Canvas.Left="100" Canvas.Top="50"/>
</Canvas>
cs

부모 Grid의 자식인 TextBlock에 선언된 Grid.Column 속성과 부모 Canvas의 자식인 TextBlock에 선언된 Canvas.Left 속성이 바로 결합속성입니다. 자신에게 없는 속성이지만 각 패널 안에서 위치를 정의하는데 사용되고 있습니다.


이처럼 결합속성은 자신이 가지고 있지 않은 속성을 정의해 다양한 방식으로 응용될 수 있습니다. 위 예제에서는 부모가 자식의 위치를 직접 정의해줄 수 없기 떄문에 자식 스스로가 부모의 결합속성을 이용해 자신의 위치를 정의하고 있는 것입니다.


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

013. 속성 상속(Resolved Dynamically)

XAML 뽀개기

프로젝트에서 XAML 코드 작업을 하다보면 점점 늘어나는 코드 양에 스트레스를 받을 때가 간혹 있습니다. Style 묶어 관리하기도 하고 Resource Dictionary 파일을 여러 나누어 관리도 하면서 나름대로의 효율을 찾아서 자신만의 스타일을 갖게 됩니다.

       

1
2
3
4
5
6
7
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
    <Label Content="Local property" FontSize="12" FontFamily="Gulim" FontWeight="Bold" FontStyle="Italic"/>
    <TextBlock Text="Lorem Ipsum is simply " FontSize="12" FontFamily="Gulim" FontWeight="Bold" FontStyle="Italic"/>
    <TextBlock Text="dummy text of the" FontSize="12" FontFamily="Gulim" FontWeight="Bold" FontStyle="Italic"/>
    <TextBlock Text="printing and typesetting " FontSize="12" FontFamily="Gulim" FontWeight="Bold" FontStyle="Italic"/>
    <TextBlock Text="industry." FontSize="12" FontFamily="Gulim" FontWeight="Bold" FontStyle="Italic"/>
</StackPanel>
cs

 

 

이해를 돕기위해 극단적으로 예제를 들고자합니다. 텍스트의 Font 관련된 속성들을 보면 FontSize, FontFamily, FontWeight, FontStyle, FontStretch, Foreground 많은 속성들이 사용됩니다. 앞에서 이야기했 이런 복잡함은 스타일, 리소스딕셔너리 다양한 방법들을 이용해 효율적으로 관리될 있습니다.

 

그런데 일반적으로 Label, TextBlock 문자열 표시에 사용되는 컨트롤들은 단독으로 흔히 사용되지 않습니다. 다른 컨트롤도 마찬가지입니다. Grid, StackPanel, ScrollViewer 다양한 패널(Panel) 자식으로 사용됩니다.

 

1
2
3
4
5
6
7
<StackPanel ChildrenFontSize="15">
    <Label Content="Local property"/>
    <TextBlock Text="Lorem Ipsum is simply "/>
    <TextBlock Text="dummy text of the"/>
    <TextBlock Text="printing and typesetting "/>
    <TextBlock Text="industry."/>
</StackPanel>
cs

 

이런 환경 구조 안에서 패널이 자식의 속성을 일괄적으로 적용해주면 어떨까요? 그러면 어떤면에서 효율적이면서도 XAML 코드를 조금이나마 작성할 있지않을까요? 다시 한번 말하지만 예제를 위한 설정입니다. 어떤 방법이든지 자신만의 스타일과 그러한 결정에 따른 장점과 단점은 항상 존재하는 법입니다.

 

1
2
3
4
5
6
7
<StackPanel FontSize="15">
    <Label Content="Attached property"/>
    <TextBlock Text="Lorem Ipsum is simply "/>
    <TextBlock Text="dummy text of the"/>
    <TextBlock Text="printing and typesetting "/>
    <TextBlock Text="industry."/>
</StackPanel>
cs

 

StackPanel은 FontSize 속성이 없기 때문에 에러가 발생합니다. StackPanel을 상속 받아서 새로운 패널 컨트롤(Custom Control) 만들 수도 있습니다. 그러면 일이 쉽게 풀릴지도 모르겠습니다.

 

1
2
3
4
5
6
7
8
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="1"
            TextBlock.FontSize="15" TextBlock.Foreground="Red">        
    <Label Content="Attached property"/>
    <TextBlock Text="Lorem Ipsum is simply "/>
    <TextBlock Text="dummy text of the"/>
    <TextBlock Text="printing and typesetting "/>
    <TextBlock Text="industry."/>
</StackPanel>
cs

 

이런 방법은 어떨까요? Class명과 함께 속성을 정의하는 것이 가능합니다. VisualTree 요소들은 부모의 속성을 상속 받으려는 특성이 있기 때문에 이러한 동작이 가능합니다. 자세한 설명은 생략합니다. 더 자세한 내용은 박문찬 MVP님의 블로그를 참고하세요.

 

 

하지만 Foreground 속성은 영향을 미치지 못하는 한계가 있습니다. 문자열을 표현하는데 있어서 Foreground 속성을 무시할 수는 없습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="1">
    <StackPanel.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="15"/>
        </Style>
        <Style TargetType="Label">
            <Setter Property="FontSize" Value="15"/>
        </Style>
    </StackPanel.Resources>
 
    <Label Content="Attached property"/>
    <TextBlock Text="Lorem Ipsum is simply "/>
    <TextBlock Text="dummy text of the"/>
    <TextBlock Text="printing and typesetting "/>
    <TextBlock Text="industry."/>
</StackPanel>
cs

 

이런 식의 코드도 작성 가능합니다. 저도 많이 사용하는 방법 중에 하나입니다. 저런 식의 리소스 선언은 하위 컨트롤에만 영향을 준다는 점이 매우 매력적입니다. Style의 BasedOn 속성을 이용해 기존의 스타일도 재사용할 수 있는 점도 아주 매력적입니다. 방법의 단점은 다양한 컨트롤이 복합적으로 존재할 경우 코드가 많이 늘어난다는 점입니다


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

012. 컨버터(Value Converter)

XAML 뽀개기

StringEmptyToVisibilityConverter.cs : 컨버터

 

1
2
3
4
5
6
7
8
9
10
11
12
public class StringEmptyToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return string.IsNullOrEmpty(value.ToString()) ? Visibility.Visible : Visibility.Collapsed;
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }
}
cs

 

IValueConverter 인터페이스를 상속 받아 StringEmptyToVisibilityConverter를 작성합니다. 들어오는 value null 또는 empty 판단해 Visibiliy 타입으로 컨버팅하는 간단한 ValueConverter입니다.

 

1
xmlns:cvt="clr-namespace:Sample.Converter"
cs

 

XAML에서 컨버터를 사용하려면 네임스페이스를 미리 선언합니다.

 

1
<cvt:StringEmptyToVisibilityConverter x:Key="StringEmptyToVisibilityConverter"/>
cs

 

앞서 작성한 벨류 컨버터는 리소스에 Key명과 함께 선언합니다.

 

1
2
3
4
5
6
7
8
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
    <Grid>
        <ScrollViewer x:Name="PART_ContentHost" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
 
        <Label x:Name="PART_GuideText" Content="{TemplateBinding GuideText}" Foreground="Red"
               Visibility="{Binding Text, Converter={StaticResource StringEmptyToVisibilityConverter}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ctr:GuideTextBox}}}"/>
    </Grid>
</Border>
cs

 

가이드 문구의 역할을 하는 Label Visibility 속성을 StringEmptyToVisibilityConverter 컨버터를 이용해 변경합니다. 여기서는 Text 속성을 이용하려고 합니다. 템플릿 안에서 Text 속성을 바인딩 문법으로 접근하는 방법은 여러가지입니다.

 

1
Visibility="{Binding Text, Converter={StaticResource StringEmptyToVisibilityConverter}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ctr:GuideTextBox}}}"
cs

 

첫번째, 원형이 되는 타입을 지정해 찾는 방법이 있습니다.

 

1
Visibility="{Binding Text, Converter={StaticResource StringEmptyToVisibilityConverter}, RelativeSource={RelativeSource TemplatedParent}}"
cs

 

두번째, 템플릿 부모에게서 찾는 방법이 있습니다. 이런 방법들은 템플릿 구성과 요구사항에 맞게 수정해서 접근하면 되겠습니다.

 



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

011. 의존 속성(Dependency Property)

XAML 뽀개기

간단한 예제를 통해 Dependency property 대해 알아봅니다. 기본으로 제공되는 UI 컨트롤은 대부분의 속성이 의존 속성으로 되어 있습니다. 기본 TextBox 이용해 입력을 유도하는 가이드 문구를 추가해 사용하는 예제를 만들어 봅니다.


GuideTextBox.cs : 의존 속성


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#region GuideText : 가이드 문구
 
public object GuideText
{
    get { return (object)GetValue(GuideTextProperty); }
    set { SetValue(GuideTextProperty, value); }
}
 
public static readonly DependencyProperty GuideTextProperty =
    DependencyProperty.Register(
        "GuideText"
        typeof(object), 
        typeof(GuideTextBox), 
        new PropertyMetadata("Guide Text"));
 
#endregion
 
cs


TextBox 상속 받은 GuideTextBox 클래스를 추가합니다. GuideText를 Dependency property로 추가 정의합니다.


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
<Style x:Key="Style_GuideTextBox" TargetType="{x:Type ctr:GuideTextBox}">
    <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
    <Setter Property="BorderBrush" Value="#FFABADB3"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
    <Setter Property="HorizontalContentAlignment" Value="Left"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="AllowDrop" Value="True"/>
    <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
    <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
    <Setter Property="Template">                
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ctr:GuideTextBox}">
                <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
                    <ScrollViewer x:Name="PART_ContentHost" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Opacity" TargetName="border" Value="0.56"/>
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="BorderBrush" TargetName="border" Value="#FF7EB4EA"/>
                    </Trigger>
                    <Trigger Property="IsKeyboardFocused" Value="True">
                        <Setter Property="BorderBrush" TargetName="border" Value="#FF569DE5"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="IsInactiveSelectionHighlightEnabled" Value="True"/>
                <Condition Property="IsSelectionActive" Value="False"/>
            </MultiTrigger.Conditions>
            <Setter Property="SelectionBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
        </MultiTrigger>
    </Style.Triggers>
</Style>
cs


Style_GuideTextBox Key명으로 스타일을 작성합니다. Blend for VS 템플릿 편집 기능을 이용하면 쉽게 기본 템플릿을 얻을 있습니다.

 

1
2
3
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
    <ScrollViewer x:Name="PART_ContentHost" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
</Border>
cs

 

기본 템플릿은 최상위 요소가 Border 감싸져 있습니다.

 

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

 

GuideTextBox 사용하기 위해 네임스페이스 선언이 필요합니다.

 

1
2
3
4
5
6
7
8
9
10
11
<ctr:GuideTextBox Height="30" Margin="5" Style="{DynamicResource Style_GuideTextBox}"
                  GuideText="입력해주세요"/>
 
<ctr:GuideTextBox Height="30" Margin="5" Style="{DynamicResource Style_GuideTextBox}" Grid.Column="1">
    <ctr:GuideTextBox.GuideText>
        <StackPanel Orientation="Horizontal">
            <Ellipse Width="10" Height="10" Fill="Red" Margin="5,0"/>
            <TextBlock Text="입력해주세요"/>
        </StackPanel>
    </ctr:GuideTextBox.GuideText>
</ctr:GuideTextBox>
cs

 

태스트를 위한 GuideTextBox를 XAML 작성합니다. 첫번째는 문자열만 두번째는 복잡하게 구성했습니다.

 

 

이것만으로는 가이드 문구를 표시할 없습니다. 스타일 Template을 수정해야 합니다.

 

1
2
3
4
5
6
7
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
    <Grid>
        <ScrollViewer x:Name="PART_ContentHost" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
        
        <Label x:Name="PART_GuideText" Content="{TemplateBinding GuideText}" Foreground="Red"/>
    </Grid>
</Border>
cs

 

Border는 하나의 자식만 가질 있으므로 Grid로 한번 감싸주었습니다. 그리고 가이드 문구를 TextBlock으로 간단히 정의할 있지만 복잡한 구성이 가능케 하기위해 Label 정의했습니다. TextBlock Text 속성은 문자열 타입만 자식으로 가질 있고 Label Content Object 타입을 가질 있습니다. 반드시 Label이어야하는 것은 아닙니다. 자세한 설명은 여기서 다루지 않습니다.

 

 


Dependency property 추가해 자유롭게 변경 가능한 가이드 문구를 추가했습니다. 간단한 예를 들었지만 다양한 방식으로 응용할 있을 것입니다.


 

이어서 다음 포스트에서는 Value Converter 이용해 입력한 Text 있을 때처럼 가이드 문구가 이상 필요하지 않을 자동으로 보이지 않도록 하는 방법을 알아보려 합니다.


Dependency property에 대한 더 자세한 내용은 박문찬 MVP님의 블로그를 참고하세요.


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

010. 데이타트리거(DataTrigger)

XAML 뽀개기

데이타 트리거에 대해서 다시 한번 살펴봅니다.  

  

1
2
3
4
public class SampleData
{
    public string Type { get; set; }
}
cs

 

예제를 위해 간단히 SampleData 클래스를 추가합니다. string 타입의 Type .Net property 정의합니다.

 

1
2
3
4
5
6
7
8
<!-- Type 데이타 트리거 -->
<DataTrigger Binding="{Binding Type}" Value="Good">
    <Setter Property="TextBox.Foreground" Value="LimeGreen" />
</DataTrigger>
<!--OR-->
<DataTrigger Binding="{Binding Type}" Value="Bad">
    <Setter Property="TextBox.Foreground" Value="Red" />
</DataTrigger>
cs

 

Style_DataTriggerTextBox 스타일에 데이타 트리거를 추가하고 Type 프로퍼티와 바인딩합니다. Type이 Good이거나 Bad 호출됩니다.

 

1
2
3
4
<TextBox x:Name="txbGood" Style="{StaticResource Style_DataTriggerTextBox}"
         Text="Good"/>
<TextBox x:Name="txbBad" Style="{StaticResource Style_DataTriggerTextBox}"
         Text="Bad" Grid.Column="1"/>
cs

 

1
2
this.txbGood.DataContext = new SampleData { Type = "Good" };
this.txbBad.DataContext = new SampleData { Type = "Bad" };
cs

 

비하인드코드에서 TextBox의 DataContext에 SampleData가 생성되도록 정의합니다.


 


런타임에서만 확인됩니다.

 

여기서 Text 속성은 의미가 없습니다. DataContext를 통해 바인딩된 Type 프로퍼티에 의해 호출됩니다.

 

멀티 데이타 트리거에 대해서도 다시 한번 살펴봅니다.  

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 멀티 데이타 트리거 -->
<MultiDataTrigger>
    <MultiDataTrigger.Conditions>
        <Condition Binding="{Binding Type}" Value="Good"/>
        <Condition Binding="{Binding Text, RelativeSource={RelativeSource Self}}" Value="Good"/>
    </MultiDataTrigger.Conditions>
    <Setter Property="TextBox.Foreground" Value="LimeGreen"/>
    <Setter Property="TextBox.FontWeight" Value="Bold"/>
</MultiDataTrigger>
<MultiDataTrigger>
    <MultiDataTrigger.Conditions>
        <Condition Binding="{Binding Type}" Value="Bad"/>
        <Condition Binding="{Binding Text, RelativeSource={RelativeSource Self}}" Value="Bad"/>
    </MultiDataTrigger.Conditions>
    <Setter Property="TextBox.Foreground" Value="Red"/>
    <Setter Property="TextBox.FontWeight" Value="Bold"/>
</MultiDataTrigger>
cs

 

기존 스타일에 멀티 데이타 트리거를 추가합니다. TextBox Text 속성을 추가 조건으로 추가했습니다. 그리고 FontWeight 속성이 Bold 되도록 Setter 추가했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<TextBox x:Name="txbGood" Style="{StaticResource Style_DataTriggerTextBox}"
         Text="Good">
    <TextBox.DataContext>
        <local:SampleData Type="Good"/>
    </TextBox.DataContext>
</TextBox>
<TextBox x:Name="txbBad" Style="{StaticResource Style_DataTriggerTextBox}"
         Text="Bad" Grid.Column="1">
    <TextBox.DataContext>
        <local:SampleData Type="Bad"/>
    </TextBox.DataContext>
</TextBox>
cs

 

기존 비하인드코드에서 DataContext에 생성한 SampleData 주석처리하고 XAML 코드에서 생성하도록 변경했습니다. 멀티 데이타 트리거의 조건에 맞게 Type Text 준비되었습니다.


 


이제는 디자인 타임에서도 바로 확인 가능합니다. 빌드를 해줘야 보입니다.

 


런타임에서 확인해보면 데이타 트리거 멀티 데이타 트리거 모두 동작하는 것을 확인할 있습니다.


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

009. 4가지 트리거(Triggers)

XAML 뽀개기

Style Trigger 비슷 점이 많습니다. 다른 점이라면 스타일은 무조건 적용되는 반면 트리거는 조건을 수반한다는 점입니다.

 

트리거는 4가지 종류가 있습니다.

 

  1. Property : Dependency Property 변경될 호출됩니다.
  2. Event : 이벤트가 발생할 호출됩니다.
  3. Data : Binding 문법으로 연결된 .NET property 특정 데이타일 호출됩니다.
  4. Multi(& MultiData) : 조건을 다수 사용해 논리곱(AND) 관계를 정의합니다.

 

1
2
3
<TextBox Style="{StaticResource Style_BlueFocusTextBox}"/>
<TextBox Style="{StaticResource Style_BlueFocusTextBox}"
         Grid.Column="1"/>
cs

 

2개의 TextBox에 동일한 스타일을 적용했습니다. 2개를 예제로 이유는 포커스 상태를 쉽게 잃기 위함일뿐 다른 이유는 없습니다.

 

1. 프로퍼티 트리거


1
2
3
4
5
6
7
8
9
10
<Style x:Key="Style_BlueFocusTextBox">
    <Style.Triggers>
        <!-- IsFocused 프로퍼티 트리거 -->
        <Trigger Property="TextBox.IsFocused" Value="True">
            <Setter Property="TextBox.BorderThickness" Value="3"/>
        </Trigger>
    </Style.Triggers>
    <Setter Property="Control.Width" Value="60"/>
    <Setter Property="Control.Height" Value="30"/>
</Style>
cs

 

TextBox Focused 상태(State) 되면 IsFocused 속성이 True 변경됩니다. 조건일 BorderThickness 속성이 3으로 변경되는 트리거를 정의했습니다.

 

 

해당 조건을 벗어나면 추가 코드 없이도 트리거가 자동 취소됩니다.

 

2. 이벤트 트리거


1
2
3
4
5
6
7
8
9
10
<Storyboard x:Key="MouseEnterAnimation">
    <ColorAnimation 
        Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)" 
        To="LightGray" Duration="0:0:0.1"/>
</Storyboard>
<Storyboard x:Key="MouseLeaveAnimation">
    <ColorAnimation 
        Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)" 
        To="White" Duration="0:0:0.1"/>
</Storyboard>
cs


1
2
3
4
5
6
7
8
<!-- MouseEnter 이벤트 트리거 -->
<EventTrigger RoutedEvent="TextBox.MouseEnter">
    <BeginStoryboard Storyboard="{StaticResource MouseEnterAnimation}"/>
</EventTrigger>
<!-- MouseLeave 이벤트 트리거 -->
<EventTrigger RoutedEvent="TextBox.MouseLeave">
    <BeginStoryboard Storyboard="{StaticResource MouseLeaveAnimation}"/>
</EventTrigger>
cs

  

MouseEnter와 MouseLeave 이벤트가 발생할 MouseEnterAnimation와 MouseLeaveAnimation Storyboard 각각 실행합니다. 스토리보드는 리소스에 정의합니다. 이벤트 트리거는 프로퍼티 트리거와 약간 다릅니다. 스토리보드를 사용하기 때문에 트리거를 취소하려면 이전 트리거에 반하는 또는 그에 준하는 트리거를 추가해야 합니다.

 

 

이렇게 추가 코드가 없으면 트리거가 자동 취소되지 않습니다. 스토리보드의 동작을 이해한다면 알맞은 트리거를 추가할 있습니다.

 

3. 데이 트리거


1
2
3
4
<!-- Text 데이타 트리거 -->
<DataTrigger Binding="{Binding Text, RelativeSource={RelativeSource Self}}" Value="Bad">
    <Setter Property="TextBox.Foreground" Value="Red" />
</DataTrigger>
cs

 

 

데이타 트리거는 프로퍼티 트리거와 비슷합니다. 예제는 Bad라는 문자열이 Text 속성에 들어왔을 호출됩니다.

 

4. 멀티 트리거


1
2
3
4
5
6
7
8
9
10
<!-- Text 멀티 트리거 -->
<MultiTrigger>
    <MultiTrigger.Conditions>
        <Condition Property="TextBox.Text" Value="Good"/>
        <!-- AND -->
        <Condition Property="TextBox.IsMouseOver" Value="True"/>
    </MultiTrigger.Conditions>
    <Setter Property="TextBox.Foreground" Value="LimeGreen"/>
    <Setter Property="TextBox.FontWeight" Value="Bold"/>
</MultiTrigger>
cs


1
2
3
4
<TextBox Style="{StaticResource Style_BlueFocusTextBox}" 
         Tag="Good"/>
<TextBox Style="{StaticResource Style_BlueFocusTextBox}"
         Grid.Column="1"/>
cs



이전에 정의한 각각의 트리거가 논리합(OR)이라면, 멀티트리거는 논리곱(AND)이라 있습니다. 정의된 모든 Condition이 참일 트리거가 호출됩니다. 예제에서는  Text 속성 Good으로 참이되고 IsMouseOver 속성이 True로 참이될 트리거가 호출됩니다. 

 

5. 멀티 데이 트리거


1
2
3
4
5
6
7
8
9
10
<!-- Text  & Tag 멀티 데이타 트리거 -->
<MultiDataTrigger>
    <MultiDataTrigger.Conditions>
        <Condition Binding="{Binding Text, RelativeSource={RelativeSource Self}}" Value="Bad"/>
        <!-- AND -->
        <Condition Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="Bad"/>
    </MultiDataTrigger.Conditions>
    <Setter Property="TextBox.Foreground" Value="Red"/>
    <Setter Property="TextBox.FontWeight" Value="Bold"/>
</MultiDataTrigger>
cs

  

1
2
3
4
<TextBox Style="{StaticResource Style_BlueFocusTextBox}" 
         Tag="Good"/>
<TextBox Style="{StaticResource Style_BlueFocusTextBox}"
         Grid.Column="1" Tag="Bad"/>
cs

 


이번엔 설명이 필요없을 같습니다. 기존 두번째 TextBox  Tag="Bad"를 추가하 트리거의 모든 조건이 참이 되도록 했습니다. DataTrigger 대해서는 다음 포스트에서 다시 살펴보겠습니다.


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

008. 스타일 파생(Derive Style)

XAML 뽀개기

1
2
3
4
5
6
7
8
9
10
11
<Style x:Key="Style_ControlBase">
    <Setter Property="Control.Width" Value="60"/>
    <Setter Property="Control.Height" Value="30"/>
</Style>
            
<SolidColorBrush x:Key="Color_Background_ReadOnlyTextBox" Color="LightGray"/>
<Style x:Key="Style_ReadOnlyTextBox" TargetType="TextBox" 
       BasedOn="{StaticResource Style_ControlBase}">
    <Setter Property="TextBox.IsReadOnly" Value="True"/>
    <Setter Property="TextBox.Background" Value="{StaticResource Color_Background_ReadOnlyTextBox}"/>
</Style>
cs


Style BasedOn 속성을 이용해 이전 Style_ControlBase 스타일을 상속 받아 새로운 스타일을 정의합니다. TargetType을 TextBox 제한하고 Setter 정의시 Class명과 함께 정의한다면 더욱 견고히 스타일을 정의할 있습니다.


1
2
3
4
5
<TextBox Grid.Column="0" Style="{StaticResource Style_ControlBase}"
         Text="TextBox"/>
 
<TextBox Grid.Column="1" Style="{StaticResource Style_ReadOnlyTextBox}"
         Text="ReadOnlyTextBox" Width="100"/>
cs


 

Style_ControlBase 스타일은 Control Class에만 국한되고 Style_ReadOnlyTextBox 스타일은 TextBox Class에만 국한되어 깔끔히 분리되어 재사용될 있습니다.

 

1
2
3
4
5
6
<Style x:Key="Style_ReadOnlyTextBox" TargetType="TextBox" 
       BasedOn="{StaticResource Style_ControlBase}">
    <Setter Property="IsReadOnly" Value="True"/>
    <Setter Property="Background" Value="{StaticResource Color_Background_ReadOnlyTextBox}"/>
    <Setter Property="Width" Value="100"/>
</Style>
cs

 

TargetType을 TextBox 제한한 상태에서는 TextBox Class명을 생략해도 됩니다. 상속 받은 Setter 속성도 다시 정의할 있습니다.

 

1
2
3
4
5
6
<Style TargetType="TextBox" 
       BasedOn="{StaticResource Style_ControlBase}">
    <Setter Property="IsReadOnly" Value="True"/>
    <Setter Property="Background" Value="{StaticResource Color_Background_ReadOnlyTextBox}"/>
    <Setter Property="Width" Value="100"/>
</Style>
cs

 

스타일의 Key 이름을 정의하지 않으면 TargetType에 정의된 모든 Class 일괄로 스타일을 적용할 있습니다.

 

1
2
<TextBox Grid.Column="2" 
         Text="ReadOnlyTextBox"/>
cs

 

 

1
2
3
4
5
6
7
8
Style Style_ReadOnlyTextBox = new Style();
Style_ReadOnlyTextBox.TargetType = typeof(TextBox);
Style_ReadOnlyTextBox.BasedOn = (Style)this.grdTest.FindResource("Style_ControlBase");
Setter setterIsReadOnly = new Setter(TextBox.IsReadOnlyProperty, true);
Setter setterBackgroundS = new Setter(TextBox.BackgroundProperty, Brushes.LightGray);
Style_ReadOnlyTextBox.Setters.Add(setterIsReadOnly);
Style_ReadOnlyTextBox.Setters.Add(setterBackgroundS);
this.txbTest.Style = Style_ReadOnlyTextBox;
cs

 

2번의 스타일 관련 포스트에서 살펴본 XAML 코드는 비하인드 코드에서 동일하게 작성할 있습니다.

 

1
this.txbTest.Style = (Style)this.grdTest.FindResource("Style_ReadOnlyTextBox");
cs

 

XAML 코드에서 이미 정의된 스타일을 적용할 수도 있습니다. 하지만 특수한 경우를 제외하고 여러모로 비효율적이고 선호하지도 권장하지도 않습니다.




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

007. 스타일 정의(Override Style)

XAML 뽀개기
1
2
3
4
5
6
7
<Grid.Resources>
    <Style x:Key="Style_ControlBase">
        <!-- Property=”[ClassName].[DependencyProperty]”-->
        <Setter Property="Control.Width" Value="60"/>
        <Setter Property="Control.Height" Value="30"/>
    </Style>
</Grid.Resources>
cs

 

Style의 Setter 정의할 Property 앞에 Class명을 지정할 있습니다.

 

1
2
3
4
<TextBox Grid.Column="0" Style="{StaticResource Style_ControlBase}"
         Text="TextBox"/>
<Button Grid.Column="1" Style="{StaticResource Style_ControlBase}"
        Content="Button"/>
cs

 

 

TextBox Button 모두 Control에서 파생된 컨트롤이기 때문에 스타일에 영향을 받습니다. 추가로 TargetType 지정하지 않았기 때문에 가능한 일이기도 합니다.

 

 

1
2
3
4
5
<Style x:Key="Style_ControlBase" TargetType="{x:Type TextBox}">
    <!-- Property=”[ClassName].[DependencyProperty]”-->
    <Setter Property="Control.Width" Value="60"/>
    <Setter Property="Control.Height" Value="30"/>
</Style>
cs

 

TargetType을 지정하면 해당 타입에만 영향을 있기 때문에 많은 스타일을 다루는 프로젝트에서는 약이 되기도 하고 독이 되기도 합니다. 저는 TargetType을 지정해서 관리하는 것을 선호합니다.

 

1
2
<TextBox Grid.Column="1" Style="{StaticResource Style_ControlBase}"
         Text="TextBox" Width="80" Height="40"/>
cs

 

 

Style을 지정했지만 해당 속성을 다시 정의하면 지정된 Style을 무시(Override)하고 다시 정의할 있습니다.


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

006. 클래스와 데이타템플릿(Class & DataTemplate)

XAML 뽀개기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person
{
    public string RealName { get; set; }
    public int Age { get; set; }
}
 
public class Family : List<Person>
{
    public Family()
    {
        Add(new Person() { RealName = "길동", Age = 40 });
        Add(new Person() { RealName = "철수", Age = 30 });
        Add(new Person() { RealName = "영희", Age = 20 });
    }
}
cs

 

목록형의 아이템 구성은 예제처럼 간단하지 않고 복잡할 있습니다. 하지만 복잡한 구성일지라도 Class 이용해 표현 가능합니다.

 

1
2
3
4
5
6
<DataTemplate x:Key="DataTemplate_ListBoxItem">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding RealName}" Margin="0,0,10,0"/>
        <TextBlock Text="{Binding Age}"/>
    </StackPanel>
</DataTemplate>
cs

 

XAML에서 목록형의 아이템 템플릿은 Class 구성에 맞추어 DataTemplate에서 정의합니다.

 

 

결과는 위와 같습니다.

 

1
2
3
4
5
6
7
8
9
10
<Grid.Resources>
    <x:Array x:Key="Peoples" Type="{x:Type local:Person}">
        <local:Person RealName="홍길동" Age="40"/>
        <local:Person RealName="박철수" Age="30"/>
        <local:Person RealName="김영희" Age="20"/>
    </x:Array>
 
    <local:Family x:Key="Family"/>
</Grid.Resources>
 
cs

 

Class에서 정의한 모델은 리소스에 정의한 XAML 안에서 사용할 있습니다.

 

1
2
3
4
5
6
<ListBox ItemsSource="{Binding Source={StaticResource Peoples}}" 
         ItemTemplate="{DynamicResource DataTemplate_ListBoxItem}"/>
 
<ListBox ItemsSource="{Binding Source={StaticResource Family}}" 
         ItemTemplate="{DynamicResource DataTemplate_ListBoxItem}" 
         Grid.Column="1"/>
cs

 

이전에 정의한 Class DataTemplate 위와 같이 정의합니다.

 

1
2
3
4
5
6
7
8
9
10
<ListBox ItemTemplate="{DynamicResource DataTemplate_ListBoxItem}" 
         Grid.Column="2">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:Person}">
            <local:Person RealName="홍길동" Age="40"/>
            <local:Person RealName="박철수" Age="30"/>
            <local:Person RealName="김영희" Age="20"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

이전 포스트에서도 다루었지만 목록형의 ItemsSource는 위와 같이 정의할 있습니다.


 

 

1
2
3
<ListBox ItemsSource="{Binding Source={StaticResource Family}}" 
         DisplayMemberPath="RealName"
         Grid.Column="3"/>
cs

 

DataTemplate 정의할 필요가 없을 때에는 DisplayMemberPath 속성을 이용해 간단히 목록을 표현할 있습니다.



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