From 0061a80f4c80451ad104eed14e6636065962c319 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Thu, 20 Jan 2022 18:23:46 -0600 Subject: [PATCH 01/26] Enable including C# and XAML source files as bundled content --- Common/Labs.UnoLib.props | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Common/Labs.UnoLib.props b/Common/Labs.UnoLib.props index 41df5c3d6..bbd230afd 100644 --- a/Common/Labs.UnoLib.props +++ b/Common/Labs.UnoLib.props @@ -12,6 +12,11 @@ true + + + + + From c20248ab9f90d0f8ac9a72d79dd2cdf3b56c0d81 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Fri, 21 Jan 2022 17:19:52 -0600 Subject: [PATCH 02/26] Added ToolkitSampleRenderer. Implemented ToolkitSampleOptionsPane. Added demo code to sample. --- .../Attributes/ToolkitSampleAttribute.cs | 67 +++--- .../ToolkitSampleOptionsPaneAttribute.cs | 24 +++ .../ToolkitSampleCategory.cs | 17 +- .../ToolkitSampleMetadata.cs | 64 ++---- .../ToolkitSampleMetadataGenerator.cs | 197 ++++++++++-------- .../ToolkitSampleSubcategory.cs | 32 +-- .../CommunityToolkit.Labs.Shared.projitems | 7 + .../MainPage.xaml | 2 +- .../MainPage.xaml.cs | 20 +- .../ToolkitSampleRenderer.xaml | 46 ++++ .../ToolkitSampleRenderer.xaml.cs | 153 ++++++++++++++ .../CanvasLayout.Sample.csproj | 8 + .../{ => SampleOne}/SamplePage.xaml | 6 +- .../{ => SampleOne}/SamplePage.xaml.cs | 4 +- .../SampleOne/SamplePageOptions.xaml | 36 ++++ .../SampleOne/SamplePageOptions.xaml.cs | 96 +++++++++ .../{ => SampleTwo}/SamplePage2.xaml | 2 +- .../{ => SampleTwo}/SamplePage2.xaml.cs | 4 +- 18 files changed, 566 insertions(+), 219 deletions(-) create mode 100644 Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleOptionsPaneAttribute.cs create mode 100644 Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml create mode 100644 Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml.cs rename Labs/CanvasLayout/samples/CanvasLayout.Sample/{ => SampleOne}/SamplePage.xaml (62%) rename Labs/CanvasLayout/samples/CanvasLayout.Sample/{ => SampleOne}/SamplePage.xaml.cs (81%) create mode 100644 Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml create mode 100644 Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs rename Labs/CanvasLayout/samples/CanvasLayout.Sample/{ => SampleTwo}/SamplePage2.xaml (90%) rename Labs/CanvasLayout/samples/CanvasLayout.Sample/{ => SampleTwo}/SamplePage2.xaml.cs (78%) diff --git a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs b/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs index 97f37a1c8..fa9746aaf 100644 --- a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs @@ -1,42 +1,47 @@ using System; -namespace CommunityToolkit.Labs.Core.Attributes +namespace CommunityToolkit.Labs.Core.Attributes; + +/// +/// Registers a control as a toolkit sample using the provided data. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public sealed class ToolkitSampleAttribute : Attribute { /// - /// When used on a class that derives from Page, that page is registered as a toolkit sample using the provided data. + /// Creates a new instance of . /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public sealed class ToolkitSampleAttribute : Attribute + public ToolkitSampleAttribute(string id, string displayName, ToolkitSampleCategory category, ToolkitSampleSubcategory subcategory, string description) { - /// - /// Creates a new instance of . - /// - public ToolkitSampleAttribute(string displayName, ToolkitSampleCategory category, ToolkitSampleSubcategory subcategory, string description) - { - Category = category; - Subcategory = subcategory; - DisplayName = displayName; - Description = description; - } + Id = id; + DisplayName = displayName; + Description = description; + Category = category; + Subcategory = subcategory; + } - /// - /// The display name for this sample page. - /// - public string DisplayName { get; } + /// + /// A unique identifier for this sample, used by the sample system. + /// + public string Id { get; } + + /// + /// The display name for this sample page. + /// + public string DisplayName { get; } - /// - /// The category that this sample belongs to. - /// - public ToolkitSampleCategory Category { get; } + /// + /// The category that this sample belongs to. + /// + public ToolkitSampleCategory Category { get; } - /// - /// A more specific category within the provided . - /// - public ToolkitSampleSubcategory Subcategory { get; } + /// + /// A more specific category within the provided . + /// + public ToolkitSampleSubcategory Subcategory { get; } - /// - /// The description for this sample page. - /// - public string Description { get; } - } + /// + /// The description for this sample page. + /// + public string Description { get; } } diff --git a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleOptionsPaneAttribute.cs b/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleOptionsPaneAttribute.cs new file mode 100644 index 000000000..f4477017b --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleOptionsPaneAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace CommunityToolkit.Labs.Core.Attributes; + +/// +/// Registers a control as the options panel for a toolkit sample. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class ToolkitSampleOptionsPaneAttribute : Attribute +{ + /// + /// Creates a new instance of . + /// + /// The unique identifier of a toolkit sample, provided via . + public ToolkitSampleOptionsPaneAttribute(string sampleId) + { + SampleId = sampleId; + } + + /// + /// The unique identifier of a toolkit sample, provided via . + /// + public string SampleId { get; } +} diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs b/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs index 622da5ea7..4f82dfbdd 100644 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs +++ b/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace CommunityToolkit.Labs.Core +namespace CommunityToolkit.Labs.Core; + +/// +/// The various categories each sample is organized into. +/// +public enum ToolkitSampleCategory : byte { /// - /// The various categories each sample is organized into. + /// Various UI controls that the user sees and interacts with. /// - public enum ToolkitSampleCategory : byte - { - /// - /// Various UI controls that the user sees and interacts with. - /// - Controls, - } + Controls, } diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs b/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs index 12d7a1a86..f61329d6c 100644 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs +++ b/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs @@ -4,48 +4,24 @@ using System; -namespace CommunityToolkit.Labs.Core -{ - /// - /// Contains the metadata needed to identify and display a toolkit sample. - /// - public sealed record ToolkitSampleMetadata - { - /// - /// Creates a new instance of . - /// - public ToolkitSampleMetadata(ToolkitSampleCategory category, ToolkitSampleSubcategory subcategory, string displayName, string description, Type sampleControlType) - { - DisplayName = displayName; - Description = description; - SampleControlType = sampleControlType; - Category = category; - Subcategory = subcategory; - } +namespace CommunityToolkit.Labs.Core; - /// - /// The category that this sample belongs to. - /// - public ToolkitSampleCategory Category { get; } - - /// - /// A more specific category within the provided . - /// - public ToolkitSampleSubcategory Subcategory { get; } - - /// - /// The display name for this sample page. - /// - public string DisplayName { get; } - - /// - /// The description for this sample page. - /// - public string Description { get; } - - /// - /// A type that can be used to construct an instance of the sample control. - /// - public Type SampleControlType { get; } - } -} +/// +/// Contains the metadata needed to identify and display a toolkit sample. +/// +/// The category that this sample belongs to. +/// A more specific category within the provided . +/// The display name for this sample page. +/// The description for this sample page. +/// A type that can be used to construct an instance of the sample control. +/// +/// The control type for the sample page's options pane. +/// Constructor should have exactly one parameter that can be assigned to the control type (). +/// +public sealed record ToolkitSampleMetadata( + ToolkitSampleCategory Category, + ToolkitSampleSubcategory Subcategory, + string DisplayName, + string Description, + Type SampleControlType, + Type? SampleOptionsPaneType = null); diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs index 968501026..82255f2c9 100644 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs @@ -8,48 +8,64 @@ using CommunityToolkit.Labs.Core.Attributes; using Microsoft.CodeAnalysis; -namespace CommunityToolkit.Labs.Core +namespace CommunityToolkit.Labs.Core; + +/// +/// Crawls all referenced projects for s and generates a static method that returns metadata for each one found. +/// +[Generator] +public partial class ToolkitSampleMetadataGenerator : ISourceGenerator { - /// - /// Crawls all referenced projects for s and generates a static method that returns metadata for each one found. - /// - [Generator] - public partial class ToolkitSampleMetadataGenerator : ISourceGenerator + /// + public void Initialize(GeneratorInitializationContext context) { - /// - public void Initialize(GeneratorInitializationContext context) - { - // not needed - } - - /// - public void Execute(GeneratorExecutionContext context) - { - // Find all types in all assemblies. - var assemblies = context.Compilation.SourceModule.ReferencedAssemblySymbols; - - var types = assemblies.SelectMany(asm => CrawlForAllNamedTypes(asm.GlobalNamespace)) - .Where(x => x is not null && x.TypeKind == TypeKind.Class && x.CanBeReferencedByName) // remove null and invalid values. - .Cast(); // strip nullability from type. - - if (types is null) - return; - - // Get all attributes + the original type symbol. - var allAttributeData = types.SelectMany(type => type.GetAttributes(), (Type, Attribute) => (Type, Attribute)); - var toolkitSampleAttributeData = allAttributeData.Where(x => IsToolkitSampleAttribute(x.Attribute)); - - // Reconstruct sample metadata from attributes - var sampleMetadata = toolkitSampleAttributeData.Select(x => ReconstructSampleMetadata(x.Type, x.Attribute)); + // not needed + } - // Build source string - var source = BuildRegistrationCallsFromMetadata(sampleMetadata); - context.AddSource($"ToolkitSampleRegistry.g.cs", source); - } + /// + public void Execute(GeneratorExecutionContext context) + { + // Find all types in all assemblies. + var assemblies = context.Compilation.SourceModule.ReferencedAssemblySymbols; + + var types = assemblies.SelectMany(asm => CrawlForAllNamedTypes(asm.GlobalNamespace)) + .Where(x => x is not null && x.TypeKind == TypeKind.Class && x.CanBeReferencedByName) // remove null and invalid values. + .Cast(); // strip nullability from type. + + if (types is null) + return; + + // Get all attributes + the original type symbol. + var allAttributeData = types.SelectMany(type => type.GetAttributes(), (Type, Attribute) => (Type, Attribute)); + + // Find and reconstruct relevant attributes. + var toolkitSampleAttributeData = allAttributeData + .Where(x => IsToolkitSampleAttribute(x.Attribute)) + .Select(x => (Attribute: ReconstructAttribute(x.Attribute), AttachedQualifiedTypeName: x.Type.ToString())); + + var optionsPaneAttributes = allAttributeData + .Where(x => IsToolkitSampleOptionsPaneAttribute(x.Attribute)) + .Select(x => (Attribute: ReconstructAttribute(x.Attribute), AttachedQualifiedTypeName: x.Type.ToString())); + + // Reconstruct sample metadata from attributes + var sampleMetadata = toolkitSampleAttributeData.Select(sample => + new ToolkitSampleRecord( + sample.Attribute.Category, + sample.Attribute.Subcategory, + sample.Attribute.DisplayName, + sample.Attribute.Description, + sample.AttachedQualifiedTypeName, + optionsPaneAttributes.FirstOrDefault(opt => opt.Attribute.SampleId == sample.Attribute.Id).AttachedQualifiedTypeName) + ); + + // Build source string + var source = BuildRegistrationCallsFromMetadata(sampleMetadata); + context.AddSource($"ToolkitSampleRegistry.g.cs", source); + } - static private string BuildRegistrationCallsFromMetadata(IEnumerable sampleMetadata) - { - return $@"// + static private string BuildRegistrationCallsFromMetadata(IEnumerable sampleMetadata) + { + return $@"// namespace CommunityToolkit.Labs.Core; internal static class ToolkitSampleRegistry @@ -57,74 +73,75 @@ internal static class ToolkitSampleRegistry public static System.Collections.Generic.IEnumerable<{nameof(ToolkitSampleMetadata)}> Execute() {{ { - string.Join("\n ", sampleMetadata.Select(MetadataToRegistryCall).ToArray()) - } + string.Join("\n ", sampleMetadata.Select(MetadataToRegistryCall).ToArray()) + } }} }}"; - static string MetadataToRegistryCall(ToolkitSampleRecord metadata) - { - return @$"yield return new {nameof(ToolkitSampleMetadata)}({nameof(ToolkitSampleCategory)}.{metadata.Category}, {nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}, ""{metadata.DisplayName}"", ""{metadata.Description}"", typeof({metadata.AssemblyQualifiedName}));"; - } + static string MetadataToRegistryCall(ToolkitSampleRecord metadata) + { + var sampleOptionsParam = metadata.SampleOptionsAssemblyQualifiedName is null ? "null" : $"typeof({metadata.SampleOptionsAssemblyQualifiedName})"; + + return @$"yield return new {nameof(ToolkitSampleMetadata)}({nameof(ToolkitSampleCategory)}.{metadata.Category}, {nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}, ""{metadata.DisplayName}"", ""{metadata.Description}"", typeof({metadata.SampleAssemblyQualifiedName}), {sampleOptionsParam});"; } + } - private static IEnumerable CrawlForAllNamedTypes(INamespaceSymbol namespaceSymbol) + private static IEnumerable CrawlForAllNamedTypes(INamespaceSymbol namespaceSymbol) + { + foreach (var member in namespaceSymbol.GetMembers()) { - foreach (var member in namespaceSymbol.GetMembers()) + if (member is INamespaceSymbol nestedNamespace) { - if (member is INamespaceSymbol nestedNamespace) - { - foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) - yield return item; - } - - if (member is INamedTypeSymbol typeSymbol) - yield return typeSymbol; + foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) + yield return item; } - } - - private static bool IsToolkitSampleAttribute(AttributeData attr) - => attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(ToolkitSampleAttribute).FullName}"; - - private static ToolkitSampleRecord ReconstructSampleMetadata(INamedTypeSymbol typeSymbol, AttributeData attributeData) - { - // Fully reconstructing the attribute as it was received - // gives us safety against changes to the attribute constructor signature. - var args = attributeData.ConstructorArguments.Select(PrepareTypeForActivator).ToArray(); - var reconstructedAttribute = (ToolkitSampleAttribute)Activator.CreateInstance(typeof(ToolkitSampleAttribute), args); + if (member is INamedTypeSymbol typeSymbol) + yield return typeSymbol; + } + } - var attachedTypeFullyQualifiedName = typeSymbol.ToString(); + private static bool IsToolkitSampleAttribute(AttributeData attr) + => attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(ToolkitSampleAttribute).FullName}"; - return new ToolkitSampleRecord(reconstructedAttribute.Category, - reconstructedAttribute.Subcategory, - reconstructedAttribute.DisplayName, - reconstructedAttribute.Description, - attachedTypeFullyQualifiedName); - } + private static bool IsToolkitSampleOptionsPaneAttribute(AttributeData attr) + => attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(ToolkitSampleOptionsPaneAttribute).FullName}"; - private static object? PrepareTypeForActivator(TypedConstant typedConstant) - { - if (typedConstant.Type is null) - throw new ArgumentNullException(nameof(typedConstant.Type)); + private static T ReconstructAttribute(AttributeData attributeData) + { + // Fully reconstructing the attribute as it was received + // gives us safety against changes to the attribute constructor signature. + var attributeArgs = attributeData.ConstructorArguments.Select(PrepareTypeForActivator).ToArray(); + return (T)Activator.CreateInstance(typeof(T), attributeArgs); + } - // Types prefixed with global:: do not work with Type.GetType and must be stripped away. - var assemblyQualifiedName = typedConstant.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + private static object? PrepareTypeForActivator(TypedConstant typedConstant) + { + if (typedConstant.Type is null) + throw new ArgumentNullException(nameof(typedConstant.Type)); - var argType = Type.GetType(assemblyQualifiedName); + // Types prefixed with global:: do not work with Type.GetType and must be stripped away. + var assemblyQualifiedName = typedConstant.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); - // Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance() - if (argType != null && typedConstant.Kind == TypedConstantKind.Enum) - return Enum.Parse(argType, typedConstant.Value?.ToString()); + var argType = Type.GetType(assemblyQualifiedName); - return typedConstant.Value; - } + // Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance() + if (argType != null && typedConstant.Kind == TypedConstantKind.Enum) + return Enum.Parse(argType, typedConstant.Value?.ToString()); - /// - /// A new record must be used instead of using directly - /// because we cannot Type.GetType using the , - /// but we can safely generate a type reference in the final output using typeof(AssemblyQualifiedName). - /// - private sealed record ToolkitSampleRecord(ToolkitSampleCategory Category, ToolkitSampleSubcategory Subcategory, string DisplayName, string Description, string AssemblyQualifiedName); + return typedConstant.Value; } + + /// + /// A new record must be used instead of using directly + /// because we cannot Type.GetType using the , + /// but we can safely generate a type reference in the final output using typeof(AssemblyQualifiedName). + /// + private sealed record ToolkitSampleRecord( + ToolkitSampleCategory Category, + ToolkitSampleSubcategory Subcategory, + string DisplayName, + string Description, + string SampleAssemblyQualifiedName, + string? SampleOptionsAssemblyQualifiedName); } diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs b/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs index af9afd8e2..887cd8780 100644 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs +++ b/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs @@ -2,24 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace CommunityToolkit.Labs.Core +namespace CommunityToolkit.Labs.Core; + +/// +/// The various subcategories used by samples. +/// +/// +/// Subcategories can be used by samples across multiple categories. +/// +public enum ToolkitSampleSubcategory : byte { /// - /// All the different subcategories used by samples. + /// No subcategory specified. /// - - // Subcategory is a flat enum so we can use a subcategory in multiple categories, - // and so we can freely move samples or whole sections in the future. - public enum ToolkitSampleSubcategory : byte - { - /// - /// No subcategory specified. - /// - None, + None, - /// - /// A sample that focuses on control layout. - /// - Layout, - } + /// + /// A sample that focuses on control layout. + /// + Layout, } + diff --git a/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems b/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems index c98a657af..3b69c1b74 100644 --- a/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems +++ b/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems @@ -24,6 +24,9 @@ MainPage.xaml + + ToolkitSampleRenderer.xaml + @@ -34,6 +37,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + diff --git a/Common/CommunityToolkit.Labs.Shared/MainPage.xaml b/Common/CommunityToolkit.Labs.Shared/MainPage.xaml index 42d9c6ec8..769452080 100644 --- a/Common/CommunityToolkit.Labs.Shared/MainPage.xaml +++ b/Common/CommunityToolkit.Labs.Shared/MainPage.xaml @@ -10,7 +10,7 @@ - + diff --git a/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs b/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs index 1272752c2..86aee94cf 100644 --- a/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs +++ b/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs @@ -30,26 +30,11 @@ public MainPage() this.InitializeComponent(); } - /// - /// Gets the backing dependency property for . - /// - public static readonly DependencyProperty MainContentProperty = - DependencyProperty.Register(nameof(MainContent), typeof(object), typeof(MainPage), new PropertyMetadata(null)); - /// /// Gets the items used for navigating. /// public ObservableCollection NavigationViewItems { get; } = new ObservableCollection(); - /// - /// Gets or sets the primary content displayed to the user. - /// - public object MainContent - { - get => (object)GetValue(MainContentProperty); - set => SetValue(MainContentProperty, value); - } - protected override void OnNavigatedTo(NavigationEventArgs e) { var samplePages = e.Parameter as IEnumerable; @@ -73,10 +58,7 @@ private void OnSelectionChanged(NavigationView sender, NavigationViewSelectionCh if (selectedMetadata is null) return; - // TODO: Switch to Frame / Frame.Navigate when grouped-sample page is added. - var controlInstance = Activator.CreateInstance(selectedMetadata.SampleControlType); - - MainContent = controlInstance; + NavFrame.Navigate(typeof(ToolkitSampleRenderer), selectedMetadata); } private IEnumerable GenerateSampleNavItemTree(IEnumerable sampleMetadata) diff --git a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml new file mode 100644 index 000000000..e70f6824a --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + 14 + + + + + + + + + + + + + + + + diff --git a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml.cs b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml.cs new file mode 100644 index 000000000..de5b40ea6 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml.cs @@ -0,0 +1,153 @@ +using CommunityToolkit.Labs.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Storage; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +namespace CommunityToolkit.Labs.Shared +{ + /// + /// Handles the display of a single toolkit sample, its source code, and the options that control it. + /// + public sealed partial class ToolkitSampleRenderer : Page + { + /// + /// Creates a new instance of . + /// + public ToolkitSampleRenderer() + { + this.InitializeComponent(); + } + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty MetadataProperty = + DependencyProperty.Register(nameof(Metadata), typeof(ToolkitSampleMetadata), typeof(ToolkitSampleRenderer), new PropertyMetadata(null)); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty SampleControlInstanceProperty = + DependencyProperty.Register(nameof(SampleControlInstance), typeof(UIElement), typeof(ToolkitSampleRenderer), new PropertyMetadata(null)); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty SampleOptionsPaneInstanceProperty = + DependencyProperty.Register(nameof(SampleOptionsPaneInstance), typeof(UIElement), typeof(ToolkitSampleRenderer), new PropertyMetadata(null)); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty XamlCodeProperty = + DependencyProperty.Register(nameof(XamlCode), typeof(string), typeof(ToolkitSampleRenderer), new PropertyMetadata(null)); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty CSharpCodeProperty = + DependencyProperty.Register(nameof(CSharpCode), typeof(string), typeof(ToolkitSampleRenderer), new PropertyMetadata(null)); + + public ToolkitSampleMetadata? Metadata + { + get { return (ToolkitSampleMetadata?)GetValue(MetadataProperty); } + set { SetValue(MetadataProperty, value); } + } + + /// + /// The sample control instance being displayed. + /// + public UIElement? SampleControlInstance + { + get => (UIElement?)GetValue(SampleControlInstanceProperty); + set => SetValue(SampleControlInstanceProperty, value); + } + + + /// + /// The options pane for the sample being displayed. + /// + public UIElement? SampleOptionsPaneInstance + { + get => (UIElement?)GetValue(SampleOptionsPaneInstanceProperty); + set => SetValue(SampleOptionsPaneInstanceProperty, value); + } + + /// + /// The XAML code being rendered. + /// + public string? XamlCode + { + get => (string?)GetValue(XamlCodeProperty); + set => SetValue(XamlCodeProperty, value); + } + + /// + /// The backing C# for the being rendered. + /// + public string? CSharpCode + { + get => (string?)GetValue(CSharpCodeProperty); + set => SetValue(CSharpCodeProperty, value); + } + + protected override async void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + Metadata = (ToolkitSampleMetadata)e.Parameter; + + XamlCode = await GetMetadataFileContents(Metadata, "xaml"); + CSharpCode = await GetMetadataFileContents(Metadata, "xaml.cs"); + + SampleControlInstance = (UIElement)Activator.CreateInstance(Metadata.SampleControlType); + + if (Metadata.SampleOptionsPaneType is not null) + SampleOptionsPaneInstance = (UIElement)Activator.CreateInstance(Metadata.SampleOptionsPaneType, SampleControlInstance); + } + + public static async Task GetMetadataFileContents(ToolkitSampleMetadata metadata, string fileExtension) + { + var filePath = GetPathToFileWithoutExtension(metadata.SampleControlType); + + try + { + var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"{filePath}.{fileExtension.Trim('.')}")); + var textContents = await FileIO.ReadTextAsync(file); + + return textContents; + } + catch (Exception ex) + { + return null; + } + } + + /// + /// Compute path to a code file bundled in the app using type information. + /// Assumes file path in project matches namespace. + /// + public static string GetPathToFileWithoutExtension(Type type) + { + var simpleAssemblyName = type.Assembly.GetName().Name; + var typeNamespace = type.Namespace; + + var folderPath = typeNamespace.Replace(simpleAssemblyName, "").Trim('.').Replace('.', '/'); + + return $"ms-appx:///{simpleAssemblyName}/{folderPath}/{type.Name}"; + } + } +} diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj b/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj index 7c9853a49..411ac6d41 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj @@ -10,13 +10,21 @@ + + + + + + + + diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml similarity index 62% rename from Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml rename to Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml index 89fc7ce05..3d49437ec 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml @@ -1,15 +1,13 @@ - + diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs similarity index 81% rename from Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml.cs rename to Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs index 68ef2e14b..44bef2fd7 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml.cs +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs @@ -21,12 +21,12 @@ // The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 -namespace CanvasLayout.Sample +namespace CanvasLayout.Sample.SampleOne { /// /// An empty page that can be used on its own or navigated to within a Frame. /// - [ToolkitSample("Canvas Layout", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "A canvas-like VirtualizingPanel for use in an ItemsControl")] + [ToolkitSample(id: nameof(SamplePage), "Canvas Layout", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "A canvas-like VirtualizingPanel for use in an ItemsControl")] public sealed partial class SamplePage : Page { public SamplePage() diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml new file mode 100644 index 000000000..17b7b4d69 --- /dev/null +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + White + Green + Yellow + + + + + + + + + + + + diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs new file mode 100644 index 000000000..69ad63a44 --- /dev/null +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.Core; +using CommunityToolkit.Labs.Core.Attributes; +using Microsoft.UI.Xaml.Controls; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Markup; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace CanvasLayout.Sample.SampleOne +{ + [ToolkitSampleOptionsPane(sampleId: nameof(SamplePage))] + public sealed partial class SamplePageOptions : UserControl + { + private readonly SamplePage _samplePage; + private SamplePage.XamlNamedPropertyRelay _xamlProperties; + + public SamplePageOptions(SamplePage samplePage) + { + Loaded += SamplePageOptions_Loaded; + + _samplePage = samplePage; + _xamlProperties = new SamplePage.XamlNamedPropertyRelay(_samplePage); + + this.InitializeComponent(); + } + + private void SamplePageOptions_Loaded(object sender, RoutedEventArgs e) + { + Loaded -= SamplePageOptions_Loaded; + + CustomText.Text = _xamlProperties.PrimaryText.Text; + FontSizeSlider.Value = _xamlProperties.PrimaryText.FontSize; + } + + private void TextBox_TextChanged(object sender, TextChangedEventArgs e) + { + _xamlProperties.PrimaryText.Text = ((TextBox)sender).Text; + } + + private void OnRadioButtonSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is RadioButtons radioButtons) + { + if (radioButtons.SelectedItem is null) + return; + + var selectedColor = (string)radioButtons.SelectedItem; + + _xamlProperties.PrimaryText.Foreground = (SolidColorBrush)XamlBindingHelper.ConvertValue(typeof(SolidColorBrush), selectedColor); + } + } + + private void Slider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e) + { + if (_xamlProperties.PrimaryText is not null && IsLoaded && _samplePage.IsLoaded) + _xamlProperties.PrimaryText.FontSize = ((Slider)sender).Value; + } + } + + // TODO: Create a source generator to automate this. + public sealed partial class SamplePage + { + /// + /// Provides the same functionality as using <SomeElement x:FieldProvider="public" x:Name="someName"> + /// on an element in XAML, without the need for the extra x:FieldProvider markup. + /// + public record XamlNamedPropertyRelay + { + private readonly SamplePage _samplePage; + + public XamlNamedPropertyRelay(SamplePage samplePage) + { + _samplePage = samplePage; + } + + public TextBlock PrimaryText => _samplePage.PrimaryText; + } + } +} diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml similarity index 90% rename from Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml rename to Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml index ebe9bba8d..757359877 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml @@ -1,5 +1,5 @@ Date: Fri, 21 Jan 2022 17:31:36 -0600 Subject: [PATCH 03/26] Moved ToolkitSampleMetadataGenerator to new folder/namespace --- .../{ => Generators}/ToolkitSampleMetadataGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Common/CommunityToolkit.Labs.Core/{ => Generators}/ToolkitSampleMetadataGenerator.cs (99%) diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs similarity index 99% rename from Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs rename to Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs index 82255f2c9..43498cdbc 100644 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs @@ -8,7 +8,7 @@ using CommunityToolkit.Labs.Core.Attributes; using Microsoft.CodeAnalysis; -namespace CommunityToolkit.Labs.Core; +namespace CommunityToolkit.Labs.Core.Generators; /// /// Crawls all referenced projects for s and generates a static method that returns metadata for each one found. From b8d880688e8449943d13cc8176f014f908d602f0 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Fri, 21 Jan 2022 17:38:51 -0600 Subject: [PATCH 04/26] Extracted generic source generator helpers as extension methods --- .../Generators/GeneratorExtensions.cs | 60 +++++++++++++++++++ .../ToolkitSampleMetadataGenerator.cs | 46 +------------- 2 files changed, 63 insertions(+), 43 deletions(-) create mode 100644 Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs diff --git a/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs b/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs new file mode 100644 index 000000000..032b3de9d --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CommunityToolkit.Labs.Core.Generators +{ + internal static class GeneratorExtensions + { + internal static IEnumerable CrawlForAllNamedTypes(this INamespaceSymbol namespaceSymbol) + { + foreach (var member in namespaceSymbol.GetMembers()) + { + if (member is INamespaceSymbol nestedNamespace) + { + foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) + yield return item; + } + + if (member is INamedTypeSymbol typeSymbol) + yield return typeSymbol; + } + } + + /// + /// Reconstructs an attribute instance as the given type. + /// + /// The attribute type to create. + /// The attribute data used to construct the instance of + /// + internal static T ReconstructAs(this AttributeData attributeData) + { + // Reconstructing the attribute instance provides some safety against changes to the attribute's constructor signature. + var attributeArgs = attributeData.ConstructorArguments.Select(PrepareTypeForActivator).ToArray(); + return (T)Activator.CreateInstance(typeof(T), attributeArgs); + } + + internal static object? PrepareTypeForActivator(this TypedConstant typedConstant) + { + if (typedConstant.Type is null) + throw new ArgumentNullException(nameof(typedConstant.Type)); + + // Types prefixed with global:: do not work with Type.GetType and must be stripped away. + var assemblyQualifiedName = typedConstant.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + + var argType = Type.GetType(assemblyQualifiedName); + + // Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance() + if (argType != null && typedConstant.Kind == TypedConstantKind.Enum) + return Enum.Parse(argType, typedConstant.Value?.ToString()); + + return typedConstant.Value; + } + } +} diff --git a/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs index 43498cdbc..553708199 100644 --- a/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs @@ -28,7 +28,7 @@ public void Execute(GeneratorExecutionContext context) // Find all types in all assemblies. var assemblies = context.Compilation.SourceModule.ReferencedAssemblySymbols; - var types = assemblies.SelectMany(asm => CrawlForAllNamedTypes(asm.GlobalNamespace)) + var types = assemblies.SelectMany(asm => asm.GlobalNamespace.CrawlForAllNamedTypes()) .Where(x => x is not null && x.TypeKind == TypeKind.Class && x.CanBeReferencedByName) // remove null and invalid values. .Cast(); // strip nullability from type. @@ -41,11 +41,11 @@ public void Execute(GeneratorExecutionContext context) // Find and reconstruct relevant attributes. var toolkitSampleAttributeData = allAttributeData .Where(x => IsToolkitSampleAttribute(x.Attribute)) - .Select(x => (Attribute: ReconstructAttribute(x.Attribute), AttachedQualifiedTypeName: x.Type.ToString())); + .Select(x => (Attribute: x.Attribute.ReconstructAs(), AttachedQualifiedTypeName: x.Type.ToString())); var optionsPaneAttributes = allAttributeData .Where(x => IsToolkitSampleOptionsPaneAttribute(x.Attribute)) - .Select(x => (Attribute: ReconstructAttribute(x.Attribute), AttachedQualifiedTypeName: x.Type.ToString())); + .Select(x => (Attribute: x.Attribute.ReconstructAs(), AttachedQualifiedTypeName: x.Type.ToString())); // Reconstruct sample metadata from attributes var sampleMetadata = toolkitSampleAttributeData.Select(sample => @@ -86,52 +86,12 @@ static string MetadataToRegistryCall(ToolkitSampleRecord metadata) } } - private static IEnumerable CrawlForAllNamedTypes(INamespaceSymbol namespaceSymbol) - { - foreach (var member in namespaceSymbol.GetMembers()) - { - if (member is INamespaceSymbol nestedNamespace) - { - foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) - yield return item; - } - - if (member is INamedTypeSymbol typeSymbol) - yield return typeSymbol; - } - } - private static bool IsToolkitSampleAttribute(AttributeData attr) => attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(ToolkitSampleAttribute).FullName}"; private static bool IsToolkitSampleOptionsPaneAttribute(AttributeData attr) => attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(ToolkitSampleOptionsPaneAttribute).FullName}"; - private static T ReconstructAttribute(AttributeData attributeData) - { - // Fully reconstructing the attribute as it was received - // gives us safety against changes to the attribute constructor signature. - var attributeArgs = attributeData.ConstructorArguments.Select(PrepareTypeForActivator).ToArray(); - return (T)Activator.CreateInstance(typeof(T), attributeArgs); - } - - private static object? PrepareTypeForActivator(TypedConstant typedConstant) - { - if (typedConstant.Type is null) - throw new ArgumentNullException(nameof(typedConstant.Type)); - - // Types prefixed with global:: do not work with Type.GetType and must be stripped away. - var assemblyQualifiedName = typedConstant.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); - - var argType = Type.GetType(assemblyQualifiedName); - - // Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance() - if (argType != null && typedConstant.Kind == TypedConstantKind.Enum) - return Enum.Parse(argType, typedConstant.Value?.ToString()); - - return typedConstant.Value; - } - /// /// A new record must be used instead of using directly /// because we cannot Type.GetType using the , From 28774dd4dfc0da4d7254759513e7808558b15dfb Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 25 Jan 2022 17:16:50 -0600 Subject: [PATCH 05/26] Fixed an issue where output was generated when no attributes were found --- .../Generators/ToolkitSampleMetadataGenerator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs index 553708199..a4e1cd76c 100644 --- a/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs @@ -29,7 +29,7 @@ public void Execute(GeneratorExecutionContext context) var assemblies = context.Compilation.SourceModule.ReferencedAssemblySymbols; var types = assemblies.SelectMany(asm => asm.GlobalNamespace.CrawlForAllNamedTypes()) - .Where(x => x is not null && x.TypeKind == TypeKind.Class && x.CanBeReferencedByName) // remove null and invalid values. + .Where(x => x?.TypeKind == TypeKind.Class && x.CanBeReferencedByName) // remove null and invalid values. .Cast(); // strip nullability from type. if (types is null) @@ -43,6 +43,9 @@ public void Execute(GeneratorExecutionContext context) .Where(x => IsToolkitSampleAttribute(x.Attribute)) .Select(x => (Attribute: x.Attribute.ReconstructAs(), AttachedQualifiedTypeName: x.Type.ToString())); + if (!toolkitSampleAttributeData.Any()) + return; + var optionsPaneAttributes = allAttributeData .Where(x => IsToolkitSampleOptionsPaneAttribute(x.Attribute)) .Select(x => (Attribute: x.Attribute.ReconstructAs(), AttachedQualifiedTypeName: x.Type.ToString())); From 10c5e864485e6a4a91d28137e7ea520e67cad0c9 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 25 Jan 2022 17:17:02 -0600 Subject: [PATCH 06/26] Added CrawlBy helper --- .../Generators/GeneratorExtensions.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs b/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs index 032b3de9d..a96231806 100644 --- a/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs +++ b/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs @@ -27,6 +27,31 @@ internal static IEnumerable CrawlForAllNamedTypes(this INamesp } } + /// + /// Crawls an object tree for nested properties of the same type and returns the first instance that matches the . + /// + /// + /// Does not filter against or return the object. + /// + public static T? CrawlBy(this T? root, Func selectPredicate, Func filterPredicate) + { + crawl: + var current = selectPredicate(root); + + if (filterPredicate(current)) + { + return current; + } + + if (current is null) + { + return default; + } + + root = current; + goto crawl; + } + /// /// Reconstructs an attribute instance as the given type. /// From 90a2049d0a0ba0428b0121077f28f5d32495cd9e Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 25 Jan 2022 17:17:48 -0600 Subject: [PATCH 07/26] Added XamlNamedPropertyRelayGenerator --- .../XamlNamedPropertyRelayGenerator.cs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 Common/CommunityToolkit.Labs.Core/Generators/XamlNamedPropertyRelayGenerator.cs diff --git a/Common/CommunityToolkit.Labs.Core/Generators/XamlNamedPropertyRelayGenerator.cs b/Common/CommunityToolkit.Labs.Core/Generators/XamlNamedPropertyRelayGenerator.cs new file mode 100644 index 000000000..c85fb3d11 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core/Generators/XamlNamedPropertyRelayGenerator.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace CommunityToolkit.Labs.Core.Generators; + +[Generator] +public class XamlNamedPropertyRelayGenerator : IIncrementalGenerator +{ + private readonly HashSet _handledConstructors = new(); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // UWP generates fields for x:Name. + var fieldSymbols = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is FieldDeclarationSyntax, + static (context, _) => ((FieldDeclarationSyntax)context.Node).Declaration.Variables.Select(v => (IFieldSymbol?)context.SemanticModel.GetDeclaredSymbol(v))) + .SelectMany(static (item, _) => item) + .Where(SymbolIsDeclaredXamlName); + + // Uno generates private properties for x:Name. + var propertySymbols = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is MemberDeclarationSyntax, + static (context, _) => context.SemanticModel.GetDeclaredSymbol((MemberDeclarationSyntax)context.Node) as IPropertySymbol) + .Where(SymbolIsDeclaredXamlName); + + context.RegisterSourceOutput(fieldSymbols, (ctx, sym) => GenerateNamedPropertyRelay(ctx, sym, sym?.Type, _handledConstructors)); + context.RegisterSourceOutput(propertySymbols, (ctx, sym) => GenerateNamedPropertyRelay(ctx, sym, sym?.Type, _handledConstructors)); + } + + private static bool SymbolIsDeclaredXamlName(T? symbol) + where T : ISymbol + { + if (symbol is null) + return false; + + // In UWP, Page inherits UserControl + // In Uno, Page doesn't appear to inherit UserControl. + var validSimpleTypeNames = new[] { "UserControl", "Page" }; + + // UWP / Uno / WinUI 3 namespaces. + var validNamespaceRoots = new[] { "Microsoft", "Windows" }; + + // Recursively crawl the base types until either UserControl or Page is found. + var validInheritedSymbol = symbol.ContainingType + .CrawlBy(x => x?.BaseType, baseType => validNamespaceRoots.Any(x => $"{baseType}".StartsWith(x)) && + $"{baseType}".Contains(".UI.Xaml.Controls.") && + validSimpleTypeNames.Any(x => $"{baseType}".EndsWith(x))); + + var containerIsPublic = symbol.ContainingType?.DeclaredAccessibility == Accessibility.Public; + var isPrivate = symbol.DeclaredAccessibility == Accessibility.Private; + var typeIsAccessible = symbol is IFieldSymbol field && field.Type.DeclaredAccessibility == Accessibility.Public || symbol is IPropertySymbol prop && prop.Type.DeclaredAccessibility == Accessibility.Public; + + return validInheritedSymbol != default && isPrivate && containerIsPublic && typeIsAccessible && !symbol.IsStatic; + } + + private static void GenerateNamedPropertyRelay(SourceProductionContext context, ISymbol? symbol, ITypeSymbol? type, HashSet handledConstructors) + { + if (symbol?.ContainingType is null || type is null) + return; + + GenerateConstructor(context, symbol, handledConstructors); + + var source = $@"namespace {symbol.ContainingType.ContainingNamespace} +{{ + partial class {symbol.ContainingType.Name} + {{ + /// + /// Provides the same functionality as using <SomeElement x:FieldProvider=""public"" x:Name=""someName""> + /// on an element in XAML, without the need for the extra x:FieldProvider markup. + /// + public partial record XamlNamedPropertyRelay + {{ + public {type} {symbol.Name} => _{symbol.ContainingType.Name}.{symbol.Name}; + }} + }} +}} +"; + + context.AddSource($"{symbol}.g", source); + } + + private static void GenerateConstructor(SourceProductionContext context, ISymbol? symbol, HashSet handledConstructors) + { + if (symbol?.ContainingType is null) + return; + + if (handledConstructors.Contains(symbol.ContainingType)) + return; + + handledConstructors.Add(symbol.ContainingType); + + var source = $@"namespace {symbol.ContainingType.ContainingNamespace} +{{ + partial class {symbol.ContainingType.Name} + {{ + /// + /// Provides the same functionality as using <SomeElement x:FieldProvider=""public"" x:Name=""someName""> + /// on an element in XAML, without the need for the extra x:FieldProvider markup. + /// + public partial record XamlNamedPropertyRelay + {{ + private readonly {symbol.ContainingType} _{symbol.ContainingType.Name}; + + public XamlNamedPropertyRelay({symbol.ContainingType} {symbol.ContainingType.Name.ToLower()}) + {{ + _{symbol.ContainingType.Name} = {symbol.ContainingType.Name.ToLower()}; + }} + }} + }} +}} +"; + + context.AddSource($"{symbol.ContainingType}.ctor.g", source); + } +} + From 239caaa8807e55e739f1eed095ec04d36514e2f9 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 25 Jan 2022 17:18:23 -0600 Subject: [PATCH 08/26] Include all x-plat libraries in source generation --- Common/Labs.UnoLib.props | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Common/Labs.UnoLib.props b/Common/Labs.UnoLib.props index bbd230afd..8e31a9ecb 100644 --- a/Common/Labs.UnoLib.props +++ b/Common/Labs.UnoLib.props @@ -21,6 +21,17 @@ + + + + + + + + + + From 7c7fbe24c65342dee15014e7a54e2c9b76f6e94b Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 25 Jan 2022 17:19:06 -0600 Subject: [PATCH 09/26] Cleaned up manually created XamlNamedPropertyRelay --- .../SampleOne/SamplePageOptions.xaml.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs index 69ad63a44..41b6c953a 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs @@ -73,24 +73,4 @@ private void Slider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e _xamlProperties.PrimaryText.FontSize = ((Slider)sender).Value; } } - - // TODO: Create a source generator to automate this. - public sealed partial class SamplePage - { - /// - /// Provides the same functionality as using <SomeElement x:FieldProvider="public" x:Name="someName"> - /// on an element in XAML, without the need for the extra x:FieldProvider markup. - /// - public record XamlNamedPropertyRelay - { - private readonly SamplePage _samplePage; - - public XamlNamedPropertyRelay(SamplePage samplePage) - { - _samplePage = samplePage; - } - - public TextBlock PrimaryText => _samplePage.PrimaryText; - } - } } From c2324d3ea8f98e426b284b13a1fc7a498030550c Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 25 Jan 2022 17:54:31 -0600 Subject: [PATCH 10/26] Strip out toolkit attributes from displayed code, enable text selection --- .../ToolkitSampleRenderer.xaml | 17 ++++++++++------- .../ToolkitSampleRenderer.xaml.cs | 6 ++++++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml index e70f6824a..a187cd232 100644 --- a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml +++ b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml @@ -2,11 +2,11 @@ x:Class="CommunityToolkit.Labs.Shared.ToolkitSampleRenderer" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="using:CommunityToolkit.Labs.Shared" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:local="using:CommunityToolkit.Labs.Shared" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" - Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> + Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" + mc:Ignorable="d"> @@ -20,7 +20,10 @@ - + @@ -29,15 +32,15 @@ 14 - + - + - + diff --git a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml.cs b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml.cs index de5b40ea6..9ff62d0cf 100644 --- a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml.cs +++ b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml.cs @@ -1,9 +1,11 @@ using CommunityToolkit.Labs.Core; +using CommunityToolkit.Labs.Core.Attributes; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Windows.Foundation; using Windows.Foundation.Collections; @@ -128,6 +130,10 @@ protected override async void OnNavigatedTo(NavigationEventArgs e) var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"{filePath}.{fileExtension.Trim('.')}")); var textContents = await FileIO.ReadTextAsync(file); + // Remove toolkit attributes + textContents = Regex.Replace(textContents, @$"\s+?\[{nameof(ToolkitSampleAttribute).Replace("Attribute", "")}.+\]", ""); + textContents = Regex.Replace(textContents, @$"\s+?\[{nameof(ToolkitSampleOptionsPaneAttribute).Replace("Attribute", "")}.+\]", ""); + return textContents; } catch (Exception ex) From ccbca533b109f217e5a55262a08bdf9fb5e0b220 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 25 Jan 2022 18:08:56 -0600 Subject: [PATCH 11/26] Fixed options pane taking up space when there's no content --- .../CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml index a187cd232..290cef422 100644 --- a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml +++ b/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml @@ -15,7 +15,7 @@ - + @@ -23,7 +23,7 @@ + Padding="20"> From 6b182d7aed3a1212dc0f91228267ffa417883b4b Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Mon, 31 Jan 2022 16:30:15 -0600 Subject: [PATCH 12/26] Split generators into 2 projects. Created generated pane options. Added basic unit tests. Misc fixes and improvements. --- AllExperimentsSamples.sln | 90 +++++++-- ...it.Labs.Core.SourceGenerators.Tests.csproj | 21 ++ .../MetadataGenerator/SamplePaneOptions.cs | 106 ++++++++++ ...ceGenerators.XamlNamedPropertyRelay.csproj | 14 ++ .../GeneratorExtensions.cs | 122 ++++++++++++ .../XamlNamedPropertyRelayGenerator.cs | 76 +++---- .../Attributes/ToolkitSampleAttribute.cs | 4 +- .../ToolkitSampleBoolOptionAttribute.cs | 34 ++++ .../ToolkitSampleOptionBaseAttribute.cs | 35 ++++ .../ToolkitSampleOptionsPaneAttribute.cs | 4 +- ...yToolkit.Labs.Core.SourceGenerators.csproj | 14 ++ .../Diagnostics/DiagnosticDescriptors.cs | 61 ++++++ .../GeneratorExtensions.cs | 122 ++++++++++++ ...tSampleGeneratedOptionPropertyContainer.cs | 19 ++ .../Metadata/IToolkitSampleOptionViewModel.cs | 27 +++ ...oolkitSampleBoolOptionMetadataViewModel.cs | 78 ++++++++ .../Metadata}/ToolkitSampleMetadata.cs | 7 +- .../IsExternalInit.cs | 0 .../ToolkitSampleCategory.cs | 2 +- .../ToolkitSampleMetadataGenerator.cs | 188 ++++++++++++++++++ .../ToolkitSampleOptionGenerator.cs | 150 ++++++++++++++ .../ToolkitSampleRecord.cs | 25 +++ .../ToolkitSampleSubcategory.cs | 2 +- .../Generators/GeneratorExtensions.cs | 85 -------- .../ToolkitSampleMetadataGenerator.cs | 110 ---------- .../AppLoadingView.xaml.cs | 13 +- .../CommunityToolkit.Labs.Shared.projitems | 12 +- .../MainPage.xaml.cs | 2 + .../GeneratedSampleOptionTemplateSelector.cs | 26 +++ .../GeneratedSampleOptionsRenderer.xaml | 26 +++ .../GeneratedSampleOptionsRenderer.xaml.cs | 43 ++++ .../ToolkitSampleRenderer.xaml | 2 +- .../ToolkitSampleRenderer.xaml.cs | 21 +- Common/Labs.UnoLib.props | 6 +- Common/Labs.Uwp.props | 4 +- Common/Labs.Wasm.props | 3 +- .../CommunityToolkit.Labs.Uwp.csproj | 4 + .../CanvasLayout.Sample.csproj | 1 - .../SampleOne/SamplePage.xaml | 12 +- .../SampleOne/SamplePage.xaml.cs | 5 +- .../SampleOne/SamplePageOptions.xaml.cs | 15 +- .../SampleTwo/SamplePage2.xaml.cs | 17 +- 42 files changed, 1292 insertions(+), 316 deletions(-) create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests.csproj create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay.csproj create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs rename Common/{CommunityToolkit.Labs.Core/Generators => CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay}/XamlNamedPropertyRelayGenerator.cs (85%) rename Common/{CommunityToolkit.Labs.Core => CommunityToolkit.Labs.Core.SourceGenerators}/Attributes/ToolkitSampleAttribute.cs (90%) create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs rename Common/{CommunityToolkit.Labs.Core => CommunityToolkit.Labs.Core.SourceGenerators}/Attributes/ToolkitSampleOptionsPaneAttribute.cs (83%) create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/CommunityToolkit.Labs.Core.SourceGenerators.csproj create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/GeneratorExtensions.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs rename Common/{CommunityToolkit.Labs.Core => CommunityToolkit.Labs.Core.SourceGenerators/Metadata}/ToolkitSampleMetadata.cs (78%) rename Common/{CommunityToolkit.Labs.Core => CommunityToolkit.Labs.Core.SourceGenerators}/System.Runtime.CompilerServices/IsExternalInit.cs (100%) rename Common/{CommunityToolkit.Labs.Core => CommunityToolkit.Labs.Core.SourceGenerators}/ToolkitSampleCategory.cs (89%) create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs rename Common/{CommunityToolkit.Labs.Core => CommunityToolkit.Labs.Core.SourceGenerators}/ToolkitSampleSubcategory.cs (91%) delete mode 100644 Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs delete mode 100644 Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs create mode 100644 Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs create mode 100644 Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml create mode 100644 Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs rename Common/CommunityToolkit.Labs.Shared/{ => Renderers}/ToolkitSampleRenderer.xaml (95%) rename Common/CommunityToolkit.Labs.Shared/{ => Renderers}/ToolkitSampleRenderer.xaml.cs (87%) diff --git a/AllExperimentsSamples.sln b/AllExperimentsSamples.sln index 2a87f0a3e..1ffd9b8fd 100644 --- a/AllExperimentsSamples.sln +++ b/AllExperimentsSamples.sln @@ -32,7 +32,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanvasLayout.Wasm", "Labs\C EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{09003B35-7A35-4BD1-9A26-5CFD02AB88DD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Labs.Core", "Common\CommunityToolkit.Labs.Core\CommunityToolkit.Labs.Core.csproj", "{210476D6-42CC-4D01-B027-478145BEA8FE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Labs.Core.SourceGenerators", "Common\CommunityToolkit.Labs.Core.SourceGenerators\CommunityToolkit.Labs.Core.SourceGenerators.csproj", "{5CB6662F-590F-4250-A19D-E27FEE9C2876}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay", "Common\CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay\CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay.csproj", "{1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Labs.Core.SourceGenerators.Tests", "Common\CommunityToolkit.Labs.Core.SourceGenerators.Tests\CommunityToolkit.Labs.Core.SourceGenerators.Tests\CommunityToolkit.Labs.Core.SourceGenerators.Tests.csproj", "{5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -185,26 +189,66 @@ Global {E8E5AAAA-B15B-4B35-8673-118F6417B6F2}.Release|x64.Build.0 = Release|Any CPU {E8E5AAAA-B15B-4B35-8673-118F6417B6F2}.Release|x86.ActiveCfg = Release|Any CPU {E8E5AAAA-B15B-4B35-8673-118F6417B6F2}.Release|x86.Build.0 = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|ARM.ActiveCfg = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|ARM.Build.0 = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|ARM64.Build.0 = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|x64.ActiveCfg = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|x64.Build.0 = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|x86.ActiveCfg = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Debug|x86.Build.0 = Debug|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|Any CPU.Build.0 = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|ARM.ActiveCfg = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|ARM.Build.0 = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|ARM64.ActiveCfg = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|ARM64.Build.0 = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|x64.ActiveCfg = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|x64.Build.0 = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|x86.ActiveCfg = Release|Any CPU - {210476D6-42CC-4D01-B027-478145BEA8FE}.Release|x86.Build.0 = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|ARM.ActiveCfg = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|ARM.Build.0 = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|ARM64.Build.0 = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|x64.ActiveCfg = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|x64.Build.0 = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|x86.ActiveCfg = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Debug|x86.Build.0 = Debug|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|Any CPU.Build.0 = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|ARM.ActiveCfg = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|ARM.Build.0 = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|ARM64.ActiveCfg = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|ARM64.Build.0 = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|x64.ActiveCfg = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|x64.Build.0 = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|x86.ActiveCfg = Release|Any CPU + {5CB6662F-590F-4250-A19D-E27FEE9C2876}.Release|x86.Build.0 = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|ARM.Build.0 = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|ARM64.Build.0 = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|x64.ActiveCfg = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|x64.Build.0 = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|x86.ActiveCfg = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Debug|x86.Build.0 = Debug|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|Any CPU.Build.0 = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|ARM.ActiveCfg = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|ARM.Build.0 = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|ARM64.ActiveCfg = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|ARM64.Build.0 = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|x64.ActiveCfg = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|x64.Build.0 = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|x86.ActiveCfg = Release|Any CPU + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D}.Release|x86.Build.0 = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|ARM.ActiveCfg = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|ARM.Build.0 = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|ARM64.Build.0 = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|x64.ActiveCfg = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|x64.Build.0 = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|x86.ActiveCfg = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Debug|x86.Build.0 = Debug|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|Any CPU.Build.0 = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|ARM.ActiveCfg = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|ARM.Build.0 = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|ARM64.ActiveCfg = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|ARM64.Build.0 = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|x64.ActiveCfg = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|x64.Build.0 = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|x86.ActiveCfg = Release|Any CPU + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -216,7 +260,9 @@ Global {FE19FFF0-6AB6-4FC7-BFDF-B6499153DCD5} = {EDD2FCF0-74FE-4AB9-B40A-7B2A4E89D59C} {A14189C0-39A8-4FBE-BF86-A78A94654C48} = {86E3CC4D-1359-4249-9C5B-C193BF65633D} {E8E5AAAA-B15B-4B35-8673-118F6417B6F2} = {86E3CC4D-1359-4249-9C5B-C193BF65633D} - {210476D6-42CC-4D01-B027-478145BEA8FE} = {09003B35-7A35-4BD1-9A26-5CFD02AB88DD} + {5CB6662F-590F-4250-A19D-E27FEE9C2876} = {09003B35-7A35-4BD1-9A26-5CFD02AB88DD} + {1683BA1A-5D66-4488-B7CA-6DF3FDE2701D} = {09003B35-7A35-4BD1-9A26-5CFD02AB88DD} + {5BD4E79C-3744-4E89-A6F2-17FBAB7E3F68} = {09003B35-7A35-4BD1-9A26-5CFD02AB88DD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1F0A4823-84EF-41AA-BBF9-A07B38DDC555} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests.csproj b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests.csproj new file mode 100644 index 000000000..a28d7077c --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs new file mode 100644 index 000000000..805e92060 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs @@ -0,0 +1,106 @@ +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using CommunityToolkit.Labs.Core.SourceGenerators.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Tests +{ + [TestClass] + public class SamplePaneOptions + { + [TestMethod] + public void PaneOptionOnNonSample() + { + string source = @" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + { + [ToolkitSampleBoolOption(""BindToMe"", ""Toggle visibility"", false)] + public partial class Sample + { + } + }"; + + VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample.Id); + } + + [DataRow("", DisplayName = "Empty string"), DataRow(" ", DisplayName = "Only whitespace"), DataRow("Test ", DisplayName = "Text with whitespace")] + [DataRow("_", DisplayName = "Underscore"), DataRow("$", DisplayName = "Dollar sign"), DataRow("%", DisplayName = "Percent symbol")] + [DataRow("class", DisplayName = "Reserved keyword 'class'"), DataRow("string", DisplayName = "Reserved keyword 'string'"), DataRow("sealed", DisplayName = "Reserved keyword 'sealed'"), DataRow("ref", DisplayName = "Reserved keyword 'ref'")] + [TestMethod] + public void PaneOptionWithBadName(string name) + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] + [ToolkitSampleBoolOption(""{name}"", ""Toggle visibility"", false)] + public partial class Sample + {{ + }} + }}"; + + VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneOptionWithBadName.Id); + } + + /// + /// Verifies the output of a source generator. + /// + /// The generator type to use. + /// The input source to process. + /// The diagnostic ids to expect for the input source code. + private static void VerifyGeneratedDiagnostics(string source, params string[] diagnosticsIds) + where TGenerator : class, IIncrementalGenerator, new() + { + VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source), diagnosticsIds); + } + + /// + /// Verifies the output of a source generator. + /// + /// The generator type to use. + /// The input source tree to process. + /// The diagnostic ids to expect for the input source code. + private static void VerifyGeneratedDiagnostics(SyntaxTree syntaxTree, params string[] diagnosticsIds) + where TGenerator : class, IIncrementalGenerator, new() + { + var sampleAttributeType = typeof(ToolkitSampleAttribute); + + var references = + from assembly in AppDomain.CurrentDomain.GetAssemblies() + where !assembly.IsDynamic + let reference = MetadataReference.CreateFromFile(assembly.Location) + select reference; + + var compilation = CSharpCompilation.Create( + "original", + new[] { syntaxTree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + IIncrementalGenerator generator = new TGenerator(); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options); + + _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics); + + HashSet resultingIds = diagnostics.Select(diagnostic => diagnostic.Id).ToHashSet(); + + Assert.IsTrue(resultingIds.SetEquals(diagnosticsIds)); + + GC.KeepAlive(sampleAttributeType); + } + } +} \ No newline at end of file diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay.csproj b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay.csproj new file mode 100644 index 000000000..9b3c8d237 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + enable + nullable + 10.0 + + + + + + + diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs new file mode 100644 index 000000000..7f6c0409f --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace CommunityToolkit.Labs.Core.Generators +{ + public static class GeneratorExtensions + { + /// + /// Crawls a namespace and all child namespaces for all contained types. + /// + /// A flattened enumerable of s. + public static IEnumerable CrawlForAllNamedTypes(this INamespaceSymbol namespaceSymbol) + { + foreach (var member in namespaceSymbol.GetMembers()) + { + if (member is INamespaceSymbol nestedNamespace) + { + foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) + yield return item; + } + + if (member is INamedTypeSymbol typeSymbol) + yield return typeSymbol; + } + } + + /// + /// Crawls an object tree for nested properties of the same type and returns the first instance that matches the . + /// + /// + /// Does not filter against or return the object. + /// + public static T? CrawlBy(this T? root, Func selectPredicate, Func filterPredicate) + { + crawl: + var current = selectPredicate(root); + + if (filterPredicate(current)) + { + return current; + } + + if (current is null) + { + return default; + } + + root = current; + goto crawl; + } + + /// + /// Reconstructs an attribute instance as the given type. + /// + /// The attribute type to create. + /// The attribute data used to construct the instance of + public static T ReconstructAs(this AttributeData attributeData) + { + // Reconstructing the attribute instance provides some safety against changes to the attribute's constructor signature. + var attributeArgs = attributeData.ConstructorArguments.Select(PrepareParameterTypeForActivator).ToArray(); + return (T)Activator.CreateInstance(typeof(T), attributeArgs); + } + + + /// + /// Attempts to reconstruct an attribute instance as the given type, returning null if and are mismatched. + /// + /// The attribute type to create. + /// The attribute data used to construct the instance of + public static T? TryReconstructAs(this AttributeData attributeData) + where T : Attribute + { + var attributeMatchesType = attributeData.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(T).FullName}"; + + if (attributeMatchesType) + return attributeData.ReconstructAs(); + + return null; + } + + /// + /// Checks whether or not a given type symbol has a specified full name. + /// + /// The input instance to check. + /// The full name to check. + /// Whether has a full name equals to . + public static bool HasFullyQualifiedName(this ISymbol symbol, string name) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name; + } + + /// + /// Performs any data transforms needed for using as a parameter in . + /// + /// The 's was null. + public static object? PrepareParameterTypeForActivator(this TypedConstant parameterTypedConstant) + { + if (parameterTypedConstant.Type is null) + throw new ArgumentNullException(nameof(parameterTypedConstant.Type)); + + // Types prefixed with global:: do not work with Type.GetType and must be stripped away. + var assemblyQualifiedName = parameterTypedConstant.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + + var argType = Type.GetType(assemblyQualifiedName); + + // Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance() + if (argType != null && parameterTypedConstant.Kind == TypedConstantKind.Enum) + return Enum.Parse(argType, parameterTypedConstant.Value?.ToString()); + + return parameterTypedConstant.Value; + } + } +} diff --git a/Common/CommunityToolkit.Labs.Core/Generators/XamlNamedPropertyRelayGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs similarity index 85% rename from Common/CommunityToolkit.Labs.Core/Generators/XamlNamedPropertyRelayGenerator.cs rename to Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs index c85fb3d11..56d237196 100644 --- a/Common/CommunityToolkit.Labs.Core/Generators/XamlNamedPropertyRelayGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs @@ -2,21 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using CommunityToolkit.Labs.Core.Generators; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; -using System.Text; -namespace CommunityToolkit.Labs.Core.Generators; +namespace CommunityToolkit.Labs.Core.SourceGenerators; [Generator] public class XamlNamedPropertyRelayGenerator : IIncrementalGenerator { - private readonly HashSet _handledConstructors = new(); + private readonly HashSet _handledConstructors = new(SymbolEqualityComparer.Default); public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -26,45 +23,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (node, _) => node is FieldDeclarationSyntax, static (context, _) => ((FieldDeclarationSyntax)context.Node).Declaration.Variables.Select(v => (IFieldSymbol?)context.SemanticModel.GetDeclaredSymbol(v))) .SelectMany(static (item, _) => item) - .Where(SymbolIsDeclaredXamlName); + .Where(IsDeclaredXamlXNameProperty); // Uno generates private properties for x:Name. var propertySymbols = context.SyntaxProvider .CreateSyntaxProvider( static (node, _) => node is MemberDeclarationSyntax, static (context, _) => context.SemanticModel.GetDeclaredSymbol((MemberDeclarationSyntax)context.Node) as IPropertySymbol) - .Where(SymbolIsDeclaredXamlName); + .Where(IsDeclaredXamlXNameProperty); context.RegisterSourceOutput(fieldSymbols, (ctx, sym) => GenerateNamedPropertyRelay(ctx, sym, sym?.Type, _handledConstructors)); context.RegisterSourceOutput(propertySymbols, (ctx, sym) => GenerateNamedPropertyRelay(ctx, sym, sym?.Type, _handledConstructors)); } - private static bool SymbolIsDeclaredXamlName(T? symbol) - where T : ISymbol - { - if (symbol is null) - return false; - - // In UWP, Page inherits UserControl - // In Uno, Page doesn't appear to inherit UserControl. - var validSimpleTypeNames = new[] { "UserControl", "Page" }; - - // UWP / Uno / WinUI 3 namespaces. - var validNamespaceRoots = new[] { "Microsoft", "Windows" }; - - // Recursively crawl the base types until either UserControl or Page is found. - var validInheritedSymbol = symbol.ContainingType - .CrawlBy(x => x?.BaseType, baseType => validNamespaceRoots.Any(x => $"{baseType}".StartsWith(x)) && - $"{baseType}".Contains(".UI.Xaml.Controls.") && - validSimpleTypeNames.Any(x => $"{baseType}".EndsWith(x))); - - var containerIsPublic = symbol.ContainingType?.DeclaredAccessibility == Accessibility.Public; - var isPrivate = symbol.DeclaredAccessibility == Accessibility.Private; - var typeIsAccessible = symbol is IFieldSymbol field && field.Type.DeclaredAccessibility == Accessibility.Public || symbol is IPropertySymbol prop && prop.Type.DeclaredAccessibility == Accessibility.Public; - - return validInheritedSymbol != default && isPrivate && containerIsPublic && typeIsAccessible && !symbol.IsStatic; - } - private static void GenerateNamedPropertyRelay(SourceProductionContext context, ISymbol? symbol, ITypeSymbol? type, HashSet handledConstructors) { if (symbol?.ContainingType is null || type is null) @@ -96,12 +67,11 @@ private static void GenerateConstructor(SourceProductionContext context, ISymbol if (symbol?.ContainingType is null) return; - if (handledConstructors.Contains(symbol.ContainingType)) + if (!handledConstructors.Add(symbol.ContainingType)) return; - handledConstructors.Add(symbol.ContainingType); - - var source = $@"namespace {symbol.ContainingType.ContainingNamespace} + var source = $@"#nullable enable +namespace {symbol.ContainingType.ContainingNamespace} {{ partial class {symbol.ContainingType.Name} {{ @@ -124,5 +94,35 @@ public XamlNamedPropertyRelay({symbol.ContainingType} {symbol.ContainingType.Nam context.AddSource($"{symbol.ContainingType}.ctor.g", source); } + + /// + /// Checks if a symbol's is or inherits from a type representing a XAML framework, and that the is a generated x:Name. + /// + /// if the is or inherits from a type representing a XAML framework. + public static bool IsDeclaredXamlXNameProperty(T? symbol) + where T : ISymbol + { + if (symbol is null) + return false; + + // In UWP, Page inherits UserControl + // In Uno, Page doesn't appear to inherit UserControl. + var validSimpleTypeNames = new[] { "UserControl", "Page" }; + + // UWP / Uno / WinUI 3 namespaces. + var validNamespaceRoots = new[] { "Microsoft", "Windows" }; + + // Recursively crawl the base types until either UserControl or Page is found. + var validInheritedSymbol = symbol.ContainingType + .CrawlBy(x => x?.BaseType, baseType => validNamespaceRoots.Any(x => $"{baseType}".StartsWith(x)) && + $"{baseType}".Contains(".UI.Xaml.Controls.") && + validSimpleTypeNames.Any(x => $"{baseType}".EndsWith(x))); + + var containerIsPublic = symbol.ContainingType?.DeclaredAccessibility == Accessibility.Public; + var isPrivate = symbol.DeclaredAccessibility == Accessibility.Private; + var typeIsAccessible = symbol is IFieldSymbol field && field.Type.DeclaredAccessibility == Accessibility.Public || symbol is IPropertySymbol prop && prop.Type.DeclaredAccessibility == Accessibility.Public; + + return validInheritedSymbol != default && isPrivate && containerIsPublic && typeIsAccessible && !symbol.IsStatic; + } } diff --git a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleAttribute.cs similarity index 90% rename from Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs rename to Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleAttribute.cs index fa9746aaf..8709c219e 100644 --- a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleAttribute.cs @@ -1,10 +1,12 @@ using System; +using System.Diagnostics; -namespace CommunityToolkit.Labs.Core.Attributes; +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; /// /// Registers a control as a toolkit sample using the provided data. /// +[Conditional("COMMUNITYTOOLKIT_KEEP_SAMPLE_ATTRIBUTES")] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public sealed class ToolkitSampleAttribute : Attribute { diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs new file mode 100644 index 000000000..1be844723 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +/// +/// Represents a boolean sample option that the user can manipulate and the XAML can bind to. +/// +/// +/// Using this attribute will automatically generate a dependency property that you can bind to in XAML. +/// +[Conditional("COMMUNITYTOOLKIT_KEEP_SAMPLE_ATTRIBUTES")] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class ToolkitSampleBoolOptionAttribute : ToolkitSampleOptionBaseAttribute +{ + /// + /// Creates a new instance of . + /// + public ToolkitSampleBoolOptionAttribute(string name, string label, bool defaultState) + : base(name, defaultState) + { + Label = label; + } + + /// + /// The source generator-friendly type name used for casting. + /// + public override string TypeName { get; } = "bool"; + + /// + /// A label to display along the boolean option. + /// + public string Label { get; } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs new file mode 100644 index 000000000..9712c5858 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +/// +/// Represents a sample option that the user can manipulate and the XAML can bind to. +/// +[Conditional("COMMUNITYTOOLKIT_KEEP_SAMPLE_ATTRIBUTES")] +public abstract class ToolkitSampleOptionBaseAttribute : Attribute +{ + /// + /// Creates a new instance of . + /// + public ToolkitSampleOptionBaseAttribute(string name, object defaultState) + { + Name = name; + DefaultState = defaultState; + } + + /// + /// A name that you can bind to in your XAML. + /// + public string Name { get; } + + /// + /// The default state. + /// + public object DefaultState { get; } + + /// + /// The source generator-friendly type name used for casting. + /// + public abstract string TypeName { get; } +} diff --git a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleOptionsPaneAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionsPaneAttribute.cs similarity index 83% rename from Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleOptionsPaneAttribute.cs rename to Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionsPaneAttribute.cs index f4477017b..f37164642 100644 --- a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleOptionsPaneAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionsPaneAttribute.cs @@ -1,11 +1,13 @@ using System; +using System.Diagnostics; -namespace CommunityToolkit.Labs.Core.Attributes; +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; /// /// Registers a control as the options panel for a toolkit sample. /// [AttributeUsage(AttributeTargets.Class)] +[Conditional("COMMUNITYTOOLKIT_KEEP_SAMPLE_ATTRIBUTES")] public sealed class ToolkitSampleOptionsPaneAttribute : Attribute { /// diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/CommunityToolkit.Labs.Core.SourceGenerators.csproj b/Common/CommunityToolkit.Labs.Core.SourceGenerators/CommunityToolkit.Labs.Core.SourceGenerators.csproj new file mode 100644 index 000000000..9b3c8d237 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/CommunityToolkit.Labs.Core.SourceGenerators.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + enable + nullable + 10.0 + + + + + + + diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 000000000..c9ac1a395 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Diagnostics +{ + /// + /// A container for all instances for errors reported by analyzers in this project. + /// + public static class DiagnosticDescriptors + { + /// + /// Gets a indicating a derived used on a member that isn't a toolkit sample. + /// + /// Format: "Cannot generate sample pane options for type {0} as it does not use ToolkitSampleAttribute". + /// + /// + public static readonly DiagnosticDescriptor SamplePaneOptionAttributeOnNonSample = new( + id: "TKSMPL0001", + title: $"Invalid sample option delaration", + messageFormat: $"Cannot generate sample pane options for type {0} as it does not use {nameof(Attributes.ToolkitSampleAttribute)}", + category: typeof(ToolkitSampleMetadataGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot generate sample pane options for a type which does not use {nameof(Attributes.ToolkitSampleAttribute)}."); + + /// + /// Gets a indicating a derived with an empty or invalid name. + /// + /// Format: "Cannot generate sample pane options for type {0} as it does not use ToolkitSampleAttribute". + /// + /// + public static readonly DiagnosticDescriptor SamplePaneOptionWithBadName = new( + id: "TKSMPL0002", + title: $"Invalid sample option delaration", + messageFormat: $"Cannot generate sample pane options for type {0} as the provided name is empty or invalid.", + category: typeof(ToolkitSampleMetadataGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot generate sample pane options when the provided name is empty or invalid."); + + /// + /// Gets a indicating a with a that doesn't have a corresponding . + /// + /// Format: "Cannot link sample options pane to type {0} as the provided sample ID does not match any known {nameof(Attributes.ToolkitSampleAttribute)}". + /// + /// + public static readonly DiagnosticDescriptor OptionsPaneAttributeWithMissingOrInvalidSampleId = new( + id: "TKSMPL0003", + title: $"Missing or invalid sample Id", + messageFormat: $"Cannot link sample options pane to type {0} as the provided sample ID does not match any known {nameof(Attributes.ToolkitSampleAttribute)}", + category: typeof(ToolkitSampleMetadataGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot link sample options pane to a provided sample ID that does not match any known {nameof(Attributes.ToolkitSampleAttribute)}."); + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/GeneratorExtensions.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/GeneratorExtensions.cs new file mode 100644 index 000000000..efb705c4f --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/GeneratorExtensions.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace CommunityToolkit.Labs.Core.SourceGenerators +{ + public static class GeneratorExtensions + { + /// + /// Crawls a namespace and all child namespaces for all contained types. + /// + /// A flattened enumerable of s. + public static IEnumerable CrawlForAllNamedTypes(this INamespaceSymbol namespaceSymbol) + { + foreach (var member in namespaceSymbol.GetMembers()) + { + if (member is INamespaceSymbol nestedNamespace) + { + foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) + yield return item; + } + + if (member is INamedTypeSymbol typeSymbol) + yield return typeSymbol; + } + } + + /// + /// Crawls an object tree for nested properties of the same type and returns the first instance that matches the . + /// + /// + /// Does not filter against or return the object. + /// + public static T? CrawlBy(this T? root, Func selectPredicate, Func filterPredicate) + { + crawl: + var current = selectPredicate(root); + + if (filterPredicate(current)) + { + return current; + } + + if (current is null) + { + return default; + } + + root = current; + goto crawl; + } + + /// + /// Reconstructs an attribute instance as the given type. + /// + /// The attribute type to create. + /// The attribute data used to construct the instance of + public static T ReconstructAs(this AttributeData attributeData) + { + // Reconstructing the attribute instance provides some safety against changes to the attribute's constructor signature. + var attributeArgs = attributeData.ConstructorArguments.Select(PrepareParameterTypeForActivator).ToArray(); + return (T)Activator.CreateInstance(typeof(T), attributeArgs); + } + + + /// + /// Attempts to reconstruct an attribute instance as the given type, returning null if and are mismatched. + /// + /// The attribute type to create. + /// The attribute data used to construct the instance of + public static T? TryReconstructAs(this AttributeData attributeData) + where T : Attribute + { + var attributeMatchesType = attributeData.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(T).FullName}"; + + if (attributeMatchesType) + return attributeData.ReconstructAs(); + + return null; + } + + /// + /// Checks whether or not a given type symbol has a specified full name. + /// + /// The input instance to check. + /// The full name to check. + /// Whether has a full name equals to . + public static bool HasFullyQualifiedName(this ISymbol symbol, string name) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name; + } + + /// + /// Performs any data transforms needed for using as a parameter in . + /// + /// The 's was null. + public static object? PrepareParameterTypeForActivator(this TypedConstant parameterTypedConstant) + { + if (parameterTypedConstant.Type is null) + throw new ArgumentNullException(nameof(parameterTypedConstant.Type)); + + // Types prefixed with global:: do not work with Type.GetType and must be stripped away. + var assemblyQualifiedName = parameterTypedConstant.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + + var argType = Type.GetType(assemblyQualifiedName); + + // Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance() + if (argType != null && parameterTypedConstant.Kind == TypedConstantKind.Enum) + return Enum.Parse(argType, parameterTypedConstant.Value?.ToString()); + + return parameterTypedConstant.Value; + } + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs new file mode 100644 index 000000000..757319489 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata +{ + /// + /// Implementors of this class contain properties which were created by source generators, are bound to in XAML, and are manipulated from another source. + /// + public interface IToolkitSampleGeneratedOptionPropertyContainer + { + /// + /// Holds a reference to the backing ViewModels for all generated properties. + /// + public IEnumerable? GeneratedPropertyMetadata { get; set; } + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs new file mode 100644 index 000000000..b12252a46 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata +{ + /// + /// A common view model for any toolkit sample option. + /// + public interface IToolkitSampleOptionViewModel : INotifyPropertyChanged + { + /// + /// The current value. Bound in XAML. + /// + public object Value { get; set; } + + /// + /// A unique identifier name for this option. + /// + /// + /// Used by the sample system to match up to the original and the control that declared it. + /// + public string Name { get; } + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs new file mode 100644 index 000000000..b7112c715 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using System.ComponentModel; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata +{ + /// + /// A metadata container for data defined in with INPC support. + /// + public class ToolkitSampleBoolOptionMetadataViewModel : IToolkitSampleOptionViewModel + { + private string _label; + private object _value; + + /// + /// Creates a new instance of . + /// + public ToolkitSampleBoolOptionMetadataViewModel(string id, string label, bool defaultState) + { + Name = id; + _label = label; + _value = defaultState; + } + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// A unique identifier for this option. + /// + /// + /// Used by the sample system to match up to the original and the control that declared it. + /// + public string Name { get; } + + /// + /// The current boolean value. + /// + public bool BoolValue + { + get => (bool)_value; + set + { + _value = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Name)); + } + } + + /// + /// The current boolean value. + /// + public object Value + { + get => BoolValue; + set + { + BoolValue = (bool)value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Name)); + } + } + + /// + /// A label to display along the boolean option. + /// + public string Label + { + get => _label; + set + { + _label = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Label))); + } + } + } +} diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs similarity index 78% rename from Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs rename to Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs index f61329d6c..0fd018adb 100644 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs @@ -3,8 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; -namespace CommunityToolkit.Labs.Core; +namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata; /// /// Contains the metadata needed to identify and display a toolkit sample. @@ -18,10 +19,12 @@ namespace CommunityToolkit.Labs.Core; /// The control type for the sample page's options pane. /// Constructor should have exactly one parameter that can be assigned to the control type (). /// +/// The options that were declared alongside this sample, if any. public sealed record ToolkitSampleMetadata( ToolkitSampleCategory Category, ToolkitSampleSubcategory Subcategory, string DisplayName, string Description, Type SampleControlType, - Type? SampleOptionsPaneType = null); + Type? SampleOptionsPaneType = null, + IEnumerable? GeneratedSampleOptions = null); diff --git a/Common/CommunityToolkit.Labs.Core/System.Runtime.CompilerServices/IsExternalInit.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/System.Runtime.CompilerServices/IsExternalInit.cs similarity index 100% rename from Common/CommunityToolkit.Labs.Core/System.Runtime.CompilerServices/IsExternalInit.cs rename to Common/CommunityToolkit.Labs.Core.SourceGenerators/System.Runtime.CompilerServices/IsExternalInit.cs diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleCategory.cs similarity index 89% rename from Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs rename to Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleCategory.cs index 4f82dfbdd..072f99041 100644 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleCategory.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace CommunityToolkit.Labs.Core; +namespace CommunityToolkit.Labs.Core.SourceGenerators; /// /// The various categories each sample is organized into. diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs new file mode 100644 index 000000000..22c2de618 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using CommunityToolkit.Labs.Core.SourceGenerators.Diagnostics; +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace CommunityToolkit.Labs.Core.SourceGenerators; + +/// +/// Crawls all referenced projects for s and generates a static method that returns metadata for each one found. +/// +[Generator] +public partial class ToolkitSampleMetadataGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var classes = context.SyntaxProvider + .CreateSyntaxProvider( + static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0, + static (ctx, _) => ctx.SemanticModel.GetDeclaredSymbol(ctx.Node)) + .Where(static m => m is not null) + .Select(static (x, _) => x!); + + var referencedTypes = context.CompilationProvider + .SelectMany((x, _) => x.SourceModule.ReferencedAssemblySymbols) + .SelectMany((asm, _) => asm.GlobalNamespace.CrawlForAllNamedTypes()) + .Where(x => x.TypeKind == TypeKind.Class && x.CanBeReferencedByName)/* + .Where(IsValidXamlControl)*/ + .Select((x, _) => (ISymbol)x); + + Execute(classes); + Execute(referencedTypes); + + void Execute(IncrementalValuesProvider types) + { + // Get all attributes + the original type symbol. + var allAttributeData = types.SelectMany(static (sym, _) => sym.GetAttributes().Select(x => (sym, x))); + + // Get all generated pane option attributes + the original type symbol. + var generatedPaneOptions = allAttributeData.Select(static (x, _) => + { + if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) + return (x.Item1, (ToolkitSampleOptionBaseAttribute)boolOptionAttribute); + + return default; + }).Collect(); + + // Find and reconstruct relevant attributes (with pane options) + var toolkitSampleAttributeData = allAttributeData.Select(static (data, _) => + { + if (data.Item2.TryReconstructAs() is ToolkitSampleAttribute sampleAttribute) + { + return (Attribute: sampleAttribute, AttachedQualifiedTypeName: data.Item1.ToString(), Symbol: data.Item1); + } + + return default; + }).Collect(); + + var optionsPaneAttributes = allAttributeData + .Select(static (x, _) => (x.Item2.TryReconstructAs(), x.Item1)) + .Where(static x => x.Item1 is not null) + .Collect(); + + var all = optionsPaneAttributes + .Combine(toolkitSampleAttributeData) + .Combine(generatedPaneOptions); + + context.RegisterSourceOutput(all, (ctx, data) => + { + var toolkitSampleAttributeData = data.Left.Right.Where(x => x != default).Distinct(); + var optionsPaneAttribute = data.Left.Left.Where(x => x != default).Distinct(); + var generatedOptionPropertyData = data.Right.Where(x => x != default).Distinct(); + + // Check for generated options which don't have a valid sample attribute + var generatedOptionsWithMissingSampleAttribute = generatedOptionPropertyData.Where(x => !toolkitSampleAttributeData.Any(sample => ReferenceEquals(sample.Symbol, x.Item1))); + + foreach (var item in generatedOptionsWithMissingSampleAttribute) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample, item.Item1.Locations.FirstOrDefault())); + + // Check for generated options with an empty or invalid name. + var generatedOptionsWithBadName = generatedOptionPropertyData.Where(x => string.IsNullOrWhiteSpace(x.Item2.Name) || + !x.Item2.Name.Any(char.IsLetterOrDigit) || + x.Item2.Name.Any(char.IsWhiteSpace) || + SyntaxFacts.GetKeywordKind(x.Item2.Name) != SyntaxKind.None); + + foreach (var item in generatedOptionsWithBadName) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithBadName, item.Item1.Locations.FirstOrDefault())); + + // Check for options pane attributes with no matching sample ID + var optionsPaneAttributeWithMissingOrInvalidSampleId = optionsPaneAttribute.Where(x => !toolkitSampleAttributeData.Any(sample => sample.Attribute.Id == x.Item1?.SampleId)); + + foreach (var item in optionsPaneAttributeWithMissingOrInvalidSampleId) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.OptionsPaneAttributeWithMissingOrInvalidSampleId, item.Item2.Locations.FirstOrDefault())); + + // Reconstruct sample metadata from attributes + var sampleMetadata = toolkitSampleAttributeData + .Select(sample => + new ToolkitSampleRecord( + sample.Attribute.Category, + sample.Attribute.Subcategory, + sample.Attribute.DisplayName, + sample.Attribute.Description, + sample.AttachedQualifiedTypeName, + optionsPaneAttribute.FirstOrDefault(x => x.Item1?.SampleId == sample.Attribute.Id).Item2?.ToString(), + generatedOptionPropertyData.Where(x => ReferenceEquals(x.Item1, sample.Symbol)).Select(x => x.Item2) + ) + ); + + if (!sampleMetadata.Any()) + return; + + // Build source string + var source = BuildRegistrationCallsFromMetadata(sampleMetadata); + ctx.AddSource($"ToolkitSampleRegistry.g.cs", source); + }); + } + } + + private static string BuildRegistrationCallsFromMetadata(IEnumerable sampleMetadata) + { + return $@"#nullable enable +namespace CommunityToolkit.Labs.Core.SourceGenerators; + +public static class ToolkitSampleRegistry +{{ + public static System.Collections.Generic.IEnumerable<{typeof(ToolkitSampleMetadata).FullName}> Execute() + {{ + { + string.Join("\n ", sampleMetadata.Select(MetadataToRegistryCall).ToArray()) + } + }} +}}"; + } + + private static string MetadataToRegistryCall(ToolkitSampleRecord metadata) + { + var sampleOptionsParam = metadata.SampleOptionsAssemblyQualifiedName is null ? "null" : $"typeof({metadata.SampleOptionsAssemblyQualifiedName})"; + var categoryParam = $"{nameof(ToolkitSampleCategory)}.{metadata.Category}"; + var subcategoryParam = $"{nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}"; + var containingClassTypeParam = $"typeof({metadata.SampleAssemblyQualifiedName})"; + var generatedSampleOptionsParam = $"new {typeof(IToolkitSampleOptionViewModel).FullName}[] {{ {string.Join(", ", metadata.GeneratedSampleOptions?.Select(BuildNewGeneratedSampleOptionMetadataSource).ToArray())} }}"; + + return @$"yield return new {typeof(ToolkitSampleMetadata).FullName}({categoryParam}, {subcategoryParam}, ""{metadata.DisplayName}"", ""{metadata.Description}"", {containingClassTypeParam}, {sampleOptionsParam}, {generatedSampleOptionsParam});"; + } + + private static string BuildNewGeneratedSampleOptionMetadataSource(ToolkitSampleOptionBaseAttribute baseAttribute) + { + if (baseAttribute is ToolkitSampleBoolOptionAttribute boolAttribute) + { + return $@"new {typeof(ToolkitSampleBoolOptionMetadataViewModel).FullName}(""{boolAttribute.Name}"", ""{boolAttribute.Label}"", {boolAttribute.DefaultState.ToString().ToLower()})"; + } + + throw new NotSupportedException($"Unsupported or unhandled type {baseAttribute.GetType()}."); + } + + /// + /// Checks if a symbol's is or inherits from a type representing a XAML framework/ + /// + /// if the is or inherits from a type representing a XAML framework. + private static bool IsValidXamlControl(INamedTypeSymbol symbol) + { + // In UWP, Page inherits UserControl + // In Uno, Page doesn't appear to inherit UserControl. + var validSimpleTypeNames = new[] { "UserControl", "Page" }; + + // UWP / Uno / WinUI 3 namespaces. + var validNamespaceRoots = new[] { "Microsoft", "Windows" }; + + // Recursively crawl the base types until either UserControl or Page is found. + var validInheritedSymbol = symbol.CrawlBy(x => x?.BaseType, baseType => validNamespaceRoots.Any(x => $"{baseType}".StartsWith(x)) && + $"{baseType}".Contains(".UI.Xaml.Controls.") && + validSimpleTypeNames.Any(x => $"{baseType}".EndsWith(x))); + + var typeIsAccessible = symbol.DeclaredAccessibility == Accessibility.Public; + + return validInheritedSymbol != default && typeIsAccessible && !symbol.IsStatic; + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs new file mode 100644 index 000000000..609ea818e --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CommunityToolkit.Labs.Core.SourceGenerators +{ + [Generator] + public class ToolkitSampleOptionGenerator : IIncrementalGenerator + { + private readonly HashSet _handledAttributes = new(); + private readonly HashSet _handledContainingClasses = new(SymbolEqualityComparer.Default); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var classes = context.SyntaxProvider + .CreateSyntaxProvider( + static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0, + static (ctx, _) => ctx.SemanticModel.GetDeclaredSymbol(ctx.Node)) + .Where(static m => m is not null) + .Select(static (x, _) => x!); + + // Get all attributes + the original type symbol. + var allAttributeData = classes.SelectMany((sym, _) => sym.GetAttributes().Select(x => (sym, x))); + + // Find and reconstruct attributes. + var sampleAttributeOptions = allAttributeData + .Select((x, _) => + { + if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) + return (Attribute: (ToolkitSampleOptionBaseAttribute)boolOptionAttribute, ContainingClassSymbol: x.Item1, Type: typeof(ToolkitSampleBoolOptionMetadataViewModel)); + + return default; + }) + .Where(x => x != default); + + context.RegisterSourceOutput(sampleAttributeOptions, (ctx, data) => + { + if (_handledContainingClasses.Add(data.ContainingClassSymbol)) + { + if (data.ContainingClassSymbol is ITypeSymbol typeSym && !typeSym.AllInterfaces.Any(x => x.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanged"))) + { + var inpcImpl = BuildINotifyPropertyChangedImplementation(data.ContainingClassSymbol); + ctx.AddSource($"{data.ContainingClassSymbol}.NotifyPropertyChanged.g", inpcImpl); + } + + var propertyContainerSource = BuildGeneratedPropertyMetadataContainer(data.ContainingClassSymbol); + ctx.AddSource($"{data.ContainingClassSymbol}.GeneratedPropertyContainer.g", propertyContainerSource); + } + + if (!_handledAttributes.Add(data.Attribute)) + return; + + var dependencyPropertySource = BuildProperty(data.ContainingClassSymbol, data.Attribute.Name, data.Attribute.TypeName, data.Type); + + ctx.AddSource($"{data.ContainingClassSymbol}.Property.{data.Attribute.Name}.g", dependencyPropertySource); + }); + + } + + private static string BuildINotifyPropertyChangedImplementation(ISymbol containingClassSymbol) + { + return $@"#nullable enable +using System.ComponentModel; + +namespace {containingClassSymbol.ContainingNamespace} +{{ + public partial class {containingClassSymbol.Name} : {nameof(System.ComponentModel.INotifyPropertyChanged)} + {{ + public event PropertyChangedEventHandler PropertyChanged; + }} +}} +"; + } + + private static string BuildGeneratedPropertyMetadataContainer(ISymbol containingClassSymbol) + { + return $@"#nullable enable +using System.ComponentModel; +using System.Collections.Generic; + +namespace {containingClassSymbol.ContainingNamespace} +{{ + public partial class {containingClassSymbol.Name} : {typeof(IToolkitSampleGeneratedOptionPropertyContainer).Namespace}.{nameof(IToolkitSampleGeneratedOptionPropertyContainer)} + {{ + private IEnumerable<{typeof(IToolkitSampleOptionViewModel).FullName}>? _generatedPropertyMetadata; + + public IEnumerable<{typeof(IToolkitSampleOptionViewModel).FullName}>? GeneratedPropertyMetadata + {{ + get => _generatedPropertyMetadata; + set + {{ + if (!(_generatedPropertyMetadata is null)) + {{ + foreach (var item in _generatedPropertyMetadata) + item.PropertyChanged -= OnPropertyChanged; + }} + + + if (!(value is null)) + {{ + foreach (var item in value) + item.PropertyChanged += OnPropertyChanged; + }} + + _generatedPropertyMetadata = value; + }} + }} + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) => PropertyChanged?.Invoke(this, e); + }} +}} +"; + } + + private static string BuildProperty(ISymbol containingClassSymbol, string propertyName, string typeName, Type viewModelType) + { + return $@"#nullable enable +using System.ComponentModel; +using System.Linq; + +namespace {containingClassSymbol.ContainingNamespace} +{{ + public partial class {containingClassSymbol.Name} + {{ + public {typeName} {propertyName} + {{ + get => (({typeName})(({viewModelType.FullName})GeneratedPropertyMetadata!.First(x => x.Name == ""{propertyName}""))!.Value); + set + {{ + if (GeneratedPropertyMetadata?.FirstOrDefault(x => x.Name == nameof({propertyName})) is {viewModelType.FullName} metadata) + {{ + metadata.Value = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof({propertyName}))); + }} + }} + }} + }} +}} +"; + } + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs new file mode 100644 index 000000000..d3fa83a3a --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +namespace CommunityToolkit.Labs.Core.SourceGenerators; + +public partial class ToolkitSampleMetadataGenerator +{ + /// + /// A new record must be used instead of using directly + /// because we cannot Type.GetType using the , + /// but we can safely generate a type reference in the final output using typeof(AssemblyQualifiedName). + /// + private sealed record ToolkitSampleRecord( + ToolkitSampleCategory Category, + ToolkitSampleSubcategory Subcategory, + string DisplayName, + string Description, + string SampleAssemblyQualifiedName, + string? SampleOptionsAssemblyQualifiedName, + IEnumerable? GeneratedSampleOptions = null); +} diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleSubcategory.cs similarity index 91% rename from Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs rename to Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleSubcategory.cs index 887cd8780..c4191159b 100644 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleSubcategory.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace CommunityToolkit.Labs.Core; +namespace CommunityToolkit.Labs.Core.SourceGenerators; /// /// The various subcategories used by samples. diff --git a/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs b/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs deleted file mode 100644 index a96231806..000000000 --- a/Common/CommunityToolkit.Labs.Core/Generators/GeneratorExtensions.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace CommunityToolkit.Labs.Core.Generators -{ - internal static class GeneratorExtensions - { - internal static IEnumerable CrawlForAllNamedTypes(this INamespaceSymbol namespaceSymbol) - { - foreach (var member in namespaceSymbol.GetMembers()) - { - if (member is INamespaceSymbol nestedNamespace) - { - foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) - yield return item; - } - - if (member is INamedTypeSymbol typeSymbol) - yield return typeSymbol; - } - } - - /// - /// Crawls an object tree for nested properties of the same type and returns the first instance that matches the . - /// - /// - /// Does not filter against or return the object. - /// - public static T? CrawlBy(this T? root, Func selectPredicate, Func filterPredicate) - { - crawl: - var current = selectPredicate(root); - - if (filterPredicate(current)) - { - return current; - } - - if (current is null) - { - return default; - } - - root = current; - goto crawl; - } - - /// - /// Reconstructs an attribute instance as the given type. - /// - /// The attribute type to create. - /// The attribute data used to construct the instance of - /// - internal static T ReconstructAs(this AttributeData attributeData) - { - // Reconstructing the attribute instance provides some safety against changes to the attribute's constructor signature. - var attributeArgs = attributeData.ConstructorArguments.Select(PrepareTypeForActivator).ToArray(); - return (T)Activator.CreateInstance(typeof(T), attributeArgs); - } - - internal static object? PrepareTypeForActivator(this TypedConstant typedConstant) - { - if (typedConstant.Type is null) - throw new ArgumentNullException(nameof(typedConstant.Type)); - - // Types prefixed with global:: do not work with Type.GetType and must be stripped away. - var assemblyQualifiedName = typedConstant.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); - - var argType = Type.GetType(assemblyQualifiedName); - - // Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance() - if (argType != null && typedConstant.Kind == TypedConstantKind.Enum) - return Enum.Parse(argType, typedConstant.Value?.ToString()); - - return typedConstant.Value; - } - } -} diff --git a/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs deleted file mode 100644 index a4e1cd76c..000000000 --- a/Common/CommunityToolkit.Labs.Core/Generators/ToolkitSampleMetadataGenerator.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Linq; -using CommunityToolkit.Labs.Core.Attributes; -using Microsoft.CodeAnalysis; - -namespace CommunityToolkit.Labs.Core.Generators; - -/// -/// Crawls all referenced projects for s and generates a static method that returns metadata for each one found. -/// -[Generator] -public partial class ToolkitSampleMetadataGenerator : ISourceGenerator -{ - /// - public void Initialize(GeneratorInitializationContext context) - { - // not needed - } - - /// - public void Execute(GeneratorExecutionContext context) - { - // Find all types in all assemblies. - var assemblies = context.Compilation.SourceModule.ReferencedAssemblySymbols; - - var types = assemblies.SelectMany(asm => asm.GlobalNamespace.CrawlForAllNamedTypes()) - .Where(x => x?.TypeKind == TypeKind.Class && x.CanBeReferencedByName) // remove null and invalid values. - .Cast(); // strip nullability from type. - - if (types is null) - return; - - // Get all attributes + the original type symbol. - var allAttributeData = types.SelectMany(type => type.GetAttributes(), (Type, Attribute) => (Type, Attribute)); - - // Find and reconstruct relevant attributes. - var toolkitSampleAttributeData = allAttributeData - .Where(x => IsToolkitSampleAttribute(x.Attribute)) - .Select(x => (Attribute: x.Attribute.ReconstructAs(), AttachedQualifiedTypeName: x.Type.ToString())); - - if (!toolkitSampleAttributeData.Any()) - return; - - var optionsPaneAttributes = allAttributeData - .Where(x => IsToolkitSampleOptionsPaneAttribute(x.Attribute)) - .Select(x => (Attribute: x.Attribute.ReconstructAs(), AttachedQualifiedTypeName: x.Type.ToString())); - - // Reconstruct sample metadata from attributes - var sampleMetadata = toolkitSampleAttributeData.Select(sample => - new ToolkitSampleRecord( - sample.Attribute.Category, - sample.Attribute.Subcategory, - sample.Attribute.DisplayName, - sample.Attribute.Description, - sample.AttachedQualifiedTypeName, - optionsPaneAttributes.FirstOrDefault(opt => opt.Attribute.SampleId == sample.Attribute.Id).AttachedQualifiedTypeName) - ); - - // Build source string - var source = BuildRegistrationCallsFromMetadata(sampleMetadata); - context.AddSource($"ToolkitSampleRegistry.g.cs", source); - } - - static private string BuildRegistrationCallsFromMetadata(IEnumerable sampleMetadata) - { - return $@"// -namespace CommunityToolkit.Labs.Core; - -internal static class ToolkitSampleRegistry -{{ - public static System.Collections.Generic.IEnumerable<{nameof(ToolkitSampleMetadata)}> Execute() - {{ - { - string.Join("\n ", sampleMetadata.Select(MetadataToRegistryCall).ToArray()) - } - }} -}}"; - - static string MetadataToRegistryCall(ToolkitSampleRecord metadata) - { - var sampleOptionsParam = metadata.SampleOptionsAssemblyQualifiedName is null ? "null" : $"typeof({metadata.SampleOptionsAssemblyQualifiedName})"; - - return @$"yield return new {nameof(ToolkitSampleMetadata)}({nameof(ToolkitSampleCategory)}.{metadata.Category}, {nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}, ""{metadata.DisplayName}"", ""{metadata.Description}"", typeof({metadata.SampleAssemblyQualifiedName}), {sampleOptionsParam});"; - } - } - - private static bool IsToolkitSampleAttribute(AttributeData attr) - => attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(ToolkitSampleAttribute).FullName}"; - - private static bool IsToolkitSampleOptionsPaneAttribute(AttributeData attr) - => attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(ToolkitSampleOptionsPaneAttribute).FullName}"; - - /// - /// A new record must be used instead of using directly - /// because we cannot Type.GetType using the , - /// but we can safely generate a type reference in the final output using typeof(AssemblyQualifiedName). - /// - private sealed record ToolkitSampleRecord( - ToolkitSampleCategory Category, - ToolkitSampleSubcategory Subcategory, - string DisplayName, - string Description, - string SampleAssemblyQualifiedName, - string? SampleOptionsAssemblyQualifiedName); -} diff --git a/Common/CommunityToolkit.Labs.Shared/AppLoadingView.xaml.cs b/Common/CommunityToolkit.Labs.Shared/AppLoadingView.xaml.cs index 0b46418db..e6e4c4c18 100644 --- a/Common/CommunityToolkit.Labs.Shared/AppLoadingView.xaml.cs +++ b/Common/CommunityToolkit.Labs.Shared/AppLoadingView.xaml.cs @@ -1,20 +1,11 @@ -using CommunityToolkit.Labs.Core; -using CommunityToolkit.Labs.Core.Attributes; +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; +using CommunityToolkit.Labs.Core.SourceGenerators; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.InteropServices.WindowsRuntime; -using System.Threading.Tasks; -using Windows.Foundation; -using Windows.Foundation.Collections; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; namespace CommunityToolkit.Labs.Shared diff --git a/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems b/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems index 3b69c1b74..ddab2e8f7 100644 --- a/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems +++ b/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems @@ -21,10 +21,14 @@ AppLoadingView.xaml + + GeneratedSampleOptionsRenderer.xaml + MainPage.xaml - + + ToolkitSampleRenderer.xaml @@ -33,11 +37,15 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile - + Designer MSBuild:Compile diff --git a/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs b/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs index 86aee94cf..f927a891e 100644 --- a/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs +++ b/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs @@ -17,6 +17,8 @@ using NavigationViewItem = Microsoft.UI.Xaml.Controls.NavigationViewItem; using NavigationView = Microsoft.UI.Xaml.Controls.NavigationView; using NavigationViewSelectionChangedEventArgs = Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs; +using CommunityToolkit.Labs.Shared.Renderers; +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; namespace CommunityToolkit.Labs.Shared { diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs new file mode 100644 index 000000000..8a3b960bf --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs @@ -0,0 +1,26 @@ +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; +using System; +using System.Collections.Generic; +using System.Text; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace CommunityToolkit.Labs.Shared.Renderers +{ + /// + /// Selects a template for a given . + /// + internal class GeneratedSampleOptionTemplateSelector : DataTemplateSelector + { + public DataTemplate? BoolOptionTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + return item switch + { + ToolkitSampleBoolOptionMetadataViewModel => BoolOptionTemplate ?? base.SelectTemplateCore(item, container), + _ => base.SelectTemplateCore(item, container), + }; + } + } +} diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml new file mode 100644 index 000000000..19350d25b --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs new file mode 100644 index 000000000..22d117839 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs @@ -0,0 +1,43 @@ +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; + +namespace CommunityToolkit.Labs.Shared.Renderers +{ + public sealed partial class GeneratedSampleOptionsRenderer : UserControl + { + public GeneratedSampleOptionsRenderer() + { + this.InitializeComponent(); + } + + /// + /// The backing for . + /// + public static readonly DependencyProperty SampleOptionsProperty = + DependencyProperty.Register(nameof(SampleOptions), typeof(IEnumerable), typeof(GeneratedSampleOptionsRenderer), new PropertyMetadata(null)); + private readonly IToolkitSampleGeneratedOptionPropertyContainer _propertyContainer; + + /// + /// The sample options that are displayed to the user. + /// + public IEnumerable? SampleOptions + { + get => (IEnumerable?)GetValue(SampleOptionsProperty); + set => SetValue(SampleOptionsProperty, value); + } + } +} diff --git a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml b/Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml similarity index 95% rename from Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml rename to Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml index 290cef422..191e7c970 100644 --- a/Common/CommunityToolkit.Labs.Shared/ToolkitSampleRenderer.xaml +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml @@ -1,5 +1,5 @@ /// Handles the display of a single toolkit sample, its source code, and the options that control it. @@ -117,8 +118,24 @@ protected override async void OnNavigatedTo(NavigationEventArgs e) SampleControlInstance = (UIElement)Activator.CreateInstance(Metadata.SampleControlType); + // Custom control-based sample options. if (Metadata.SampleOptionsPaneType is not null) + { SampleOptionsPaneInstance = (UIElement)Activator.CreateInstance(Metadata.SampleOptionsPaneType, SampleControlInstance); + } + + // Source generater-based sample options + else if (SampleControlInstance is IToolkitSampleGeneratedOptionPropertyContainer propertyContainer) + { + // Pass the generated sample options to the displayed Control instance. + // Generated properties reference these in getters and setters. + propertyContainer.GeneratedPropertyMetadata = Metadata.GeneratedSampleOptions; + + SampleOptionsPaneInstance = new GeneratedSampleOptionsRenderer + { + SampleOptions = propertyContainer.GeneratedPropertyMetadata + }; + } } public static async Task GetMetadataFileContents(ToolkitSampleMetadata metadata, string fileExtension) diff --git a/Common/Labs.UnoLib.props b/Common/Labs.UnoLib.props index 8e31a9ecb..b4f390179 100644 --- a/Common/Labs.UnoLib.props +++ b/Common/Labs.UnoLib.props @@ -26,8 +26,10 @@ - + + diff --git a/Common/Labs.Uwp.props b/Common/Labs.Uwp.props index 67bc7dbff..8f03b95fa 100644 --- a/Common/Labs.Uwp.props +++ b/Common/Labs.Uwp.props @@ -148,9 +148,9 @@ - + {210476d6-42cc-4d01-b027-478145bea8fe} - CommunityToolkit.Labs.Core + CommunityToolkit.Labs.Core.SourceGenerators diff --git a/Common/Labs.Wasm.props b/Common/Labs.Wasm.props index 2254f8fcb..c58e49a6b 100644 --- a/Common/Labs.Wasm.props +++ b/Common/Labs.Wasm.props @@ -45,8 +45,7 @@ - + diff --git a/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj b/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj index 13539d88a..fc4e9fff7 100644 --- a/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj +++ b/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj @@ -17,6 +17,10 @@ + + {5cb6662f-590f-4250-a19d-e27fee9c2876} + CommunityToolkit.Labs.Core.SourceGenerators + {a14189c0-39a8-4fbe-bf86-a78a94654c48} CanvasLayout.Sample diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj b/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj index 411ac6d41..937314b20 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj @@ -27,7 +27,6 @@ - \ No newline at end of file diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml index 3d49437ec..2c1ee9169 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml @@ -7,7 +7,13 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - - - + + + + [ToolkitSample(id: nameof(SamplePage), "Canvas Layout", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "A canvas-like VirtualizingPanel for use in an ItemsControl")] + [ToolkitSampleBoolOption("IsShown", "Toggle visibility", true)] + [ToolkitSampleBoolOption("ButtonIsEnabled", "Enabled the button", false)] public sealed partial class SamplePage : Page { public SamplePage() diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs index 41b6c953a..e45093fea 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml.cs @@ -2,30 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Labs.Core; -using CommunityToolkit.Labs.Core.Attributes; +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; using Microsoft.UI.Xaml.Controls; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Markup; using Windows.UI.Xaml.Media; -using Windows.UI.Xaml.Navigation; // The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 namespace CanvasLayout.Sample.SampleOne { - [ToolkitSampleOptionsPane(sampleId: nameof(SamplePage))] + //[ToolkitSampleOptionsPane(sampleId: nameof(SamplePage))] public sealed partial class SamplePageOptions : UserControl { private readonly SamplePage _samplePage; diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs index ef43445fe..1f5997fde 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs @@ -2,22 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Labs.Core; -using CommunityToolkit.Labs.Core.Attributes; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; -using Windows.UI.Xaml; +using CommunityToolkit.Labs.Core.SourceGenerators; +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; -using Windows.UI.Xaml.Navigation; namespace CanvasLayout.Sample.SampleTwo { From 2238e520b73469b5a34dbe546a5c11e5dac5ecec Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Fri, 4 Feb 2022 22:43:29 +0000 Subject: [PATCH 13/26] Fixed missing WinAppSDK sample head in CanvasLayout project --- Labs/CanvasLayout/CanvasLayout.sln | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Labs/CanvasLayout/CanvasLayout.sln b/Labs/CanvasLayout/CanvasLayout.sln index 1aea57559..febbe0bd9 100644 --- a/Labs/CanvasLayout/CanvasLayout.sln +++ b/Labs/CanvasLayout/CanvasLayout.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Labs.Core" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platforms", "Platforms", "{E6AFDCD6-F59A-4EC4-BCA7-1E47A5A15751}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanvasLayout.WinAppSdk", "samples\CanvasLayout.WinAppSdk\CanvasLayout.WinAppSdk.csproj", "{70DF1194-D158-473E-B350-F630231FB328}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\..\Common\CommunityToolkit.Labs.Shared\CommunityToolkit.Labs.Shared.projitems*{3baac2da-7124-460e-a9a9-13138843cd57}*SharedItemsImports = 4 @@ -142,6 +144,36 @@ Global {21128E83-CA73-49E2-B8DA-552B9CE81A25}.Release|x64.Build.0 = Release|Any CPU {21128E83-CA73-49E2-B8DA-552B9CE81A25}.Release|x86.ActiveCfg = Release|Any CPU {21128E83-CA73-49E2-B8DA-552B9CE81A25}.Release|x86.Build.0 = Release|Any CPU + {70DF1194-D158-473E-B350-F630231FB328}.Debug|Any CPU.ActiveCfg = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|Any CPU.Build.0 = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|Any CPU.Deploy.0 = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|ARM.ActiveCfg = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|ARM.Build.0 = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|ARM.Deploy.0 = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|ARM64.ActiveCfg = Debug|arm64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|ARM64.Build.0 = Debug|arm64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|ARM64.Deploy.0 = Debug|arm64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|x64.ActiveCfg = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|x64.Build.0 = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|x64.Deploy.0 = Debug|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|x86.ActiveCfg = Debug|x86 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|x86.Build.0 = Debug|x86 + {70DF1194-D158-473E-B350-F630231FB328}.Debug|x86.Deploy.0 = Debug|x86 + {70DF1194-D158-473E-B350-F630231FB328}.Release|Any CPU.ActiveCfg = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|Any CPU.Build.0 = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|Any CPU.Deploy.0 = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|ARM.ActiveCfg = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|ARM.Build.0 = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|ARM.Deploy.0 = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|ARM64.ActiveCfg = Release|arm64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|ARM64.Build.0 = Release|arm64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|ARM64.Deploy.0 = Release|arm64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|x64.ActiveCfg = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|x64.Build.0 = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|x64.Deploy.0 = Release|x64 + {70DF1194-D158-473E-B350-F630231FB328}.Release|x86.ActiveCfg = Release|x86 + {70DF1194-D158-473E-B350-F630231FB328}.Release|x86.Build.0 = Release|x86 + {70DF1194-D158-473E-B350-F630231FB328}.Release|x86.Deploy.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -150,6 +182,7 @@ Global {3BAAC2DA-7124-460E-A9A9-13138843CD57} = {E6AFDCD6-F59A-4EC4-BCA7-1E47A5A15751} {6DC6B31C-D03C-4E53-A1A5-CAF51227440B} = {E6AFDCD6-F59A-4EC4-BCA7-1E47A5A15751} {21128E83-CA73-49E2-B8DA-552B9CE81A25} = {E6AFDCD6-F59A-4EC4-BCA7-1E47A5A15751} + {70DF1194-D158-473E-B350-F630231FB328} = {E6AFDCD6-F59A-4EC4-BCA7-1E47A5A15751} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6D11723C-7575-40DF-9BC9-300A09554B5D} From 6133bf049792bc23067d98d46256e66aa85a67a7 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Fri, 4 Feb 2022 17:59:02 -0600 Subject: [PATCH 14/26] Added title property to generated sample pane option --- .../ToolkitSampleBoolOptionAttribute.cs | 4 ++-- .../ToolkitSampleOptionBaseAttribute.cs | 8 +++++++- .../ToolkitSampleBoolOptionMetadataViewModel.cs | 17 ++++++++++++++++- .../ToolkitSampleMetadataGenerator.cs | 4 ++-- .../GeneratedSampleOptionsRenderer.xaml | 5 ++++- .../GeneratedSampleOptionsRenderer.xaml.cs | 3 ++- .../SampleOne/SamplePage.xaml.cs | 2 +- 7 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs index 1be844723..a03a15ace 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs @@ -16,8 +16,8 @@ public sealed class ToolkitSampleBoolOptionAttribute : ToolkitSampleOptionBaseAt /// /// Creates a new instance of . /// - public ToolkitSampleBoolOptionAttribute(string name, string label, bool defaultState) - : base(name, defaultState) + public ToolkitSampleBoolOptionAttribute(string name, string label, bool defaultState, string? title = null) + : base(name, defaultState, title) { Label = label; } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs index 9712c5858..d6c45ae9a 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs @@ -12,8 +12,9 @@ public abstract class ToolkitSampleOptionBaseAttribute : Attribute /// /// Creates a new instance of . /// - public ToolkitSampleOptionBaseAttribute(string name, object defaultState) + public ToolkitSampleOptionBaseAttribute(string name, object defaultState, string? title = null) { + Title = title; Name = name; DefaultState = defaultState; } @@ -28,6 +29,11 @@ public ToolkitSampleOptionBaseAttribute(string name, object defaultState) /// public object DefaultState { get; } + /// + /// A title to display on top of the option. + /// + public string? Title { get; } + /// /// The source generator-friendly type name used for casting. /// diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs index b7112c715..ff1639387 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs @@ -13,14 +13,16 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata public class ToolkitSampleBoolOptionMetadataViewModel : IToolkitSampleOptionViewModel { private string _label; + private string? _title; private object _value; /// /// Creates a new instance of . /// - public ToolkitSampleBoolOptionMetadataViewModel(string id, string label, bool defaultState) + public ToolkitSampleBoolOptionMetadataViewModel(string id, string label, bool defaultState, string? title = null) { Name = id; + _title = title; _label = label; _value = defaultState; } @@ -74,5 +76,18 @@ public string Label PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Label))); } } + + /// + /// A label to display along the boolean option. + /// + public string? Title + { + get => _title; + set + { + _title = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title))); + } + } } } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs index 22c2de618..b2cd3b382 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs @@ -157,14 +157,14 @@ private static string BuildNewGeneratedSampleOptionMetadataSource(ToolkitSampleO { if (baseAttribute is ToolkitSampleBoolOptionAttribute boolAttribute) { - return $@"new {typeof(ToolkitSampleBoolOptionMetadataViewModel).FullName}(""{boolAttribute.Name}"", ""{boolAttribute.Label}"", {boolAttribute.DefaultState.ToString().ToLower()})"; + return $@"new {typeof(ToolkitSampleBoolOptionMetadataViewModel).FullName}(id: ""{boolAttribute.Name}"", label: ""{boolAttribute.Label}"", defaultState: {boolAttribute.DefaultState.ToString().ToLower()}, title: ""{boolAttribute.Title}"")"; } throw new NotSupportedException($"Unsupported or unhandled type {baseAttribute.GetType()}."); } /// - /// Checks if a symbol's is or inherits from a type representing a XAML framework/ + /// Checks if a symbol's is or inherits from a type representing a XAML framework. /// /// if the is or inherits from a type representing a XAML framework. private static bool IsValidXamlControl(INamedTypeSymbol symbol) diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml index 19350d25b..f958181a8 100644 --- a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml @@ -14,7 +14,10 @@ - + + + + diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs index f85f174d2..1491c3689 100644 --- a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs @@ -40,7 +40,6 @@ public GeneratedSampleOptionsRenderer() /// public static readonly DependencyProperty SampleOptionsProperty = DependencyProperty.Register(nameof(SampleOptions), typeof(IEnumerable), typeof(GeneratedSampleOptionsRenderer), new PropertyMetadata(null)); - private readonly IToolkitSampleGeneratedOptionPropertyContainer _propertyContainer; /// /// The sample options that are displayed to the user. @@ -50,5 +49,7 @@ public IEnumerable? SampleOptions get => (IEnumerable?)GetValue(SampleOptionsProperty); set => SetValue(SampleOptionsProperty, value); } + + public static Visibility NullOrWhiteSpaceToVisibility(string? str) => string.IsNullOrWhiteSpace(str) ? Visibility.Collapsed : Visibility.Visible; } } diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs index c78d3784e..7763a1edf 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs @@ -38,8 +38,8 @@ namespace CanvasLayout.Sample.SampleOne /// An empty page that can be used on its own or navigated to within a Frame. /// [ToolkitSample(id: nameof(SamplePage), "Canvas Layout", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "A canvas-like VirtualizingPanel for use in an ItemsControl")] + [ToolkitSampleBoolOption("ButtonIsEnabled", "Enable the button", false, title: "Boolean tests")] [ToolkitSampleBoolOption("IsShown", "Toggle visibility", true)] - [ToolkitSampleBoolOption("ButtonIsEnabled", "Enabled the button", false)] public sealed partial class SamplePage : Page { public SamplePage() From e7d1506b01553f47536633501779c2a653cd4a35 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Mon, 7 Feb 2022 13:53:51 -0600 Subject: [PATCH 15/26] Added generated multi-choice option for sample options pane --- .../Attributes/MultiChoiceOption.cs | 16 ++++ ...ToolkitSampleMultiChoiceOptionAttribute.cs | 40 ++++++++++ .../ToolkitSampleOptionBaseAttribute.cs | 6 +- .../Metadata/IToolkitSampleOptionViewModel.cs | 2 +- ...oolkitSampleBoolOptionMetadataViewModel.cs | 4 +- ...ampleMultiChoiceOptionMetadataViewModel.cs | 77 +++++++++++++++++++ .../ToolkitSampleMetadataGenerator.cs | 31 ++++++-- .../ToolkitSampleOptionGenerator.cs | 7 +- .../GeneratedSampleOptionTemplateSelector.cs | 3 + .../GeneratedSampleOptionsRenderer.xaml | 21 ++++- .../SampleOne/SamplePage.xaml | 13 ++-- .../SampleOne/SamplePage.xaml.cs | 16 +++- 12 files changed, 213 insertions(+), 23 deletions(-) create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs new file mode 100644 index 000000000..7d047f870 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs @@ -0,0 +1,16 @@ +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +/// +/// An option used in and . +/// +/// A label shown to the user for this option. +/// The value passed to XAML when this option is selected. +public record MultiChoiceOption(string Label, string Value) +{ + public override string ToString() + { + return Label; + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs new file mode 100644 index 000000000..ad8895eff --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +/// +/// Represents a boolean sample option that the user can manipulate and the XAML can bind to. +/// +/// +/// Using this attribute will automatically generate a dependency property that you can bind to in XAML. +/// +[Conditional("COMMUNITYTOOLKIT_KEEP_SAMPLE_ATTRIBUTES")] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class ToolkitSampleMultiChoiceOptionAttribute : ToolkitSampleOptionBaseAttribute +{ + /// + /// Creates a new instance of . + /// + public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, string label, string value, string? title = null) + : base(bindingName, null, title) + { + Label = label; + Value = value; + } + + /// + /// The source generator-friendly type name used for casting. + /// + public override string TypeName { get; } = "string"; + + /// + /// The displayed text shown beside this option. + /// + public string Label { get; private set; } + + /// + /// The value to provide in XAML when this item is selected. + /// + public string Value { get; private set; } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs index d6c45ae9a..3f0bf0cbd 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs @@ -12,10 +12,10 @@ public abstract class ToolkitSampleOptionBaseAttribute : Attribute /// /// Creates a new instance of . /// - public ToolkitSampleOptionBaseAttribute(string name, object defaultState, string? title = null) + public ToolkitSampleOptionBaseAttribute(string bindingName, object? defaultState, string? title = null) { Title = title; - Name = name; + Name = bindingName; DefaultState = defaultState; } @@ -27,7 +27,7 @@ public ToolkitSampleOptionBaseAttribute(string name, object defaultState, string /// /// The default state. /// - public object DefaultState { get; } + public object? DefaultState { get; } /// /// A title to display on top of the option. diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs index b12252a46..61e06d80f 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs @@ -14,7 +14,7 @@ public interface IToolkitSampleOptionViewModel : INotifyPropertyChanged /// /// The current value. Bound in XAML. /// - public object Value { get; set; } + public object? Value { get; set; } /// /// A unique identifier name for this option. diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs index ff1639387..3e99a610e 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs @@ -54,12 +54,12 @@ public bool BoolValue /// /// The current boolean value. /// - public object Value + public object? Value { get => BoolValue; set { - BoolValue = (bool)value; + BoolValue = (bool)(value ?? false); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Name)); } } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs new file mode 100644 index 000000000..68ead3463 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using System.ComponentModel; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata +{ + /// + /// A metadata container for data defined in with INPC support. + /// + public class ToolkitSampleMultiChoiceOptionMetadataViewModel : IToolkitSampleOptionViewModel + { + private string? _title; + private object? _value; + + /// + /// Creates a new instance of . + /// + public ToolkitSampleMultiChoiceOptionMetadataViewModel(string name, MultiChoiceOption[] options, string? title = null) + { + Name = name; + Options = options; + _title = title; + _value = options[0].Value; + } + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// A unique identifier for this option. + /// + /// + /// Used by the sample system to match up to the original and the control that declared it. + /// + public string Name { get; } + + /// + /// The available options presented to the user. + /// + public MultiChoiceOption[] Options { get; } + + /// + /// The current boolean value. + /// + public object? Value + { + get => _value; + set + { + if (value is MultiChoiceOption op) + _value = op.Value; + else + _value = value; + + // Value is null when selection changes + if (value is not null) + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Name)); + } + } + + /// + /// A label to display along the boolean option. + /// + public string? Title + { + get => _title; + set + { + _title = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title))); + } + } + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs index b2cd3b382..6451c6c03 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs @@ -52,6 +52,9 @@ void Execute(IncrementalValuesProvider types) if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) return (x.Item1, (ToolkitSampleOptionBaseAttribute)boolOptionAttribute); + if (x.Item2.TryReconstructAs() is ToolkitSampleMultiChoiceOptionAttribute multiChoiceOptionAttribute) + return (x.Item1, (ToolkitSampleOptionBaseAttribute)multiChoiceOptionAttribute); + return default; }).Collect(); @@ -148,19 +151,37 @@ private static string MetadataToRegistryCall(ToolkitSampleRecord metadata) var categoryParam = $"{nameof(ToolkitSampleCategory)}.{metadata.Category}"; var subcategoryParam = $"{nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}"; var containingClassTypeParam = $"typeof({metadata.SampleAssemblyQualifiedName})"; - var generatedSampleOptionsParam = $"new {typeof(IToolkitSampleOptionViewModel).FullName}[] {{ {string.Join(", ", metadata.GeneratedSampleOptions?.Select(BuildNewGeneratedSampleOptionMetadataSource).ToArray())} }}"; + var generatedSampleOptionsParam = $"new {typeof(IToolkitSampleOptionViewModel).FullName}[] {{ {string.Join(", ", BuildNewGeneratedSampleOptionMetadataSource(metadata).ToArray())} }}"; return @$"yield return new {typeof(ToolkitSampleMetadata).FullName}({categoryParam}, {subcategoryParam}, ""{metadata.DisplayName}"", ""{metadata.Description}"", {containingClassTypeParam}, {sampleOptionsParam}, {generatedSampleOptionsParam});"; } - private static string BuildNewGeneratedSampleOptionMetadataSource(ToolkitSampleOptionBaseAttribute baseAttribute) + private static IEnumerable BuildNewGeneratedSampleOptionMetadataSource(ToolkitSampleRecord sample) { - if (baseAttribute is ToolkitSampleBoolOptionAttribute boolAttribute) + // Handle group-able items + var multiChoice = sample.GeneratedSampleOptions.Where(x => x is ToolkitSampleMultiChoiceOptionAttribute) + .Cast() + .GroupBy(x => x.Name); + + foreach (var item in multiChoice) { - return $@"new {typeof(ToolkitSampleBoolOptionMetadataViewModel).FullName}(id: ""{boolAttribute.Name}"", label: ""{boolAttribute.Label}"", defaultState: {boolAttribute.DefaultState.ToString().ToLower()}, title: ""{boolAttribute.Title}"")"; + yield return $@"new {typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel).FullName}(name: ""{item.Key}"", options: new[] {{ {string.Join(",", item.Select(x => $@"new {typeof(MultiChoiceOption).FullName}(""{x.Label}"", ""{x.Value}"")").ToArray())} }}, title: ""{item.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Title)).Title}"")"; } - throw new NotSupportedException($"Unsupported or unhandled type {baseAttribute.GetType()}."); + var remainingItems = sample.GeneratedSampleOptions?.Except(multiChoice.SelectMany(x => x)); + + // Handle non-grouped items + foreach (var item in remainingItems ?? Enumerable.Empty()) + { + if (item is ToolkitSampleBoolOptionAttribute boolAttribute) + { + yield return $@"new {typeof(ToolkitSampleBoolOptionMetadataViewModel).FullName}(id: ""{boolAttribute.Name}"", label: ""{boolAttribute.Label}"", defaultState: {boolAttribute.DefaultState?.ToString().ToLower()}, title: ""{boolAttribute.Title}"")"; + } + else + { + throw new NotSupportedException($"Unsupported or unhandled type {item.GetType()}."); + } + } } /// diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs index 609ea818e..78b7d3684 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs @@ -15,6 +15,7 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators [Generator] public class ToolkitSampleOptionGenerator : IIncrementalGenerator { + private readonly HashSet _handledPropertyNames = new(); private readonly HashSet _handledAttributes = new(); private readonly HashSet _handledContainingClasses = new(SymbolEqualityComparer.Default); @@ -37,6 +38,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) return (Attribute: (ToolkitSampleOptionBaseAttribute)boolOptionAttribute, ContainingClassSymbol: x.Item1, Type: typeof(ToolkitSampleBoolOptionMetadataViewModel)); + if (x.Item2.TryReconstructAs() is ToolkitSampleMultiChoiceOptionAttribute multiChoiceOptionAttribute) + return (Attribute: (ToolkitSampleOptionBaseAttribute)multiChoiceOptionAttribute, ContainingClassSymbol: x.Item1, Type: typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel)); + return default; }) .Where(x => x != default); @@ -60,7 +64,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var dependencyPropertySource = BuildProperty(data.ContainingClassSymbol, data.Attribute.Name, data.Attribute.TypeName, data.Type); - ctx.AddSource($"{data.ContainingClassSymbol}.Property.{data.Attribute.Name}.g", dependencyPropertySource); + if (_handledPropertyNames.Add(data.Attribute.Name)) + ctx.AddSource($"{data.ContainingClassSymbol}.Property.{data.Attribute.Name}.g", dependencyPropertySource); }); } diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs index 2289fb3ea..a133c1430 100644 --- a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs @@ -20,11 +20,14 @@ internal class GeneratedSampleOptionTemplateSelector : DataTemplateSelector { public DataTemplate? BoolOptionTemplate { get; set; } + public DataTemplate? MultiChoiceOptionTemplate { get; set; } + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) { return item switch { ToolkitSampleBoolOptionMetadataViewModel => BoolOptionTemplate ?? base.SelectTemplateCore(item, container), + ToolkitSampleMultiChoiceOptionMetadataViewModel => MultiChoiceOptionTemplate ?? base.SelectTemplateCore(item, container), _ => base.SelectTemplateCore(item, container), }; } diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml index f958181a8..dca478dd0 100644 --- a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml @@ -4,6 +4,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:CommunityToolkit.Labs.Shared.Renderers" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" + xmlns:attrs="using:CommunityToolkit.Labs.Core.SourceGenerators.Attributes" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:metadata="using:CommunityToolkit.Labs.Core.SourceGenerators.Metadata" d:DesignHeight="300" @@ -20,10 +22,25 @@ + + + + + + + + - - + + + + + diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml index 2c1ee9169..3b2e96227 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml @@ -8,12 +8,11 @@ mc:Ignorable="d"> - - - public static readonly DiagnosticDescriptor SamplePaneOptionWithDuplicateName = new( id: "TKSMPL0004", - title: $"Duplicate sample option delaration", + title: $"Duplicate sample option name", messageFormat: $"Cannot generate sample pane option with name {{0}} as the provided name is already in use by another sample option", category: typeof(ToolkitSampleMetadataGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: $"Cannot generate sample pane option when the provided name is used by another sample option."); + + /// + /// Gets a indicating a derived that contains a name which is already defined as a member in the attached class. + /// + /// Format: "Cannot generate sample pane options with name {0} the provided name is already defined as a member in the attached class". + /// + /// + public static readonly DiagnosticDescriptor SamplePaneOptionWithConflictingName = new( + id: "TKSMPL0005", + title: $"Conflicting sample option name", + messageFormat: $"Cannot generate sample pane option with name {{0}} as the provided name is already defined as a member in the attached class", + category: typeof(ToolkitSampleMetadataGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot generate sample pane option when the provided name is already defined as a member in the attached class."); } } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs index bd2dcc21b..f3d4ec70f 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs @@ -108,6 +108,12 @@ void Execute(IncrementalValuesProvider types) foreach (var item in generatedOptionsWithDuplicateName) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithDuplicateName, item.SelectMany(x => x.Item1.Locations).FirstOrDefault(), item.Key)); + // Check for generated options that conflict with an existing property name + var generatedOptionsWithConflictingPropertyNames = generatedOptionPropertyData.Where(x => GetAllMembers((INamedTypeSymbol)x.Item1).Any(y => x.Item2.Name == y.Name)); + + foreach (var item in generatedOptionsWithConflictingPropertyNames) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithConflictingName, item.Item1.Locations.FirstOrDefault(), item.Item2.Name)); + // Check for options pane attributes with no matching sample ID var optionsPaneAttributeWithMissingOrInvalidSampleId = optionsPaneAttribute.Where(x => !toolkitSampleAttributeData.Any(sample => sample.Attribute.Id == x.Item1?.SampleId)); @@ -215,4 +221,14 @@ private static bool IsValidXamlControl(INamedTypeSymbol symbol) return validInheritedSymbol != default && typeIsAccessible && !symbol.IsStatic; } + + private static IEnumerable GetAllMembers(INamedTypeSymbol symbol) + { + foreach (var item in symbol.GetMembers()) + yield return item; + + if (symbol.BaseType is not null) + foreach (var item in GetAllMembers(symbol.BaseType)) + yield return item; + } } diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml index 3b2e96227..3124bb558 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml @@ -11,8 +11,8 @@ diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs index fa069fdec..eb456446c 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs @@ -37,11 +37,11 @@ namespace CanvasLayout.Sample.SampleOne /// /// An empty page that can be used on its own or navigated to within a Frame. /// - [ToolkitSampleBoolOption("IsVisible", "IsVisible", true)] + [ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)] - [ToolkitSampleMultiChoiceOption("TextColor", label: "Teal", value: "#0ddc8c", title: "Text foreground")] - [ToolkitSampleMultiChoiceOption("TextColor", label: "Sand", value: "#e7a676")] - [ToolkitSampleMultiChoiceOption("TextColor", label: "Dull green", value: "#5d7577")] + [ToolkitSampleMultiChoiceOption("TextForeground", label: "Teal", value: "#0ddc8c", title: "Text foreground")] + [ToolkitSampleMultiChoiceOption("TextForeground", label: "Sand", value: "#e7a676")] + [ToolkitSampleMultiChoiceOption("TextForeground", label: "Dull green", value: "#5d7577")] [ToolkitSampleMultiChoiceOption("TextSize", label: "Small", value: "12", title: "Text size")] [ToolkitSampleMultiChoiceOption("TextSize", label: "Normal", value: "16")] From 72e2db3392e7aa3c6eacbd5bc86162086b2eb4ce Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Mon, 7 Feb 2022 18:15:12 -0600 Subject: [PATCH 18/26] Added multi-choice extra title diagnostic error + tests --- .../MetadataGenerator/SamplePaneOptions.cs | 24 +++- .../Diagnostics/DiagnosticDescriptors.cs | 15 ++ .../ToolkitSampleMetadataGenerator.cs | 135 ++++++++++++------ .../SampleOne/SamplePage.xaml.cs | 2 +- 4 files changed, 127 insertions(+), 49 deletions(-) diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs index 8daccd553..545e8ecfa 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs @@ -175,6 +175,28 @@ public partial class Sample2 VerifyGeneratedDiagnostics(source); } + [TestMethod] + public void PaneMultipleChoiceOptionWithMultipleTitles() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", label: ""Segoe UI"", value: ""Segoe UI"", title: ""Font"")] + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", label: ""Arial"", value: ""Arial"", title: ""Other font"")] + + [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] + public partial class Sample + {{ + }} + }}"; + + VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneMultiChoiceOptionWithMultipleTitles.Id); + } + /// /// Verifies the output of a source generator. /// @@ -218,7 +240,7 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() HashSet resultingIds = diagnostics.Select(diagnostic => diagnostic.Id).ToHashSet(); - Assert.IsTrue(resultingIds.SetEquals(diagnosticsIds)); + Assert.IsTrue(resultingIds.SetEquals(diagnosticsIds), $"Expected one of [{string.Join(", ", diagnosticsIds)}] diagnostic Ids. Got [{string.Join("", resultingIds)}]"); GC.KeepAlive(sampleAttributeType); } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 5220042c2..75cdfa2c8 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -85,5 +85,20 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: $"Cannot generate sample pane option when the provided name is already defined as a member in the attached class."); + + /// + /// Gets a indicating a that contains a title which is already defined in another . + /// + /// Format: "Cannot generate multiple choice sample pane option with title {{0}} as the title was defined multiple times". + /// + /// + public static readonly DiagnosticDescriptor SamplePaneMultiChoiceOptionWithMultipleTitles = new( + id: "TKSMPL0006", + title: $"Conflicting sample option name", + messageFormat: $"Cannot generate multiple choice sample pane option with title {{0}} as the title was defined multiple times", + category: typeof(ToolkitSampleMetadataGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot generate multiple choice sample pane option as the title was defined multiple times."); } } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs index f3d4ec70f..036ce6914 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs @@ -34,8 +34,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var referencedTypes = context.CompilationProvider .SelectMany((x, _) => x.SourceModule.ReferencedAssemblySymbols) .SelectMany((asm, _) => asm.GlobalNamespace.CrawlForAllNamedTypes()) - .Where(x => x.TypeKind == TypeKind.Class && x.CanBeReferencedByName)/* - .Where(IsValidXamlControl)*/ + .Where(x => x.TypeKind == TypeKind.Class && x.CanBeReferencedByName) + /*.Where(IsValidXamlControl)*/ .Select((x, _) => (ISymbol)x); Execute(classes); @@ -62,9 +62,7 @@ void Execute(IncrementalValuesProvider types) var toolkitSampleAttributeData = allAttributeData.Select(static (data, _) => { if (data.Item2.TryReconstructAs() is ToolkitSampleAttribute sampleAttribute) - { return (Attribute: sampleAttribute, AttachedQualifiedTypeName: data.Item1.ToString(), Symbol: data.Item1); - } return default; }).Collect(); @@ -84,41 +82,7 @@ void Execute(IncrementalValuesProvider types) var optionsPaneAttribute = data.Left.Left.Where(x => x != default).Distinct(); var generatedOptionPropertyData = data.Right.Where(x => x != default).Distinct(); - // Check for generated options which don't have a valid sample attribute - var generatedOptionsWithMissingSampleAttribute = generatedOptionPropertyData.Where(x => !toolkitSampleAttributeData.Any(sample => ReferenceEquals(sample.Symbol, x.Item1))); - - foreach (var item in generatedOptionsWithMissingSampleAttribute) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample, item.Item1.Locations.FirstOrDefault())); - - // Check for generated options with an empty or invalid name. - var generatedOptionsWithBadName = generatedOptionPropertyData.Where(x => string.IsNullOrWhiteSpace(x.Item2.Name) || // Must not be null or empty - !x.Item2.Name.Any(char.IsLetterOrDigit) || // Must be alphanumeric - x.Item2.Name.Any(char.IsWhiteSpace) || // Must not have whitespace - SyntaxFacts.GetKeywordKind(x.Item2.Name) != SyntaxKind.None); // Must not be a reserved keyword - - foreach (var item in generatedOptionsWithBadName) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithBadName, item.Item1.Locations.FirstOrDefault(), item.Item1.ToString())); - - // Check for generated options with duplicate names. - var generatedOptionsWithDuplicateName = generatedOptionPropertyData.GroupBy(x => x.Item1, SymbolEqualityComparer.Default) // Group by containing symbol (allow reuse across samples) - .SelectMany(y => y.GroupBy(x => x.Item2.Name) // In this symbol, group options by name. - .Where(x => x.Any(x => x.Item2 is not ToolkitSampleMultiChoiceOptionAttribute)) // Exclude Multichoice. - .Where(x => x.Count() > 1)); // Options grouped by name should only contain 1 item. - - foreach (var item in generatedOptionsWithDuplicateName) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithDuplicateName, item.SelectMany(x => x.Item1.Locations).FirstOrDefault(), item.Key)); - - // Check for generated options that conflict with an existing property name - var generatedOptionsWithConflictingPropertyNames = generatedOptionPropertyData.Where(x => GetAllMembers((INamedTypeSymbol)x.Item1).Any(y => x.Item2.Name == y.Name)); - - foreach (var item in generatedOptionsWithConflictingPropertyNames) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithConflictingName, item.Item1.Locations.FirstOrDefault(), item.Item2.Name)); - - // Check for options pane attributes with no matching sample ID - var optionsPaneAttributeWithMissingOrInvalidSampleId = optionsPaneAttribute.Where(x => !toolkitSampleAttributeData.Any(sample => sample.Attribute.Id == x.Item1?.SampleId)); - - foreach (var item in optionsPaneAttributeWithMissingOrInvalidSampleId) - ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.OptionsPaneAttributeWithMissingOrInvalidSampleId, item.Item2.Locations.FirstOrDefault())); + ReportDiagnostics(ctx, toolkitSampleAttributeData, optionsPaneAttribute, generatedOptionPropertyData); // Reconstruct sample metadata from attributes var sampleMetadata = toolkitSampleAttributeData @@ -144,6 +108,89 @@ void Execute(IncrementalValuesProvider types) } } + private static void ReportDiagnostics(SourceProductionContext ctx, + IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, + IEnumerable<(ToolkitSampleOptionsPaneAttribute?, ISymbol)> optionsPaneAttribute, + IEnumerable<(ISymbol, ToolkitSampleOptionBaseAttribute)> generatedOptionPropertyData) + { + ReportGeneratedOptionsPaneDiagnostics(ctx, toolkitSampleAttributeData, generatedOptionPropertyData); + ReportDiagnosticsForLinkedOptionsPane(ctx, toolkitSampleAttributeData, optionsPaneAttribute); + } + + private static void ReportDiagnosticsForLinkedOptionsPane(SourceProductionContext ctx, + IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, + IEnumerable<(ToolkitSampleOptionsPaneAttribute?, ISymbol)> optionsPaneAttribute) + { + // Check for options pane attributes with no matching sample ID + var optionsPaneAttributeWithMissingOrInvalidSampleId = optionsPaneAttribute.Where(x => !toolkitSampleAttributeData.Any(sample => sample.Attribute.Id == x.Item1?.SampleId)); + + foreach (var item in optionsPaneAttributeWithMissingOrInvalidSampleId) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.OptionsPaneAttributeWithMissingOrInvalidSampleId, item.Item2.Locations.FirstOrDefault())); + } + + private static void ReportGeneratedOptionsPaneDiagnostics(SourceProductionContext ctx, + IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, + IEnumerable<(ISymbol, ToolkitSampleOptionBaseAttribute)> generatedOptionPropertyData) + { + ReportGeneratedMultiChoiceOptionsPaneDiagnostics(ctx, generatedOptionPropertyData); + + // Check for generated options which don't have a valid sample attribute + var generatedOptionsWithMissingSampleAttribute = generatedOptionPropertyData.Where(x => !toolkitSampleAttributeData.Any(sample => ReferenceEquals(sample.Symbol, x.Item1))); + + foreach (var item in generatedOptionsWithMissingSampleAttribute) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample, item.Item1.Locations.FirstOrDefault())); + + // Check for generated options with an empty or invalid name. + var generatedOptionsWithBadName = generatedOptionPropertyData.Where(x => string.IsNullOrWhiteSpace(x.Item2.Name) || // Must not be null or empty + !x.Item2.Name.Any(char.IsLetterOrDigit) || // Must be alphanumeric + x.Item2.Name.Any(char.IsWhiteSpace) || // Must not have whitespace + SyntaxFacts.GetKeywordKind(x.Item2.Name) != SyntaxKind.None); // Must not be a reserved keyword + + foreach (var item in generatedOptionsWithBadName) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithBadName, item.Item1.Locations.FirstOrDefault(), item.Item1.ToString())); + + // Check for generated options with duplicate names. + var generatedOptionsWithDuplicateName = generatedOptionPropertyData.GroupBy(x => x.Item1, SymbolEqualityComparer.Default) // Group by containing symbol (allow reuse across samples) + .SelectMany(y => y.GroupBy(x => x.Item2.Name) // In this symbol, group options by name. + .Where(x => x.Any(x => x.Item2 is not ToolkitSampleMultiChoiceOptionAttribute)) // Exclude Multichoice. + .Where(x => x.Count() > 1)); // Options grouped by name should only contain 1 item. + + foreach (var item in generatedOptionsWithDuplicateName) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithDuplicateName, item.SelectMany(x => x.Item1.Locations).FirstOrDefault(), item.Key)); + + // Check for generated options that conflict with an existing property name + var generatedOptionsWithConflictingPropertyNames = generatedOptionPropertyData.Where(x => GetAllMembers((INamedTypeSymbol)x.Item1).Any(y => x.Item2.Name == y.Name)); + + foreach (var item in generatedOptionsWithConflictingPropertyNames) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithConflictingName, item.Item1.Locations.FirstOrDefault(), item.Item2.Name)); + } + + private static void ReportGeneratedMultiChoiceOptionsPaneDiagnostics(SourceProductionContext ctx, IEnumerable<(ISymbol, ToolkitSampleOptionBaseAttribute)> generatedOptionPropertyData) + { + var generatedMultipleChoiceOptionWithMultipleTitles = new List<(ISymbol, ToolkitSampleOptionBaseAttribute)>(); + + var multiChoiceOptionsGroupedBySymbol = generatedOptionPropertyData.GroupBy(x => x.Item1, SymbolEqualityComparer.Default) + .Where(x => x.Any(x => x.Item2 is ToolkitSampleMultiChoiceOptionAttribute)); + + foreach (var symbolGroup in multiChoiceOptionsGroupedBySymbol) + { + var optionsGroupedByName = symbolGroup.GroupBy(x => x.Item2.Name); + + foreach (var nameGroup in optionsGroupedByName) + { + var optionsGroupedByTitle = nameGroup.Where(x => !string.IsNullOrWhiteSpace(x.Item2?.Title)) + .GroupBy(x => x.Item2.Title) + .SelectMany(x => x); + + if (optionsGroupedByTitle.Count() > 1) + generatedMultipleChoiceOptionWithMultipleTitles.Add(optionsGroupedByTitle.First()); + } + } + + foreach (var item in generatedMultipleChoiceOptionWithMultipleTitles) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneMultiChoiceOptionWithMultipleTitles, item.Item1.Locations.FirstOrDefault(), item.Item2.Title)); + } + private static string BuildRegistrationCallsFromMetadata(IEnumerable sampleMetadata) { return $@"#nullable enable @@ -179,23 +226,17 @@ private static IEnumerable BuildNewGeneratedSampleOptionMetadataSource(T .GroupBy(x => x.Name); foreach (var item in multiChoice) - { - yield return $@"new {typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel).FullName}(name: ""{item.Key}"", options: new[] {{ {string.Join(",", item.Select(x => $@"new {typeof(MultiChoiceOption).FullName}(""{x.Label}"", ""{x.Value}"")").ToArray())} }}, title: ""{item.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Title)).Title}"")"; - } + yield return $@"new {typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel).FullName}(name: ""{item.Key}"", options: new[] {{ {string.Join(",", item.Select(x => $@"new {typeof(MultiChoiceOption).FullName}(""{x.Label}"", ""{x.Value}"")").ToArray())} }}, title: ""{item.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Title))?.Title}"")"; + // Handle non-grouped items var remainingItems = sample.GeneratedSampleOptions?.Except(multiChoice.SelectMany(x => x)); - // Handle non-grouped items foreach (var item in remainingItems ?? Enumerable.Empty()) { if (item is ToolkitSampleBoolOptionAttribute boolAttribute) - { yield return $@"new {typeof(ToolkitSampleBoolOptionMetadataViewModel).FullName}(id: ""{boolAttribute.Name}"", label: ""{boolAttribute.Label}"", defaultState: {boolAttribute.DefaultState?.ToString().ToLower()}, title: ""{boolAttribute.Title}"")"; - } else - { throw new NotSupportedException($"Unsupported or unhandled type {item.GetType()}."); - } } } diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs index eb456446c..b9e80ba2b 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs @@ -47,7 +47,7 @@ namespace CanvasLayout.Sample.SampleOne [ToolkitSampleMultiChoiceOption("TextSize", label: "Normal", value: "16")] [ToolkitSampleMultiChoiceOption("TextSize", label: "Big", value: "32")] - [ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Segoe UI", value: "Segoe UI", title: "Font")] + [ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Segoe UI", value: "Segoe UI")] [ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Arial", value: "Arial")] [ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Consolas", value: "Consolas")] From e4be348abf302cab0587d21c0ed04de4dd5c0bbf Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Mon, 7 Feb 2022 18:52:54 -0600 Subject: [PATCH 19/26] Cleanup, added remaining unit tests --- ...tions.cs => ToolkitSampleMetadataTests.cs} | 132 ++++++++++++++++-- .../Diagnostics/DiagnosticDescriptors.cs | 45 ++++++ .../ToolkitSampleMetadataGenerator.cs | 37 ++++- 3 files changed, 195 insertions(+), 19 deletions(-) rename Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/{MetadataGenerator/SamplePaneOptions.cs => ToolkitSampleMetadataTests.cs} (69%) diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/ToolkitSampleMetadataTests.cs similarity index 69% rename from Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs rename to Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/ToolkitSampleMetadataTests.cs index 545e8ecfa..863123c37 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/MetadataGenerator/SamplePaneOptions.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/ToolkitSampleMetadataTests.cs @@ -6,13 +6,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; using System.Linq; namespace CommunityToolkit.Labs.Core.SourceGenerators.Tests { [TestClass] - public class SamplePaneOptions + public class ToolkitSampleMetadataTests { [TestMethod] public void PaneOptionOnNonSample() @@ -24,9 +23,14 @@ public void PaneOptionOnNonSample() namespace MyApp { [ToolkitSampleBoolOption(""BindToMe"", ""Toggle visibility"", false)] - public partial class Sample + public partial class Sample : Windows.UI.Xaml.Controls.UserControl { } + } + + namespace Windows.UI.Xaml.Controls + { + public class UserControl { } }"; VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample.Id); @@ -47,9 +51,14 @@ namespace MyApp {{ [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] [ToolkitSampleBoolOption(""{name}"", ""Toggle visibility"", false)] - public partial class Sample + public partial class Sample : Windows.UI.Xaml.Controls.UserControl {{ }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} }}"; VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneOptionWithBadName.Id); @@ -67,10 +76,15 @@ namespace MyApp {{ [ToolkitSampleBoolOption(""IsVisible"", ""Toggle x"", false)] [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] - public partial class Sample + public partial class Sample : Windows.UI.Xaml.Controls.UserControl {{ public string IsVisible {{ get; set; }} }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} }}"; VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneOptionWithConflictingName.Id); @@ -92,10 +106,15 @@ public partial class Sample : Base {{ }} - public class Base + public class Base : Windows.UI.Xaml.Controls.UserControl {{ public string IsVisible {{ get; set; }} }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} }}"; VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneOptionWithConflictingName.Id); @@ -115,9 +134,14 @@ namespace MyApp [ToolkitSampleBoolOption(""test"", ""Toggle y"", false)] [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] - public partial class Sample + public partial class Sample : Windows.UI.Xaml.Controls.UserControl {{ }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} }}"; VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneOptionWithDuplicateName.Id); @@ -139,9 +163,14 @@ namespace MyApp [ToolkitSampleBoolOption(""test"", ""Toggle y"", false)] [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] - public partial class Sample + public partial class Sample : Windows.UI.Xaml.Controls.UserControl {{ }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} }}"; VerifyGeneratedDiagnostics(source); @@ -160,16 +189,21 @@ namespace MyApp [ToolkitSampleBoolOption(""test"", ""Toggle y"", false)] [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] - public partial class Sample + public partial class Sample : Windows.UI.Xaml.Controls.UserControl {{ }} [ToolkitSampleBoolOption(""test"", ""Toggle y"", false)] [ToolkitSample(id: nameof(Sample2), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] - public partial class Sample2 + public partial class Sample2 : Windows.UI.Xaml.Controls.UserControl {{ }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} }}"; VerifyGeneratedDiagnostics(source); @@ -189,14 +223,88 @@ namespace MyApp [ToolkitSampleMultiChoiceOption(""TextFontFamily"", label: ""Arial"", value: ""Arial"", title: ""Other font"")] [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] - public partial class Sample + public partial class Sample : Windows.UI.Xaml.Controls.UserControl {{ }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} }}"; VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneMultiChoiceOptionWithMultipleTitles.Id); } + [TestMethod] + public void SampleGeneratedOptionAttributeOnUnsupportedType() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", label: ""Segoe UI"", value: ""Segoe UI"", title: ""Font"")] + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", label: ""Arial"", value: ""Arial"")] + [ToolkitSampleBoolOption(""Test"", ""Toggle visibility"", false)] + public partial class Sample + {{ + }} + }}"; + + VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SampleGeneratedOptionAttributeOnUnsupportedType.Id, DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample.Id); + } + + [TestMethod] + public void SampleAttributeOnUnsupportedType() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] + public partial class Sample + {{ + }} + }}"; + + VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SampleAttributeOnUnsupportedType.Id); + } + + [TestMethod] + public void SampleOptionPaneAttributeOnUnsupportedType() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSampleOptionsPane(sampleId: nameof(Sample))] + public partial class SampleOptionsPane + {{ + }} + + [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + {{ + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SampleOptionPaneAttributeOnUnsupportedType.Id); + } + /// /// Verifies the output of a source generator. /// @@ -240,7 +348,7 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() HashSet resultingIds = diagnostics.Select(diagnostic => diagnostic.Id).ToHashSet(); - Assert.IsTrue(resultingIds.SetEquals(diagnosticsIds), $"Expected one of [{string.Join(", ", diagnosticsIds)}] diagnostic Ids. Got [{string.Join("", resultingIds)}]"); + Assert.IsTrue(resultingIds.SetEquals(diagnosticsIds), $"Expected one of [{string.Join(", ", diagnosticsIds)}] diagnostic Ids. Got [{string.Join(", ", resultingIds)}]"); GC.KeepAlive(sampleAttributeType); } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 75cdfa2c8..a0e240b8f 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -100,5 +100,50 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: $"Cannot generate multiple choice sample pane option as the title was defined multiple times."); + + /// + /// Gets a indicating a that was used on an unsupported type. + /// + /// Format: "Cannot generate sample metadata as the attribute was used on an unsupported type.". + /// + /// + public static readonly DiagnosticDescriptor SampleAttributeOnUnsupportedType = new( + id: "TKSMPL0007", + title: $"ToolkitSampleAttribute declared on an invalid type", + messageFormat: $"Cannot generate sample metadata as the attribute was used on an unsupported type", + category: typeof(ToolkitSampleMetadataGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot generate sample metadata as the attribute was used on an unsupported type."); + + /// + /// Gets a indicating a that was used on an unsupported type. + /// + /// Format: "Cannot generate options pane metadata as the attribute was used on an unsupported type.". + /// + /// + public static readonly DiagnosticDescriptor SampleOptionPaneAttributeOnUnsupportedType = new( + id: "TKSMPL0008", + title: $"Toolkit sample options pane declared on an invalid type", + messageFormat: $"Cannot generate options pane metadata as the attribute was used on an unsupported type", + category: typeof(ToolkitSampleMetadataGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot generate options pane metadata as the attribute was used on an unsupported type."); + + /// + /// Gets a indicating a derived that was used on an unsupported type. + /// + /// Format: "Cannot generate sample option metadata as the attribute was used on an unsupported type.". + /// + /// + public static readonly DiagnosticDescriptor SampleGeneratedOptionAttributeOnUnsupportedType = new( + id: "TKSMPL0009", + title: $"Toolkit sample option declared on an invalid type", + messageFormat: $"Cannot generate sample option metadata as the attribute was used on an unsupported type", + category: typeof(ToolkitSampleMetadataGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot generate sample option metadata as the attribute was used on an unsupported type."); } } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs index 036ce6914..3d34d2544 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs @@ -35,7 +35,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .SelectMany((x, _) => x.SourceModule.ReferencedAssemblySymbols) .SelectMany((asm, _) => asm.GlobalNamespace.CrawlForAllNamedTypes()) .Where(x => x.TypeKind == TypeKind.Class && x.CanBeReferencedByName) - /*.Where(IsValidXamlControl)*/ .Select((x, _) => (ISymbol)x); Execute(classes); @@ -46,7 +45,7 @@ void Execute(IncrementalValuesProvider types) // Get all attributes + the original type symbol. var allAttributeData = types.SelectMany(static (sym, _) => sym.GetAttributes().Select(x => (sym, x))); - // Get all generated pane option attributes + the original type symbol. + // Find and reconstruct generated pane option attributes + the original type symbol. var generatedPaneOptions = allAttributeData.Select(static (x, _) => { if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) @@ -58,7 +57,7 @@ void Execute(IncrementalValuesProvider types) return default; }).Collect(); - // Find and reconstruct relevant attributes (with pane options) + // Find and reconstruct sample attributes var toolkitSampleAttributeData = allAttributeData.Select(static (data, _) => { if (data.Item2.TryReconstructAs() is ToolkitSampleAttribute sampleAttribute) @@ -113,8 +112,32 @@ private static void ReportDiagnostics(SourceProductionContext ctx, IEnumerable<(ToolkitSampleOptionsPaneAttribute?, ISymbol)> optionsPaneAttribute, IEnumerable<(ISymbol, ToolkitSampleOptionBaseAttribute)> generatedOptionPropertyData) { - ReportGeneratedOptionsPaneDiagnostics(ctx, toolkitSampleAttributeData, generatedOptionPropertyData); + ReportDiagnosticsForInvalidAttributeUsage(ctx, toolkitSampleAttributeData, optionsPaneAttribute, generatedOptionPropertyData); ReportDiagnosticsForLinkedOptionsPane(ctx, toolkitSampleAttributeData, optionsPaneAttribute); + ReportDiagnosticsGeneratedOptionsPane(ctx, toolkitSampleAttributeData, generatedOptionPropertyData); + } + + private static void ReportDiagnosticsForInvalidAttributeUsage(SourceProductionContext ctx, + IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, + IEnumerable<(ToolkitSampleOptionsPaneAttribute?, ISymbol)> optionsPaneAttribute, + IEnumerable<(ISymbol, ToolkitSampleOptionBaseAttribute)> generatedOptionPropertyData) + { + var toolkitAttributesOnUnsupportedType = toolkitSampleAttributeData.Where(x => x.Symbol is not INamedTypeSymbol namedSym || !IsValidXamlControl(namedSym)); + var optionsAttributeOnUnsupportedType = optionsPaneAttribute.Where(x => x.Item2 is not INamedTypeSymbol namedSym || !IsValidXamlControl(namedSym)); + var generatedOptionAttributeOnUnsupportedType = generatedOptionPropertyData.Where(x => x.Item1 is not INamedTypeSymbol namedSym || !IsValidXamlControl(namedSym)); + + + foreach (var item in toolkitAttributesOnUnsupportedType) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SampleAttributeOnUnsupportedType, item.Symbol.Locations.FirstOrDefault())); + + + foreach (var item in optionsAttributeOnUnsupportedType) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SampleOptionPaneAttributeOnUnsupportedType, item.Item2.Locations.FirstOrDefault())); + + + foreach (var item in generatedOptionAttributeOnUnsupportedType) + ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SampleGeneratedOptionAttributeOnUnsupportedType, item.Item1.Locations.FirstOrDefault())); + } private static void ReportDiagnosticsForLinkedOptionsPane(SourceProductionContext ctx, @@ -128,7 +151,7 @@ private static void ReportDiagnosticsForLinkedOptionsPane(SourceProductionContex ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.OptionsPaneAttributeWithMissingOrInvalidSampleId, item.Item2.Locations.FirstOrDefault())); } - private static void ReportGeneratedOptionsPaneDiagnostics(SourceProductionContext ctx, + private static void ReportDiagnosticsGeneratedOptionsPane(SourceProductionContext ctx, IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, IEnumerable<(ISymbol, ToolkitSampleOptionBaseAttribute)> generatedOptionPropertyData) { @@ -255,8 +278,8 @@ private static bool IsValidXamlControl(INamedTypeSymbol symbol) // Recursively crawl the base types until either UserControl or Page is found. var validInheritedSymbol = symbol.CrawlBy(x => x?.BaseType, baseType => validNamespaceRoots.Any(x => $"{baseType}".StartsWith(x)) && - $"{baseType}".Contains(".UI.Xaml.Controls.") && - validSimpleTypeNames.Any(x => $"{baseType}".EndsWith(x))); + $"{baseType}".Contains(".UI.Xaml.Controls.") && + validSimpleTypeNames.Any(x => $"{baseType}".EndsWith(x))); var typeIsAccessible = symbol.DeclaredAccessibility == Accessibility.Public; From 9ec8cf444da8b0eb47677c5304b6f21f032be910 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Mon, 7 Feb 2022 19:18:57 -0600 Subject: [PATCH 20/26] Moved unused sample options pane to second sample --- .../CanvasLayout.Sample/SampleTwo/SamplePage2.xaml | 2 +- .../{SampleOne => SampleTwo}/SamplePageOptions.xaml | 2 +- .../SamplePageOptions.xaml.cs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) rename Labs/CanvasLayout/samples/CanvasLayout.Sample/{SampleOne => SampleTwo}/SamplePageOptions.xaml (95%) rename Labs/CanvasLayout/samples/CanvasLayout.Sample/{SampleOne => SampleTwo}/SamplePageOptions.xaml.cs (85%) diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml index 757359877..793463c30 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml @@ -10,6 +10,6 @@ d:DesignWidth="400"> - + diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml similarity index 95% rename from Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml rename to Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml index 17b7b4d69..12672f189 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePageOptions.xaml +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml @@ -1,5 +1,5 @@ Date: Mon, 7 Feb 2022 19:26:27 -0600 Subject: [PATCH 21/26] Fixed namespaces --- .../GeneratorExtensions.cs | 4 ++-- .../XamlNamedPropertyRelayGenerator.cs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs index 7f6c0409f..5c5f245bd 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs @@ -10,7 +10,7 @@ using System.Linq; using System.Text; -namespace CommunityToolkit.Labs.Core.Generators +namespace CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay { public static class GeneratorExtensions { @@ -24,7 +24,7 @@ public static IEnumerable CrawlForAllNamedTypes(this INamespac { if (member is INamespaceSymbol nestedNamespace) { - foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) + foreach (var item in nestedNamespace.CrawlForAllNamedTypes()) yield return item; } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs index 56d237196..f68cb1da0 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Labs.Core.Generators; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Generic; using System.Linq; -namespace CommunityToolkit.Labs.Core.SourceGenerators; +namespace CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay; [Generator] public class XamlNamedPropertyRelayGenerator : IIncrementalGenerator From 3cfa61519d9040503ce58a321e7cdcdf83de5f45 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Mon, 7 Feb 2022 19:42:53 -0600 Subject: [PATCH 22/26] Cleaning up comments --- .../Metadata/IToolkitSampleOptionViewModel.cs | 2 +- .../ToolkitSampleBoolOptionMetadataViewModel.cs | 9 +++++---- .../ToolkitSampleMultiChoiceOptionMetadataViewModel.cs | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs index 61e06d80f..3839b1655 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs @@ -12,7 +12,7 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata public interface IToolkitSampleOptionViewModel : INotifyPropertyChanged { /// - /// The current value. Bound in XAML. + /// The current value. This property is provided for binding all generated properties in XAML. /// public object? Value { get; set; } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs index 3e99a610e..f7ad66735 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs @@ -41,6 +41,9 @@ public ToolkitSampleBoolOptionMetadataViewModel(string id, string label, bool de /// /// The current boolean value. /// + /// + /// Provided to accomodate binding to a property that is a non-nullable . + /// public bool BoolValue { get => (bool)_value; @@ -51,9 +54,7 @@ public bool BoolValue } } - /// - /// The current boolean value. - /// + /// public object? Value { get => BoolValue; @@ -78,7 +79,7 @@ public string Label } /// - /// A label to display along the boolean option. + /// A title to display on top of the boolean option. /// public string? Title { diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs index 68ead3463..2c831fbfd 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs @@ -50,14 +50,14 @@ public object? Value get => _value; set { + // XAML converting a null value isn't supported for all types. + if (value is null) + return; + if (value is MultiChoiceOption op) _value = op.Value; - else - _value = value; - // Value is null when selection changes - if (value is not null) - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Name)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Name)); } } From 217787a6df3ff79bc3bfa12ea84a5888c36d765b Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 8 Feb 2022 15:44:12 -0600 Subject: [PATCH 23/26] Added generator summary --- .../XamlNamedPropertyRelayGenerator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs index f68cb1da0..6f6faf01c 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs @@ -9,6 +9,9 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay; +/// +/// Generates code that provides access to XAML elements with x:Name from code-behind by wrapping an instance of a control, without the need to use x:FieldProvider="public" directly in markup. +/// [Generator] public class XamlNamedPropertyRelayGenerator : IIncrementalGenerator { From 53e5fe515f5582104de40c50cf0043c7f45389c5 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Tue, 8 Feb 2022 17:29:53 -0600 Subject: [PATCH 24/26] Improved and expanded comments, minor cleanup. --- .../Attributes/MultiChoiceOption.cs | 7 +++- .../Attributes/ToolkitSampleAttribute.cs | 7 +++- .../ToolkitSampleBoolOptionAttribute.cs | 12 ++++--- ...ToolkitSampleMultiChoiceOptionAttribute.cs | 12 +++++-- .../ToolkitSampleOptionBaseAttribute.cs | 9 +++-- .../IGeneratedToolkitSampleOptionViewModel.cs | 36 +++++++++++++++++++ ...tSampleGeneratedOptionPropertyContainer.cs | 10 ++++-- .../Metadata/IToolkitSampleOptionViewModel.cs | 27 -------------- ...oolkitSampleBoolOptionMetadataViewModel.cs | 8 +++-- .../Metadata/ToolkitSampleMetadata.cs | 4 +-- ...ampleMultiChoiceOptionMetadataViewModel.cs | 4 +-- .../ToolkitSampleMetadataGenerator.cs | 2 +- .../ToolkitSampleOptionGenerator.cs | 4 +-- .../ToolkitSampleRecord.cs | 3 ++ .../GeneratedSampleOptionTemplateSelector.cs | 2 +- .../GeneratedSampleOptionsRenderer.xaml.cs | 21 ++++++++--- 16 files changed, 112 insertions(+), 56 deletions(-) create mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IGeneratedToolkitSampleOptionViewModel.cs delete mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs index 7d047f870..c706ad261 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs @@ -3,12 +3,17 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; /// -/// An option used in and . +/// Holds data for a multiple choice option. +/// Primarily used by . /// /// A label shown to the user for this option. /// The value passed to XAML when this option is selected. public record MultiChoiceOption(string Label, string Value) { + /// + /// The string has been overriden to display the label only, + /// especially so the data can be easily displayed in XAML without a custom template, converter or code behind. + /// public override string ToString() { return Label; diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleAttribute.cs index 8709c219e..91e9853a8 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleAttribute.cs @@ -13,6 +13,11 @@ public sealed class ToolkitSampleAttribute : Attribute /// /// Creates a new instance of . /// + /// A unique identifier for this sample, used by the sample system. + /// The display name for this sample page. + /// The category that this sample belongs to. + /// A more specific category within the provided . + /// A short description of this sample. public ToolkitSampleAttribute(string id, string displayName, ToolkitSampleCategory category, ToolkitSampleSubcategory subcategory, string description) { Id = id; @@ -43,7 +48,7 @@ public ToolkitSampleAttribute(string id, string displayName, ToolkitSampleCatego public ToolkitSampleSubcategory Subcategory { get; } /// - /// The description for this sample page. + /// A short description of this sample. /// public string Description { get; } } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs index a03a15ace..3eea97edd 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs @@ -7,7 +7,8 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; /// Represents a boolean sample option that the user can manipulate and the XAML can bind to. /// /// -/// Using this attribute will automatically generate a dependency property that you can bind to in XAML. +/// Using this attribute will automatically generate an -enabled property +/// that you can bind to in XAML, and displays an options pane alonside your sample which allows the user to manipulate the property. /// [Conditional("COMMUNITYTOOLKIT_KEEP_SAMPLE_ATTRIBUTES")] [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] @@ -16,8 +17,11 @@ public sealed class ToolkitSampleBoolOptionAttribute : ToolkitSampleOptionBaseAt /// /// Creates a new instance of . /// - public ToolkitSampleBoolOptionAttribute(string name, string label, bool defaultState, string? title = null) - : base(name, defaultState, title) + /// The name of the generated property, which you can bind to in XAML. + /// The initial value for the bound property. + /// A title to display on top of this option. + public ToolkitSampleBoolOptionAttribute(string bindingName, string label, bool defaultState, string? title = null) + : base(bindingName, defaultState, title) { Label = label; } @@ -25,7 +29,7 @@ public ToolkitSampleBoolOptionAttribute(string name, string label, bool defaultS /// /// The source generator-friendly type name used for casting. /// - public override string TypeName { get; } = "bool"; + internal override string TypeName { get; } = "bool"; /// /// A label to display along the boolean option. diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs index ad8895eff..9bc32037f 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs @@ -4,10 +4,12 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; /// -/// Represents a boolean sample option that the user can manipulate and the XAML can bind to. +/// Represents a boolean sample option. /// /// -/// Using this attribute will automatically generate a dependency property that you can bind to in XAML. +/// Using this attribute will automatically generate an -enabled property +/// that you can bind to in XAML, and displays an options pane alonside your sample which allows the user to manipulate the property. +/// /// [Conditional("COMMUNITYTOOLKIT_KEEP_SAMPLE_ATTRIBUTES")] [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] @@ -16,6 +18,10 @@ public sealed class ToolkitSampleMultiChoiceOptionAttribute : ToolkitSampleOptio /// /// Creates a new instance of . /// + /// The name of the generated property, which you can bind to in XAML. + /// The displayed text shown beside this option. + /// The value to provide in XAML when this item is selected. + /// A title to display on top of this option. public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, string label, string value, string? title = null) : base(bindingName, null, title) { @@ -26,7 +32,7 @@ public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, string label, /// /// The source generator-friendly type name used for casting. /// - public override string TypeName { get; } = "string"; + internal override string TypeName { get; } = "string"; /// /// The displayed text shown beside this option. diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs index 3f0bf0cbd..ab93643f3 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs @@ -4,14 +4,17 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; /// -/// Represents a sample option that the user can manipulate and the XAML can bind to. +/// Represents an abstraction of a sample option that the user can manipulate and the XAML can bind to. /// [Conditional("COMMUNITYTOOLKIT_KEEP_SAMPLE_ATTRIBUTES")] public abstract class ToolkitSampleOptionBaseAttribute : Attribute { /// - /// Creates a new instance of . + /// Creates a new instance of . /// + /// The name of the generated property, which you can bind to in XAML. + /// The initial value for the bound property. + /// A title to display on top of this option. public ToolkitSampleOptionBaseAttribute(string bindingName, object? defaultState, string? title = null) { Title = title; @@ -37,5 +40,5 @@ public ToolkitSampleOptionBaseAttribute(string bindingName, object? defaultState /// /// The source generator-friendly type name used for casting. /// - public abstract string TypeName { get; } + internal abstract string TypeName { get; } } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IGeneratedToolkitSampleOptionViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IGeneratedToolkitSampleOptionViewModel.cs new file mode 100644 index 000000000..9212f958a --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IGeneratedToolkitSampleOptionViewModel.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata +{ + /// + /// A common interface for all generated toolkit sample options. + /// Implementations of this interface are updated from the sample pane UI, and referenced by the generated property. + /// + /// + /// Must implement to notify when the user changes a value in the sample pane UI. + /// + /// However, the must be emitted as the changed property name when updates, so the + /// propogated IPNC event can notify the sample control of the change. + /// + public interface IGeneratedToolkitSampleOptionViewModel : INotifyPropertyChanged + { + /// + /// The current value. Can be updated by the user via the sample pane UI. + /// + /// A generated property's getter and setter directly references this value, making it available to bind to. + /// + public object? Value { get; set; } + + /// + /// A unique identifier name for this option. + /// + /// + /// Used by the sample system to match up to the original and the control that declared it. + /// + public string Name { get; } + } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs index 757319489..ac7c651ff 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs @@ -7,13 +7,17 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata { /// - /// Implementors of this class contain properties which were created by source generators, are bound to in XAML, and are manipulated from another source. + /// Implementors of this class contain one or more source-generated properties + /// which are bound to in the XAML of a toolkit sample + /// and manipulated from a data-generated options pane. /// public interface IToolkitSampleGeneratedOptionPropertyContainer { /// - /// Holds a reference to the backing ViewModels for all generated properties. + /// Holds a reference to all generated ViewModels that act + /// as a proxy between the current actual value and the + /// generated properties which consume them. /// - public IEnumerable? GeneratedPropertyMetadata { get; set; } + public IEnumerable? GeneratedPropertyMetadata { get; set; } } } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs deleted file mode 100644 index 3839b1655..000000000 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleOptionViewModel.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.ComponentModel; - -namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata -{ - /// - /// A common view model for any toolkit sample option. - /// - public interface IToolkitSampleOptionViewModel : INotifyPropertyChanged - { - /// - /// The current value. This property is provided for binding all generated properties in XAML. - /// - public object? Value { get; set; } - - /// - /// A unique identifier name for this option. - /// - /// - /// Used by the sample system to match up to the original and the control that declared it. - /// - public string Name { get; } - } -} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs index f7ad66735..a2117fe6d 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs @@ -8,9 +8,13 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata { /// - /// A metadata container for data defined in with INPC support. + /// An INPC-enabled metadata container for data defined in an . /// - public class ToolkitSampleBoolOptionMetadataViewModel : IToolkitSampleOptionViewModel + /// + /// Instances of these are generated by the and + /// provided to the app alongside the sample registration. + /// + public class ToolkitSampleBoolOptionMetadataViewModel : IGeneratedToolkitSampleOptionViewModel { private string _label; private string? _title; diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs index 0fd018adb..c1d787a5a 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs @@ -19,7 +19,7 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata; /// The control type for the sample page's options pane. /// Constructor should have exactly one parameter that can be assigned to the control type (). /// -/// The options that were declared alongside this sample, if any. +/// The generated sample options that were declared alongside this sample, if any. public sealed record ToolkitSampleMetadata( ToolkitSampleCategory Category, ToolkitSampleSubcategory Subcategory, @@ -27,4 +27,4 @@ public sealed record ToolkitSampleMetadata( string Description, Type SampleControlType, Type? SampleOptionsPaneType = null, - IEnumerable? GeneratedSampleOptions = null); + IEnumerable? GeneratedSampleOptions = null); diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs index 2c831fbfd..fc9f4d1e1 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMultiChoiceOptionMetadataViewModel.cs @@ -8,9 +8,9 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata { /// - /// A metadata container for data defined in with INPC support. + /// An INPC-enabled metadata container for data defined in an . /// - public class ToolkitSampleMultiChoiceOptionMetadataViewModel : IToolkitSampleOptionViewModel + public class ToolkitSampleMultiChoiceOptionMetadataViewModel : IGeneratedToolkitSampleOptionViewModel { private string? _title; private object? _value; diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs index 3d34d2544..22339b286 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs @@ -236,7 +236,7 @@ private static string MetadataToRegistryCall(ToolkitSampleRecord metadata) var categoryParam = $"{nameof(ToolkitSampleCategory)}.{metadata.Category}"; var subcategoryParam = $"{nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}"; var containingClassTypeParam = $"typeof({metadata.SampleAssemblyQualifiedName})"; - var generatedSampleOptionsParam = $"new {typeof(IToolkitSampleOptionViewModel).FullName}[] {{ {string.Join(", ", BuildNewGeneratedSampleOptionMetadataSource(metadata).ToArray())} }}"; + var generatedSampleOptionsParam = $"new {typeof(IGeneratedToolkitSampleOptionViewModel).FullName}[] {{ {string.Join(", ", BuildNewGeneratedSampleOptionMetadataSource(metadata).ToArray())} }}"; return @$"yield return new {typeof(ToolkitSampleMetadata).FullName}({categoryParam}, {subcategoryParam}, ""{metadata.DisplayName}"", ""{metadata.Description}"", {containingClassTypeParam}, {sampleOptionsParam}, {generatedSampleOptionsParam});"; } diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs index 78b7d3684..5739e6395 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs @@ -95,9 +95,9 @@ namespace {containingClassSymbol.ContainingNamespace} {{ public partial class {containingClassSymbol.Name} : {typeof(IToolkitSampleGeneratedOptionPropertyContainer).Namespace}.{nameof(IToolkitSampleGeneratedOptionPropertyContainer)} {{ - private IEnumerable<{typeof(IToolkitSampleOptionViewModel).FullName}>? _generatedPropertyMetadata; + private IEnumerable<{typeof(IGeneratedToolkitSampleOptionViewModel).FullName}>? _generatedPropertyMetadata; - public IEnumerable<{typeof(IToolkitSampleOptionViewModel).FullName}>? GeneratedPropertyMetadata + public IEnumerable<{typeof(IGeneratedToolkitSampleOptionViewModel).FullName}>? GeneratedPropertyMetadata {{ get => _generatedPropertyMetadata; set diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs index d3fa83a3a..d90f12a4d 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs @@ -9,6 +9,9 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators; public partial class ToolkitSampleMetadataGenerator { + /// + /// Used to hold interim data during the source generation process by . + /// /// /// A new record must be used instead of using directly /// because we cannot Type.GetType using the , diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs index a133c1430..7058f0d44 100644 --- a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs @@ -14,7 +14,7 @@ namespace CommunityToolkit.Labs.Shared.Renderers { /// - /// Selects a template for a given . + /// Selects a sample option template for the provided . /// internal class GeneratedSampleOptionTemplateSelector : DataTemplateSelector { diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs index 1491c3689..e59a0feef 100644 --- a/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs @@ -28,6 +28,19 @@ namespace CommunityToolkit.Labs.Shared.Renderers { + /// + /// Displays the provided for manipulation by the user. + /// + /// + /// Sample pages implement via source generators, + /// and are provided a reference to the same given to this control. + /// + /// When the user updates the , + /// a PropertyChanged event with the should be emitted. + /// + /// The sample page sees this property change event via the generated , + /// causing it to re-get the proxied . + /// public sealed partial class GeneratedSampleOptionsRenderer : UserControl { public GeneratedSampleOptionsRenderer() @@ -39,14 +52,14 @@ public GeneratedSampleOptionsRenderer() /// The backing for . /// public static readonly DependencyProperty SampleOptionsProperty = - DependencyProperty.Register(nameof(SampleOptions), typeof(IEnumerable), typeof(GeneratedSampleOptionsRenderer), new PropertyMetadata(null)); + DependencyProperty.Register(nameof(SampleOptions), typeof(IEnumerable), typeof(GeneratedSampleOptionsRenderer), new PropertyMetadata(null)); /// - /// The sample options that are displayed to the user. + /// The generated sample options that should be displayed to the user. /// - public IEnumerable? SampleOptions + public IEnumerable? SampleOptions { - get => (IEnumerable?)GetValue(SampleOptionsProperty); + get => (IEnumerable?)GetValue(SampleOptionsProperty); set => SetValue(SampleOptionsProperty, value); } From 16e0e7c861f05dbe08bbb3e47e66149738c91db8 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Thu, 17 Feb 2022 17:46:03 -0600 Subject: [PATCH 25/26] Added missing comment --- .../ToolkitSampleOptionGenerator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs index 5739e6395..f75579723 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs @@ -12,6 +12,10 @@ namespace CommunityToolkit.Labs.Core.SourceGenerators { + /// + /// For the generated sample pane options, this generator creates the backing properties needed for binding in the UI, + /// as well as implementing the for relaying data between the options pane and the generated property. + /// [Generator] public class ToolkitSampleOptionGenerator : IIncrementalGenerator { From c71694d1956de4603ccfb0e3d791044dae2773d7 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Thu, 17 Feb 2022 18:07:15 -0600 Subject: [PATCH 26/26] Removed extraneous extensions helpers --- .../GeneratorExtensions.cs | 122 ------------------ .../XamlNamedPropertyRelayGenerator.cs | 36 +++++- 2 files changed, 32 insertions(+), 126 deletions(-) delete mode 100644 Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs deleted file mode 100644 index 5c5f245bd..000000000 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/GeneratorExtensions.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Text; - -namespace CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay -{ - public static class GeneratorExtensions - { - /// - /// Crawls a namespace and all child namespaces for all contained types. - /// - /// A flattened enumerable of s. - public static IEnumerable CrawlForAllNamedTypes(this INamespaceSymbol namespaceSymbol) - { - foreach (var member in namespaceSymbol.GetMembers()) - { - if (member is INamespaceSymbol nestedNamespace) - { - foreach (var item in nestedNamespace.CrawlForAllNamedTypes()) - yield return item; - } - - if (member is INamedTypeSymbol typeSymbol) - yield return typeSymbol; - } - } - - /// - /// Crawls an object tree for nested properties of the same type and returns the first instance that matches the . - /// - /// - /// Does not filter against or return the object. - /// - public static T? CrawlBy(this T? root, Func selectPredicate, Func filterPredicate) - { - crawl: - var current = selectPredicate(root); - - if (filterPredicate(current)) - { - return current; - } - - if (current is null) - { - return default; - } - - root = current; - goto crawl; - } - - /// - /// Reconstructs an attribute instance as the given type. - /// - /// The attribute type to create. - /// The attribute data used to construct the instance of - public static T ReconstructAs(this AttributeData attributeData) - { - // Reconstructing the attribute instance provides some safety against changes to the attribute's constructor signature. - var attributeArgs = attributeData.ConstructorArguments.Select(PrepareParameterTypeForActivator).ToArray(); - return (T)Activator.CreateInstance(typeof(T), attributeArgs); - } - - - /// - /// Attempts to reconstruct an attribute instance as the given type, returning null if and are mismatched. - /// - /// The attribute type to create. - /// The attribute data used to construct the instance of - public static T? TryReconstructAs(this AttributeData attributeData) - where T : Attribute - { - var attributeMatchesType = attributeData.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(T).FullName}"; - - if (attributeMatchesType) - return attributeData.ReconstructAs(); - - return null; - } - - /// - /// Checks whether or not a given type symbol has a specified full name. - /// - /// The input instance to check. - /// The full name to check. - /// Whether has a full name equals to . - public static bool HasFullyQualifiedName(this ISymbol symbol, string name) - { - return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name; - } - - /// - /// Performs any data transforms needed for using as a parameter in . - /// - /// The 's was null. - public static object? PrepareParameterTypeForActivator(this TypedConstant parameterTypedConstant) - { - if (parameterTypedConstant.Type is null) - throw new ArgumentNullException(nameof(parameterTypedConstant.Type)); - - // Types prefixed with global:: do not work with Type.GetType and must be stripped away. - var assemblyQualifiedName = parameterTypedConstant.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); - - var argType = Type.GetType(assemblyQualifiedName); - - // Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance() - if (argType != null && parameterTypedConstant.Kind == TypedConstantKind.Enum) - return Enum.Parse(argType, parameterTypedConstant.Value?.ToString()); - - return parameterTypedConstant.Value; - } - } -} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs index 6f6faf01c..1eddeb81c 100644 --- a/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs @@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; using System.Collections.Generic; using System.Linq; @@ -115,10 +116,12 @@ public static bool IsDeclaredXamlXNameProperty(T? symbol) var validNamespaceRoots = new[] { "Microsoft", "Windows" }; // Recursively crawl the base types until either UserControl or Page is found. - var validInheritedSymbol = symbol.ContainingType - .CrawlBy(x => x?.BaseType, baseType => validNamespaceRoots.Any(x => $"{baseType}".StartsWith(x)) && - $"{baseType}".Contains(".UI.Xaml.Controls.") && - validSimpleTypeNames.Any(x => $"{baseType}".EndsWith(x))); + var validInheritedSymbol = CrawlBy( + symbol.ContainingType, + x => x?.BaseType, + baseType => validNamespaceRoots.Any(x => $"{baseType}".StartsWith(x)) && + $"{baseType}".Contains(".UI.Xaml.Controls.") && + validSimpleTypeNames.Any(x => $"{baseType}".EndsWith(x))); var containerIsPublic = symbol.ContainingType?.DeclaredAccessibility == Accessibility.Public; var isPrivate = symbol.DeclaredAccessibility == Accessibility.Private; @@ -126,5 +129,30 @@ public static bool IsDeclaredXamlXNameProperty(T? symbol) return validInheritedSymbol != default && isPrivate && containerIsPublic && typeIsAccessible && !symbol.IsStatic; } + + /// + /// Crawls an object tree for nested properties of the same type and returns the first instance that matches the . + /// + /// + /// Does not filter against or return the object. + /// + public static T? CrawlBy(T? root, Func selectPredicate, Func filterPredicate) + { + crawl: + var current = selectPredicate(root); + + if (filterPredicate(current)) + { + return current; + } + + if (current is null) + { + return default; + } + + root = current; + goto crawl; + } }