超酷代码
针对 Silverlight 开发的 3 个重要技巧
Jeff Prosise
代码下载可从 MSDN 代码库
浏览代码联机
内容
按需程序集加载
实时呈现
避免区域的依赖项
打开新页
我撰写本文,Silverlight 2 是在按下禁用热,并开发人员都获得第一个看什么许多认为表示 Web 编程的未来。无论您是 Silverlight proponent 中竞争的技术,如 Adobe 弹性找到更多的 allure,很令人兴奋若要查看替代 JavaScript,HTML,和 AJAX 我们可以看到获得 mindshare 用于创建 Web 应用程序。并与 Microsoft 已经硬盘工作 Silverlight 3,将来有永远不会似乎变亮。
适用的任何平台,成为 Silverlight 开发人员在路途不没有几个 potholes。您知道,是例如 XamlReader.load 正常测试,在美国的 PC 上的多个调用会失败,在其他国家 / 地区的 PC 上吗?未实现的 Silverlight 的呈现引擎 intimately 到 UI 线程并且此事实 profoundly 会影响您的代码的结构?您知道您可以通过动态加载程序集,减少 XAP 文件的大小,但是这样不会丢失的强类型化优点需要知识库的 CLR 内部吗?如果此 intrigues 您,阅读。我有一些提示并共享的技巧将进行 Silverlight 的生命稍低 bumpy,并让您一个好并且多得到的通知 Silverlight 程序员,太。
有关更多信息打包更快的传递的 Silverlight 内容,请参阅在1 月 2009 期,领先的.
按需程序集加载
精心设计的 Silverlight 应用程序的 hallmarks 之一是以前称为应用程序包小 XAP 文件。XAP 文件作为嵌入的资源 (尤其是图像) 和程序集引用的结果的所有太通常 swell 到 unmanageable 的大小。大 XAP 文件,在长花下载,如果它增长太大,Silverlight 可能无法加载它。
更大的应用程序可以将打包在小 XAP 文件中,如果您仔细考虑资源和应用程序使用的程序集,并保留那些可以延迟加载或下载根据 Web 服务器上。您可以使用 WebClient 或其他类 Silverlight 的网络堆栈中下载的其他资源和程序集在下载应用程序包。通常最好上获取应用程序的 UI 并运行快速而且然后启动异步网络请求其他资产需要比发布 100MB XAP 文件和强制用户花五分钟等待到达 100%的进度指示器。
请求资源在 Silverlight 中加载往往是简单和简单。图 1 中的代码段是例如下载 JPEG 在原站点的部署中,并通过将下载的位分配给名为 MyImage 一个 XAML 图像显示。
图 1 从原始格式的站点下载图像
WebClient wc = new WebClient();
wc.OpenReadCompleted +=
new OpenReadCompletedEventHandler(wc_OpenReadCompleted);
wc.OpenReadAsync(new Uri("JetCat.jpg", UriKind.Relative));
...
void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error == null)
{
BitmapImage bi = new BitmapImage();
bi.SetSource(e.Result);
MyImage.Source = bi;
}
}
按需集加载会但是应更加困难。 乍一似乎简单: 使用 WebClient 下载程序集和 AssemblyPart.load 其加载到在 Appdomain。 问题是 Silverlight 的 JIT 编译器可以获取导致许多开发人员认为它是无法下载请求的程序集,并享受强类型,过的好处,种方法中。 在现实,但是,您可以执行两。 但您需要知道您正在执行和具有程序集加载 CLR 中的工作方式基本了解。
为了说明,请看下列代码:
private void CreateCalendarButton_Click(object sender, RoutedEventArgs e)
{
Calendar cal = new Calendar();
cal.Width = 300.0;
cal.Height = 200.0;
cal.SelectedDatesChanged += new
EventHandler<SelectionChangedEventArgs>(cal_SelectedDatesChanged);
LayoutRoot.Children.RemoveAt(0);
LayoutRoot.Children.Add(cal);
}
图 2 保持超出该 XAP 程序集
它是一个按钮单击处理程序动态创建一个日历控件,并将其添加 XAML 场景到。 (它也会删除触发该事件假定为 LayoutRoot 的子级集合中的该 0th 项的按钮)。 因为日历实现在 System.Windows.controls.dll 这不嵌入插件 silverlight 中但属于而该扩展的 BCL 中此代码运行良好,只要将对 System.Windows.controls.dll 引用添加到项目。 该引用导致 System.Windows.controls.dll XAP 文件中包含并自动载入该 Appdomain。
现在假设您想来将智能化并只加载 System.Windows.controls.dll 如果它的需要,这就是如果用户单击按钮。 因此您将对 System.Windows.controls.dll 引用添加到该项目以满足编译器 (否则,编译器不会编译对日历因为编译器不知道日历类型是),并且,Visual Studio 的属性窗口中中, 您将 System.Windows.controls.dll 的 Copy Local 属性设置为 False,以防止它被嵌入到 XAP 文件中,(如我那样 图 2 中)。
下一步,您部署 System.Windows.controls.dll 的副本与服务器上应用程序的 ClientBin 文件夹中 XAP 文件。 最后,您重组您的代码,如 图 3 所示。 按钮单击处理程序现在下载 System.Windows.controls.dll 从 Web 服务器、 将其加载到与 assemblypart.load,appdomain 和实例化一个日历控件。
图 3 按需集加载的 Doesn't 工作
private void CreateCalendarButton_Click(object sender, RoutedEventArgs e)
{
WebClient wc = new WebClient();
wc.OpenReadCompleted +=
new OpenReadCompletedEventHandler(wc_OpenReadCompleted);
wc.OpenReadAsync(new Uri("System.Windows.Controls.dll",
UriKind.Relative));
}
void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error == null)
{
// Load the downloaded assembly
AssemblyPart part = new AssemblyPart();
part.Load(e.Result);
// Create a Calendar control
Calendar cal = new Calendar();
cal.Width = 300.0;
cal.Height = 200.0;
cal.SelectedDatesChanged += new
EventHandler<SelectionChangedEventArgs>(cal_SelectedDatesChanged);
LayoutRoot.Children.RemoveAt(0);
LayoutRoot.Children.Add(cal);
}
}
它看上去合理和代码编译只是好但运行时单击处理程序生成类似 图 4 中的异常。 编译的代码是正确的。 不引发异常的代码。 因此,内容提供? 该错误消息似乎表明 CLR 试图加载 System.Windows.controls.dll,但因为您以编程方式加载它不需要。
这是案例的一个很好的示例,其中知识库的 CLR 内部可以让您更好的 Silverlight 程序员。 问题此处是当 JIT 编译器编译 wc_OpenReadCompleted 方法,它扫描方法,看到它引用一个名为日历的类型并且会尝试加载 System.Windows.controls.dll,以便可以解析该引用。
图 4 天哪 !
遗憾的是,此时即使执行该方法之前, 您不会因此获得能够调用 AssemblyPart.load。 这是一个典型鸡彩蛋问题时。 您需要调用 AssemblyPart.load 加载在的程序集,但 JIT 编译器在可以调用它之前,intervenes 的并且尝试加载它。 尝试失败,因为 System.Windows.controls.dll 不在应用程序包。
这是的点的许多程序员引发最自己动手,和结束点播集加载不在 Silverlight 工作或求助于反射来实例化日历类型:
AssemblyPart part = new AssemblyPart();
Assembly a = part.Load(e.Result);
Object cal = (Object)a.CreateInstance("Calendar");
此方法的工作但粗心。 您不能返回 assembly.createinstance 日历因为这样做这样将导致 JIT 编译器尝试加载程序集方法执行之前将引用进行转换。 并且如果您不能强制转换到日历,然后控件的方法、 属性,和事件必须通过反射,过访问。 该代码快速增长如此不实用的吸引力只中并嵌入 System.Windows.controls.dll 应用程序包和 Live 与增加 XAP 大小。
好消息是您可以结合加载的动态集和强类型。 只需重建您的代码行中的 图 5 。 观察 wc_OpenReadCompleted 不再引用日历类型 ; 所有引用已都移动到一个名为 CreateCalendar 的不同方法。 此外,CreateCalendar 属性化方式 JIT 编译器不会尝试内联方法。 (如果内联是出现您就是右重新启动因为 wc_OpenReadCompleted 将包含对日历类型隐式引用) 现在 JIT 编译器不会检查已加载 System.Windows.controls.dll,直到调用 CreateCalendar,并按该时间,您已经已加载它到在 Appdomain。
图 5 按需集加载的工作原理
private void CreateCalendarButton_Click(object sender, RoutedEventArgs e)
{
WebClient wc = new WebClient();
wc.OpenReadCompleted +=
new OpenReadCompletedEventHandler(wc_OpenReadCompleted);
wc.OpenReadAsync(new Uri("System.Windows.Controls.dll",
UriKind.Relative));
}
void wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (e.Error == null)
{
// Load the downloaded assembly
AssemblyPart part = new AssemblyPart();
part.Load(e.Result);
// Create a Calendar control
CreateCalendar();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void CreateCalendar()
{
Calendar cal = new Calendar();
cal.Width = 300.0;
cal.Height = 200.0;
cal.SelectedDatesChanged += new
EventHandler<SelectionChangedEventArgs>(cal_SelectedDatesChanged);
LayoutRoot.Children.RemoveAt(0);
LayoutRoot.Children.Add(cal);
}
见解: 呈现和用户界面线程
通知 Jeff 说的不注意大量的一个方面是 Silverlight 的在 Silverlight 中的所有呈现都完成应用程序的 UI 线程上,如果 hog UI 线程,则防止从发生的任何呈现。因为 WPF 提供了一个呈现线程,很可能意外 Silverlight 不。您可能想知道原因。
决定系统开销和 decoupling framerates 之间附到一个一权衡能力下。Silverlight,我们的线程上亮方法重量并没有不隔离应用程序代码从呈现系统。这意味着您可以执行多个将动画 (例如具有布局基于动画或运行的自定义代码) 中,并且没有最小的延迟和开销获取呈现系统。在关闭的端是如果您做过多,您可能影响视频播放等操作。
也就是说,Silverlight 呈现系统将利用多核处理,并用许多线程加速为它的呈现。因此,呈现是很少"线程上,,但它与您的应用程序以避免同步和数据的副本同步。
—Ashraf Michail,主体级结构设计版 Silverlight
顺便说一下,如果这是 Windows Presentation Foundation (WPF),而不是 Silverlight,您可以通过解决此问题更完善的方式注册 AppDomain.AssemblyResolve 事件处理程序,并有加载 System.Windows.controls.dll。在 Silverlight 中, 存在的 AppDomain.AssemblyResolve,但它的属性化这意味着用户代码不能为其注册处理程序的 SecurityCritical。
图 5 假定您包括在项目中对 System.Windows.Controls.dll,但是您设置复制本地为 False (参见 图 2 ) 和部署 ClientBin 文件夹中的程序集。证明它可以工作,下载本专栏附带 OnDemandAssemblyDemo 应用程序,然后单击该的按钮标记为日历控件。日历控件显示位置按钮。显著,OnDemandAssemblyDemo.xap 不包含您可以轻松地验证通过使用 WinZip 打开 XAP 文件的 System.Windows.controls.dll 的副本。时间 !这将是一个很好冰-器在下一个 Silverlight 方。
实时呈现
不按大量的 Silverlight 的一个方面是在 Silverlight 中的所有呈现都完成应用程序的 UI 线程上的并如果 hog UI 线程,您将防止从所都进行的任何呈现。这意味着要避免在 UI 线程上的长时间运行循环,如果您要修改该循环中的 XAML 场景,或者如果所有动画均会发生一次。
这听起来简单,避免在 UI 线程上的长时间运行循环,但实际上,它可能对您编写的代码有渊博的影响。请考虑应用程序调用 OpenFileDialogDemo, 图 6 中的说明。它将演示如何使用 Silverlight 的 OpenFileDialog 类来允许用户浏览他或她的硬盘上的图像文件然后将图像加载到 XAML 图像对象。运行应用程序,单击打开按钮在页面的顶部,选择多个图像文件 (在更大好),然后单击 OpenFileDialog 的打开按钮。
图 6 中的 OpenFileDialogDemo
您会发现一个由一个图像您选择使用与 XamlReader.load 动态创建的对象的场景到弹出,假设在页面上的随机位置。一旦该图像将显示,您可以单击他们进行到前面,,甚至使用鼠标将它们拖动在页周围。
尽管它明显的简单性 OpenFileDialogDemo 提供在审慎与 UI 线程中的实际课程。当我最初编写代码以显示该 OpenFileDialog,并加载图像文件时,我结构它类似于 图 7 中代码段。一旦用户已关闭对话框,简单的 foreach 循环将遍历所选文件,并加载逐个。
图 7 加载图像文件的简单方法
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "JPEG Files (*.jpg;*.jpeg)|*.jpg;*.jpeg|" +
"PNG Files (*.png)|*.png|All Files (*.*)|*.*";
ofd.FilterIndex = 1;
ofd.Multiselect = true;
if ((bool)ofd.ShowDialog())
{
foreach (FileInfo fi in ofd.Files)
{
using (Stream stream = fi.OpenRead())
{
BitmapImage bi = new BitmapImage();
bi.SetSource(stream);
GetNextImage().Source = bi;
}
}
}
遗憾的是,没有图像出现在屏幕上直到所有已加载。 如果用户选择一个或两个图像文件,但它是 intolerable 选定 40 或 50 个文件,延迟并不很了不起。 以是短应用程序未满足最低要求我为其,设置,因为我希望"弹出"在屏幕上为它们所加载的图像。 获取它? 弹出 ! 弹出 ! 弹出 !
该的问题当然,是在 UI 线程上运行的 foreach 循环中,并运行该循环时 Silverlight 无法呈现图像,它们已将其添加到场景,这意味着它是时间步骤后,需要一个的一和重组代码以使其能够及时地呈现图像。
图 8 显示了一个问题的解决方案。 修改后的 foreach 循环不执行任何操作多个将 FileInfo 对象添加到一个 System.Collections.generic.queue。 这使该循环,以快速地运行,并手动编写控件重新向 Silverlight 以便它可以向下呈现的业务。 或许,最值得关注的 restructured 代码是如何 dequeues 和处理响应 CompositionTarget.rendering 事件中的 FileInfo 对象。
图 8 A 更佳方法加载图像文件
private Queue<FileInfo> _files = new Queue<FileInfo>();
...
public Page()
{
InitializeComponent();
// Register a handler for Rendering events
CompositionTarget.Rendering +=
new EventHandler(CompositionTarget_Rendering);
}
...
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "JPEG Files (*.jpg;*.jpeg)|*.jpg;*.jpeg|" +
"PNG Files (*.png)|*.png|All Files (*.*)|*.*";
ofd.FilterIndex = 1;
ofd.Multiselect = true;
if ((bool)ofd.ShowDialog())
{
// Reset the queue
_files.Clear();
// Place each FileInfo in a queue
foreach (FileInfo fi in ofd.Files)
{
_files.Enqueue(fi);
}
}
...
private void CompositionTarget_Rendering(Object sender, EventArgs e)
{
if (_files.Count != 0)
{
FileInfo fi = _files.Dequeue();
using (Stream stream = fi.OpenRead())
{
BitmapImage bi = new BitmapImage();
bi.SetSource(stream);
GetNextImage().Source = bi;
}
}
}
CompositionTarget.rendering 是回一个每个帧呈现调通常用于实现游戏的循环。 从 WPF 借用并显示最迟在 Silverlight 2 开发周期。 Silverlight 已准备好 re-render 场景每次引发该事件。
OpenFileDialogDemo 注册 compositiontarget.rendering 事件 (CompositionTarget_Rendering) 处理程序,并 dequeues 一个的 FileInfo 对象将其转换为是 XAML 图像每次调用该处理程序时。 结果? 为已加载它们由于 Silverlight 现在具有可以更新以下每个新的映像的场景,图像中弹出在屏幕上。 这是 OpenFileDialogDemo 的最终版本中的 foreach 循环构造方式,并且很为什么当您在运行,您看到逐个代替同时屏幕上显示的图像。
要注意,不要 overuse CompositionTarget.rendering。 如果 OpenFileDialogDemo 有作为运行的动画它添加图像到该的场景动画可能会 stutter 因为每个帧将被延迟按照加载图像位,并将其分配给图像对象所需的时间量。 但需要、 格式,需要和 OpenFileDialogDemo 是 CompositionTarget.rendering—indeed 可接受的利用一个很好示例时, 此目标将是难否则完成。
避免区域的依赖项
最终的提示将使用 XamlReader.load 动态创建 XAML 对象。 您可以发现有何不妥此代码?
Rectangle rect = (Rectangle)XamlReader.Load(
String.Format(
"<Rectangle xmlns=\"https://schemas.microsoft.com/client/2007\" " +
"Width=\"{0}\" Height=\"{1}\" Stroke=\"Black\" Fill=\"Yellow\" />",
100.5, 100.0
)
);
如果您识别此代码在美国的大多数 PC 上运行,但将无法在欧洲和世界上的其他部分中的大多数 PC 上,授予您自己一个 pat 上。要演示,请首先配置操作系统以显示在美国的数字、 货币、 日期,和时间格式如果没有已配置这种方式。(在 Vista,转到可以通过控制面板区域和语言选项对话框的格式选项卡)。 执行 XamlReader.load 调用,并验证该调用成功执行。现在将区域的格式更改为法语,并再次执行该调用。这次 XamlReader.load 引发异常:"无效的属性值 100,5 属性宽度"( 图 9 )。问题是该十进制数字,如 100.5 写入 100,5 (请注意小数点位置的逗号) 在许多国家 / 地区。并且因为主机 PC 上 String.Format 认可的区域设置,十进制 100.5 成为"100,5。遗憾的是,XamlReader.load 不知道如何进行的"100,5",以便它将引发异常。
图 9 由 XamlReader.Load 引发的异常
下面的代码演示调用 XamlReader.load,以便它工作运行 Silverlight 的任何 PC 上正确的方法:
Rectangle rect = (Rectangle)XamlReader.Load(
String.Format(
CultureInfo.InvariantCulture,
"<Rectangle xmlns=\"https://schemas.microsoft.com/client/2007\" " +
"Width=\"{0}\" Height=\"{1}\" Stroke=\"Black\" Fill=\"Yellow\" />",
100.5, 100.0
)
);
传递给 String.Format 在第一个参数是引用固定区域性的 CultureInfo 对象。 XamlReader.load 需要区域性的不变量的字符串,以便使用 CultureInfo.InvariantCulture 可确保该 String.Format 生成格式正确的十进制值 (以及格式正确的日期和时间) 如果您使用的。 不 coincidentally 前, 一节 OpenFileDialogDemo 应用程序使用此技术确保自己对 XamlReader.load 工作无论区域设置。
如果的字符串传递给 XamlReader.load 包含小数由 String.Format,始终使用 CultureInfo.InvariantCulture 将正确格式。 您的应用程序运行在该通配符后, 就 liable 在各种区域设置下执行。
打开新页
如果您要开始使用 Silverlight,并且您是经验丰富的.NET 开发人员,您已经知道您需要了解的 90%。 但您应该了解的.NET Silverlight 使 nuances。
说的页面,许多读者已要求我要更新, Silverlight 1.0 页面打开框架在 2008 年显示 Silverlight 2。 和,迁移已完成。 您可以查看一个示例使用在更新的 Framework 的应用程序 wintellect.com/silverlight/pageturndemo/您可以下载该源代码 wintellect.com/Downloads/PageTurnDemo2.zip.
在页面打开 Framework 居住在 PageTurn.cs,和 page.xaml.cs 中的代码说明了其工作原理。 API 类似于 (在 C# 而不是 JavaScript),Silverlight 1.0 版本,并且我更改以便将更好的页面。 我提供的触发框架使您可以更新用户界面页打开已完成每次一个 PageTurned 事件假设显示当前页码。
将您的问题和意见发送 Jeff 到 wicked@Microsoft.com.
Jeff Prosise 是 MSDN Magazine 一个软件编辑器和有多部书籍,包括 Programming Microsoft.NET (2002,Microsoft Press) 的作者。 他还是 Wintellect (的创始人之一 wintellect.com),软件咨询和培训公司专门研究 Microsoft.NET 的。 与在 Jeff wicked@Microsoft.com.