XAML or HTML

032. 데이터템플릿 & 트리거(DataTemplate & Trigger) #3

XAML 뽀개기

이전 포스트에서는 데이타템플릿, 스타일을 컨버터, 셀렉터를 활용해 하나의 컨트롤에서 다양한 형태를 보여줄 있었습니다. 이번 포스트에서는 트리거를 이용하는 방법을 살펴봅니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Style & Template Trigger -->
<DataTemplate x:Key="DataTemplate_Message" DataType="{x:Type local:ExchangeMessage}">
    <StackPanel>
        <TextBlock x:Name="PART_Name" Text="{Binding Name}"/>
        <Border x:Name="PART_Background" Background="White" CornerRadius="5" Margin="0,3">
            <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
        </Border>
        <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"/>
    </StackPanel>
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding IsMine}" Value="True">
            <Setter Property="Background" Value="#FFFFEB33" TargetName="PART_Background"/>
            <Setter Property="HorizontalAlignment" Value="Right" TargetName="PART_Time"/>
            <Setter Property="Visibility" TargetName="PART_Name">
                <Setter.Value>Collapsed</Setter.Value>
            </Setter>
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>
cs

 

이번에는 데이타템플릿이 하나만 필요합니다. 다른 사람의 메시지를 기본 템플릿으로 정의하고 데이타트리거를 이용해 메시지일 때에 달라져야하는 속성들을 정의합니다. 이전 예제와 동일한 형태로 정의하므로 자세한 프로퍼티 사항들은 생략합니다. 필요한 요소들을 TargetName 지정해야하므로 x:Name 선언하는 것에 주의합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Style x:Key="Style_Message" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <ContentPresenter x:Name="PART_ListBoxItem" 
                                  HorizontalAlignment="Left" 
                                  Margin="5,0,20,5"/>
                <ControlTemplate.Triggers>
                    <DataTrigger Binding="{Binding IsMine}" Value="True">
                        <Setter Property="HorizontalAlignment" Value="Right" TargetName="PART_ListBoxItem"/>
                        <Setter Property="Margin" Value="20,0,5,5" TargetName="PART_ListBoxItem"/>
                    </DataTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        <Setter.Value>
    </Setter>
</Style>
cs


스타일도 데이타템플릿과 동일한 방법으로 데이타프리거를 이용합니다. PART_ListBoxItem의 HorizontalAlignment 속성은 Left, Margin은 5,0,20,5로 선언합니다. 기본으로 선언한 스타일은 다른 사람의 메시지 스타일이 됩니다. 다음엔 데이터트리거를 이용해서 내 메시지일 떄에 달라져야하는 속성들의 값을 반대로 정의합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Style & Template Trigger -->
<ListBox ItemTemplate="{StaticResource DataTemplate_Message}" 
         ItemContainerStyle="{StaticResource Style_Message}"
         Grid.Column="1">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

ItemTemplate ItemContainerStyle 선언하는 것은 이전과 다를 것이 없습니다.

 

 

실행해서 태스트해봅니다

 

컨버터를 사용할지 셀렉터를 사용할지 트리거를 사용할지는 여러분이 판단하시길 바랍니다. 여러 조건과 상황에 따른 옳은 판단이 중요할 같습니다.

 

참고 : JERRIE PELSER > 3 Techniques you can use to make your data templates dynamic

031. 데이터템플릿 & 셀렉터(DataTemplate & Selector) #2

XAML 뽀개기

이전 포스트에서는 데이타템플릿(DataTemplate) 하는 기능을 알아보았고 컨버터(Converter) 이용해 내가 보낸 메시지와 다른 사람이 보낸 메시지를 다르게 보이게 하는 방법을 살펴봤습니다. 이번 포스트에서는 셀렉터(Selector) 어떤 기능을 하는지 어떻게 사용하는지 살펴봅니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Template Selector -->
<DataTemplate x:Key="DataTemplate_Message_Left" DataType="{x:Type local:ExchangeMessage}">
    <StackPanel>
        <TextBlock Text="{Binding Name}" 
                   Visibility="{Binding IsMine, Converter={StaticResource BooleanToVisibilityConverter}, 
                                                ConverterParameter=inverted}"/>
    <Border Background="{Binding IsMine, Converter={StaticResource BooleanToBrushConverter}}" 
            CornerRadius="5" Margin="0,3">
        <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
    </Border>
    <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"/>
</DataTemplate>
cs

 

기존의 데이타템플릿의 키값을 DataTemplate_Message_Left로 변경하고 전체 XAML 코드를 [복사 > 붙여넣기]합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<DataTemplate x:Key="DataTemplate_Message_Right" DataType="{x:Type local:ExchangeMessage}">
    <StackPanel>
        <TextBlock Text="{Binding Name}" 
                   Visibility="{Binding IsMine, Converter={StaticResource BooleanToVisibilityConverter}, 
                                                ConverterParameter=inverted}"/>
        <Border Background="{Binding IsMine, Converter={StaticResource BooleanToBrushConverter}}" 
                CornerRadius="5" Margin="0,3">
            <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
        </Border>
        <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"
                   HorizontalAlignment="Right"/>
    </StackPanel>
</DataTemplate>
cs

 

복사+붙여넣기한 데이타템플릿의 키값은 DataTemplate_Message_Right으로 변경합니다. 데이타템플릿이 2 준비되었습니다. 확인 시간을 오른쪽으로 정렬하기 위해 PART_Time의 HorizontalAlignment 속성을 Right 선언했습니다.

 

MessageStyleTemplateSelector.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 MessageTemplateSelector : DataTemplateSelector
{
    public DataTemplate LeftTemplate { get; set; }
    public DataTemplate RightTemplate { get; set; }
    public string PropertyToCheck { get; set; }
    public string PropertyValue { get; set; }
 
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        var exchangeMessage = (ExchangeMessage)item;
        var type = exchangeMessage.GetType();
        var property = type.GetProperty(PropertyToCheck);
 
        if (property.GetValue(exchangeMessage, null).ToString() == PropertyValue)
        {
            return RightTemplate;
        }
        else
        {
            return LeftTemplate;
        }
    }
}
cs

 

DataTemplateSelector를 상속 받은 MessageTemplateSelector 추가합니다. SelectTemplate 메소드를 재정의합니다. Selector 작성할 메소드를 재정의하는 일은 필수입니다. 사용자가 지정한 프로퍼티(PropertyToCheck) 지정한 (PropertyValue) 일치하면 지정한 데이터템플릿을 리턴하도록 코드를 작성했습니다.

 

1
xmlns:slt="clr-namespace:Sample.Selector" 
cs

 

셀렉터를 XAML 코드에서 사용하려면 네임스페이스를 선언해야 합니다.

 

1
2
3
4
<slt:MessageTemplateSelector x:Key="MessageTemplateSelector" 
                             PropertyToCheck="IsMine" PropertyValue="True" 
                             LeftTemplate="{StaticResource DataTemplate_Message_Left}" 
                             RightTemplate="{StaticResource DataTemplate_Message_Right}"/>
cs

 

MessageTemplateSelector를 리소스에 선언합니다. 이전 포스트에서는 IsMine 프로퍼티에 True 값이 될때 컨버터가 동작하도록 했었습니다. 이번에도 마찬가지 방식으로 지정한 데이타템플릿이 리턴되도록 했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
<!-- Style & Template Selector -->
<ListBox ItemTemplateSelector="{StaticResource MessageTemplateSelector}" 
         Grid.Column="1">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

ItemTemplateSelector 속성에 셀렉터를 선언합니다.

 


확인 시간이 오른쪽으로 정렬되었습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- Style Selector -->
<Style x:Key="Style_Message_Left" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <ContentPresenter x:Name="PART_ListBoxItem" 
                                  HorizontalAlignment="Left" 
                                  Margin="5,0,20,5"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<Style x:Key="Style_Message_Right" TargetType="{x:Type ListBoxItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <ContentPresenter x:Name="PART_ListBoxItem" 
                                  HorizontalAlignment="Right" 
                                  Margin="20,0,5,5"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
cs

 

추가로 스타일셀렉터를 살펴봅니다. 아이템 전체를 오른쪽으로 정렬하려고 합니다. 데이타템플릿과 마찬가지로 Style_Message_Left, Style_Message_Right 2개의 스타일을 정의합니다. PART_ListBoxItem의 HorizontalAlignment, Margin 속성을 서로 반대로 정의합니다.

 

1
2
3
4
<slt:MessageStyleSelector x:Key="MessageStyleSelector" 
                          PropertyToCheck="IsMine" PropertyValue="True"
                          LeftStyle="{StaticResource Style_Message_Left}"
                          RightStyle="{StaticResource Style_Message_Right}"/>
cs

 

MessageTemplateSelector와 마찬가지로 MessageStyleSelector를 리소스에 선언합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Style & Template Selector -->
<ListBox ItemTemplateSelector="{StaticResource MessageTemplateSelector}" 
         ItemContainerStyleSelector="{StaticResource MessageStyleSelector}" 
         Grid.Column="1">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

ItemContainerStyleSelector속성에 셀렉터를 선언합니다.


 

실행해서 태스트해봅니다

 

이처럼 셀렉터는 하나의 컨트롤에 여러 개의 템플릿이나 여러 개의 스타일을 함께 표현할 있게 해줍니다.

 

참고 : JERRIE PELSER > 3 Techniques you can use to make your data templates dynamic

030. 데이타템플릿 & 컨버터(DataTemplate & Converter) #1

XAML 뽀개기

이번 포스트 제목은 데이타템플릿(DataTemplate)이긴 하지만 예전 포스트들에서 다뤘던 다양한 기능들과 함께 살펴봅니다.

 

목록형 컨트롤들에서는 하나의 값을 보여주기도 하지만 다양한 형태의 값들을 함께 보여주기도 합니다. 데이타템플릿은 이처럼 다양한 데이터를 어떻게 구성해서 보이게 할지 정의하는 역할을 합니다.

 


이번 시리즈 포스트의 최종 결과물입니다. 평소에 흔하게 사용하는 채팅앱을 예로 들어 이해를 돕고자 합니다. 2개의 목록형 예제는 시각적인 결과는 동일하지만 전혀 다른 방법으로 만들었습니다. 이번 시리즈 포스트에서는 데이터템플릿뿐만 아니라 스타일(Style), 컨버터(Converter), 셀렉터(Selector), 트리거(Trigger) 다양한 기능들을 함께 다루게 됩니다.

 

1
2
3
4
5
6
7
public class ExchangeMessage
{
    public bool IsMine { get; set; }            // 메시지 구분자
    public string Name { get; set; }            // 발신자 이름
    public string Message { get; set; }         // 발신 내용
    public DateTime ConfirmedTime { get; set; } // 확인 시간
}
cs

 

먼저 리스트박스(ListBox) 보이게 샘플 데이터의 모델 클래스 ExchangeMessage입니다. Name 메시지를 발신자의 이름입니다. Message 발신자의 메시지 내용입니다. ConfirmedTime은 메시지를 확인한 시간입니다. 마지막으로 IsMine은 메시지가 것인지 다른 사람의 것인지 구분하기 위한 bool 타입의 구분자입니다. 다른 요소들이 있을 있지만 예제 코드를 최소화하기 위해 제외했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Grid.Resources>
    <Style TargetType="{x:Type ListBox}">
        <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
        <Setter Property="Background" Value="#FFB2C7D9"/>
    </Style>
</Grid.Resources>
 
<ListBox>
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

예제를 위한 기본 리스트박스 XAML 코드입니다. X라는 친구와 대화를 나누는 시나리오입니다. 디자인 스타일은 여러분이 생각하는 그 채팅앱에서 가져왔습니다. 그리고 가로 스크롤은 불필요하기 때문에 HorizontalScrollBarVisibility 속성을 Disabled을 선언하였고 바탕색(Background) 속성에 #FFB2C7D9 브러쉬를 선언했습니다.

 

 

리스트박스에 ItemsSource 속성만 정의한 디자인뷰에서 모든 값이 제대로 보이지 않습니다.


1
2
3
4
5
6
7
8
9
<ListBox DisplayMemberPath="Message">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs

 

 

DisplayMemberPath 속성을 이용하면 위처럼 모델 데이터의 일부만 표현이 가능합니다. 하지만 표출하고자 하는 것은 위처럼 Message만이 아닙니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
<!-- DataTemplate-->
<DataTemplate x:Key="DataTemplate_Message_Basic">
    <StackPanel>
        <TextBlock Text="{Binding Name}" 
                   Visibility="{Binding IsMine}"/>
        <Border Background="{Binding IsMine}" 
                CornerRadius="5" Margin="0,3">
            <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
        </Border>
        <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"/>
    </StackPanel>
</DataTemplate>
cs

 

드디어 데이타템플릿을 정의합니다. Blend for VS [추가 템플릿 편집 > 생성된 항목(ItemTemplate) 편집 > 현재 항목/복사본 편집] 기능을 이용하면 위지위스(WYSIWYG) 방식으로 작성할 수도 있습니다. 각각의 모델 데이터를 바인딩 문법을 이용해 정의합니다. 추가로 확인한 시간은 StringFormat 속성을 이용해 시간만 간략히 보이도록 합니다.

 

1
2
3
4
5
6
7
8
9
10
<!-- DataTemplate -->
<ListBox ItemTemplate="{StaticResource DataTemplate_Message_Basic}">
    <ListBox.ItemsSource>
        <x:Array Type="{x:Type local:ExchangeMessage}">
            <local:ExchangeMessage Name="X Friend" Message="Charles~ R u there?" ConfirmedTime="2018-03-01 12:24:33" IsMine="False"/>
            <local:ExchangeMessage Name="Me" Message="Yeah~ i'm here. What's up?" ConfirmedTime="2018-03-01 12:25:44" IsMine="True"/>
            <local:ExchangeMessage Name="X Friend" Message="Do u have time tonight? Let's get it!" ConfirmedTime="2018-03-01 12:26:55" IsMine="False"/>
        </x:Array>
    </ListBox.ItemsSource>
</ListBox>
cs


(x:Key) 이용해 정의한 DataTemplate을 리스트박스의 ItemTemplate 속성에 StaticResource 문법을 이용해 선언합니다.

 

 

DataTemplate을 선언한 리스트박스가 이전과 다르게 다양한 여러 데이터가 함께 보이게 됩니다. 여기서 끝이 아니겠죠?

 

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
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanToVisibilityConverter : IValueConverter
{
    enum Parameters
    {
        NORMAL, INVERTED
    }
 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var boolValue = (bool)value;
        var strParam = parameter.ToString().ToUpper();
        var direction = (Parameters)Enum.Parse(typeof(Parameters), strParam);
 
        if (direction == Parameters.INVERTED)
        {
            return !boolValue ? Visibility.Visible : Visibility.Collapsed;
        }
        else
        {
            return boolValue ? Visibility.Visible : Visibility.Collapsed;
        }
    }
    
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}
 
cs

 

BooleanToVisibilityConverter 컨버터를 추가합니다. 내가 보낸 메시지의 발신자 이름은 보이지않고고 X라는 친구의 이름은 보여야 합니다. 기능을 컨버터를 이용해 표현합니다. 파라미터(ConverterParameter) 이용해 정반대로 컨버팅하는 기능도 추가했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ValueConversion(typeof(bool), typeof(Brush))]
public class BooleanToBrushConverter : IValueConverter
{
    public Brush FalseBrush { get; set; }
    public Brush TrueBrush { get; set; }
 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? TrueBrush : FalseBrush;
    }
 
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }
}
cs

 

컨버터를 하나 추가합니다. 이번 컨버터는 내가 보낸 메시지와 친구가 보낸 메시지의 바탕색(Background) 다르게 하기 위한 것입니다. Brush 타입의 프로퍼티를 추가해서 고정된 브러쉬가 아닌 사용자가 컨버터를 리소스에 선언할 때마다 다른 브러쉬를 정의할 있도록 합니다.

 

1
2
3
4
<!-- Converter -->
<cvt:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<cvt:BooleanToBrushConverter x:Key="BooleanToBrushConverter" 
                             TrueBrush="#FFFFEB33" FalseBrush="White"/>
cs

 

준비된 컨버터를 XAML 코드에서 사용하기 위해서는 리소스로 선언해야 합니다. 컨버터에 추가한 Brush 타입의 2개의 프로퍼티에는 원하는 브러쉬를 선언합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- DataTemplate-->
<DataTemplate x:Key="DataTemplate_Message_Basic">
    <StackPanel>
        <TextBlock Text="{Binding Name}" 
                   Visibility="{Binding IsMine, Converter={StaticResource BooleanToVisibilityConverter}, 
                                                ConverterParameter=inverted}"/>
        <Border Background="{Binding IsMine, Converter={StaticResource BooleanToBrushConverter}}" 
                CornerRadius="5" Margin="0,3">
            <TextBlock Text="{Binding Message}" TextWrapping="Wrap" Grid.Column="1" FontSize="11" Margin="5,3,5,7"/>
        </Border>
        <TextBlock x:Name="PART_Time" Text="{Binding ConfirmedTime, StringFormat=hh:mm:ss}" FontSize="8"/>
    </StackPanel>
</DataTemplate>
cs

 

리소스에 선언한 컨버터를 비인딩 문법에 Converter 속성을 이용해 데이타템플릿에 추가로 정의합니다.

 

 

2개의 컨버터를 이용해 발신자 이름의 표시 유무와 바탕색을 다르게 표현했습니다.

 

내가 보낸 메시지와 다른 사람이 보낸 메시지를 더욱 명확하게 구분해 보기 위해선 추가 작업이 필요합니다. 다른 사람이 보낸 메시지는 좌측으로 정렬하고 내가 보낸 메시지는 우측으로 정렬하면 나은 표현 방법이 같습니다.

 

추가할 방법들은 다음 포스트에서 살펴봅시다.

 

이번 포스트에서 추가로 사용된 Convertert StringFormat 예전 포스트를 참고하기 바랍니다.

 

참고 : JERRIE PELSER > 3 Techniques you can use to make your data templates dynamic

029. 툴팁 말풍선 슬라이더(ToolTip Balloon Slider) #3

XAML 뽀개기

이전 포스트들에서 여러가지 방법으로 (Thumb) 말풍선 툴팁(ToolTip) 따라다니는 슬라이더(Slider) 만들어 보았습니다. 그런데 뭔가 만족스럽지 않았습니다. 그래서  깔끔한 방법이 없을까 한참을 고민하고 태스트하고 찾아보던 끝에 하나의 포스트를 이어가기로 했습니다.

 

예제에 사용된 대부분의 코드는 동일하니 달라진 점만 일부 설명합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 툴팁 팝업 -->
<Popup x:Name="PART_ToolTipPopup"
       PlacementTarget="{Binding ElementName=Thumb}"
       Placement="Center" VerticalOffset="-30"
       AllowsTransparency="True">
    <StackPanel>
        <Border CornerRadius="5" Background="SkyBlue">
            <TextBlock Text="{Binding Value, ElementName=PART_Track, StringFormat=N0}"
                       Foreground="Black" HorizontalAlignment="Center" Margin="10,5" />
        </Border>
        <ed:RegularPolygon Fill="SkyBlue" Margin="0,-8,0,0" Panel.ZIndex="-1"                                               
                           PointCount="4" Width="10" Height="15" />
    </StackPanel>
</Popup>
cs

XAML 코드에서 사용된 Popup 그대로 사용됩니다. 추가로 이전 예제에서 불필요하게 사용되었던 VerticalOffset 프로퍼티는 -30으로 고정 시켰습니다. 팝업의 세로 위치는 변하지 않으므로 미리 고정되어도 같습니다.

 

1
2
3
4
5
6
7
8
9
<!-- New Behavior -->
<Slider Value="5" Minimum="0" Maximum="100" IsSnapToTickEnabled="True"
        TickFrequency="1" TickPlacement="Both"
        VerticalAlignment="Center" Margin="10,0"
        Style="{DynamicResource Style_Slider}">
    <i:Interaction.Behaviors>
        <bhv:ToolTipToPopupSliderBehavior />
    </i:Interaction.Behaviors>
</Slider>
cs

 

슬라이더의 XAML 코드입니다. 달라진 점은 IsSnapToTickEnabled 프로퍼티를 True 활성화하여 Tick 스냅이 걸리도록 점입니다. 썸이 이동할 1 단위로 스냅된다는 시나리오를 추가했습니다. TickFrequency, TickPlacement 등의 프로퍼티는 Tick 디자인 화면에 노출되도록 하는 프로퍼티들입니다.

 

아래부터는 새로운 방식으로 만든 비헤이비어(Behavior)입니다.

 

ToolTipToPopupSliderBehavior.cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void AssociatedObject_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    _popup.HorizontalOffset = 0;
    _popup.IsOpen = true;
 
    var xMoveOffset = AssociatedObject.ActualWidth / (AssociatedObject.Maximum - AssociatedObject.Minimum);
    if (e.NewValue > e.OldValue && e.NewValue != AssociatedObject.Maximum)
    {
        _popup.HorizontalOffset += xMoveOffset;
    }
    else if (e.NewValue < e.OldValue && e.OldValue != AssociatedObject.Maximum)
    {
        _popup.HorizontalOffset -= xMoveOffset;            
    }
}
cs

이번 새로운 비헤이비어에서는 마우스와 키보드 이벤트를 각각 따로 이용하지 않고 동시에 적용되는 ValueChanged 이벤트를 이용했습니다. 이전에는 각각에 해당하는 이벤트를 핸들링했기 때문에 조금 중복되는 느낌을 받았는데 공통으로 발생하는 ValueChanged 이벤트를 이용함으로서  깔끔해진 느낌이 나는 합니다.

 

그리고 이번에는 슬라이더의 MinimumMaximum 프로퍼티의 값과 실제 ActualWidth의 크기를 이용해 썸이 움직였을때 팝업이 움직여야하는 HorizontalOffset 값을 매번 계산하여 초기화하고 다시 넣어주도록 하였습니다슬라이더의 크기가 고정이 아니라 윈도우 리사이징  어떤 이유로 변경이 가능하다는 가정된 시나리오 아래에 그렇게 되도록 했습니다.

 

그리고 기존에 Key.Left와 Key.Right의 판단은 OldValue와 NewValue를 비교하는 것으로 대체했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#region Event handler < Mouse
 
private void _thumb_MouseLeave(object sender, MouseEventArgs e)
{
    _popup.IsOpen = false;
}
 
#endregion
 
#region Event handler < Keyboard
                
private void Slider_PreviewKeyUp(object sender, KeyEventArgs e)
{
    _popup.IsOpen = false;
}
 
#endregion
cs

 

팝업이 닫히는 동작의 경우는 이전 방법 그대로 각각의 이벤트 핸들러를 이용하도록 했습니다.



제가 사용했던 여러 방식들이 정답이 아니거나 옳지 못할 수도 있습니다. 하지만 나름대로는 몇일, 몇시간을 투자해서 얻은 결론이라 흥미로웠습니다. 나중에라도 나은 방법을 알게 된다면 기쁜 마음으로 다시 공유하게 같습니다.

 

마지막으로 아래 링크는 이번 예제에서는 직접적으로 사용되진 않았지만 예제 프로젝트 안에 일부 주석과 FollowingPopup.cs 파일 등에 남겨 두었습니다. 키보드도 함께 지원해야 한다는 저의 시나리오에 부합되지 않아 사용하지 않았습니다만 괜찮은 참고자료인 같아 남겨둡니다. 간단한 내용이지만 다른 시선으로 눈을 뜨게 해준 stackoverflow에서 찾은 질문과 답변입니다. 저에게는 QueryCursor라는 이벤트를 이렇게도 사용할 있구나 하는 새로운 발견이었습니다.

 

참고 : Make Tooltip of WPF slider stay on screen while dragging