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 ;)).
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>
-->
<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>
-->
<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 ! :)
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>
-->
<TextBlock Text="{Binding Name}"></TextBlock>
<ItemsControl ItemsSource="{Binding Contacts}">
<ItemsControl.ItemTemplate>
<DataTemplate>
-->
<TextBlock Text="{Binding Mail}"></TextBlock>
-->
</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... ;)