Browse by Tags

All Tags » out of browser   (RSS)

One of the reasons I prefer to manage navigation as an event, rather than a strongly typed interface or handler, is because it allows for so much flexibility and extensibility in the navigation pipeline. In my Jounce framework, for example, the basic navigation event simply wires up an instance of a view to a view model and makes no presumptions about where that view belongs - it leaves positioning the view to the developer. The region manager simply listens for navigation events and passes the views off to region adapters without worrying about how they are wired, so it automatically handles the view simply by extending the pipeline by listening for the message as well. This same model makes it incredibly simple to place views in child windows using the new Silverlight 5 Out-of-Browser feature.

The first thing I'll do is create a controller to listen to navigation messages. It will expect a specific parameter to be passed that indicates when the view should be passed to a window. If that parameter exists, it will use parameters for height, width, and title to spin up the new window. A more complete implementation would store those literals as constants. Here is the shell for the class that implements the listener and subscribes to it:

[Export]
public class WindowController : IEventSink<ViewNavigationArgs>,
                                IPartImportsSatisfiedNotification
{
    [Import]
    public IEventAggregator EventAggregator { get; set; }

    [Import]
    public IViewModelRouter Router { get; set; }
    
    public void OnImportsSatisfied()
    {
        EventAggregator.SubscribeOnDispatcher(this);
    }

Now the handler. The handler will check the parameters for a special "OpenInWindow" parameter that must be set to true. It will only respond when that's the case, and everything else goes through the normal view routing. Because the project uses region management, there is no conflict because these views will not be routed to specific regions. First, if the parameter doesn't exist, the method simply returns. Note the use of the Jounce extension methods that conveniently cast the parameter to a specific type:

public void HandleEvent(ViewNavigationArgs publishedEvent)
{
    var parms = publishedEvent.ViewParameters;
    if (!parms.ContainsKey("OpenInWindow") ||
        !parms.ParameterValue<bool>("OpenInWindow"))
    {
        return;
    }
}

Next, the router is used to get the view model that is mapped to the view, then spin up a non-shared instance of the view and view model. This allows multiple instances to be created and therefore supports multiple windows with the same view/view model combination. The method to get the view takes an object for a parameter that it will set to the data context of the view. Jounce is smart enough to recognize when that object is a Jounce view model, and will take the additional steps of wiring in visual states and calling the InitializeVm and ActivateView methods on the view model. Notice that the parameters are passed into the view model as well - Jounce will pass these in when it attaches the view model to the view.

var viewModelTag = Router.GetViewModelTagForView(
    publishedEvent.ViewType);
var viewModel = Router.GetNonSharedViewModel(viewModelTag);
var view = Router.GetNonSharedView(
    publishedEvent.ViewType,
    viewModel,
    publishedEvent.ViewParameters 
    as Dictionary<string, object>);

Finally, the window is opened with the view set as the content:

new Window
        {
            Title = parms.ParameterValue<string>("WindowTitle"),
            Width = parms.ParameterValue<int>("WindowWidth"),
            Height = parms.ParameterValue<int>("WindowHeight"),
            Content = view,
            Visibility = Visibility.Visible
        };

That's all there is to it. The controller must be imported somewhere to begin listening for events. Then you can simply export the view with a tag like [ExportAsView("MyView")] and publish the navigation using the Jounce extension methods to turn the view tag into a navigation event and add parameters:

var window = "MyView".AsViewNavigationArgs()
    .AddNamedParameter("OpenInWindow", true)
    .AddNamedParameter("WindowTitle", "My View Title")
    .AddNamedParameter("WindowHeight", 300)
    .AddNamedParameter("WindowWidth", 600);
EventAggregator.Publish(window);

Of course you can get even more clever with how you obtain the title or set the sizes, and now opening a child window is not only as easy as publishing an event, but also fully testable in your view models because you can mock the controller for testing.This technique is demonstrated in detail in the chapter about OOB applications in Designing Silverlight Business Applications: Best Practices for Using Silverlight Effectively in the Enterprise (Microsoft .NET Development Series).

Jeremy Likness

Silverlight Out of Browser (OOB) applications are becoming more and more popular due to the convenience of being able to install and launch them locally. As Silverlight applications become larger and more composable, advanced techniques such as dynamically loading modules are also becoming more popular.

The "out of the box" Managed Extensibility Framework provision for dynamic modules is the DeploymentCatalog. This will download a XAP file based on a URI and integrate it with the current solution. It also works in OOB mode and will attempt to retrieve the URI from the same location as the in-browser version (the only caveat is that you must specify the absolute, rather than relative, URI).

What happens if the user is running on their desktop, and offline? This gets quite interesting. It turns out that most functions will simply use the browser cache, so if the items are cached then they will load with no problem. However, if the cache is cleared, you can run into problems.

To address this issue, I created the OfflineCatalog. This MEF catalog behaves like the DeploymentCatalog with a few exceptions. First, it will save any XAP file to isolated storage whenever it retrieves one, and second, if the application is OOB and offline, it will automatically load the XAPs from isolated storage instead of trying to fetch them from the web.

Instead of building my own catalog from scratch, I decided to cheat a little bit and use some of the existing catalogs "under the covers." To start with, we'll base the class on ComposablePartCatalog. I'm setting up some helpers — an aggregate catalog to aggregate the parts I discover, a list of assemblies to load from the XAP, and a static list of parts so that if I use multiple catalogs I won't ever try to load the same assembly more than once. It looks like this:

public class OfflineCatalog : ComposablePartCatalog
{
    private readonly AggregateCatalog _typeCatalogs = new AggregateCatalog();

    private readonly List<Assembly> _assemblies = new List<Assembly>();

    private static readonly List<string> _parts = new List<string>();

    public Uri Uri { get; private set; }

    public OfflineCatalog(string uri)
    {
        Uri = new Uri(uri, UriKind.Relative);
    }

    public OfflineCatalog(Uri uri)
    {
        Uri = uri;
    }

    public override IQueryable<ComposablePartDefinition> Parts
    {
        get { return _typeCatalogs.Parts; }
    }
}

This will asynchronously load, so I provide an event to "listen to" when the loading is complete:

...
public event EventHandler<AsyncCompletedEventArgs> DownloadCompleted;
...

Now I can wire up the download - it will simply try to download the XAP using a web client if the application is online, and read it from isolated storage if the application is offline:

public void DownloadAsync()
{
    if (NetworkInterface.GetIsNetworkAvailable())
    {
        Debug.WriteLine("Begin async download of XAP {0}", Uri);
        var webClient = new WebClient();
        webClient.OpenReadCompleted += WebClientOpenReadCompleted;
        webClient.OpenReadAsync(Uri);
    }
    else
    {
        _ReadFromIso();
    }
}

For this example, I just take the full URI and replace some of the non-friendly characters with dots to make a filename - that is how I'll store/retrieve the catalog from isolated storage:

private string _AsFileName()
{
    return Uri.ToString().Replace(':', '.').Replace('/', '.');
}

Now I can easily read in the file and send the stream off for processing:

private void _ReadFromIso()
{
    Debug.WriteLine("Attempting to retrieve XAP {0} from isolated storage.", Uri);

    using (var iso = IsolatedStorageFile.GetUserStoreForApplication())
    {
        if (iso.FileExists(_AsFileName()))
        {
            _ProcessXap(iso.OpenFile(_AsFileName(), FileMode.Open, FileAccess.Read));
        }
        else
        {
            if (DownloadCompleted != null)
            {
                DownloadCompleted(this, new AsyncCompletedEventArgs(
                                            new Exception(
                                                string.Format(
                                                    "The requested XAP was not found in isolated storage: {0}",
                                                    Uri)), false, null));
            }
        }
    }
}

Now it's simple to wire in the download event. Once downloaded, I simply write to isolated storage and then call the same method to parse it back out:

private void WebClientOpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    Debug.WriteLine("Download of xap {0} completed.", Uri);

    if (e.Error != null)
    {
        // will try to read from ISO as a fallback 
        Debug.WriteLine("Catalog load failed: {0}", e.Error.Message);                
    }
    else
    {
        var isoName = _AsFileName();

        Debug.WriteLine("Attempting to store XAP {0} to local file {1}", Uri, isoName);

        using (var iso = IsolatedStorageFile.GetUserStoreForApplication())
        {
            using (var br = new BinaryReader(e.Result))
            {
                using (var bw = new BinaryWriter(iso.OpenFile(isoName, FileMode.Create, FileAccess.Write)))
                {
                    bw.Write(br.ReadBytes((int) e.Result.Length));
                }
            }
        }
    }

    _ReadFromIso();
}

Notice if the load fails, I'll still try to read the isolated storage version as a fallback. Now we need to take a look at the "meat" of the method that reads from iso. The ProcessXap method does several things. The AppManifest.xaml provides us with a list of the parts (assemblies) contained. We parse that into a LINQ XML document and begin iterating it. We call a method that loads these into the assembly space and adds them to the list of assemblies. I add all of the assemblies first because I want to make sure any dependencies are already loaded before I start putting the parts into the MEF catalogs. Otherwise, MEF will choke if I try to add an assembly that references another assembly that hasn't been parsed yet. You can see how I take advantage of the existing MEF catalogs: for each assembly, I simply call the GetTypes method, pass those into a TypeCatalog, and add it to the aggregate catalog. When MEF asks us for the parts, I simply tell the aggregate catalog to pass along its parts. Take a look at this main loop:

private void _ProcessXap(Stream stream)
{
    var manifestStr = new
        StreamReader(
        Application.GetResourceStream(new StreamResourceInfo(stream, null),
                                        new Uri("AppManifest.xaml", UriKind.Relative))
            .Stream).ReadToEnd();

    var deploymentRoot = XDocument.Parse(manifestStr).Root;

    if (deploymentRoot == null)
    {
        Debug.WriteLine("Unable to find manifest for XAP {0}", Uri);
        if (DownloadCompleted != null)
        {
            DownloadCompleted(this,
                                new AsyncCompletedEventArgs(new Exception("Could not find manifest root in XAP"),
                                                            false, null));
        }
        return;
    }

    var parts = (from p in deploymentRoot.Elements().Elements() select p).ToList();

    foreach (var src in
        from part in parts
        select part.Attribute("Source")
        into srcAttr where srcAttr != null select srcAttr.Value)
    {
        _ProcessPart(src, stream);
    }

    foreach(var assembly in _assemblies)
    {
        try
        {
            _typeCatalogs.Catalogs.Add(new TypeCatalog(assembly.GetTypes()));
        }
        catch (ReflectionTypeLoadException ex)
        {
            Debug.WriteLine("Exception encountered loading types: {0}", ex.Message);

            if (Debugger.IsAttached)
            {
                foreach (var item in ex.LoaderExceptions)
                {
                    Debug.WriteLine("With exception: {0}", item.Message);
                }
            }

            throw;
        }
    }
Debug.WriteLine("Xap file {0} successfully loaded and processed.", Uri); if (DownloadCompleted != null) { DownloadCompleted(this, new AsyncCompletedEventArgs(null, false, null)); } }

So how do we process the parts? The AssemblyPart provided by the framework takes care of it for us, as you can see here:

private void _ProcessPart(string src, Stream stream)
{
    Debug.WriteLine("Offline catalog is parsing assembly part {0}", src);

    var assemblyPart = new AssemblyPart();

    var srcInfo = Application.GetResourceStream(new StreamResourceInfo(stream, "application/binary"),
                                                new Uri(src, UriKind.Relative));

    lock (((ICollection)_parts).SyncRoot)
    {
        if (_parts.Contains(src))
        {
            return;
        }

        _parts.Add(src);

        if (src.EndsWith(".dll"))
        {
            var assembly = assemblyPart.Load(srcInfo.Stream);
            _assemblies.Add(assembly);                    
        }
        else
        {
            assemblyPart.Load(srcInfo.Stream);
        }
    }
}      

Notice I am locking on the main list to make sure I don't load a duplicate.

That's it - now we can simply pass one or many of these catalogs to the composition host and we're good to go (basically, take a look at any examples that use the deployment catalog and use this in its place).

Now once the user has the application, they can run it offline even though the modules are dynamic. Of course, you'll have to download all modules first - you can put a check to see if it is running on the desktop and force a download to make it happen. It will also automatically check for new XAP files when going back online, so you can release updates to modules independent of the fully composed application.

I don't have a project example for this but hope I've provided enough source for you to piece together the catalog yourself and take advantage of it in your Silverlight OOB applications.

Jeremy Likness

This is a quick and simple post to address three very common questions I receive about Silverlight Out-of-Browser (OOB) applications. In case you haven't heard, applications made with Silverlight version 3 and later can be installed locally to your machine (whether it is a Windows machine or a Mac) and run "out of the browser" as a local application. In Silverlight 4, you can also run with partial trust. That is not the focus on this post.

Creating an out of browser application is quite easy. Open the project properties for your Silverlight application. You will find a checkbox that allows for installation when out of browser:

Silverlight Out of Browser

Simply check the box and you are on your way! Of course, you can also click the button to set other properties, such as the name and description for the application, provide icons for installation, and decide whether you want it to run with elevated trust.

Question One: How do I publish new versions of my Silverlight Out-of-Browser Application?

This is easy. Silverlight does not do version checking by itself. To Silverlight, your "version" is basically the Uri of the XAP file from which it was originally downloaded. If you publish a new XAP file for the web version, when the user revisits, it automatically pulls down the newest version (sometimes this doesn't happen immediately because the XAP file may be cached by the browser, similar to images and other resources). The out of browser application is the same: when the application starts, it automatically polls the server. If a new XAP file is detected, the bits are downloaded and the application is updated.

Of course, this sort of "forces" the update out of the box. Jeff Wilcox's blog has an excellent article about Out of Browser that demonstrates how to handle the update experience programmatically by prompting the user.

Question Two: Do I need to embed my images for out-of-browser to work?

The answer is "no." Out-of-browser works very similar to "in the browser." Images can fatten XAP files so most people place the images in the ClientBin folder where the XAP file resides. This allows you to reference the image with a relative path:

...
<image source="foo.jpg"/>
...

If foo.jpg is located in the root of ClientBin on the server, it will load just fine. But what about with OOB? The same thing happens. The installed application retains a pointer to the Uri where it was installed from (this is how it can check for new versions). When a relative image reference is found, it will simply download it relative to the XAP file directory, the same as the online application. Therefore, you do not have to package the images in the XAP for them to become available.

But what about when the application is offline?

This is tricky territory. The word on the street is that it will still use your browser's cache to retrieve the images. This makes it very unpredictable, because you never know when the cache will clear and in my tests, you'll often end up with an application that has no images. If you are writing an application that spends a good deal of time in offline mode, you will probably want to embed your images in the XAP to make sure they are available all of the time.

Question Three: Do the web and desktop versions of my application share the same isolated storage?

Yes, they do.

This one may seem confusing because the default size of isolated storage for a web application is 1 megabyte (MB) while the default for out-of-browser applications is 25 megabytes. However, despite the difference in quota, both versions point to the same isolated storage location.

The Example

To illustrate the image and isolated storage points, I made a very simple out of browser application that you can play with here. It contains an image file that is external to the XAP, so you can see how it is fetched the same way both in and out of browser. It also automatically detects the mode it is running in. In the web browser mode, you can enter text and click a "serialize" button to write that text to isolated storage. In the OOB mode, it has a "deserialize" button that reads from isolated storage and writes into the text box. You can run the web and out of browser versions at the same time, serializing data from the web version and immediately deserializing it from the OOB version to see how they share the same isolated storage.

To install the application to your desktop, simply right-click and select the option to install:

Installing an OOB application

The code for this is simple. Note that I did not do any error checking, so you'll want to serialize some text before attempting to deserialize it. This is the code behind for the application:

public partial class MainPage
{
    private const string FILE = "testserialization.txt";
        
    public MainPage()
    {
        InitializeComponent();
        Application.Current.InstallStateChanged += Current_InstallStateChanged;
        ISOButton.Content = Application.Current.IsRunningOutOfBrowser ? "Deserialize" : "Serialize";
    }

    void Current_InstallStateChanged(object sender, System.EventArgs e)
    {
        ISOButton.Content = Application.Current.IsRunningOutOfBrowser ? "Deserialize" : "Serialize";
    }

    private void IsoButtonClick(object sender, RoutedEventArgs e)
    {
        if (Application.Current.IsRunningOutOfBrowser)
        {
            using (var iso = IsolatedStorageFile.GetUserStoreForApplication())
            {
                if (iso.FileExists(FILE))
                {
                    using (var br = new BinaryReader(iso.OpenFile(FILE, FileMode.Open, FileAccess.Read)))
                    {
                        Text.Text = br.ReadString();
                    }
                }
            }
        }
        else
        {
            using (var iso = IsolatedStorageFile.GetUserStoreForApplication())
            {
                using (var bw = new BinaryWriter(iso.OpenFile(FILE, FileMode.Create, FileAccess.Write)))
                {
                    bw.Write(Text.Text);
                }
            }
            Text.Text = "--- wrote to iso ---";
        }
    }
}

Notice that the content and behavior of the button changes based on whether or not the application is running out of browser (OOB). It also hooks into the InstallStateChanged event to update "on the fly" when the user installs.

Hopefully this short post demonstrates some useful concepts about running your applications out of browser. You can download the source for the example here.

Jeremy Likness