第一次翻译msdn中的文章,也不知道翻译完后能不能读懂,试一试吧。
原文的地址:Extreme ASP.NET:Encapsulate Silverlight with ASP.NET Controls作者:Fritz Onion
中文名称:极限ASP.NET:将Silverlight封装成ASP.NET控件
这是微软网站上翻译的:使用 ASP.NET 控件封装 Silverlight
本文中的代码下载地址: ExtremeASPNET2008_01.exe,在线看代码地址:Browse the Code Online
本文分以下部分:
1、使用Silverlight
2、构建一个自定义Silverlight控件
3、ASP.NET AJAX与IScriptControl
4、The asp:Xaml and asp:Media Controls
因为有很多种方式可以将Silverlight加入到应用中,导致有许多ASP.NET开发人员都感觉到困惑。
除了使整个页面作为一个Silverlight控件,还有其它的方式,你还可以将你的网页作为Silverlight的一部分,以及整合丰富的UI元素,只要保持基本的应用结构相同,都能带来很多好处。
后一种方法有时被称为“adding islands of richness”到你的网页。这些islands通过许多可脚本化的方法和事件作为”possible bridges”与Silverlight进行联系。
在本月的专栏中,我要去探索构建自定义服务器控件封装Silverlight内容的技术。 把Silverlight包装成一个自定义的ASP.NET控件,使之成为有吸引力的技术。 It makes integrating Silverlight content as easy as using any other server-side control, which greatly increases the chances of adoption. The process of adding handlers, setting properties, and calling methods on the Silverlight control is exactly the same as with any other control. And this approach keeps pages free from the clutter of Silverlight-specific JavaScript, making it easier to maintain and deploy.
1、使用Silverlight
在我进入构建自定义Silverlight控件的详细内容之前,我应该准确的描述一下在什么地方需要将Silverlight内容呈现给客户端。因此,我将首先向介绍如何在ASP.NET页面加入XAML文件,以及Silverlight如何去页面进行交互?当然,如何将页面上的一组控件加上控件之间的行为进行封装,是很清晰的。
第一步是编写一个XAML文件通过Silverlight来呈现。我的目标是即要保持XAML足够简单,又完成复杂的控制与交互。所以我选择了用一个标题呈现一个单独的sphere。如图1所示的XAML代码定义了一个TextBlock,作为标题,以及一个渐变画笔描绘一个椭圆形。这个XAML也在resources portion of the Canvas中定义了两个故事板,描述两个动画,内容分别是控制sphere的增长和收缩,每个过程持续2秒钟时间。最后,我想将这个加入了XAML的Silverlight通过JavaScript程序来设置标题的文本,当用用户点击的呈现的sphere时,能够调用动画。
图1 Sphere.xaml File
<!-- File: Sphere.xaml -->
<Canvas
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="300" Height="300"
Background="White"
>
<Canvas.Resources>
<Storyboard x:Name="growAnimation">
<DoubleAnimation
Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="(Ellipse.Width)"
To="250" Duration="0:0:2" />
<DoubleAnimation
Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="(Ellipse.Height)"
To="250" Duration="0:0:2" />
</Storyboard>
<Storyboard x:Name="shrinkAnimation">
<DoubleAnimation
Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="(Ellipse.Width)"
To="200" Duration="0:0:2" />
<DoubleAnimation
Storyboard.TargetName="ellipse"
Storyboard.TargetProperty="(Ellipse.Height)"
To="200" Duration="0:0:2" />
</Storyboard>
</Canvas.Resources>
<TextBlock x:Name="titleText" Width="200" Height="24"
Canvas.Left="94" Text="[Title]" TextWrapping="Wrap" />
<Ellipse Width="200" Height="200"
x:Name="ellipse" Canvas.Left="47" Canvas.Top="41">
<Ellipse.Fill>
<RadialGradientBrush GradientOrigin="0.75,0.25"
Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="Yellow" Offset="0" />
<GradientStop Color="Green" Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Canvas>
为了将XAML加入到网页中,我第一需要创建一个Silverlight插件。做这件事的最简单的方式是添加包含在Silverlight 1.0 SDK中的Silverlight.js脚本文件。这个脚本文件定义了一个Silverlight.createObject方法,可以用来创建插件。Silverlight插件需要使用页面上的一个html元素(通常都是用<div>),因此最好是定义一个<div>元素,然后在<div>元素内部嵌入一段脚本,用来调用createObject方法。
图2显示的是如何通过使用<div>元素创建一个Silverlight创建实例。假设本页面与图1所示Sphere.xaml在同一个目录中,当在浏览器中浏览,这个页面会显示一个可爱的绿球,如图3所示。
图2 Create and Associate Silverlight Control
<%@ Page Language="C#" %>
<html >
<head runat="server">
<script type="text/javascript" src="silverlight.js"></script>
</head>
<body>
<form id="form1" runat="server">
<div id="slControlHost">
<script type="text/javascript">
Silverlight.createObject(
"Sphere.xaml",
document.getElementById('slControlHost'), "slControl",
{ width:'300', height:'300', version:'1.0' },
{ onError:null, onLoad:null },
null);
</script>
</div>
</form>
</body>
</html>
图3 Control Displaying a Green Sphere
.gif)
接下来的任务是与XAML的互动,可以通过编程更改标题文本,同时也可以像刚才讲的,让增长和收缩的动画运行。做这件事情最简单的方法是在XAML中的对象上声明指定的处理程序。在这个例子中,当用户点击sphere时发生增长和收缩动画,动画的运行根据当前的sphere的大小而定。
首先,为定义在sphere之中的椭圆增加一个MouseLeftButtonDown事件,然后在页面上加入相应的JavaScript方法。同样,我在TextBlock增加了Loaded事件,事件完成的功能是将TextBlock中的内容显示在sphere之上。加入到XAML中的这些程序像下面这样:
<Ellipse MouseLeftButtonDown="javascript:onSphereButtonDown" ... <TextBlock Loaded="javascript:onTextLoaded" ...
我添加到ASP.NET页面上的相对应的脚本方法如图4所示
图4 JavaScript Methods Added to New Script Block
<script type="text/javascript">
function onSphereButtonDown(sender, args)
{
// Run the grow or shrink animation,
// depending on whether it is currently
// 'grown' or 'shrunk'
var animationName = (sender.Width==200) ?
"growAnimation" : "shrinkAnimation";
var sl = sender.getHost();
var animation = sl.content.findName(animationName);
if (animation)
animation.begin();
}
function onTextLoaded(sender, args)
{
sender.Text = "My Growing Sphere";
}
</script>
这就是所有需要手动加入到ASP.NET页面的Silverlight内容。当这个页面在浏览器中打开,显示的标题是 “My Growing Sphere”。当用户点击sphere时,sphere会增长,下一秒会收缩。就像你看到的那样,将Silverlight内容集成到ASP.NET页面相对来讲是比较容易的,很容易操作,因为它的整个对象模型通过脚本暴露在页面上。
然而,对于一个像sphere这样的元素,你可能需要将它打包成一个自定义控件,因为打包后就可以通过拖动的方式将它方便的加入到页面的设计时。这正是封装Silverlight发挥作用的地方。
2、构建一个自定义Silverlight控件
下面开始将Silverlight封装成一个新的ASP.NET控件:
namespace MsdnMagazine
{
public class SilverlightSphere : Control
{
}
}
在这个例子中,SilverlightSphere继承自System.Web.UI.Control类。我没有选择继承WebControl类,因为WebControl类定义的样式属性并不适用于Silverlight控件。
接下来,我需要将JavaScript代码手工包含进来。最简单的方法是在ASP.NET控件中定义一个JavaScript文件,然后将其作为一个resource嵌入到已编译的程序集中,可以通过将两个事件方法移动到一个单独的JavaScript文件中完成,这个文件就是SilverlightSphere.js:
// File: SilverlightSphere.js
function onSphereButtonDown(sender, args)
{
// Same implementation as before
}
function onTextLoaded(sender, args)
{
// Same implementation as before
}
接下来,新的SilverlightSphere.js文件与原来的Silverlight.js文件作为resource嵌入在这个控件项目中。这样做是为了这个控件在部署的时候能够成为一个单独的程序集,而不依赖其它文件。同样,对于提取嵌入resourece,即使用WebResource.axd程序机制嵌入在ASP.NET中的XAML文件以及XAML文件引用的两个JavaScript文件也很有意义。
After I set the Build Action to Embedded Resource for all three files in Visual Studio? (/res from the command line compiler), I then use the assembly-level attribute System.Web.UI.WebResource to grant permission for these resources to be served by WebResource.axd and to associate a MIME type for the response.我使用下面的声明来完成这些(其中SlSphere是控件项目名称):
[assembly: WebResource("SlSphere.SilverlightSphere.js", "text/javascript")]
[assembly: WebResource("SlSphere.Silverlight.js", "text/javascript")]
[assembly: WebResource("SlSphere.Sphere.xaml", "text/xml")]
现在JavaScript文件作为嵌入resources被编译到程序集之中。因此,通过重写继承自Control基类的OnInit方法,在重写的方法中使用ClientScriptManager类的(通过Page.ClientScriptManager访问)RegisterClientScriptResource方法,可以引用这些文件并呈现在页面上。
protected override void OnInit(EventArgs e)
{
Page.ClientScript.RegisterClientScriptResource(this.GetType(),
"SlSphere.Silverlight.js");
Page.ClientScript.RegisterClientScriptResource(this.GetType(),
"SlSphere.SilverlightSphere.js");
base.OnInit(e);
}
控件最后一步,也是最重要的部分就是实现虚方法Render。在重写虚方法Render中有两件事情要完成。第一,需要呈现一个<div>标记,用来作为Silverlight插件。第二,必需呈现嵌入的脚本来创建Silverlight插件。
通过调用ClientScriptManager类的GetWebResourceUrl方法生成引用的XAML文件。这将确保嵌入在程序集中的XAML文件内容被Silverlight控件初始化。最后,需要为Silverlight插件指定一个惟一标识ID。代码看图5
图5 Implement the Virtual Render Method
Const string _silverlightCreateScript = @"Silverlight.createObject(
'{0}, document.getElementById('{1}'), '{2},
{{ width:'300', height:'300', version:'1.0' }},
{{ onError:null, onLoad:null }},
null);";
protected override void Render(HtmlTextWriter writer)
{
string script = string.Format(_silverlightCreateScript, Page.ClientScript.GetWebResourceUrl(GetType(),"SlSphere.Sphere.xaml"),this.ClientID, this.ClientID + "_ctrl");
writer.AddAttribute(HtmlTextWriterAttribute.Id,this.ClientID);
writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.AddAttribute(HtmlTextWriterAttribute.Type,"text/javascript");
writer.RenderBeginTag(HtmlTextWriterTag.Script);
writer.Write(script);
writer.RenderEndTag();
writer.RenderEndTag();
base.Render(writer);
}
通过封装一些必要的Silverlight在ASP.NET页面上,我们现在有了一个服务器端控件(JavaScript和XAML都完全嵌入到resources中)。有了这个控件以后,就可以在页面上创建这个控件的实例像下面这样定义:
<%@ Register Assembly="SlSphere" Namespace="MsdnMagazine" TagPrefix="csc" %> ... <csc:SilverlightSphere ID="_silverlightSphere" runat="server" />
现在我们可以做一个另人激动的事情了,在任何ASP.NET应用中使用Silverlight都可以方便的把会增长和收缩的sphere集成进来,而不需要担心Silverlight中的内容。
当然,你可以轻车熟路的构建有趣的、复杂的控件在Silverlight上展示。然而,当你开始想要添加一些东西到您的控件上去时,你很快会遇到怎么样设置服务器控件的XAML属性值的问题。例如,想要在控件中暴露Title的property,然后映射titleText元素的Text property。没有什么显而易见的方法能够在当前的实现中完成这个想法。
当前实现的另一个缺点是,XAML作为固定的嵌入资源。虽然这样部署起来很方便,Silverlight和XAML在一起的主要好处就是分离了设计的行为,即使XAML可以被替代。可以在控件上定义一个XamlUrl property,允许覆盖嵌入的XAML内容。这个方法不难,但它可能会带来一些问题,因为XAML暴露了JavaScript程序的名字,而这个需要设计师在最新版的XAML中进行确认。
3、ASP.NET AJAX与IScriptControl
问题的核心是如何在client-side script控制server-side properties。幸运的是,ASP.NET AJAX extensions提供了一个清晰的方法:通过定义在System.Web.Extensions assembly的IScriptControl interface,用client-side 元素控制server-side properties。当执行绑在页面的ScriptManager控件,会生成 JavaScript files文件的引用和定义一些JavaScript类的实例,它们分别是:
public interface IScriptControl
{
IEnumerable<ScriptDescriptor> GetScriptDescriptors();
IEnumerable<ScriptReference> GetScriptReferences();
}
可以想象有这样一个描述符,它作为服务器端控件的客户端代替,通过定义服务器控件的属性初始化。它会提供一个服务器控件属性与客户端属性之间的链接,就是通过Silverlight and JavaScript handlers搭建的。在一个名空间和类之下,描述符同样还提供了一个清晰的范围机制用来隔离所有的控件客户端特性(至少作为 ASP.NET AJAX client libraries在JavaScript中定义名空间和类)。
当构建一个自定义的ASP.NET AJAX控件用来hosts Silverlight内容。首先创建一个客户端类定义方法与Silverlight控件属性交互。这对于增加一个标题和XamlUrl property到控件上是很有意义的,因此,我开始定义这个类使用这些属性,名字就叫MsdnMagazine.SilverlightAjaxSphere:
// File: SilverlightAjaxSphere.js
Type.registerNamespace('MsdnMagazine');
MsdnMagazine.SilverlightAjaxSphere = function(element) {
MsdnMagazine.SilverlightAjaxSphere.initializeBase(
this, [element]);
this._title = null;
this._xamlUrl = null;
}
从初始化开始,需要增加一些方法到类的原型上(见图6),而不是嵌入在一个<div>元素的脚本,像在第一个控件的执行一样,这一次创建的Silverlight插件在初始化客户端方法的类。Note that this is only possible since the _xamlUrl property I defined in the class will be populated with the server-side value prior to this method being called.也可以通过<div>元素的ID去呈现服务器端控件继承自Sys.UI.Control的get_id方法。
图6 Populate the Methods for Class Prototype
// File: SilverlightAjaxSphere.js
MsdnMagazine.SilverlightAjaxSphere.prototype = {
initialize : function() {
MsdnMagazine.SilverlightAjaxSphere.callBaseMethod(
this, 'initialize');
var hostId = this.get_id() + 'Host';
Silverlight.createObject(this._xamlUrl,
$get(this.get_id()), hostId,
{ width:'300', height:'300', version:'1.0' },
{ onError:null,
onLoad:Function.createDelegate(this, this._onXamlLoaded) },
null);
},
dispose : function() {
MsdnMagazine.SilverlightAjaxSphere.callBaseMethod(this,
'dispose');
},
// Once called, Silverlight control is initialized and
// Xaml elements can be accessed.
_onXamlLoaded : function(root) {
var root = $get(this.get_id() + 'Host');
var sphere = root.content.findName('ellipse');
if (sphere) {
sphere.addEventListener('MouseLeftButtonDown',
Function.createDelegate(this,
this._onSphereButtonDown));
}
else
throw Error.invalidOperation(
"You must have an ellipse element.");
var title = root.content.findName('titleText');
if (title)
title.Text = this._title;
},
_onSphereButtonDown : function(sender, args)
{
// same implementation as before
},
// property accessors
//
get_title : function() {
return this._title;
},
set_title : function(value) {
if (this._title !== value)
this._title = value;
},
get_xamlUrl : function() {
return this._xamlUrl;
},
set_xamlUrl : function(value) {
if (this._xamlUrl !== value)
this._xamlUrl = value;
}
}
MsdnMagazine.SilverlightAjaxSphere.registerClass(
'MsdnMagazine.SilverlightAjaxSphere', Sys.UI.Control);
if (typeof(Sys) !== 'undefined')
Sys.Application.notifyScriptLoaded();
When the initialize method is called on my class, the Silverlight plug-in is just being created and the XAML has not yet been evaluated. I therefore use the onLoad event handler defined in the createObject method of Sys.Silverlight to specify a callback to be invoked once the XAML is loaded—this allows me to wire up the event handlers and initialize properties on individual XAML elements. This handler, which I have called _onXamlLoaded, is passed a reference to the root Silverlight element, which I can use to retrieve the named elements in the XAML.
You should be as accommodating as possible when interacting with named XAML elements, which entails checking to see if they exist before actually modifying their content or wiring up event handlers. If the element is missing and is not crucial to the operation of the control (like the titleText element in my control’s XAML), just skip the initialization steps for that element. This way, a designer can elect to remove certain elements from the XAML and the control will still work.
The rest of the class’s implementation defines the property accessors for title and xamlUrl and implements the _onSphereButtonDown function to initiate the grow and shrink animations, as I did before. This time, however, the method is encapsulated in a class.
The final step in the client-side script is to register the new class, specifying the base class as Sys.UI.Control.
The remaining task is to build the server-side control that uses this class via the IScriptControl interface (see Figure 7). Like the client-side class, the server control exposes two properties: Title and XamlUrl. (These are backed by ViewState, as all server-side control properties should always be.) I initialize the XamlUrl property to the URL, referencing the embedded XAML file using the same GetWebResourceUrl method I used in the last control implementation. By setting this property in an override of OnInit, I ensure that the property will default to this URL, but any overridden version specified by the client will always take priority since state is loaded later in the lifecycle.
图7 Server-Side Control
// File: SilverlightAjaxSphere.cs
//
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
[assembly: WebResource("SlAjaxSphere.SilverlightAjaxSphere.js",
"text/javascript")]
[assembly: WebResource("SlAjaxSphere.Silverlight.js",
"text/javascript")]
[assembly: WebResource("SlAjaxSphere.Sphere.xaml", "text/xml")]
namespace MsdnMagazine
{
public class SilverlightAjaxSphere : Control, IScriptControl
{
public string Title
{
get { return (string)(ViewState["title"] ?? "[title]"); }
set { ViewState["title"] = value; }
}
public string XamlUrl
{
get { return (string)(ViewState["xamlurl"] ??
string.Empty); }
set { ViewState["xamlurl"] = value; }
}
protected override void OnInit(EventArgs e)
{
// Default the XamlUrl to my embedded resource.
// If this is set in the
// markup explicitly, it will be overridden.
XamlUrl = Page.ClientScript.GetWebResourceUrl(GetType(),
"SlAjaxSphere.Sphere.xaml");
base.OnInit(e);
}
public IEnumerable<ScriptReference> GetScriptReferences()
{
ScriptReference sr1 = new ScriptReference(
" SlAjaxSphere.Silverlight.js",
GetType().Assembly.FullName);
ScriptReference sr2 = new ScriptReference(
" SlAjaxSphere.SilverlightAjaxSphere.js",
GetType().Assembly.FullName);
return new ScriptReference[] { sr1, sr2 };
}
public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
ScriptControlDescriptor scd = new ScriptControlDescriptor(
"MsdnMagazine.SilverlightAjaxSphere",
this.ClientID);
scd.AddProperty("title", this.Title);
scd.AddProperty("xamlUrl", this.XamlUrl);
return new ScriptDescriptor[] { scd };
}
protected override void OnPreRender(EventArgs e)
{
if (!DesignMode)
{
ScriptManager sm = ScriptManager.GetCurrent(Page);
if (sm == null)
throw new HttpException(
"A ScriptManager control must exist on the current page.");
sm.RegisterScriptControl(this);
sm.RegisterScriptDescriptors(this);
}
base.OnPreRender(e);
}
protected override void Render(HtmlTextWriter writer)
{
writer.AddAttribute(HtmlTextWriterAttribute.Id,
this.ClientID);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.RenderEndTag();
base.Render(writer);
}
}
}
The next two methods, GetScriptDescriptors and GetScriptReferences, contain the implementation of the IScriptControl interface. Since I am embedding the JavaScript files as resources in the assembly, I use the overloaded constructor of ScriptReference, which takes the resource name and assembly name. This generates a reference to the embedded script resources in much the same way RegisterClientScriptResource did in the previous control implementation. GetScriptDescriptors is the key to creating an instance of the client-side MsdnMagazine.SilverlightAjaxSphere class initialized with properties from the server-side control.
The ScriptControlDescriptor class takes the name of the client-side class and the ID of the associated client-side element in its constructor. I then use the AddProperty method of the class to specify the initial value of each property of the new client-side class. By passing in the current property values of my control, I ensure that the client-side class will be created with the current property values defined in my control.
I still need to take care of one last bit of housekeeping in an override of OnPreRender, where I register the control as a script control with the current ScriptManager on the page (throwing an exception if there is no ScriptManager) as well as with the script descriptors. This causes the ScriptManager to invoke the two methods of IScriptControl on the control to pull in the script references and create the initial client-side class. The Render method remains identical to the previous control implementation, simply creating the
I now have a complete server-side control that encapsulates Silverlight. In this implementation, the control exposes properties that are used to initialize elements of the XAML (or even replace the entire XAML if need be). The following declarations will create an instance of my new control on a page with the title text set to a custom string:
<%@ Register Assembly="SlAjaxSphere" Namespace="MsdnMagazine"
TagPrefix="sas" %>
...
<sas:SilverlightSphere ID="_silverlightSphere" runat="server"
Title="My custom title!" />
4、The asp:Xaml and asp:Media Controls
两个新的ASP.NET AJAX控件被在2007年5月开发出来了,分别是 asp:Xaml和asp:Media。(注意:asp:Media是继承自asp:Xaml).这两个控件提供了Silverlight封装,他们的实现方式与我在前面的章节介绍的例子是相似的。事实上,开发出来的这两个控件的灵感来自于我在前面介绍的控件。
asp:Xaml控件需要指定一个XAML文件,同时它还包括了创建Silverlight plug-in、处理一些详细信息以及加载XAML文件的属性。例如:可以使用Sphere.xaml作为这个控件的XamlUrl属性值。
<asp:Xaml runat="server" ID="_sphereXaml" Windowless="true"
Width="300px" Height="300px"
XamlUrl="~/Sphere.xaml" />
asp:Media控件让(video or audio)等多媒体体内容嵌入到ASP.NET页面变得非常容易,它已经是一个非常好的示例,用来展示封装Silverlight作为ASP.NET服务器端控件。多媒体控件的一个另人好奇的特性是提供8个不同的皮肤,你可以选择其中一个来呈现,同时也可以自己设计并制作一个属于自己的皮肤。图8展示的是使用Expression皮肤的多媒体控件播放视频的画面:
图8 Media Control Displays Video
.gif)
<asp:Media runat="server" ID="_butterflyVideo"
MediaSkin="Expression" MediaUrl="~/Butterfly.wmv" />
另一种方式是创建属于自己的Silverlight服务器端控件,该控件起源于asp:Xaml控件,然后像前面讲到的一样增加一些属性和方法。在本专栏的代码下载中,包含了另一个继承自asp:Xaml的SilverlightSphere控件版本。