[Silverlight] Custom Control 작성법

2010. 5. 21. 11:22WEB/Silverlight

이 문서는 Silverlight 3.0 기준으로 Style, ControlTemplate, XML Namespance, UserControl에 기본적인 지식이 있는 사용자를 대상으로 작성되었습니다.

Silverlight에서는 기본적으로 UserControl을 지원합니다. 이 UserControl은 완전히 외부와 분리된, 캡슐화된 컨트롤을 생성할 수 있으며, 다양한 곳에서 재사용될 수 있습니다. 하지만 만약 기능은 동일하지만 UI만 다르게 보여주고 싶다면 어떻게 해야 할까요? 물론 다양한 방법이 있겠지만 UserControl의 로직과 디자인이 모두 수정되어야 할 가능성이 높거나, 혹은 디자인 별로 N개의 컨트롤을 생성해야 할 것입니다.

여기서 제시하는 CustomControl을 사용하면, Logic의 수정 없이 디자이너는 작성된 Control의 UI를 쉽게 변경할 수 있습니다. 기본적으로 제공되는 다른 Silverlight Control 처럼 Style을 지정하거나 ControlTemplate를 사용할 수 있게 됩니다.

작성법

여기에서는 예제로 WatermarkTextBox를 제작해보도록 하겠습니다.
  1. 먼저 ‘Silverlight Class Library Project’를 생성합니다. 이 프로젝트를 생성하면 class파일인 .cs파일이 덩그러니 생성될 것입니다. 먼저 이 파일을 삭제합니다.
  2. 그리고 생성할 CustomControl명의 Class파일을 추가합니다. 여기서 저는 WatermarkTextBox 생성할 것이므로 WatermarkTextBox.cs파일을 추가하였습니다. 그리고 TextBox Class를 상속받습니다.
    using System;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Ink;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Shapes;
    
    namespace Nsi.Silverlight.Controls
    {
        public class WatermarkTextBox : TextBox
        {
    
        }
    }
    여기서 TextBox가 아니라 DatePicker 같은 다른 컨트롤을 상속 받을 수 있습니다. 무엇을 상속 받느냐에 따라 어떤 Property를 기본적으로 사용할 수 있는지가 결정됩니다. 앞으로 이 파일에 Control의 Logic 부분을 작성하게 될 것입니다.
  3. Control의 외관을 지정하기 위해서는 ‘themes’ 폴더를 생성하고, 그 폴더 안에 ‘generic.xaml’파일을 생성해야 합니다. ‘themes’ 폴더를 생성하고 내부에 XML파일을 ‘generic.xaml’파일명으로 추가합니다.
  4. 추가된 ‘generic.xaml’파일은 기본적으로 ‘Build Action’이 ‘Page’로 설정되어 있을 것입니다. ‘Page’에서 ‘Resurce’로 변경합니다.
    image
  5. 또 한가지 더 ‘Custom Tool’ 값으로 설정된 ‘MsBuild:MarkupCompilePass1’를 삭제합니다. 
    image 
  6. generic.xaml은 다음과 같은 기본적은 코드를 포함해야 합니다. 물론 기존의 코드가 있다면 삭제하고 다음과 같은 코드를 입력합니다.
    <ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    </ResourceDictionary>
    이로써 generic.xaml은 Resource Dictionary로서 Custom Control의 외형을 제공하는 Template 파일로 환골탈태하였습니다.
  7. Control의 Style과 Template를 작성해줍니다. Style의 TargetType을 설정해 주어야 하는데, TargetType는 여기서 작성하는 CustomControl을 지정해줍니다.
    <ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Nsi.Silverlight.Controls"> 
        <Style TargetType="local:WatermarkedTextBox">
    
        </Style>
    </ResourceDictionary>
  8. 이제 Control의 Template을 지정해줍니다. Style의 하위 엘리먼트인 Setter에 Property="Template"을 추가하고, ControlTemplate을 작성하여 Control의 외형을 잡아줍니다.
    <ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Nsi.Silverlight.Controls">
        <Style TargetType="local:Nsi.Silverlight.Controls">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="local:WatermarkTextBox">
                        <Grid x:Name="Root" Width="{TemplateBinding Width}" Cursor="{TemplateBinding Cursor}" Height="{TemplateBinding Height}" Background="{TemplateBinding Background}" VerticalAlignment="{TemplateBinding VerticalAlignment}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="WatermarkStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition From="WatermarkHidden" GeneratedDuration="00:00:00.0000000" To="WatermarkVisible"/>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="WatermarkVisible">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0000000" Storyboard.TargetName="WaterMarkTextBlock" Storyboard.TargetProperty="(UIElement.Opacity)">
                                                <EasingDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="WatermarkHidden">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0000000" Storyboard.TargetName="WaterMarkTextBlock" Storyboard.TargetProperty="(UIElement.Opacity)">
                                                <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <TextBlock x:Name="WatermarkTextBlock" Text="{TemplateBinding WatermarkText}"  Margin="3" VerticalAlignment="Center" />
                            <TextBox x:Name="WatermarkedTextBox" Background="Transparent" Foreground="{TemplateBinding Foreground}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>
    위 코드에서 TemplateBinding의 값들은 모두 Class파일에서 상속했던 TextBox에서 기본적으로 제공되는 것입니다. 만약 여기에 없는 Property를 TemplateBinding으로 사용하고 싶다면, 해당 Property를 Class파일에 DependencyProperty로 선언해주어야 합니다. 위에서는 WatermarkText라는 새로운 Property를 TemplateBinding으로 사용하고 있습니다.
  9. 이제 Class파일로 돌아옵니다. 생성자를 생성하고 DefaultStyleKey를 generic.xaml에서 지정한 Style을 사용하도록 다음과 같이 코드를 작성합니다.
    namespace Nsi.Silverlight.Controls
    {
        public class WatermarkTextBox : Control
        {
            public WatermarkTextBox()
            {
                DefaultStyleKey = typeof(WatermarkTextBox);
            }
        }
    }
    위 코드를 작성함으로써 시스템은 generic.xaml의 WatermarkedTextBox타입의 Style를 기본 Style로 사용합니다.
  10. 이제 generic.xaml에 정의된 각 컨트롤과 상호작용을 해야 합니다. 그러기 위해서는 가장 먼저 generic.xaml에 정의된 컨트롤을 찾아오는 것입니다. 컨트롤을 찾기는 OnApplyTemplate()메서드를 오버라이딩하고, OnApplyTemplate()메서드 내에서 GetTemplateChild()메서드에 해당 컨트롤 명을 넘겨줌으로써 가능합니다.
    using System;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Ink;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Shapes;
    
    namespace Nsi.Silverlight.Controls
    {
        public class WatermarkTextBox : Control
        {
            //Control명을 지정
            public const string RootElementName = "Root";
            public const string WatermarkTextBlockElementName = "WatermarkTextBlock";
            public const string WatermarkedTextBoxElementName = "WatermarkedTextBox";
    
            //찾은 Control을 담고 있을 멤버변수
            private Grid RootElement;
            private TextBlock WatermarkTextBlock;
            private TextBox WatermarkedTextBox;
    
            public WatermarkTextBox()
            {
                DefaultStyleKey = typeof(WatermarkTextBox);
            }
            //generic.xaml에 정의된 컨트롤 찾기
            public override void OnApplyTemplate()
            {
                base.OnApplyTemplate();
    
                        //Control 찾기
                RootElement = GetTemplateChild(RootElementName) as Grid;
                WatermarkTextBlock = GetTemplateChild(WatermarkTextBlockElementName) as TextBlock;
                WatermarkedTextBox = GetTemplateChild(WatermarkedTextBoxElementName) as TextBox;
            }
        }
    }
  11. 컨트롤을 찾았으면, 이벤트나 필요한 설정 등을 추가적으로 해줍니다.
    using System;
    using System.Net;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Ink;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Shapes;
    
    namespace Nsi.Silverlight.Controls
    {
        public class WatermarkTextBox : Control
        {
            //Control Name 지정
            public const string RootElementName = "Root";
            public const string WatermarkTextBlockElementName = "WatermarkTextBlock";
            public const string WatermarkedTextBoxElementName = "WatermarkedTextBox";
    
            //찾은 Control을 담고 있을 멤버변수
            private Grid RootElement;
            private TextBlock WatermarkTextBlock;
            private TextBox WatermarkedTextBox;
    
            public WatermarkTextBox()
            {
                DefaultStyleKey = typeof(WatermarkTextBox);
            }
            //generic.xaml에 정의된 컨트롤 찾기
            public override void OnApplyTemplate()
            {
                base.OnApplyTemplate();
    
                RootElement = GetTemplateChild(RootElementName) as Grid;
                WatermarkTextBlock = GetTemplateChild(WatermarkTextBlockElementName) as TextBlock;
                WatermarkedTextBox = GetTemplateChild(WatermarkedTextBoxElementName) as TextBox;
    
                //Attach Events
                WatermarkedTextBox.GotFocus -= new RoutedEventHandler(WatermarkedTextBox_GotFocus);
                WatermarkedTextBox.LostFocus -= new RoutedEventHandler(WatermarkedTextBox_LostFocus);
                WatermarkedTextBox.TextChanged -= new TextChangedEventHandler(WatermarkedTextBox_TextChanged);
    
                WatermarkedTextBox.GotFocus += new RoutedEventHandler(WatermarkedTextBox_GotFocus);
                WatermarkedTextBox.LostFocus += new RoutedEventHandler(WatermarkedTextBox_LostFocus);
                WatermarkedTextBox.TextChanged += new TextChangedEventHandler(WatermarkedTextBox_TextChanged);
            }
    
            //Events
            private void WatermarkedTextBox_GotFocus(object sender, RoutedEventArgs e)
            {
                VisualStateManager.GoToState(this, "WatermarkHidden", false);
            }
            private void WatermarkedTextBox_LostFocus(object sender, RoutedEventArgs e)
            {
                if (string.IsNullOrEmpty(this.WatermarkedTextBox.Text))
                    VisualStateManager.GoToState(this, "WatermarkVisible", true);
            }
            private void WatermarkedTextBox_TextChanged(object sender, RoutedEventArgs e)
            {
                if (this.WatermarkedTextBox.Text.Trim() == "")
                    VisualStateManager.GoToState(this, "WatermarkVisible", true);
                else
                    VisualStateManager.GoToState(this, "WatermarkHidden", true);
            }
        }
    }
  12. 이제 generic.xaml에서 사용한 WatermarkText라는 Property를 DependencyProperty로 선언해야 합니다. DepdendencyProperty를 선언하기 위해서는 기존 Property선언과 비교할 때, 조금 더 코드를 입력해주면 됩니다. 먼저 외부에 공개될 Property에서 사용할 DependencyProperty를 먼저 등록해줍니다.
    //DependencyProperty
            public static DependencyProperty WatermarkTextProperty = DependencyProperty.Register("WatermarkText", typeof(string), typeof(WatermarkedTextBox), new PropertyMetadata(""));
    PropertyMetadata에는 두 가지 값을 설정할 수 있는데, 이 컨트롤이 로드 될 때 설정될 기본 값과, 이 값이 변경될 때 호출될 CollBack method(쉽게 말해 Changed 이벤트)를 지정할 수 있습니다. 참고하시기 바랍니다.
  13. 이제 외부에 공개될 Public Property를 다음과 같이 DependencyProperty를 사용해서 선언해줍니다.
    public string WatermarkText
    {
        get { return (string)GetValue(WatermarkTextProperty); }
        set { SetValue(WatermarkTextProperty, value); }
    }
  14. 이제 마지막으로 TemplatePart를 추가해주어야 합니다. 지정하지 않아도 크게 동작에는 문제가 없습니다. 그런데도 TemplatePart를 지정하는 이유는 이 CustomControl의 UI를 디자이너가 변경하고자 할 때, 반드시 포함되어야 하는 Control 명을 디자이너에게 알려주기 위해서입니다. class의 상단에 다음과 같이 코드를 추가합니다.
    [TemplatePart(Name = WatermarkTextBox.RootElementName, Type = typeof(Grid))]
    [TemplatePart(Name = WatermarkTextBox.WatermarkTextBlockElementName, Type = typeof(TextBox))]
    [TemplatePart(Name = WatermarkTextBox.WatermarkTextBoxElementName, Type = typeof(TextBox))]
    public class WatermarkTextBox : Control
    {
        ...
    }
  15. 이제 테스트 해볼 차례입니다. 프로젝트를 Build하면 Bin/Debug폴더에 WatermarkTextbox.dll이 생성될 것입니다.
    image
    이 파일을 사용할 Silverlight Project에 Add Reference를 통해 추가하고 페이지에 다음과 같은 코드로 컨트롤을 사용해봅시다.
    <UserControl x:Class="NsiDatePicker.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:watermarkTextBox="clr-namespace:Nsi.Silverlight.Controls;assembly=WatermarkTextBox"
        Width="400" Height="300">
        <StackPanel x:Name="LayoutRoot" Background="White">
            <watermarkTextBox:WatermarkTextBox WatermarkText="입력해주세요." />
        </StackPanel>
    </UserControl>

    결과화면

    image