完美實現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應用程序加載速度(結合應用程序庫緩存效果更好)實現起來簡單方便,在實際項目開發中可加以借鑒。同時對於資源的處理可以根據實際項目考慮放在單獨的資源工程中,方便組織管理。如果對本文有任何問題或建議,歡迎給我留言討論。

 

參考文章:

http://www.codeproject.com/Articles/192738/What-is-AppManifest-xaml-in-Silverlight

http://www.kunal-chowdhury.com/2011/03/application-library-caching-in.html

 

附上源代碼 (Silverlight5 + VS2010)

 

版權說明:本文章版權歸本人及博客園共同所有,未經允許請勿用於任何商業用途。轉載請標明原文出處:

http://www.cnblogs.com/talywy/archive/2012/11/01/Silverlight-Dynamic-Load.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM