This post was inspired by this stack overflow question.
We C# programmers are used to our compiler (and in recent years, our code editor) catching typos and stupid mistakes long before the program runs, actually, we are used to the program not being able to run until we fix all those mistakes, if I type user.UsrId when in fact the user id is stored in user.UserId (the first example is missing an e) the editor will highlight the wrong name and the compiler will refuse to compile the code.
On the other hand, WPF data bindings are just text that isn’t verified by any compiler, if I make the same mistake in a data binding expression the code will compile just fine – but that binding will do nothing when I run the code.
In my opinion this is not a huge problem, since those problems are easy to spot (they tend leave a blank control in the UI) and the warning printed to the debugger output window are pretty informative, however, I do think twice (and often much more then twice) before refactoring a class used in data binding.
This question on stackoverflow made think: this shouldn’t be such a difficult problem to solve, XAML files are strictly formatted XML files and reflections can be used to check if a property exist.
So, here is some proof-of-concept code that processes an XAML file and check binding are valid.
This is a proof of concept, it is not production quality code, it is full of rough edges and covers only the simpler cases.
This code takes two parameters, the file name of a XAML file and the file name to the assembly that contains the data classes referenced in the XAML, it will then look for data templates inside the XAML and verify all bindings inside the templates.
I don’t have the free time to make this into a useful tool at the moment – so if anyone likes to build something usable out of this please do, I’m placing this code in the public domain, you may do whatever you like with it including using it in a commercial project, if you build a “real” tool out of this code or even without using this code send me a message and I’ll update this post with a link to you.
here what I think needs to be done:
- This code only checks bindings inside data templates because DataTemplate has an handy DataType property I can use to figure out the time it is bound to, you can easily add an attached property you can set on every element that will enable the check out side data templates.
- The ParseBinding method is really bad, this needs a real parser.
- The ParseType method only understands one of 3 ways that you can specify a namespace.
- likewise, it also only recognize the short {Binding} syntax and not <Binding>.
- This works on one XAML file and one assembly, this should work on an entire project - the csproj file is an easy to parse XML file, if you parse the csproj and get all XAML files and all referenced assemblies you can make this a post-build action that will automatically verify all your binding and stop the build with a real compiler-like error when verification fails.
- This code only understand the simplest option for the Binding.Path property.
- This will not verify binding where Source, RelativeSource or ElementName are set – but in most cases it should be easy to verify those.
using System;
using System.Collections.Generic;
using System.Xml;
using System.Reflection;
// This is a proof of concept, it is not production
// quality code, it is full of rough edges and covers
// only the simpler cases.
//
// This code takes two parameters, the file name of a XAML
// file and the file name to the assembly that contains the
// data classes referenced in the XAML, it will then look
// for data templates inside the XAML and verify all bindings
// inside the templates.
//
// I don’t have the free time to make this into a useful tool
// at the moment – so if anyone likes to build something usable
// out of this please do, I’m placing this code in the public domain,
// you may do whatever you like with it including using it in a
// commercial project.
namespace BindingValidator
{
class Program
{
static void Main(string[] args)
{
// this is
var xamlfile = args[0];
var asmFile = args[1];
// Load assembly containing data objects so we can use reflection on them
// In a real application there may be multiple assemblies we need to load
var asm = Assembly.ReflectionOnlyLoadFrom(asmFile);
// Load XAML file
XmlDocument doc = new XmlDocument();
doc.Load(xamlfile);
// Process the XAML
ScanXaml(doc.DocumentElement, null, new Dictionary<string,string>(), asm);
// Wait for a key so we can see the result when debugging
Console.ReadKey();
}
public static void ScanXaml(XmlElement element, Type dataContextType, Dictionary<string,string> namespaces, Assembly dataAssembly)
{
// If the element is a data template get the data type
if (element.Name == "DataTemplate")
{
XmlAttribute dataTypeAttr = element.Attributes["DataType"];
if (dataTypeAttr == null)
{
dataContextType = null;
}
else
{
dataContextType = ParseType(dataTypeAttr.Value, namespaces, dataAssembly);
}
}
// scan attributes
foreach (XmlAttribute attr in element.Attributes)
{
// xmlns are importent because we will nee the namespace name in ParseType
// In a real app you would probably want to also check the default namespace
if (attr.Name.StartsWith("xmlns:"))
{
namespaces[attr.Name.Substring(6)] = attr.Value;
}
// if this is a binding, validate it
string attrVal = attr.Value;
if (attrVal.StartsWith("{Binding"))
{
ValidateBinding(attrVal, dataContextType);
}
// maybe we need to check explicitly setting the DataContext property
}
// recourse over child elements
foreach (XmlNode child in element.ChildNodes)
{
if (child is XmlElement)
{
ScanXaml((XmlElement)child, dataContextType, namespaces, dataAssembly);
}
}
}
private static void ValidateBinding(string str, Type dataContextType)
{
// if we don't know the data type
if (dataContextType == null) return;
// parse the {Binding} expression
var keyValuePairs = ParseBinding(str);
// look for some of the many binding options we don't support
if (keyValuePairs.ContainsKey("Source") ||
keyValuePairs.ContainsKey("ElementName") ||
keyValuePairs.ContainsKey("RelativeSource") ||
!keyValuePairs.ContainsKey("Path")||
keyValuePairs["Path"].Length==0)
{
return;
}
// finally, check the property exist
// we assume the Path is a simpale property name, we should handle all
// the possibilities supported by the path syntax
// (for example: Property.SomeList/ListItemProperty)
if (dataContextType.GetProperty(keyValuePairs["Path"]) == null)
{
Console.WriteLine("Error \"" + dataContextType.FullName + "\" does not contain property \"" + keyValuePairs["Path"] + "\"");
}
}
private static Dictionary<string,string> ParseBinding(string str)
{
// this is a really bad way to parse the exception,
// but this is just proof of concept and I didn't have
// time to write a parser
var result = new Dictionary<string, string>();
string[] parts = str.Substring(8, str.Length - 9).Split(',');
int i = 1;
if (parts[0].IndexOf('=')!=-1)
{
i = 0;
}
else
{
result.Add("Path", parts[0].Trim());
}
for (; i < parts.Length; ++i)
{
int pos = parts[i].IndexOf('=');
result.Add(parts[i].Substring(0, pos).Trim(), parts[i].Substring(pos + 1).Trim());
}
return result;
}
private static Type ParseType(string str, Dictionary<string, string> namespaces, Assembly dataAssembly)
{
// this assumes all namespaces are clr-namespace: with no assembly info
// in a real app you need to handle both clr=namespace definitions with
// an assembly and URL namespaces
int pos = str.IndexOf(':');
string ns = str.Substring(0, pos);
string typeName = str.Substring(pos + 1);
string nsValue = namespaces[ns];
string nsName = nsValue.Substring(14);
var type = dataAssembly.GetType(nsName + "." + typeName);
if (type == null)
{
Console.WriteLine("Error: \"" + str + "\" is not a valid type");
}
return type;
}
}
}
Edit:
Mike Strobel wrote in a comment that Rob Relyea from the XAML team has already made such a utility - check it out here
posted @ Wednesday, September 15, 2010 4:13 PM