Metro-style Class Browser

27th May 2012

Summary:
In this article I look at how to create a simple Metro-style browser for the new Windows 8 WinRT classes. Along the way we use a bit of WinRT reflection, LINQ-to-XML, and nice XAML layout features.

Introduction

When it became available in 'beta' format a few days ago, I bought myself a copy of Charles Petzold's new book "Writing Windows 8 Apps With C# and XAML". The book's informative and very good value for $10! I got to the point, about halfway through the book, where Petzold creates a small metro app to show all the classes that derive from DependencyObject. It's a nice example, but he hard-codes a list of example classes and uses them as a starting point. I decided to see if I could implement a simple 'class browser' app without hard-coding anything.

The first thing we need to solve is the simple .NET-oriented question, "where are the WinRT assemblies?!". If you look at the references in a C# Metro app you'll see the following:

The path for the Windows RT assembly reference points to C:\Program Files (x86)\Windows Kits\8.0\Windows Metadata. This is the developer version of WinRT, where all the various separate assemblies are compiled into a single, large file. The default set of WinRT assemblies are installed in C:\Windows\System32\WinMetadata:

Note that WinRT assemblies are not 'true' assemblies in the .NET sense. WinRT is a COM-based abstraction layer sitting directly on top of the Windows 8 kernel. WinRT class interfaces are exposed via binary .winmd (Windows Metadata) files. The 'assembly' terminology does seem to have been retained in WinRT though. For example:

Type.GetType("WinRT Class Name").GetTypeInfo().Assembly.FullName

will return the name of the WinRT assembly containing the specified class. For example:

"Windows.Data.Json, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime"

However, unless I'm missing something, it doesn't seem to be possible to dynamically load WinRT assemblies. The .NET-based System.Reflection.Assembly.Load() method (understandably) throws an 'operation not permitted' exception if you try.

If you look in the C:\Program Files (x86)\Windows Kits\8.0\Windows Metadata\EN directory you'll see a single Windows.xml file. This turns out to be an extremely useful starting-off point for our class browser app, as it lists all WinRT classes and interfaces, along with a short description of each! It also provides some information on namespaces, properties, etc. For example:

  <?xml version="1.0" encoding="utf-8"?>
  <doc>
    <assembly>
      <name>Windows</name>
    </assembly>
    <members>
      <member name="N:Windows.ApplicationModel">
        <summary>Provides a Metro style app with access to core system functionality.</summary>
      </member>
      <member name="T:Windows.ApplicationModel.DesignMode">
        <summary>Enables you to detect whether your app is in design mode in a visual designer.</summary>
      </member>
      <member name="P:Windows.ApplicationModel.DesignMode.DesignModeEnabled">
        <summary>Gets a value that indicates whether the process is running in design mode.</summary>
        <returns>True if the process is running in design mode; otherwise false.</returns>
      </member>
      <member name="T:Windows.ApplicationModel.Package">
        <summary>Provides information about an app package.</summary>
      </member>

Note that each name attribute of the member element has a one-character key, where 'T' is for 'type' (class), 'N' is namespace, 'P' is property, etc. So, parsing this large XML file will provide all the basic information we need to display a list of all WinRT classes. We can then use reflection to provide the class hierarchy for a selected class, along with other details like its properties, methods and events.

Reading XML

The first issue we run into concerns the fact that Metro apps don't have free access to the file system. Through the app's manifest, developers can configure which folders the app has potential access to (if the user grants permission). However, access is limited to the documents, music, pictures, and videos libraries. Therefore, we can't directly access Windows.xml in C:\Program Files (x86)\Windows Kits\8.0\Windows Metadata\EN. To get around the restriction I put a copy of the file in the project's Assets folder and set its Build Action property to Content.

To hold a list of all the classes, along with their descriptions, I created a TypeSummary class:

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using System.Threading.Tasks;
  
  namespace MetroClassBrowser
  {
      public class TypeSummary
      {
          public string Name { get; set; }
          public string Summary { get; set; }
  
          public TypeSummary() { }
          public TypeSummary(string name, string summary)
          {
              this.Name = name;
              this.Summary = summary;
          }
  
          public override string ToString()
          {
              return Name;
          }
      }
  }

I then added a TypeSummaryList property (which holds the list of classes and descriptions) to the app's main page:

  namespace MetroClassBrowser
  {
      public sealed partial class MainPage : Page
      {
          public List<TypeSummary> TypeSummaryList { get; set; }
          : 

The following code reads, parses and filters the XML into the TypeSummaryList list. Notice how quick and easy it is to asynchronously read the XML and then filter it using LINQ-to-XML:

  public MainPage()
  {
      this.InitializeComponent();
      CreateTypeSummaryFromXml();
  }

  private async  void CreateTypeSummaryFromXml()
  {
      XmlDocument doc = await ReadXml("Assets", "Windows.xml");
      var xml = doc.GetXml();
      
      // Create an XDocument we can use with LINQ-to-XML
      var xdoc = XDocument.Parse(xml);
      
      // LINQ-to-XML: select all elements that are WinRT types and 
      // create a list of TypeSummary...
      TypeSummaryList =
          (from member in xdoc.Descendants("member")
           where member.Attribute("name").Value.StartsWith("T:")
           select new TypeSummary
           {
               // Chop off the "T:" (T: indicates this is a type)
               Name = member.Attribute("name").Value.Substring(2),  
               Summary = member.Element("summary").Value,
           }).ToList<TypeSummary>();
  }
  
  private async Task<XmlDocument> ReadXml(string folder, string file)
  {
      StorageFolder storageFolder =
          await Windows.ApplicationModel.Package.Current.
              InstalledLocation.GetFolderAsync(folder);
      StorageFile storageFile = await storageFolder.GetFileAsync(file);
  
      return await XmlDocument.LoadFromFileAsync(storageFile);
  }

I created a simple UI for the app that essentially consists of a ListView to hold the list of classes and descriptions (I created a DataTemplate in <Page.Resources> that defines how each ListView item should appear), along with a series of TextBlock elements that show detailed info on the class selected in the ListView:

  <Page
      x:Class="MetroClassBrowser.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:MetroClassBrowser"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      FontSize="24"
      mc:Ignorable="d">
      
      <Page.Resources>
          <DataTemplate x:Key="classSummaryTemplate">
              <StackPanel>
                  <TextBlock Text="{Binding Path=Name}" 
                             FontSize="16" 
                             Margin="10" 
                             Foreground="MediumOrchid" 
                             TextWrapping="Wrap" />
                  
                  <TextBlock Text="{Binding Path=Summary}" 
                             FontSize="12" 
                             Foreground="Bisque" 
                             Margin="10" 
                             TextWrapping="Wrap" />
              </StackPanel>
          </DataTemplate>
      </Page.Resources>
      
      <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
          <Grid.ColumnDefinitions>
              <ColumnDefinition Width=".4*" />
              <ColumnDefinition Width=".6*" />            
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
              <RowDefinition Height="60" />
              <RowDefinition Height="*" />  <!-- * = take remaining available space -->
          </Grid.RowDefinitions>        
  
          <TextBlock Grid.Column="0" 
                     Grid.Row="0" 
                     Grid.ColumnSpan="2" 
                     Text="Metro Class Browser" 
                     FontSize="48" 
                     Margin="10,10,0,0" />
          
          <ListView Name="typeList" 
                    Grid.Column="0" 
                    Grid.Row="1"
                    ItemsSource="{Binding}" 
                    ItemTemplate="{StaticResource classSummaryTemplate}" 
                    HorizontalAlignment="Stretch" 
                    VerticalAlignment="Stretch" 
                    Margin="10,10,10,30" 
                    SelectionChanged="typeList_SelectionChanged" />
          
          <Grid Grid.Column="1" Grid.Row="1">
              <Grid.ColumnDefinitions>
                  <ColumnDefinition Width=".2*" />
                  <ColumnDefinition Width=".8*" />
              </Grid.ColumnDefinitions>
              <Grid.RowDefinitions>
                  <RowDefinition Height="60" />
                  <RowDefinition Height="*" />
              </Grid.RowDefinitions>
  
              <TextBlock Name="tbSelectedClass" 
                     Grid.Column="0" 
                     Grid.Row="0" 
                     Grid.ColumnSpan="2"
                     Text="{Binding ElementName=typeList, Path=SelectedItem.Name}"  
                     TextAlignment="Center"
                     FontSize="18" 
                     Margin="10,10,10,10"
                     VerticalAlignment="Top" 
                     Foreground="MediumOrchid"
                     Height="60" />
              
              <StackPanel Grid.Column="0" Grid.Row="1">
                  <TextBlock Text="Classification:" 
                             FontSize="14" 
                             Margin="10" 
                             TextAlignment="Right" />
                  
                  <TextBlock Text="Base Class:" 
                             FontSize="14" 
                             Margin="10" 
                             TextAlignment="Right" />
                  
                  <TextBlock Text="Class Hierarchy:" 
                             FontSize="14" 
                             Margin="10" 
                             TextAlignment="Right" />
                  
                  <TextBlock Text="Properties:" 
                             FontSize="14" 
                             Margin="10" 
                             TextAlignment="Right" />
                  
                  <TextBlock Text="Methods:" 
                             FontSize="14" 
                             Margin="10" 
                             TextAlignment="Right" />
                  
                  <TextBlock Text="Events:" 
                             FontSize="14" 
                             Margin="10" 
                             TextAlignment="Right" />            
              </StackPanel>
              
              <StackPanel Grid.Column="1" Grid.Row="1">
                  <TextBlock Name="tbClassification" 
                             Text="" FontSize="14" 
                             Margin="10" 
                             Foreground="PaleGreen" />
                  
                  <TextBlock Name="tbBaseClass" 
                             Text="" 
                             FontSize="14" 
                             Margin="10" 
                             Foreground="PaleGreen" />
                  
                  <TextBlock Name="tbClassInfo" 
                             Text="" 
                             TextWrapping="Wrap" 
                             FontSize="14" 
                             Margin="10" 
                             Foreground="PaleGreen" />
                  
                  <TextBlock Name="tbClassProps" 
                             Text="" 
                             TextWrapping="Wrap" 
                             FontSize="14" 
                             Margin="10" 
                             Foreground="PaleGreen" />
                  
                  <TextBlock Name="tbClassMethods" 
                             Text="" 
                             TextWrapping="Wrap" 
                             FontSize="14" 
                             Margin="10" 
                             Foreground="PaleGreen" />
                  
                  <TextBlock Name="tbClassEvents" 
                             Text="" 
                             TextWrapping="Wrap" 
                             FontSize="14" 
                             Margin="10" 
                             Foreground="PaleGreen" />
              </StackPanel>                
          </Grid>
      </Grid>
  </Page>
  

The ListView is data bound to the TypeSummaryList list. To set the its data context I added the following to the CreateTypeSummaryFromXml() method:

  private async void CreateTypeSummaryFromXml()
  {
      :
      :
      typeList.DataContext = TypeSummaryList;
  }
  

We now need to deal with what happens when a class is selected in the ListView. First, I created a new ClassInfo class to encapsulate information on the selected class. The key method is BuildClassHierarchy(), which takes a Type and recursively calls itself, checking the BaseType property to move "up" the inheritance chain. The ClassInfo constructor takes a class name, in string format, and creates a Type using Type.GetType(className). The WinRT GetTypeInfo() extension method is the gateway to Metro reflection, and this is used by various properties on the ClassInfo class to create lists of properties, methods and events for the encapsulated class.

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Reflection;
  using System.Text;
  using System.Threading.Tasks;
  
  namespace MetroClassBrowser
  {
      public class ClassInfo
      {
          private TypeInfo _typeInfo;
  
          public string ClassName { get; private set; }
          public List<string> ClassHierarchy { get; set; }
  
          public string Classification 
          { 
              get 
              {
                  if (_typeInfo.IsClass)
                      return "Class";
                  else if (_typeInfo.IsInterface)
                      return "Interface";
                  else
                      return "Unknown";
              }
          }
  
          public string BaseClass
          {
              get
              {
                  if (_typeInfo.BaseType != null)
                      return _typeInfo.BaseType.Name;
                  else
                      return "(none)";
              }
          }
  
          public List<string> Properties
          {
              get
              {
                  var properties = new List<string>();
                  foreach (PropertyInfo pi in _typeInfo.DeclaredProperties)
                  {
                      properties.Add(pi.Name);
                  }
  
                  return properties;
              }
          }
  
          public List<string> Methods
          {
              get
              {
                  var methods = new List<string>();
                  foreach (MethodInfo mi in _typeInfo.DeclaredMethods)
                  {
                      if(mi.IsPublic)
                          methods.Add(mi.Name);
                  }
  
                  return methods;
              }
          }
  
          public List<string> Events
          {
              get
              {
                  var events = new List<string>();
                  foreach (EventInfo ei in _typeInfo.DeclaredEvents)
                  {
                      events.Add(ei.Name);
                  }
  
                  return events;
              }
          }
  
          public string ClassHierarchyString  { get { return BuildCollectionString(ClassHierarchy, " > ", ""); } }
          public string ClassPropertiesString { get { return BuildCollectionString(Properties, ", ", ""); } }
          public string ClassMethodsString    { get { return BuildCollectionString(Methods, "(), ", "()"); } }
          public string ClassEventsString     { get { return BuildCollectionString(Events, ", ", ""); } }
  
          public ClassInfo(string className)
          {
              ClassName = className;
              ClassHierarchy = new List<string>();
  
              Type t = Type.GetType(className);
              if (t == null)
                  return;
  
              _typeInfo = t.GetTypeInfo();
              BuildClassHierarchy(t);
          }
  
          private void BuildClassHierarchy(Type type)
          {
              var typeInfo = type.GetTypeInfo();  // GetTypeInfo() is an extension method
              ClassHierarchy.Add(typeInfo.Name);
  
              if (typeInfo.BaseType != null)
                  BuildClassHierarchy(typeInfo.BaseType);
          }
  
          private string BuildCollectionString(List<string> list, string separator, string finalSeparator)
          {
              var s = new StringBuilder();
  
              if (list != null && list.Count > 0)
              {
                  for (int i = 0; i < (list.Count - 1); i++)
                      s.Append(list[i] + separator);
  
                  s.Append(list[list.Count - 1]);
                  s.Append(finalSeparator);
              }
  
              return s.ToString();
          }
      }
  }
  

The following SelectionChanged event handler for the ListView gets the selected namespace + class name and creates a full Assembly Qualified Name for the class. This is necessary because the type system can't resolve WinRT class names like "Ellipse", or even "Windows.UI.Xaml.Shapes.Ellipse". The assembly qualified name is then passed to the ClassInfo constructor, and then the various TextBlock's are populated with data:

  private void typeList_SelectionChanged(object sender, SelectionChangedEventArgs e)
  {
      // Create the assembly-qalified name required to use GetType() - 
      // here's an example of the format:
      // "Windows.UI.Xaml.Shapes.Ellipse, Version=255.255.255.255, 
      //   Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime"
  
      if (tbSelectedClass.Text.Length == 0)
          return;
  
      var assemblyQualifiedName = tbSelectedClass.Text + 
          ", Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime";
  
      ClassInformation = new ClassInfo(assemblyQualifiedName);
  
      tbClassInfo.Text = ClassInformation.ClassHierarchyString;
      tbClassification.Text = ClassInformation.Classification;
      tbBaseClass.Text = ClassInformation.BaseClass;
      tbClassProps.Text = ClassInformation.ClassPropertiesString;
      tbClassMethods.Text = ClassInformation.ClassMethodsString;
      tbClassEvents.Text = ClassInformation.ClassEventsString;
  }
  

Running the app produces:

To complete the app, I decided to add an App Bar that provides a search feature. The XAML for the app bar is as follows:

  <Page.TopAppBar>
      <AppBar>
          <Grid>
              <Grid.ColumnDefinitions>
                  <ColumnDefinition Width=".4*" />
                  <ColumnDefinition Width=".6*" />
              </Grid.ColumnDefinitions>
  
              <TextBlock Name="tbSearchResultsCount" 
                         Grid.Column="0"
                         Margin="10"
                         Width="400" 
                         Height="60" 
                         FontSize="24" 
                         HorizontalAlignment="Left"
                         VerticalAlignment="Center" />
              
              <StackPanel Grid.Column="1" 
                          Orientation="Horizontal" 
                          HorizontalAlignment="Right">
                  
                  <TextBox Name="tbSearch" 
                       Margin="10" 
                       Width="300"  
                       Height="50" 
                       FontSize="24" 
                       VerticalContentAlignment="Center" KeyUp="tbSearch_KeyUp" />
                  
                  <Button Name="btnSearch" 
                      HorizontalAlignment="Right"
                      Style="{StaticResource SearchAppBarButtonStyle}" 
                      Tapped="btnSearchtapped" />
              
                  <Button Name="btnClearSearch" 
                      HorizontalAlignment="Right"
                      Style="{StaticResource DiscardAppBarButtonStyle}" 
                      Tapped="btnClearSearchtapped" />                
              </StackPanel>
          </Grid>
      </AppBar>
  </Page.TopAppBar>
  

I added the following handlers for the app bar events. Notice once again how simple LINQ-to-XML makes it to search our list of classes and return a result set:

  private async void btnSearchtapped(object sender, TappedRoutedEventArgs e)
  {
      if (tbSearch.Text.Length == 0)
          return;
  
      var searchResults = from results in TypeSummaryList
                          where results.Name.ToLower().Contains(tbSearch.Text.ToLower())
                          select results;
  
      if (searchResults.Count() == 0)
      {
          MessageDialog dlg = new MessageDialog("No matching classes found");
          await dlg.ShowAsync();
      }
      else
      {
          typeList.DataContext = searchResults;
          tbSearchResultsCount.Text = "Showing " + 
              searchResults.Count().ToString() + " matches";
      }
  }
  
  private void btnClearSearchtapped(object sender, TappedRoutedEventArgs e)
  {
      typeList.DataContext = TypeSummaryList;
      tbSearch.Text = "";
      tbSearchResultsCount.Text = "";
  }
  
  private void tbSearch_KeyUp(object sender, KeyEventArgs e)
  {
      if (e.Key == Windows.System.VirtualKey.Enter)
          btnSearchtapped(tbSearch, null);
  }
  

The search feature looks like this:

Summary:
In this article we reviewed how to create a simple Metro-style browser for all the new Windows 8 WinRT classes. Along the way we used a bit of WinRT reflection, LINQ-to-XML, and some nice XAML layout features. The resulting app is actually almost quite useful!