Silverlight 4 & MEF – Switching on functionality based on application context

In my head, I see Silverlight 4 applications as either running;

  • In Browser
  • Out of Browser
  • Out of Browser and Trusted
  • Out of Browser and Trusted and in the presence of COM interop ( i.e. on Windows )

and you might write functionality that only works in certain of those contexts – e.g.;

  • HTML display only works out of browser.
  • You can only read the contents of “My Documents” when out of browser and elevated
  • You can only run any COM interop code when in the presence of COM interop

and I thought that it might be a good use of MEF as a way of only bringing in the functionality that is going to work in a particular context.

So, I defined an enum;

  [Flags]
  public enum AppContextStatus
  {
    InBrowser = 1,
    OutOfBrowser = 2,
    InOrOutOfBrowser = 3,
    OutOfBrowserTrusted = 4,
    OutOfBrowserCOMInterop = 8,
    All = 15
  }

and then I wanted to be able to figure out which of these particular states my code might be running in. So, a little class;

public static class AppContext
  {
    public static AppContextStatus CurrentStatus
    {
      get
      {
        AppContextStatus status =
          Application.Current.IsRunningOutOfBrowser ?
            AppContextStatus.OutOfBrowser : AppContextStatus.InBrowser;

        if ((status & AppContextStatus.OutOfBrowser) != 0)
        {
          if (Application.Current.HasElevatedPermissions)
          {
            status |= AppContextStatus.OutOfBrowserTrusted;
          }
          if (ComAutomationFactory.IsAvailable)
          {
            status |= AppContextStatus.OutOfBrowserCOMInterop;
          }
        }
        return (status);
      }
    }
  }

Then, what I wanted to do was to ensure that when I export a component’s functionality into MEF that component is only brought into play if its requirements around this “AppContext” are suitable. That is – if the application is running in the browser but the component needs to be “out-of-browser” then that component should not be available.

I wanted something similar to the PartCreationPolicy attribute that MEF uses to say whether your application supports [Shared/NonShared/Any] in terms of its creation policy. In MEF, you attribute your component with a PartCreationPolicy and MEF then uses that as part of its matching between the import contract and the export contract.

So, initially I looked to see if PartCreationPolicyAttribute derived from some MEF base class that I could also derive from in order to add metadata to an exported component in the same way that it did. But, it derived from Attribute and there didn’t seem to be a way that I could hook into the same process.

I tried a second route in that a more general way of adding metadata is to use the PartMetadataAttribute attribute to build up a collection of [string:object] metadata that lives on a component. However, PartMetadataAttribute is sealed so I couldn’t derive from that and use it for my own purposes.

So, in the end I just defined my own constant for use in a PartMetadata attribute instantiation as in something like this;

  [Export(typeof(IView))]
  [PartCreationPolicy(CreationPolicy.NonShared)]
  [PartMetadata(AppContextConstants.AppContextKey, AppContextStatus.All)]
  public partial class TimeView : UserControl, IView
  {
    public TimeView()
    {
      InitializeComponent();
    }
    [Import("TimeViewModel")]
    public object ViewModel
    {
      set
      {
        this.DataContext = value;
      }
    }
  }

and then I put into place a simple filtered Catalog ( see docs ) which uses the current AppContext to figure out which parts to return based on whether the application is in-browser/out-of-browser and so on. That is;

  public class AppContextCatalog : ComposablePartCatalog
  {
    public AppContextCatalog(ComposablePartCatalog innerCatalog)
    {
      this.innerCatalog = innerCatalog;
    }
    public override IQueryable<ComposablePartDefinition> Parts
    {
      get
      {
        AppContextStatus appStatus = AppContext.CurrentStatus;

        var query =
          from p in this.innerCatalog.Parts
          where 
            (
              p.Metadata.ContainsKey(AppContextConstants.AppContextKey) &&
                (((AppContextStatus)p.Metadata[AppContextConstants.AppContextKey] & 
                  appStatus) != 0)
            ) || 
            (
              !p.Metadata.ContainsKey(AppContextConstants.AppContextKey)
            )            
          select p;

        return (query);
      }
    }
    ComposablePartCatalog innerCatalog;
  }

which is just taking an existing catalog and filtering it down to only include the components that say they are suitable in the current environment that the application is running in. It also includes any components that don’t mention AppContextKey as it’d be a little unfair to exclude them 🙂

I can easily set up a catalog like this one using a bit of code to pull in all the exported components from my current assembly as in;

      AppContextCatalog catalog = new AppContextCatalog(
        new AssemblyCatalog(Assembly.GetExecutingAssembly()));

      CompositionHost.InitializeContainer(new CompositionContainer(catalog));

and then I can start building some views. For example – a view that is always available because it just displays the current time 🙂 Here’s the View XAML/Code and then the ViewModel code ( which provides the current time 🙂 )

<UserControl x:Class="SilverlightApplication5.Views.TimeView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White">
        <Viewbox>
            <TextBlock
                Text="{Binding Time}" />
        </Viewbox>
    </Grid>
</UserControl>

 [Export(typeof(IView))]
  [PartCreationPolicy(CreationPolicy.NonShared)]
  [PartMetadata(AppContextConstants.AppContextKey, AppContextStatus.All)]
  public partial class TimeView : UserControl, IView
  {
    public TimeView()
    {
      InitializeComponent();
    }
    [Import("TimeViewModel")]
    public object ViewModel
    {
      set
      {
        this.DataContext = value;
      }
    }
  }
  [Export("TimeViewModel", typeof(object))]
  [PartCreationPolicy(CreationPolicy.NonShared)]
  public class TimeViewModel
  {
    public string Time
    {
      get
      {
        return (DateTime.Now.ToShortTimeString());
      }
    }
  }

Note that the view model is imported into the view by just using a name to match them up but, more importantly, the view says that it is available in AppContextStatus.All whereas this next view below uses the WebBrowser which doesn’t really do much inside of the browser – here’s the code/XAML;

<UserControl x:Class="SilverlightApplication5.Views.HtmlView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White">
        <WebBrowser
            Source="http://localhost:32768/SilverlightApplication5.Web/ClientBin/TestPage.html" />
    </Grid>
</UserControl>

  [Export(typeof(IView))]
  [PartMetadata(AppContextConstants.AppContextKey, AppContextStatus.OutOfBrowser)]
  public partial class HtmlView : UserControl, IView
  {
    public HtmlView()
    {
      InitializeComponent();
    }
  }

this WebBrowser control is only communicating with the site-of-origin so it doesn’t need to be trusted and consequently the view is marking itself as needing OutOfBrowser but isn’t asking to be trusted. So, this view won’t exist when we run inside a browser.

By contrast, this next view is trying to go to http://www.microsoft.com and so it needs to be trusted and so its code/XAML looks like;

<UserControl x:Class="SilverlightApplication5.Views.TrustedHtmlView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White">
        <WebBrowser
            Source="http://www.microsoft.com" />
    </Grid>
</UserControl>

  [Export(typeof(IView))]
  [PartMetadata(AppContextConstants.AppContextKey, AppContextStatus.OutOfBrowserTrusted)]
  public partial class TrustedHtmlView : UserControl, IView
  {
    public TrustedHtmlView()
    {
      InitializeComponent();
    }
  }

and, because it’s going beyond site-of-origin it marks itself as needing OutOfBrowserTrusted. Finally, this view uses a little COM interop – here’s the XAML/Code and the “ViewModel” class that sits behind it;

<UserControl x:Class="SilverlightApplication5.Views.FilesView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    
    <Grid x:Name="LayoutRoot" Background="White">
        <ListBox
            ItemsSource="{Binding Files}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock
                        Text="{Binding}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>

  [Export(typeof(IView))]  
  [PartCreationPolicy(CreationPolicy.NonShared)]
  public partial class FilesView : UserControl, IView
  {
    public FilesView()
    {
      InitializeComponent();
    }
    [Import("FilesViewModel")]
    public object ViewModel
    {
      set
      {
        this.DataContext = value;
      }
    }
  }
  [Export("FilesViewModel", typeof(object))]
  [PartCreationPolicy(CreationPolicy.NonShared)]
  [PartMetadata(AppContextConstants.AppContextKey, AppContextStatus.OutOfBrowserCOMInterop)]
  public class FilesViewModel
  {
    public FilesViewModel()
    {
      // being lazy and doing this in the viewmodel
    }
    public IEnumerable<string> Files
    {
      get
      {
        return (Directory.EnumerateFiles(
          Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)));
      }
    }
  }

what I really like about that last exampple is that the View says nothing about whether it may or may not need to run in or out of browser. However, it imports a ViewModel and the ViewModel says that it only runs out of browser with COM interop. Therefore, the view won’t show up in scenarios where its ViewModel isn’t supported. Cool? Yes!

It’s easy to construct a simple UI to display these views in ( say ) a TabControl with something like this;

<Grid x:Name="LayoutRoot" Background="White">
        <ctl:TabControl
            ItemsSource="{Binding Tabs}" />
    </Grid>

and a little bit of code;

  public partial class MainPage : UserControl
  {
    [ImportMany]
    public IEnumerable<IView> Views { get; set; }

    public IEnumerable<TabItem> Tabs
    {
      get
      {
        return (this.Views.Select(
          (v, i) =>
            new TabItem()
            {
              Header = string.Format("View {0}", i),
              Content = v
            }));
      }
    }

    public MainPage()
    {
      InitializeComponent();

      PartInitializer.SatisfyImports(this);

      this.Loaded += (s, e) =>
        {
          this.DataContext = this;
        };
    }
  }

notice that the need for both a Views and a Tabs property here is really just down to how the TabControl works.

Now, if I run this application in the browser I see something like;

image

that is – a single view is available in the browser. Out of browser but untrusted gives;

image

i.e. the original view is still there but now a new view has arrived because we’re running out of browser. Running trusted then gives a 3rd and a 4th view;

image image

although that last view wouldn’t show up on OS X.

I’m not 100% sure whethere there’s a better way to do this in MEF than to add a filtered catalog like this and then use PartMetadata but someone will probably tell me if there is. If you want to play with the source then it’s here for download.