Dynamic Theming in Silverlight with Implicit Styles (plus Custom Build Events)
Recently I’ve had a few questions about how to dynamically set or change the visual theme for a Silverlight application at runtime. The scenario is generally that an application has many possible themes, and needs to pick one depending on, for instance, a particular user or client who has logged in. What if these themes are huge, or there are hundreds of them? You probably wouldn’t want to bloat the downloaded .xap file by including every theme for every user. So, some approach is needed that lets us target a specific theme to download – at runtime – that isn’t in the application’s resources.
For our simple example, let’s say we have two different customers for a website – SuperAwesome Co. LLC, and SomeOtherCompany Inc. Both of these customers have their own internal users that will be using our site, so the site needs to be branded with the company’s name and colour scheme depending on who logs in. We don’t want to include both custom themes in the application’s .xap file, because then users will always be downloading bits they don’t need!
As our site scaled out to include more users and companies with custom themes, this would degrade the experience for every user as they waited for downloads (not to mention contributing to universal entropy production). Clearly we should split the themes out of the application and into separately downloadable components.
So to summarize, I’d like to build an example that supports:
- Theming an entire Silverlight app at runtime using implicit styles
- Downloading a theme dynamically, without needing to include the theme in the app’s resources/.xap file
- Adding/changing themes without recompiling the Silverlight app
- Showing a “loading” screen while the right theme downloads
- Automatically compressing and deploying resource dictionaries with a post-build event in Visual Studio
Silverlight enables a few approaches to this problem, and many users have posted or blogged solutions for specific pieces of this scenario. However, I’m still writing this for a few reasons:
- I’ve still had some feedback that it can be a bit tricky to put the pieces together – e.g. downloading a zipped XAML theme using a WebClient, unpacking the zip, reading the XAML theme and instantiating a ResourceDictionary, merging it into the application resources, and applying it across an app.
- To give an example of using custom build actions in Visual Studio.
- To show off the new implicit styling support in Silverlight 4 Beta!
Step 1: Creating the App
Since we’ll be using a WebClient, we need to have an active webserver to handle requests – so we’ll create a new Silverlight application hosted in a web app (the default). After everything is done, it will look something like the below. We’ll get to the various files in a bit.
Note I’m using Visual Studio here so I can take advantage of custom build events, but this would work equally well in Blend or another tool if you don’t mind manually compressing the themes (or writing an external script to do it).
Let’s also mock up our sample UI in MainPage.xaml:
1: <Grid x:Name="LayoutRoot" Background="White">
2: <TextBlock Name="textBlock1" Style="{StaticResource headerTextBlockStyle}" HorizontalAlignment="Center" VerticalAlignment="Top" />
3:
4: <StackPanel Margin="0,90,0,94" HorizontalAlignment="Left" Width="167">
5: <Button Content="Home" />
6: <Button Content="Sales Forecasting" />
7: <Button Content="Purchase Order History" />
8: </StackPanel>
9: </Grid>
Pretty simple – a header TextBlock with an explicit style defined (note it’s not in the control’s or application’s resources – we’ll be downloading it!), as well as some buttons which will be styled implicitly. It looks pretty bland as-is:
Step 2: Creating Themes
You’ll notice the two .xaml files in the themes directory in the web project folder – these will be our themes for SuperAwesomeCo and SomeOtherCompany. They’re pretty standard resource dictionaries that can contain any mix of explicit and implicit styles:
1: <ResourceDictionary
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:sys="clr-namespace:System;assembly=mscorlib"
5: >
6:
7: <!-- explicit style (has a key) -->
8: <Style x:Name="headerTextBlockStyle" TargetType="TextBlock">
9: <Setter Property="Text" Value="SuperAwesome Co. LLC" />
10: <Setter Property="Foreground" Value="Orange" />
11: <Setter Property="FontWeight" Value="Bold" />
12: <Setter Property="FontSize" Value="15" />
13: </Style>
14:
15: <!-- implicit style (no key - just a TargetType) -->
16: <Style TargetType="Button">
17: <Setter Property="Foreground" Value="Orange" />
18: <Setter Property="Background" Value="Yellow" />
19: </Style>
20: </ResourceDictionary>
Note the implicit style for Button – in Silverlight 4 beta, creating a style without a key will apply that style to all instances of the target type in an application, unless they already have an explicit style set (or if they’re in another control’s template).
Step 3: Custom Build Events: Packaging Themes as .zip Files
Our sample themes are pretty trivial, but they could potentially be quite large – a theme for a big app might have a large number of explicit styles, and could easily reach hundreds of kilobytes in size. Since they’re just .xaml files that contain text, they should compress well – so let’s turn them into standard .zip files, which the web readily recognizes as application/zip MIME types.
We could compress the files manually, but given that we’ve built the themes in Visual Studio anyway, why not create a custom build event to do it automatically.
Steps to accomplish this:
- Get a commandline tool for compressing files. I prefer 7-Zip, which is an open-source program that has a standalone .exe for command line use. Make sure to get the Command Line version (7za.exe). Notice I added this to the web project in the solution explorer screenshot above.
- Add a custom build action to the web project to compress each theme using Z-Zip. In the Build Events section of the web project properties, add the following:
for %%a in (”$(ProjectDir)themes\*.xaml”) do “$(ProjectDir)7za.exe” a -tzip “$(ProjectDir)ClientBin\CompressedThemes\%%~na.zip” “$(ProjectDir)themes\%%~na.xaml”
The result should look like:
Breaking this down into parts:
for %%a in -loop through each file, storing filename as variable “a”
(”$(ProjectDir)themes\*.xaml”) – find .xaml in <projectdir>/themes
do
“$(ProjectDir)7za.exe” a –tzip – call 7-zip to add (“a”) to zip file (“tzip”)
“$(ProjectDir)ClientBin\CompressedThemes\%%~na.zip” – zip file
“$(ProjectDir)themes\%%~na.xaml” – source .xaml file
Note that the special syntax “~n” in “%%~na.xaml” means take the only the “name” part of the filename from the %%a variable and omit the .xaml extension.
This will find every .xaml file in the themes folder and turn it into a compressed .zip file in the ClientBin/CompressedThemes folder every time we build the project.
Step 4: Downloading and Applying Theme
We’ll need to determine what theme to download when the app starts – this could be based on a login form, policy, or any number of things. For the purposes of our example, let’s just assume we’re SuperAwesomeCo and want “theme 1.xaml”.
There’s a few things to do here – download a compressed theme zip file, uncompress it, parse out the theme and create a ResourceDictionary, and apply the theme across the application. Thankfully, we can do all that with a few lines of code in App.xaml.cs – no need to change the xaml or code for any content pages. The code is fairly straightforward:
1: private string _themeZipFilePath = "CompressedThemes/theme 1.zip";
2: private string _themeContentFileName = "theme 1.xaml";
3:
4: private void Application_Startup(object sender, StartupEventArgs e)
5: { ...
6: // download a target compressed theme file
7: WebClient webclient = new WebClient();
8: webclient.OpenReadCompleted += new OpenReadCompletedEventHandler(webclient_OpenReadCompleted);
9: webclient.OpenReadAsync(new Uri(App.Current.Host.Source, _themeZipFilePath));
10: }
11:
12: void webclient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
13: {
14: if (e.Cancelled)
15: return;
16: else if (e.Error != null)
17: return; // error handling here
18:
19: // retrieve XAML file from the zip package
20: StreamResourceInfo zipInfo = new StreamResourceInfo(e.Result, "application/zip");
21: StreamResourceInfo themeInfo = Application.GetResourceStream(zipInfo, new Uri(_themeContentFileName, UriKind.Relative));
22:
23: // read and load the XAML file as a ResourceDictionary
24: string themeXaml = new StreamReader(themeInfo.Stream).ReadToEnd();
25: ResourceDictionary theme = XamlReader.Load(themeXaml) as ResourceDictionary;
26:
27: // merge ResourceDictionary into the application's resources
28: Application.Current.Resources.MergedDictionaries.Add(theme);
29: ...
30: }
When the app starts, we create a new WebClient instance to download the zip file. Note this is why we needed the web project while debugging – there has to be a server to handle the request. We could also have used a webservice to retrieve the themes, but a direct download with WebClient serves our purposes nicely for this example.
Once the file has been downloaded, we initialize a StreamResourceInfo with an application/ zip content type. Since we then know it’s a zip file, we can grab the xaml file out of the zip file by calling GetResourceStream, and read it using a StreamReader.
Finally, we can use XamlReader to parse the file as a ResourceDictionary, and merge it into the application resources MergedDictionaries collection – this will make its content available to the entire application (making the headerTextBlockStyle static resource lookup for an explicit style in MainPage.xaml valid), and automatically apply any implicit styles it contains!
Step 5: Niceties: Displaying a Splash Screen While Downloading
Even a zipped theme might take a perceptible amount of time to download. To mitigate this, we can make a splash screen that will display while the theme is downloading so the user doesn’t see the ugly unthemed page during the initial load time.
For testing purposes let’s just create a simple SplashScreen control, and in SplashScreen.xaml we’ll just show a “loading” message:
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="Loading..." HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="18" />
</Grid>
Now, let’s add to our theme-loading code in App.xaml.cs in order to display the splash screen while the theme is loading:
private void Application_Startup(object sender, StartupEventArgs e)
{
Grid root = new Grid();
root.Children.Add(new SplashScreen());
this.RootVisual = root;
// download theme
...
}
void webclient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
// apply theme
...
Grid root = this.RootVisual as Grid;
root.Children.Clear();
root.Children.Add(new MainPage());
}
Note we can only set the RootVisual once, so we’ll initialize it to an empty grid. When the app first starts, we’ll add a SplashScreen to the grid. After the theme is downloaded and merged into the application’s resources, we’ll clear the SplashScreen and create our real application root (MainPage).
Voilà! SuperAwesomeCo employees will now see a custom-themed application.
We can modify these themes (or add new ones) at any time without requiring any recompilation just by modifying the contents of the theme zip files.
Download
Source code:
NB: I didn’t include the 7-Zip executable, so you’ll want to grab the command line version from the 7-Zip download page and add it to the ExternalResourceSample.Web project if you want to use the custom build event to automatically create zip files.
Note this is a Silverlight 4 Beta project built in Visual Studio 2010 Beta 2, so content and APIs are subject to change.