Silverlight 1.1 바로 시작하기

커스텀 컨트롤
준비 사항
Silverlight 개발의 기초에서 개발에 필요한 도구와 기술에 대해 설명하고 있습니다.
이벤트 핸들링에서 개체의 이벤트를 처리하는 방법에 대해 설명하고 있습니다.

커스텀 컨트롤 만들기

컨트롤의 기본 구성
Silverlight에서 컨트롤은 UI를 구성하는 XAML 마크업과 개체 모델을 구성하는 코드-비하인드로 구분할 수 있습니다. 특히 Silverlight와 같이 풍부한 사용자 인터페이스가 강조되는 프로그래밍에서 UI의 미려함은 매우 중요하므로 디자이너는 미적 감각을 충분히 살려 디자인된 컨트롤을 마크업으로 전달하고 개발자는 코드-비하인드에서 컨트롤을 모델링하고 제어하여 다른 XAML 페이지에서 재사용할 수 있습니다.

이렇게 작성된 XAML과 코드는 하나의 라이브러리 파일(.dll)로 컴파일 되며 다른 프로젝트에서는 이 라이브러리 파일의 어셈블리를 참조하여 사용이 가능합니다.

여기에서는 커스텀 컨트롤의 개요를 살펴보기 위해 MyButton이라는 간단한 버튼을 만들어보겠습니다.
이 버튼은 아주 단순하게 캡션과 너비 및 높이 속성을 가지며 클릭 이벤트를 노출하여 버튼이 클릭되었을 때 핸들링 할 수 있도록 할 것입니다. 또한 속성을 변경하면 캡션이 현재 너비 및 높이의 한 가운데에 표시될 수 있도록 하겠습니다.

Silverlight 컨트롤 프로젝트 만들기
기본적으로 커스텀 컨트롤은 XAML이나 코드를 포함하지 않는 라이브러리의 형태로 배포될 것입니다. 때문에 Silverlight 컨트롤은 일반적으로 Silverlight 클래스 라이브러리 프로젝트를 기반으로 작성합니다.

Howto:3-1 Silverlight 컨트롤 작성을 위한 클래스 라이브러리 프로젝트 만들기
1. Visual Studio를 실행하고 메뉴에서 File->New->Project를 선택하거나 Ctrl+Shift+N를 눌러 새 프로젝트를 선택합니다.
2. 새 프로젝트 다이얼로그 박스에서 Project types를 Visual C#->Silverlight로 선택하고 Templates에서 Silverlight Class Library를 선택한 후 프로젝트 이름을 MyButton이라고 입력하고 [OK]를 누릅니다.
사용자 삽입 이미지

3. 기본으로 생성되어 있는 Class1.cs는 필요 없으므로 솔루션 익스플로러에서 Class1.cs를 선택하고 마우스 오른쪽 버튼을 누른 후 Delete를 선택하여 삭제합니다.
4. 메뉴에서 Project->Add New Item을 선택하거나 Ctrl+Shift+A를 눌러 새 아이템을 선택합니다.
5. 새 아이템 다이얼로그 박스에서 Silverlight User Control을 선택하고 클래스 이름을 MyButton이라고 입력하고 [OK]를 누릅니다.
사용자 삽입 이미지

노트
다시 설명하겠지만, Silverlight User Control은 하나의 라이브러리 파일로 배포하기 위해 XAML파일의 build action 속성을 embedded resource 형태로 설정하며 따라서 컴파일시 XAML 파일은 어셈블리에 포함되게 됩니다.


UI 디자인하기
실제 작성해야 할 컨트롤은 미려한 디자인과 기능성 그리고 인터페이스 접근성을 고려해야 할 것입니다. 그러기 위해 UI의 디자인은 Expression Blend나 Design 등 XAML 코드를 작성할 수 있는 디자인 툴을 사용하는 것이 좋습니다.

여기에서는 단순히 TextBlock을 하나 만드는 걸로 만족하겠습니다.
Howto:3-2 XAML로 기본적인 UI 디자인 하기
1. MyButton.xaml을 열고 다음 코드를 루트 Canvas 엘리먼트 안에 넣습니다.
XAML
<TextBlock x:Name="text"></TextBlock>

2. 이 TextBlock은 버튼의 캡션을 표시하는 역할을 수행할 것입니다.
코드에서 제어할 필요가 있는 UI 요소는 반드시 x:Name으로 이름을 지정하여 코드에서 참조할 수 있도록 해야 합니다.

개체 참조 얻기
디자인이 완료되었으면 코드-비하인드에서는 UI 모델을 구현하기 위해 제어할 UI 개체의 참조를 획득해야 합니다.
MyButton.xaml.cs를 보면 다음과 같이 Control 클래스를 상속하며 기본 생성자가 미리 만들어져 있음을 확인할 수 있습니다.
C# 기본 생성자
namespace MyButton
{
    public class MyButton : Control
    {
        public MyButton()
        {

            System.IO.Stream s = this.GetType().Assembly.GetManifestResourceStream("MyButton.MyButton.xaml");
            this.InitializeFromXaml(new System.IO.StreamReader(s).ReadToEnd());
        }
    }
}

이 코드는 MyButton 클래스의 어셈블리로부터 XAML 파일을 리소스 스트림으로 읽어와서 그 스트림으로부터 초기화를 수행합니다. 이를 위해 MyButton.xaml 파일의 속성을 보면 build action이 embedded resource로 설정되어 있을 것입니다.

이 특수한 형태의 초기화는 Control 클래스의 InitializeFromXaml 메소드를 통해 구현됩니다.

미리 생성된 코드에서 InitializeFromXaml 메소드를 호출만 하지만 이 메소드는 XAML 스트림을 읽어서 초기화한 후 이 XAML의 루트 개체에 대한 참조를 반환합니다. 이 참조를 클래스의 지역 변수로 저장하여 UI 개체를 코드로 제어할 수 있습니다.

또한 루트 개체에 대한 참조를 얻으면 XAML에서 x:Name을 지정한 개체에 대한 참조를 FindName 메소드를 사용하여 얻을 수 있고 이런 식으로 XAML내의 모든 개체를 코드에서 제어할 수 있습니다.
Howto:3-3 XAML에서 디자인된 개체의 참조를 얻기
1. MyButton.xaml.cs를 엽니다.
2. 클래스의 private 지역 변수로 FrameworkElement 타입의 implementationRoot를 선언하고 InitializeFromXaml의 반환값을 implementationRoot에 대입합니다.
3. TextBlock에 대한 참조를 보유하기 위해 클래스의 private 지역 변수로 TextBlock 타입의 text를 선언하고 implementationRoot.FindName("text") 메소드를 사용하여 참조를 얻습니다. 이렇게 완성된 코드는 다음과 같습니다.

C# 개체 참조를 보유하기 위한 코드 수정
namespace MyButton
{
    public class MyButton : Control
    {
        private FrameworkElement implementationRoot;
        private TextBlock text;

        public MyButton()
        {

            System.IO.Stream s = this.GetType().Assembly.GetManifestResourceStream("MyButton.MyButton.xaml");
            implementationRoot =
this.InitializeFromXaml(new System.IO.StreamReader(s).ReadToEnd());

            text = implementationRoot.FindName("text") as TextBlock;
        }
    }
}

속성 및 이벤트 추가하기
컨트롤에서 가장 중요한 것은 적절한 속성(Property)과 이벤트(Event)를 정의하는 것입니다.
속성과 이벤트는 이 컨트롤을 사용하는 마크업에서 어트리뷰트의 형식으로 선언하고 설정할 수 있으며 이 컨트롤 개체에 대한 참조를 획득한 코드에서도 접근할 수 있습니다. 앞서 얘기한 것처럼 여기에서는 캡션(Caption), 너비(Width), 높이(Height) 속성과 Click 이벤트를 추가하도록 하겠습니다.

Control 클래스는 FrameworkElement를 상속하고 있으며 기본적인 너비(Width), 높이(Height), 클리핑 영역(Clip), 커서(Cursor), 불투명도(Opacity) 등 많은 기본적인 속성을 그대로 받아 옵니다. 따라서 우리가 추가하려는 속성인 너비와 높이는 FrameworkElement에서 이미 선언이 되어 있으며 이 속성은 virtual이 아니므로 오버라이딩이 불가능하고 만약 똑같이 Width라는 이름의 속성을 만든다면 컴파일러는 이 속성이 FrameworkElement.Width 속성에 의해 숨겨질 것이라는 경고를 내릴 것입니다.

이렇게 부모 클래스에서 미리 선언된 일반적인 이름의 속성을 재정의 하고 싶을 때에는 속성의 선언에 new 키워드를 추가해야 합니다.
Howto:3-4 속성 추가하기
1. Width 속성을 추가합니다. Width는 implementationRoot 즉, 컨테이너 역할을 하는 루트 캔버스 개체의 너비와 싱크되며 이 값을 수정하면 텍스트의 위치를 가운데 정렬하기 위해 레이아웃 계산을 다시 수행합니다. Width라는 이름은 이 컨트롤이 상속하는 FrameworkElement의 Width와 중복되어 이 속성이 숨겨지므로 new 키워드를 사용하여 재정의 하고 이 속성을 다른 클래스로 파생할 수 있도록 virtual 키워드도 추가하였습니다.
C# Width 속성 정의(클래스에 추가할 것)
public virtual new double Width
{
    get { return implementationRoot.Width; }
    set
    {
        implementationRoot.Width = value;
        UpdateLayout();
    }
}

UpdateLayout 메소드는 텍스트의 위치를 중앙 정렬하는 기능을 수행하며 일반적으로 컨트롤의 속성이 변경되어 레이아웃이나 화면 출력을 변경할 필요가 있을 때 내부적으로 사용됩니다. UpdateLayout 메소드의 정의는 아래에 있습니다.

2. Width와 마찬가지로 Height 속성을 추가합니다.
C# Height 속성 정의(클래스에 추가할 것)
public virtual new double Height
{
    get { return implementationRoot.Height; }
    set
    {
        implementationRoot.Height= value;
        UpdateLayout();
    }
}

3. 텍스트 내용을 나타내는 Caption 속성을 추가합니다. Caption은 새로 정의하는 속성이므로 일반적인 속성 선언과 같이 하면 됩니다.
C# Caption 속성 정의(클래스에 추가할 것)
public string Caption
{
    get { return text.Text; }
    set
    {
        text.Text = value;
        UpdateLayout();
    }
}

4. 속성이 변경될 때 텍스트 위치 계산을 위한 UpdateLayout 메소드를 구현합니다. UpdateLayout은 내부적으로만 사용되므로 private으로 선언합니다. TextBlock은 현재 자신의 텍스트 내용에 해당하는 너비와 높이를 반환하는 ActualWidth와 ActualHeight이라는 편리한 속성을 제공합니다.
C# UpdateLayout 메소드 정의(클래스에 추가할 것)
private void UpdateLayout()
{
    text.SetValue(Canvas.LeftProperty, (implementationRoot.Width - text.ActualWidth) / 2);
    text.SetValue(Canvas.TopProperty, (implementationRoot.Height - text.ActualHeight) / 2);
}

이제 이벤트를 추가합니다.
우리가 추가할 이벤트는 버튼 클릭이 완료된 시점입니다. 클릭 동작은 마우스 버튼이 눌렸다가(down) 뗀(up) 그 순간을 의미하며 이런 이벤트는 별도로 정의되어 있지 않습니다.

주의해야 할 점은 버튼 위에서 마우스 버튼을 눌렀다가 떼지 않고 버튼 바깥으로 이동할 수도 있다는 겁니다. 따라서 클릭 이벤트를 구현하기 위해서는 마우스 버튼이 눌린 상태를 기억하는 변수를 하나 만들고 버튼 바깥으로 마우스가 나간 경우 그 상태를 클리어 해줄 필요가 있습니다.
Howto:3-5 이벤트 핸들링 및 외부로 노출할 이벤트 정의
1. 마우스의 눌린 상태를 기억하기 위한 멤버 변수를 클래스의 private으로 선언합니다.
C# 마우스 버튼 상태 멤버 변수 추가(클래스에 추가할 것)
private bool mouseDowned;

2. 외부로 노출할 이벤트를 선언합니다. 이벤트는 delegate를 정의하여 전달할 파라미터의 타입을 명시해야 하는데 우리가 추가할 Click 이벤트는 특별히 전달해야 할 파라미터가 없으므로 미리 선언된 delegate인 EventHandler를 사용하면 됩니다. 클래스에 다음의 이벤트 선언을 추가합니다. 만약 사용자가 특정한 파라미터를 전달하고 싶다면 EventArgs 클래스를 상속하는 파라미터 전달용 클래스를 정의하고 delegate를 따로 선언해야 합니다.
 
C# 이벤트 선언 추가(클래스에 추가할 것)
public event EventHandler Clicked;

3. 루트 개체의 마우스 이벤트를 처리할 이벤트 핸들러를 추가합니다. 여기에서 필요한 이벤트는 MouseLeftButtonDown, MouseLeftButtonUp 그리고 MouseLeave 입니다. MyButton 생성자 코드에 다음 코드를 추가합니다.
C# 이벤트 핸들러 추가(생성자에 추가할 것)
implementationRoot.MouseLeftButtonDown += new MouseEventHandler(implementationRoot_MouseLeftButtonDown);
implementationRoot.MouseLeftButtonUp += new MouseEventHandler(implementationRoot_MouseLeftButtonUp);
implementationRoot.MouseLeave += new EventHandler(implementationRoot_MouseLeave);

기본적으로 이벤트 핸들러는 코드 자동완성 기능을 사용하는게 편리합니다. 예를 들어 MouseLeftButtonDown 이벤트는 MouseLeftButtonDown += 까지 입력하고 탭을 두번 누르면 골격 코드가 자동으로 생성됩니다.

4. 자동으로 생성된 코드를 수정하여 마우스 버튼이 눌렸을 때 mouseDowned를 true로 설정하고 마우스 버튼이 들렸을 때 mouseDowned가 true일 경우 Clicked 이벤트를 발생시키고 마우스 커서가 빠져나갔을 때 mouseDowned를 false로 설정합니다.
C# 이벤트 핸들러 구현
void implementationRoot_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
    mouseDowned = true;
}

void implementationRoot_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
    if (mouseDowned == true && Clicked != null)
    {
        Clicked(this, null);
        mouseDowned = false;
    }
}

void implementationRoot_MouseLeave(object sender, EventArgs e)
{
    mouseDowned = false;
}

주의할 점은 Clicked 이벤트가 null인지 여부를 반드시 확인해야 합니다. 또 여기서 Clicked 이벤트의 EventArgs를 null로 전달하였기 때문에 사용하는 쪽에서 이 파라미터를 null 체크 없이 접근할 경우 억세스 위반이 일어날 것입니다.

여기까지 작성된 소스코드 및 프로젝트를 첨부하였습니다. 직접 작성한 것과 비교해보세요 :)
MyButton1.zip

첫번째 예제



컨트롤 테스트 하기

테스트를 위한 Silverlight 페이지 프로젝트 추가
작성된 컨트롤은 라이브러리일 뿐 실제 UI가 아닙니다. 이 컨트롤을 테스트하려면 실제로 이 컨트롤을 Silverlight 페이지에 로드하여 표시해야 합니다.

Howto:3-6 테스트 프로젝트 추가하기
1. MyButton 프로젝트를 그대로 열어놓은 상태에서 메뉴의 File->Add->New Project를 선택하고 Silverlight Project 템플릿을 선택한 후 이름을 MyButtonTester라고 설정하고 [OK]를 누릅니다. 솔루션 익스플로러에서 다음과 같은 모습을 확인할 수 있습니다.


2. 메뉴의 Project->Add Reference를 선택하거나 솔루션 익스플로러에서 MyButtonTester 프로젝트의 Reference 항목에서 마우스 오른쪽 버튼을 클릭한 후 Add Reference를 선택합니다. 참조 추가 창에서 [Project] 탭을 보면 작성된 MyButton 프로젝트 항목이 있을 것입니다. MyButton을 선택하고 [OK]를 누릅니다.


3. 솔루션 익스플로러의 MyButtonTester 프로젝트에서 마우스 오른쪽 버튼을 클릭한 후 Set as Startup Project를 선택하고 TestPage.html에서 마우스 오른쪽 버튼을 클릭한 후 Set as Startup Page를 선택하여 테스트 프로젝트로 디버깅 할 수 있게 합니다.

4. 테스트할 페이지 즉, Page.xaml의 루트 엘리먼트에 사용할 컨트롤의 네임스페이스를 선언하고 어셈블리를 지정합니다.
XAML 네임스페이스 선언 및 어셈블리 지정
<Canvas x:Name="parentCanvas"
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Loaded="Page_Loaded"
    x:Class="MyButtonTester.Page;assembly=ClientBin/MyButtonTester.dll"
    xmlns:my="clr-namespace:MyButton;assembly=ClientBin/MyButton.dll"
    Width="640"
    Height="480"
    Background="White">
</Canvas>

외부 라이브러리를 사용할 때에는 위와 같이 루트 엘리먼트에서 "xmlns:이름" 구문으로 네임스페이스를 지정하고 clr-namespace를 "컨트롤의 네임스페이스명" 으로 지정하며 assembly를 라이브러리 파일(.dll)의 경로로 지정해주면 됩니다.

5. 루트 캔버스에 컨트롤을 추가합니다.
XAML 컨트롤 추가
<Canvas ... 생략 ...>
    <my:MyButton x:Name="testButton" Width="100" Height="50" Caption="button test" />

</Canvas>

6. 파일을 저장하고 Clicked 이벤트를 처리하기 위해 코드-비하인드에 Clicked 이벤트 핸들러를 추가합니다. 앞서서 x:Name으로 testButton을 지정했으므로 코드-비하인드에서는 자동 생성된 코드에 의해 같은 이름으로 개체를 참조할 수 있습니다.
C# MyButton의 Clicked 이벤트 핸들러 추가
public void Page_Loaded(object o, EventArgs e)
{
    // Required to initialize variables
    InitializeComponent();
    testButton.Clicked += new EventHandler(testButton_Clicked);
}

void testButton_Clicked(object sender, EventArgs e)
{
    testButton.Caption = "Clicked!";
}

7. F6을 눌러 빌드하고 F5를 눌러 디버그 모드로 실행하여 클릭했을 때 글자가 바뀌는지 확인합니다.


여기까지 진행해보면 분명히 클릭 이벤트는 잘 작동하지만 우리가 의도한대로 텍스트가 버튼의 한가운데로 정렬되지 않았음을 알 수 있습니다. 왜냐하면 마크업에서 MyButton의 어트리뷰트에 Width=100 Height=50을 지정하였는데 이대로라면 텍스트가 해당 위치를 벗어나지 않는 다는 것을 의미합니다. 그런데 출력된 텍스트는 한참 멀리 떨어져 있고 자세히 보면 640, 480의 한가운데로 정렬되어 있음을 알 수 있습니다.

이 문제는 Silverlight의 버그인지 사용 방법상의 차이인지 확실치 않습니다만 다음 절에서 얘기할 약간의 추가 작업을 통해 수정할 수 있습니다.
우선 여기까지의 프로젝트를 첨부하였습니다.
MyButton2.zip

두번째 예제



특수한 문제점과 해결

Silverlight 페이지 즉, MyButtonTester에서 MyButton을 사용할 때의 모습을 간략하게 도식화하면 다음과 같습니다.


MyButtonTester의 XAML코드에서 MyButton을 하나 선언하였고 여기에서 Width, Height, Caption속성을 설정(set)하였습니다. 따라서 이 프로젝트를 디버깅 모드로 실행했을 때 MyButton의 Width, Height, Caption 속성의 'set' 부분의 코드가 실행될 것을 기대할 수 있습니다.
MyButton.xaml.cs에서 Width, Height, Caption 속성의 'set' 코드에 F9를 눌러 브레이크 포인트를 각각 설정하고 F5를 눌러 디버깅 모드로 실행해보시기 바랍니다.

이상하게도 Caption 속성에는 브레이크가 걸리지만 Width와 Height에는 걸리지 않는 걸 확인할 수 있습니다.
혹시 Width와 Height이 정상적으로 작동하지 않은지 확인하기 위해 MyButtonTester의 코드-비하인드에서 코드로 직접 Width와 Height을 설정해보면 그 코드에 대해서는 브레이크가 작동하며 코드 자체는 정상적이라는 걸 알 수 있습니다.

이 문제는 Control에서 상속받은 모든 기본 속성에서 공통적으로 나타납니다. 따라서 Control 및 그 상위 클래스들에서 상속받는 속성과 같은 이름의 속성을 재정의했을 때에는 이들 속성에 대한 초기화를 별도로 해줄 필요가 있습니다.

Howto:3-7 상위 클래스에서 상속받은 속성과 같은 이름을 가지는 속성을 재정의 할 때의 추가 작업
1. MyButton 클래스의 생성자에 컨트롤이 로드 완료되었을 때 처리할 이벤트 핸들러를 추가합니다. 마찬가지로 코드 자동 생성기능을 활용합니다.
C# 컨트롤 로드 완료시 처리 이벤트 핸들러 추가
public MyButton()
{
    // ... 생략
    implementationRoot.Loaded += new EventHandler(implementationRoot_Loaded);
}

2. 추가된 이벤트 핸들러에서 상위 클래스(base)와 중복되는 이름의 속성을 하나씩 가져옵니다.
C# 이벤트 핸들러 작성
// 클라이언트 측에서 이 컨트롤의 로드를 완료하였을 때 처리
void implementationRoot_Loaded(object sender, EventArgs e)
{
    // 로드가 완료되었을 때 상위 클래스의 속성으로부터 미완료된 속성을 수작업으로 설정해야 함
    implementationRoot.Width = base.Width;
    implementationRoot.Height = base.Height;
    UpdateLayout();
}

3. F6을 눌러 빌드하고 F5를 눌러 다시 실행하여 정상적으로 Width와 Height 속성이 적용되었는지 확인합니다.

여기까지 완성된 프로젝트입니다.
MyButton3.zip

완성된 컨트롤 프로젝트


이 외에도 커스텀으로 작성한 컨트롤을 올린 페이지는 Expression Blend를 사용할 수 없게 되며 VS의 IDE에서도 XAML 코드의 인텔리센스를 지원하지 않는 등 외부적인 문제도 아직 남아있습니다. 하지만 이 문제는 아마도 정식판이 나오면서 해결되지 않을까 기대를 해봅니다.

참고

Silverlight 공식 QuickStarts 참고:
http://silverlight.net/QuickStarts/BuildUi/CustomControl.aspx

Silverlight 샘플 컨트롤 참고: Silverlight 1.1 SDK에서 SilverlightControls 폴더를 참고합니다.

개발자들이 공개하는 커스텀 컨트롤:
http://blogs.msdn.com/devdave/archive/2007/05/17/silverlight-1-1-alpha-layout-system-and-controls-framework.aspx (Grid, StackPanel 등의 컨테이너 샘플)
신고
Posted by gongdo


티스토리 툴바