본문 바로가기

.Net Technology/WPF

(4) XAML 기반의 WPF 애플리케이션 만들기

XAML 기반의 WPF 애플리케이션 만들기

XAML(Extensible Application Markup Language)는 닷넷 객체들의 트리를 마크업으로 정의하는 XML 기반의 문법이다. WPF의 UI를 만드는데 있어서 XAML은 굉장히 많은 비중을 차지하고 있고 실제로 추상적이지 않은 닷넷 객체들을 정의할 수 있다. 앞으로 살펴볼 것이지만 *.xaml 파일에 정의된 XAML은 닷넷 네임스페이스에 선언된 타입과 직접 연결이 되는 타입으로 변환된다. 

XAML은 XML기반의 문법이기 때문에 XML의 장점과 단점을 모두 가지게 된다. 장점에 대해서 살펴 보자면 XAML은 자기 기술적인(self-describing) 특징을 가지고 있다. XAML 안의 각각의 개체들은 대체로 닷넷 네임스페이스에서 주어진 이름을 그대로 표현하고 있다. (Button, Window, Application등) 그리고 XML의 애트리뷰트는 요소의 속성(Height, Width 등)이나 이벤트(Startup, Click 등)를 지정하게 된다. 

XAML을 이용해서 간단하게 객체를 정의하고 속성을 정의할 수 있고 절차적으로 표현하게 된다.예를 들어 다음 코드를 살펴보도록 하자. 

<!?XAML로 WPF 버튼의 정의 -->
<Button Name = "btnClickMe" Height = "40" Width = "100" Content = "Click Me" />


이 코드는 C#에서 다음과 같이 정의할 수 있다. 

// C#코드로 같은 버튼을 정의
Button btnClickMe = new Button();
btnClickMe.Height = 40;
btnClickMe.Width = 100;
btnClickMe.Content = "Click Me";



그럼 이번에는 XAML의 단점에 대해서 살펴보도록 하자. 먼저 XAML의 내용이 굉장히 커질 수 있고 또한 상당히 예민하기 때문에 상당히 복잡하고 어렵게 느껴질 수 있다. 하지만 대부분의 개발자들은 직접 손으로 XAML을 작성하지는 않을 것이다. 오히려 비주얼 스튜디오나 익스프레션 블렌드라는 툴을 이용해서 XAML을 작성하게 될 것이고 필요에 따라서만 XAML을 직접 손으로 수정할 수 있을 것이다. 

XAML을 다루는 툴이 지원되고 있다 하더라도 우리는 XAML문법은 반드시 알고 있어야 한다. XAML의 문법에 대해서는 예제를 통해서 살펴볼 것이고 *.xaml 파일만 이용해서 애플리케이션을 만들어 보도록 하겠다. 


XAML에서 MainWinodw의 정의 

앞에서는 C#코드로 System.Windows.Window를 상속받아서 MainWindow 클래스를 만들었다. 이 클래스는 하나의 버튼을 가지고 있고 버튼이 클릭되었을 때 이벤트를 발생시켰다. 이와 같은 창을 XAML 문법을 이용해서 만들어 보도록 하겠다. (MainWindow.xaml 파일 에 다음과 같이 정의해 보자)

<!--Window의 정의 -->
<Window x:Class="SimpleXamlApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="My Xaml App" Height="200" Width="300"
WindowStartupLocation ="CenterScreen">

<!--버튼의 설정-->
<Button Width="133" Height="24" Name="btnExitApp" Click ="btnExitApp_Clicked">
    Exit Application
</Button>

<!-- 버튼의 Click이벤트의 구현 -->
<x:Code>
  <![CDATA[
  private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
  {
  // 애플리케이션의 종료
  Application.Current.Shutdown();
  }
  ]]>
</x:Code>
</Window>


 
먼저 루트 엘리먼트인 <Window>를 살펴보면 Class라는 애트리뷰트를 지정한 것을 볼 수 있을 것이다. 여기서 x라는 접두사는 앞에서 정의한 http://schemas.microsoft.com/winfx/2006/xaml 네임스페이스를 나타낸다. (네임스페이스에 대해서는 뒷부분에서 다룰 예정이다.) 그리고 Window 개체에 Title, Height, Width, WindowsStartupLocation 애트리뷰트들을 추가해서 값을 지정해주었다. 이렇게 System.Windows.Window 클래스의 속성들을 같은 이름으로 제공해주고 있기 때문에 직접 설정하는 것이 가능하다. 

다음으로 Window 안에 여러 엘리먼트들을 추가할 수 있고 우리는 여기서 Button이라는 개체를 추가하였다. 그리고 Content 속성을 묵시적으로 선언하였다. 변수 이름을 Name이라는 애트리뷰터에 설정하였고 여러 속성들을 지정해 주었다. 그리고 버튼에 Click 이벤트를 지정해 주었고 클릭 이벤트가 발생하면 지정한 메서드가 실행하게 된다. 

마지막으로 XAML은 *.xaml 파일에 직접 이벤트나 다른 메서드들을 넣을 수 있게 지원하고 있기 때문에 <Code>라는 엘리먼트를 지정한 것을 볼 수 있을 것이다. XML 문법을 안전하게 보호하기 위해서 CDATA를 이용하여 코드를 작성하였다. 

여기서 중요한 부분은 <Code> 객체를 이용하는 것은 추천하지 않는다. 왜냐하면 비록 단 하나의 파일로 처리할 수 있다 하더라도 UI와 프로그램 로직이 분리되지 않고 하나로 존재하기 때문이다. 대부분의 WPF 애플리케이션은 C#코드와 상호 동작하게 된다는 것을 알아두자.


XAML안에 Application 객체 정의하기

XAML은 기본 생성자를 가지고 있는 어떠한 닷넷 클래스도 정의할 수 있다고 설명했었다. 그럼 MyApp.xaml 라는 새로운 파일에 다음과 같은 코드를 작성해 보도록 하자. 

<!-- Main() 메서드와 같은 기능을 StartupUri 애트리뷰트로 대처할 수 있다 -->
<Application x:Class="SimpleXamlApp.MyApp"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
</Application>



C#에서 Application을 상속받아 구현한 클래스와 이 XAML 코드는 MainWindow을 정의한 XAML 만큼 완전히 일치하지는 않는다. 다른 부분은 바로 Main() 메서드가 없다는 것이다. 닷넷 프로그램은 반드시 프로그램의 진입 점을 가지고 있어야 한다. 하지만 XAML에서는 StartupUri 속성을 통하여 처음 시작될 UI를 지정하게 된다. 즉, 애플리케이션이 시작될 때 지정된 *.xaml 파일이 화면에 보여지게 될 것이다. 
비록 Main() 메서드가 컴파일 시 자동으로 만들어 진다고 하더라도 <Code> 엘리멘트를 이용해서 Exit 이벤트를 작성할 수 있다. 

<Application x:Class="SimpleXamlApp.MyApp"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml" Exit ="AppExit">
<x:Code>
<![CDATA[
private void AppExit(object sender, ExitEventArgs e)
{
MessageBox.Show("App has exited");
}
]]>
</x:Code>
</Application>




msbuild.exe를 이용한 XAML 파일의 처리 

여기서 중요한 부분은 닷넷 어셈블리가 이해할 수 있게 변형해 주어야 한다는 것이다. 우리가 선언한 XAML은 C# 컴파일러를 이용해서 만들 수 없다. 왜냐하면 C# 컴파일러는 XAML을 이해할 수 없기 때문이다. 하지만 명령 프롬포트로 실행할 수 있는 msbuild.exe라는 도구는 올바른 파일 명을 지정해 주었을 경우 XAML을 C#코드로 변경준다. 

msbuild.exe 툴은 XML 기반의 복잡한 빌드 스크립트를 정의할 수 있게 해준다. 비주얼 스튜디오의 *.csproj 파일처럼 XML로 정의하여 사용할 수 있다는 것이다. 커맨드 라인에 이 파일 하나만 지정해주면 알아서 빌드를 진행하게 된다. 먼저 SimpleXamlApp.csproj라는 파일이 다음과 같이 선언되어 있다고 가정해 보도록 하자. 

<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <RootNamespace>SimpleXamlApp</RootNamespace>
    <AssemblyName>SimpleXamlApp</AssemblyName>
    <OutputType>winexe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>
  <ItemGroup>
    <ApplicationDefinition Include="MyApp.xaml" />
    <Page Include="MainWindow.xaml" />
  </ItemGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
  <Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" />
</Project>



여기서 <PropertyGroup> 엘리먼트는 빌드를 위해서 기본적으로 지정해야 하는 요소이고, 루트 네임스페이스, 어셈블리의 이름과 같은 클래스들을 지정하게 된다. 

첫 번째로 지정되어 있는 <ItemGroup>에는 전역 어셈블리들을 지정하고 있고 앞에서 설명한 WPF의 기본 어셈블리들이 정의되어 있는 것을 볼 수 있다. 두 번째 <ItemGroup> 엘리먼트는 굉장히 흥미로운 부분이다. <ApplicationDefinition> 엘리먼트의 Include 애트리뷰트에서는 Application 객체인 MyApp*.xaml을 할당해주었다. 그리고 <Page> 엘리먼트에서는 Window 클래스로 정의된 *xaml과 같은 파일들을 추가할 수 있다. 

하지만 *. csproj에서 가장 흥미로운 부분은 마지막에 선언된 <Import> 라는 엘리먼트이다. 여기서 빌드 과정 동안에 각각 여러 가지 명령들을 포함하고 있는 *.targets라는 두 개의 파일을 참조하고 있다. Microsoft.WinFX.targets 파일은 XAML을 C#코드로 만들기 위해서 필요한 빌드 셋팅들을 가지고 있고, Microsoft.CSharp.targets 파일은 C# 컴파일러와 상호 운용하기 위한 데이터들을 가지고 있다. 

Msbuild.exe에 대해서 더 자세히 살펴보고 싶다면 닷넷 프레임워크 3.5 SDK 문서에서 “MSBuild” 라는 토픽을 검색해보면 더 많이 살펴 볼 수 있을 것이다.



그럼 여기서 msbuild.exe 툴을 이용해서 SimpleXamlApp.csproj 파일을 실행시켜 보도록 하자. 

msbuild SimpleXamlApp.csproj

빌드가 완성되면 \bin\Debug 폴더에 어셈블리들이 있는 것을 볼 수 있을 것이고 기대했던 대로 WPF 애플리케이션을 실행할 수 있을 것이다. 여기서 닷넷 어셈블리로 잘 변환되었는지 확인하고 싶다면 ildasm.exe를 이용해서 SimpleXamlApp.exe 열어본다면 다음과 같이 XAML이 실행 가능한 애플리케이션으로 변환되어 보여지는 것을 볼 수 있을 것이다. 

 
[그림6] 닷넷 어셈블리로 변환된 마크업


마크업을 닷넷 어셈블리로의 변경하기

마크업을 닷넷 어셈블리로 어떻게 변경하는지 확실히 이해하기 위해서 msbuild.exe의 동작을 좀 더 깊게 이해할 필요가 있고 또한 컴파일러에 의해서 동작되는 다양한 바이너리들을 살펴 보아야 한다.

XAML과 C#코드의 매핑

앞에서 언급했듯이 MSBuild 스크립트 안에 정의된 *.targets 파일은 XAML요소를 C#코드로 변경하기 위한 다수의 명령들을 정의하고 있다. 앞에서 *csproj 파일을 msbuild.exe로 처리하게 되면 두 개의 *.g.cs 파일을 \obj\Debug 디렉토리에 생성하게 된다. 즉, *.xaml 파일의 이름을 기반으로한 MainWindow.g.cs와 MyApp.g.cs와 같은 파일이 생성되는 것이다.

만약 MainWindow.g.cs 파일을 열어본다면 Window클래스를 확장한 클래스와 btnExitApp_Clicked() 메서드가 선언되어 있는 것을 볼 수 있을 것이다. 또한 이 클래스는 System.Windows.Controls.Button 클래스가 멤버 변수로 선언되어 있는 것을 볼 수 있을 것이다. 하지만 이상하게도 Button이나 Window에 지정했던 Height, Width, Title과 같은 속성을 설정하는 코드는 찾아 볼 수 없을 것이다. 이 부분에 대한 내용은 잠시 후에 살펴보도록 하겠다. 

마지막으로 이 클래스 파일에는 _contentLoaded라는 bool 타입의 private 멤버 변수를 정의하고 있다. 하지만 이 변수는 XAML 마크업 안에 직접 선언하지 않았던 변수이다. 다음 코드는 생성된 MainWindow 클래스의 코드를 보여주고 있다. 

public partial class MainWindow :
System.Windows.Window, System.Windows.Markup.IComponentConnector
{
  internal System.Windows.Controls.Button btnExitApp;
  // 이 멤버 변수에 대해서는 곧 설명할 것이다. 
  private bool _contentLoaded;

  private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
  {
    //애플리케이션의 종료
    Application.Current.Shutdown();
  }
  ...
}



Window를 상속받은 이 클래스는 System.Windows.Markup네임스페이스에 정의되어 있는 IComponentConnector 인터페이스를 구현하고 있다. 이 인터페이스는 Connect()라는 메서드를 정의하고 있고 이 메서드에서는 MainWindow.xaml 파일에 정의했던 이벤트 로직을 구현하고 있다.

void System.Windows.Markup.IComponentConnector.Connect(int connectionId,
object target)
{
  switch (connectionId)
  {
    case 1:
      this.btnExitApp = ((System.Windows.Controls.Button)(target));
      this.btnExitApp.Click += new
      System.Windows.RoutedEventHandler(this.btnExitApp_Clicked);
      return;
  }
  this._contentLoaded = true;
}



마지막으로 MainWindow 클래스는 InitializeComponent()라는 메서드를 구현하고 있다. 이 메서드는 어셈블리에 포함되어 있는 *.xaml 파일 리소스의 위치를 분석한다. 그렇게 리소스의 위치를 할당한 후에 Aplication 객체의 Application.LoadComponent()를 호출해서 각각의 객체들을 로드시킨다. 그리고 마지막으로 앞에서 언급했던 _contentLoaded 변수에 true를 할당해주면서 애플리케이션의 생명주기 동안에 확실히 로드 되었는지에 대한 요청을 확실히 처리할 수 있게 해준다.

public void InitializeComponent() {
  if (_contentLoaded) {
    return;
  }
  _contentLoaded = true;
  System.Uri resourceLocater = new
  System.Uri("/SimpleXamlApp;component/mainwindow.xaml",
  System.UriKind.RelativeOrAbsolute);
  System.Windows.Application.LoadComponent(this, resourceLocater);
}


그렇다면 어떤 리소스들이 로드 되었는지 궁금할 수 있을 것이다. 


BAML의 역할

msbuild.exe가 *.csproj 파일을 처리할 때 MainWindow.xaml파일의 이름을 기반으로 해서 *.baml이란 파일을 추가적으로 생성하게 된다. 이미 이름에서 추측할 수 있을 것이다. BAML은 Binary Application Markup Language로서 XAML을 바이너리로 표현한 언어를 뜻한다. *.baml 파일은 컴파일 된 어셈블리에 리소스로 추가되어있다. 이것은 리플렉터를 이용해서 확인해 볼 수 있다. 다음 [그림7]은 리플렉터로 어셈블리를 열어본 화면을 보여주고 있다.
 

[그림7] Lutz Roeder’s Reflector로 확인해본 *baml 리소스


Application.LoadComponent()를 호출하면 BAML 리소스를 읽어 온 후에 정의된 객체들을 트리로 구성한 후에 Width, Height와 같은 속성들을 설정하게 된다. 실제로 *.baml이나 *.g.resources파일을 비주얼 스튜디오로 열어보면 XAML 애트리뷰트들의 초기 값들을 조사해볼 수 있을 것이다. 다음 [그림8]은 StartupLocation과 CenterScreen 속성에 대한 설정을 보여주고 있다. 

 
[그림]8] BAML의 내부


MyApp.g.cs 파일에 자동으로 생성된 코드에는 몇 가지 의문이 남아 있다. Application을 상속 받은 클래스에 정의된 Main()에 대한 메서드를 살펴보자. Main()에서는 InitializeComponent() 메서드를 호출한다. 그리고 이 메서드에서는 StartupUri 속성을 설정하고 각각 객체들의 속서들을 설정하게 된다. 

namespace SimpleXamlApp
{
  public partial class MyApp : System.Windows.Application
  {
     void AppExit(object sender, ExitEventArgs e)
    {
       MessageBox.Show("App has exited");
    }

    [System.Diagnostics.DebuggerNonUserCodeAttribute()]
    public void InitializeComponent() {
      this.Exit += new System.Windows.ExitEventHandler(this.AppExit);
      this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);
    }

    [System.STAThreadAttribute()]
    [System.Diagnostics.DebuggerNonUserCodeAttribute()]
    public static void Main() {
      SimpleXamlApp.MyApp app = new SimpleXamlApp.MyApp();
      app.InitializeComponent();
      app.Run();
    }
  }
}


XAML이 어셈블리가 되는 과정의 정리

우리는 XML 문서를 msbuild.exe 프로그램을 이용해서 빌드하였고 그 결과 닷넷 어셈블리를 만들 수 있었다. 이미 본 것처럼 msbuild.exe는 *.targets 파일에 설정에 따라서 XAML 파일을 처리하게 된다. 다음 [그림9]는 지금까지 살펴보았던 빌드 과정을 그림으로 보여주고 있다. 
 

[그림8] XAML을 어셈블리로 컴파일되는 과정

이 컴파일 과정에 있어서 또한 중요한 부분 중에 하나는 *.xaml 파일을 모두 C#코드와 바이너리 리소스로 변경한다는 것이다. 하지만 뒷부분에서 살펴 볼 내용이지만 *.xaml 파일을 프로그램에서 읽어서 동적으로 Window객체를 생성하는 것도 가능하다. 이 경우에는 *.xaml 파일이 실제로 접근이 가능한 물리적인 경로에 위치하고 있어야 한다. 


코드 비하인드 파일을 이용한 로직의 구분

XAML에 대해서 살펴보기 전에 기본이 되는 프로그램 모델에 대해서 살펴보도록 하겠다. 지금까지의 예제는 그렇게 작성하지 않았지만 WPF은 UI와 프로그램 로직이 구분되어 개발하게 된다. 

오히려 이벤트 핸들러를 XAML안에 직접 선언하는 것 보다는 C# 코드에서 그러한 로직을 선언하는 것을 더 선호하게 된다. 즉, *xaml에는 어떠한 프로그래밍 로직을 선언하지 남겨두지 않고 UI와 관련된 마크업 언어만 남겨두는 것이다. 코드 비하인드 파일은 MainWindow.xaml.cs에 작성된다. 

// MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace SimpleXamlApp
{
  public partial class MainWindow : Window
  {
     public MainWindow()
    {
      // 이 메서드는 MainWindow.g.cs 파일에 구현되어있다.
      InitializeComponent();
    }
     private void btnExitApp_Clicked(object sender, RoutedEventArgs e)
    {
      // 현재 애플리케이션의 종료
      Application.Current.Shutdown();
    }
  }
}

여기서 우리는 Partial 클래스를 이용해서 *.g.cs 파일을 정의하게 되고 UI를 다루는 로직을 또한 구분하게 된다. 그렇기 때문에 InitializeComponent() 메서드는 MainWindow.g.cs 파일에 정의된다. 그리고 Window의 생성자에서 이 함수를 호출하고 BAML 리소스를 처리하게 된다. 

앞에서 살펴보았던 예제에 코드 비하인드를 생성할 수 있다. 대부분의 동작이 MyApp.g.cs 파일에서 발생하기 때문에 MyApp.xaml.cs 에서는 약간의 코드만 추가하면 된다. 

// MyApp.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace SimpleXamlApp
{
  public partial class MyApp : Application
  {
    private void AppExit(object sender, ExitEventArgs e)
    {
      MessageBox.Show("App has exited");
    }
  }
}


msbuild.exe를 이용해서 재 컴파일 하기 전에 *.csproj 파일에 새로운 C# 파일을 추가해 주어야 한다. 파일을 열고 <Compile> 엘리먼트에 다음과 같이 파일을 첨부해준다. 

<Project DefaultTargets="Build" xmlns=
"http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <RootNamespace>SimpleXamlApp</RootNamespace>
    <AssemblyName>SimpleXamlApp</AssemblyName>
    <OutputType>winexe</OutputType>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>
  <ItemGroup>
    <ApplicationDefinition Include="MyApp.xaml" />
    <Compile Include = "MainWindow.xaml.cs" />
    <Compile Include = "MyApp.xaml.cs" />
    <Page Include="MainWindow.xaml" />
  </ItemGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
  <Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets" />
</Project>



msbuild.exe 스크립트를 실행하면 동일한 어셈블리가 생성되는 것을 볼 수 있을 것이다. 하지만 개발적인 관점에서 보면 프로그래밍 로직(C#)과 프레젠테이션 영역의 구분은 굉장히 중요하다. 다행히도 비주얼 스튜디오에서 프로젝트를 생성하면 이 로직을 항상 구분해 준다.