Silverpath 我的Silverlight开发心得 [1] 动态加载XAP时出现的致命错误
有哪些朋友现在在用Silverlight程序引导启动另一个Silverlight?如果你是其中的一员,你可能难免碰到了如下的诡异问题。如果你不是,也许你会在读完本文后放下js + xaml的Loader...改用Silverlight做引导 :)
首先简单说一下背景,最近做了几个项目的部分内容,发现XAP包很大,让那个蓝色的Loading小圈圈转来转去很不友好。于是就考虑写一个加载程序,界面美观,进度条优雅,速度流畅。于是便按照传统的方法进行加载:
1)使用WebClient下载某XAP
2)(可选)通过分析配置文件得到Dll列表
3)加载每个Dll
4)在MainPage所在Dll加载过程中使用反射创建MainPage的实例
5)将其放到当前的Grid/Page里
写起来不难,如果写过的朋友请跳过下一段,直接进入诡异的问题部分。 如果没有写过的朋友,可以借鉴下面的代码作为铺垫&入门~
----------------------------------可看可不看的----例子部分分割线-----------------------------------------
1)新建项目
添加一个项目叫LargeXAP,别忘了往里面放一个视频。。为了增加大小,将这个视频设定成Resource 资源,编译。
另外一个是我们要真正启动的。不要忘记设定LoaderSampleTestPage.html为启动页,这样才能用Loader引导。做完这一切准备工作后我们就可以进入代码了
2)XAML代码部分
双击打开Loader的MainPage.xaml,添加如下代码到LayoutRoot节点里
<TextBlock x:Name="_progress" FontSize="30" Text="0%"/>
这样我们就创建好了所有需要的XAML... 因为只是为了后面的恐怖问题做Demo,所以没有任何Silverlight的Cool炫效果。当然大家可以自己加入。
为了看出我们的程序是否被加载,我们需要打开LargeXAP的XAML并加入如下代码:
<MediaElement Source="Wildlife.wmv" AutoPlay="True"/>
我们嵌入的WMV文件就能在这里得到体现,我们也能清楚地知道LargeXAP程序是否被启动了
3)C#代码部分
进入Loader的MainPage.xaml.cs,修改代码到如下状态
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; namespace LoaderSample { public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); this.Loaded += new RoutedEventHandler(MainPage_Loaded); } void MainPage_Loaded(object sender, RoutedEventArgs e) { WebClient _wc = new WebClient(); _wc.DownloadProgressChanged += new DownloadProgressChangedEventHandler(_wc_DownloadProgressChanged); _wc.OpenReadCompleted += new OpenReadCompletedEventHandler(_wc_OpenReadCompleted); _wc.OpenReadAsync(new Uri("LargeXAP.xap", UriKind.Relative)); } void _wc_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) { _progress.Text = e.ProgressPercentage.ToString() + "%"; } void _wc_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e) { System.Windows.Resources.StreamResourceInfo Info = new System.Windows.Resources.StreamResourceInfo(e.Result, "application/binary"); AssemblyPart AP = new AssemblyPart(); System.Reflection.Assembly Asm; System.Windows.Resources.StreamResourceInfo streamInfo = Application.GetResourceStream(Info, new Uri("LargeXAP.dll", UriKind.Relative)); Asm = AP.Load(streamInfo.Stream); this.Content = (Asm.CreateInstance("LargeXAP.MainPage") as FrameworkElement); } } }
关键语句解释:
System.Windows.Resources.StreamResourceInfo Info = new System.Windows.Resources.StreamResourceInfo(e.Result, "application/binary");
将下载得到的XAP文件流转换为Silverlight支持的资源信息,其类型为程序文件,application/binary |
AssemblyPart AP = new AssemblyPart();
动态创建一个应用程序组件,可以表示一个Dll的存在并加载它 |
System.Reflection.Assembly Asm;
准备应用程序集对象,为了后面的反射操作做准备 |
System.Windows.Resources.StreamResourceInfo streamInfo = Application.GetResourceStream(Info, new Uri("LargeXAP.dll", UriKind.Relative));
创建另一个资源流,对前面的资源流进行进一步读取,读取到的是XAP内部的LargeXAP.dll |
Asm = AP.Load(streamInfo.Stream);
将DLL的资源流读取到程序组件中,同时返回一个更新了的当前程序集(Asm). |
this.Content = (Asm.CreateInstance("LargeXAP.MainPage") as FrameworkElement);
通过反射创建一个MainPage对象,并将其放置到当前Page上以完成Load进程 |
我们已经完成了这个Demo,运行结果如下:
载入中:
载入完成
------------------------------------------入门例子完---------------------------------------------------
一切看来很好,但事情总有特殊情况。错误的发生有时候是发生在意料之外的。如果有这样一个应用程序,它使用了App.xaml作为载体存储了大量的Resource,也就是App.Resources 中存储了大量的样式,使用这个Loader就会出现很重大的问题:
我们来修改LargeXAP中的App.xaml 使其拥有一个Resource,
<Application.Resources> <SolidColorBrush Color="Blue" x:Key="SColorBrush_"/> </Application.Resources>
可以看到我增加了一条Resource记录,这个时候我们修改LargeXAP中MainPage.xaml,使其能显示这个颜色
<Gridx:Name="LayoutRoot"Background="{StaticResourceSColorBrush_}">
<MediaElementSource="Wildlife.wmv"AutoPlay="True"/>
</Grid>
在添加了绑定后,运行LargeXAP项目,[不是Loader],查看结果,会发现屏幕边上出现了蓝色的底色,这说明我们的Resource被调用了。
这个时候再运行Loader,就会出现错误:
我们能发现VS会自动把Debug引入到LargeXAP中,并告诉我们没有找到资源。
凭经验大家可能和我想的一样,可能的解决方案就应该是:
让App.xaml也Load到刚才那个程序集里面,而不是只创建MainPage的实例
于是我就按照这个线索修改了Loader MainPage.xaml.cs中的代码, 尝试创建一个App的Instance,这样资源就应该都Load进来了:
Application _a= (Asm.CreateInstance("LargeXAP.App") as Application); this.Content = (Asm.CreateInstance("LargeXAP.MainPage") as FrameworkElement);
这个时候运行了程序
错误再次出现 而且内容相同!
也就是说我们创建的App并无起到作用,到底起到没有?设定断点调试。
仔细看看_a的状态,就可以发现,所有的资源,都已经成功的加载了。可为什么还是无法找到呢?
仔细思索了Silverlight程序默认的开始方式,发现Silverlight本身并无一种机制让多个App同时运行,也就是说SL程序应该是地地道道的单实例应用程序
可是这里我们用了不寻常的方法强行Create了一个App,难道这个App的产生会对我们当前的App,造成一些毁灭性的影响?
同时,追溯Resource绑定的根源,其原理也是向父容器中提交查询,将合乎Key名的资源索引过来。如果父容器其中没有,则在App中索引,这样App就成了全局资源库[顺理成章的]。换句话说,App成为资源库的前提,是成为所有UIElement的Parent, 也是最高级Parent. 在初始化中我们能看到RootVisual这个东西被设定,也就是App的唯一下级了。那么如此推断,唯一的可能就是,这个新创建的App没有与我们当前操作的所有UIElement构成关系,造成了意外的孤立。
大家可能也想到了,如果使用Application.Current获取当前的活动App, 创建一个App对其资源进行遍历,再写入到Current中,不就合乎要求了?
于是我们修改代码:
Application _a= (Asm.CreateInstance("LargeXAP.App") as Application); foreach (var Res in _a.Resources.Keys) { Application.Current.Resources.Add(Res, _a.Resources[Res]); } this.Content = (Asm.CreateInstance("LargeXAP.MainPage") as FrameworkElement);
但这段看似有理的代码却得到了如下的结果:
重复键?跟踪Application.Current发现,当前App中资源是存在的,也就是Load进来了。可是为什么无法对MainPage造成影响呢??
想来想去,想到了重要的问题, Silverlight并没有多App并存的机制,那么当App被反射创建也就是不正常的创建的时候,一个默认逻辑会被调用,Application.Current会变更成刚刚创建的这个App,导致我们无法访问原来的有意义的App.
于是我尝试了将源代码段修改成如下诡异的代码段:
Application _real = Application.Current; Application _a= (Asm.CreateInstance("LargeXAP.App") as Application); foreach (var Res in _a.Resources.Keys) { _real.Resources.Add(Res, _a.Resources[Res]); } this.Content = (Asm.CreateInstance("LargeXAP.MainPage") as FrameworkElement);
仔细看看,其实很简单,在创建App之前缓下一个对象,到时候操作这个缓存的App——原来的有意义的App 即可
这个文章的目的是少让大家走弯路了~
说的不对,大家多多指正,
如果没什么特别多的技术错误,我就继续写这个系列,把我的心得共享出来~ 也谢谢大家支持!
欢迎大家线上线下交流~ 互相学习~
--------------------------------
Mike Luan
15801397431
--------------------------------
提供Silverlight解决方案 和Silverlight项目承包