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="http://schemas.microsoft.com/client/2007" xmlns:x="http://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("http://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);