牢记性能方面的技巧
就 在 Silverlight 1.0 问市之前,Microsoft 已发布了名为“基于 Silverlight 的应用程序的性能技巧”的文档 (msdn2.microsoft.com/bb693295)。利用此指南中汇集的技巧,您可避免某些常见错误,进而优化性能。其中包含以下最佳实践:
- 使用“可见性”属性而不是“不透明度”来隐藏对象。
- 不要使用 MediaElement 和“路径”对象的“宽度”和“高度”属性。
- 使用后以编程方式分离注册的事件处理程序。
- 慎重使用透明控件背景。
它 们所蕴含的道理不难理解。不难想象,为什么使用想要显示的宽度和高度来编码视频会更好(而不是以一个比例编码而以另一个比例显示)。这是因为 Silverlight 呈现引擎不必动态缩放每帧。记录下这些技巧会有所裨益,Silverlight 还有一个特性,它只需用普通的操作就能重设视频的大小,并且不会减弱性能。
我要添加到该文档中的一个最佳实践是要避免对 FindName 的冗余调用。我经常看见结构如下的事件处理程序:
function onClick(sender, args)
{
var rect = sender.findName('Rect');
rect.fill = 'red';
}
问 题是 FindName 必须搜索 XAML 对象树来查找其目标。如果要在应用程序的生命周期内多次引用名为 "Rect" 的 XAML 对象,则应对其进行一次初始化,例如,在根 canvas 的加载事件的事件处理程序中,并在全局变量中存储引用。然后,您就不必在每次调用 onClick 时都调用 FindName,而是可以:
function onClick(sender, args)
{
_rect.fill = 'red';
}
新 生成的代码将比原来的代码要快。但是如果开发人员这样做,他应确保插件操作完毕后释放该引用。在正常页面中它从来不是一个问题,但是在整个片段往来频繁的 AJAX 应用程序中,释放这些引用以避免内存泄漏就非常重要。因此,在使用 ASP.NET Silverlight 控件(当前可从“Extensions 3.5 Preview 社区技术预览”中得到)时配用的 Sys.UI.Silverlight.Control ajax 类型提供了一个 pluginDispose 方法,可将其覆盖以释放引用和事件处理程序。
提供有吸引力的安装体验
对于没有安装 Silverlight 的用户,基于 Silverlight 的应用程序安装体验通常不够友好,图 1 中所示的单调画面就是例证。通常调用实例化 Silverlight 控件的 Silverlight.createObjectEx 函数,在 Silverlight 不存在时显示“Get Microsoft Silverlight”(获得 Microsoft Silverlight)按钮。单击按钮会将用户带到 Silverlight 网站,以下载并安装。通过将 Silverlight.createObjectEx 的 inplaceInstallPrompt 参数设置为 true,可使用户不必离开网页就能下载并安装 Silverlight,改善一下体验。但是这还不够,尤其是页面的大部分或所有内容均涉及 Silverlight 时。
图 1 默认的 Silverlight 安装体验 (单击该图像获得较大视图)
Microsoft 最近发布了一个所有使用 Silverlight 的开发人员都需要阅读的文档:《Silverlight Installation Experience Guide》(Silverlight 安装体验指南)。可从 go.microsoft.com/fwlink/?LinkId=106023 下载该文档以及示例代码。文档概述了没有安装 Silverlight 时,在 Silverlight DIV 中显示 HTML 内容(包括“获得 Microsoft Silverlight”按钮和特定于浏览器的说明)以及安装 Silverlight 时,显示 XAML 内容的通用技术。其思路是为用户描绘出安装 Silverlight 后可以看到的内容,并且希望能促进用户单击按钮。
本栏目附有名为 RevolvingAuto 的示例应用程序,它展示了在此出现的一些最佳实践,包括如何构建更好的安装体验。我将向您简要显示此源代码的关键部分。
RevolvingAuto 对于其安装体验是最值得注意的。图 2 显示了没有安装 Silverlight 的访问者所看到的主页。如果已安装 Silverlight,则“Get Microsoft Silverlight”(获得 Microsoft Silverlight)按钮之后是着色的。因为对 Silverlight.createObjectEx 的调用包含一个 inplaceInstallPrompt="true" 参数,所以用户安装 Silverlight 时不必离开网页。更可贵的是,主页包含完成安装后创建 Silverlight 控件的逻辑,因此不必再手动刷新。(此功能在 Internet Explorer® 中运行良好,因为它不必在安装 Silverlight 后重新启动。但是,它在需要重新启动的浏览器中无法发挥作用)。
图 2 安装 Silverlight 前的 RevolvingAuto (单击该图像获得较大视图)
Default.html 中有改善安装体验的 JavaScript(请参见图 3)。要创建 Silverlight 控件,大部分 Silverlight 应用程序都构造代码和标记,如下所示:

Figure 3 RevolvingAuto 应用程序—Default.html
<html >
<head>
<title>Revolving Auto</title>
<script type="text/javascript" src="Silverlight.js"></script>
<script type="text/javascript" src="Default.html.js"></script>
<style>
.AgInstalled {
margin-left: auto; margin-right: auto;
width: 612px; height: 700px; text-align: left }
.AgNotInstalled {
margin-left: auto; margin-right: auto;
width: 612px; height: 700px; text-align: left;
background-image: url(Images/Install.jpg);
background-repeat: no-repeat; padding-top: 100px }
</style>
</head>
<body style="background-color: black; text-align: center">
<div style="height: 40px"></div>
<div id="Container" class="AgInstalled">
<div id="SilverlightPlugInHost" style="padding-left: 200px">
</div>
</div>
<script type="text/javascript">
var _id;
if (!Silverlight.isInstalled('1.0'))
{
document.getElementById('Container').className =
'AgNotInstalled';
_id = window.setTimeout('checkInstall()', 3000);
}
else
document.getElementById ('SilverlightPlugInHost')
.removeAttribute('style');
createSilverlight();
function checkInstall()
{
if (Silverlight.isInstalled('1.0'))
{
window.clearInterval(_id);
document.getElementById('Container')
.className = 'AgInstalled';
document.getElementById ('SilverlightPlugInHost')
.removeAttribute('style');
createSilverlight();
}
}
</script>
</body>
</html>
<div id="SilverlightPlugInHost">
<script type="text/javascript">
createSilverlight();
</script>
</div>
而 RevolvingAuto 采用如下方式进行构造:
<div id="Container">
<div id="SilverlightPlugInHost">
</div>
</div>
<script type="text/javascript">
// Code to create the control
</script>
<script> 元素内的代码调用 Silverlight.isInstalled(在 Silverlight.js 中与 Silverlight.createObjectEx 一起执行)确定是否安装了 Silverlight。如果已经安装,则脚本调用 createSilverlight 创建控件。如果没有安装,则脚本动态设置包含 Silverlight DIV 的 DIV 模式以显示背景图像,然后调用 createSilverlight 来显示“Get Microsoft Silverlight”(获得 Microsoft Silverlight)按钮。背景图像是图 2 中按钮之后的一个图像。
如 果确定 Silverlight 没有安装,则创建脚本还会使用 window.setTimeout 来设置对名为 checkInstall 的本地函数每三秒调用一次。安装完成后,checkInstall 删除背景图像,清除定时器以使函数不再调用,并且再次调用 createSilverlight 来创建 Silverlight 控件。
不要让用户等待
Silverlight 1.0 的特点是让您拥有丰富的图像、音频和视频媒体体验。但是这些文件会很大(非常大!),如果在 XAML 中声明 Image 或 MediaElement 对象,并声明媒体与它们配合,则下载时间会非常长。问题是必须先下载所有 XAML 及其引用的资源,然后 Silverlight 才能显示一个 XAML。这样用户就会产生疑惑,究竟在进行什么操作或到底有没有操作发生。如果用户只等待了几秒钟,不会有太大问题,但是如果等待时间以分计,则用户就 可能放弃并转到更好的站点。
这也是为什么设计出色的 Silverlight 应用程序不静态链接媒体资源的原因,相反,它们使用内置的 Silverlight 下载器对象,它搭载在浏览器的 XmlHttpRequest 堆栈上,异步下载这些资产。不采用这种操作
<MediaElement Source="FunnyVideo.wmv" />
您 选择的是声明没有 Source 属性的 MediaElement,可防止 Silverlight 控件在渲染任何 XAML 前等待视频下载。然后创建一个下载器对象并异步下载视频,在下载完成后调用 MediaElement 的 SetSource 方法,将视频传递到 MediaElement。如果愿意,还可在此更新进度指示器。下载器通过激发 downloadProgressChanged 和 Completed 事件在执行过程中予以协助。
Silverlight 还使下载多个资产比仅下载一个资产更为容易。只需将所有资产(音频、视频、图像,甚至 XAML 或 XML)打包到一个 ZIP 文件中,并使用下载器下载它即可。.然后,在调用 SetSource 将内容分配到对象时,在第二个参数中指定 ZIP 文件中文件的名称。这是最简捷的方法。
示例如下:
downloader.open('GET', 'Assets.zip');
downloader.send();
...
// In the Completed event handler
_media.setSource(sender, 'FunnyVideo.wmv');
RevolvingAuto 应用程序使用下载器对象异步下载包含 105 个图像文件的 ZIP 文件。这些文件共同组成了以 3.5 度递增的 360 度汽车旋转视图。在运行时,应用程序弹出和弹入图像以旋转汽车。
DownloadProgressChanged 事件处理程序更新进度条,它只是随下载进行增加宽度的一个 XAML 矩形。Completed 事件处理程序将下载的图像分配给动态创建的 Image 对象。根据本文第一部分引用的性能文档的说明,它还会隐藏进度条并注销两个下载器事件处理程序。
在 不让用户等待这一特性上,大多数浏览器都在同一线程中驱动 UI 并执行 JavaScript。因此,如果 JavaScript 任务运行时间长(例如一些要花费 5 或 10 秒钟来执行的数字处理),UI 将在这段时间内无响应。前面的性能技巧文档建议将运行时间长的 JavaScript 任务分割成更短小的任务。在 Silverlight 2.0 中,可以将运行时间长的任务委派给后台线程。但这不是版本 1.0 的选项。
使用 CreateFromXaml 减小 XAML 的大小
示例应用程序的另一个特色是它如何使用 CreateFromXaml 来动态创建 XAML Image 对象,而不是用多个几乎相同的 Image 声明搅乱 XAML 文件。在图 4 中看到的 XAML 文件 (Scene.xaml) 几乎是空的。它声明了几个 Canvase 和 Rectangle、一个 Storyboard 和一个 ScaleTransform,但是没有声明一个单个的 Image 对象。而是用 Default.html.js 中的 for loop 创建了 105 个 Image 对象,动画中每个图像一个,并从下载器用图像位加以初始化(请参见图 5)。

Figure 5 RevolvingAuto 应用程序 —Default.html.js
function createSilverlight()
{
Silverlight.createObjectEx({
source: 'Scene.xaml',
parentElement: document.getElementById('SilverlightPlugInHost'),
id: 'SilverlightPlugIn',
properties: {
width: '900',
height: '700',
background:'black',
isWindowless: false,
inplaceInstallPrompt: true,
version: '1.0'
},
events: {
onError: null,
onLoad: null
},
context: null
});
}
var _zoom = 1; // Zoom factor
var _index = 0; // Image index
var _progressBar; // Progress bar
var _progressBarContainer; // Progress bar container
var _progressBarCanvas; // Progress bar canvas
var _progressBarWidth; // Full width of progress bar
var _control // Silverlight control
var _transform; // Zoom transform
var _timer; // Timer storyboard
var _token1, _token2; // Event handler tokens
var _images = new Array(36); // References to XAML images
var _photos = 105; // Number of photos
function onLoaded(sender)
{
// Initialize XAML references and other variables
transform = sender.findName('ZoomTransform');
timer = sender.findName('TimerStoryboard');
progressBar = sender.findName('ProgressBar');
progressBarContainer = sender.findName('ProgressBarContainer');
progressBarCanvas = sender.findName('ProgressBarCanvas');
progressBarWidth = _progressBarContainer.width - 4;
control = sender.getHost();
var downloader = _control.createObject('downloader');
token1 = downloader.addEventListener('downloadProgressChanged',
downloadProgressChanged);
token2 = downloader.addEventListener('completed', downloadCompleted);
downloader.open('GET', 'Assets/AutoPhotos.zip');
downloader.send();
}
function downloadProgressChanged(sender, args)
{
// Update the progress bar
_progressBar.width = _progressBarWidth * sender.downloadProgress;
}
function downloadCompleted(sender, args)
{
// Hide the progress bar
progressBarCanvas.visibility = 'Collapsed';
// Deregister downloader event handlers
sender.removeEventListener('downloadProgressChanged', _token1);
sender.removeEventListener('completed', _token2);
// Create XAML images and assign downloaded bits to them
var canvas = sender.findName('AutoCanvas');
for (i=0; i<_photos; i++)
{
var xaml =
'<Image Canvas.Left="225" Width="450" Visibility="Collapsed" />';
var image = _control.content.createFromXaml(xaml);
image.setSource(sender, (i + 1).toString() + '.JPG');
canvas.children.add(image);
images[i] = image;
}
// Register mousewheel event handler
if (window.addEventListener)
window.addEventListener('DOMMouseScroll', onMouseWheelTurned, false);
else
window.onmousewheel = document.onmousewheel = onMouseWheelTurned;
// Make the first image visible
images[0].visibility = 'Visible';
// Start rotating
timer.begin();
}
function onTick(sender, args)
{
// Hide the current image
images[_index].visibility = 'Collapsed';
// Update the image index
if (++_index == _photos)
index = 0; // Wrap around
// Show the new image
images[_index].visibility = 'Visible';
// Restart the timer
timer.begin();
}
function onMouseWheelTurned(event)
{
var delta = 0;
if (!event) // Internet Explorer
event = window.event;
if (event.wheelDelta) // Internet Explorer
{
delta = event.wheelDelta;
if (window.opera)
delta = -delta;
}
else if (event.detail) // Mozilla
delta = -event.detail;
if (delta != 0)
{
if (delta > 0)
{
// Zoom in
zoom = Math.min(2, _zoom + 0.05);
}
else
{
// Zoom out
zoom = Math.max(1, _zoom - 0.05);
}
transform.scaleX = _transform.scaleY = _zoom;
}
if (event.preventDefault)
event.preventDefault();
event.returnValue = false;
}

Figure 4 RevolvingAuto 应用程序—Scene.xaml
<Canvas
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Loaded="onLoaded">
<Canvas x:Name="ProgressBarCanvas"
Canvas.Left="260" Canvas.Top="100">
<Rectangle x:Name="ProgressBarContainer" Canvas.Left="0"
Canvas.Top="0" Width="380" Height="12" Stroke="#606060" />
<Rectangle x:Name="ProgressBar" Canvas.Left="2" Canvas.Top="2"
Width="0" Height="8" Fill="#303030" />
</Canvas>
<Canvas x:Name="AutoCanvas">
<Canvas.Resources>
<Storyboard x:Name="TimerStoryboard" Duration="0:0:0.2"
Completed="onTick" />
</Canvas.Resources>
<Canvas.RenderTransform>
<ScaleTransform x:Name="ZoomTransform" CenterX="450" />
</Canvas.RenderTransform>
</Canvas>
</Canvas>
尽可能减小 XAML 文件是明智之举,它可以缩短应用程序的加载时间。并且速度不会减慢。(分析 XML 是一定要花费代价的)。只要重复声明 XAML 对象,就可考虑使用 CreateFromXaml。
为了具有可达性,支持交互式缩放
"Section 508" 目前是可用性的一个时髦用语并且有充分的理由:它清楚解读了 U.S. 联邦要求,要让有残疾的人员可以使用软件。如果还没要求您达到 "Section 508" 的标准,并且您生活在美国,您会看到,可能很快就迫切地需要了解它的内容。(有关 "Section 508" 的详细信息,请参阅 section508.gov)。
"Section 508" 涵盖的范围非常广,例如通过彩色编码传达的信息还必须能以其他方式表达(例如,通过标签或弹出工具提示)、规定页面闪烁频率的上限和下限等等。非常奇怪的 是,它没有指定能帮助弱视用户的最小字体大小。在我这个年龄,看小号字都需要用放大镜了,因此对文字的大小十分看重。所以在我创作的 Silverlight 应用程序中,我经常不断地创建缩放功能。幸运的是,Silverlight 为我助上了一臂之力。
Silverlight ScaleTransforms 可用于缩放任何 XAML 对象,包括包含其他对象的 Canvas 对象。实现交互式缩放很简单,只需声明一个 ScaleTransform 并根据鼠标滚轮操作或其他事件修改其 ScaleX 和 ScaleY 属性即可。要缩放整个 Silverlight 显示,只需将 ScaleTransform 应用到根 Canvas。
第一次显示 RevolvingAuto 主页时,旋转的汽车图像相对较小。但是,可以通过滚动鼠标滚轮将显示放大两倍(请参见图 6)。在图 6 (Default.html.js) 的 onMouseWheelTurned 函数中可找到关键代码。它做一些工作来考量浏览器报告鼠标滚轮事件时所用方式的差异,然后增加或减少 ZoomTransform 的 ScaleX 和 ScaleY 属性。
图 6 缩放前后的 RevolvingAuto (单击该图像获得较大视图)
注意:在放大旋转汽车时没有损失分辨率。这是因为下载的图像是所分配的 Image 对象大小的两倍。放大时,ScaleTransform 增加了当前显示的 Image 对象的尺寸,允许 Image 对象更真实地显示它表达的信息。
将 Storyboards 用于手工动画
Silverlight 一个最酷的功能是它充分支持动画。用几行 XAML 就可以让对象淡入淡出、沿页面缩放以及弹入和弹出视图。但是不能为任何东西制做动画,至少不能声明动画。为数字属性、“颜色”属性和“点”属性制做动画很 容易。但是,如果要通过随时间变化改变图像的“来源”属性来制做图像动画(在每个瞬间将图像变成另一个),则必须编写一些代码,并且该代码的结构将影响动 画的质量。
Silverlight 1.0 缺少一个明确的定时器对象,并且 window.setTimeout 对于动画不太理想。这也是聪明的 Silverlight 开发人员在需要可编程定时器时使用 Storyboard 对象的原因。您所要做的只是在 XAML 中声明一个空的 Storyboard 对象,并为它的完成事件指派一个处理程序,如下所示:
<Storyboard x:Name="Timer"
Duration="0:0:0.05"
Completed="onTick">
</Storyboard>
准备开始制做动画时,调用 Storyboard 的 begin 方法:
最后,在 Completed 事件处理程序中,执行必要的操作(例如,修改 Image 对象的 Source 属性)并调用 begin 来再次启动定时器运行:
function onTick(sender, args)
{
// TODO: Do work
_timer.begin();
}
在本例中,_timer 是一个变量,引用名为 "Timer" 的 XAML Storyboard 对象。
它正是 RevolvingAuto 用于渲染旋转动画的技术。图 5 中的 onTick 函数被调用来响应 Storyboard.Completed 事件,通过关闭上一图像的 Visibility 属性(折叠)并打开下一图像的 Visibility 属性(可见),显示下一帧动画。
使用 Tag 属性存储单个对象数据
Silverlight 应用程序创建的每个 XAML 对象都具有名为 Tag 的读/写属性,可用于存储用户定义的字符串。Tag 属性提供强大并且易于使用的方法,用于将任意数据与 XAML 对象相关联。
我 和我们团队的成员已经以多种方式使用过 Tag。有一个项目要求让用户能在 Silverlight 渲染的内容中进行搜索,我们在页面的 XAML 对象中嵌入关键字并建立了一个搜索功能,至上而下扫描 XAML,检查每个对象的 Tag 属性。在另一个实例中,我们使用 Tag 将元数据包含在 XAML 元素中。在运行时,我们使用以名称/值配对形式存储的元数据,围绕 XAML 文档中的 XAML 元素来构建更复杂的 XAML。例如,为应用程序构建内容的设计器可用以下方式声明 Image 对象:
<Image Source="ProductDemo.jpg"
Tag="Mode:Lightbox" />
应用程序应看到 "Mode:Lightbox" 并为图像加上 Lightbox 效果,使用户能够执行缩放、平移图像以及更多操作。
Tag 属性一个非常实用的功效是作为一种为图像附加描述的方式。设想一个包含几百个图像的画布,它要求在光标停留在一个图像上时弹出图像的描述。可以使用 Tag property 将描述附加到每个图像,并在弹出描述前读取光标下图像的 Tag 属性。
将 ASP.NET AJAX 用于服务器回调
开 发人员有时会对 Silverlight 1.0 的网络堆栈功能如此有限感到遗憾。除下载器对象外,版本 1.0 在网络方面作用不大。Silverlight 2.0 具有丰富的多协议网络堆栈,使得网络功能大为改观。不过在此之前,可以使用 ASP.NET AJAX 将来自 Silverlight 应用程序托管浏览器的调用发送到服务器。
我 最近发布的 MyComix comic-cataloging 应用程序中的 Silverlight 查看器提供了一个强有力的示例。要亲身体验,请键入 URL mycomix.wintellect.com/Spotlight.aspx?Item=1166 并等待漫画封面出现。然后将光标移到窗口的上半部,一个信息面板将滑入视图中,显示有关漫画的信息,包括标题、刊号、等级以及出版年份。面板中的数据来自 于存储在服务器上的数据库,并通过调用 ASP.NET AJAX Web 服务进行检索。
MyComix.asmx Web 服务公开一个名为 GetComicInfo 的方法,它接受漫画 ID 作为输入参数并返回包含漫画所有相关可用信息的 ComicInfo 对象。(ASP.NET AJAX Web 服务基本上是一个传统的 ASMX,所带的 Web 服务类属于 ScriptService 而不是 WebService。)查看器页面声明对 Web 服务的引用,如下所示:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Services>
<asp:ServiceReference Path="~/MyComix.asmx" />
</Services>
</asp:ScriptManager>
然后,激发一个对 Web 服务的 GetComicInfo 方法的异步调用,如下所示:
MyComixScriptService.GetComicInfo(_id, OnGetComicInfoCompleted);
调用在网络中效率极高,因为它使用 JavaScript Object Notation (JSON) 编码且没有视图状态或其他不必要的数据传输。并且由于调用是异步的,浏览器不会因等待其完成而冻结。如果用户在调用返回前显示信息面板,则面板只显示空的内容。
可 以从 wintellect.com/Downloads/MyComix.zip 下载 MyComix 源代码,其中包括 ASP.NET AJAX Web 服务。Silverlight 查看器可以在名为 Spotlight.aspx 和 Spotlight.xaml 的文件中找到。
代码下载 (33860 KB)