Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

The WPF Podcatcher Series - Part 2 (Structural Skinning)

0.00/5 (No votes)
5 Mar 2008 1  
The second article in a series devoted to a WPF application that plays streaming audio podcasts off the Internet. This article discusses the idea and implementation of look-less applications.

InitialScreenshot.png

Table of Contents

Introduction

About two months ago I unveiled a pet project of mine, named Podder. It is an audio podcast player (a.k.a. podcatcher) built in WPF. The application comes pre-loaded with a dozen or so podcasts, so you can start listening to episodes right away. If there are other podcasts that you would like to listen to, it is easy to add them into Podder at any time. Podder is free, fun, and easy to use.

The first article in this series explained that Podder supports what I call “structural skinning.” This article explores what structural skinning is, how I implemented it, and even shows structural skinning in action as we examine a truly fantastic Podder skin built by Grant Hinkson, a professional Visual Designer.

System Requirements

At the top of this article, you will find the Podder source code and binaries available for download. These are different from the file downloads on the first article in this series. Many improvements and features have gone into Podder since the first version, so be sure to download the latest and greatest bits.

Binaries

If you do not want to browse the application’s source code, then just download the binaries and run Podder.exe. You must have the .NET Framework 3.5 installed for Podder to run.

Source Code

The other option is to download the source code. You must have Visual Studio 2008 installed to open and build the Podder solution. If you want to build the project containing Grant’s skin, you must have the Infragistics NetAdvantage for WPF v7.2 (or later) installed. You can get a free trial of it here.

If you download the source code but do not have or want to install the Infragistics WPF controls, you will need to exclude the Podder.GrantHinkson project from the solution. To do that in Visual Studio 2008, open Solution Explorer, right-click on the Podder.GrantHinkson project, and select ‘Unload Project’ from the context menu. Keep in mind that if you do this, you will not be able to use Grant’s skin and will be stuck with my plain old boring UI.

Background

Throughout this article, we explore what it means to support “structural skinning.” Briefly, it means that the application’s UI is completely replaceable at run time. Previous articles of mine have already discussed almost all of the concepts and techniques employed to implement structural skinning. If you have not already done so, I recommend you read ‘Creating a Skinned User Interface in WPF’ and then ‘Using MVC to Unit Test WPF Applications’. I consider those articles to be prerequisites of this article.

This article demonstrates the power of structural skinning by showing how a professional visual designer skinned Podder. Grant Hinkson, a colleague of mine at Infragistics, was the designer with whom I worked on this project. It was an eye-opening experience for me to work closely with an accomplished designer, which I discussed in this blog post. It was a pleasure to work with Grant, since he was very excited about the project and strived to do a fantastic job. Thanks a lot, Grant!

Watch Podder in Action

Screenshots can only convey so much. Now that CodeProject articles can contain Silverlight videos, I decided to show Podder in action. I would like to thank Karl Shifflett for taking the time to write his excellent article that explains how to create and deploy Silverlight videos in CodeProject articles.

Without further adieu, I give you Podder in action…



If you do not see the video embedded in the article or would like to watch this video at a larger size, please click here.

Listen to Craig Shoemaker Interview Grant Hinkson

Working with Grant on Podder was an eye-opening experience for me. We view WPF in very different ways, and enjoy learning new ways to think about WPF from each other. Grant is an authority on WPF visual design. He has been doing visual design work on WPF controls and applications for years at Infragistics. He has given several presentations about the designer/developer workflow, which is a very important aspect of professional WPF development.

I also have the privilege of working with Craig Shoemaker, host of Polymorphic Podcast and the new Pixel8 podcast. Craig happily agreed to interview Grant and discuss what it was like working on Podder as a visual designer. The following discussion captures the perspective of an accomplished WPF visual designer, his thoughts about working on Podder, and WPF visual design in general. I highly recommend you check this out:

Craig Shoemaker and Grant Hinkson Discussing Podder and WPF Visual Design (duration 9:37)

I would like to thank Craig and Grant for taking the time to have this discussion, and for Craig to pull it all together into an MP3 file.

Definition of Structural Skinning

I think it is easiest to explain what structural skinning is by providing an analogy. WPF controls are said to be “look-less” because they do not have an intrinsic visual appearance. They depend on control templates to provide an “explanation” of how the control should be rendered. A template is like a cookie cutter that WPF uses to generate a tree of visual elements at run time. That tree of visual elements is what you see when you look at a control on the screen.

A control is responsible for exposing state via properties and providing behavior, such as raising an event in response to user interaction. Controls are not responsible for knowing what they should look like. All controls have a default template, which is why you can see a control on the screen even if you do not assign it a control template. If you want to alter the way a control looks, all you have to do is assign a custom control template to its Template property.

Take the idea of a look-less control, apply it to an entire application, and that is what I refer to as “structural skinning.” You might think of an application that supports structural skinning as a look-less application. It has a default appearance baked in, but you can easily replace the default UI with a completely new UI at run time or compile time. When I say, “replace the UI” I literally mean swap out all of the controls and elements in a Window and replace them with an entirely new visual tree. One, roughly accurate, way to think about it is that a structural skin is to a look-less application as a control template is to a look-less control.

Podder is a look-less application, which means that visual designers can create an endless array of imaginative skins to expose its data and behavior to the end-user. Grant, the visual designer who created Podder’s new skin, worked almost exclusively in Expression Blend and wrote almost no code to create his skin. When a system’s architecture supports structural skinning, the developers work in Visual Studio, and visual designers work in Blend. That is the way it should be, in my opinion.

Pros and Cons of Designing Look-less Applications

Most applications do not need to swap out their user interfaces at run time. This is definitely an esoteric functional requirement for most line-of-business (LOB) applications. With that said, there are reasons to keep this concept in mind when designing and building applications in WPF. Let us take a moment to discuss when it does and does not make sense to use this technique.

Obviously, if you need to support changing the entire UI at run time, structural skinning is very useful. Common examples of this are software products that allow for high degrees of end-user customization. These are typically products in the entertainment and multimedia space, such as Windows Media Player.

On the other hand, suppose you are creating a business application under very tight time constraints. You might not have time for such niceties as a clean design, unit testing, visual designers, and sleep. In that situation, perhaps it is best to avoid structural skinning. It takes time, both up front and during the development process, to make an application look-less. If you are stuck doing slash-and-burn development, you probably do not have time to read this article, so this is a moot point.

Most applications do not require the ability to swap out their UI at run time, and their developers are not working under wretched sweatshop conditions (I hope). What do those applications stand to gain by having the structural skinning concept factored into their design? What are the benefits? What are the risks?

The single most important benefit of designing an application to be look-less is that it practically requires you to build a clean, modular, unit testable, loosely coupled architecture. In a truly look-less design, the application logic (Controller) should make no assumptions about the skin (View) hosted in the user interface. The skin binds to properties on data objects (Model), and communicates user interaction to the application logic via routed commands. This arrangement creates code that is easy to unit test, because testing the application logic and data model does not involve UI code. As I mentioned previously, you can learn much more about implementing Model-View-Controller in WPF by reading this article. Structural skinning depends heavily on the use of MVC.

Another benefit of creating look-less applications comes into play when you are working with visual designers. Developers and designers should have as few dependencies on each other as possible. If a developer wants to forge ahead and create a new Window in an application, but cannot collaborate with a visual designer beforehand, he/she can create a temporary skin for that Window. That skin might be useful for testing out the new Window’s features while debugging. Later on, after a visual designer has replaced it with the “real” skin, the developer can easily use the old dev skin (chock full of convenient debugging info) if necessary.

From a visual designer’s perspective, having the flexibility to apply a UI via skins makes it easy to prototype various layouts, interaction models, color schemes, etc. You can create several skins, test them out, compare them against each other in design meetings, and throw away the ones that do not work out. In addition, the creation of skins typically does not require much, if any, coding since all of the interaction logic already exists in the visual container that hosts a skin.

The biggest risk of designing for structural skinning is that it could overcomplicate an otherwise simple application. For example, if you are building a simple utility application for a small number of users, it is probably overkill to burden the application design with the concepts described herein. Additionally, if your development team is not familiar with WPF, MVC, or loosely coupled architectures, the learning curve might be prohibitively steep.

The Basics of Structural Skinning

This section of the article reviews the essential techniques and classes that support structural skinning. We begin with a brief refresher on how to implement MVC and UI skinning in WPF. First, look at the following diagram, which summarizes how the various pieces fit together.

StructuralSkinningDiagram.png

MVC

Structural skinning depends heavily on my technique for implementing Model-View-Controller (MVC) in WPF. Here is a very brief refresher on how that works.

The View is responsible for rendering data to the screen, and handling simple interaction logic (ex. changing colors when the mouse is over an element). The Model is responsible for storing the state of the application and executing domain logic. The Controller is responsible for performing application logic in response to user interaction, by wiring the View and Model together. Elements in the View bind to properties on Model objects and execute routed commands to inform the Controller of user interaction. Sometimes the View directly updates the Model via two-way data bindings. The Controller tells the Model to perform some processing in response to user input, and can request that the View’s visual container (i.e. a Window) perform some UI-related task. For a much more thorough review of this topic, please read ‘Using MVC to Unit Test WPF Applications’.

UI Skinning

Structural skinning utilizes the skinning techniques described in my ‘Creating a Skinned User Interface in WPF’ article. Here is a very brief refresher on how that works.

Hierarchical resource resolution, merged resource dictionaries, and dynamic resource references can work together to create a simple and flexible system for skinning any user interface. You create Styles that target various elements in a user interface, collectively known as a skin. You package all of those Styles into a resource dictionary and give them pre-determined resource keys. You use those same resource keys as the keys of dynamic resource references applied to the Style property of elements in your UI. When you merge one of those resource dictionaries into the Application’s resource collection, elements with dynamic resource references applied to their Style property automatically use those Styles. At that point, you have applied a skin to the UI.

Putting the Pieces Together

Now that we have reviewed the two pillars of look-less applications, we can summarize how it all works in one sentence:

Look-less application Views are dynamically loaded resources.

The remainder of this article explains what that sentence means. For the rest of this article, I use the terms “skin” and “structural skin” interchangeably. A skin contains one or more views, a la MVC.

Discovering Skins at Run Time

From an application’s perspective, a skin can come from one of two places: either it is in the application itself, or it is loaded from an external source at run time. Podder has a default skin baked into its main assembly, and has the ability to load skins from arbitrary .NET DLLs.

When Podder starts up, it needs to create a list of all available skins for the user to select. It searches through a “Skins” directory, which must exist in the same directory as the Podder EXE, and looks for subdirectories that contain DLLs that match a certain naming convention. It loads each of those DLLs into a child AppDomain and uses some reflection magic to extract information about the skin contained in that DLL. The skin DLLs are loaded in a child AppDomain so that we can unload all of those external DLLs, thus reducing the Podder working set and improving performance.

Here is the code that discovers information about skins at start up:

/// <summary>
/// A read-only list of information about every available skin.
/// </summary>
public class SkinInfoCollection : ReadOnlyCollection<SkinInfo>
{
    public SkinInfoCollection()
        : base(new List<SkinInfo>())
    {
        // Add the default skin, which is baked into Podder.
        base.Items.Add(SkinInfo.Default);

        // Now load the external skins, if any exist.
        this.LoadExternalSkins();

        (base.Items as List<SkinInfo>).Sort();
    }

    void LoadExternalSkins()
    {
        string location = Assembly.GetExecutingAssembly().Location;
        string rootDir = Path.GetDirectoryName(location);
        string skinsDir = Path.Combine(rootDir, "Skins");
        if (Directory.Exists(skinsDir))
        {
            AppDomain childDomain = AppDomain.CreateDomain("LoadingZone");
            try
            {
                string thisAssembly = Assembly.GetExecutingAssembly().FullName;
                string typeName = typeof(SkinAssemblyLoader).FullName;

                // Create an instance of SkinAssemblyLoader in the  
                // child AppDomain. A proxy to the object is returned.
                var loader = 
                    childDomain.CreateInstanceAndUnwrap(thisAssembly, typeName) 
                    as SkinAssemblyLoader;

                string[] skinFolders = Directory.GetDirectories(skinsDir);
                foreach (string skinFolder in skinFolders)
                {
                    string[] resourceAssemblies = 
                        Directory.GetFiles(skinFolder, "Podder.*.dll");

                    Debug.Assert(
                        resourceAssemblies.Length == 1, 
                        "There are too many skin DLLs here: " + skinFolder);

                    foreach (string assm in resourceAssemblies)
                    {
                        var skinInfo = loader.GetSkinInfoForAssembly(assm);
                        if (skinInfo != null)
                            base.Items.Add(skinInfo);
                    }
                }
            }
            finally
            {
                AppDomain.Unload(childDomain);
            }
        }
    }

    /// <summary>
    /// This class is instantiated in a separate AppDomain.  
    /// It loads each resource assembly and extracts some
    /// information from them.  When the object is finished 
    /// being used, the AppDomain in which it lives is unloaded.
    /// </summary>
    private class SkinAssemblyLoader : MarshalByRefObject
    {
        /// <summary>
        /// This method runs in a temporary AppDomain, and 
        /// extracts information about a resource assembly.
        /// </summary>
        /// <param name="assemblyFile">
        /// An absolute file path to a resource assembly.
        /// </param>
        internal SkinInfo GetSkinInfoForAssembly(string assemblyFile)
        {
            Assembly assm = null;
            try
            {
                assm = Assembly.LoadFrom(assemblyFile);
            }
            catch (Exception ex)
            {
                Debug.Fail("Error while loading resource assembly: " + ex.Message);
            }
            if (assm == null)
                return null;

            object[] attrs = 
                assm.GetCustomAttributes(typeof(PodderSkinAssemblyAttribute), false);

            if (attrs == null || attrs.Length != 1)
                return null;

            var attr = attrs[0] as PodderSkinAssemblyAttribute;
            return new SkinInfo(assemblyFile, attr.SkinName);
        }
    }
}

Applying a Skin at Run Time

Applying a skin really boils down to merging a ResourceDictionary into the Application’s Resources collection. From the perspective of loading skins, a skin is just a ResourceDictionary declared in a file called “Resources.xaml” that can be extracted from an assembly. The name “Resources.xaml” is an arbitrary identifier; I could have used any valid file name. Later we will see what those resource dictionaries contain, but for now, we focus on how to import a skin into the application at run time.

Podder applies a skin at two points in time: when it starts up, and when the user selects a skin in the UI. It automatically applies the default skin the first time Podder runs on a user’s machine, or if no external skins exist. After the first run of the application, it loads whatever skin the user selected when the application previously shut down.

Here is the class responsible for applying a skin:

 public partial class App : Application
 {
     public App()
     {
         // Thanks to Marlon Grech for the great idea of elevating the app's
         // processing priority, so that episode playback is not interrupted 
         // when the CPU is undergoing heavy loads.
         Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
     }

    /// <summary>
    /// Loads the specified skin and applies it to the Podder user interface.
    /// </summary>
    /// <param name="skinInfo">The skin to load.</param>
    public static void ApplySkin(SkinInfo skinInfo)
    {
        if (skinInfo == null)
            throw new ArgumentNullException("skinInfo");

        var uiSettings = PodderDataSource.Instance.PersistedUISettings;

        // Do not apply the same skin two times in a row.
        bool ignore =
            App.Current.Resources.MergedDictionaries.Count != 0 &&
            skinInfo.Equals(uiSettings.Skin);

        if (ignore)
            return;

        ResourceDictionary skinResources = App.GetSkinResources(skinInfo);
        if (skinResources == null)        
            skinResources = App.GetSkinResources(SkinInfo.Default);

        if (0 < App.Current.Resources.Count)
            App.Current.Resources.Clear();

        App.Current.Resources.MergedDictionaries.Add(skinResources);

        uiSettings.Skin = skinInfo;
    }

    static ResourceDictionary GetSkinResources(SkinInfo skinInfo)
    {
        ResourceDictionary skinDictionary = null;
        if (skinInfo.IsDefault)
        {
            skinDictionary = new ResourceDictionary();
            skinDictionary.Source = 
                new Uri("DefaultSkin/Resources.xaml", UriKind.Relative);
        }
        else
        {
            try
            {
                // Load all of the DLLs used by the skin into our AppDomain.
                string path = Path.GetDirectoryName(skinInfo.AssemblyName);
                string[] assemblies = Directory.GetFiles(path, "*.dll");
                foreach(string assembly in assemblies)
                    Assembly.LoadFrom(assembly);

                // Load the master ResourceDictionary for the skin.
                string assemblyName = 
                    Path.GetFileNameWithoutExtension(skinInfo.AssemblyName);
                string packUri = assemblyName + ";component/Resources.xaml";
                Uri resourceLocator = new Uri(packUri, UriKind.Relative);
                skinDictionary = 
                    Application.LoadComponent(resourceLocator) 
                    as ResourceDictionary;
            }
            catch (Exception ex)
            {
                Debug.Fail("Could not load skin: " + ex.Message);
            }
        }
        return skinDictionary;
    }
}

Consuming Skin Resources

Simply merging some skin resources into an Application’s resource collection is not going to make the skin appear in the UI. We must create a link between those resources and the UI container(s) displayed by the application. For example, we must use dynamic resource references to “bind” Podder’s main Window to the skin loaded into the Application’s main ResourceDictionary. This is how the application’s Views are dynamically loaded via resource bindings.

Here is Podder’s main Window XAML:

<ui:ConfigurableWindow 
  x:Class="Podder.UI.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:model="clr-namespace:Podder.Model;assembly=PodderLib"
  xmlns:cmd="clr-namespace:Podder"
  xmlns:ui="clr-namespace:Podder.UI"

  Content="{DynamicResource VIEW_MainWindow}"

  DataContext="{x:Static model:PodderDataSource.Instance}"

  Icon="..\Images\app.ico"
  Title="Podder"
  >
  <ui:ConfigurableWindow.CommandBindings>
    <!-- CommandBindings omitted for clarity... -->
  </ui:ConfigurableWindow.CommandBindings>
</ui:ConfigurableWindow>

The crucial point to observe is that the Window’s Content is given a dynamic resource reference, effectively binding it to a resource whose x:Key is “VIEW_MainWindow”. Whatever object is found in the resource hierarchy with that x:Key will be displayed by the Window. If no object with that x:Key is found, the Window will remain empty. In practice, a UserControl, which acts as a View, is loaded into the Window. That View binds to the Window’s DataContext (the Model) and executes routed commands, which are handled by the Window’s list of CommandBindings and eventually dispatched to the Window’s Controller.

At this point, we have seen the mechanics of how MVC and UI skinning work together, to create a system where Views are dynamically loaded and consumed as resources. Now let us briefly turn our attention to how a skin is set up.

The Structure of a Structural Skin

As we saw earlier, Podder loads a structural skin by extracting a ResourceDictionary from a file called Resources.xaml. That file is located in an assembly, and you compile it with the default Build Action of ‘Page’. If the skin is contained in a separate DLL from Podder (i.e. it is not the default skin), that file must exist at the root level of the project. Podder will not extract the file from a subdirectory of the project.

The ResourceDictionary in Resources.xaml is a collection of resources that a skin needs to apply to Podder to achieve its visual theme. Some of those resources are the views applied to the UI containers, as seen in the previous section.


Here is a truncated version of the ResourceDictionary used for the application’s default skin:

<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:model="clr-namespace:Podder.Model;assembly=PodderLib"
  xmlns:podder="clr-namespace:Podder"
  xmlns:ui="clr-namespace:Podder.UI"
  xmlns:views="clr-namespace:Podder.DefaultSkin.Views"
  >
  <!-- VIEWS 
  This view is loaded by the MainWindow.  
  It is located based on the x:Key value, which is used 
  in a dynamic resource reference by MainWindow.
  -->

  <views:MainWindowView x:Key="VIEW_MainWindow" />

  <!-- Other resources omitted for clarity... -->
</ResourceDictionary>

The main Window hosts and displays the MainWindowView, which is a UserControl. Since it is part of the default skin, other skins do not use it. They would have their own version of MainWindowView, as a completely separate control. Here is what the default skin’s MainWindowView looks like after it has been loaded into Podder and pumped full of data:

DefaultSkin_MainWindowView.png

Here is the default skin’s MainWindowView XAML:

<UserControl 
  x:Class="Podder.DefaultSkin.Views.MainWindowView"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:podder="clr-namespace:Podder"
  xmlns:views="clr-namespace:Podder.DefaultSkin.Views"
  Background="{DynamicResource PrimaryViewBackBrush}"
  >
  <UserControl.Style>
    <Style>
      <Style.Triggers>
        <!-- 
        When downloading a podcast's RSS feed we show a wait cursor. 
        -->
        <DataTrigger 
          Binding="{Binding Path=IsRetrievingEpisodeList}" 
          Value="True"
          >
          <Setter 
            Property="FrameworkElement.Cursor" 
            Value="Wait" 
            />
        </DataTrigger>
      </Style.Triggers>
    </Style>
  </UserControl.Style>
  <Grid Margin="4">
    <Grid.Resources>
      <Style TargetType="GroupBox">
        <Setter 
          Property="Background" 
          Value="{DynamicResource GroupBoxBackBrush}" 
          />
        <Setter 
          Property="IsEnabled" 
          Value="{Binding Path=NetworkConnection.IsAvailable}" 
          />
      </Style>
    </Grid.Resources>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <GroupBox 
      x:Name="skinsGroupBox"
      Grid.Row="0" 
      Header="{podder:ResourceString MainWindowView_SkinsHeader}" 
      >
      <ComboBox 
        IsSynchronizedWithCurrentItem="True" 
        ItemsSource="{Binding Path=Skins}" 
        Width="200" 
        />
    </GroupBox>
    <GroupBox 
      Grid.Row="1" 
      Header="{podder:ResourceString MainWindowView_PodcastsHeader}"
      >
      <views:MainWindowView_Podcasts />
    </GroupBox>
    <GroupBox 
      Grid.Row="2" 
      Header="{podder:ResourceString MainWindowView_EpisodesHeader}"
      >
      <views:MainWindowView_Episodes />
    </GroupBox>
    <GroupBox 
      Grid.Row="3" 
      Header="{podder:ResourceString MainWindowView_ControlPanelHeader}"
      >
      <views:MainWindowView_ControlPanel />
    </GroupBox>
  </Grid>
</UserControl>

MainWindowView contains several smaller UserControls. This keeps each piece of the UI in a small, simple, separate file.

Conclusion

Podder is an audio podcast player application that can display an arbitrary user interface, called a structural skin. It has a default skin and, as of this writing, a new skin created by Grant Hinkson, an accomplished Visual Designer. Programs that allow for structural skinning are “look-less applications,” similar to the idea of look-less controls in WPF. The application offers a data model and a set of services via routed commands, which a skin can display and consume in a pluggable fashion. Structural skinning uses the Model-View-Controller and UI skinning techniques presented in other articles of mine here on CodeProject.

Revision History

  • March 5, 2008 – Created the article

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here