Silverlight for the Enterprises - Application Partitioning
In this post we will look at one of the architectural aspects of the enterprise class applications - application partitioning. Application partitioning is done for variety of reasons including the following:
- To optimize download times
- To chunk the application down to a set of manageable deployment units
- To isolate sensitive parts of the application from the anonymous parts
- To get loosely coupled integration with external applications
- To bridge the differences between the development model and the deployment model
To help with application partitioning, Silverlight 2 allows the creation of multiple deployment units with each unit packaged into a file with .XAP extension. The core runtime provides networking, IO and reflection libraries to create type system artifacts from the bytecode streams embedded in the XAP packages. For example, sets of related user UserControls may be packed together into their respective packages and deployed on same or multiple web sites.
We will look at this from a simple application that searches AdventureWorks data extracted into an object list. The object list List<ProductInfo> is embedded in one of the packages to make the application a self contained solution. The following is the schematic of the solution:
The Adventureworks Product Search screen is a part of the fictitious Inventory Manager application built with Silverlight 2. The above picture shows that the main Silverlight package (InventoryMain.xap) is deployed on Inventory_Web site while the details package (InventoryDetails.xap) is deployed on InventoryDetails_web site. The product search screen will be composed of two UserControls - Page.xaml located inside InventoryMain.xap and ProductListView.xaml packaged inside InventoryDetails.xap.
Here are the detailed steps:
Step 1: Create a blank solution (AppPart) using "Visual Studio Solutions" template. This template is located inside other project types category located on the "Add New Project" dialog.
Step 2: Create two Silverlight projects: InventoryMain and InventoryDetails using VS2008 "Silverlight Application" template.
During the creation of these projects, the template will ask you to create a web site to map each of the projects. Map InventoryMain to Inventory_Web site and InventoryDetails to InventoryDetails_Web site. Select "ASP.NET Web Application" template for these sites. The "ASP.NET Web Application" template will allow to use a fixed port number. The completed Solution Explorer will look like the screen shown:
Step 3: Change the port numbers to the web projects from their defaults to (Inventory_Web:1071, InventoryDetails_Web:1072) from the project property pages.
Step 4: Add InventoryMain.Page.xaml to contain the following markup:
<!--this is sample code; only meant for demo purpose and not for production use--> <UserControl xmlns:my="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" x:Class="InventoryMain.Page" xmlns="https://schemas.microsoft.com/client/2007" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" Width="400" Height="336" > <Border BorderBrush="Blue" BorderThickness="2" CornerRadius="5,5,5,5" Margin="5,5,5,5"> <Grid x:Name="LayoutRoot" Background="White"> <Grid.Resources> <Style x:Key="LabelStyle" TargetType="TextBlock"> <Setter Property="FontFamily" Value="Verdana"/> <Setter Property="FontSize" Value="20"/> <Setter Property="Foreground" Value="Blue"/> </Style> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition Width="320"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Canvas Grid.Column="0"> <TextBlock Text="AdventureWorks Product Search" Canvas.Left="40" Canvas.Top="8" Style="{StaticResource LabelStyle}"/> <StackPanel Background="LightSteelBlue" Height="250" Width="320" Canvas.Left="40" Canvas.Top="40"> <TextBlock Text="Search By Product Name" HorizontalAlignment="Center" Height="25"/> <TextBox x:Name="textSearchCriteria" Width="175" Height="25" HorizontalAlignment="Center" Margin="10,2,0,1"/> <Button x:Name="buttonSubmitSearch" Content="Submit search" Width="150" HorizontalAlignment="Center" Margin="10,2,0,1" Click="buttonSubmitSearch_Click"/> </StackPanel> <TextBlock Text="Search Status:" Foreground="Blue" Height="22.537" Margin="0,0,0,0" Canvas.Top="250" Canvas.Left="60.384"/> <TextBlock x:Name="textStatus" Text="OK" Foreground="Red" Canvas.Top="250" Canvas.Left="168" RenderTransformOrigin="2.14499998092651,-1.06500005722046" Width="168" Height="62.537"/> </Canvas> <Canvas Grid.Column="1"> <StackPanel x:Name="searchResultsPanel" Canvas.Left="40" Canvas.Top="40" Width="320" Height="250" Background="Khaki" Visibility="Collapsed"> <TextBlock Text="Search Results" Height="25" HorizontalAlignment="Center"/> </StackPanel> </Canvas> </Grid> </Border> </UserControl> |
Step 5: Add InventoryMain.ProductInfo.cs to contain the following code:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace InventoryMain
{
public class ProductInfo
{
public int _productID;
public string _productName;
public string _productNumber;
public int _productSafetyStockLevel;
public int _productReorderPoint;
public int ProductID
{
get { return this._productID; }
set { this._productID = value; }
}
public string ProductName
{
get { return this._productName; }
set { this._productName = value; }
}
public string ProductNumber
{
get { return this._productNumber; }
set { this._productNumber = value; }
}
public int ProductSafetyStockLevel
{
get { return this._productSafetyStockLevel; }
set { this._productSafetyStockLevel = value; }
}
public int ProductReorderPoint
{
get { return this._productReorderPoint; }
set { this._productReorderPoint = value; }
}
}
}
Step 6: Add InventoryMain.PackageUtil.cs to contain the following code:
//this is sample code; only meant for demo purpose and not for production use
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Resources;
using System.Reflection;
using System.Net;
using System.IO;
using System.Xml;
using System.Xml.Linq;
namespace InventoryMain
{
//extracts various artifacts from the package stream
public class PackageUtil
{
public static Assembly LoadAssemblyFromXap(Stream packageStream, string
assemblyName)
{
string appManifestString =
new StreamReader(Application.GetResourceStream(
new StreamResourceInfo(packageStream, null),
new Uri("AppManifest.xaml", UriKind.Relative)).Stream)
.ReadToEnd();
Deployment deployment = (Deployment)XamlReader.Load(appManifestString);
Assembly asm = null;
foreach (AssemblyPart assemblyPart in deployment.Parts)
{
if (assemblyPart.Source == assemblyName)
{
string source = assemblyPart.Source;
StreamResourceInfo streamInfo =
Application.GetResourceStream(
new StreamResourceInfo(packageStream, "application/binary"),
new Uri(source, UriKind.Relative));
asm = assemblyPart.Load(streamInfo.Stream);
break;
}
}
return asm;
}
}
//abstracts the WebClient
public class SLPackage
{
private Uri _packageUri;
public class PackageEventArgs : EventArgs
{
private Stream _packageStream;
private string _packageUri;
public PackageEventArgs(Stream packageStream, string packageUri)
{
this._packageStream = packageStream;
this._packageUri = packageUri;
}
public Stream PackageStream { get { return _packageStream; } }
public String PackageUri { get { return _packageUri; } }
}
public delegate void PackageEventHandler(object sender,
PackageEventArgs e);
public event PackageEventHandler PackageDownloaded;
public SLPackage(Uri uri)
{
_packageUri = uri;
}
public void LoadPackage()
{
WebClient webClient = new WebClient();
webClient.OpenReadCompleted +=
new OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
webClient.OpenReadAsync(_packageUri);
}
private void webClient_OpenReadCompleted(object sender,
OpenReadCompletedEventArgs e)
{
PackageEventArgs pe = null;
try
{
pe = new PackageEventArgs(e.Result,
this._packageUri.OriginalString);
}
catch
{
PackageDownloaded(this, null);
return;
}
PackageDownloaded(this, pe);
}
}
}
The PackageUtil class hides the clutter in the XAML controls that download the packages. If a package is on a different domain than the originating domain of the XAP, and if clientaccesspolicy.xml is not deployed on the 2nd domain, the WebClient.OpenReadAsync will never throw and exception. However the called delegate (in this case webClient_OpenReadCompleted) will throw a TargetInvocationException which is not the real reason why the call is failing. Hopefully beta2 will have a better exception reporting.
The delegate method catches all the exceptions and triggers the PackageDownloaded event with a null PackageEventArgs argument. It will be the responsibility of the downloader class (in this case InventoryMain.Page.xaml user control) to verify the null stream and take appropriate corrective measures.
Step 7: Modify the InventoryMain.Page.xaml.cs to have the following code:
//this is sample code; only meant for demo purpose and not for production use
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
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 System.Reflection;
namespace InventoryMain
{
public partial class Page : UserControl
{
private bool detailsLoaded =false;
private Uri packageUri =
new Uri("https://localhost:1072/ClientBin/InventoryDetails.xap",
UriKind.Absolute);
public Page()
{
InitializeComponent();
}
private void buttonSubmitSearch_Click(object sender, RoutedEventArgs e)
{
//if not already loaded load the package
//Get a reference to the control and add it to the children
searchResultsPanel.DataContext =
DataUtil.GetInventoryInfoByName(this.textSearchCriteria.Text);
if (!detailsLoaded)
{
SLPackage detailsPackage;
detailsPackage = new SLPackage(packageUri);
detailsPackage.PackageDownloaded += new
SLPackage.PackageEventHandler(LoadDetailsGrid);
detailsPackage.LoadPackage();
this.textStatus.Text = "Loading...";
}
}
void LoadDetailsGrid(object sender, SLPackage.PackageEventArgs e)
{
if (e == null)
{
textStatus.Text = "Fatal package load error!!!";
return;
}
Assembly asm = PackageUtil.LoadAssemblyFromXap(e.PackageStream,
"InventoryDetails.dll");
object uc = asm.CreateInstance("InventoryDetails.ProductListView");
this.searchResultsPanel.Children.Add((UIElement)uc);
this.textStatus.Text = "OK";
detailsLoaded = true;
this.Width = 740;
this.searchResultsPanel.Visibility = Visibility.Visible;
}
}
}
Because of the sandbox in which the Silvelright runtime operates, all network operations are asynchronous in nature. This will give the Silverlight runtime better control in terms of monitoring the runaway applications. Downloading of the package is abstracted inside SLPackage class which can cache the package stream if necessary and use it for extracting multiple resources. In this version of SLPackage, the field _packageStream is not populated as I did not need it. You can easily modify the webClient_OpenReadCompleted delegate method to populate the reference to the package stream.
Also, look at the following code Click handler:
private void buttonSubmitSearch_Click(object sender, RoutedEventArgs e)
{
//if not already loaded load the package
//Get a reference to the control and add it to the children
searchResultsPanel.DataContext =
DataUtil.GetInventoryInfoByName(this.textSearchCriteria.Text);
if (!detailsLoaded)
{
SLPackage detailsPackage;
detailsPackage = new SLPackage(packageUri);
detailsPackage.PackageDownloaded += new
SLPackage.PackageEventHandler(LoadDetailsGrid);
detailsPackage.LoadPackage();
this.textStatus.Text = "Loading...";
}
}
Since LoadPackage() returns immediately the tendency is to wait for the async operation to complete with a mutex or similar thread synchronization object. But this is a NO NO as this will block the UI thread and will not receive any asynch thread messages posted by the WebClient async read operation result and in turn the SLPackage.PackageDownloaded event.
The processing of the downloaded package (extraction of resources, UserControls, composition of UI from these controls and resources) should be synchronized with the download completion event. When the download is complete, SLPackage causes a PackageDownloaded event which will invoke LoadDetailsGrid delegate method. LoadDetailsGrid is responsible for the extraction of the assembly reference containing the “InventoryDetails.ProductListView” UserControl and load it into the visual tree of the InventoryMain.Page.
The state flows into the new control through the DataContext property set to its parent (searchResultsPanel) inside buttonSubmitSearch_Click handler through the following code fragment:
searchResultsPanel.DataContext =
DataUtil.GetInventoryInfoByName(this.textSearchCriteria.Text);
8.
Step 8: Implement DataUtil.GetInventoryInfoByName using LINQ and the inline List<ProductInfo> created from the data extracted from AdventureWorks database (which is not necessary for testing the code). Add the DataUtil class to InventoryMain project using the following code:
//this is a sample code; only meant for demo purpose only and not for production use
using System;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Collections;
using System.Collections.Generic;
namespace InventoryMain
{
public class DataUtil
{
public static IEnumerable<ProductInfo> GetInventoryInfoByName(string productName)
{
List<ProductInfo> productData = GetSampleData();
var products = from product in productData
where product.ProductName.StartsWith(productName)
select product;
return products;
}
public static List<ProductInfo> GetSampleData()
{
List<ProductInfo> products = new List<ProductInfo>();
products.Add(new ProductInfo { ProductID = 341, ProductName = "Flat Washer 1", ProductNumber = "FW-1000", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 342, ProductName = "Flat Washer 6", ProductNumber = "FW-1200", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 343, ProductName = "Flat Washer 2", ProductNumber = "FW-1400", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 344, ProductName = "Flat Washer 9", ProductNumber = "FW-3400", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 345, ProductName = "Flat Washer 4", ProductNumber = "FW-3800", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 346, ProductName = "Flat Washer 3", ProductNumber = "FW-5160", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 347, ProductName = "Flat Washer 8", ProductNumber = "FW-5800", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 348, ProductName = "Flat Washer 5", ProductNumber = "FW-7160", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 349, ProductName = "Flat Washer 7", ProductNumber = "FW-9160", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 350, ProductName = "Fork Crown", ProductNumber = "FC-3654", ProductSafetyStockLevel = 800, ProductReorderPoint = 600 });
products.Add(new ProductInfo { ProductID = 351, ProductName = "Front Derailleur Cage", ProductNumber = "FC-3982", ProductSafetyStockLevel = 800, ProductReorderPoint = 600 });
products.Add(new ProductInfo { ProductID = 352, ProductName = "Front Derailleur Linkage", ProductNumber = "FL-2301", ProductSafetyStockLevel = 800, ProductReorderPoint = 600 });
products.Add(new ProductInfo { ProductID = 355, ProductName = "Guide Pulley", ProductNumber = "GP-0982", ProductSafetyStockLevel = 800, ProductReorderPoint = 600 });
products.Add(new ProductInfo { ProductID = 356, ProductName = "LL Grip Tape", ProductNumber = "GT-0820", ProductSafetyStockLevel = 800, ProductReorderPoint = 600 });
products.Add(new ProductInfo { ProductID = 357, ProductName = "ML Grip Tape", ProductNumber = "GT-1209", ProductSafetyStockLevel = 800, ProductReorderPoint = 600 });
products.Add(new ProductInfo { ProductID = 358, ProductName = "HL Grip Tape", ProductNumber = "GT-2908", ProductSafetyStockLevel = 800, ProductReorderPoint = 600 });
products.Add(new ProductInfo { ProductID = 359, ProductName = "Thin-Jam Hex Nut 9", ProductNumber = "HJ-1213", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 360, ProductName = "Thin-Jam Hex Nut 10", ProductNumber = "HJ-1220", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 361, ProductName = "Thin-Jam Hex Nut 1", ProductNumber = "HJ-1420", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 362, ProductName = "Thin-Jam Hex Nut 2", ProductNumber = "HJ-1428", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 363, ProductName = "Thin-Jam Hex Nut 15", ProductNumber = "HJ-3410", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 364, ProductName = "Thin-Jam Hex Nut 16", ProductNumber = "HJ-3416", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 365, ProductName = "Thin-Jam Hex Nut 5", ProductNumber = "HJ-3816", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 366, ProductName = "Thin-Jam Hex Nut 6", ProductNumber = "HJ-3824", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 367, ProductName = "Thin-Jam Hex Nut 3", ProductNumber = "HJ-5161", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 368, ProductName = "Thin-Jam Hex Nut 4", ProductNumber = "HJ-5162", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 369, ProductName = "Thin-Jam Hex Nut 13", ProductNumber = "HJ-5811", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 370, ProductName = "Thin-Jam Hex Nut 14", ProductNumber = "HJ-5818", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 371, ProductName = "Thin-Jam Hex Nut 7", ProductNumber = "HJ-7161", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 372, ProductName = "Thin-Jam Hex Nut 8", ProductNumber = "HJ-7162", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 373, ProductName = "Thin-Jam Hex Nut 12", ProductNumber = "HJ-9080", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
products.Add(new ProductInfo { ProductID = 374, ProductName = "Thin-Jam Hex Nut 11", ProductNumber = "HJ-9161", ProductSafetyStockLevel = 1000, ProductReorderPoint = 750 });
return products;
}
}
}
Note: In order for the above code to compile, you need to add System.Xml.Linq.dll to the InventoryMain’s reference list
9. Now we will focus on the InventyoryDetails project. Let us remove the default Page.xaml and add a “Sivlerlight User Control” with name it ProductListView.xaml.
Step 9: Replace the default markup with the following xaml:
<!--this is sample code; only meant for demo purpose and not for production use-->
<UserControl x:Class="InventoryDetails.ProductListView"
xmlns="https://schemas.microsoft.com/client/2007"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
>
<data:DataGrid x:Name="dataGridProductListBase" ItemsSource="{Binding}"
Height="225" Width="320" VerticalAlignment="Top"
AutoGenerateColumns="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"/>
</UserControl>
That is it; we are finished with the details UserControl. This is merely the skin. Pay close attention to the attribute ItemsSource="{Binding}" in the above markup… this attribute shows the data binding engine to pick up the data from the object (in this case List<ProductInfo>) bound to the DataContext. In the absence of this attribute, the DataGrid will be empty even if the search output results in a number of products.
Step 10: Compile the project and test it by accessing the URL (https://localhost:1071/InventoryMainTestPage.html). Use “Thin” as the search criteria. We will see the following screen:
Step 11: InventoryMain.xap package originated from https://localhost:1071/ and is trying to access InventoryDetails.xap from https://localhost:1072/ domain.
This violates the sandbox rules. WebClient embedded inside SLPackage looks for clientaccesspolicy.xml for any policies that allows the first domain to access https://locahost:1072/ domain’s resources. Deploy the following clientaccesspolicy.xml to the root folder of InventoryDetails_Web project:
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from>
<domain uri="https://localhost:1071"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
Step 12: Recompile the project and test it by accessing https://localhost:1071/InventoryMainTestPage.html. If you had followed all the steps, you should see the following screen:
DataGrid in the above view is generating default columns… this can easily be changed by applying templates.
One important note before we close is the caching of XAP packages… WebClient will open the stream to the package from the browser cache if it is already downloaded. If the package is not in the cache, SL runtime with the help of the browser sandbox, verifies the cross-domain policies, downloads the package, caches locally (assuming that there no cache-control: private kind of headers in the response) and hands over the stream to eh application code through utility classes like SLPackage.
Comments
Anonymous
May 18, 2008
PingBack from http://www.travel-hilarity.com/travel-airline-tickets/?p=1267Anonymous
May 20, 2008
Great technique! This will be super useful for helping keep the initial download of SL apps small. Side note: If you have just raw xaml content (not in a .xap or having code behind) you can just retrieve that from a web service as a String and use System.Windows.Markup.XamlReader.Load(XamlString) to turn that into an object.Anonymous
September 11, 2008
I create the web and project step by step, but when I run the application, it will throw exception. Should I make any modification in Silverlight Beta 2? Thanks a lot.Anonymous
October 13, 2008
Hi, This post mainly talks about loading the dll from XAP file asynchronously. But i have a module (Module1.dll) ready and i want to package it with the main silverlight application. But i don't want to add the reference. Can someone let me know how to have modules packaged at run time to the main silverlight application? The other dlls names are configured in a configuration file. At runtime i need to access package it with main app. Any inputs will be highly appriciated. Thanks, Rathi