在 Silverlight 中你如果想把 UI 封装成单独的一部分或者创建一个新的页面,你可能会在 Visual Studio 中通过 右击 “ 项目 -> 添加 -> 添加新项 ->Silverlight 用户控件 ” 这样来创建控件。 如果你是这么做的,那么这篇文章非常适合你。它将适用于任何基于XAML 技术: WPF 、 silverlight 、 Windows Phone 和 Windows 8 Runtime 。
尽管用户控件很棒,它们能快速的拼在一起,或一次又一次的重复使用,这是它们的很大一个价值所在。但是如果我告诉你还有另一种控件类型,具有干净的代码、更强大性能更好,而且比用户控件的方式更加灵活、重复的使用,那它将会是大量开发人员的最爱吗 ?
其实这个你早就知道,因为你已经一直在使用他们: Button 、 ListBox 、 ItemsControls 、 Grid 、 StackPanel 等。你可以查看 Xaml Style 彻底改变控件的外观和体验,而不触及任何代码。这是多么强大的想法,看看下面一个 Silverlight ListBox 行星 DEMO 。在左边,你会看到一个绑定了行星名单的 ListBox 。在右边,你能看到一个太阳系,但事实上,这也是一个 ListBox 。这里没有涉及到额外的代码,完全是由修改 Template 达到效果。你可以按上下键,它有正常 ListBox 的功能。
让我重复一遍:做到这一点我没有添加任何后台代码到 ListBox 。事实上,该页面后台代码完全是空的。如果你不相信,这里有源码 下载
解剖用户控件
首先,让我们解剖一个典型的用户控件看看 , 充分了解下它是怎么工作的这是关键。在下面我们控件中一部分 XAML 确定了布局,为了保持它是一个简单的例子,里有只一个 Grid 和一个 Button 。
1
<
UserControl
x:Class
="MyApp.SilverlightControl1"
2
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
>
4
5
<
Grid
x:Name
="LayoutRoot"
Background
="White"
>
6
<
Button
Content
="Click Me"
Click
="Button_Click"
Opacity
=".5"
/>
7
</
Grid
>
8
</
UserControl
>
我们控件的后台代码:
1
using System.Windows;
2
using System.Windows.Controls;
3
using System.Windows.Media;
4
5
namespace SolarSystemRetemplate
6 {
7
public
partial
class SilverlightControl1 : UserControl
8 {
9
public SilverlightControl1()
10 {
11 InitializeComponent();
12 }
13
14
private
void Button_Click(
object sender, RoutedEventArgs e)
15 {
16 LayoutRoot.Background =
new SolidColorBrush(Colors.Red);
17 }
18 }
19 }
这里有两个地方值得注意: ”LayoutRoot”是在 XAML 中使用 X:Name定义的,我们在后台代码中通过这个名字自动获取了这个变量。 而且 Button的 Click事件与后台代码中的事件处理程序奇迹般的挂接了。实际上这是编译程序和调用方法 InitializeComponent处理了这一切 --但是有趣的是这个方法在这里不存在。实际上为了表示这是一个局部类, Visual Studio为你私底下创建了一个小(秘密)文件。你可以右击方法选择“转到定义“。下面是该文件的内容:
1
namespace MyApp {
2
3
public
partial
class SilverlightControl1 : System.Windows.Controls.UserControl {
4
5
internal System.Windows.Controls.Grid LayoutRoot;
6
7
private
bool _contentLoaded;
8
9
///
<summary>
10
///
InitializeComponent
11
///
</summary>
12
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
13
public
void InitializeComponent() {
14
if (_contentLoaded)
15
return ;
16 _contentLoaded =
true ;
17 System.Windows.Application.LoadComponent(
this ,
18
new System.Uri(
"
/MyApp;component/SilverlightControl1.xaml
" ,
19 System.UriKind.Relative));
20
this .LayoutRoot = ((System.Windows.Controls.Grid)(
this .FindName(
"
LayoutRoot
" )));
21 }
22 }
23 }
你会注意到 LayoutRoot在这里被定义成 internal,并且它的赋值使用了 “FindName”方法。
这就是使用用户控件的好处之一:它会自动为你做很多工作,但自定义控件则需要你自己来完成这些工作(但是如果考虑到你的效率的话,这并不是那么糟糕)。这里说明下:用户控件只是另一种自定义控件。
解剖自定义控件
自定义控件不像用户控件会有一个 xaml和一个后台代码组成,换成除了一个默认的 XAML Template以外其余的全部是代码。你可以认为 XAML Template和用户控件的 XAML文件作用一样,但是这里要注意, XAML Template可以实现任何改变。这里要注意另外一件事件,因为 Template不具有 Visual Studio为您生成的隐藏代码局部类,所以任何事件处理程序不能在 Template中定义。那么我们怎样重新创建上述用户控件为一个自定义控件呢?
对于 Silverlight这是很容易的,右键单击您的项目,选择 “添加 -> 新建项 –> Silverlight模板化控件”。 WPF 和 Windows Phone不伴随此模板,所以你必须手工通过创建一个类和一个通用模板文件。你做到了这一点后你会发现两个新文件:首先一个简单的 C#类,第二个是在 \Themes\Generic.xaml下创建了一个新文件。第二个文件汇集了你所有控件的 Template样式。它的名字必须是 Generic.xaml而且必须在该目录下,这样自定义控件才能使用所有的 Template。
下面让我们一起来看看Template是怎么写的,和上面用户控件一样也是添加了一个Button和一个Grid。
1
<
ResourceDictionary
2
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
4
xmlns:local
="clr-namespace:MyApp"
>
5
6
<
Style
TargetType
="local:TemplatedControl1"
>
7
<
Setter
Property
="Template"
>
8
<
Setter.Value
>
9
<
ControlTemplate
TargetType
="local:TemplatedControl1"
>
10
<
Border
Background
="
{TemplateBinding Background}
"
11
BorderBrush
="
{TemplateBinding BorderBrush}
"
12
BorderThickness
="
{TemplateBinding BorderThickness}
"
>
13
<
Grid
x:Name
="LayoutRoot"
>
14
<
Button
x:Name
="ClickButton"
Content
="Click me!"
Opacity
=".5"
/>
15
</
Grid
>
16
</
Border
>
17
</
ControlTemplate
>
18
</
Setter.Value
>
19
</
Setter
>
20
</
Style
>
21
</
ResourceDictionary
>
首先第一,注意 Border上 TemplateBinding语句,它是控件中一个重要的功能。您可以直接在你的控件代码中定义一个依赖项属性绑定。由于自定义控件继承 Control,你将自动继承 Background、 BorderBrush、 BorderThickness 和其他属性。请注意 我这里我没有给按钮添加 click事件。如果这里添加了,模板将会加载失败。我们将在后台加上 click处理程序,接下来,让我们一起看代码吧:
1
using System.Windows;
2
using System.Windows.Controls;
3
using System.Windows.Controls.Primitives;
4
using System.Windows.Media;
5
6
namespace MyApp
7 {
8 [TemplatePart(Name=
"
LayoutRoot
" , Type=
typeof (Control))]
9 [TemplatePart(Name =
"
ClickButton
" , Type =
typeof (ButtonBase))]
10
public
class TemplatedControl1 : Control
11 {
12 Control layoutRoot;
13 ButtonBase button;
14
public TemplatedControl1()
15 {
16
this .DefaultStyleKey =
typeof (TemplatedControl1);
17 }
18
public
override
void OnApplyTemplate()
19 {
20
if (button !=
null )
//
unhook from previous template part
21
{
22 button.Click -=
new RoutedEventHandler(button_Click);
23 }
24 button = GetTemplateChild(
"
ClickButton
" )
as ButtonBase;
25
if (button !=
null )
26 {
27 button.Click +=
new RoutedEventHandler(button_Click);
28 }
29 layoutRoot = GetTemplateChild(
"
LayoutRoot
" )
as Panel;
30
base .OnApplyTemplate();
31 }
32
33
private
void button_Click(
object sender, RoutedEventArgs e)
34 {
35 layoutRoot.Background =
new SolidColorBrush(Colors.Red);
36 }
37 }
38 }
首先在控件中声明 ”TemplatePart” ,它指定预期元素的名称和和类型。在 demo 中 LayoutRoot 的类型是 Panel ( Grid 的类型是 Control )、 ClickButton 的类型是 ButtonBase 。这些不是严格要求,但是当你调用写好的自定义控件时,它们能帮助 Expression Blend 了解模板的要求。我总是控件层次结构申明需要的最小类型,使 Template 更加灵活。比如我用 ButtonBase 而不是 Button ,因为我只要用到定义 ButtonBase 基类的 Click 事件。同样 LayoutRoot 也一样,我只需要它的 BackGround 属性。
在构造函数中,我定义了 ”DefaultStyleKey” ,它告诉 Framework 我在 Themes\Generic.xaml 中定义了默认 Template 。
最后,最重要的部分是 ”OnApplyTemplate” ,此方法当 Template 加载完后被调用。这是我们早期的机会,抢先对 Template 中 controls 的引用,即控件中申明的 TemplatePart 。在这种情况下,我抢先引用在 Template 中定义 ButtonBase ,如果找到它,我将给它添加一个 click 事件处理程序。此外,如果一个新的 Template 被应用,一定要记住去除以前实例中的事情处理程序。同样重要要注意的是 Template 部件总是可选的!所以你要检查所有引用 template 的部件是否为 null 。
添加 Visual States 到控件
现在添加一些鼠标状态到我们的控件,并控制动画何时触发。在后台代码中我们定义的添加两个 TemplateVisualState 属性:
1 [TemplateVisualState(GroupName =
"
HoverStates
" , Name =
"
MouseOver
" )]
2 [TemplateVisualState(GroupName =
"
HoverStates
" , Name =
"
Normal
" )]
接下来给控件添加 visual state 的触发:
1
bool isMouseOver;
2
protected
override
void OnMouseEnter(System.Windows.Input.MouseEventArgs e)
3 {
4 isMouseOver =
true ;
5 ChangeVisualState(
true );
6
base .OnMouseEnter(e);
7 }
8
protected
override
void OnMouseLeave(System.Windows.Input.MouseEventArgs e)
9 {
10 isMouseOver =
false ;
11 ChangeVisualState(
true );
12
base .OnMouseLeave(e);
13 }
14
15
private
void ChangeVisualState(
bool useTransitions)
16 {
17
if (isMouseOver)
18 {
19 GoToState(useTransitions,
"
MouseOver
" );
20 }
21
else
22 {
23 GoToState(useTransitions,
"
Normal
" );
24 }
25 }
26
27
private
bool GoToState(
bool useTransitions,
string stateName)
28 {
29
return VisualStateManager.GoToState(
this , stateName, useTransitions);
30 }
这正是我们需要的所有代码。它非常简单。如果鼠标停留,则触发 MouseOver 状态,否则则触发正常状态。请注意,实际上我们没有真正定义什么是 ”MouseOver” ,这是 Template 的工作。好接下来让我们来定义:
1
<
ControlTemplate
TargetType
="local:TemplatedControl1"
>
2
<
Border
Background
="
{TemplateBinding Background}
"
3
BorderBrush
="
{TemplateBinding BorderBrush}
"
4
BorderThickness
="
{TemplateBinding BorderThickness}
"
>
5
<
VisualStateManager.VisualStateGroups
>
6
<
VisualStateGroup
x:Name
="HoverStates"
>
7
<
VisualState
x:Name
="MouseOver"
>
8
<
Storyboard
>
9
<
ColorAnimation
10
Storyboard.TargetName
="BackgroundElement"
11
Storyboard.TargetProperty
="(Rectangle.Fill).(SolidColorBrush.Color)"
12
To
="Yellow"
Duration
="0:0:.5"
/>
13
</
Storyboard
>
14
</
VisualState
>
15
<
VisualState
x:Name
="Normal"
>
16
<
Storyboard
>
17
<
ColorAnimation
18
Storyboard.TargetName
="BackgroundElement"
19
Storyboard.TargetProperty
="(Rectangle.Fill).(SolidColorBrush.Color)"
20
To
="Transparent"
Duration
="0:0:.5"
/>
21
</
Storyboard
>
22
</
VisualState
>
23
</
VisualStateGroup
>
24
</
VisualStateManager.VisualStateGroups
>
25
<
Grid
x:Name
="LayoutRoot"
>
26
<
Rectangle
x:Name
="BackgroundElement"
Fill
="Transparent"
/>
27
<
Button
x:Name
="ClickButton"
28
Content
="Click me!"
Opacity
=".5"
/>
29
</
Grid
>
30
</
Border
>
31
</
ControlTemplate
>
好了,你现在有一个控件,当 ButtonBase 被点击以及鼠标悬停或离开时, Panel 的背景色会改变,这样可以解决于很多控件,不用重写代码。