[Silverlight] Custom Control 작성법
2010. 5. 21. 11:22ㆍWEB/Silverlight
이 문서는 Silverlight 3.0 기준으로 Style, ControlTemplate, XML Namespance, UserControl에 기본적인 지식이 있는 사용자를 대상으로 작성되었습니다.
Silverlight에서는 기본적으로 UserControl을 지원합니다. 이 UserControl은 완전히 외부와 분리된, 캡슐화된 컨트롤을 생성할 수 있으며, 다양한 곳에서 재사용될 수 있습니다. 하지만 만약 기능은 동일하지만 UI만 다르게 보여주고 싶다면 어떻게 해야 할까요? 물론 다양한 방법이 있겠지만 UserControl의 로직과 디자인이 모두 수정되어야 할 가능성이 높거나, 혹은 디자인 별로 N개의 컨트롤을 생성해야 할 것입니다.
여기서 제시하는 CustomControl을 사용하면, Logic의 수정 없이 디자이너는 작성된 Control의 UI를 쉽게 변경할 수 있습니다. 기본적으로 제공되는 다른 Silverlight Control 처럼 Style을 지정하거나 ControlTemplate를 사용할 수 있게 됩니다.
작성법
여기에서는 예제로 WatermarkTextBox를 제작해보도록 하겠습니다.- 먼저 ‘Silverlight Class Library Project’를 생성합니다. 이 프로젝트를 생성하면 class파일인 .cs파일이 덩그러니 생성될 것입니다. 먼저 이 파일을 삭제합니다.
- 그리고 생성할 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 부분을 작성하게 될 것입니다. - Control의 외관을 지정하기 위해서는 ‘themes’ 폴더를 생성하고, 그 폴더 안에 ‘generic.xaml’파일을 생성해야 합니다. ‘themes’ 폴더를 생성하고 내부에 XML파일을 ‘generic.xaml’파일명으로 추가합니다.
- 추가된 ‘generic.xaml’파일은 기본적으로 ‘Build Action’이 ‘Page’로 설정되어 있을 것입니다. ‘Page’에서 ‘Resurce’로 변경합니다.
- 또 한가지 더 ‘Custom Tool’ 값으로 설정된 ‘MsBuild:MarkupCompilePass1’를 삭제합니다.
- 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 파일로 환골탈태하였습니다. - 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>
- 이제 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으로 사용하고 있습니다. - 이제 Class파일로 돌아옵니다. 생성자를 생성하고 DefaultStyleKey를 generic.xaml에서 지정한 Style을 사용하도록 다음과 같이 코드를 작성합니다.
namespace Nsi.Silverlight.Controls { public class WatermarkTextBox : Control { public WatermarkTextBox() { DefaultStyleKey = typeof(WatermarkTextBox); } } }
위 코드를 작성함으로써 시스템은 generic.xaml의 WatermarkedTextBox타입의 Style를 기본 Style로 사용합니다. - 이제 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; } } }
- 컨트롤을 찾았으면, 이벤트나 필요한 설정 등을 추가적으로 해줍니다.
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); } } }
- 이제 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 이벤트)를 지정할 수 있습니다. 참고하시기 바랍니다. - 이제 외부에 공개될 Public Property를 다음과 같이 DependencyProperty를 사용해서 선언해줍니다.
public string WatermarkText { get { return (string)GetValue(WatermarkTextProperty); } set { SetValue(WatermarkTextProperty, value); } }
- 이제 마지막으로 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 { ... }
- 이제 테스트 해볼 차례입니다. 프로젝트를 Build하면 Bin/Debug폴더에 WatermarkTextbox.dll이 생성될 것입니다.
이 파일을 사용할 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>
결과화면
'WEB > Silverlight' 카테고리의 다른 글
[Silverlight] SortDescriptions in Silverlight DataGrid (0) | 2011.02.23 |
---|---|
[Silverlight] 컨트롤에 Validation Tooltip 적용하기 (0) | 2010.05.22 |
[Silverlight] 'ServiceReferences.ClientConfig'을(를) 열 수 없습니다. (0) | 2010.04.15 |
[Silverlight] Element to Element Binding (0) | 2009.11.17 |
[Silverlight] Create CheckBoxList in Silverlight (0) | 2009.11.16 |