再談XAML與WPF

撰文:吳俊毅
時間:2009/6/11

在筆者先前討論過XBAP應用程式後再一次回到WPF的主題,在這個章節中筆者要說明XAML與WPF之間的關係,讀者先往下看幾個主題。

1. 再談XAML

什麼是XAML呢,XAML是(eXtensible Application Markup Language)的簡稱,XAML發音為”zammel”,而XAML為一種標籤語言,關於這個部分市面上許多書籍都已經講過太多了筆者快速的帶過就好,簡單的說XAML承襲XML的Well-Format文件的所有特性,在這裡微軟透過XAML描述並呈現WPF應用程式的外觀,它可以設置.cs的後置檔,程式的事件與運算邏輯可由後置檔案完成,WPF可在<Window>標籤內的x:Class=”命名空間.類別名稱”
如下:

<Window x:Class="WpfHelloWorldApp.Window1">
</Window>


當然XAML檔案不一定要有後置檔,如上程式若去除掉x:Class=”WpfHelloWorldApp.Window1”
的敘述即表示沒有後置檔案,而通常沒有後置檔的XAML程式碼通常稱為loose XAML(鬆散的XAML),這種檔案通常只是將XAML當做文件來使用,如XPS Document或WF的Flow Document文檔等,而XAML檔案執行時會預先被編譯成BAML(Binary Application Markup
Language)的二進位直接檔案,但它是一個副檔名為.g.cs的檔案,在使用正式版本的Visual Studio IDE工具後您通常無法在檔案總管中察覺這個檔案的存在,若您在InitializeComponent()點選右鍵a”移至定義”所看到的程式內容便是IDE工具(auto-generated)自動產生的BAML內容,它會在obj\Debug 下產生,而同樣是一個繼承Window的類別,只是會多實作IComponentConnector介面,在InitializeComponent() 中會透過System.Windows.Application.LoadComponent()方法將XAML的內容讀取進來,筆者擷取一段BAML範例內容如下:

public partial class Window1 : System.Windows.Window,
System.Windows.Markup.IComponentConnector {
#line 6 "..\..\Window1.xaml"
internal System.Windows.Controls.Button btnHelloWorld;

#line default
#line hidden
private bool _contentLoaded;

/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public void InitializeComponent() {

if (_contentLoaded) {
return;
}

_contentLoaded = true;

System.Uri resourceLocater = new System.Uri("/WpfHelloWorldApp;component/window1.xaml",
System.UriKind.Relative);

#line 1 "..\..\Window1.xaml"

System.Windows.Application.LoadComponent(this, resourceLocater);


#line default
#line hidden

}
….略

與XML相同的,在XAML當中標籤代表Element(元素) 語法如:

<元素名稱></元素名稱>,每個XAML元素都可對應到CLR物件,也就是說XAML程式碼也都可以改以C#撰寫,不過當然通常不會這麼做,因為簡單的三行XAML程式碼可能要7-8行的C#程式碼才能完成相同的UI畫面,而且通常根(以下統稱Root)標籤<Window></Window>包起來稱為WPF桌面應用程式,而根標籤為<Page></Page>包起來的則是瀏覽器應用程式,如XBAP應用程式,<UserControl></UserControl>則為使用者控制項,如WPF使用者控制項函式庫,Silverlight等都屬於此類型。

2. XAML命名空間

在XAML中使用xmlns來引用預設命名空間,這裡使用的xmlns關鍵字與XML相同,不過在這裡引用的是預設的CLR命名空間,一般寫法如下:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
</Window>

許多人都會有一個疑問就是為什麼是一個網址,其實這是XML 1.0就已經規範的做法,只是在這裡微軟將其預設對應到以下的.NET的命名空間:

System.Windows
System.Windows.Automation
System.Windows.Controls
System.Windows.Controls.Primitves
System.Windows.Data
System.Windows.Documents
System.Windows.Forms.Integration
System.Windows.Ink
System.Windows.Input
System.Windows.Media
System.Windows.Media.Animation
System.Windows.Media.Effects
System.Windows.Media.Imaging
System.Windows.Media.Media3D
System.Windows.Media.TextFormation
System.Windows.Navigation
System.Windows.Shapes

除了xmlns之外還有一個第二命名空間,使用了x:前置詞來表示與CLR物件對應的參考,雖然在XAML中第二命名空間並不是必要的,不過如果您要指定<Button>的Name屬性,因為此屬性是對應到CLR物件的屬性,這時您就必須須告第二命名空間,透過前置詞對應到Button的Name屬性,範例如下:

<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Button x:Name=”Button1”>OK</Button>
</Grid>
</Window>

如果需要在XAML中使用自行開發的Assembly中的物件怎麼辦,這是不存在於預設的WPF命名空間中的,其實可以透過 xmlns:(自訂前置詞)=”clr-namespace:
(該組件完整命名空間);assembly:(組件名稱)”,範例如下:

<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
xmlns:my=”clr-namespace:CustomControls.MyTextBox;assembly:MyTextBox”>
<Grid>
<Button x:Name=”Button1”>OK</Button>
<my:MyTextBox x:Name=”MyTextBox1”></my:MyTextBox>
</Grid>
</Window>





前面提到XAML通常將事件處理程序寫在後置檔中,在XAML也提供一種寫法可將程式碼寫在XAML程式中,只要包在<x:Code></x:Code>標籤中,如下程式也是可以正常的執行:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WpfHelloWorldApp.Window1">
<Grid>
<Button x:Name="Button1" Click="Button1_Click">OK</Button>
<x:Code>
void Button1_Click(Object sender, RoutedEventArgs e)
{
Button1.Content = "Hello World";

}

</x:Code>
</Grid>
</Window>

不過一般來說並不建議如此做,這樣使的程式碼不易管理且不容易偵錯!

3. 預設內容屬性與直接內容屬性
所謂的預設屬性是指XAML物件(或說元素Element)都有其預設的內容屬性,如當您寫<Button>OK</Button>它怎麼會知道要將OK放入Button的Content屬性中呢?這有點像我們以前寫VB6的時候textBox不用指定Text屬性,編譯器會自動將值給textBox的Text屬性,不過在WPF的物件中只有容器類的物件預設內容屬性是Content,一般文字類如TextBlock的物件預設內容屬性是Text屬性。
在XAML指定屬性值也可採明確的直接內容屬性寫法,如:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”>
<Grid>
<Button>
<Button.Content>
OK
</Button.Content>
</Button>
</Grid>
</Window>

此兩種寫法XAML編譯器都可以正確的執行。

4. 設定XAML的資源
以往在撰寫程式時常常需要在程式中使用到一些資源(Resources),如ICON,Bitmap,字型等,在.NET平台通常會使用.resx檔案或是.resource檔案定義資源檔,在XAML中操作使用資源方式有一些改變,XAML使用Resource的TAG(或說屬性)來定義資源,在WFP應用程式中屬於容器類別的物件都可以定義自己的Resource,而可以定義的Resource的種類有Style、Brush、Template、DataSource四種,常見的Brush(筆刷)如SolidColorBrush定義在Grid.Resource中,後由Button的Foreground屬性來取用該Resource,範例如下:

<Grid>
<Grid.Resources>
<SolidColorBrush x:Key="MyBrush" Color="gold"/>
</Grid.Resources>
<Button Height="23"
HorizontalAlignment="Left"
Margin="59.976,60.809,0,0"
Name="button1"
VerticalAlignment="Top"
Width="75"
BorderBrush="Black"
Foreground="{StaticResource MyBrush}">Button</Button>
</Grid>




如範例程式中中讀者會注意到在Foregound旁有一個大括號{}包起來的東西,這是XAML的標記擴充屬性,或稱標記的擴充值,使用大括號包起來的部分編譯器會特別處理,包括資料繫結也是。需要注意的是資源的定義一定要加上x:Key屬性,如同在.resx的資源檔案中需使用資源ID來識別一樣。

還有一點,資源有資源的Scope,在Grid定義的資源是無法被它的上一層所使用的,如果您定義的資源是整個Window都會使用到可以直接定義在Window中,如:

<Window x:Class="InkCavansDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="350.693" Width="486.472">
<Window.Resources>
<SolidColorBrush x:Key="MyBrush" Color="gold"/>
</Window.Background>
<Grid>
<Button Height="23"
HorizontalAlignment="Left"
Margin="59.976,60.809,0,0"
Name="button1"
VerticalAlignment="Top"
Width="75"
BorderBrush="Black"
Foreground="{StaticResource MyBrush}">Button</Button>
</Grid>
</Window>

這樣在Window中的任何物件皆可以使用SolidColorBrush這個資源。談到這裡有些讀者還是會有疑問,說筆者沒說清楚ㄚ,StaticResource這個關鍵字意義是什麼?呵~筆者卻時漏掉了,在XAML的資源類型共有靜態StaticResource與動態DynamicResource這兩種,簡單的說Static的資源必須是編譯時期決定且一旦決定無法修改其內容,Dynamic則可在Runtime時動態修改其內容,所謂的動態當然指的就是可以使用C#修改其指定的資源。

5. 一個簡單的XAML檔案
前面提到XAML命名空間不一定要寫在Root,也可以寫在任何一個XAML元素中,如Button,在loose XAML中只放一個<Button>OK</Button>的敘述也是合法的,可以直接存成.xaml檔案然後直接執行後系統預設會以IE預覽結果,不過若沒有指定xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presen-tation" 執行會回傳System.Windows.Markup.XamlParseException的錯誤!,這是因為編譯器無法找到Button是定義在哪一個命名空間中,如下圖,錯誤畫面:




若加入xmlns的Button的XAML程式碼如下:
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">OK</Button>
IE的預覽結果如下:


如圖中會發現Button佔滿了整個畫面,這是WPF容器物件特性,因為我們沒指定Button的長與寬與容器內要使用何種的排版方式的關係。

6. XAML編譯與執行
XAML必須經過Parse才可編譯成功並執行,若沒有IDE工具要如何編譯XAML檔案呢,在.NET 3.0與3.5中XAML還是透過csc編譯器或是MSBuild來編譯,在C#程式碼中除了透過前面提到的LoadComponent()來讀取XAML內容之外,還可以使用XamlReader來讀取並Parse
XAML檔案,首先我們先建立一個MyWindow.xaml檔案,內容如下:
<Window xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Hello World"
Width="300"
Height="300">
<Grid>
<Button x:Name="btnHelloWorld" Width="100" Height="23">OK</Button>
</Grid>
</Window>

接著在建立一個App.cs,這個類別必須繼承Application類別,並實作一個靜態的Main()方法的應用程式進入點以便啟動應用程式,詳細程式碼如下:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
using System.IO;
using System.Windows.Markup;

namespace WpfHelloWorldApp
{
public partial class App : Application
{
[STAThread]
public static void Main()
{
App app = new App();
Window win1 = null;

using (FileStream fs = new FileStream(@"MyWindow.xaml", FileMode.Open,
FileAccess.Read))
{
win1 = (Window)XamlReader.Load(fs);
}

if (win1 != null)
app.Run(win1);
}
}
}

如上程式讀者可以發現,筆者宣告的Window物件是取用XamlReader讀取出來的Window物件,該Window即是XamlReader將MyWindow.xaml的內容Parse後得到的物件,其內有一個Button物件,是否可以如預期的執行結果呢,我們先編譯看看。

在Visual Studio 2008的命令提示字元輸入如下命令,如下圖(筆者直接貼上包含命令與執行結果的圖示):


成功編譯後目錄下應會產生一個WPFApplication.exe檔案,執行該檔案後應會看見一個300*300的WPF視窗且中間有一個OK的Button,如下圖:


透過以上的介紹相信讀者對於XAML應該更有概念了,下次筆者再介紹更深入的XAML程式設計,下次見!

留言

張貼留言

這個網誌中的熱門文章

軟體架構設計:API 設計準則(二)、API Design-First 原則、策略與開發流程

什麼是 gRPC ?

常見的程式碼壞味道(Code Smell or Bad Smell)