In this article, you will see a Genuilder extension to fix most spelling mistakes automatically at compile and design time (when you save your XAML file).
Table of Contents
Introduction
Do you remember the time you lose 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 two letters in a binding path.
I've developed 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 Name}"></TextBlock>
<TextBlock Text="{Binding Address}"></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 Name}"></TextBlock>
<TextBlock Text="{Binding Address}"></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 returns a type that is not present in your WPF/Silverlight project (i.e., 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 instantiate 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 are 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 builds 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 matches 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 example).
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 prints them in the output error windows, or corrects 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
outputs the same errors as XamlVerifierEvaluator
, but, before returning them, creates corrections of the XAML files with the help of suggestions.
XamlCorrector.SaveCorrections()
replaces 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 responsibility if the sky falls on you... ;)
History
- 30th December, 2011: Initial version