Browsing XAP contents in Silverlight 2
The Silverlight 2 runtime doesn’t have an easy way to browse the contents of the XAP file. This is quite unfortunate if one wants to browse embedded resources and XAML within the XAP.
The XAP is essentially a ZIP file renamed. One easy way to browse the contents of a XAP manually is to rename the file and tack on “.zip” at the end. (Technically, you don’t need to do this to read the contents—but some folks who depend on file extensions will find this useful.)
Now let’s try to do something interesting. Let’s try to view the contents of a XAP using Silverlight 2 at runtime! There are a few ways that center around the following ideas:
- Download the XAP and have the contents ready in a Stream.
- Read the contents by using some implementation of the ZIP file format. (See http://www.pkware.com/documents/casestudies/APPNOTE.TXT)
There is a useful ZIP implementation available over at http://www.silverlightcontrib.org/. I also happened to come across a light weight implementation by Jason Cooke (of Microsoft) and I’ll show you that here. This implementation will not actually decompress the resources within the XAP—it will merely allow you to enumerate through them as strings. Using those streams, you can load the resources by using Application.GetResourceStream(<string>).
WebXapFileLoader (courtesy of Jason Cooke)
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Windows.Browser;
namespace SLFileIO
{
public static class WebXapFileLoader
{
private static Dictionary<object, WebClient> clients = new Dictionary<object, WebClient>();
private static Dictionary<object, FileListCallback> callbacks = new Dictionary<object, FileListCallback>();
public static void Load(FileListCallback callback)
{
Uri originalUrl = HtmlPage.Document.DocumentUri;
string xamlFile = (string)HtmlPage.Plugin.GetProperty("source");
if (xamlFile == null)
throw new Exception("Could not get xap source");
Uri xamlLocation = new Uri(originalUrl, xamlFile);
WebClient client = new WebClient();
Guid token = Guid.NewGuid();
clients.Add(token, client);
callbacks.Add(token, callback);
client.OpenReadCompleted += XapReloaded;
client.OpenReadAsync(xamlLocation, token);
}
private static void XapReloaded(object sender, OpenReadCompletedEventArgs e)
{
try
{
callbacks[e.UserState](e.Result);
}
finally
{
callbacks.Remove(e.UserState);
clients.Remove(e.UserState);
}
}
}
public delegate void FileListCallback(Stream xapFileStream);
}
XapInspector (courtesy of Jason Cooke)
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace SLFileIO
{
public static class XapInspector
{
public static IList<string> GetFileNames(Stream stream)
{
var ret = new List<string>();
using (var archiveStream = new BinaryReader(stream))
{
while (true)
{
string file = GetFileName(archiveStream);
if (file == null) break;
ret.Add(file);
}
}
return ret;
}
private static string GetFileName(BinaryReader reader)
{
// Info from http://www.pkware.com/documents/casestudies/APPNOTE.TXT
var headerSignature = reader.ReadInt32(); // local file header signature 4 bytes (0x04034b50)
if (headerSignature != 0x04034b50)
return null; // Not a local file header
reader.ReadInt16(); // version needed to extract 2 bytes
reader.ReadInt16(); // general purpose bit flag 2 bytes
reader.ReadInt16(); // compression method 2 bytes
reader.ReadInt16(); // last mod file time 2 bytes
reader.ReadInt16(); // last mod file date 2 bytes
reader.ReadInt32(); // crc-32 4 bytes
var compressedsize = reader.ReadInt32(); // compressed size 4 bytes
reader.ReadInt32(); // uncompressed size 4 bytes
var filenamelength = reader.ReadInt16(); // file name length 2 bytes
var extrafieldlength = reader.ReadInt16(); // extra field length 2 bytes
var fn = reader.ReadBytes(filenamelength); // file name (variable size)
var filename = UTF8Encoding.UTF8.GetString(fn, 0, filenamelength);
reader.ReadBytes(extrafieldlength); // extra field (variable size)
reader.ReadBytes(compressedsize); // compressed data (variable size)
return filename;
}
}
}
Putting it together
I put together a quick Silverlight application and added a ListBox called “XapFileList”. I was able to bind that to the list of files inside of the XAP using the following simple line of code.
private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
WebXapFileLoader.Load(
xapFileStream =>
this.XapFileList.ItemsSource = XapInspector.GetFileNames(xapFileStream));
}
Very simple. :) Thanks Jason!
Checking for Design Time in Silverlight Applications
Occasionally, you’ll want to alter your application logic to behave different when in design mode. (E.g., when you’re viewing your application in Blend or Visual Studio "Cider”.) Usually, you’ll want to alter your application to use mock data sources and perhaps skip out on validation, user prompts, etc..
So how do you check for design time? Easy! Use the DesignerProperties.GetIsInDesignMode(…) method and it’ll return ‘true’ if your application is currently hosted by a designer.
Works great for Blend. Unfortunately, this currently does not work with Cider. (Cider, btw, is the designer surface you see in Visual Studio.) I’ve found it useful to create my own application-level property to provide hints about design time to work around the Cider issue.
I’ve created a static Boolean property that uses DesignerProperties.GetIsInDesignMode(…) first (because it does work in Blend) and falls back to examining the current Application instance. This works because the Application instantiated during design time is, for the time being, either nothing or of type Application.
public static bool InDesignMode
{
get
{
if (inDesignMode.HasValue == false)
{
inDesignMode =
Application.Current == null ||
Application.Current.GetType() == typeof(Application);
}
return inDesignMode.Value;
}
}
Simple enough and it gets the job done. :) That said, this will probably be scrapped in Visual Studio 2010 when we get a better XAML editor … I hope.
If you want to see this working in a real designer, don’t forget that you can open up another instance of Visual Studio (or your favorite debugger) and attach to VS/Blend/<whatever> and set your breakpoints to watch how your application really behaves while in design mode. Very useful!
Silverlight 2 – Using Generic.xaml to define control templates
I’ve seen a number of posts describing troubles in getting Generic.xaml working in a Silverlight 2 project. After going through some of the common pitfalls myself, I thought it might be useful to post a concise list of steps required.
- In Silverlight 2 and beyond, place the Generic.xaml file in a (project root) folder named Themes. (Names are not case-sensitive)
- Make sure Generic.xaml is configured such that:
- Build Action is set to "Resource"
- Copy to Output Directory is set to "Do not copy"
- Custom Tool is blank
- In any of your controls that are trying to use templates defined in Generic.xaml, make sure you set the DefaultStyleKey property in your class constructor.
public MyControl(){ this.DefaultStyleKey = typeof(MyControl);}
- Verify the general structure of your generic.xaml file.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:MyExampleControl"> <Style TargetType="my:MyControl"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="my:MyControl"> <TextBlock Text="Foo" /> </ControlTemplate> </Setter.Value> </Setter> </Style></ResourceDictionary>
Remember that control templates are only applied when the control is added to the visual tree. If you're still not able to see your template after verifying the above steps, take a close look at your code to make sure your control is being added to the visual tree for rendering.
Debugging Silverlight in Firefox, Safari
If you’re already familiar with attaching to processes in Visual Studio (or WinDbg), this is nothing new. However, I’ve seen this question come up a few times—particularly with people coming over from competing technologies.
In Visual Studio, just select the “Attach to process” option (available in the Debug or Tools menu, depending on your workspace view) and select the type of code you want to debug, Silverlight in this case, and the process you want to debug. This applies to any process that is hosting a Silverlight instance.
You should be aware that this general advice applies to all sorts of applications. For example, if you want to debug the behavior of your Silverlight control library during design-time operations, you could use one instance of the debugger to attach to the application that is rendering the design surface. (E.g., attach to Blend or event to another instance of Visual Studio.)
Firefox loads localhost resources slowly
If while debugging your Silverlight application using the Visual Studio web development server, you notice Firefox is painfully slow to load any resource, you might want to disable IPv6 support. This should get Firefox (<=3) working on par with Safari, IE, et al..
- Open Firefox
- Navigate to “about:blank”
- Find the entry for “network.dns.disableIPv6” and set it to false.
Now try loading your local development site! This applies to the general case of Firefox loading anything from localhost.
AJAX Navigation and a Firefox 3 JavaScript bug
I was playing around with some more AJAX navigation goodness today and ran into an interesting Firefox 3 bug. After creating some unit tests, I grew frustrated running them in Firefox 3 and having my tests fail because the browser URL would continuously reload.
After some investigation, the culprit was determined to be a line of code similar to:
// This will cause a page reload in Firefox
document.location.hash = "";
It turns out setting document.location.hash to null or “” (empty string) will cause the current page to reload. One workaround is to prefix all values with the “#” hash character or have a special case for null/empty strings and set the value to “#”.
// This feels completely unnecessary...
document.location.hash = (!hash || hash.length == 0) ? "#" : hash;
The workarounds are simple enough but this still feels dirty to not be able to use the location.hash property consistently across all browsers. This behavior doesn’t reproduce in IE7, IE8, Opera 9.6 and Safari 3.1. Just posting this out there in case anyone else is running into the same strangeness.
Browser History Integration Part 1/3 – Using ASP.NET AJAX API
Summary
With more and more RIA (rich internet applications) becoming common place, there is an increasing trend to want to store history/state information in the browser history journal to allow for deep-links and using the browser back/forward buttons in websites that rarely if ever (want to) refresh the host page.
I’m planning to post 3 blogs exploring a few ways to achieve the stated goals. In this first post (“frist!”), I’ll briefly go over using the ASP.NET AJAX framework. In subsequent posts, I’ll dig into other options such as SWFAddress and rolling your own history abstraction layer.
Disclaimer: while I hope you find the information presented here useful, I am by no means an expert on this topic. I’m merely sharing the results of some of my own personal (sometimes painful) experiences of browser history integration in JavaScript, Flash and Silverlight applications.
Challenges
Key to successful browser history integration with any RIA-type application is:
- Being able to update the location URL fragment (a.k.a. hash) information without a page refresh.
- Having the location URL fragment changes recorded in the browser history journal.
- The ability to detect location URL fragment changes.
The good news is that the latest modern browsers prepped for HTML5 (e.g., Internet Explorer 8, Firefox 3, Safari 3, Opera 9.6) all support this fairly well.
The bad news is most people don’t use the latest modern browsers and there are a plethora of bugs/cross-compatibility issues with Internet Explorer 6, Firefox 1 and 2, Safari 1 and 2 and earlier versions of Opera. Alas, now is not the time for browser-bashing. Now is the time for solutions!
ASP.NET AJAX API
With .NET 3.5 service pack 1, the ASP.NET AJAX API was formally released as part of the ASP.NET framework. The AJAX API contains a wealth of features but in this post I’ll be focusing on those relevant to providing browser history services.
The ASP.NET AJAX API and everything detailed below can also be used in non-ASP.NET solutions (e.g., PHP, Ruby on Rails, plain HTML, etc.) as the AJAX scripts are also available for standalone static deployment. For more information, see the standalone documentation on http://www.asp.net/.
1. Enabling History Support
The first step to getting started with ASP.NET AJAX is to enable history functionality with a ScriptManager control available on your hosting page. The ASP.NET ScriptManager control provides a new property EnableHistory that needs to be set to “true” to allow for us to start making use of the browser history APIs.
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" EnableHistory="true" runat="server" />
<div>
<asp:Silverlight ID="Xaml1" HtmlAccess="Enabled" runat="server"
Source="~/ClientBin/BrowserHistoryIntegration.xap" Width="100%" Height="100%" />
</div>
</form>
</body>
2. Adding History Points
Once this is set to true, we’re ready to start using the history APIs. It’s really quite simple to do as we’ll only be using 1 method to add state information to the history. The example below demonstrates simple usage of the Sys.Application.addHistoryPoint method in JavaScript. (Later, we’ll be making use of this same method but from within a Silverlight application.)
<script type="text/javascript">
//<[!CDATA[
// Example: adding a simple state history point
var simpleState = { menu: "main" };
Sys.Application.addHistoryPoint(simpleState, "MySite - Main Menu");
// Adding a more complex state entry
var complexState =
{
menu: "about",
submenu: "contactUs",
foo: { bar: 123 }
};
Sys.Application.addHistoryPoint(complexState, "MySite - Contact Us");
//]]>
</script>
This method will merge provided state information with anything that might already be existing in the browser URL hash. I won’t go into those details here but I do think it is important to understand what’s happening to the browser URL when we call the Sys.Application.addHistoryPoint method. The original URL of this example application might look something like:
http://server/myapp/default.aspx
After adding a history point, we’ll see the URL updated to:
http://server/myapp/default.aspx#menu=main
The history API is managing a collection of name-value pairs for us, similar to query string parameters except the AJAX history parameters will not be visible to the responding HTTP servers. This is quite helpful as it allows us to have multiple pieces of state information in our URLs without any extra work on the part of the developer. A drawback to this approach is that you do lose out on some of the “pretty” URL structures that some sites like to use when just one single state token is required. (I’ll talk more about single state tokens in a later post when I jump into some SWFAddress and custom script examples.)
3. Navigation Events
Adding history points is great but what about handling deep links and browser back/forward events? We need to sign up to receive navigation events and respond as appropriate. To do this, we can register a callback using the Sys.Application.add_navigate method.
<script type="text/javascript">
//<[!CDATA[
// Define a navigation event handler
function myNavHandler(sender, args) {
// Grab a reference to the state collection object
var stateObj = args.get_state();
// Some custom state processing
for (var prop in stateObj) {
// In this example, we only care about the "menu"
// state but other state name-value pairs could
// potentially exist in our collection.
if (prop == "menu") {
// Do something with this information...
doSomething(stateObj[prop]);
}
}
};
// Let's sign up to receive AJAX navigation events.
Sys.Application.add_navigate(myNavHandler);
//]]>
</script>
Supported Browsers
(From http://msdn.microsoft.com/en-us/library/bb470452.aspx)
- Microsoft Internet Explorer 6.0 or later versions
- Mozilla Firefox version 1.5 or later versions
- Opera version 9.0 or later versions
- Apple Safari version 2.0 or later versions
That’s the official list but in my own experience, most derivatives of the Trident, Gecko or WebKit rendering engines work as well. Yes, that means Chrome too.
References
MSDN Documentation – Managing Browser History Using Client Script
Silverlight Lynx Beta Plugin Available
Looks like the first version of Silverlight for Lynx is available in Beta format. Can’t wait for RTM! ;)
Hello world... and Windows Live Writer.
I just setup Windows Live Writer to work with my BlogEngine.net blog and I have to admit that it’s an absolutely wonderful experience. Highly recommended.
I know I’m way behind the times on this one. :)