Mike Taulty's Blog
Bits and Bytes from Microsoft UK
Silverlight 4 & MEF – Switching on functionality based on application context

Blogs

Mike Taulty's Blog

Elsewhere

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.


Posted Mon, Feb 1 2010 8:41 AM by mtaulty
Filed under: ,

Comments

Glenn Block wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Mon, Feb 1 2010 12:38 PM

Hi Mike, nice post. You are certainly digging in well past the most common scenarios these days ;-)

Using part / metadata for this type of filtering does make sense, in fact I demoed something very similar in my PDC talk. In that case I decided to use a custom ApplicationStateAwareCollection rather than a catalog to apply the appropriate filters, as well as to apply ordering.

Using a catalog with part metadata is a good way to go if you have a global filter you want to apply. It is the approach we recommend for those cases. Using a custom collection which filters on metadata is useful when the filter is specific to a particular part rather than global (http://codepaste.net/1n2bw7 for an example). It also allows ordering. Finally it's just easier to use a custom collection as there is less change of shooting yourself in the foot.

One very tricky thing about custom catalogs that change based on state is getting recomposition/rejection right. If you don't get it right you will get inconsistent / weird behavior. I will take a look at your code and let you know.

Andrew Marshall wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Wed, Feb 3 2010 5:23 AM

Hi Mike,

I took a similar approach in my CUDA interop demo, but instead of using PartMetadata I created a MetadataAttribute called RuntimeEnvironmentAttribute, which implements an interface (IRuntimeRequirements, say) with a property I could query about the environment required to import the part.

This way, the import declaration type is

[ImportMany]

IEnumerable<Lazy<ExportedType,IRuntimeRequirements>> Extensions

so it can be filtered with

Extensions.Where( ext=>!ext.Metadata.ElevatedPermissionsRequired).Select(ext=>ext.Value);

for example.

What I would really prefer, is to invert this behaviour so that an import knows whether it is available or not, and MEF handles the filtering automatically. I don't like the fact that the application has to know that an import may or may not have attributes on it that need to be queried.

Andrew.

mtaulty wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Wed, Feb 3 2010 7:21 AM

Andrew,

Got you ( I think :-) ) - but the if the application uses a catalog that is specialised to filter out the parts that don't make sense in the particular runtime environment then "all" the application has to know is to use that particular catalog rather and the imports/exports are largely left untouched?

Mike.

Amr Elsehemy wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Wed, Feb 3 2010 2:43 PM

I am big fan of all your work (podcasts, posts and demos), and will have a MEF/SL talk at www.cairocodecamp.com later this month, and will use most of what you showed here and on channel 9.

So I would like to thank you :)

mtaulty wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Wed, Feb 3 2010 4:51 PM

Amr - thanks for the kind comments & I hope your code camp goes well - use whatever you like.

Mike.

Dudu wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Thu, Feb 4 2010 9:40 AM

Nice job. two powerful technologies coming together.

Can i do the same trick with network awareness?

Pull up views depending on my network status.

mtaulty wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Thu, Feb 4 2010 2:20 PM

Dudu,

Yes, you could do the same trick with network awareness. One thing to say is that network status can change during the lifetime of the application so you'd need to consider this as the component parts should get _recomposed_ when the network status changes - I'll perhaps do an example of this if I can to illustrate it as I think it's an interesting idea.

Mike.

sladapter wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Wed, Feb 10 2010 10:01 AM

Mike,

I followed your MEF tutorial, and I'm using MEF in my application. I have a main Silverlight Navigation app project. But I put all my Pages in a separate View Project. Each page is tagged with PageMetadataAttribute that inherited from ExportAttribute. This view project is packaged in a separate xap file. In the MainPage of my main project I download the this package and import those pages. Everything seemed fine until I go to a page that contains a DataGrid. I got the following error:

"System.Windows.Markup.XamlParseException: AG_E_PARSER_BAD_TYPE [Line: 40 Position: 156]

  at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator) ..."

The error line is my DataGrid tag line. Seems it does not think <data:DataGrid /> is a valid tag.

Then I found if I add my View Project to my MainProject's reference list, the error is gone, but that beats the whole purpose of using MEF (loose coupling).

Or if I add System.Windows.Controls.Data.dll to my MainProject references even my main project does not need this dll as dependency, this error is gone as well.

Then my question is how many extra dlls do I need to put into my MainProject reference list?  Why I need to reference all the dependency dlls of my View Package in my main project? Because in theory, I can switch my View project. So my main project has no idea what controls are used in my View package.

Did I do something wrong?

mtaulty wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Wed, Feb 10 2010 2:32 PM

sladapter,

I wonder if this is a limitation of the PackageCatalog as it stands today in the SL 4 Toolkit for the beta?

The docs for the Package class say;

"Downloads a secondary XAP and loads those assemblies into the AppDomain. Currently only supports a limited set of scenarios, namely only loads the list of AssemblyParts found in the AppManifest.xaml file of the secondary XAP. List of known issues: - Transparent Platform Extensions (TPE) aren't loaded supported. - Resources in the XAP directly are not supported, any needed resources need to be embedded in the assembly. - Versioning not supported. Currently if Silverlight finds another assembly with the same name but different version it will not load the new assembly. It purely uses the assembly name to identify already loaded assemblies."

and the docs for the PackageCatalog also say;

"This type is dependent on the Package class, which is currently an experimental API added in this assembly. Which makes this API also an experimental API and will likely change if it ever ships as part of Silverlight."

I'm hoping that this situation will improve by the RTM of Silverlight 4 but I can't make promises.

Thanks,

Mike.

sladapter wrote re: Silverlight 4 & MEF – Switching on functionality based on application context
on Wed, Feb 10 2010 7:53 PM

Mike,

Thank you for the explanation.  

I checked the my View Package's AppManifest.xaml file in the bin directory.  Among all the dlls in my View Project Reference list, System.Windows.Controls.Data.dll, System.Windows.Controls.Data.DataForm.Toolkit

and System.Windows.Interactivity.dll are not listed in the AppManifest.xam.  But it does include the AssemblyPart for Microsoft.Maps.MapControl.Common.dll, Microsoft.Maps.MapControl.dll. That explains why I had to add  System.Windows.Controls.Data.dll to my main project references for my DataGrid page to work, but I did not have to add Microsoft.Maps.MapControl.dll to the main project's references for my Bing map page to work.

Now the question would be what determines which dll is included in AppManifest.xaml?  Why some dlls are missing from this list?

Add a Comment

(optional)  
(optional)
(required)  
Remember Me?