From 1d06777df5b01e128baf1915e4816db05a02c102 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Thu, 4 Feb 2021 21:46:35 +0200 Subject: [PATCH 1/4] Use roslyn instead of reflection for Component Generator --- .../ComponentLocation.cs | 23 -- .../ComponentWrapperGenerator.cs | 351 +++++++++--------- .../ComponentWrapperGenerator.csproj | 4 +- .../ITypeOrNamespaceSymbolExtensions.cs | 49 +++ src/ComponentWrapperGenerator/Program.cs | 79 ++-- .../TypesToGenerate.txt | 98 +++-- 6 files changed, 309 insertions(+), 295 deletions(-) delete mode 100644 src/ComponentWrapperGenerator/ComponentLocation.cs create mode 100644 src/ComponentWrapperGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs diff --git a/src/ComponentWrapperGenerator/ComponentLocation.cs b/src/ComponentWrapperGenerator/ComponentLocation.cs deleted file mode 100644 index 2528e317..00000000 --- a/src/ComponentWrapperGenerator/ComponentLocation.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System.Reflection; - -namespace ComponentWrapperGenerator -{ - public class ComponentLocation - { - public Assembly Assembly { get; } - public string NamespaceName { get; } - public string NamespacePrefix { get; } - public string XmlDocFilename { get; } - - public ComponentLocation(Assembly assembly, string namespaceName, string namespacePrefix, string xmlDocFilename) - { - Assembly = assembly; - NamespaceName = namespaceName; - NamespacePrefix = namespacePrefix; - XmlDocFilename = xmlDocFilename; - } - } -} diff --git a/src/ComponentWrapperGenerator/ComponentWrapperGenerator.cs b/src/ComponentWrapperGenerator/ComponentWrapperGenerator.cs index 98873a6c..af50c7f2 100644 --- a/src/ComponentWrapperGenerator/ComponentWrapperGenerator.cs +++ b/src/ComponentWrapperGenerator/ComponentWrapperGenerator.cs @@ -1,17 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using ComponentWrapperGenerator.Extensions; +using Microsoft.CodeAnalysis; using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; using System.Text; -using System.Windows.Input; using System.Xml; -using XF = Xamarin.Forms; namespace ComponentWrapperGenerator { @@ -20,15 +19,12 @@ public class ComponentWrapperGenerator #pragma warning restore CA1724 // Type name conflicts with namespace name { private GeneratorSettings Settings { get; } - private IList XmlDocs { get; } - - public ComponentWrapperGenerator(GeneratorSettings settings, IList xmlDocs) + public ComponentWrapperGenerator(GeneratorSettings settings) { Settings = settings ?? throw new ArgumentNullException(nameof(settings)); - XmlDocs = xmlDocs ?? throw new ArgumentNullException(nameof(xmlDocs)); } - public void GenerateComponentWrapper(Type typeToGenerate, string outputFolder) + public void GenerateComponentWrapper(INamedTypeSymbol typeToGenerate, string outputFolder) { typeToGenerate = typeToGenerate ?? throw new ArgumentNullException(nameof(typeToGenerate)); @@ -38,7 +34,7 @@ public void GenerateComponentWrapper(Type typeToGenerate, string outputFolder) GenerateHandlerFile(typeToGenerate, propertiesToGenerate, outputFolder); } - private void GenerateComponentFile(Type typeToGenerate, IEnumerable propertiesToGenerate, string outputFolder) + private void GenerateComponentFile(INamedTypeSymbol typeToGenerate, IEnumerable propertiesToGenerate, string outputFolder) { var fileName = Path.Combine(outputFolder, $"{typeToGenerate.Name}.generated.cs"); var directoryName = Path.GetDirectoryName(fileName); @@ -47,7 +43,7 @@ private void GenerateComponentFile(Type typeToGenerate, IEnumerable ctor.IsPublic && !ctor.GetParameters().Any()); + var componentHasPublicParameterlessConstructor = typeToGenerate.Constructors + .Any(ctor => ctor.DeclaredAccessibility == Accessibility.Public && !ctor.Parameters.Any()); var staticConstructor = string.Empty; if (!isComponentAbstract && componentHasPublicParameterlessConstructor) @@ -147,15 +141,17 @@ protected override void RenderAttributes(AttributesBuilder builder) File.WriteAllText(fileName, outputBuilder.ToString()); } - private static string GetNamespacePrefix(Type type, List usings) + private static string GetNamespacePrefix(ITypeSymbol type, List usings) { // Check if there's a 'using' already. If so, check if it has an alias. If not, add a new 'using'. var namespaceAlias = string.Empty; - var existingUsing = usings.FirstOrDefault(u => u.Namespace == type.Namespace); + var namespaceName = type.ContainingNamespace.GetFullName(); + + var existingUsing = usings.FirstOrDefault(u => u.Namespace == namespaceName); if (existingUsing == null) { - usings.Add(new UsingStatement { Namespace = type.Namespace, IsUsed = true, }); + usings.Add(new UsingStatement { Namespace = type.ContainingNamespace.GetFullName(), IsUsed = true, }); return string.Empty; } else @@ -172,35 +168,36 @@ private static string GetNamespacePrefix(Type type, List usings) } } - private static readonly List DisallowedComponentPropertyTypes = new List + private static readonly List DisallowedComponentPropertyTypes = new List { - typeof(XF.Brush), - typeof(XF.Button.ButtonContentLayout), // TODO: This is temporary; should be possible to add support later - typeof(XF.ColumnDefinitionCollection), - typeof(XF.ControlTemplate), - typeof(XF.DataTemplate), - typeof(XF.Element), - typeof(XF.Font), // TODO: This is temporary; should be possible to add support later - typeof(XF.FormattedString), - typeof(XF.Shapes.Geometry), - typeof(ICommand), - typeof(object), - typeof(XF.Page), - typeof(XF.ResourceDictionary), - typeof(XF.RowDefinitionCollection), - typeof(XF.ShellContent), - typeof(XF.ShellItem), - typeof(XF.ShellSection), - typeof(XF.Style), // TODO: This is temporary; should be possible to add support later - typeof(XF.IVisual), - typeof(XF.View), + "Xamarin.Forms.Brush", + "Xamarin.Forms.Button.ButtonContentLayout", // TODO: This is temporary; should be possible to add support later + "Xamarin.Forms.ColumnDefinitionCollection", + "Xamarin.Forms.ControlTemplate", + "Xamarin.Forms.DataTemplate", + "Xamarin.Forms.Element", + "Xamarin.Forms.Font", // TODO: This is temporary; should be possible to add support later + "Xamarin.Forms.FormattedString", + "Xamarin.Forms.Shapes.Geometry", + "System.Windows.Input.ICommand", + "System.Object", + "Xamarin.Forms.Page", + "Xamarin.Forms.ResourceDictionary", + "Xamarin.Forms.RowDefinitionCollection", + "Xamarin.Forms.ShellContent", + "Xamarin.Forms.ShellItem", + "Xamarin.Forms.ShellSection", + "Xamarin.Forms.Style", // TODO: This is temporary; should be possible to add support later + "Xamarin.Forms.IVisual", + "Xamarin.Forms.View", }; - private string GetPropertyDeclaration(PropertyInfo prop, IList usings) + private static string GetPropertyDeclaration(IPropertySymbol prop, IList usings) { - var propertyType = prop.PropertyType; + var propertyType = prop.Type; string propertyTypeName; - if (propertyType == typeof(IList)) + + if (propertyType.GetFullName() == "System.Collections.Generic.IList") { // Lists of strings are special-cased because they are handled specially by the handlers as a comma-separated list propertyTypeName = "string"; @@ -220,11 +217,10 @@ private string GetPropertyDeclaration(PropertyInfo prop, IList u return $@"{xmlDocContents}{indent}[Parameter] public {propertyTypeName} {GetIdentifierName(prop.Name)} {{ get; set; }} "; } - private static string GetXmlDocText(XmlElement xmlDocElement) { var allText = xmlDocElement?.InnerXml; - allText = allText.Replace("To be added.", string.Empty, StringComparison.Ordinal); + allText = allText?.Replace("To be added.", string.Empty, StringComparison.Ordinal); if (string.IsNullOrWhiteSpace(allText)) { return null; @@ -232,50 +228,52 @@ private static string GetXmlDocText(XmlElement xmlDocElement) return allText; } - private string GetXmlDocContents(PropertyInfo prop, string indent) + private static string GetXmlDocContents(IPropertySymbol prop, string indent) { - foreach (var xmlDoc in XmlDocs) - { - - var xmlDocContents = string.Empty; - // Format of XML docs we're looking for in a given property: - // - // Gets or sets the of the ActivityIndicator. This is a bindable property. - // A used to display the ActivityIndicator. Default is . - // - // - var xmlDocNodeName = $"P:{prop.DeclaringType.Namespace}.{prop.DeclaringType.Name}.{prop.Name}"; - var xmlDocNode = xmlDoc.SelectSingleNode($"//member[@name='{xmlDocNodeName}']"); - if (xmlDocNode != null) + var xmlDocString = prop.GetDocumentationCommentXml(); + + if (string.IsNullOrEmpty(xmlDocString)) + { + return null; + } + + var xmlDoc = new XmlDocument(); + // Returned XML doc string has no root element, which does not allow to parse it. + xmlDoc.LoadXml($"{xmlDocString}"); + var xmlDocNode = xmlDoc.FirstChild; + + var xmlDocContents = string.Empty; + // Format of XML docs we're looking for in a given property: + // + // Gets or sets the of the ActivityIndicator. This is a bindable property. + // A used to display the ActivityIndicator. Default is . + // + // + + var summaryText = GetXmlDocText(xmlDocNode["summary"]); + var valueText = GetXmlDocText(xmlDocNode["value"]); + + if (summaryText != null || valueText != null) + { + var xmlDocContentBuilder = new StringBuilder(); + if (summaryText != null) { - var summaryText = GetXmlDocText(xmlDocNode["summary"]); - var valueText = GetXmlDocText(xmlDocNode["value"]); - - if (summaryText != null || valueText != null) - { - var xmlDocContentBuilder = new StringBuilder(); - if (summaryText != null) - { - xmlDocContentBuilder.AppendLine($"{indent}/// "); - xmlDocContentBuilder.AppendLine($"{indent}/// {summaryText}"); - xmlDocContentBuilder.AppendLine($"{indent}/// "); - } - if (valueText != null) - { - xmlDocContentBuilder.AppendLine($"{indent}/// "); - xmlDocContentBuilder.AppendLine($"{indent}/// {valueText}"); - xmlDocContentBuilder.AppendLine($"{indent}/// "); - } - xmlDocContents = xmlDocContentBuilder.ToString(); - } - return xmlDocContents; + xmlDocContentBuilder.AppendLine($"{indent}/// "); + xmlDocContentBuilder.AppendLine($"{indent}/// {summaryText}"); + xmlDocContentBuilder.AppendLine($"{indent}/// "); } + if (valueText != null) + { + xmlDocContentBuilder.AppendLine($"{indent}/// "); + xmlDocContentBuilder.AppendLine($"{indent}/// {valueText}"); + xmlDocContentBuilder.AppendLine($"{indent}/// "); + } + xmlDocContents = xmlDocContentBuilder.ToString(); } - - return null; + return xmlDocContents; } - private static string GetTypeNameAndAddNamespace(Type type, IList usings) + private static string GetTypeNameAndAddNamespace(ITypeSymbol type, IList usings) { var typeName = GetCSharpType(type); if (typeName != null) @@ -286,10 +284,12 @@ private static string GetTypeNameAndAddNamespace(Type type, IList u.Namespace == type.Namespace); + var containingNamespaceName = type.ContainingNamespace.GetFullName(); + + var existingUsing = usings.FirstOrDefault(u => u.Namespace == containingNamespaceName); if (existingUsing == null) { - usings.Add(new UsingStatement { Namespace = type.Namespace, IsUsed = true, }); + usings.Add(new UsingStatement { Namespace = containingNamespaceName, IsUsed = true, }); } else { @@ -303,16 +303,19 @@ private static string GetTypeNameAndAddNamespace(Type type, IList usings) + private static string FormatTypeName(ITypeSymbol type, IList usings) { - if (!type.IsGenericType) + var namedType = type as INamedTypeSymbol; + + if (namedType == null || !namedType.IsGenericType) { return type.Name; } + var typeNameBuilder = new StringBuilder(); - typeNameBuilder.Append(type.Name.Substring(0, type.Name.IndexOf('`', StringComparison.Ordinal))); + typeNameBuilder.Append(type.Name); typeNameBuilder.Append('<'); - var genericArgs = type.GetGenericArguments(); + var genericArgs = namedType.TypeArguments; for (var i = 0; i < genericArgs.Length; i++) { if (i > 0) @@ -326,41 +329,42 @@ private static string FormatTypeName(Type type, IList usings) return typeNameBuilder.ToString(); } - private static readonly Dictionary> TypeToAttributeHelperGetter = new Dictionary> + private static readonly Dictionary> TypeToAttributeHelperGetter = new Dictionary> { - { typeof(XF.Color), propValue => $"AttributeHelper.ColorToString({propValue})" }, - { typeof(XF.CornerRadius), propValue => $"AttributeHelper.CornerRadiusToString({propValue})" }, - { typeof(XF.GridLength), propValue => $"AttributeHelper.GridLengthToString({propValue})" }, - { typeof(XF.ImageSource), propValue => $"AttributeHelper.ObjectToDelegate({propValue})" }, - { typeof(XF.Keyboard), propValue => $"AttributeHelper.ObjectToDelegate({propValue})" }, - { typeof(XF.LayoutOptions), propValue => $"AttributeHelper.LayoutOptionsToString({propValue})" }, - { typeof(XF.Thickness), propValue => $"AttributeHelper.ThicknessToString({propValue})" }, - { typeof(DateTime), propValue => $"AttributeHelper.DateTimeToString({propValue})" }, - { typeof(TimeSpan), propValue => $"AttributeHelper.TimeSpanToString({propValue})" }, - { typeof(bool), propValue => $"{propValue}" }, - { typeof(double), propValue => $"AttributeHelper.DoubleToString({propValue})" }, - { typeof(float), propValue => $"AttributeHelper.SingleToString({propValue})" }, - { typeof(int), propValue => $"{propValue}" }, - { typeof(string), propValue => $"{propValue}" }, - { typeof(IList), propValue => $"{propValue}" }, + { "Xamarin.Forms.Color", propValue => $"AttributeHelper.ColorToString({propValue})" }, + { "Xamarin.Forms.CornerRadius", propValue => $"AttributeHelper.CornerRadiusToString({propValue})" }, + { "Xamarin.Forms.GridLength", propValue => $"AttributeHelper.GridLengthToString({propValue})" }, + { "Xamarin.Forms.ImageSource", propValue => $"AttributeHelper.ObjectToDelegate({propValue})" }, + { "Xamarin.Forms.Keyboard", propValue => $"AttributeHelper.ObjectToDelegate({propValue})" }, + { "Xamarin.Forms.LayoutOptions", propValue => $"AttributeHelper.LayoutOptionsToString({propValue})" }, + { "Xamarin.Forms.Thickness", propValue => $"AttributeHelper.ThicknessToString({propValue})" }, + { "System.DateTime", propValue => $"AttributeHelper.DateTimeToString({propValue})" }, + { "System.TimeSpan", propValue => $"AttributeHelper.TimeSpanToString({propValue})" }, + { "System.Boolean", propValue => $"{propValue}" }, + { "System.Double", propValue => $"AttributeHelper.DoubleToString({propValue})" }, + { "System.Single", propValue => $"AttributeHelper.SingleToString({propValue})" }, + { "System.Int32", propValue => $"{propValue}" }, + { "System.String", propValue => $"{propValue}" }, + { "System.Collections.Generic.IList", propValue => $"{propValue}" }, }; - private static string GetPropertyRenderAttribute(PropertyInfo prop) + private static string GetPropertyRenderAttribute(IPropertySymbol prop) { - var propValue = prop.PropertyType.IsValueType ? $"{GetIdentifierName(prop.Name)}.Value" : GetIdentifierName(prop.Name); + var propValue = prop.Type.IsValueType ? $"{GetIdentifierName(prop.Name)}.Value" : GetIdentifierName(prop.Name); var formattedValue = propValue; - if (TypeToAttributeHelperGetter.TryGetValue(prop.PropertyType, out var formattingFunc)) + + if (TypeToAttributeHelperGetter.TryGetValue(prop.Type.GetFullName(), out var formattingFunc)) { formattedValue = formattingFunc(propValue); } - else if (prop.PropertyType.IsEnum) + else if (prop.Type.TypeKind == TypeKind.Enum) { formattedValue = $"(int){formattedValue}"; } else { // TODO: Error? - Console.WriteLine($"WARNING: Couldn't generate attribute render for {prop.DeclaringType.Name}.{prop.Name}"); + Console.WriteLine($"WARNING: Couldn't generate attribute render for {prop.ContainingType.Name}.{prop.Name}"); } return $@" if ({GetIdentifierName(prop.Name)} != null) @@ -370,28 +374,28 @@ private static string GetPropertyRenderAttribute(PropertyInfo prop) "; } - private static readonly Dictionary TypeToCSharpName = new Dictionary + private static readonly Dictionary TypeToCSharpName = new Dictionary { - { typeof(bool), "bool" }, - { typeof(byte), "byte" }, - { typeof(sbyte), "sbyte" }, - { typeof(char), "char" }, - { typeof(decimal), "decimal" }, - { typeof(double), "double" }, - { typeof(float), "float" }, - { typeof(int), "int" }, - { typeof(uint), "uint" }, - { typeof(long), "long" }, - { typeof(ulong), "ulong" }, - { typeof(object), "object" }, - { typeof(short), "short" }, - { typeof(ushort), "ushort" }, - { typeof(string), "string" }, + { SpecialType.System_Boolean, "bool" }, + { SpecialType.System_Byte, "byte" }, + { SpecialType.System_SByte, "sbyte" }, + { SpecialType.System_Char, "char" }, + { SpecialType.System_Decimal, "decimal" }, + { SpecialType.System_Double, "double" }, + { SpecialType.System_Single, "float" }, + { SpecialType.System_Int32, "int" }, + { SpecialType.System_UInt32, "uint" }, + { SpecialType.System_Int64, "long" }, + { SpecialType.System_UInt64, "ulong" }, + { SpecialType.System_Object, "object" }, + { SpecialType.System_Int16, "short" }, + { SpecialType.System_UInt16, "ushort" }, + { SpecialType.System_String, "string" }, }; - private static string GetCSharpType(Type propertyType) + private static string GetCSharpType(ITypeSymbol propertyType) { - return TypeToCSharpName.TryGetValue(propertyType, out var typeName) ? typeName : null; + return TypeToCSharpName.TryGetValue(propertyType.SpecialType, out var typeName) ? typeName : null; } /// @@ -401,7 +405,7 @@ private static string GetCSharpType(Type propertyType) /// /// /// - private static Type GetBaseTypeOfInterest(Type type) + private static INamedTypeSymbol GetBaseTypeOfInterest(INamedTypeSymbol type) { do { @@ -416,7 +420,7 @@ private static Type GetBaseTypeOfInterest(Type type) return null; } - private void GenerateHandlerFile(Type typeToGenerate, IEnumerable propertiesToGenerate, string outputFolder) + private void GenerateHandlerFile(INamedTypeSymbol typeToGenerate, IEnumerable propertiesToGenerate, string outputFolder) { var fileName = Path.Combine(outputFolder, "Handlers", $"{typeToGenerate.Name}Handler.generated.cs"); var directoryName = Path.GetDirectoryName(fileName); @@ -425,7 +429,7 @@ private void GenerateHandlerFile(Type typeToGenerate, IEnumerable Directory.CreateDirectory(directoryName); } - Console.WriteLine($"Generating component handler for type '{typeToGenerate.FullName}' into file '{fileName}'."); + Console.WriteLine($"Generating component handler for type '{typeToGenerate.Name}' into file '{fileName}'."); var componentName = typeToGenerate.Name; var componentVarName = char.ToLowerInvariant(componentName[0]) + componentName.Substring(1); @@ -516,7 +520,7 @@ namespace {Settings.RootNamespace}.Handlers File.WriteAllText(fileName, outputBuilder.ToString()); } - private static string GetPropertySetAttribute(PropertyInfo prop, List usings) + private static string GetPropertySetAttribute(IPropertySymbol prop, List usings) { // Handle null values by resetting to default value var resetValueParameterExpression = BindablePropertyExistsForProp(prop) @@ -524,7 +528,7 @@ private static string GetPropertySetAttribute(PropertyInfo prop, List properties, IList usings) + private static string GetDefaultPropertyValues(ITypeSymbol type, IEnumerable properties, IList usings) { var bindableProps = properties.Where(BindablePropertyExistsForProp).ToList(); @@ -576,7 +580,7 @@ private static string GetDefaultPropertyValues(Type type, IEnumerable 0; } - private static readonly Dictionary TypeToAttributeHelperSetter = new Dictionary + private static readonly Dictionary TypeToAttributeHelperSetter = new Dictionary { - { typeof(XF.Color), "AttributeHelper.StringToColor((string)attributeValue{0})" }, - { typeof(XF.CornerRadius), "AttributeHelper.StringToCornerRadius(attributeValue{0})" }, - { typeof(XF.GridLength), "AttributeHelper.StringToGridLength(attributeValue{0})" }, - { typeof(XF.ImageSource), "AttributeHelper.DelegateToObject(attributeValue{0})" }, - { typeof(XF.Keyboard), "AttributeHelper.DelegateToObject(attributeValue{0})" }, - { typeof(XF.LayoutOptions), "AttributeHelper.StringToLayoutOptions(attributeValue{0})" }, - { typeof(XF.Thickness), "AttributeHelper.StringToThickness(attributeValue{0})" }, - { typeof(DateTime), "AttributeHelper.StringToDateTime(attributeValue{0})" }, - { typeof(TimeSpan), "AttributeHelper.StringToTimeSpan(attributeValue{0})" }, - { typeof(bool), "AttributeHelper.GetBool(attributeValue{0})" }, - { typeof(double), "AttributeHelper.StringToDouble((string)attributeValue{0})" }, - { typeof(float), "AttributeHelper.StringToSingle((string)attributeValue{0})" }, - { typeof(int), "AttributeHelper.GetInt(attributeValue{0})" }, - { typeof(IList), "AttributeHelper.GetStringList(attributeValue)" }, + { "Xamarin.Forms.Color", "AttributeHelper.StringToColor((string)attributeValue{0})" }, + { "Xamarin.Forms.CornerRadius", "AttributeHelper.StringToCornerRadius(attributeValue{0})" }, + { "Xamarin.Forms.GridLength", "AttributeHelper.StringToGridLength(attributeValue{0})" }, + { "Xamarin.Forms.ImageSource", "AttributeHelper.DelegateToObject(attributeValue{0})" }, + { "Xamarin.Forms.Keyboard", "AttributeHelper.DelegateToObject(attributeValue{0})" }, + { "Xamarin.Forms.LayoutOptions", "AttributeHelper.StringToLayoutOptions(attributeValue{0})" }, + { "Xamarin.Forms.Thickness", "AttributeHelper.StringToThickness(attributeValue{0})" }, + { "System.DateTime", "AttributeHelper.StringToDateTime(attributeValue{0})" }, + { "System.TimeSpan", "AttributeHelper.StringToTimeSpan(attributeValue{0})" }, + { "System.Boolean", "AttributeHelper.GetBool(attributeValue{0})" }, + { "System.Double", "AttributeHelper.StringToDouble((string)attributeValue{0})" }, + { "System.Single", "AttributeHelper.StringToSingle((string)attributeValue{0})" }, + { "System.Int32", "AttributeHelper.GetInt(attributeValue{0})" }, + { "System.Collections.Generic.IList", "AttributeHelper.GetStringList(attributeValue)" }, }; - private static IEnumerable GetPropertiesToGenerate(Type componentType) + private static IEnumerable GetPropertiesToGenerate(ITypeSymbol componentType) { - var allPublicProperties = componentType.GetProperties(); - - return - allPublicProperties - .Where(HasPublicGetAndSet) - .Where(prop => prop.DeclaringType == componentType) - .Where(prop => !DisallowedComponentPropertyTypes.Contains(prop.PropertyType)) - .Where(IsPropertyBrowsable) - .OrderBy(prop => prop.Name, StringComparer.OrdinalIgnoreCase) - .ToList(); + var allPublicProperties = componentType.GetMembers() + .OfType() + .Where(p => p.DeclaredAccessibility == Accessibility.Public); + + return allPublicProperties + .Where(HasPublicGetAndSet) + .Where(IsPropertyBrowsable) + .Where(prop => !DisallowedComponentPropertyTypes.Contains(prop.Type.GetFullName())) + .OrderBy(prop => prop.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); } - private static bool HasPublicGetAndSet(PropertyInfo propInfo) + private static bool HasPublicGetAndSet(IPropertySymbol propInfo) { - return propInfo.GetGetMethod() != null && propInfo.GetSetMethod() != null; + return propInfo.GetMethod?.DeclaredAccessibility == Accessibility.Public + && propInfo.SetMethod?.DeclaredAccessibility == Accessibility.Public; } - private static bool IsPropertyBrowsable(PropertyInfo propInfo) + private static bool IsPropertyBrowsable(IPropertySymbol propInfo) { // [EditorBrowsable(EditorBrowsableState.Never)] - var attr = (EditorBrowsableAttribute)Attribute.GetCustomAttribute(propInfo, typeof(EditorBrowsableAttribute)); - return (attr == null) || (attr.State != EditorBrowsableState.Never); + return !propInfo.GetAttributes().Any(a => a.AttributeClass?.Name == nameof(EditorBrowsableAttribute) + && a.ConstructorArguments.Length == 1 + && a.ConstructorArguments[0].Value?.Equals((int)EditorBrowsableState.Never) == true); } private static string GetIdentifierName(string possibleIdentifier) diff --git a/src/ComponentWrapperGenerator/ComponentWrapperGenerator.csproj b/src/ComponentWrapperGenerator/ComponentWrapperGenerator.csproj index cb314ccd..116c0691 100644 --- a/src/ComponentWrapperGenerator/ComponentWrapperGenerator.csproj +++ b/src/ComponentWrapperGenerator/ComponentWrapperGenerator.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.0 + netcoreapp3.1 false @@ -17,6 +17,8 @@ + + diff --git a/src/ComponentWrapperGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs b/src/ComponentWrapperGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs new file mode 100644 index 00000000..308801be --- /dev/null +++ b/src/ComponentWrapperGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; + +namespace ComponentWrapperGenerator.Extensions +{ + internal static class ITypeOrNamespaceSymbolExtensions + { + internal static string GetFullName(this INamespaceOrTypeSymbol namespaceOrType) + { + var stack = new Stack(); + + stack.Push(GetName(namespaceOrType)); + + if (namespaceOrType.ContainingType != null) + { + stack.Push(GetName(namespaceOrType.ContainingType)); + } + + var currentNamespace = namespaceOrType.ContainingNamespace; + while (!currentNamespace.IsGlobalNamespace) + { + stack.Push(currentNamespace.Name); + currentNamespace = currentNamespace.ContainingNamespace; + } + + return string.Join('.', stack); + } + + /// + /// Returns name with generic type arguments (is any). + /// + private static string GetName(INamespaceOrTypeSymbol namespaceOrType) + { + if (namespaceOrType is INamedTypeSymbol namedType && namedType.IsGenericType) + { + var genericTypesNames = string.Join(", ", namedType.TypeArguments.Select(GetFullName)); + return $"{namedType.Name}<{genericTypesNames}>"; + } + else + { + return namespaceOrType.Name; + } + } + } +} diff --git a/src/ComponentWrapperGenerator/Program.cs b/src/ComponentWrapperGenerator/Program.cs index b046d758..9a2a933c 100644 --- a/src/ComponentWrapperGenerator/Program.cs +++ b/src/ComponentWrapperGenerator/Program.cs @@ -1,25 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using Microsoft.CodeAnalysis; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Xml; +using System.Reflection; +using System.Threading.Tasks; namespace ComponentWrapperGenerator { internal class Program { - private static readonly List ComponentLocations = - new List - { - new ComponentLocation(typeof(Xamarin.Forms.Element).Assembly, "Xamarin.Forms", "XF", @"bin\Debug\netcoreapp3.0\Xamarin.Forms.Core.xml"), - new ComponentLocation(typeof(Xamarin.Forms.DualScreen.TwoPaneView).Assembly, "Xamarin.Forms.DualScreen", "XFD", null), - }; - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "")] - internal static int Main(string[] args) + internal static async Task Main(string[] args) { // Un-comment these lines for easier debugging if (args.Length == 0) @@ -45,9 +37,8 @@ internal static int Main(string[] args) RootNamespace = "Microsoft.MobileBlazorBindings.Elements", }; - var xmlDocs = LoadXmlDocs(ComponentLocations.Select(loc => loc.XmlDocFilename).Where(loc => loc != null)); - - var generator = new ComponentWrapperGenerator(settings, xmlDocs); + var compilation = await GetRoslynCompilationAsync().ConfigureAwait(false); + var generator = new ComponentWrapperGenerator(settings, compilation); foreach (var typeNameToGenerate in listOfTypeNamesToGenerate) { @@ -62,7 +53,9 @@ internal static int Main(string[] args) continue; } - if (!TryGetTypeToGenerate(typeNameToGenerate, out var typeToGenerate)) + var typeToGenerate = compilation.GetTypeByMetadataName(typeNameToGenerate); + + if (typeToGenerate == null) { Console.WriteLine($"WARNING: Couldn't find type {typeNameToGenerate}."); Console.WriteLine(); @@ -75,46 +68,38 @@ internal static int Main(string[] args) return 0; } - private static IList LoadXmlDocs(IEnumerable xmlDocLocations) + private static async Task GetRoslynCompilationAsync() { - var xmlDocs = new List(); - foreach (var xmlDocLocation in xmlDocLocations) - { - var xmlDoc = new XmlDocument(); + var metadataReferences = new[] { + MetadataReferenceFromAssembly(typeof(Xamarin.Forms.Element).Assembly), + MetadataReferenceFromAssembly(typeof(Xamarin.Forms.DualScreen.TwoPaneView).Assembly), + MetadataReferenceFromAssembly(typeof(object).Assembly), + MetadataReferenceFromAssembly(Assembly.Load(new AssemblyName("System.Runtime"))), + MetadataReferenceFromAssembly(Assembly.Load(new AssemblyName("netstandard"))) + }; - // Depending on whether you run from VS or command line, the relative path of the XML docs will be - // different. There's undoubtedly a better way to do this, but this works great. - var xmlDocPath = Path.Combine(Directory.GetCurrentDirectory(), xmlDocLocation); - if (!File.Exists(xmlDocPath)) - { - xmlDocPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetFileName(xmlDocLocation)); - } + var projectName = "NewProject"; + var projectInfo = ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Create(), projectName, projectName, LanguageNames.CSharp, + metadataReferences: metadataReferences); - xmlDoc.Load(xmlDocPath); - xmlDocs.Add(xmlDoc); - } - return xmlDocs; + using var workspace = new AdhocWorkspace(); + var project = workspace.AddProject(projectInfo); + + return await project.GetCompilationAsync().ConfigureAwait(false); } - private static bool IsCommentLine(string typeNameToGenerate) + private static PortableExecutableReference MetadataReferenceFromAssembly(Assembly assembly) { - return typeNameToGenerate[0] == '#'; + var assemblyPath = assembly.Location; + var xmlDocPath = Path.ChangeExtension(assemblyPath, "xml"); + var docProvider = File.Exists(xmlDocPath) ? XmlDocumentationProvider.CreateFromFile(xmlDocPath) : null; + + return MetadataReference.CreateFromFile(assemblyPath, documentation: docProvider); } - private static bool TryGetTypeToGenerate(string typeName, out Type typeToGenerate) + private static bool IsCommentLine(string typeNameToGenerate) { - foreach (var componentLocation in ComponentLocations) - { - var fullTypeName = componentLocation.NamespaceName + "." + typeName; - typeToGenerate = componentLocation.Assembly.GetType(fullTypeName); - if (typeToGenerate != null) - { - return true; - } - } - - typeToGenerate = null; - return false; + return typeNameToGenerate[0] == '#'; } } } diff --git a/src/ComponentWrapperGenerator/TypesToGenerate.txt b/src/ComponentWrapperGenerator/TypesToGenerate.txt index 11a97b2c..5cf86651 100644 --- a/src/ComponentWrapperGenerator/TypesToGenerate.txt +++ b/src/ComponentWrapperGenerator/TypesToGenerate.txt @@ -1,56 +1,52 @@ # Xamarin.Forms components -ActivityIndicator -BaseMenuItem -BaseShellItem -BoxView -Button -CheckBox -ContentPage -ContentView -DatePicker -#Element // Element is special so we don't generate it -Entry -FlyoutItem -#FormattedString // Not used anymore because Label directly contains Span elements -Frame -GestureElement -Grid +Xamarin.Forms.ActivityIndicator +Xamarin.Forms.BaseMenuItem +Xamarin.Forms.BaseShellItem +Xamarin.Forms.BoxView +Xamarin.Forms.Button +Xamarin.Forms.CheckBox +Xamarin.Forms.ContentPage +Xamarin.Forms.ContentView +Xamarin.Forms.DatePicker +#Xamarin.Forms.Element // Element is special so we don't generate it +Xamarin.Forms.Entry +Xamarin.Forms.FlyoutItem +#Xamarin.Forms.FormattedString // Not used anymore because Label directly contains Span elements +Xamarin.Forms.Frame +Xamarin.Forms.GestureElement +Xamarin.Forms.Grid #GridCell // Not based on real XF type -Image -ImageButton -InputView -Label -Layout -#FlyoutDetailPage // Not based on real XF type -#FlyoutFlyoutPage // Not based on real XF type -FlyoutPage -MenuItem +Xamarin.Forms.Image +Xamarin.Forms.ImageButton +Xamarin.Forms.InputView +Xamarin.Forms.Label +Xamarin.Forms.Layout +Xamarin.Forms.FlyoutPage +Xamarin.Forms.MenuItem #ModalContainer // Not based on real XF type -NavigableElement -Page -#Picker // Manually written to use custom logic for generics and binding -ProgressBar -ScrollView -Shell -ShellContent +Xamarin.Forms.NavigableElement +Xamarin.Forms.Page +#Xamarin.Forms.Picker // Manually written to use custom logic for generics and binding +Xamarin.Forms.ProgressBar +Xamarin.Forms.ScrollView +Xamarin.Forms.Shell +Xamarin.Forms.ShellContent #ShellFlyoutHeader // Not based on real XF type -ShellGroupItem -ShellItem -ShellSection -Slider -Span -StackLayout -Stepper +Xamarin.Forms.ShellGroupItem +Xamarin.Forms.ShellItem +Xamarin.Forms.ShellSection +Xamarin.Forms.Slider +Xamarin.Forms.Span +Xamarin.Forms.StackLayout +Xamarin.Forms.Stepper #StyleSheet // Not based on real XF type -Switch -Tab -TabBar -TabbedPage -TemplatedPage -TemplatedView -TimePicker -View -VisualElement - -# Xamarin.Forms.DualScreen components -TwoPaneView +Xamarin.Forms.Switch +Xamarin.Forms.Tab +Xamarin.Forms.TabBar +Xamarin.Forms.TabbedPage +Xamarin.Forms.TemplatedPage +Xamarin.Forms.TemplatedView +Xamarin.Forms.TimePicker +Xamarin.Forms.View +Xamarin.Forms.VisualElement +Xamarin.Forms.DualScreen.TwoPaneView From ae1be4b0b4dfea6c7a5932665b8b926012a7b25b Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Wed, 10 Feb 2021 01:08:51 +0200 Subject: [PATCH 2/4] Add Component source generator project --- CI/CI.Windows.proj | 1 + MobileBlazorBindings.sln | 15 +++++ .../ComponentWrapperGenerator.csproj | 4 ++ .../GeneratorSettings.cs | 11 ---- src/ComponentWrapperGenerator/Program.cs | 24 +++++++- .../ComponentWrapperGenerator.cs | 43 +++++--------- .../DiagnosticDescriptors.cs | 17 ++++++ .../ITypeOrNamespaceSymbolExtensions.cs | 6 +- .../GeneratorSettings.cs | 14 +++++ .../GeneratorSettingsParser.cs | 37 ++++++++++++ ...leBlazorBindings.ComponentGenerator.csproj | 26 +++++++++ .../SourceGenerator.cs | 58 +++++++++++++++++++ .../UsingStatement.cs | 2 +- 13 files changed, 214 insertions(+), 44 deletions(-) delete mode 100644 src/ComponentWrapperGenerator/GeneratorSettings.cs rename src/{ComponentWrapperGenerator => Microsoft.MobileBlazorBindings.ComponentGenerator}/ComponentWrapperGenerator.cs (94%) create mode 100644 src/Microsoft.MobileBlazorBindings.ComponentGenerator/DiagnosticDescriptors.cs rename src/{ComponentWrapperGenerator => Microsoft.MobileBlazorBindings.ComponentGenerator}/Extensions/ITypeOrNamespaceSymbolExtensions.cs (88%) create mode 100644 src/Microsoft.MobileBlazorBindings.ComponentGenerator/GeneratorSettings.cs create mode 100644 src/Microsoft.MobileBlazorBindings.ComponentGenerator/GeneratorSettingsParser.cs create mode 100644 src/Microsoft.MobileBlazorBindings.ComponentGenerator/Microsoft.MobileBlazorBindings.ComponentGenerator.csproj create mode 100644 src/Microsoft.MobileBlazorBindings.ComponentGenerator/SourceGenerator.cs rename src/{ComponentWrapperGenerator => Microsoft.MobileBlazorBindings.ComponentGenerator}/UsingStatement.cs (88%) diff --git a/CI/CI.Windows.proj b/CI/CI.Windows.proj index 66e9b8e1..e5381b99 100644 --- a/CI/CI.Windows.proj +++ b/CI/CI.Windows.proj @@ -35,6 +35,7 @@ + diff --git a/MobileBlazorBindings.sln b/MobileBlazorBindings.sln index d8004a0e..f85b82dc 100644 --- a/MobileBlazorBindings.sln +++ b/MobileBlazorBindings.sln @@ -188,6 +188,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsBlazorSample", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MobileBlazorBindings.WindowsForms", "src\Microsoft.MobileBlazorBindings.WindowsForms\Microsoft.MobileBlazorBindings.WindowsForms.csproj", "{1D22DA82-38EB-410C-88EA-37D3D786CCC5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.MobileBlazorBindings.ComponentGenerator", "src\Microsoft.MobileBlazorBindings.ComponentGenerator\Microsoft.MobileBlazorBindings.ComponentGenerator.csproj", "{514BC0B0-9097-47CF-9CEA-CEB431B828D9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1067,6 +1069,18 @@ Global {1D22DA82-38EB-410C-88EA-37D3D786CCC5}.Release|iPhone.Build.0 = Release|Any CPU {1D22DA82-38EB-410C-88EA-37D3D786CCC5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {1D22DA82-38EB-410C-88EA-37D3D786CCC5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Debug|iPhone.Build.0 = Debug|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|Any CPU.Build.0 = Release|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|iPhone.ActiveCfg = Release|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|iPhone.Build.0 = Release|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1154,6 +1168,7 @@ Global {965EEC08-D454-44D3-8E53-5DC12CB8801C} = {50192AAF-25B6-447A-99AA-BA8161DBB594} {DA6F7E8F-9A53-42BB-B29E-169C07EA1FD0} = {965EEC08-D454-44D3-8E53-5DC12CB8801C} {1D22DA82-38EB-410C-88EA-37D3D786CCC5} = {175AB6E2-5FB5-4C15-94C2-DCA2EE6B0703} + {514BC0B0-9097-47CF-9CEA-CEB431B828D9} = {175AB6E2-5FB5-4C15-94C2-DCA2EE6B0703} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A645A7FF-3F09-414D-A391-7E50C3597F05} diff --git a/src/ComponentWrapperGenerator/ComponentWrapperGenerator.csproj b/src/ComponentWrapperGenerator/ComponentWrapperGenerator.csproj index 116c0691..4c98866a 100644 --- a/src/ComponentWrapperGenerator/ComponentWrapperGenerator.csproj +++ b/src/ComponentWrapperGenerator/ComponentWrapperGenerator.csproj @@ -31,6 +31,10 @@ + + + + diff --git a/src/ComponentWrapperGenerator/GeneratorSettings.cs b/src/ComponentWrapperGenerator/GeneratorSettings.cs deleted file mode 100644 index 7d72505c..00000000 --- a/src/ComponentWrapperGenerator/GeneratorSettings.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -namespace ComponentWrapperGenerator -{ - public class GeneratorSettings - { - public string FileHeader { get; set; } - public string RootNamespace { get; set; } - } -} diff --git a/src/ComponentWrapperGenerator/Program.cs b/src/ComponentWrapperGenerator/Program.cs index 9a2a933c..0a036d4a 100644 --- a/src/ComponentWrapperGenerator/Program.cs +++ b/src/ComponentWrapperGenerator/Program.cs @@ -9,6 +9,8 @@ namespace ComponentWrapperGenerator { + using Microsoft.MobileBlazorBindings.ComponentGenerator; + internal class Program { internal static async Task Main(string[] args) @@ -38,7 +40,7 @@ internal static async Task Main(string[] args) }; var compilation = await GetRoslynCompilationAsync().ConfigureAwait(false); - var generator = new ComponentWrapperGenerator(settings, compilation); + var generator = new ComponentWrapperGenerator(settings); foreach (var typeNameToGenerate in listOfTypeNamesToGenerate) { @@ -61,13 +63,31 @@ internal static async Task Main(string[] args) Console.WriteLine(); continue; } - generator.GenerateComponentWrapper(typeToGenerate, outputFolder); + + var generatedSources = generator.GenerateComponentWrapper(typeToGenerate); + WriteFiles(generatedSources, outputFolder); + Console.WriteLine(); } return 0; } + private static void WriteFiles((string HintName, string Source)[] generatedSources, string outputFolder) + { + foreach(var (hintName, source) in generatedSources) + { + var fileName = $"{hintName}.generated.cs"; + var path = hintName.EndsWith("Handler", StringComparison.Ordinal) + ? Path.Combine(outputFolder, "Handlers") + : outputFolder; + + Directory.CreateDirectory(path); + + File.WriteAllText(Path.Combine(path, fileName), source); + } + } + private static async Task GetRoslynCompilationAsync() { var metadataReferences = new[] { diff --git a/src/ComponentWrapperGenerator/ComponentWrapperGenerator.cs b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/ComponentWrapperGenerator.cs similarity index 94% rename from src/ComponentWrapperGenerator/ComponentWrapperGenerator.cs rename to src/Microsoft.MobileBlazorBindings.ComponentGenerator/ComponentWrapperGenerator.cs index af50c7f2..77530b86 100644 --- a/src/ComponentWrapperGenerator/ComponentWrapperGenerator.cs +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/ComponentWrapperGenerator.cs @@ -1,22 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using ComponentWrapperGenerator.Extensions; using Microsoft.CodeAnalysis; +using Microsoft.MobileBlazorBindings.ComponentGenerator.Extensions; using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.IO; using System.Linq; using System.Text; using System.Xml; -namespace ComponentWrapperGenerator +namespace Microsoft.MobileBlazorBindings.ComponentGenerator { -#pragma warning disable CA1724 // Type name conflicts with namespace name public class ComponentWrapperGenerator -#pragma warning restore CA1724 // Type name conflicts with namespace name { private GeneratorSettings Settings { get; } public ComponentWrapperGenerator(GeneratorSettings settings) @@ -24,26 +21,23 @@ public ComponentWrapperGenerator(GeneratorSettings settings) Settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - public void GenerateComponentWrapper(INamedTypeSymbol typeToGenerate, string outputFolder) + public (string HintName, string Source)[] GenerateComponentWrapper(INamedTypeSymbol typeToGenerate) { typeToGenerate = typeToGenerate ?? throw new ArgumentNullException(nameof(typeToGenerate)); var propertiesToGenerate = GetPropertiesToGenerate(typeToGenerate); - GenerateComponentFile(typeToGenerate, propertiesToGenerate, outputFolder); - GenerateHandlerFile(typeToGenerate, propertiesToGenerate, outputFolder); + var componentSource = GenerateComponentFile(typeToGenerate, propertiesToGenerate); + var handlerSource = GenerateHandlerFile(typeToGenerate, propertiesToGenerate); + + return new[] { componentSource, handlerSource }; } - private void GenerateComponentFile(INamedTypeSymbol typeToGenerate, IEnumerable propertiesToGenerate, string outputFolder) + private (string HintName, string Source) GenerateComponentFile(INamedTypeSymbol typeToGenerate, IEnumerable propertiesToGenerate) { - var fileName = Path.Combine(outputFolder, $"{typeToGenerate.Name}.generated.cs"); - var directoryName = Path.GetDirectoryName(fileName); - if (!string.IsNullOrEmpty(directoryName)) - { - Directory.CreateDirectory(directoryName); - } + var hintName = typeToGenerate.Name; - Console.WriteLine($"Generating component for type '{typeToGenerate.MetadataName}' into file '{fileName}'."); + Console.WriteLine($"Generating component for type '{typeToGenerate.MetadataName}' into file '{hintName}'."); var componentName = typeToGenerate.Name; var componentHandlerName = $"{componentName}Handler"; @@ -138,7 +132,7 @@ protected override void RenderAttributes(AttributesBuilder builder) }} "); - File.WriteAllText(fileName, outputBuilder.ToString()); + return (hintName, outputBuilder.ToString()); } private static string GetNamespacePrefix(ITypeSymbol type, List usings) @@ -220,7 +214,7 @@ private static string GetPropertyDeclaration(IPropertySymbol prop, IList propertiesToGenerate, string outputFolder) + private (string HintName, string Source) GenerateHandlerFile(INamedTypeSymbol typeToGenerate, IEnumerable propertiesToGenerate) { - var fileName = Path.Combine(outputFolder, "Handlers", $"{typeToGenerate.Name}Handler.generated.cs"); - var directoryName = Path.GetDirectoryName(fileName); - if (!string.IsNullOrEmpty(directoryName)) - { - Directory.CreateDirectory(directoryName); - } + var hintName = $"{typeToGenerate.Name}Handler"; - Console.WriteLine($"Generating component handler for type '{typeToGenerate.Name}' into file '{fileName}'."); + Console.WriteLine($"Generating component handler for type '{typeToGenerate.Name}' into file '{hintName}'."); var componentName = typeToGenerate.Name; var componentVarName = char.ToLowerInvariant(componentName[0]) + componentName.Substring(1); @@ -517,7 +506,7 @@ namespace {Settings.RootNamespace}.Handlers }} "); - File.WriteAllText(fileName, outputBuilder.ToString()); + return (hintName, outputBuilder.ToString()); } private static string GetPropertySetAttribute(IPropertySymbol prop, List usings) diff --git a/src/Microsoft.MobileBlazorBindings.ComponentGenerator/DiagnosticDescriptors.cs b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/DiagnosticDescriptors.cs new file mode 100644 index 00000000..03ec566a --- /dev/null +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/DiagnosticDescriptors.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; + +namespace Microsoft.MobileBlazorBindings.ComponentGenerator +{ + internal static class DiagnosticDescriptors + { + public static readonly DiagnosticDescriptor TypeNotFound = new DiagnosticDescriptor( +#pragma warning disable RS2008 // Enable analyzer release tracking + id: "MBB0001", +#pragma warning restore RS2008 // Enable analyzer release tracking + title: "Type not found", + messageFormat: "Type {0} is not found in the project or referenced assemblies.", + category: "ComponentGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + } +} diff --git a/src/ComponentWrapperGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs similarity index 88% rename from src/ComponentWrapperGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs rename to src/Microsoft.MobileBlazorBindings.ComponentGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs index 308801be..9c58a746 100644 --- a/src/ComponentWrapperGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/Extensions/ITypeOrNamespaceSymbolExtensions.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; -namespace ComponentWrapperGenerator.Extensions +namespace Microsoft.MobileBlazorBindings.ComponentGenerator.Extensions { internal static class ITypeOrNamespaceSymbolExtensions { @@ -27,11 +27,11 @@ internal static string GetFullName(this INamespaceOrTypeSymbol namespaceOrType) currentNamespace = currentNamespace.ContainingNamespace; } - return string.Join('.', stack); + return string.Join(".", stack); } /// - /// Returns name with generic type arguments (is any). + /// Returns name with generic type arguments (if any). /// private static string GetName(INamespaceOrTypeSymbol namespaceOrType) { diff --git a/src/Microsoft.MobileBlazorBindings.ComponentGenerator/GeneratorSettings.cs b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/GeneratorSettings.cs new file mode 100644 index 00000000..d05808f9 --- /dev/null +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/GeneratorSettings.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.MobileBlazorBindings.ComponentGenerator +{ + public class GeneratorSettings + { + public string FileHeader { get; set; } + public string RootNamespace { get; set; } +#pragma warning disable CA1819 // Properties should not return arrays + public string[] TypesToGenerate { get; set; } +#pragma warning restore CA1819 // Properties should not return arrays + } +} diff --git a/src/Microsoft.MobileBlazorBindings.ComponentGenerator/GeneratorSettingsParser.cs b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/GeneratorSettingsParser.cs new file mode 100644 index 00000000..c8d6ecd3 --- /dev/null +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/GeneratorSettingsParser.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.MobileBlazorBindings.ComponentGenerator +{ + internal static class GeneratorSettingsParser + { + internal static GeneratorSettings Parse(string json) + { + // Adding external nuget dependencies to source generators is a bit troublesome, + // therefore this class parses simple json using regexes (instead of Newtonsoft.Json or System.Text.Json). + + return new GeneratorSettings + { + FileHeader = GetStringValue(json, "fileHeader"), + RootNamespace = GetStringValue(json, "rootNamespace"), + TypesToGenerate = GetStringArrayValues(json, "typesToGenerate") + }; + } + + private static string GetStringValue(string json, string key) + { + var regex = new Regex($@"""{key}""\s*:\s*""(?[^""]*)""", RegexOptions.IgnoreCase); + var match = regex.Match(json); + + return match?.Groups["value"]?.Value; + } + + private static string[] GetStringArrayValues(string json, string key) + { + var regex = new Regex($@"""{key}""\s*:\s*\[(\s*""(?[^""]*)""\s*,?\s*)*\]", RegexOptions.IgnoreCase); + var match = regex.Match(json); + + return match?.Groups["value"]?.Captures.Cast().Select(c => c.Value).ToArray(); + } + } +} diff --git a/src/Microsoft.MobileBlazorBindings.ComponentGenerator/Microsoft.MobileBlazorBindings.ComponentGenerator.csproj b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/Microsoft.MobileBlazorBindings.ComponentGenerator.csproj new file mode 100644 index 00000000..41626d73 --- /dev/null +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/Microsoft.MobileBlazorBindings.ComponentGenerator.csproj @@ -0,0 +1,26 @@ + + + + Component source generator for MobileBlazorBindings + Source generator which allows to generate Blazor component wrappers for thirs party Xamarin.Forms components. + blazor;mobileblazorbindings + netstandard2.0 + false + + + + + + + + + + + + + + Microsoft400 + + + + diff --git a/src/Microsoft.MobileBlazorBindings.ComponentGenerator/SourceGenerator.cs b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/SourceGenerator.cs new file mode 100644 index 00000000..21ed7e60 --- /dev/null +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/SourceGenerator.cs @@ -0,0 +1,58 @@ +using Microsoft.CodeAnalysis; +using System; +using System.IO; +using System.Linq; + +namespace Microsoft.MobileBlazorBindings.ComponentGenerator +{ + [Generator] + public class SourceGenerator : ISourceGenerator + { + private const string SettingsFileName = "component-generator.json"; + + public void Execute(GeneratorExecutionContext context) + { + var settingsFile = context.AdditionalFiles + .FirstOrDefault(f => string.Equals(Path.GetFileName(f.Path), SettingsFileName, StringComparison.OrdinalIgnoreCase)); + + if (settingsFile is null) + { + return; + } + + var settings = GeneratorSettingsParser.Parse(settingsFile.GetText().ToString()); + + if (settings.TypesToGenerate is null) + { + return; + } + var componentGenerator = new ComponentWrapperGenerator(settings); + + foreach (var typeName in settings.TypesToGenerate) + { + var type = context.Compilation.GetTypeByMetadataName(typeName); + + if (type == null) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.TypeNotFound, + Location.Create(settingsFile.Path, default, default), + typeName); + context.ReportDiagnostic(diagnostic); + } + else + { + var generatedFiles = componentGenerator.GenerateComponentWrapper(type); + + foreach (var (hintName, source) in generatedFiles) + { + context.AddSource(hintName, source); + } + } + } + } + + public void Initialize(GeneratorInitializationContext context) + { + } + } +} diff --git a/src/ComponentWrapperGenerator/UsingStatement.cs b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/UsingStatement.cs similarity index 88% rename from src/ComponentWrapperGenerator/UsingStatement.cs rename to src/Microsoft.MobileBlazorBindings.ComponentGenerator/UsingStatement.cs index cc521eda..24aeb243 100644 --- a/src/ComponentWrapperGenerator/UsingStatement.cs +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/UsingStatement.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -namespace ComponentWrapperGenerator +namespace Microsoft.MobileBlazorBindings.ComponentGenerator { internal sealed class UsingStatement { From 6eaf4a6630c594ec2084a11712100c5a08af65cf Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Thu, 11 Feb 2021 10:53:47 +0200 Subject: [PATCH 3/4] Update generator to better support third party components --- .../ComponentWrapperGenerator.cs | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.MobileBlazorBindings.ComponentGenerator/ComponentWrapperGenerator.cs b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/ComponentWrapperGenerator.cs index 77530b86..23c6cc85 100644 --- a/src/Microsoft.MobileBlazorBindings.ComponentGenerator/ComponentWrapperGenerator.cs +++ b/src/Microsoft.MobileBlazorBindings.ComponentGenerator/ComponentWrapperGenerator.cs @@ -46,17 +46,29 @@ public ComponentWrapperGenerator(GeneratorSettings settings) // header var headerText = Settings.FileHeader; + var typeNamespace = typeToGenerate.ContainingNamespace.GetFullName(); + var typeNamespaceAlias = GetNamespaceAlias(typeToGenerate.ContainingNamespace); + // usings var usings = new List { new UsingStatement { Namespace = "Microsoft.AspNetCore.Components", IsUsed = true, }, new UsingStatement { Namespace = "Microsoft.MobileBlazorBindings.Core", IsUsed = true, }, - new UsingStatement { Namespace = "Microsoft.MobileBlazorBindings.Elements.Handlers", IsUsed = true, }, + new UsingStatement { Namespace = $"{Settings.RootNamespace}.Handlers", IsUsed = true, }, new UsingStatement { Namespace = "System.Threading.Tasks", IsUsed = true, }, - new UsingStatement { Namespace = "Xamarin.Forms", Alias = "XF" }, - new UsingStatement { Namespace = "Xamarin.Forms.DualScreen", Alias = "XFD" }, + new UsingStatement { Namespace = "Xamarin.Forms", Alias = "XF" } }; + if (typeNamespace != "Xamarin.Forms") + { + usings.Add(new UsingStatement { Namespace = typeNamespace, Alias = typeNamespaceAlias }); + } + + if (Settings.RootNamespace != "Microsoft.MobileBlazorBindings.Elements") + { + usings.Add(new UsingStatement { Namespace = "Microsoft.MobileBlazorBindings.Elements", IsUsed = true }); + } + var componentNamespacePrefix = GetNamespacePrefix(typeToGenerate, usings); // props @@ -345,7 +357,7 @@ private static string FormatTypeName(ITypeSymbol type, IList usi private static string GetPropertyRenderAttribute(IPropertySymbol prop) { var propValue = prop.Type.IsValueType ? $"{GetIdentifierName(prop.Name)}.Value" : GetIdentifierName(prop.Name); - var formattedValue = propValue; + string formattedValue; if (TypeToAttributeHelperGetter.TryGetValue(prop.Type.GetFullName(), out var formattingFunc)) { @@ -353,12 +365,11 @@ private static string GetPropertyRenderAttribute(IPropertySymbol prop) } else if (prop.Type.TypeKind == TypeKind.Enum) { - formattedValue = $"(int){formattedValue}"; + formattedValue = $"(int){propValue}"; } else { - // TODO: Error? - Console.WriteLine($"WARNING: Couldn't generate attribute render for {prop.ContainingType.Name}.{prop.Name}"); + formattedValue = $"AttributeHelper.ObjectToDelegate({propValue})"; } return $@" if ({GetIdentifierName(prop.Name)} != null) @@ -425,20 +436,34 @@ private static INamedTypeSymbol GetBaseTypeOfInterest(INamedTypeSymbol type) var componentHandlerName = $"{componentName}Handler"; var componentBaseName = GetBaseTypeOfInterest(typeToGenerate).Name; var componentHandlerBaseName = $"{componentBaseName}Handler"; + var componentHandlerNamespace = $"{Settings.RootNamespace}.Handlers"; // header var headerText = Settings.FileHeader; + var typeNamespace = typeToGenerate.ContainingNamespace.GetFullName(); + var typeNamespaceAlias = GetNamespaceAlias(typeToGenerate.ContainingNamespace); + // usings var usings = new List { //new UsingStatement { Namespace = "Microsoft.AspNetCore.Components", IsUsed = true, }, // Typically needed only when there are event handlers for the EventArgs types new UsingStatement { Namespace = "Microsoft.MobileBlazorBindings.Core", IsUsed = true, }, + new UsingStatement { Namespace = "Microsoft.MobileBlazorBindings.Elements", IsUsed = true, }, new UsingStatement { Namespace = "System", IsUsed = true, }, new UsingStatement { Namespace = "Xamarin.Forms", Alias = "XF" }, - new UsingStatement { Namespace = "Xamarin.Forms.DualScreen", Alias = "XFD" }, }; + if(typeNamespace != "Xamarin.Forms") + { + usings.Add(new UsingStatement { Namespace = typeNamespace, Alias = typeNamespaceAlias }); + } + + if (componentHandlerNamespace != "Microsoft.MobileBlazorBindings.Elements.Handlers") + { + usings.Add(new UsingStatement { Namespace = "Microsoft.MobileBlazorBindings.Elements.Handlers", IsUsed = true }); + } + var componentNamespacePrefix = GetNamespacePrefix(typeToGenerate, usings); // props @@ -487,7 +512,7 @@ public override void ApplyAttribute(ulong attributeEventHandlerId, string attrib outputBuilder.Append($@"{headerText} {usingsText} -namespace {Settings.RootNamespace}.Handlers +namespace {componentHandlerNamespace} {{ public {classModifiers}partial class {componentHandlerName} : {componentHandlerBaseName} {{ @@ -545,8 +570,7 @@ private static string GetPropertySetAttribute(IPropertySymbol prop, List(attributeValue)"; } return $@" case nameof({GetNamespacePrefix(prop.ContainingType, usings)}{prop.ContainingType.Name}.{GetIdentifierName(prop.Name)}): @@ -637,6 +661,18 @@ private static string GetIdentifierName(string possibleIdentifier) : possibleIdentifier; } + private static string GetNamespaceAlias(INamespaceSymbol namespaceSymbol) + { + var alias = ""; + while (!namespaceSymbol.IsGlobalNamespace) + { + alias = namespaceSymbol.Name[0] + alias; + namespaceSymbol = namespaceSymbol.ContainingNamespace; + } + + return alias; + } + private static readonly List ReservedKeywords = new List { "class", }; } From d046573411129477007d78a1d9ea43e007c0f655 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Thu, 11 Feb 2021 01:23:43 +0200 Subject: [PATCH 4/4] Add generator to Weather sample --- MobileBlazorBindings.sln | 17 ++++++++++++- ...indings.PancakeView.SourceGenerator.csproj | 24 +++++++++++++++++++ .../component-generator.json | 6 +++++ .../MobileBlazorBindingsWeather.csproj | 3 ++- 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 samples/MobileBlazorBindingsWeather/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator.csproj create mode 100644 samples/MobileBlazorBindingsWeather/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator/component-generator.json diff --git a/MobileBlazorBindings.sln b/MobileBlazorBindings.sln index f85b82dc..cca172fc 100644 --- a/MobileBlazorBindings.sln +++ b/MobileBlazorBindings.sln @@ -188,7 +188,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinFormsBlazorSample", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MobileBlazorBindings.WindowsForms", "src\Microsoft.MobileBlazorBindings.WindowsForms\Microsoft.MobileBlazorBindings.WindowsForms.csproj", "{1D22DA82-38EB-410C-88EA-37D3D786CCC5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.MobileBlazorBindings.ComponentGenerator", "src\Microsoft.MobileBlazorBindings.ComponentGenerator\Microsoft.MobileBlazorBindings.ComponentGenerator.csproj", "{514BC0B0-9097-47CF-9CEA-CEB431B828D9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.MobileBlazorBindings.ComponentGenerator", "src\Microsoft.MobileBlazorBindings.ComponentGenerator\Microsoft.MobileBlazorBindings.ComponentGenerator.csproj", "{514BC0B0-9097-47CF-9CEA-CEB431B828D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator", "samples\MobileBlazorBindingsWeather\Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator\Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator.csproj", "{EEA60BDC-FCAB-4154-B64B-F5BA15337A30}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1081,6 +1083,18 @@ Global {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|iPhone.Build.0 = Release|Any CPU {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {514BC0B0-9097-47CF-9CEA-CEB431B828D9}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Debug|iPhone.Build.0 = Debug|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Release|Any CPU.Build.0 = Release|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Release|iPhone.ActiveCfg = Release|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Release|iPhone.Build.0 = Release|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1169,6 +1183,7 @@ Global {DA6F7E8F-9A53-42BB-B29E-169C07EA1FD0} = {965EEC08-D454-44D3-8E53-5DC12CB8801C} {1D22DA82-38EB-410C-88EA-37D3D786CCC5} = {175AB6E2-5FB5-4C15-94C2-DCA2EE6B0703} {514BC0B0-9097-47CF-9CEA-CEB431B828D9} = {175AB6E2-5FB5-4C15-94C2-DCA2EE6B0703} + {EEA60BDC-FCAB-4154-B64B-F5BA15337A30} = {9B1F38B1-0661-4883-AD4D-ADA9ECA6941B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A645A7FF-3F09-414D-A391-7E50C3597F05} diff --git a/samples/MobileBlazorBindingsWeather/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator.csproj b/samples/MobileBlazorBindingsWeather/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator.csproj new file mode 100644 index 00000000..504e45cb --- /dev/null +++ b/samples/MobileBlazorBindingsWeather/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + + + + + diff --git a/samples/MobileBlazorBindingsWeather/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator/component-generator.json b/samples/MobileBlazorBindingsWeather/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator/component-generator.json new file mode 100644 index 00000000..6d4bfde3 --- /dev/null +++ b/samples/MobileBlazorBindingsWeather/Microsoft.MobileBlazorBindings.PancakeView.SourceGenerator/component-generator.json @@ -0,0 +1,6 @@ +{ + "rootNamespace": "Microsoft.MobileBlazorBindings.PancakeView.Elements", + "typesToGenerate": [ + "Xamarin.Forms.PancakeView.PancakeView" + ] +} diff --git a/samples/MobileBlazorBindingsWeather/MobileBlazorBindingsWeather/MobileBlazorBindingsWeather.csproj b/samples/MobileBlazorBindingsWeather/MobileBlazorBindingsWeather/MobileBlazorBindingsWeather.csproj index c809145e..a18fafdb 100644 --- a/samples/MobileBlazorBindingsWeather/MobileBlazorBindingsWeather/MobileBlazorBindingsWeather.csproj +++ b/samples/MobileBlazorBindingsWeather/MobileBlazorBindingsWeather/MobileBlazorBindingsWeather.csproj @@ -11,6 +11,7 @@ - + + \ No newline at end of file