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; namespace ComponentWrapperGenerator { // TODO: XML Doc Comments #pragma warning disable CA1724 // Type name conflicts with namespace name public class CpfComponentWrapperGenerator #pragma warning restore CA1724 // Type name conflicts with namespace name { public CpfComponentWrapperGenerator(GeneratorSettings settings) { Settings = settings ?? throw new ArgumentNullException(nameof(settings)); } private GeneratorSettings Settings { get; } public void GenerateComponentWrapper(Type typeToGenerate, string outputFolder) { typeToGenerate = typeToGenerate ?? throw new ArgumentNullException(nameof(typeToGenerate)); var propertiesToGenerate = GetPropertiesToGenerate(typeToGenerate); GenerateComponentFile(typeToGenerate, propertiesToGenerate, outputFolder); //GenerateHandlerFile(typeToGenerate, propertiesToGenerate, outputFolder); } private void GenerateComponentFile(Type typeToGenerate, IEnumerable propertiesToGenerate, string outputFolder) { var fileName = Path.Combine(outputFolder, $"{typeToGenerate.Name}.generated.cs"); var directoryName = Path.GetDirectoryName(fileName); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } Console.WriteLine($"Generating component for type '{typeToGenerate.FullName}' into file '{fileName}'."); var componentName = typeToGenerate.Name; //var componentHandlerName = $"{componentName}Handler"; //var componentBaseName = GetBaseTypeOfInterest(typeToGenerate).Name; var componentBaseName = $": Element<{typeToGenerate.FullName}>"; if (typeToGenerate.IsSubclassOf(typeof(CPF.Controls.ContentControl))) { componentBaseName += " ,IHandleChildContentText"; } if (componentName == "UIElement") { componentName = "Element"; componentBaseName = ""; } // header var headerText = Settings.FileHeader; // usings var usings = new List { new UsingStatement { Namespace = "Microsoft.AspNetCore.Components" }, new UsingStatement { Namespace = "CPF" }, new UsingStatement { Namespace = "CPF.Input" }, new UsingStatement { Namespace = "CPF.Shapes" }, new UsingStatement { Namespace = "CPF.Razor" }, new UsingStatement { Namespace = "CPF.Drawing" }, new UsingStatement { Namespace = "CPF.Controls" }, new UsingStatement { Namespace = "CPF.Razor.Controls" } }; // props var propertyDeclarationBuilder = new StringBuilder(); if (propertiesToGenerate.Any()) { propertyDeclarationBuilder.AppendLine(); } foreach (var prop in propertiesToGenerate) { propertyDeclarationBuilder.Append(GetPropertyDeclaration(prop, usings)); } var events = typeToGenerate.GetEvents(BindingFlags.Public | BindingFlags.Instance); foreach (var prop in events) { if (typeToGenerate == typeof(CPF.UIElement) || (typeToGenerate != typeof(CPF.UIElement) && prop.DeclaringType != typeof(CPF.UIElement) && prop.DeclaringType != typeof(CPF.Visual) && prop.DeclaringType != typeof(CPF.CpfObject))) { propertyDeclarationBuilder.Append(GetEventDeclaration(prop, usings)); } } var propertyDeclarations = propertyDeclarationBuilder.ToString(); //var propertyAttributeBuilder = new StringBuilder(); //foreach (var prop in propertiesToGenerate) //{ // propertyAttributeBuilder.Append(GetPropertyRenderAttribute(prop)); //} //var propertyAttributes = propertyAttributeBuilder.ToString(); //var eventHandlerAttributes = ""; var usingsText = string.Join( Environment.NewLine, usings .Distinct() .Where(u => u.Namespace != Settings.RootNamespace) .OrderBy(u => u.ComparableString) .Select(u => u.UsingText)); var isComponentAbstract = typeToGenerate.IsAbstract; var classModifiers = string.Empty; if (isComponentAbstract) { classModifiers += "abstract "; } var componentHasPublicParameterlessConstructor = typeToGenerate .GetConstructors() .Any(ctor => ctor.IsPublic && !ctor.GetParameters().Any()); var des = ""; var d = typeToGenerate.GetCustomAttribute(); if (d != null) { des = d.Description; } var outputBuilder = new StringBuilder(); outputBuilder.Append($@"{headerText} {usingsText} namespace {Settings.RootNamespace} {{ /// /// {des} /// public {classModifiers}partial class {componentName} {componentBaseName} {{ {propertyDeclarations} }} }} "); File.WriteAllText(fileName, outputBuilder.ToString()); } //private static readonly List DisallowedComponentPropertyTypes = new List //{ // 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(ICommand), // typeof(XF.Keyboard), // TODO: This is temporary; should be possible to add support later // 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), //}; private static string GetPropertyDeclaration(PropertyInfo prop, IList usings) { var propertyType = prop.PropertyType; string propertyTypeName; if (propertyType == typeof(IList) || propertyType == typeof(CPF.ViewFill) || propertyType == typeof(CPF.Drawing.Color) || propertyType == typeof(CPF.Drawing.Brush)) { // Lists of strings are special-cased because they are handled specially by the handlers as a comma-separated list propertyTypeName = "string"; } else { propertyTypeName = GetTypeNameAndAddNamespace(propertyType, usings); if (propertyType.IsValueType && (!propertyType.IsGenericType || propertyType.GetGenericTypeDefinition() == typeof(Nullable))) { propertyTypeName += "?"; } } var des = ""; var d = prop.GetCustomAttribute(); if (d != null) { des = $" /// \r\n /// {d.Description}\r\n /// \r\n"; } return $@"{des} [Parameter] public {propertyTypeName} {GetIdentifierName(prop.Name)} {{ get; set; }} "; } private static string GetEventDeclaration(EventInfo prop, IList usings) { var propertyType = prop.EventHandlerType; string propertyTypeName; if (propertyType == typeof(EventHandler)) { // Lists of strings are special-cased because they are handled specially by the handlers as a comma-separated list propertyTypeName = "EventCallback"; } else { //propertyTypeName = GetTypeNameAndAddNamespace(propertyType, usings); //if (propertyType.IsValueType) //{ // propertyTypeName += "?"; //} propertyTypeName = $"EventCallback<{propertyType.GetGenericArguments()[0]}>"; } var des = ""; var d = prop.GetCustomAttribute(); if (d != null) { des = $" /// \r\n /// {d.Description}\r\n /// \r\n"; } return $@"{des} [Parameter] public {propertyTypeName} {GetIdentifierName(prop.Name)} {{ get; set; }} "; } private static string GetTypeNameAndAddNamespace(Type type, IList usings) { var typeName = GetCSharpType(type); if (typeName != null) { return typeName; } // 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); if (existingUsing == null) { usings.Add(new UsingStatement { Namespace = type.Namespace }); } else { if (existingUsing.Alias != null) { namespaceAlias = existingUsing.Alias + "."; } } typeName = namespaceAlias + FormatTypeName(type, usings); return typeName; } private static string FormatTypeName(Type type, IList usings) { if (!type.IsGenericType) { return type.Name; } var typeNameBuilder = new StringBuilder(); typeNameBuilder.Append(type.Name.Substring(0, type.Name.IndexOf('`', StringComparison.Ordinal))); typeNameBuilder.Append("<"); var genericArgs = type.GetGenericArguments(); for (int i = 0; i < genericArgs.Length; i++) { if (i > 0) { typeNameBuilder.Append(", "); } typeNameBuilder.Append(GetTypeNameAndAddNamespace(genericArgs[i], usings)); } typeNameBuilder.Append(">"); return typeNameBuilder.ToString(); } //private static readonly Dictionary> TypeToAttributeHelperGetter = new Dictionary> //{ // { typeof(XF.Color), propValue => $"AttributeHelper.ColorToString({propValue})" }, // { typeof(XF.CornerRadius), propValue => $"AttributeHelper.CornerRadiusToString({propValue})" }, // { typeof(XF.ImageSource), propValue => $"AttributeHelper.ImageSourceToString({propValue})" }, // { typeof(XF.LayoutOptions), propValue => $"AttributeHelper.LayoutOptionsToString({propValue})" }, // { typeof(XF.Thickness), propValue => $"AttributeHelper.ThicknessToString({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}" }, //}; // private static string GetPropertyRenderAttribute(PropertyInfo prop) // { // var propValue = prop.PropertyType.IsValueType ? $"{GetIdentifierName(prop.Name)}.Value" : GetIdentifierName(prop.Name); // var formattedValue = propValue; // if (TypeToAttributeHelperGetter.TryGetValue(prop.PropertyType, out var formattingFunc)) // { // formattedValue = formattingFunc(propValue); // } // else if (prop.PropertyType.IsEnum) // { // formattedValue = $"(int){formattedValue}"; // } // else // { // // TODO: Error? // Console.WriteLine($"WARNING: Couldn't generate attribute render for {prop.DeclaringType.Name}.{prop.Name}"); // } // return $@" if ({GetIdentifierName(prop.Name)} != null) // {{ // builder.AddAttribute(nameof({GetIdentifierName(prop.Name)}), {formattedValue}); // }} //"; // } 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" }, }; private static string GetCSharpType(Type propertyType) { return TypeToCSharpName.TryGetValue(propertyType, out var typeName) ? typeName : null; } ///// ///// Finds the next non-generic base type of the specified type. This matches the Mobile Blazor Bindings ///// model where there is no need to represent the intermediate generic base classes because they are ///// generally only containers and have no API functionality that needs to be generated. ///// ///// ///// //private static Type GetBaseTypeOfInterest(Type type) //{ // do // { // type = type.BaseType; // if (!type.IsGenericType) // { // return type; // } // } // while (type != null); // return null; //} // private void GenerateHandlerFile(Type typeToGenerate, IEnumerable propertiesToGenerate, string outputFolder) // { // var fileName = Path.Combine(outputFolder, "Handlers", $"{typeToGenerate.Name}Handler.generated.cs"); // var directoryName = Path.GetDirectoryName(fileName); // if (!string.IsNullOrEmpty(directoryName)) // { // Directory.CreateDirectory(directoryName); // } // Console.WriteLine($"Generating component handler for type '{typeToGenerate.FullName}' into file '{fileName}'."); // var componentName = typeToGenerate.Name; // var componentVarName = char.ToLowerInvariant(componentName[0]) + componentName.Substring(1); // var componentHandlerName = $"{componentName}Handler"; // var componentBaseName = GetBaseTypeOfInterest(typeToGenerate).Name; // var componentHandlerBaseName = $"{componentBaseName}Handler"; // // header // var headerText = Settings.FileHeader; // // usings // var usings = new List // { // //new UsingStatement { Namespace = "Microsoft.AspNetCore.Components" }, // Typically needed only when there are event handlers for the EventArgs types // new UsingStatement { Namespace = "Microsoft.MobileBlazorBindings.Core" }, // new UsingStatement { Namespace = "System" }, // new UsingStatement { Namespace = "Xamarin.Forms", Alias = "XF" } // }; // //// props // //var propertySettersBuilder = new StringBuilder(); // //foreach (var prop in propertiesToGenerate) // //{ // // propertySettersBuilder.Append(GetPropertySetAttribute(prop, usings)); // //} // //var propertySetters = propertySettersBuilder.ToString(); // var usingsText = string.Join( // Environment.NewLine, // usings // .Distinct() // .Where(u => u.Namespace != Settings.RootNamespace) // .OrderBy(u => u.ComparableString) // .Select(u => u.UsingText)); // var isComponentAbstract = typeToGenerate.IsAbstract; // var classModifiers = string.Empty; // if (isComponentAbstract) // { // classModifiers += "abstract "; // } // var applyAttributesMethod = string.Empty; // // if (!string.IsNullOrEmpty(propertySetters)) // // { // // applyAttributesMethod = $@" // // public override void ApplyAttribute(ulong attributeEventHandlerId, string attributeName, object attributeValue, string attributeEventUpdatesAttributeName) // // {{ // // switch (attributeName) // // {{ // //{propertySetters} default: // // base.ApplyAttribute(attributeEventHandlerId, attributeName, attributeValue, attributeEventUpdatesAttributeName); // // break; // // }} // // }} // //"; // // } // var outputBuilder = new StringBuilder(); // outputBuilder.Append($@"{headerText} //{usingsText} //namespace {Settings.RootNamespace}.Handlers //{{ // public {classModifiers}partial class {componentHandlerName} : {componentHandlerBaseName} // {{ // public {componentName}Handler(NativeComponentRenderer renderer, XF.{componentName} {componentVarName}Control) : base(renderer, {componentVarName}Control) // {{ // {componentName}Control = {componentVarName}Control ?? throw new ArgumentNullException(nameof({componentVarName}Control)); // Initialize(renderer); // }} // partial void Initialize(NativeComponentRenderer renderer); // public XF.{componentName} {componentName}Control {{ get; }} //{applyAttributesMethod} }} //}} //"); // File.WriteAllText(fileName, outputBuilder.ToString()); // } // private static string GetPropertySetAttribute(PropertyInfo prop, List usings) // { // // Handle null values by resetting to default value // var resetValueParameterExpression = string.Empty; // var bindablePropertyForProp = GetBindablePropertyForProp(prop); // if (bindablePropertyForProp != null) // { // var declaredDefaultValue = bindablePropertyForProp.DefaultValue; // var defaultValueForType = GetDefaultValueForType(prop.PropertyType); // var needsCustomResetValue = declaredDefaultValue == null ? false : !declaredDefaultValue.Equals(defaultValueForType); // if (needsCustomResetValue) // { // var valueExpression = GetValueExpression(declaredDefaultValue, usings); // if (string.IsNullOrEmpty(valueExpression)) // { // Console.WriteLine($"WARNING: Couldn't get value expression for {prop.DeclaringType.Name}.{prop.Name} of type {prop.PropertyType.FullName}."); // } // resetValueParameterExpression = valueExpression; // } // } // var formattedValue = string.Empty; // if (TypeToAttributeHelperSetter.TryGetValue(prop.PropertyType, out var propValueFormat)) // { // var resetValueParameterExpressionAsExtraParameter = string.Empty; // if (!string.IsNullOrEmpty(resetValueParameterExpression)) // { // resetValueParameterExpressionAsExtraParameter = ", " + resetValueParameterExpression; // } // formattedValue = string.Format(CultureInfo.InvariantCulture, propValueFormat, resetValueParameterExpressionAsExtraParameter); // } // else if (prop.PropertyType.IsEnum) // { // var resetValueParameterExpressionAsExtraParameter = string.Empty; // if (!string.IsNullOrEmpty(resetValueParameterExpression)) // { // resetValueParameterExpressionAsExtraParameter = ", (int)" + resetValueParameterExpression; // } // var castTypeName = GetTypeNameAndAddNamespace(prop.PropertyType, usings); // formattedValue = $"({castTypeName})AttributeHelper.GetInt(attributeValue{resetValueParameterExpressionAsExtraParameter})"; // } // else if (prop.PropertyType == typeof(string)) // { // formattedValue = // string.IsNullOrEmpty(resetValueParameterExpression) // ? "(string)attributeValue" // : string.Format(CultureInfo.InvariantCulture, "(string)attributeValue ?? {0}", resetValueParameterExpression); // } // else // { // // TODO: Error? // Console.WriteLine($"WARNING: Couldn't generate property set for {prop.DeclaringType.Name}.{prop.Name}"); // } // return $@" case nameof(XF.{prop.DeclaringType.Name}.{GetIdentifierName(prop.Name)}): // {prop.DeclaringType.Name}Control.{GetIdentifierName(prop.Name)} = {formattedValue}; // break; //"; // } //private static string GetValueExpression(object declaredDefaultValue, List usings) //{ // if (declaredDefaultValue is null) // { // throw new ArgumentNullException(nameof(declaredDefaultValue)); // } // return declaredDefaultValue switch // { // bool boolValue => boolValue ? "true" : "false", // int intValue => GetIntValueExpression(intValue), // float floatValue => floatValue.ToString("F", CultureInfo.InvariantCulture) + "f", // "Fixed-Point": https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#the-fixed-point-f-format-specifier // double doubleValue => doubleValue.ToString("F", CultureInfo.InvariantCulture), // "Fixed-Point": https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#the-fixed-point-f-format-specifier // Enum enumValue => GetTypeNameAndAddNamespace(enumValue.GetType(), usings) + "." + Enum.GetName(enumValue.GetType(), declaredDefaultValue), // XF.LayoutOptions layoutOptionsValue => GetLayoutOptionsValueExpression(layoutOptionsValue), // string stringValue => $@"""{stringValue}""", // // TODO: More types here // _ => null, // }; //} //private static string GetLayoutOptionsValueExpression(XF.LayoutOptions layoutOptionsValue) //{ // var expandSuffix = layoutOptionsValue.Expands ? "AndExpand" : string.Empty; // return $"XF.LayoutOptions.{layoutOptionsValue.Alignment}{expandSuffix}"; //} //private static string GetIntValueExpression(int intValue) //{ // return intValue switch // { // int.MinValue => "int.MinValue", // int.MaxValue => "int.MaxValue", // _ => intValue.ToString(CultureInfo.InvariantCulture), // }; //} //private static object GetDefaultValueForType(Type propertyType) //{ // if (propertyType.IsValueType) // { // return Activator.CreateInstance(propertyType); // } // return null; //} //private static XF.BindableProperty GetBindablePropertyForProp(PropertyInfo prop) //{ // var bindablePropertyField = prop.DeclaringType.GetField(prop.Name + "Property"); // if (bindablePropertyField == null) // { // return null; // } // return (XF.BindableProperty)bindablePropertyField.GetValue(null); //} //private static readonly Dictionary TypeToAttributeHelperSetter = new Dictionary //{ // { typeof(XF.Color), "AttributeHelper.StringToColor((string)attributeValue{0})" }, // { typeof(XF.CornerRadius), "AttributeHelper.StringToCornerRadius(attributeValue{0})" }, // { typeof(XF.ImageSource), "AttributeHelper.StringToImageSource(attributeValue{0})" }, // { typeof(XF.LayoutOptions), "AttributeHelper.StringToLayoutOptions(attributeValue{0})" }, // { typeof(XF.Thickness), "AttributeHelper.StringToThickness(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)" }, //}; static HashSet DisallowedPropertyName = new HashSet { "Site" }; private static IEnumerable GetPropertiesToGenerate(Type componentType) { var allPublicProperties = componentType.GetProperties(); return allPublicProperties .Where(HasPublicGetAndSet) .Where(prop => componentType == typeof(CPF.UIElement) || (componentType != typeof(CPF.UIElement) && prop.DeclaringType != typeof(CPF.UIElement) && prop.DeclaringType != typeof(CPF.Visual) && prop.DeclaringType != typeof(CPF.CpfObject))) //.Where(prop => !DisallowedComponentPropertyTypes.Contains(prop.PropertyType)) .Where(prop => !DisallowedPropertyName.Contains(prop.Name)) .Where(IsPropertyBrowsable) .OrderBy(prop => prop.Name, StringComparer.OrdinalIgnoreCase) .ToList(); } private static bool HasPublicGetAndSet(PropertyInfo propInfo) { if (propInfo.PropertyType == typeof(CPF.UIElementTemplate) || propInfo.PropertyType.IsGenericType && propInfo.PropertyType.GetGenericTypeDefinition() == typeof(CPF.UIElementTemplate<>)) { return false; } return propInfo.GetGetMethod() != null && propInfo.GetSetMethod() != null && propInfo.GetCustomAttribute(typeof(CPF.NotCpfProperty)) == null; } private static bool IsPropertyBrowsable(PropertyInfo propInfo) { // [EditorBrowsable(EditorBrowsableState.Never)] var attr = (EditorBrowsableAttribute)Attribute.GetCustomAttribute(propInfo, typeof(EditorBrowsableAttribute)); return (attr == null) || (attr.State != EditorBrowsableState.Never); } private static string GetIdentifierName(string possibleIdentifier) { return ReservedKeywords.Contains(possibleIdentifier, StringComparer.Ordinal) ? $"@{possibleIdentifier}" : possibleIdentifier; } private static readonly List ReservedKeywords = new List { "class", }; } }