Daniel Vaughan

.NET Adventures

Compile-Time Validation of Composite Object Data Binding Expressions

clock November 7, 2009 18:06 by author Daniel Vaughan

Introduction

Prompted by a recent comment on the T4 Metadata Generation template article, which I released some weeks ago, I have implemented a new mechanism for concatenating property paths. This allows compile time validation of properties that exist on composite or nested members.

Background

Previously I have demonstrated how generated metadata can be used to provide compile-time validation of binding expressions. Rather than using string literals in binding expressions, one is able to use the x:Static markup extension and a T4 generated constant to indicate the binding path; as shown in the following excerpt.

<Label Content="{Binding Path={x:Static Metadata:PersonMetadata.NamePath}}"/>

Overcoming Limitations

This approach works fine when targeting a property from a single instance in a DataContext, but what happens when we wish to target a nested instance’s property? For example, and as demonstrated in the downloadable sample from the article mentioned above, if we have a ListBox populated with Person instances, and we wish to bind a label to the listbox’s SelectedItem.Address.StreetAddress property, we can do so using the following XAML:

<ListBox x:Name="listBox" Background="Black">
  <ListBox.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <Label Content="{Binding Path={x:Static Metadata:PersonMetadata.NamePath}}"/>
      </StackPanel>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>
<Label Content="{Binding ElementName=listBox, 
    Path={Demo:JoinPath 
                SelectedItem, 
                {x:Static Metadata:PersonMetadata.Address}, 
                {x:Static Metadata:AddressMetadata.StreetLine}}}"/>

Here we see a custom MarkupExtension called JoinPathExtension is used to enable the concatenation of path strings to create a PropertyPath that is used to target the nested Address instance. In this case, the string values of ‘SelectedItem’, ‘Address’, and ‘StreetLine’ combine to produce a PropertyPath ‘SelectedItem.Address.StreetLine’.

You will notice, when you open the CS Window1.xaml file in the sample download, that errors are reported for the Path expressions. These don’t prevent the designer from loading in either Visual Studio or Blend. They are, however, annoying.

 

Diagram: Visual Studio Xaml designer errors.

Attempting to resolve this issue I switched to using named arguments. No luck there either I’m afraid, with the x:Static expression resulting in a compile time error:

(Unknown property 'Converter' for type 'MS.Internal.Markup.MarkupExtensionParser+UnknownMarkupExtension' encountered while parsing a Markup Extension. Line x position Y)

My fellow disciple Philipp Sumi has a great post outlining the VS designer bug. 

I have experimented with a number of approaches, including (as Philipp suggests) explicit property syntax, and have settled on the one shown above.

The main parts of the JoinPathExtension are shown:

CS:

/// <summary>
/// Allows a set of property path strings to be concatenates 
/// into a <see cref="PropertyPath"/> instance.
/// </summary>
[MarkupExtensionReturnType(typeof(PropertyPath))]
public class JoinPathExtension : MarkupExtension
{
  readonly List<string> members = new List<string>(); 
 
  public JoinPathExtension()
  {
    /* Intentionally left blank. */
  }
 
  public JoinPathExtension(string member0)
  {
    if (member0 == null)
    {
      throw new ArgumentNullException("member0");
    }
    members.Add(member0);
  }
 
  public JoinPathExtension(string member0, string member1)
    : this(member0)
  {
    if (member1 == null)
    {
      throw new ArgumentNullException("member1");
    }
    members.Add(member1);
  }
 
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    var path = string.Join(".", members.ToArray());
    var result = new PropertyPath(path);
    return result;
  }
 
  void SetMember(int index, string value)
  {
    if (value == null)
    {
      throw new ArgumentNullException("value");
    }
    if (members.Count < index + 1)
    {
      members.Add(value);
      return;
    }
    members[index] = value;
  }
 
  #region Named member properties
  [ConstructorArgument("member0")]
  public string Member0
  {
    get
    {
      return members[0];
    }
    set
    {
      SetMember(0, value);
    }
  }
 
  public string Member1
  {
    get
    {
      return members[1];
    }
    set
    {
      SetMember(1, value);
    }
  }
 
}

VB.NET:

Imports System.Windows.Markup

Public Class JoinPathExtension
    Inherits MarkupExtension
    ' Methods
    Public Sub New()
        Me.members = New List(Of String)
    End Sub

    Public Sub New(ByVal memberList As String())
        Me.members = New List(Of String)
        If (memberList Is Nothing) Then
            Throw New ArgumentNullException("memberList")
        End If
        Me.members.AddRange(memberList)
    End Sub

    Public Sub New(ByVal member1 As String)
        Me.members = New List(Of String)
        If (member1 Is Nothing) Then
            Throw New ArgumentNullException("member1")
        End If
        Me.members.Add(member1)
    End Sub

    Public Sub New(ByVal member1 As String, ByVal member2 As String)
        Me.New(member1)
        If (member2 Is Nothing) Then
            Throw New ArgumentNullException("member2")
        End If
        Me.members.Add(member2)
    End Sub

    Public Overrides Function ProvideValue(ByVal serviceProvider As IServiceProvider) As Object
        Return New PropertyPath(String.Join(".", Me.members.ToArray), New Object(0 - 1) {})
    End Function


    ' Properties
    <ConstructorArgument("member1")> _
    Public Property Member() As String
        Get
            Return Me.members.Item(0)
        End Get
        Set(ByVal value As String)
            If (value Is Nothing) Then
                Throw New ArgumentNullException("value")
            End If
            If (Me.members.Count < 1) Then
                Me.members.Add(value)
            Else
                Me.members.Item(0) = value
            End If
        End Set
    End Property

    <ConstructorArgument("member2")> _
    Public Property Member2() As String
        Get
            Return Me.members.Item(1)
        End Get
        Set(ByVal value As String)
            If (value Is Nothing) Then
                Throw New ArgumentNullException("value")
            End If
            If (Me.members.Count < 2) Then
                Me.members.Add(value)
            Else
                Me.members.Item(1) = value
            End If
        End Set
    End Property


    ' Fields
    Private ReadOnly members As List(Of String)
End Class

Conclusion

We have seen how by using a custom MarkupExtension we are able to concatenate generated property name constants to produce PropertyPaths, which can be consumed by Path binding expressions. Having the capability to join path expression adds a lot to the flexibility of the generated metadata approach. We are now able to fully express property paths for nested objects in binding expressions, without resorting to string literals; increasing dramatically the flexibility of this approach.

Download the sample code from here.

 

 



Banishing String Literals from XAML Resource References

clock October 3, 2009 19:15 by author Daniel Vaughan

Introduction

Since my initial experimentation with generating project metadata data using T4 (Text Template Transformation Toolkit), there have been several obvious opportunities to expand its scope. One such opportunity has been to use T4 to generate static properties representing XAML keys. This serves to reduce the reliance on string literals when referencing resources. I have subsequently augmented my MetadataGeneration.tt template to do just that.

x:Key Property Generation

To demonstrate, I have updated the sample application provided with my previous article, and employed a couple ResourceDictionaries to show how we can reference a ‘default’ dictionary using constant names, and also how we can cross reference with an auxiliary ResourceDictionary, overriding the resources using constant name values.

In the following excerpt we see a button that has its Background defined using a Resource whose key is defined as a static property in a generated class.

<Button 
Background="{StaticResource {x:Static Keys:MainDictionaryXamlMetadata.ButtonBackgroundKey}}"
Margin
="0,5,0,0" Content="Change" HorizontalAlignment="Left" Click="Button_ChangeClick"/>

This is useful, because it means if we modify the name of the background brush in the ResourceDictionary and forget to update references to it, we will be alerted at compile time, rather than at runtime.

The MetadataGeneration.tt template scours your project looking for XAML files, and then generates classes for them containing all x:Key attributes, represented as static properties. As we can see in the following excerpt, that the ButtonBackGround key is defined as a LinearGradientBrush in the MainDictionary.xaml.

MainDictionary.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <LinearGradientBrush x:Key="ButtonBackground">
        <GradientStop Color="AliceBlue" Offset="0" />
        <GradientStop Color="Yellow" Offset=".7" />
    </LinearGradientBrush>
    <SolidColorBrush x:Key="WindowForegroundBrush" Color="White"/>
</ResourceDictionary>

Being able to reference one ResourceDictionary from another is useful. If we take another ResourceDictionary, which redefines the resources of the first, we are able to do so in a safer way; expressing our intent with a dedicated property, and using the non-literal string key names derived from the MainDictionary.xaml.

SecondaryDictionary.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Metadata="clr-namespace:CSharpDesktopClrDemo.XamlMetadata.Folder1.Metadata">
    <LinearGradientBrush x:Key="{x:Static Metadata:MainDictionaryXamlMetadata.ButtonBackgroundKey}">
        <GradientStop Color="AliceBlue" Offset="0" />
        <GradientStop Color="Blue" Offset=".7" />
    </LinearGradientBrush>
    <SolidColorBrush x:Key="{x:Static Metadata:MainDictionaryXamlMetadata.WindowForegroundBrushKey}" Color="Azure"/>
</ResourceDictionary>

So, we can define our resources wherever we like; in a separate assembly for example, yet we still retain compile time validation of resource key references.

App.xaml

<Application x:Class="DanielVaughan.MetaGen.Demo.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources>
        <ResourceDictionary Source="pack://application:,,,/DanielVaughan.MetaGen.Demo;component/Folder1/MainDictionary.xaml"/>
        <!--<ResourceDictionary Source="pack://application:,,,/DanielVaughan.MetaGen.Demo;component/Folder1/SecondaryDictionary.xaml"/>-->
    </Application.Resources>
</Application>

Implementation

To accomplish the discovery of XAML files and associated Keys, and the subsequent generation of metadata classes, during project traversal we must do two things: detect when the project item is a XAML file, and keep a track of the current project directory. Now accomplishing the first is easy. Detecting when the current project item is a project folder, on the other hand, turned out to be hack-worthy, as you will notice in the following excerpt.

string processingDirectory = string.Empty;

public void ProcessProjectItem(ProjectItem projectItem,
    Dictionary<string, NamespaceBuilder> namespaceBuilders, string activeNamespace)
{
    FileCodeModel fileCodeModel = projectItem.FileCodeModel;

    if (fileCodeModel != null)
    {
        foreach (CodeElement codeElement in fileCodeModel.CodeElements)
        {
            WalkElements(codeElement, null, null, namespaceBuilders);
        }
    }
    
    string activeNamespaceCopy = activeNamespace;
    if (string.IsNullOrEmpty(activeNamespaceCopy))
    {
        if (string.IsNullOrEmpty(xamlRootNamespace))
        {
            activeNamespaceCopy = rootNamespace; 
        }
        else
        {
            activeNamespaceCopy = string.Format("{0}.{1}", 
                rootNamespace, xamlRootNamespace);
        }
    }
    
    if (projectItem.ProjectItems != null 
        && projectItem.ProjectItems.Count > 0)
    {
        /* This is a hack to determine if we have a directory.
            If you know the proper way for doing this, please let me know. */
        try
        {
            var foo = projectItem.Document;
        }
        catch (Exception ex)
        {
            string newNamespace = projectItem.Name.Replace(" ", string.Empty); 
            activeNamespaceCopy += "." + newNamespace; 
        }
    }
    
    string itemName = projectItem.Name; 
    if (generateXamlKeys && itemName.EndsWith(".xaml", true, CultureInfo.InvariantCulture))
    {    
        /* Retrieve or create the namespace builder. */
        NamespaceBuilder namespaceBuilder;

        if (!namespaceBuilders.TryGetValue(activeNamespaceCopy, out namespaceBuilder))
        {
            namespaceBuilder = new NamespaceBuilder(activeNamespaceCopy, null, 0);
            namespaceBuilders[activeNamespaceCopy] = namespaceBuilder;
        }
        
        string fileName = projectItem.get_FileNames(0);
        string text = System.IO.File.ReadAllText(fileName);
        MatchCollection matches = xClassRegex.Matches(text);                

        if (matches.Count > 0)
        {
            string xamlMetadataClassName = ConvertProjectItemNameToTypeOrMemberName(itemName.Substring(0, itemName.Length - 4));                
            var classComments = new List<string> {string.Format("/// <summary>Metadata for XAML {0}</summary>", itemName)};
            XamlBuilder xamlBuiler = new XamlBuilder(xamlMetadataClassName, classComments, 1);
            namespaceBuilder.AddChild(xamlBuiler);
            
            foreach (Match match in matches)
            {
                Group keyGroup = match.Groups["KeyName"];
                string keyName = keyGroup.Value;
                var keyComments = new List<string> {string.Format("/// <summary>Represents x:Key=\"{0}\"/></summary>", keyName)};
                xamlBuiler.AddChild(new XamlKeyBuilder(keyName, keyComments));
            }
        }
    }

    if (projectItem.ProjectItems != null)
    {
        foreach (ProjectItem childItem in projectItem.ProjectItems)
        {
            ProcessProjectItem(childItem, namespaceBuilders, activeNamespaceCopy);
        }
    }
}

We see that generating XAML metadata works in the same way as the class and interface metadata generation, in that we represent the XAML file using a XamlBuilder, and keys within the XAML file are represented as XamlKeyBuilders.

Generating Namespaces for XAML Metadata Classes

To avoid collisions with type names and generated namespace, I offer a customizable xamlRootNamespace configuration variable. This variable is used to construct namespace names for generated XAML metadata classes as the following example illustrates:

If we have a XAML file called Window1.xaml. It will be represented by a class named [generatedClassPrefix]Window1[generatedXamlClassSuffix][generatedClassSuffix]

Conclusion

We have seen how XAML Resource keys, ordinarily referenced using magic strings, can be eliminated using generated Type and File metadata.

I am still rather pleased at what one is able to achieve by combining T4 and the DTE. Visual Studio 2010 will see T4 move to a more visible position within the IDE. This, together with the new features of T4 in VS2010, will surely make it an indispensible tool.

To download the template source and demo applications, please visit the updated T4 Metadata article on Codeproject.

 



About the author

Daniel VaughanDaniel Vaughan is a software developer with a decade of commercial experience across a wide range of industries including e-commerce, multimedia, and finance. While originally from Australia and the UK, Daniel is currently based in Geneva Switzerland; working with WPF, WCF, and WF within the finance industry. In his spare time Daniel likes to spend time concocting novel ideas, such as employing neural networks to predict user navigation behaviour in WPF applications, and a grid computing framework for Silverlight. Daniel is also the creator of a number of open-source projects including Calcium, and Clog. E-mail me Send mail

 

Microsoft MVP logo Disciple
CodeProject MVP
WPF and Silverlight Insiders

 

 

RecentComments

Comment RSS

Sign in