committed by
GitHub
25 changed files with 349 additions and 97 deletions
@ -0,0 +1,167 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.ReactiveUI; |
||||
|
using Microsoft.CodeAnalysis; |
||||
|
using Microsoft.CodeAnalysis.CSharp; |
||||
|
using XamlNameReferenceGenerator.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace XamlNameReferenceGenerator.Tests |
||||
|
{ |
||||
|
public class NameResolverTests |
||||
|
{ |
||||
|
private const string NamedControl = "NamedControl.xml"; |
||||
|
private const string NamedControls = "NamedControls.xml"; |
||||
|
private const string XNamedControl = "xNamedControl.xml"; |
||||
|
private const string XNamedControls = "xNamedControls.xml"; |
||||
|
private const string NoNamedControls = "NoNamedControls.xml"; |
||||
|
private const string CustomControls = "CustomControls.xml"; |
||||
|
private const string DataTemplates = "DataTemplates.xml"; |
||||
|
private const string SignUpView = "SignUpView.xml"; |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(NamedControl)] |
||||
|
[InlineData(XNamedControl)] |
||||
|
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Control(string resource) |
||||
|
{ |
||||
|
var xaml = await LoadEmbeddedResource(resource); |
||||
|
var compilation = CreateAvaloniaCompilation(); |
||||
|
var resolver = new NameResolver(compilation); |
||||
|
var controls = resolver.ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(1, controls.Count); |
||||
|
Assert.Equal("UserNameTextBox", controls[0].Name); |
||||
|
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(NamedControls)] |
||||
|
[InlineData(XNamedControls)] |
||||
|
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Named_Controls(string resource) |
||||
|
{ |
||||
|
var xaml = await LoadEmbeddedResource(resource); |
||||
|
var compilation = CreateAvaloniaCompilation(); |
||||
|
var resolver = new NameResolver(compilation); |
||||
|
var controls = resolver.ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(3, controls.Count); |
||||
|
Assert.Equal("UserNameTextBox", controls[0].Name); |
||||
|
Assert.Equal("PasswordTextBox", controls[1].Name); |
||||
|
Assert.Equal("SignUpButton", controls[2].Name); |
||||
|
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
||||
|
Assert.Equal(typeof(TextBox).FullName, controls[1].TypeName); |
||||
|
Assert.Equal(typeof(Button).FullName, controls[2].TypeName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Resolve_Types_From_Avalonia_Markup_File_With_Custom_Controls() |
||||
|
{ |
||||
|
var xaml = await LoadEmbeddedResource(CustomControls); |
||||
|
var compilation = CreateAvaloniaCompilation(); |
||||
|
var resolver = new NameResolver(compilation); |
||||
|
var controls = resolver.ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(2, controls.Count); |
||||
|
Assert.Equal("ClrNamespaceRoutedViewHost", controls[0].Name); |
||||
|
Assert.Equal("UriRoutedViewHost", controls[1].Name); |
||||
|
Assert.Equal(typeof(RoutedViewHost).FullName, controls[0].TypeName); |
||||
|
Assert.Equal(typeof(RoutedViewHost).FullName, controls[1].TypeName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Resolve_Named_Controls_From_Avalonia_Markup_File_Without_Named_Controls() |
||||
|
{ |
||||
|
var xaml = await LoadEmbeddedResource(NoNamedControls); |
||||
|
var compilation = CreateAvaloniaCompilation(); |
||||
|
var resolver = new NameResolver(compilation); |
||||
|
var controls = resolver.ResolveNames(xaml); |
||||
|
|
||||
|
Assert.Empty(controls); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Resolve_Elements_From_DataTemplates() |
||||
|
{ |
||||
|
var xaml = await LoadEmbeddedResource(DataTemplates); |
||||
|
var compilation = CreateAvaloniaCompilation(); |
||||
|
var resolver = new NameResolver(compilation); |
||||
|
var controls = resolver.ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(2, controls.Count); |
||||
|
Assert.Equal("UserNameTextBox", controls[0].Name); |
||||
|
Assert.Equal("NamedListBox", controls[1].Name); |
||||
|
Assert.Equal(typeof(TextBox).FullName, controls[0].TypeName); |
||||
|
Assert.Equal(typeof(ListBox).FullName, controls[1].TypeName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Resolve_Names_From_Complex_Views() |
||||
|
{ |
||||
|
var compilation = |
||||
|
CreateAvaloniaCompilation() |
||||
|
.AddSyntaxTrees( |
||||
|
CSharpSyntaxTree.ParseText( |
||||
|
"using Avalonia.Controls;" + |
||||
|
"namespace Controls {" + |
||||
|
" public class CustomTextBox : TextBox { }" + |
||||
|
"}")); |
||||
|
|
||||
|
var xaml = await LoadEmbeddedResource(SignUpView); |
||||
|
var resolver = new NameResolver(compilation); |
||||
|
var controls = resolver.ResolveNames(xaml); |
||||
|
|
||||
|
Assert.NotEmpty(controls); |
||||
|
Assert.Equal(9, controls.Count); |
||||
|
Assert.Equal("UserNameTextBox", controls[0].Name); |
||||
|
Assert.Equal("UserNameValidation", controls[1].Name); |
||||
|
Assert.Equal("PasswordTextBox", controls[2].Name); |
||||
|
Assert.Equal("PasswordValidation", controls[3].Name); |
||||
|
Assert.Equal("AwesomeListView", controls[4].Name); |
||||
|
Assert.Equal("ConfirmPasswordTextBox", controls[5].Name); |
||||
|
Assert.Equal("ConfirmPasswordValidation", controls[6].Name); |
||||
|
Assert.Equal("SignUpButton", controls[7].Name); |
||||
|
Assert.Equal("CompoundValidation", controls[8].Name); |
||||
|
} |
||||
|
|
||||
|
private static CSharpCompilation CreateAvaloniaCompilation(string name = "AvaloniaCompilation2") |
||||
|
{ |
||||
|
var compilation = CSharpCompilation |
||||
|
.Create(name, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(ITypeDescriptorContext).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(ISupportInitialize).Assembly.Location)) |
||||
|
.AddReferences(MetadataReference.CreateFromFile(typeof(TypeConverterAttribute).Assembly.Location)); |
||||
|
|
||||
|
var avaloniaAssemblyLocation = typeof(TextBlock).Assembly.Location; |
||||
|
var avaloniaAssemblyDirectory = Path.GetDirectoryName(avaloniaAssemblyLocation); |
||||
|
var avaloniaAssemblyReferences = Directory |
||||
|
.EnumerateFiles(avaloniaAssemblyDirectory!) |
||||
|
.Where(file => file.EndsWith(".dll") && file.Contains("Avalonia")) |
||||
|
.Select(file => MetadataReference.CreateFromFile(file)) |
||||
|
.ToList(); |
||||
|
|
||||
|
return compilation.AddReferences(avaloniaAssemblyReferences); |
||||
|
} |
||||
|
|
||||
|
private static async Task<string> LoadEmbeddedResource(string shortResourceName) |
||||
|
{ |
||||
|
var assembly = typeof(NameResolverTests).Assembly; |
||||
|
var fullResourceName = assembly |
||||
|
.GetManifestResourceNames() |
||||
|
.First(name => name.EndsWith(shortResourceName)); |
||||
|
|
||||
|
await using var stream = assembly.GetManifestResourceStream(fullResourceName); |
||||
|
using var reader = new StreamReader(stream!); |
||||
|
return await reader.ReadToEndAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" |
||||
|
xmlns:rxui="http://reactiveui.net"> |
||||
|
<custom:RoutedViewHost Name="ClrNamespaceRoutedViewHost" /> |
||||
|
<rxui:RoutedViewHost Name="UriRoutedViewHost" /> |
||||
|
</Window> |
||||
@ -0,0 +1,17 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<StackPanel> |
||||
|
<TextBox x:Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<ListBox Name="NamedListBox"> |
||||
|
<ListBox.ItemTemplate> |
||||
|
<DataTemplate x:Name="NamedDataTemplate"> |
||||
|
<TextBox x:Name="TemplatedTextBox" |
||||
|
Watermark="Templated input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</DataTemplate> |
||||
|
</ListBox.ItemTemplate> |
||||
|
</ListBox> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -0,0 +1,6 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<TextBox Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</Window> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<StackPanel> |
||||
|
<TextBox Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBox Name="PasswordTextBox" |
||||
|
Watermark="Password input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<Button Name="SignUpButton" |
||||
|
Content="Sign up" /> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<TextBox Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</Window> |
||||
@ -0,0 +1,44 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:controls="clr-namespace:Controls"> |
||||
|
<StackPanel> |
||||
|
<controls:CustomTextBox Margin="0 10 0 0" |
||||
|
Name="UserNameTextBox" |
||||
|
Watermark="Please, enter user name..." |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBlock Name="UserNameValidation" |
||||
|
Foreground="Red" |
||||
|
FontSize="12" /> |
||||
|
<TextBox Margin="0 10 0 0" |
||||
|
Name="PasswordTextBox" |
||||
|
Watermark="Please, enter your password..." |
||||
|
UseFloatingWatermark="True" |
||||
|
PasswordChar="*" /> |
||||
|
<TextBlock Name="PasswordValidation" |
||||
|
Foreground="Red" |
||||
|
FontSize="12" /> |
||||
|
<ListBox x:Name="AwesomeListView"> |
||||
|
<ListBox.ItemTemplate> |
||||
|
<DataTemplate> |
||||
|
<TextBlock x:Name="MeaningLessName" Text="{Binding}" /> |
||||
|
</DataTemplate> |
||||
|
</ListBox.ItemTemplate> |
||||
|
</ListBox> |
||||
|
<TextBox Margin="0 10 0 0" |
||||
|
x:Name="ConfirmPasswordTextBox" |
||||
|
Watermark="Please, confirm the password..." |
||||
|
UseFloatingWatermark="True" |
||||
|
PasswordChar="*" /> |
||||
|
<TextBlock x:Name="ConfirmPasswordValidation" |
||||
|
TextWrapping="Wrap" |
||||
|
Foreground="Red" |
||||
|
FontSize="12" /> |
||||
|
<Button Margin="0 10 0 5" |
||||
|
Content="Sign up" |
||||
|
x:Name="SignUpButton" /> |
||||
|
<TextBlock x:Name="CompoundValidation" |
||||
|
TextWrapping="Wrap" |
||||
|
Foreground="Red" |
||||
|
FontSize="12" /> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -0,0 +1,6 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<TextBox x:Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
</Window> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<Window xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<StackPanel> |
||||
|
<TextBox x:Name="UserNameTextBox" |
||||
|
Watermark="Username input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<TextBox x:Name="PasswordTextBox" |
||||
|
Watermark="Password input" |
||||
|
UseFloatingWatermark="True" /> |
||||
|
<Button x:Name="SignUpButton" |
||||
|
Content="Sign up" /> |
||||
|
</StackPanel> |
||||
|
</Window> |
||||
@ -1,13 +0,0 @@ |
|||||
using Xunit; |
|
||||
|
|
||||
namespace XamlNameReferenceGenerator.Tests |
|
||||
{ |
|
||||
public class XamlXNameReferenceXamlParserTests |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void Should_Resolve_Named_Controls_From_Xaml_Markup() |
|
||||
{ |
|
||||
// TODO: Fix all stuff.
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,17 @@ |
|||||
|
using XamlX.Ast; |
||||
|
using XamlX.Transform; |
||||
|
|
||||
|
namespace XamlNameReferenceGenerator.Infrastructure |
||||
|
{ |
||||
|
internal class DataTemplateTransformer : IXamlAstTransformer |
||||
|
{ |
||||
|
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
||||
|
{ |
||||
|
if (node is XamlAstObjectNode objectNode && |
||||
|
objectNode.Type is XamlAstXmlTypeReference typeReference && |
||||
|
typeReference.Name == "DataTemplate") |
||||
|
objectNode.Children.Clear(); |
||||
|
return node; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace XamlNameReferenceGenerator.Infrastructure |
||||
|
{ |
||||
|
internal interface INameResolver |
||||
|
{ |
||||
|
IReadOnlyList<(string TypeName, string Name)> ResolveNames(string xaml); |
||||
|
} |
||||
|
} |
||||
@ -1,9 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
|
|
||||
namespace XamlNameReferenceGenerator.Parsers |
|
||||
{ |
|
||||
internal interface INameReferenceXamlParser |
|
||||
{ |
|
||||
IReadOnlyList<(string TypeName, string Name)> GetNamedControls(string xaml); |
|
||||
} |
|
||||
} |
|
||||
@ -1,46 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Xml; |
|
||||
|
|
||||
namespace XamlNameReferenceGenerator.Parsers |
|
||||
{ |
|
||||
internal class XmlDocumentNameReferenceXamlParser : INameReferenceXamlParser |
|
||||
{ |
|
||||
public IReadOnlyList<(string TypeName, string Name)> GetNamedControls(string xaml) |
|
||||
{ |
|
||||
var document = new XmlDocument(); |
|
||||
document.LoadXml(xaml); |
|
||||
|
|
||||
var names = new List<(string TypeName, string Name)>(); |
|
||||
IterateThroughAllNodes(document, node => |
|
||||
{ |
|
||||
var type = node.Name; |
|
||||
if (type.Contains(":")) |
|
||||
type = type.Split(':')[1]; |
|
||||
|
|
||||
var name = node.Attributes?["x:Name"]?.Value ?? |
|
||||
node.Attributes?["Name"]?.Value; |
|
||||
if (!string.IsNullOrWhiteSpace(name)) |
|
||||
names.Add((type, name)); |
|
||||
}); |
|
||||
return names; |
|
||||
} |
|
||||
|
|
||||
private static void IterateThroughAllNodes(XmlDocument doc, Action<XmlNode> elementVisitor) |
|
||||
{ |
|
||||
foreach (XmlNode node in doc.ChildNodes) |
|
||||
{ |
|
||||
IterateNode(node, elementVisitor); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static void IterateNode(XmlNode node, Action<XmlNode> elementVisitor) |
|
||||
{ |
|
||||
elementVisitor(node); |
|
||||
foreach (XmlNode childNode in node.ChildNodes) |
|
||||
{ |
|
||||
IterateNode(childNode, elementVisitor); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue