CPF/CpfRazorSample/ComponentWrapperGenerator/CpfComponentWrapperGenerator.cs
2024-01-09 00:39:58 +08:00

637 lines
30 KiB
C#

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<PropertyInfo> 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<T>";
componentBaseName = "";
}
// header
var headerText = Settings.FileHeader;
// usings
var usings = new List<UsingStatement>
{
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<System.ComponentModel.DescriptionAttribute>();
if (d != null)
{
des = d.Description;
}
var outputBuilder = new StringBuilder();
outputBuilder.Append($@"{headerText}
{usingsText}
namespace {Settings.RootNamespace}
{{
/// <summary>
/// {des}
/// </summary>
public {classModifiers}partial class {componentName} {componentBaseName}
{{
{propertyDeclarations}
}}
}}
");
File.WriteAllText(fileName, outputBuilder.ToString());
}
//private static readonly List<Type> DisallowedComponentPropertyTypes = new List<Type>
//{
// 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<UsingStatement> usings)
{
var propertyType = prop.PropertyType;
string propertyTypeName;
if (propertyType == typeof(IList<string>) || 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<System.ComponentModel.DescriptionAttribute>();
if (d != null)
{
des = $" /// <summary>\r\n /// {d.Description}\r\n /// <summary>\r\n";
}
return $@"{des} [Parameter] public {propertyTypeName} {GetIdentifierName(prop.Name)} {{ get; set; }}
";
}
private static string GetEventDeclaration(EventInfo prop, IList<UsingStatement> 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<System.ComponentModel.DescriptionAttribute>();
if (d != null)
{
des = $" /// <summary>\r\n /// {d.Description}\r\n /// <summary>\r\n";
}
return $@"{des} [Parameter] public {propertyTypeName} {GetIdentifierName(prop.Name)} {{ get; set; }}
";
}
private static string GetTypeNameAndAddNamespace(Type type, IList<UsingStatement> 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<UsingStatement> 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<Type, Func<string, string>> TypeToAttributeHelperGetter = new Dictionary<Type, Func<string, string>>
//{
// { 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<string>), 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<Type, string> TypeToCSharpName = new Dictionary<Type, string>
{
{ 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;
}
///// <summary>
///// 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.
///// </summary>
///// <param name="type"></param>
///// <returns></returns>
//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<PropertyInfo> 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<UsingStatement>
// {
// //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<UsingStatement> 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<UsingStatement> 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<Type, string> TypeToAttributeHelperSetter = new Dictionary<Type, string>
//{
// { 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<string>), "AttributeHelper.GetStringList(attributeValue)" },
//};
static HashSet<string> DisallowedPropertyName = new HashSet<string>
{
"Site"
};
private static IEnumerable<PropertyInfo> 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<string> ReservedKeywords = new List<string>
{ "class", };
}
}