Customized Control 和 User Control
相信大家比較熟悉使用Silverlight的User Control,在VS2008的Silverlight外掛程式中,可以通過添加新項(Add new Item)建立一個User Control,而且Silverlight在網頁中嵌入的本身就是一個User Control。
本文中講的是如何開發Customized Control,Customized Control是與User Control完全不同的,Customized Control是繼承或者間接繼承System.Windows.Controls.Control的,而User Control必須繼承System.Windows.Controls.Control.UserControl。Customized Control更接近於Silverlight本身提供的Button、CheckBox等Control, User Control則比較類似這些原生Control的組合體。
準備工作
在閱讀本文之前至少應該掌握:
建立和使用簡單Silverlight程式
使用和編輯Silverlight控制項範本
使用Silverlight的Visual State
C#基本知識
使用Visual studio的項目和解決方案
推薦連結
http://msdn.microsoft.com/zh-cn/magazine/cc721611.aspx
一篇for SL2 beta 2的自訂控制項開發介紹,有些地方需要變動才能正常工作
http://silverlight.net/GetStarted/
官方的Silverlight入門, 其中有SL群組成員blog的連結
http://msdn.microsoft.com/en-us/library/cc278068(VS.95).aspx
MSDN的Control Customization介紹
http://msdn.microsoft.com/en-us/library/cc838194(VS.95).aspx
MSDN的Silverlight類庫參考
環境
Visual Studio 2008 with Service Pack 1
Silverlight Tools for Visual Studio 2008 SP1
[optional]Expression Blend 2 with Service Pack 1
(提示:串連給出的預設是英文,大家可以根據習慣選擇。開發工具和Service Pack語言必須對應。)
(注意:Visual Studio和Expression Blend不是免費工具,這裡給出的是試用版本連結,用於商業開發可能存在風險。)
接下來我們可以開始Customized Control之旅了。
(因為不想在基礎的地方浪費時間,所以前面部分會非常簡略,請參考推薦連結)
一、建立解決方案
Customized Control一般會編寫成DLL檔案,所以建立工程的時候應該選擇Silverlight Class Library,同時為了調試,我們還要在解決方案中再建立一個新的Silverlight Application project。如果希望用Web網站來調試Silverlight Application,就還需要建立解決方案中的第三個project,推薦使用動態產生的HTML。然後需要在解決放案屬性中定義好項目的依賴關係以及start up project。考慮到這部分大家都比較瞭解,就不再贅述。
二、建立Control的C#類
建立的Silverlight Class Library中預設會有一個Class1.cs,這是一個普通的C#類,與Silverlight並無關係,可以選擇保留它利用VS的重構功能換成喜歡的名字,也可以刪掉它再重建立立一個類。總之我們的Class Library中只需要保留一個我們要開發的控制項名字的類就可以了,比如我打算開發一個MenuItem。然後我們要繼承Control類,當然也可以繼承任何其它繼承了Control的類。最後代碼看起來像是這樣:
publicclassMenuItem:Control
{
publicMenuItem()
{
}
}
OK,這樣我們就製作出了一個最基本的Customized Control。
三、調用Control
接下來我們希望在Silverlight Application中調用產生的Customized Control,首先要添加引用,因為現在在同一Solution中,所以可以選擇項目引用。
然後我們要把我們類庫的clr命名空間加入到XAML的命名空間當中,所以修改Page.xaml的UserControl為:
<UserControlx:Class="SilverlightMenuTest.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:SilverlightMenu;assembly=SilverlightMenu"
Width="400"Height="300"/>
clr-namespace寫我們類庫中使用的namespace,assembly寫類庫項目的名稱就可以了,XML命名空間可以是我們喜歡的任何名字,這裡用了custom。
然後我們可以在XAML中使用我們的新控制項了,控制項名稱和C#類名相同,前面要帶上命名空間,代碼最後類似這樣:
<UserControlx:Class="SilverlightMenuTest.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:SilverlightMenu;assembly=SilverlightMenu"
Width="400"Height="300">
<Gridx:Name="LayoutRoot"Background="White">
<custom:MenuItem/>
</Grid>
</UserControl>
接下來我們可以運行程式了,但是當然結果是一片空白。
四、添加模板
這個一片空白的Control顯然沒有任何意義,接下來我們要讓它變成可見的。只需要設定好MenuItem的Template屬性就可以了,可以看到這裡設定了Width和Height,Template中的元素會自動把自己限制在這個範圍內。
<UserControlx:Class="SilverlightMenuTest.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:SilverlightMenu;assembly=SilverlightMenu"
Width="400"Height="300">
<Gridx:Name="LayoutRoot"Background="White"Width="100"Height="24">
<custom:MenuItem>
<custom:MenuItem.Template>
<ControlTemplate>
<RectangleFill="Black"StrokeThickness="1"RadiusX="2"RadiusY="2"x:Name="Bg"/>
</ControlTemplate>
</custom:MenuItem.Template>
</custom:MenuItem>
</Grid>
</UserControl>
顯示這段代碼我們就可以看到一個黑色的圓角矩形了。
五、綁定屬性
這樣我們有了一個可見的控制項,但是這個控制項的任何屬性(如Background之類的)都沒有辦法發生作用。現在我們可以想想辦法讓矩形的顏色是控制項的背景色。使用Silverlight的屬性文法,可以將矩形的Fill屬性綁定到MenuItem控制項的Background。
<UserControlx:Class="SilverlightMenuTest.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:SilverlightMenu;assembly=SilverlightMenu"
Width="400"Height="300">
<Gridx:Name="LayoutRoot"Background="White"Width="100"Height="24">
<custom:MenuItemBackground="Blue">
<custom:MenuItem.Template>
<ControlTemplate>
<RectangleFill="{TemplateBindingBackground}"StrokeThickness="1"RadiusX="2"RadiusY="2"x:Name="Bg"/>
</ControlTemplate>
</custom:MenuItem.Template>
</custom:MenuItem>
</Grid>
</UserControl>
感興趣的話,可以試著把Rectangle的Stroke綁定到MenuItem的BorderBrush。這樣,我們就可以通過控制項的屬性控制控制項的顯示效果了。
六、添加預設範本
當然你不能要求使用者總是編輯你的Control的模板,所以我們必須要有一個預設的模板。VisualStudio總是會從項目的themes檔案夾下尋找generic.xaml作為預設範本,注意這個項目是控制項的類所在的項目,也就是我們建立的Silverlight Class Library。
如果你看過之前的介紹Silverlight自訂控制項開發的文章,你會發現,這個地方Silverlight 2 RTW版和Silverlight 2 beta 2或者beta 1是不相容的,我們必須將generic.xaml放到themes目錄下。beta 2版本是放在根目錄下,而beta 1版本是放在根目錄下且必須修改generic.xaml的屬性才會生效。
一個空的generic.xaml的內容如下(但是VS不會自動為你產生這個檔案,需要手動添加):
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
</ResourceDictionary>
我們需要通過style把新控制項的模板放進去,但之前應該先把clr命名空間加入。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:SilverlightMenu;assembly=SilverlightMenu">
</ResourceDictionary>
最後的XAML類似這樣(style大家並不陌生,就不多講了)
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:SilverlightMenu;assembly=SilverlightMenu">
<StyleTargetType="custom:MenuItem">
<SetterProperty="Template">
<Setter.Value>
<ControlTemplateTargetType="custom:MenuItem">
<RectangleFill="{TemplateBindingBackground}"StrokeThickness="1"RadiusX="2"RadiusY="2"x:Name="Bg"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
但僅僅如此控制項是不會有任何變化的,要在MenuItem的建構函式中加入一句:
this.DefaultStyleKey=typeof(MenuItem);
這樣我們的Control就更接近原生Control了,可以這樣調用:
<UserControlx:Class="SilverlightMenuTest.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:SilverlightMenu;assembly=SilverlightMenu"
Width="400"Height="300">
<Gridx:Name="LayoutRoot"Background="White"Width="100"Height="24">
<custom:MenuItemBackground="Black"></custom:MenuItem>
</Grid>
</UserControl>
再運行就可以看到黑色矩形了。
七、綁定事件
到此為止,我們的Control還只能是顯示出來看看,沒有任何互動性,下面我們就來給它添加一定的互動性。因為Control本身提供了一些事件,所以我們很容易捕獲這些事件並作出響應。對於Control來說,我們只需要添加事件處理函數並且綁定到相應事件就可以了。
namespaceSystem.Windows.Controls
{
publicclassMenuItem:Control
{
publicMenuItem()
{
this.DefaultStyleKey=typeof(MenuItem);
this.MouseEnter+=newMouseEventHandler(OnMouseEnter);
}
publicvoidOnMouseEnter(objectsender,MouseEventArgsargs)
{
}
}
}
可以在OnMouseEnter中設定斷點來觀察事件響應情況。
八、操作模板成員
現在我們知道了如何通過事件處理函數響應事件,我們可以在其中改變控制項自身的狀態,比如,當滑鼠划過時,改變控制項的背景。前面說到背景顯示是通過一個Rectangle實現的,那麼要改變背景就需要取到這個Rectangle,我們要使用一個函數GetTemplateChild來取這個元素。注意因為GetTemplateChild並不一定總能取到元素,而且元素類型並不一定是我們期望的(瞭解Template的朋友會知道,Template是一個可以被EndUser修改的屬性,所以不能以任何形式斷言控制項Template的結構和其中的元素名),所以一定要處理異常。下面的代碼是讓滑鼠在控制項上時背景變成紅色:
public class MenuItem : Control
…{
public MenuItem()
…{
this.DefaultStyleKey = typeof(MenuItem);
this.MouseEnter += new MouseEventHandler(OnMouseEnter);
this.MouseLeave += new MouseEventHandler(OnMouseLeave);
}
public void OnMouseEnter(object sender, MouseEventArgs args)
…{
try
…{
Rectangle BgRect = GetTemplateChild("Bg") as Rectangle;
BgRect.Fill = new SolidColorBrush(Color.FromArgb(255, 255, 0, 0));
}
catch
…{
}
}
public void OnMouseLeave(object sender, MouseEventArgs args)
…{
try
…{
Rectangle BgRect = GetTemplateChild("Bg") as Rectangle;
BgRect.Fill = new SolidColorBrush(Color.FromArgb(255, 0, 0, 0));
}
catch
…{
}
}
}有些時候,我們希望在模板生效的時候就對某些模板成員進行操作,如綁定事件,調整屬性等,就需要一個事件OnApplyTemplate,我們只能通過override父類的OnApplyTemplate來響應模板生效事件:
九、添加可視狀態(VisualState)
上面的代碼實現了我們想要的功能,然而卻有很多問題:
顏色是寫入程式碼
無法保留設定的背景色(只能是黑色)
顯示樣式用C#代碼書寫,不符合XAML+C#介面和邏輯分開的精神
那麼如何解決呢?很顯然,跟使用Silverlight開發Web應用一樣,答案就是使用VisualState。首先我們要為我們的控制項範本定義VisualState,因為VisualState必須定義在root element上,所以這裡我稍微做了一點改動,給模板加了一個容器。關於如何定義和使用VisualState,早有不少高手寫的很清楚,這裡就不再重複了。下面的Visual定義了2個互斥的狀態MouseOver和MouseOut,MouseOver狀態將背景Rectangle顏色變紅,MouseOut則恢複原狀。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
xmlns:custom="clr-namespace:System.Windows.Controls;assembly=SilverlightMenu">
<Style TargetType="custom:MenuItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="custom:MenuItem">
<Grid>
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="MouseOverStates">
<vsm:VisualState x:Name="MouseOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="Bg" Storyboard.TargetProperty="Fill" Duration="0:0:0">
<ObjectAnimationUsingKeyFrames.KeyFrames>
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush Color="#FFFF0000"></SolidColorBrush>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames.KeyFrames>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="MouseOut">
<Storyboard>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Rectangle Fill="{TemplateBinding Background}" StrokeThickness="1" RadiusX="2" RadiusY="2" x:Name="Bg"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
定義好了Visual State,我們的事件就可以寫的很簡單了:
public class MenuItem : Control
{
public MenuItem()
{
this.DefaultStyleKey = typeof(MenuItem);
this.MouseEnter += new MouseEventHandler(OnMouseEnter);
this.MouseLeave += new MouseEventHandler(OnMouseLeave);
}
public void OnMouseEnter(object sender, MouseEventArgs args)
{
VisualStateManager.GoToState(this, "MouseOver", false);
}
public void OnMouseLeave(object sender, MouseEventArgs args)
{
VisualStateManager.GoToState(this, "MouseOut", false);
}
}這樣運行下試試看,是不是很cool?
十、添加自訂屬性
接下來到了最後的部分了,也是最少資料涉及的部分,Silverlight Customized Control自訂屬性。下面我們來以一個Text屬性為例示範如何建立一個自訂屬性,這個例子將會用到一些前面的內容。首先我們要為DependencyObject的屬性聲明一個公有的DependencyProperty,這個屬性是靜態,可以用於資料繫結。(還記得資料繫結中使用的DependencyProperty吧,就是這樣的哦^^)
public static readonly DependencyProperty TextProperty;
靜態屬性在靜態建構函式中初始化,這裡有一點複雜,首先看完整代碼:
static MenuItem()
{
TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MenuItem), new PropertyMetadata(false, new PropertyChangedCallback(MenuItem.OnTextPropertyChanged)));
}
DependencyProperty應該由DependencyProperty.Register建立,在MSDN中,這個函數的原型如下:
public static DependencyProperty Register(
string name,
Type propertyType,
Type ownerType,
PropertyMetadata typeMetadata
)
其中name是屬性在XAML中使用的名字,propertyType則是屬性的類型,注意這個屬性理論上可以是任何類型,但是一般只使用整數、布爾、字串、還有UI元素這些類型,其它類型需要定義複雜的字串到對象的屬性文法作為轉換規則(之後可能會單寫一篇blog講converter)。ownerType就是我們自己定義的類了。typeMetadata有點複雜,PropertyMetadata建構函式有3個重載,提供選擇指定defaultValue和propertyChangedCallback中任意一個或者同時指定2個。原型如下:
public PropertyMetadata(
Object defaultValue,
PropertyChangedCallback propertyChangedCallback
)
defaultValue是屬性的預設值沒什麼可說,propertyChangedCallback也很簡單,就是當屬性改變時的處理函數,這個是把屬性同我們的類聯絡起來的關鍵了,PropertyChangedCallback 是個委託類型,它的簽名也可以在MSDN查到:
public delegate void PropertyChangedCallback(
DependencyObject d,
DependencyPropertyChangedEventArgs e
)
為了理解這個函數,我們可以先看一下我對Text屬性的實現:
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as MenuItem).OnTextPropertyChanged(e);
}
void OnTextPropertyChanged(DependencyPropertyChangedEventArgs e)
{
try
{
(GetTemplateChild("TB") as TextBlock).Text = e.NewValue as string;
}
catch
{
}
}
---摘自chongqingit網