Click here to Skip to main content
Click here to Skip to main content

Tagged as

XamlVerifier - check or auto correct binding path at compile and design time

, 9 Jan 2012 Ms-PL
Rate this:
Please Sign up or sign in to vote.
Stop wasting time on a stupid typo in binding paths.

XamlVerifier - check or auto correct binding path at compile and design time

Table of content

Introduction

Do you remember the time you loose everytime you make a spelling mistake on a binding path in your xaml files ?

You code, hit F5, which, for SL applications, deploy on Cassini or IIS after 10 or 20 seconds just to see that you have inverted 2 letters in a binding path.

I've developped a Genuilder extension to fix most of your spelling mistake automatically at compile and design time (when you save your xaml file). This is a beta, use it at your own risks.

Under the hood I use a generated pattern matcher lexer with Antlr, and NRefactory AST visitor to populate the types table... after showing how to use it, I will explain the implementation.

How to use it ?

First, install genuilder as explained here (and vote for it Wink | ;) ).

Then in your solution add a new Genuilder project. (New Project/Visual C#/Genuilder)

Modify the program.cs file of your Genuilder project to install the XamlVerifierExtension:

static void Main(string[] args)
{
	foreach(var project in Projects.InSubDirectories("../..").ExceptForThisAssembly())
	{
		var ex = new ExtensibilityFeature();
		ex.AddExtension(new XamlVerifierExtension());
		project.InstallFeature(ex);
		project.Save();
	}
}

Run the Genuilder project, and reload your project.

Now let's add a PersonViewModel, in our project.

public class PersonViewModel
{
	public string Name
	{
		get;
		set;
	}
	public string Address
	{
		get;
		set;
	}
}

If you want to bind it in your MainWindows1.xaml here is the code with a spelling mistake.

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBlock Text="{Binding Naem}"></TextBlock>
        <TextBlock Text="{Binding Addres}"></TextBlock>
    </Grid>
</Window>

You will notice such error only at run time. It breaks your flow and reduce your feedback. XamlVerifier can spot such error for you when you save your xaml file, just add a Start Verify comment like this :

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <!-- Start Verify : PersonViewModel -->
        <TextBlock Text="{Binding Naem}"></TextBlock>
        <TextBlock Text="{Binding Addres}"></TextBlock>
    </Grid>
</Window>

Compile or save and you will see the error with some suggestions

But if you are a very busy person, you don't care about errors and just want them to be fixed.

For this, you just have to modify the Program.cs of your Genuilder project and run it one more time. (AutoCorrect property)

static void Main(string[] args)
{
	foreach(var project in Projects.InSubDirectories("../..").ExceptForThisAssembly())
	{
		var ex = new ExtensibilityFeature();
		ex.AddExtension(new XamlVerifierExtension()
		{
			AutoCorrect = true
		});
		project.InstallFeature(ex);
		project.Save();
	}
}

Save your XAML file (or compile your project) and just in front of your eyes XamlVerifier fix everything for you.

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <!-- Start Verify : PersonViewModel -->
        <TextBlock Text="{Binding Name}"></TextBlock>
        <TextBlock Text="{Binding Address}"></TextBlock>
    </Grid>
</Window>

XamlVerifier is smart enough to correct every identifier in a binding path (for example : Contatc.Addrses.ZpiCodee, will be fixed in Contact.Address.ZipCode). And it can also fix the type name in the Start Verify comment ! Smile | :)

Now imagine every PersonViewModel have a list of ContactViewModel, you just have to use a new Start Verify and use End Verify to go back in the PersonViewModel context :

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <!-- Start Verify : PersonViewModel -->
        <TextBlock Text="{Binding Name}"></TextBlock>
        <ItemsControl ItemsSource="{Binding Contacts}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <!-- Start Verify : ContactViewModel -->
                    <TextBlock Text="{Binding Mail}"></TextBlock>
                    <!-- End Verify -->
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <TextBlock Text="{Binding Address}"></TextBlock>
    </Grid>
</Window>

Now let's talk about the limitations of XamlVerifier.

Limitations

These limitations, depending on the demand, will be addressed in future release.

Cannot fix path on referenced types

This means that if the path return a type that is not present in your WPF/Silverlight project (ie FileInfo), then XamlVerifier will not fix it.

This limitation come from the implementation of XamlVerifier : to resolve identifier in path, it looks in a type table only populated by code files of your project.

Slow on large projects

By default, XamlVerifier will parse every code file in your project everytime you save or compile a xaml file.

You can fix that by setting the XamlVerifierExtension.CodeFiles property.

For example, in this example I will parse only ViewModel.cs files.

foreach(var project in Projects.InSubDirectories("../..").ExceptForThisAssembly())
{
	var ex = new ExtensibilityFeature();
	ex.AddExtension(new XamlVerifierExtension()
	{
		AutoCorrect = false,
		CodeFiles = new FileQuery()
			.SelectInThisDirectory(true)
			.Like(".*ViewModel.cs").ToQuery()
	});
	project.InstallFeature(ex);
	project.Save();
}

You can do the same to filter XAML files.

Implementation

The implementation took me very few line of code, I use Antlr to parse Xaml files, NRefactory to parse code files, Levenshtein distance to partial match property name and class name.

The integration of XamlVerifier to Visual studio is done with Genuilder.Extensibility. (XamlVerifierExtension)

Antlr, pattern matcher with lexer

This part was easy once I managed to install Antlr properly in my project, I just had to define a Lexer to fetch Start Verify, End Verify and Binding token along with identifiers.

lexer grammar XamlVerifierLexer;

options {
    language=CSharp3;
    TokenLabelType=CommonToken;
	k=10;
	filter=true;
}

@namespace{Genuilder.Extensions.XamlVerifier}

fragment F_WHITESPACE
	:	(' ' | '\t' | '\v' | '\f')*;

fragment F_PATH
	:	('a'..'z' | 'A'..'Z' | '_')
		('a'..'z' | 'A'..'Z' | '_' | '0'..'9')*('.'('a'..'z' | 'A'..'Z' | '_')
		('a'..'z' | 'A'..'Z' | '_' | '0'..'9')*)*;

STARTVERIFY
	:	'<!--' F_WHITESPACE 'Start Verify' F_WHITESPACE ':' F_WHITESPACE c=F_PATH F_WHITESPACE '-->' { YieldStartVerify($c); };
ENDVERIFY
	:	'<!--' F_WHITESPACE 'End Verify' F_WHITESPACE '-->';
BINDING
	:	'{Binding' F_WHITESPACE 'Path='? F_WHITESPACE c=F_PATH { YieldBinding($c); };

A "fragment" token is a private token of the Lexer and will not output when I will call XamlVerifierLexer.NextToken().

Note that filter=true; means that the lexer will just ignore characters when no token match input, it's called lexer pattern matching.

{ YieldStartVerify($c); } will call the YieldStartVerify method of the lexer with the $c token as parameter (here c=F_PATH).

I use these methods to keep track of binding'path and type's name. F_Path is a fragment, so I will not be able to get it by calling XamlVerifierLexer.NextToken().

These methods are in a partial class of the lexer :

public int index;
public IToken id;
public IToken path;
public Location idLocation;	

void YieldStartVerify(IToken id)
{
	this.id = id;
	idLocation = new Location(id.Line, id.CharPositionInLine + 1);
	index = id.StartIndex;
}
void YieldBinding(IToken path)
{
	this.path = path;
	idLocation = new Location(path.Line, path.CharPositionInLine + 1);
	index = path.StartIndex;
}	

To transform these ANTLR tokens into the following strongly typed one, I use XamlVerifierReader.

The implementation just instanciate a Lexer, take one antlr token after another and transform them in strongly typed ones. (XamlVerifierNode).

public class XamlVerifierReader
{
	public XamlVerifierReader(Stream stream)
		: this(new StreamReader(stream))
	{
	}

	public XamlVerifierReader(string content)
		: this(new StringReader(content))
	{
	}


	XamlVerifierLexer _XamlLexer;
	public XamlVerifierReader(TextReader reader)
	{
		_XamlLexer = new XamlVerifierLexer(new ANTLRStringStream(reader.ReadToEnd()));
	}


	public IEnumerable<XamlVerifierNode> ReadAll()
	{
		while(true)
		{
			var node = Read();
			if(node == null)
				yield break;
			yield return node;
		}
	}
	public XamlVerifierNode Read()
	{
		var token = _XamlLexer.NextToken();
		if(token.Type == XamlVerifierLexer.EOF)
			return null;

		var location = new Location(token.Line, token.CharPositionInLine + 1);

		if(token.Type == XamlVerifierLexer.STARTVERIFY)
		{
			return new StartVerify(_XamlLexer.id.Text, location, _XamlLexer.idLocation, _XamlLexer.index);
		}
		else if(token.Type == XamlVerifierLexer.ENDVERIFY)
		{
			return new EndVerify(location);
		}
		else if(token.Type == XamlVerifierLexer.BINDING)
		{
			return new XamlBinding(_XamlLexer.path.Text, location, _XamlLexer.idLocation, _XamlLexer.index);
		}
		else
			throw new NotSupportedException("Not supported token at " + location.ToString());
	}
}

XamlVerifierEvaluator will iterate on these nodes and output binding path/type name errors and suggestions.

But, in order to know whether a type or property exists or not, I must be able to parse code files. NRefactory was the way to go.

NRefactory, building type and property table

As you can see in the following class diagram, the XamlVerifierEvaluator will output an enumerable of XamlVerifierError and there is two types of error, each one with one suggestion to fix them.

XamlVerifierPropertyError, will give you errors about wrong binding path.

XamlVerifierTypeError, will give you errors about wrong type name in Start Verify comments.

XamlFiles are parsed with the XamlVerifierReader as seen just before.

CodeFiles are a list of CompilationUnit. (ASTs of code files parsed by NRefactory)

When XamlVerifierEvaluator will find a new Start Verify node or a new Binding path node it will seek the type in a lookup table, and for each binding path, in a table lookup of properties.

With the help of NRefactory and the visitor pattern, constructing these tables is not a problem. And as you can see in the code of _AllErrorsCore(), I use the TypeResolver type.

private IEnumerable<XamlVerifierError> _AllErrorsCore()
{
	TypeResolver resolver = new TypeResolver();
	foreach(var codeFile in CodeFiles)
	{
		resolver.VisitCompilationUnit(codeFile, null);
	}

The TypeResolver class just build these lookup tables by overriding some Visit* methods of AbstractAstVisitor.

public override object VisitNamespaceDeclaration(NamespaceDeclaration namespaceDeclaration, object data)
{
	nsStack.Push(namespaceDeclaration);
	try
	{
		return base.VisitNamespaceDeclaration(namespaceDeclaration, data);
	}
	finally
	{
		nsStack.Pop();
	}
}

public override object VisitTypeDeclaration(TypeDeclaration typeDeclaration, object data)
{
	TypeDecl typeDecl = GetOrCreateTypeDecl(typeDeclaration);
	stackTypes.Push(typeDecl);
	try
	{
		return base.VisitTypeDeclaration(typeDeclaration, data);
	}
	finally
	{
		stackTypes.Pop();
	}
}

public override object VisitPropertyDeclaration(PropertyDeclaration propertyDeclaration, object data)
{
	if(stackTypes.Count == 0)
		return null;
	var typeDecl = stackTypes.Peek();
	typeDecl.Properties.Add(new PropertyDecl(propertyDeclaration));
	return null;
}

Then for each XamlFile I use a XamlVerifierContext to keep track of current contextual type when I must resolve a binding path.

Each nodes, StartVerify, EndVerify and Binding, will update the current context, and/or output XamlVerifierErrors.

foreach(var xamlFile in XamlFiles)
{
		XamlVerifierReader reader = new XamlVerifierReader(File.ReadAllText(xamlFile));
		XamlVerifierContext context = new XamlVerifierContext();
		foreach(var node in reader.ReadAll())
		{
			IEnumerable<XamlVerifierError> errors = node.Visit(resolver, context);
			if(errors != null)
			{
				foreach(var error in errors)
				{
					error.File = xamlFile;
					yield return error;
				}
			}
		}
	}
}

StartVerify will update the context and check if a type really exists.

internal override IEnumerable<XamlVerifierError> Visit(TypeResolver resolver, XamlVerifierContext context)
{
	var result = resolver.FindType(Type);
	context.Types.Push(Type);
	if(!result.Success)
		yield return new XamlVerifierTypeError()
		{
			Suggestion = result.Suggestion,
			TypeName = context.Types.Peek(),
			Location = IdLocation,
			Index = Index
		};
}

EndVerify just pop the current Type out of the context.

internal override IEnumerable<XamlVerifierError> Visit(TypeResolver resolver, XamlVerifierContext context)
{
	if(context.Types.Count > 0)
		context.Types.Pop();
	yield break;
}

XamlBinding will check if the property exists, and for each component of the path, will try to resolve the component.

internal override IEnumerable<XamlVerifierError> Visit(TypeResolver resolver, XamlVerifierContext context)
{
	if(context.Types.Count == 0)
		yield break;


	var type = context.Types.Peek();
	int index = Index;
	for(int i = 0; i < Paths.Length; i++)
	{
		if(type == null)
			yield break;
		var result = resolver.FindProperty(type, Paths[i]);
		if(!result.Success)
		{
			yield return new XamlVerifierPropertyError()
			{
				PropertyName = Paths[i],
				Suggestion = result.Suggestion,
				ClassName = type,
				Location = IdLocation + new Location(0, i == 0 ? 0 : Paths[i - 1].Length + 1),
				Index = index
			};
		}
		type = result.TypeName;
		index += Paths[i].Length + 1;
	}
}

Traditionally, type and property lookups are implemented with hash table. In my case, I need to support partial matching, so I use FuzzyCollection, as you can see the use in TypeResolver methods.

FuzzyResult<PropertyDecl> GetProperty(string typeName, string propertyName, bool exact = false)
{
	var type = types.GetClosest(new TypeDecl(typeName)).FirstOrDefault();
	if(type == null || (exact && type.Distance != 0.0))
		return null;
	var property = type.Value.Properties.GetClosest(new PropertyDecl(propertyName)).FirstOrDefault();

	if(property == null && type.Value.BaseType != null)
	{
		return GetProperty(type.Value.BaseType, propertyName, true);
	}
	return property;
}

internal ResolveResult FindProperty(string typeName, string propertyName)
{
	var property = GetProperty(typeName, propertyName);

	if(property == null)
	{
		return new ResolveResult()
		{
			Success = false
		};
	}
	else
	{
		var tn = property.Value.Declaration.TypeReference.ToString();
		if(property.Distance == 0)
			return new ResolveResult()
			{
				TypeName = tn
			};
		else
			return new ResolveResult()
			{
				Success = false,
				Suggestion = property.Value.Name,
				TypeName = tn
			};
	}
}

internal ResolveResult FindType(string typeName)
{
	var type = types.GetClosest(new TypeDecl(typeName)).FirstOrDefault();
	if(type == null)
		return new ResolveResult()
		{
			Success = false
		};
	if(type.Distance == 0)
		return new ResolveResult();

	var closest = new FuzzyCollection<string>(Metrics.LevenshteinDistance);
	closest.Add(type.Value.Name);
	closest.Add(type.Value.FullName);
	var c = closest.GetClosest(typeName).First();

	return new ResolveResult()
	{
		Success = false,
		Suggestion = c.Value
	};
}

FuzzyCollection, implementing partial matching with metrics

GetClosest return an ordered list of closest matchs determined by the metric (distance calculator) given in the constructor.

FuzzyCollection have a O(n) time complexity so its fine for small collection of at most 500 elements, but clever algorithm like kD tree can make it down to O(log n). (k nearest neighbour problem)

The "distance" between two properties is the distance of their property's name (using Levenshtein distance)

public TypeDecl(string name, string ns)
{
	Namespace = ns;
	Name = name;
	_Properties = new FuzzyCollection<PropertyDecl>((a, b) => Metrics.LevenshteinDistance(a.Name, b.Name));
}

And the distance between two type declarations is the minimum distance between their name and full name (Person and MyProject.Person for exemple).

types = new FuzzyCollection<TypeDecl>((a, b) =>
{
	return Math.Min(Metrics.LevenshteinDistance(a.Name, b.Name), Metrics.LevenshteinDistance(a.FullName, b.FullName));
});

For this reason, when you specify <!-- Start Verify : Person --> or <!-- Start Verify : ConsoleApplication1.Person -->, both will be accepted, since both type are distance 0 from ConsoleApplication1.Person of the TypeResolver.

Nothing special in the implementation of FuzzyCollection.

public class FuzzyCollection<T>
{
	Func<T, T, int> _Metric;
	public FuzzyCollection(Func<T, T, int> metric)
	{
		_Metric = metric;
	}

	public List<FuzzyResult<T>> GetClosest(T source)
	{
		var results = _objs.Select(o => new FuzzyResult<T>
		{
			Distance = _Metric(source, o),
			Value = o
		}).ToList();
		results.Sort((a, b) => a.Distance.CompareTo(b.Distance));
		return results;
	}

	List<T> _objs = new List<T>();

	public T Add(T obj)
	{
		_objs.Add(obj);
		return obj;
	}
}

Genuilder Extensibility, plugging everything with MSBuild

All this code was very easy to unit test with very few line of code.

Now I need to get xaml files and CodeCompileUnits' code files from MSBuild, and output these errors in visual studio's error window. For this reason I create a new IExtension in with Genuilder.Extensibility called XamlVerifierExtension.

You can see the documentation of Genuilder.Extensibility on codeplex.

AutoCorrect will fix error with suggestions when encounter a new one.

CodeFiles and XamlFiles are FileQuery object, that will select what code file and xaml file you want to parse (to use in large project). All by default.

StopAfterFirstError will stop after seeing one error.

public void Execute(ExtensionContext extensionContext)
{
	var xamlFiles = XamlFiles ?? new FileQuery().SelectInThisDirectory(true).All().ToQuery();
	var codeFiles = CodeFiles ?? new FileQuery().SelectInThisDirectory(true).All().ToQuery();

	var xamlFilesByName = extensionContext.GenItems
		.GetByQuery(xamlFiles)
		.Where(i => i.SourceType == SourceType.Page)
		.ToDictionary(o => o.Name);

	if(xamlFilesByName.Count == 0)
		return;

	XamlVerifierEvaluator evaluator = new XamlVerifierEvaluator();
	evaluator.XamlFiles.AddRange(xamlFilesByName.Keys);

	foreach(var compilationUnitExtension in extensionContext.GenItems
		.GetByQuery(codeFiles)
		.Where(i => i.SourceType == SourceType.Compile)
		.Select(i => i.GetExtension<CompilationUnitExtension>()))
	{
		compilationUnitExtension.ParseMethodBodies = false;
		if(compilationUnitExtension.CompilationUnit != null)
			evaluator.CodeFiles.Add(compilationUnitExtension.CompilationUnit);
	}

This part just take all files recursively in the project directory if XamlFiles and CodeFiles are not set.

XamlVerifierEvaluator.XamlFiles is populated with xaml GenItems selected by XamlVerifierExtension.XamlFiles.

XamlVerifierEvaluator.CodeFiles is populated with code files CodeCompileUnit (without parsing methods body) thanks to the object extension CompilationUnitExtension.

Then it iterates on all errors and print them in the output error windows, or correct them.

XamlCorrector corrector = AutoCorrect ? new XamlCorrector()
{
	Evaluator = evaluator
} : null;

var errors = corrector == null ? evaluator.AllErrors() : corrector.AllErrors();

foreach(var error in errors)
{
	if(AutoCorrect)
		continue;

	var item = xamlFilesByName[error.File];
	item.Logger.Error(ToString(error), error.Location);

	if(StopAfterFirstError)
	{
		break;
	}
}

if(corrector != null)
	corrector.SaveCorrections();

XamlCorrector output the same errors as XamlVerifierEvaluator, but, before returning them, create corrections of the xaml files with the help of suggestions.

XamlCorrector.SaveCorrections(), replace actual xaml files by the corrections.

Here is how I output errors in error's window.

private string ToString(XamlVerifierError error)
{
	var propertyError = error as XamlVerifierPropertyError;
	if(propertyError != null)
	{
		return propertyError.PropertyName + " does not exist in " + propertyError.ClassName + Suggestion(error);
	}
	var classError = error as XamlVerifierTypeError;
	if(classError != null)
	{
		return classError.TypeName + " does not exist" + Suggestion(error);
	}
	return error.ToString();
}

private string Suggestion(XamlVerifierError error)
{
	if(error.Suggestion == null)
		return "";
	return ", do you mean " + error.Suggestion + " ?";
}

Conclusion

All this design is entirely streamed, which means that the ANTLR parser moves at the same speed as you iterate on errors.

This is a beta, use AutoCorrect at your own risks, I don't take any responsability if the sky falls on you... Wink | ;)

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Nicolas Dorier
Software Developer Freelance
France France
I am a trainer and a curious developer.
 
CEO of AO-IS, we created a tool to make IaaS on Azure more easy IaaS Management Studio.
 
If you are interested for working with me, for fun coding stuff, for freelance stuff, or interested in using our cloud training infrastructure freely for a kickass presentation for the dev community ? this way Smile | :)

Comments and Discussions

 
QuestionCan't get it to work Pinmembershogedal24-Oct-12 21:30 
AnswerRe: Can't get it to work PinmemberNicolas Dorier24-Oct-12 23:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.1411023.1 | Last Updated 9 Jan 2012
Article Copyright 2011 by Nicolas Dorier
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid