完美实现Silverlight动态加载

这段时间利用项目空隙,研究了一下Silverlight的动态加载技术。动态加载分为两种:

1、网页局部加载(即一个网页上有多个Silverlight应用)

2、单个Silverlight应用动态加载(即模块分步加载)。

这里讨论的是第二种加载方式,对于这种加载模式的处理, 目前网上比较常见的方法也有两种:一种是动态加载xap包,另一种是动态加载dll, 两种方法的实现原理都是一样的(个人比较推荐前一种,因为xap是压缩包可节省带宽,而且如果需要加载多个dll时,后一种方案处理起来较为麻烦)。但是有一些细节处理让人不是很满意,比如silverlight动态加载(研究与探讨) 这篇文章,虽然实现了动态加载,但是没有很好的解决dll重复引用的问题。本文将在前人研究的基础上,做些总结和改进,提供一个较为完美的解决方案。

一、认识XAP包

使用VS新建一个名为MainApp的Silverlight应用程序,使用默认设置生成一个对应的MainApp.Web工程。完成编译,这时会在对应的Web工程的ClienBin目录下生成MainApp.xap文件(实际上是一个zip包)

 

 

 

 

 

 

使用工具打开xap:

 

 

 

 

 

 

可以看到xap包中有一个AppMainfest.xaml文件和一个dll文件,我们着重介绍一下AppMainfest.xaml文件。使用文本编辑工具打开这个文件:

<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
EntryPointAssembly="MainApp" EntryPointType="MainApp.App" RuntimeVersion="5.0.61118.0">
  <Deployment.Parts>
    <AssemblyPart x:Name="MainApp" Source="MainApp.dll" />
  </Deployment.Parts>
</Deployment>

Deployment结点中描述了应用程序入口程序集名称,入口类型,运行时版本。Deployment.Parts下描述了各个引用的程序集名称及dll所在位置。我们新建一个Model工程,并在MainApp中引用,再次编译,这次生成的XAP包中又多了Model的引用。

 

 

 

 

 

 

 

 

再次打开AppMainfest.xaml :

<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
EntryPointAssembly="MainApp" EntryPointType="MainApp.App" RuntimeVersion="5.0.61118.0">
  <Deployment.Parts>
    <AssemblyPart x:Name="MainApp" Source="MainApp.dll" />
    <AssemblyPart x:Name="Model" Source="Model.dll" />
  </Deployment.Parts>
</Deployment>

对应的AssemblyPart也对应的增加了,不难想象如果在MainApp中引用了很多dll,那么最终生成的xap包的体积也会变的很大,造成用户访问程序时加载速度很慢。因为即使有些dll在一开始并没有用到,也会在首次加载中一同下载到客户端。对于这种情况,有一种简单的处理方法—— 应用程序库缓存

二、应用程序库缓存(Application Libary Caching)

我们在MainApp工程中随便添加几个引用,模拟真实开发环境。重新编译后,新生成的xap包增加了800多KB。

此时,打开MainApp工程属性页面,勾选"Reduce XAP size by using application libary caching"

再次编译,ClientBin目录下生成了几个zip文件,同时这次生成的xap包了体积又降回了原来的9KB

仔细观察的话,会发现原来xap包中引用的外部dll和新生成的zip文件一一对应。而此时的AppMainfest.xaml 也发生了变化:

<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 EntryPointAssembly="MainApp" EntryPointType="MainApp.App" RuntimeVersion="5.0.61118.0">
  <Deployment.Parts>
    <AssemblyPart x:Name="MainApp" Source="MainApp.dll" />
    <AssemblyPart x:Name="Model" Source="Model.dll" />
  </Deployment.Parts>
  <Deployment.ExternalParts>
    <ExtensionPart Source="System.Windows.Controls.Pivot.zip" />
    <ExtensionPart Source="System.Windows.Data.zip" />
    <ExtensionPart Source="System.Xml.Linq.zip" />
    <ExtensionPart Source="System.Xml.Serialization.zip" />
    <ExtensionPart Source="System.Xml.Utils.zip" />
    <ExtensionPart Source="System.Xml.XPath.zip" />
    <ExtensionPart Source="System.Windows.Controls.zip" />
  </Deployment.ExternalParts>
</Deployment>

多出了Deployment.ExternalParts结点。通过这种方式,用户访问Silverlight应用时,xap下载的速度会得到改善,当程序中用到某一个外部程序集时,则会自动下载对应的Zip包到客户端,并加载其中的程序集。这样只要合理组织程序集之间的引用就可以达到提高加载速度的目的。非常方便简单。

三、动态加载XAP

有了对Xap包结构和AppMainfest.xaml 结构的初步认识之后,要实现Xap包的动态加载就比较容易了。新建一个Silverlight应用程序EmployeeDataGrid,添加一些逻辑代码和引用。之后再将工程添加到MainApp.Web工程的Silverlight Application中。

编译之后在ClinetBin目录下会生成EmployeeDataGrid.xap文件。运行MainApp.Web工程,看到下面的页面。

首页打开加载MainApp页面,点击页面上的“加载员工列表”按钮,动态加载EmployeeDataGrid.xap并初始化员工列表,代码如下:

 /// <summary>
 /// 加载员工列表按钮点击事件
 /// </summary>
 private void BtnLoadEmployeeListClick(object sender, RoutedEventArgs e)
 {
     LayoutRoot.Children.Remove(BtnLoadEmployeeList);
     LoadXapProgressPanel.Visibility = Visibility.Visible;
 
     // 下载xap包
     var xapClient = new WebClient();
     xapClient.OpenReadCompleted += new OpenReadCompletedEventHandler(ManageXapOpenReadCompleted);
     xapClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ManageXapDownloadProgressChanged);
     var xapUri = new Uri(HtmlPage.Document.DocumentUri, "ClientBin/EmployeeDataGrid.xap");
     xapClient.OpenReadAsync(xapUri);
 }
 
 /// <summary>
 /// Xap包下载完成
 /// </summary>
 /// <param name="sender"></param>
 /// <param name="e"></param>
 private void ManageXapOpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
 {
     if (e.Error == null)
     {
         // 利用反射创建页面
         Assembly assembly = XapHelper.LoadAssemblyFromXap(e.Result);
         var employeeDataGrid = assembly.CreateInstance("EmployeeDataGrid.MainPage") as UserControl;
 
         // 将列表页面加载到主页面中
         Grid.SetRow(employeeDataGrid, 1);
         LayoutRoot.Children.Add(employeeDataGrid);
 
         LayoutRoot.Children.Remove(LoadXapProgressPanel);
     }
     else
     {
         MessageBox.Show(e.Error.Message);
     }
 } 

加载Xap中程序集的代码:

/// <summary>
/// 从XAP包中返回程序集信息
/// </summary>
/// <param name="packageStream">Xap Stream</param>
/// <returns>入口程序集</returns>
public static Assembly LoadAssemblyFromXap(Stream packageStream)
{
    // 加载AppManifest.xaml
    var streamResourceInfo = new StreamResourceInfo(packageStream, null);
    Stream stream = Application.GetResourceStream(streamResourceInfo, new Uri("AppManifest.xaml", UriKind.Relative)).Stream;
    XmlReader xmlReader = XmlReader.Create(stream);
 
    // 读取程序集信息
    Assembly entryAssembly = null;
    string entryAssemblyName = string.Empty;
    var assemblyPartInfos = new List<AssemblyPartInfo>();
    while (xmlReader.Read())
    {
        switch (xmlReader.NodeType)
        {
            case XmlNodeType.Element:
                if (xmlReader.Name == "Deployment")
                {
                    // 入口程序集名称
                    entryAssemblyName = xmlReader.GetAttribute("EntryPointAssembly");
                }
                else if (xmlReader.Name == "AssemblyPart")
                {
                    var name = xmlReader.GetAttribute("x:Name");
                    var source = xmlReader.GetAttribute("Source");
 
                    assemblyPartInfos.Add(new AssemblyPartInfo { Name = name, Source = source });
                }
                break;
            default:
                break;
        }
    }
 
    var assemblyPart = new AssemblyPart();
    streamResourceInfo = new StreamResourceInfo(packageStream, "application/binary");
    // 加载程序集
    foreach (var assemblyPartInfo in assemblyPartInfos)
    {
        var assemblyUri = new Uri(assemblyPartInfo.Source, UriKind.Relative);
        StreamResourceInfo streamInfo = Application.GetResourceStream(streamResourceInfo, assemblyUri);
 
        // 入口程序集
        if (assemblyPartInfo.Name == entryAssemblyName)
        {
            entryAssembly = assemblyPart.Load(streamInfo.Stream);
        }
        // 其他程序集
        else
        {
            assemblyPart.Load(streamInfo.Stream);
        }
    }
 
    return entryAssembly;
}

至此,已经可以实现Xap的动态加载了。

四、一些细节问题

一、重复引用的程序集

让我们回过头来看看MainApp.xap和EmployeeDataGrid.xap中的程序集。

发现,MainApp.xap中已经有的MainApp.dll和Model.dll在EmployeeDataGrid.xap中重复存在了,比较不爽。有人会说只要在发布之前手动删除EmployeeDataGrid.xap中重复的dll就可以了。虽然这样是可行的,但是无疑带来了很多无谓的工作量。其实有一种非常简单的方法可以解决这一问题。只要设置EmployeeDataGrid引用工程中那些重复引用程序集的Copy Local属性为Fasle,再重新编译一下,这次生成xap包时就不会讲这些程序集压缩进去。

二、资源问题

之前看网上的资料,有人提到动态加载xap后一些用到资源的地方会有问题。为此我在Demo中做了测试。在MainApp工程中新建一个UserControl,里面放了一个TextBlock,Text属性绑定到TestControl的Text属性上。并且样式使用静态样式TestStyle(定义在MainApp工程的Resource\Style.xaml中, 在App.xaml中添加)

TestControl:

<UserControl x:Class="MainApp.TestControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
 
    <TextBlock Text="{Binding Path=Text}" Name="TxtTest" Style="{StaticResource TestStyle}" />
    
</UserControl>

Style.xaml:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 
    <Style x:Key="TestStyle" TargetType="TextBlock" >
        <Setter Property="FontSize" Value="20"/>
        <Setter Property="Foreground" Value="Blue" />
    </Style>
    
</ResourceDictionary>

这样在MainApp工程中用到TestControl没有任何问题。然而当在EmployeeDataGrid中使用TestControl时却出了问题:

因为在EmployeeDataGrid中构造TestControl时,TesrControl内部的TextBlock从当前程序集中找不到名为TestStyle的资源,所以无法完成初始化,就报错了。对于这个错误,有两个解决办法:

1、修改TestControl的代码,TestBlock对资源的应用放在后台代码中进行:

public TestControl()
{
    InitializeComponent();
    this.DataContext = this;
 
    var style = Application.Current.Resources["TestStyle"] as Style;
    if (style != null)
    {
        TxtTest.Style = style;
    }
}

2、不修改TestControl的代码,而是在EmployeeDataGrid的App中引用MainApp中的资源。

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             x:Class="EmployeeDataGrid.App">
    
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/MainApp;component/Resource/Style.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
    
</Application>

比较两种方法,第二种方法优势明显。第一种方法对于资源的引用都放在后台代码中,增加工作量,而且在设计时也看不到直观效果。所以个人推荐采用第二种方法。

五、结尾

由于xap包自身的压缩特性和AppMainfest.xaml 对程序集引用的描述全是由IDE自动生成,无需人为干预。所以通过Xap动态加载提高Silverlight应用程序加载速度(结合应用程序库缓存效果更好)实现起来简单方便,在实际项目开发中可加以借鉴。同时对于资源的处理可以根据实际项目考虑放在单独的资源工程中,方便组织管理。如果对本文有任何问题或建议,欢迎给我留言讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
通过网上的搜索,感觉Silverlight Loader挺好用的,下面详细介绍一下怎么使用Silverlight Loader。 Silverlight Loader的下载地址如下:http://silverlightloader.codeplex.com/ 其实文章并不长,只有下面红色的部分才是需要你实现的代码,这里可能不能显示颜色,可以通过以下网址查看: http://hi.baidu.com/ck436/blog/item/a736edc487d6fea58226acab.html 下面首先介绍一下加载的原理: 其实就是建二个项目:一个较小,用于加载较大的项目文件,并在加载过程中给用户显示加载进度,假设为SLLoader.xap;另一个为我们的具体功能实现项目,不妨假设为LoadTarget.xap。 客户端下载程序时,首先加载SLLoader.xap,这个过程应该是比较快的,然后由SLLoader.xap一边加载较大的LoadTarget.xap,一边显示进度或动画,当加载LoadTarget.xap完毕后,就调用打开LoadTarget.xap,至些,我们的项目已加载完毕。 关于该加载过程的详细资料,可参考如下页面: How the Silverlight loader works http://www.apijunkie.com/APIJunkie/blog/page/How-the-Silverlight-loader-works.aspx Silverlight Loader Getting Started Guide http://www.apijunkie.com/APIJunkie/blog/page/Silverlight-Loader-Getting-Started-Guide.aspx Silverlight Loader Class Diagram http://silverlightloader.codeplex.com/wikipage?title=Class%20Diagram&referringTitle=Home 网上有一篇关于实现加载的介绍,基本上是翻译过来的,不是很详细,文章在此: http://silverlightchina.net/html/tips/2010/0115/588.html 下面我根据上面的几篇文章详细介绍项目的实现过程: 第一步:创建一个SLLoader.xap项目或在现有的SLLoader.xap项目中使用Silverlight Loader 1.在现有SLLoader.xap项目中使用SilverlightLoader:先下载SilverlightLoader压缩,从下载的Release中找到SilverlightLoader.cs,并将其添加到SLLoader.xap项目中,具体实现方法:在SLLoader.xap项目中右键添加已有项,浏览找到SilverlightLoader.cs即可添加。 2.通过项目模板创建一个SLLoader.xap项目:将SilverlightLoader模板文件拷贝到VS的模板文件夹下,如My Documents\Visual Studio 2008\ProjectTemplates\Visual C#。而后就可以在新建项目时看到Silverlight Loader项目了。 第二步:实现ISliverlightLoader接口 1.添加对SilverlightLoader的引用 在App.xaml.cs中添加对命名空间SilverlightLoader的引用,实现代码: using SilverlightLoader; 在MainPage.xaml.cs中添加对命名空间SilverlightLoader的引用,实现代码: using SilverlightLoader; 2.创建动态加载部数据的开始点 在App.xaml.cs中的Application_Startup事件中开始载入数据,代码如下: private void Application_Startup(object sender, StartupEventArgs e) { //无需该默认语句 //this.RootVisual = new MainPage(); //建立加载页 MainPage loader = new MainPage(); //设置该加载页为加载项 this.RootVisual = loader; //建立加载数据管理类,并开始加载目标xap, //该类的构造函数中的第三个参数为模拟加载网速, //为了模拟加载过程和调试,可以通过设置该参数来模拟加载速度,单位为Kb, //当设该参数为0时,则采用真实带宽加载目标xap。 PackageDownloadManager pdm = new PackageDownloadManager(loader, e.InitParams, 0); } 3.在MainPage.xaml.cs中实现ISliverlightLoader接口 有如下几个ISilverlightLoader接口需要添加,也就是在下面几接口中实现加载进度的交互显示: // 初始化加载加载的数据列表 void initCallback(List<Uri> packageSourceList); // 回调函数,用于标识开始加载数据 void downloadStartCallback(Uri packageSource); // 回调函数,用于标识加载进度发生变化 void downloadProgressCallback(Uri packageSource, DownloadProgressEventArgs eventArgs); // 回调函数,用于标识加载数据结束 void downloadCompleteCallback(Uri packageSource, DownloadCompleteEventArgs eventArgs); MainPage.xaml.cs中的实现代码如下,我现在把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; using SilverlightLoader; namespace SLLoader { //注意此处的类继承:SilverlightLoader.ISilverlightLoader public partial class MainPage : UserControl, SilverlightLoader.ISilverlightLoader { public MainPage() { InitializeComponent(); } #region ISilverlightLoader Members //初始化加载加载的数据列表 void ISilverlightLoader.initCallback(System.Collections.Generic.List<Uri> packageSourceList) { } //回调函数,用于标识开始加载数据 void ISilverlightLoader.downloadStartCallback(Uri packageSource) { LoadProgress.Text = "加载初始化..."; } //回调函数,用于标识加载进度发生变化 void ISilverlightLoader.downloadProgressCallback(Uri packageSource, DownloadProgressEventArgs eventArgs) { float offset = ((float)eventArgs.ProgressPercentage * 4 / 100f) * 1000; TimeSpan ts = new TimeSpan(0, 0, 0, 0, (int)offset); //显示加载进度,例如下面二行代码中LoadProgress为一个Textblock,LoadProgressBar为一个进度条,则可以使用下面语句显示进度 LoadProgress.Text = eventArgs.ProgressPercentage.ToString() + "%"; LoadProgressBar.Value = eventArgs.ProgressPercentage; } //回调函数,用于标识加载数据结束 void ISilverlightLoader.downloadCompleteCallback(Uri packageSource, DownloadCompleteEventArgs eventArgs) { LoadProgress.Text = "加载完成"; XapUtil.setCurrentXapFile(packageSource); } #endregion } } 第三步:在HTML页面或asp:Silverlight控件设置初始载入界面与载入目标 该步就是在HTML或asp:Silverlight控件中加入对SLLoader.xap的调用,并将LoadTarget.xap的名字做为参数传递给SLLoader.xap,以便加载, 可以看出,在建立整个项目的过程中,SLLoader.xap和LoadTarget.xap并不需要打交道,只在HHTML或asp:Silverlight控件设置中才把二者联系起来。 1.HTML中的设置方法: <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="550" height="400"> <param name="source" value="SLLoader.xap" /> <param name="onerror" value="onSilverlightError" /> <param name="background" value="white" /> <param name="minRuntimeVersion" value="2.0.31005.0" /> <param name="autoUpgrade" value="true" /> <param name="initParams" value="LoaderSourceList=LoadTarget.xap" /> <a href="http://go.microsoft.com/fwlink/?LinkID=124807" style="text-decoration: none;"> <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style: none" /> </a> </object> 注意SLLoader.xap和LoadTarget.xap的位置,此时把HTML文件、SLLoader.xap和LoadTarget.xap放在同一目录下即可实现加载。 2.在asp:Silverlight控件中设置的方法 <asp:Silverlight ID="Xaml1" runat="server" Source="~/SLLoader.xap" MinimumVersion="2.0.31005.0" Width="550" Height="400" InitParameters="LoaderSourceList=LoadTarget.xap" /> 与在HTML中通过Object对象设置的原理相同。 3.如果此时编译SLLoader.xap项目,可能通不过,提示缺少对System.Runtime.Serialization.Json的引用, 为解决此问题,需要添加如下三个引用,参考文章:http://www.cnblogs.com/Blackie/archive/2009/07/09/1520080.html 1.System.Runtime.Serialization 2.System.ServiceModel 3.System.ServiceModel.Web 至此,一个加载功能制作完毕。 注意事项: (1)App和MailPage都需要添加对命名空间SilverlightLoader的引用; (2)PackageDownloadManager类构造函数中的第三个参数需要更改,看前面的代码注释,在此处将参数改为一个较小的值,即可减小加载速度,以便查看加载效果; (3)需要添加以下三个引用,只有同时添加,才能正确引用System.Runtime.Serialization.Json: 1.System.Runtime.Serialization 2.System.ServiceModel 3.System.ServiceModel.Web 目前已知的问题: 实现加载功能的目的就是不想显示Silverlight默认的那个蓝色整圆圆的加载动画, 但如果网速过慢,还是会显示加载SLLoader.xap的动画,另SLLoader.xap加载完LoadTarget.xap后,打开LoadTarget.xap的过程中也可能会出现默认的那个加载动画,虽然只是一瞬间,不知道该怎么屏蔽这个默认的动画,请大家支招。 还请大家教我实现屏蔽默认加载动画的方法,我的邮箱:ck436#126.com,把#改为@
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值