使用 ASP.NET 控件封装 Silverlight
Fritz Onion
代码下载位置: ExtremeASPNET2008_01.exe (700 KB)
Browse the Code Online
世界上许多 ASP.NET 开发人员都想知道如何将 SilverlightTM 整合到其应用程序中,以及整合到其应用程序的什么位置。显然,回答该问题并不是那么容易,因为可以采用的方法多种多样。您可以将整个页面作为一个 Silverlight 控件并在此页面上执行任何操作,也可以在页面上确定 Silverlight 作用到的部分,并在更能体现许多 UI 元素作用的地方集成这些 UI 元素,您更有可能采取后一种方法。
有时将后一种方法称为在页面上“添加丰富多彩的岛屿”。而有了 Silverlight,这些岛屿就可能通过可编写脚本的方法和事件与周围内容联系起来。
在本月的专栏中,我将研究用于生成封装 Silverlight 内容的自定义服务器控件的方法。将 Silverlight 内容封装到自定义 ASP.NET 控件有多个优点,正是这些优点,使其成为备受关注的技术。该技术使集成的 Silverlight 内容使用起来就像使用其他任何服务器端控件一样简单,从而极大增加了被采用的机会。在 Silverlight 控件上添加处理程序、设置属性和调用方法的过程与在其他任何控件上的处理过程完全相同。而且此方法可保持页面的简洁,不会受混乱的 Silverlight 特定的 JavaScript 所影响,从而可轻松进行维护和部署。
使用 Silverlight
在详细介绍生成自定义控件以承载 Silverlight 内容之前,应先介绍将 Silverlight 内容呈现给客户时需要些什么。因此,一开始我会介绍如何在 ASP.NET 页面上承载 XAML 文件以及在页面和呈现的 Silverlight 内容之间添加交互。在我的页面展示最终要通过控件封装的行为之后,您就会对如何将控件的各部分结合在一起更加清楚。
第一步,编写 Silverlight 呈现的 XAML 文件。我的目标是,既要使 XAML 在呈现控件时尽可能简单,不花哨,又要使其内容足以能引起读者的兴趣。因此,我选择呈现带有标题的单个球体。图 1 所示的 XAML 定义一个用作标题的 TextBlock 和一个用光线渐变画笔绘制的椭圆。此 XAML 还在 Canvas 的资源部分定义两个 Storyboard,分别绘制两个将球体变大和变小的动画,每个持续时间为两秒。最后,我想在 Silverlight 中承载此 XAML,然后当用户在呈现的球体上单击时,使用 JavaScript 处理程序设置标题文本并调用动画。
Figure 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> 元素,并将对嵌入脚本块中 createObject 的调用放在 <div> 中。
图 2 说明了如何创建 Silverlight 控件的实例并将其与 <div> 元素关联。此页面假定图 1 所示的 Sphere.xaml 文件与页面本身位于同一目录。在 Web 浏览器中查看时,此页面显示一个如图 3 所示的可爱的绿球。
Figure 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>
Figure 3 Control Displaying a Green Sphere
接下来的任务是与 XAML 交互,以便可通过编程更改标题文本,并启动上文所述的可变大变小的动画。最简单的交互方法是以声明的方式将处理程序分配给 XAML 中定义对象的事件。在本例中,用户单击球体会出现变大变小的动画——而运行哪个动画取决于球体的当前大小。
首先,我将处理程序添加到定义球体的 Ellipse 的 MouseLeftButtonDown 事件,然后在页面上放置用 JavaScript 编写的相应方法。按类似的方式处理 TextBlock 的 Loaded 事件,以将球体上显示的文本设置为自定义的字符串。添加到 XAML 的处理程序与下面的形式类似:
<Ellipse MouseLeftButtonDown="javascript:onSphereButtonDown" ... <TextBlock Loaded="javascript:onTextLoaded" ...
图 4 显示了添加到 ASP.NET 页面上新脚本块中的相应 JavaScript 方法。
Figure 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>
手动将 Silverlight 内容集成到 ASP.NET 页面的所有步骤就是这些。在浏览器中显示此页时,标题文本会显示“My Growing Sphere”。当用户第一次单击球体时,球体变大,第二次单击,球体变小。如您所见,将 Silverlight 内容集成到 ASP.NET 页面相对容易,而且易于操作,因为其整个对象模型都向页面上的脚本公开。
但是,对于诸如球体之类的简单元素,您可能希望将其打包成自定义控件,这样只需将其控件拖放到页面的设计图面,就可以集成到应用程序。这便是封装 Silverlight 发挥作用的地方。
生成自定义 Silverlight 控件
封装 Silverlight 的任务从创建新自定义 ASP.NET 控件开始:
namespace MsdnMagazine { public class SilverlightSphere : Control { } }
在本例中,我从 System.Web.UI.Control 派生。我选择不从 WebControl 派生是因为该类定义的其他样式属性不适合此控件。
接着,需要呈现手动包含的 JavaScript 处理程序。在 ASP.NET 控件中集成 JavaScript 的最简单方法是,先定义一个 JavaScript 文件,然后将其作为资源嵌入编译好的程序集中。将两个事件处理程序放在单独的 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 文件作为资源嵌入控件项目。这使控件可以作为单个程序集部署,而没有任何依赖文件。您还可以将 XAML 文件作为资源嵌入,并使用 ASP.NET 中提取嵌入资源的 WebResource.axd 处理程序机制引用该文件和这两个 JavaScript 文件。
在 Visual Studio® 中针对这三个文件将“生成操作”设置为“嵌入的资源”(命令行编译器的 /res)后,再使用程序集级属性 System.Web.UI.WebResource 授予这些资源的权限,以便得到 WebResource.axd 的支持并关联用于响应的 MIME 类型。使用下面的声明可实现这一点(其中 SlSphere 是控件项目的名称):
[assembly: WebResource("SlSphere.SilverlightSphere.js", "text/javascript")] [assembly: WebResource("SlSphere.Silverlight.js", "text/javascript")] [assembly: WebResource("SlSphere.Sphere.xaml", "text/xml")]
现在,JavaScript 文件作为资源被编译成程序集。因此,现在可以使用 ClientScriptManager 类(通过 Page.ClientScriptManager 访问)的 RegisterClientScriptResource 方法使呈现的页包含这些文件的引用。在从 Control 基类继承的 OnInit 方法的重写方法中对每个文件调用此方法:
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 方法。在此方法中必须完成两个任务。首先,需要呈现 <div> 标记以作为承载 Silverlight 插件的 HTML 元素。其次,必须呈现嵌入的脚本以创建 Silverlight 插件。
调用 ClientScriptManager 类的 GetWebResourceUrl 方法可生成对 XAML 文件的引用。这可确保使用作为资源嵌入到程序集中的 XAML 文件的内容初始化 Silverlight 控件。最后,需要为 Silverlight 插件本身指定唯一的标识符。在本例中,使用控件的 ID (this.ClientID) 并连接“_ctrl”作为其标识符(请参见图 5)。
Figure 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); }
现在即拥有一个服务器端控件(以及嵌入的 JavaScript 和 XAML 资源),该控件封装在 ASP.NET 页面上部署 Silverlight 所需的各部分。可以在页面上通过下列声明创建此控件的实例:
<%@ Register Assembly="SlSphere" Namespace="MsdnMagazine" TagPrefix="csc" %> ... <csc:SilverlightSphere ID="_silverlightSphere" runat="server" />
现在我能达成颇为引人注目的成果——任何 ASP.NET 应用程序都可以轻松集成 Silverlight 中承载的变大变小球体,而无需考虑承载 Silverlight 内容的详细信息。
当然,使用高级 XAML 呈现,可以生成更引人入胜的复杂控件。但是,当开始考虑可能要添加到控件的功能时,很快便会遇到如何使用服务器控件中的属性值处理 XAML 的问题。例如,假定要公开控件中的 Title 属性,并将其映射到 XAML 文件中 titleText 元素的 Text 属性。通过当前实现显然无法做到这一点。
当前实现的另一个缺点是 XAML 被固定为嵌入的资源。尽管这种实现方式便于部署,但 Silverlight 和 XAML 将行为与设计分离的优点却使 XAML 可被替换。可以在控件上定义一个 XamlUrl 属性来重写嵌入的 XAML 内容。此方法实现起来不难,但会在 XAML 中公开 JavaScript 处理程序的名称时带来问题,这就要求设计者使用最新版 XAML。
ASP.NET AJAX 和 IScriptControl
问题的症结是确定如何将服务器端属性引入客户端脚本。很幸运,ASP.NET AJAX 扩展提供了一种简洁的方法,即通过 System.Web.Extensions 程序集定义的 IScriptControl 接口将服务器端控件与客户端属性关联起来。此接口定义了两个方法:GetScriptReferences 和 GetScriptDescriptors。实现这些方法并将其与页面的 ScriptManager 控件关联后,它们会分别生成对 JavaScript 文件的引用和声明 JavaScript 类的实例:
public interface IScriptControl { IEnumerable<ScriptDescriptor> GetScriptDescriptors(); IEnumerable<ScriptReference> GetScriptReferences(); }
您可以将描述符视作服务器端控件的客户端代理,其中含有初始化为服务器控件定义的属性。这会将 Silverlight 和 JavaScript 处理程序可使用的服务器控件属性和客户端属性联系起来。描述符还提供了一个简洁的范围机制,用于隔离命名空间和类下所有控件的客户端功能(ASP.NET AJAX 客户端库至少在 JavaScript 中定义了命名空间和类)。
当生成承载 Silverlight 内容的 ASP.NET AJAX 控件时,首先创建客户端类定义,其中含有定义好的与 Silverlight 控件交互的方法和属性。可以将 Title 和 XamlUrl 属性都添加到此控件,因此开始时先定义带有这些属性的类,将类命名为 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 插件。请注意,只有在调用此方法之前,用服务器端值填充类中定义的 _xamlUrl 属性,才有可能创建。还可以访问 <div> 的标识符,它是由服务器端控件使用从 Sys.UI.Control 基类继承的 get_id 方法呈现的。
Figure 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();
当在类中调用初始化方法时,即会创建 Silverlight 插件,但尚未对 XAML 求值。因此,我使用 Sys.Silverlight 的 createObject 方法中定义的 onLoad 事件处理程序指定加载 XAML 后调用的回调——这便可以绑定事件处理程序并初始化各 XAML 元素的属性。此处理程序(我已将其称为 _onXamlLoaded)以引用的方式传递给根 Silverlight 元素,可以使用此元素检索 XAML 中的命名元素。
当与命名 XAML 元素交互时,必须保持耐心,因为这要求必须检查这些元素是否存在,然后才能真正修改其内容或绑定事件处理程序。如果缺少元素且元素对控件的操作不重要(就像控件的 XAML 中的 titleText 元素对控件不重要一样),请跳过该元素的初始化步骤。通过这种方式,设计者可以选择删除某些元素,而控件仍正常工作。
类的其余实现是定义标题和 xamlUrl 的属性访问器,并实现 _onSphereButtonDown 函数以启动变大和变小动画,这与前面类似。但是,此时方法是在类中封装的。
客户端脚本的最后步骤是注册新类,指定基类为 Sys.UI.Control。
余下的任务是生成通过 IScriptControl 接口使用此类的服务器端控件(请参见图 7)。与客户端类相似,服务器控件公开两个属性:Title 和 XamlUrl。(ViewState 支持这些属性,因为 ViewState 总是支持所有服务器端控件属性。)我将 XamlUrl 属性初始化为 URL,使用上个控件实现中使用过的同一个 GetWebResourceUrl 方法引用嵌入的 XAML 文件。通过在 OnInit 的重写中设置此属性,确保该属性默认设为此 URL,但客户端指定的任何重写版本将始终优先,因为状态在生命周期的后期加载。
Figure 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); } } }
接下来的两个方法 GetScriptDescriptors 和 GetScriptReferences 都包含 IScriptControl 接口的实现。因为将在程序集中将 JavaScript 文件作为资源嵌入,所以使用 ScriptReference 的重载构造函数,该函数使用资源名称和程序集名称。这会生成对嵌入脚本资源的引用,其方式与在前面的控件实现中生成对 RegisterClientScriptResource 的实现大致相同。GetScriptDescriptors 是创建使用服务器端控件中属性初始化客户端 MsdnMagazine.SilverlightAjaxSphere 类的实例关键。
ScriptControlDescriptor 类采用客户端类的名称和其构造函数中关联客户端元素的 ID。然后,使用类的 AddProperty 方法指定新客户端类的每个属性的初始值。通过传递控件中的当前属性值,将确保使用控件中的当前属性值创建客户端类。
仍需要最后关注 OnPreRender 的重写,在此重写中,使用页面上的当前 ScriptManager(若无 ScriptManager 将引发异常)以及脚本描述符将控件注册为脚本控件。这会导致 ScriptManager 调用控件上 IScriptControl 的两个方法获取脚本引用并创建初始的客户端类。Render 方法与前面的控件实现相同,仅创建 <div> 元素充当 Silverlight 插件的宿主。
现在就得到了封装 Silverlight 的完整的服务器端控件。在此实现中,控件公开用于初始化 XAML 元素的属性(或如需要,甚至替换整个 XAML)。可以在页面上通过下列声明创建新控件的实例,且标题文本设置为自定义字符串:
<%@ Register Assembly="SlAjaxSphere" Namespace="MsdnMagazine" TagPrefix="sas" %> ... <sas:SilverlightSphere ID="_silverlightSphere" runat="server" Title="My custom title!" />
asp:Xaml 和 asp:Media 控件
2007 年 5 月发布的 ASP.NET Futures 版本中引入了两个新 ASP.NET AJAX 控件:asp:Xaml 和 asp:Media。(请注意,asp:Media 继承自 asp:XAML。)这两个控件都提供自己的 Silverlight 封装,而且这两个控件生成的方式与前文提供的示例大致相同。实际上,我生成控件的灵感就来自这两个控件。
asp:Xaml 控件可以将 XAML 文件与控件关联。它包含 Silverlight 插件创建的各种属性,这些属性处理创建插件和加载 XAML 文件的所有细节。例如,可以通过下列声明,使用 XAML 文件显示 Sphere.xaml 文件:
<asp:Xaml runat="server" ID="_sphereXaml" Windowless="true" Width="300px" Height="300px" XamlUrl="~/Sphere.xaml" />
使用 asp:Media 控件可以轻松将媒体内容(视频或音频)嵌入 ASP.NET 页面,并在创建封装 Silverlight 的 ASP.NET 控件时对这些功能实实在在地加以展示。Media 控件其中一个吸引人的功能是它能提供八种不同的外观(XAML 文件),您可以选择这些外观来呈现媒体,或者可以设计自己的外观。图 8 显示了一个使用 Expression 外观播放视频的 Media 控件示例。
Figure 8 Media Control Displays Video
<asp:Media runat="server" ID="_butterflyVideo" MediaSkin="Expression" MediaUrl="~/Butterfly.wmv" />
另一种创建自己处理过的 Silverlight 服务器端控件的方法是直接从 asp:Xaml 控件派生,然后用属性和方法添加控件,方法大致与上文第二种控件实现方法类似。在本期专栏的代码下载中,我包含了另一个继承自 asp:Xaml 的 SilverlightSphere 控件版本。