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/ToolkitSampleMetadataTests.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/ToolkitSampleMetadataTests.cs new file mode 100644 index 000000000..863123c37 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.Tests/CommunityToolkit.Labs.Core.SourceGenerators.Tests/ToolkitSampleMetadataTests.cs @@ -0,0 +1,356 @@ +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.Linq; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Tests +{ + [TestClass] + public class ToolkitSampleMetadataTests + { + [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 : Windows.UI.Xaml.Controls.UserControl + { + } + } + + namespace Windows.UI.Xaml.Controls + { + public class UserControl { } + }"; + + 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 : Windows.UI.Xaml.Controls.UserControl + {{ + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + VerifyGeneratedDiagnostics(source, DiagnosticDescriptors.SamplePaneOptionWithBadName.Id); + } + + [TestMethod] + public void PaneOptionWithConflictingPropertyName() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""IsVisible"", ""Toggle x"", false)] + [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] + 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); + } + + [TestMethod] + public void PaneOptionWithConflictingInheritedPropertyName() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""IsVisible"", ""Toggle x"", false)] + [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] + public partial class Sample : 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); + } + + [TestMethod] + public void PaneOptionWithDuplicateName() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""test"", ""Toggle x"", false)] + [ToolkitSampleBoolOption(""test"", ""Toggle y"", false)] + + [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.SamplePaneOptionWithDuplicateName.Id); + } + + [TestMethod] + public void PaneOptionWithDuplicateName_AllowedForMultiChoice() + { + 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 y"", false)] + + [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); + } + + [TestMethod] + public void PaneOptionWithDuplicateName_AllowedBetweenSamples() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Labs.Core.SourceGenerators; + using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""test"", ""Toggle y"", false)] + + [ToolkitSample(id: nameof(Sample), ""Test Sample"", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: """")] + 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 : Windows.UI.Xaml.Controls.UserControl + {{ + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + 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 : 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. + /// + /// 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), $"Expected one of [{string.Join(", ", diagnosticsIds)}] diagnostic Ids. Got [{string.Join(", ", resultingIds)}]"); + + 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/XamlNamedPropertyRelayGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs new file mode 100644 index 000000000..1eddeb81c --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators.XamlNamedPropertyRelay/XamlNamedPropertyRelayGenerator.cs @@ -0,0 +1,158 @@ +// 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.Linq; + +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 +{ + private readonly HashSet _handledConstructors = new(SymbolEqualityComparer.Default); + + 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(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(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 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.Add(symbol.ContainingType)) + return; + + var source = $@"#nullable enable +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); + } + + /// + /// 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 = 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; + 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; + } + + /// + /// 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; + } +} + 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..c706ad261 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/MultiChoiceOption.cs @@ -0,0 +1,21 @@ +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +/// +/// 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 new file mode 100644 index 000000000..91e9853a8 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleAttribute.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; + +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 +{ + /// + /// 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; + DisplayName = displayName; + Description = description; + Category = category; + Subcategory = subcategory; + } + + /// + /// 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; } + + /// + /// A more specific category within the provided . + /// + public ToolkitSampleSubcategory Subcategory { get; } + + /// + /// 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 new file mode 100644 index 000000000..3eea97edd --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleBoolOptionAttribute.cs @@ -0,0 +1,38 @@ +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 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)] +public sealed class ToolkitSampleBoolOptionAttribute : ToolkitSampleOptionBaseAttribute +{ + /// + /// 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 ToolkitSampleBoolOptionAttribute(string bindingName, string label, bool defaultState, string? title = null) + : base(bindingName, defaultState, title) + { + Label = label; + } + + /// + /// The source generator-friendly type name used for casting. + /// + internal 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/ToolkitSampleMultiChoiceOptionAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs new file mode 100644 index 000000000..9bc32037f --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs @@ -0,0 +1,46 @@ +using System; +using System.Diagnostics; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +/// +/// Represents a boolean sample option. +/// +/// +/// 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)] +public sealed class ToolkitSampleMultiChoiceOptionAttribute : ToolkitSampleOptionBaseAttribute +{ + /// + /// 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) + { + Label = label; + Value = value; + } + + /// + /// The source generator-friendly type name used for casting. + /// + internal 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 new file mode 100644 index 000000000..ab93643f3 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionBaseAttribute.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +/// +/// 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 . + /// + /// 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; + Name = bindingName; + DefaultState = defaultState; + } + + /// + /// A name that you can bind to in your XAML. + /// + public string Name { get; } + + /// + /// The default state. + /// + 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. + /// + internal abstract string TypeName { get; } +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionsPaneAttribute.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionsPaneAttribute.cs new file mode 100644 index 000000000..f37164642 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Attributes/ToolkitSampleOptionsPaneAttribute.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics; + +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 +{ + /// + /// 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.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..a0e240b8f --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,149 @@ +// 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; + +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 contains an empty or invalid name.". + /// + /// + 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)}."); + + /// + /// Gets a indicating a derived that contains a name which is already in use by another sample option. + /// + /// Format: "Cannot generate sample pane options with name {0} as the provided name is already in use by another sample option". + /// + /// + public static readonly DiagnosticDescriptor SamplePaneOptionWithDuplicateName = new( + id: "TKSMPL0004", + 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."); + + /// + /// 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."); + + /// + /// 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/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/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 new file mode 100644 index 000000000..ac7c651ff --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/IToolkitSampleGeneratedOptionPropertyContainer.cs @@ -0,0 +1,23 @@ +// 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 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 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; } + } +} 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..a2117fe6d --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleBoolOptionMetadataViewModel.cs @@ -0,0 +1,98 @@ +// 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 +{ + /// + /// An INPC-enabled metadata container for data defined in an . + /// + /// + /// 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; + private object _value; + + /// + /// Creates a new instance of . + /// + public ToolkitSampleBoolOptionMetadataViewModel(string id, string label, bool defaultState, string? title = null) + { + Name = id; + _title = title; + _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. + /// + /// + /// Provided to accomodate binding to a property that is a non-nullable . + /// + public bool BoolValue + { + get => (bool)_value; + set + { + _value = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Name)); + } + } + + /// + public object? Value + { + get => BoolValue; + set + { + BoolValue = (bool)(value ?? false); + 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))); + } + } + + /// + /// A title to display on top of 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/Metadata/ToolkitSampleMetadata.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs new file mode 100644 index 000000000..c1d787a5a --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/Metadata/ToolkitSampleMetadata.cs @@ -0,0 +1,30 @@ +// 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; + +namespace CommunityToolkit.Labs.Core.SourceGenerators.Metadata; + +/// +/// 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 (). +/// +/// The generated sample 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, + IEnumerable? GeneratedSampleOptions = null); 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..fc9f4d1e1 --- /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 +{ + /// + /// An INPC-enabled metadata container for data defined in an . + /// + public class ToolkitSampleMultiChoiceOptionMetadataViewModel : IGeneratedToolkitSampleOptionViewModel + { + 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 + { + // XAML converting a null value isn't supported for all types. + if (value is null) + return; + + if (value is MultiChoiceOption op) + _value = op.Value; + + 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/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.SourceGenerators/ToolkitSampleCategory.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleCategory.cs new file mode 100644 index 000000000..072f99041 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleCategory.cs @@ -0,0 +1,16 @@ +// 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. + +namespace CommunityToolkit.Labs.Core.SourceGenerators; + +/// +/// The various categories each sample is organized into. +/// +public enum ToolkitSampleCategory : byte +{ + /// + /// Various UI controls that the user sees and interacts with. + /// + Controls, +} diff --git a/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs new file mode 100644 index 000000000..22339b286 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleMetadataGenerator.cs @@ -0,0 +1,298 @@ +// 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) + .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))); + + // Find and reconstruct 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); + + if (x.Item2.TryReconstructAs() is ToolkitSampleMultiChoiceOptionAttribute multiChoiceOptionAttribute) + return (x.Item1, (ToolkitSampleOptionBaseAttribute)multiChoiceOptionAttribute); + + return default; + }).Collect(); + + // Find and reconstruct sample attributes + 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(); + + ReportDiagnostics(ctx, toolkitSampleAttributeData, optionsPaneAttribute, generatedOptionPropertyData); + + // 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 void ReportDiagnostics(SourceProductionContext ctx, + IEnumerable<(ToolkitSampleAttribute Attribute, string AttachedQualifiedTypeName, ISymbol Symbol)> toolkitSampleAttributeData, + IEnumerable<(ToolkitSampleOptionsPaneAttribute?, ISymbol)> optionsPaneAttribute, + IEnumerable<(ISymbol, ToolkitSampleOptionBaseAttribute)> 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, + 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 ReportDiagnosticsGeneratedOptionsPane(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 +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(IGeneratedToolkitSampleOptionViewModel).FullName}[] {{ {string.Join(", ", BuildNewGeneratedSampleOptionMetadataSource(metadata).ToArray())} }}"; + + return @$"yield return new {typeof(ToolkitSampleMetadata).FullName}({categoryParam}, {subcategoryParam}, ""{metadata.DisplayName}"", ""{metadata.Description}"", {containingClassTypeParam}, {sampleOptionsParam}, {generatedSampleOptionsParam});"; + } + + private static IEnumerable BuildNewGeneratedSampleOptionMetadataSource(ToolkitSampleRecord sample) + { + // Handle group-able items + var multiChoice = sample.GeneratedSampleOptions.Where(x => x is ToolkitSampleMultiChoiceOptionAttribute) + .Cast() + .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}"")"; + + // Handle non-grouped items + var remainingItems = sample.GeneratedSampleOptions?.Except(multiChoice.SelectMany(x => x)); + + 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()}."); + } + } + + /// + /// 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; + } + + 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/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs new file mode 100644 index 000000000..f75579723 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleOptionGenerator.cs @@ -0,0 +1,159 @@ +// 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 +{ + /// + /// 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 + { + private readonly HashSet _handledPropertyNames = new(); + 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)); + + if (x.Item2.TryReconstructAs() is ToolkitSampleMultiChoiceOptionAttribute multiChoiceOptionAttribute) + return (Attribute: (ToolkitSampleOptionBaseAttribute)multiChoiceOptionAttribute, ContainingClassSymbol: x.Item1, Type: typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel)); + + 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); + + if (_handledPropertyNames.Add(data.Attribute.Name)) + 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(IGeneratedToolkitSampleOptionViewModel).FullName}>? _generatedPropertyMetadata; + + public IEnumerable<{typeof(IGeneratedToolkitSampleOptionViewModel).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..d90f12a4d --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleRecord.cs @@ -0,0 +1,28 @@ +// 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 +{ + /// + /// 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 , + /// 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.SourceGenerators/ToolkitSampleSubcategory.cs b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleSubcategory.cs new file mode 100644 index 000000000..c4191159b --- /dev/null +++ b/Common/CommunityToolkit.Labs.Core.SourceGenerators/ToolkitSampleSubcategory.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. + +namespace CommunityToolkit.Labs.Core.SourceGenerators; + +/// +/// The various subcategories used by samples. +/// +/// +/// Subcategories can be used by samples across multiple categories. +/// +public enum ToolkitSampleSubcategory : byte +{ + /// + /// No subcategory specified. + /// + None, + + /// + /// A sample that focuses on control layout. + /// + Layout, +} + diff --git a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs b/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs deleted file mode 100644 index 97f37a1c8..000000000 --- a/Common/CommunityToolkit.Labs.Core/Attributes/ToolkitSampleAttribute.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; - -namespace CommunityToolkit.Labs.Core.Attributes -{ - /// - /// When used on a class that derives from Page, that page is registered as a toolkit sample using the provided data. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public sealed class ToolkitSampleAttribute : Attribute - { - /// - /// Creates a new instance of . - /// - public ToolkitSampleAttribute(string displayName, ToolkitSampleCategory category, ToolkitSampleSubcategory subcategory, string description) - { - Category = category; - Subcategory = subcategory; - DisplayName = displayName; - Description = description; - } - - /// - /// The display name for this sample page. - /// - public string DisplayName { get; } - - /// - /// The category that this sample belongs to. - /// - public ToolkitSampleCategory Category { get; } - - /// - /// A more specific category within the provided . - /// - public ToolkitSampleSubcategory Subcategory { get; } - - /// - /// The description for this sample page. - /// - public string Description { get; } - } -} diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs b/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs deleted file mode 100644 index 622da5ea7..000000000 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleCategory.cs +++ /dev/null @@ -1,17 +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. - -namespace CommunityToolkit.Labs.Core -{ - /// - /// The various categories each sample is organized into. - /// - public enum ToolkitSampleCategory : byte - { - /// - /// Various UI controls that the user sees and interacts with. - /// - Controls, - } -} diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs b/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs deleted file mode 100644 index 12d7a1a86..000000000 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadata.cs +++ /dev/null @@ -1,51 +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; - -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; - } - - /// - /// 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; } - } -} diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs b/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs deleted file mode 100644 index 968501026..000000000 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleMetadataGenerator.cs +++ /dev/null @@ -1,130 +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 -{ - /// - /// 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 => 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)); - - // 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) - { - return @$"yield return new {nameof(ToolkitSampleMetadata)}({nameof(ToolkitSampleCategory)}.{metadata.Category}, {nameof(ToolkitSampleSubcategory)}.{metadata.Subcategory}, ""{metadata.DisplayName}"", ""{metadata.Description}"", typeof({metadata.AssemblyQualifiedName}));"; - } - } - - 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 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); - - var attachedTypeFullyQualifiedName = typeSymbol.ToString(); - - return new ToolkitSampleRecord(reconstructedAttribute.Category, - reconstructedAttribute.Subcategory, - reconstructedAttribute.DisplayName, - reconstructedAttribute.Description, - attachedTypeFullyQualifiedName); - } - - 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 , - /// 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); - } -} diff --git a/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs b/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs deleted file mode 100644 index af9afd8e2..000000000 --- a/Common/CommunityToolkit.Labs.Core/ToolkitSampleSubcategory.cs +++ /dev/null @@ -1,25 +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. - -namespace CommunityToolkit.Labs.Core -{ - /// - /// All the different subcategories used by samples. - /// - - // 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, - - /// - /// A sample that focuses on control layout. - /// - Layout, - } -} diff --git a/Common/CommunityToolkit.Labs.Shared/AppLoadingView.xaml.cs b/Common/CommunityToolkit.Labs.Shared/AppLoadingView.xaml.cs index f27cb2093..a84bc0b56 100644 --- a/Common/CommunityToolkit.Labs.Shared/AppLoadingView.xaml.cs +++ b/Common/CommunityToolkit.Labs.Shared/AppLoadingView.xaml.cs @@ -1,10 +1,8 @@ -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; @@ -14,10 +12,6 @@ using Windows.System; 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; #else using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; diff --git a/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems b/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems index c98a657af..ddab2e8f7 100644 --- a/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems +++ b/Common/CommunityToolkit.Labs.Shared/CommunityToolkit.Labs.Shared.projitems @@ -21,19 +21,34 @@ AppLoadingView.xaml + + GeneratedSampleOptionsRenderer.xaml + MainPage.xaml + + + ToolkitSampleRenderer.xaml + Designer MSBuild:Compile + + Designer + MSBuild:Compile + 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 c55a86c74..135401084 100644 --- a/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs +++ b/Common/CommunityToolkit.Labs.Shared/MainPage.xaml.cs @@ -29,6 +29,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 { @@ -42,26 +44,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; @@ -85,10 +72,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/Renderers/GeneratedSampleOptionTemplateSelector.cs b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs new file mode 100644 index 000000000..7058f0d44 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionTemplateSelector.cs @@ -0,0 +1,35 @@ +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; +using System; +using System.Collections.Generic; +using System.Text; + +#if WINAPPSDK +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +#else +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#endif + +namespace CommunityToolkit.Labs.Shared.Renderers +{ + /// + /// Selects a sample option template for the provided . + /// + 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 new file mode 100644 index 000000000..dca478dd0 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..e59a0feef --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml.cs @@ -0,0 +1,68 @@ +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 CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; + +#if WINAPPSDK +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +#else +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; +#endif + +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() + { + this.InitializeComponent(); + } + + /// + /// The backing for . + /// + public static readonly DependencyProperty SampleOptionsProperty = + DependencyProperty.Register(nameof(SampleOptions), typeof(IEnumerable), typeof(GeneratedSampleOptionsRenderer), new PropertyMetadata(null)); + + /// + /// The generated sample options that should be displayed to the user. + /// + 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/Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml b/Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml new file mode 100644 index 000000000..191e7c970 --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + 14 + + + + + + + + + + + + + + + + diff --git a/Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml.cs b/Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml.cs new file mode 100644 index 000000000..8b0cf38ee --- /dev/null +++ b/Common/CommunityToolkit.Labs.Shared/Renderers/ToolkitSampleRenderer.xaml.cs @@ -0,0 +1,186 @@ +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using CommunityToolkit.Labs.Core.SourceGenerators.Metadata; +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; +using Windows.Storage; + +#if WINAPPSDK +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +#else +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; +#endif + +namespace CommunityToolkit.Labs.Shared.Renderers +{ + /// + /// 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); + + // 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) + { + var filePath = GetPathToFileWithoutExtension(metadata.SampleControlType); + + try + { + 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) + { + 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/Common/Labs.MultiTarget.props b/Common/Labs.MultiTarget.props index 55e17b9f9..a5e5c7bfb 100644 --- a/Common/Labs.MultiTarget.props +++ b/Common/Labs.MultiTarget.props @@ -12,11 +12,14 @@ - - + + + + + @@ -26,4 +29,13 @@ + + + + + + + \ No newline at end of file diff --git a/Common/Labs.Uwp.props b/Common/Labs.Uwp.props index e0cec3606..1a227f5f6 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 165cf1f3a..af858a5db 100644 --- a/Common/Labs.Wasm.props +++ b/Common/Labs.Wasm.props @@ -45,7 +45,7 @@ - diff --git a/Common/Labs.WinAppSdk.props b/Common/Labs.WinAppSdk.props index e9b012f7e..d0e12e126 100644 --- a/Common/Labs.WinAppSdk.props +++ b/Common/Labs.WinAppSdk.props @@ -13,11 +13,17 @@ true WINAPPSDK - + + + - + + + + \ No newline at end of file diff --git a/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj b/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj index 27545b7cd..fc4e9fff7 100644 --- a/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj +++ b/CommunityToolkit.Labs.Uwp/CommunityToolkit.Labs.Uwp.csproj @@ -16,13 +16,15 @@ Designer - + + {5cb6662f-590f-4250-a19d-e27fee9c2876} + CommunityToolkit.Labs.Core.SourceGenerators + {a14189c0-39a8-4fbe-bf86-a78a94654c48} CanvasLayout.Sample - \ No newline at end of file diff --git a/CommunityToolkit.Labs.Uwp/Package.appxmanifest b/CommunityToolkit.Labs.Uwp/Package.appxmanifest index 07ed0faab..7cfaaa0cb 100644 --- a/CommunityToolkit.Labs.Uwp/Package.appxmanifest +++ b/CommunityToolkit.Labs.Uwp/Package.appxmanifest @@ -7,7 +7,7 @@ IgnorableNamespaces="uap mp"> diff --git a/CommunityToolkit.Labs.WinAppSdk/CommunityToolkit.Labs.WinAppSdk.csproj b/CommunityToolkit.Labs.WinAppSdk/CommunityToolkit.Labs.WinAppSdk.csproj index 9a134624e..bd459aa34 100644 --- a/CommunityToolkit.Labs.WinAppSdk/CommunityToolkit.Labs.WinAppSdk.csproj +++ b/CommunityToolkit.Labs.WinAppSdk/CommunityToolkit.Labs.WinAppSdk.csproj @@ -20,9 +20,9 @@ - + {210476d6-42cc-4d01-b027-478145bea8fe} - CommunityToolkit.Labs.Core + CommunityToolkit.Labs.Core.SourceGenerators diff --git a/Labs/CanvasLayout/CanvasLayout.sln b/Labs/CanvasLayout/CanvasLayout.sln index 1fa4e01e3..febbe0bd9 100644 --- a/Labs/CanvasLayout/CanvasLayout.sln +++ b/Labs/CanvasLayout/CanvasLayout.sln @@ -15,7 +15,7 @@ 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", "{D6A5ACBA-582D-4A52-9452-1D143CA4C51F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanvasLayout.WinAppSdk", "samples\CanvasLayout.WinAppSdk\CanvasLayout.WinAppSdk.csproj", "{70DF1194-D158-473E-B350-F630231FB328}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -144,36 +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 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|Any CPU.ActiveCfg = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|Any CPU.Build.0 = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|Any CPU.Deploy.0 = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|ARM.ActiveCfg = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|ARM.Build.0 = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|ARM.Deploy.0 = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|ARM64.ActiveCfg = Debug|arm64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|ARM64.Build.0 = Debug|arm64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|ARM64.Deploy.0 = Debug|arm64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|x64.ActiveCfg = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|x64.Build.0 = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|x64.Deploy.0 = Debug|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|x86.ActiveCfg = Debug|x86 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|x86.Build.0 = Debug|x86 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Debug|x86.Deploy.0 = Debug|x86 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|Any CPU.ActiveCfg = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|Any CPU.Build.0 = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|Any CPU.Deploy.0 = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|ARM.ActiveCfg = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|ARM.Build.0 = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|ARM.Deploy.0 = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|ARM64.ActiveCfg = Release|arm64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|ARM64.Build.0 = Release|arm64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|ARM64.Deploy.0 = Release|arm64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|x64.ActiveCfg = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|x64.Build.0 = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|x64.Deploy.0 = Release|x64 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|x86.ActiveCfg = Release|x86 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|x86.Build.0 = Release|x86 - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F}.Release|x86.Deploy.0 = Release|x86 + {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 @@ -182,7 +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} - {D6A5ACBA-582D-4A52-9452-1D143CA4C51F} = {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} diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj b/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj index 1fe429042..935bb389d 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/CanvasLayout.Sample.csproj @@ -10,16 +10,24 @@ + + + + + + + + - + WinUITarget=$(WinUITarget) diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml new file mode 100644 index 000000000..3124bb558 --- /dev/null +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml @@ -0,0 +1,18 @@ + + + + + + diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs new file mode 100644 index 000000000..b9e80ba2b --- /dev/null +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleOne/SamplePage.xaml.cs @@ -0,0 +1,62 @@ +// 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.IO; +using System.Linq; +using CommunityToolkit.Labs.Core.SourceGenerators; +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; +using System.Runtime.InteropServices.WindowsRuntime; + +#if !WINAPPSDK +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; +#else +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +#endif + +// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace CanvasLayout.Sample.SampleOne +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + [ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)] + + [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")] + [ToolkitSampleMultiChoiceOption("TextSize", label: "Big", value: "32")] + + [ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Segoe UI", value: "Segoe UI")] + [ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Arial", value: "Arial")] + [ToolkitSampleMultiChoiceOption("TextFontFamily", label: "Consolas", value: "Consolas")] + + [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() + { + this.InitializeComponent(); + } + } +} diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml deleted file mode 100644 index 89fc7ce05..000000000 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml.cs deleted file mode 100644 index 0204b7f0a..000000000 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage.xaml.cs +++ /dev/null @@ -1,41 +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 CommunityToolkit.Labs.Core; -using CommunityToolkit.Labs.Core.Attributes; - -#if !WINAPPSDK -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; -#else -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -#endif - -// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 - -namespace CanvasLayout.Sample -{ - /// - /// 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")] - public sealed partial class SamplePage : Page - { - public SamplePage() - { - this.InitializeComponent(); - } - } -} diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml.cs deleted file mode 100644 index e2c602da0..000000000 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml.cs +++ /dev/null @@ -1,43 +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 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; - -#if WINAPPSDK -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Navigation; -#else -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; -#endif - -namespace CanvasLayout.Sample -{ - [ToolkitSample("Example sample", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "An empty sample used to demonstrate the sample system.")] - public sealed partial class SamplePage2 : UserControl - { - public SamplePage2() - { - this.InitializeComponent(); - } - } -} diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml similarity index 73% rename from Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml rename to Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml index ebe9bba8d..793463c30 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SamplePage2.xaml +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml @@ -1,5 +1,5 @@ - + diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs new file mode 100644 index 000000000..c6ff1026d --- /dev/null +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePage2.xaml.cs @@ -0,0 +1,24 @@ +// 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; +using CommunityToolkit.Labs.Core.SourceGenerators.Attributes; + +#if WINAPPSDK +using Microsoft.UI.Xaml.Controls; +#else +using Windows.UI.Xaml.Controls; +#endif + +namespace CanvasLayout.Sample.SampleTwo +{ + [ToolkitSample(id: nameof(SamplePage2), "Example sample", ToolkitSampleCategory.Controls, ToolkitSampleSubcategory.Layout, description: "An empty sample used to demonstrate the sample system.")] + public sealed partial class SamplePage2 : UserControl + { + public SamplePage2() + { + this.InitializeComponent(); + } + } +} diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml new file mode 100644 index 000000000..12672f189 --- /dev/null +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + White + Green + Yellow + + + + + + + + + + + + diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml.cs b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml.cs new file mode 100644 index 000000000..89cbcb591 --- /dev/null +++ b/Labs/CanvasLayout/samples/CanvasLayout.Sample/SampleTwo/SamplePageOptions.xaml.cs @@ -0,0 +1,73 @@ +// 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 Microsoft.UI.Xaml.Controls; + +#if WINAPPSDK +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Markup; +using Microsoft.UI.Xaml.Media; +#else +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Markup; +using Windows.UI.Xaml.Media; +#endif + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace CanvasLayout.Sample.SampleTwo +{ + [ToolkitSampleOptionsPane(sampleId: nameof(SamplePage2))] + public sealed partial class SamplePageOptions : UserControl + { + private readonly SamplePage2 _samplePage; + private SamplePage2.XamlNamedPropertyRelay _xamlProperties; + + public SamplePageOptions(SamplePage2 samplePage) + { + Loaded += SamplePageOptions_Loaded; + + _samplePage = samplePage; + _xamlProperties = new SamplePage2.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; + } + } +} diff --git a/Labs/CanvasLayout/samples/CanvasLayout.Uwp/CanvasLayout.Uwp.csproj b/Labs/CanvasLayout/samples/CanvasLayout.Uwp/CanvasLayout.Uwp.csproj index bcf7c0f23..1271ecbac 100644 --- a/Labs/CanvasLayout/samples/CanvasLayout.Uwp/CanvasLayout.Uwp.csproj +++ b/Labs/CanvasLayout/samples/CanvasLayout.Uwp/CanvasLayout.Uwp.csproj @@ -21,7 +21,6 @@ {1c029cc0-bb45-45ff-ad34-086ec4840db0} CommunityToolkit.Labs.Uwp.UI.CanvasLayout - {a14189c0-39a8-4fbe-bf86-a78a94654c48} CanvasLayout.Sample diff --git a/Toolkit.Labs.All.sln b/Toolkit.Labs.All.sln index b73f78ac8..6db94c94e 100644 --- a/Toolkit.Labs.All.sln +++ b/Toolkit.Labs.All.sln @@ -32,11 +32,15 @@ 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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.WinAppSdk", "CommunityToolkit.Labs.WinAppSdk\CommunityToolkit.Labs.WinAppSdk.csproj", "{05AE3090-DAEE-4629-856B-120034CC4B1A}" +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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanvasLayout.WinAppSdk", "Labs\CanvasLayout\samples\CanvasLayout.WinAppSdk\CanvasLayout.WinAppSdk.csproj", "{3150A3C5-0DC3-4D6D-9598-1F767A7F323B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Labs.WinAppSdk", "CommunityToolkit.Labs.WinAppSdk\CommunityToolkit.Labs.WinAppSdk.csproj", "{53E592FC-E73C-474B-89C3-0570854CB160}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanvasLayout.WinAppSdk", "Labs\CanvasLayout\samples\CanvasLayout.WinAppSdk\CanvasLayout.WinAppSdk.csproj", "{25341845-BFEE-47E0-A6A9-16CADDF829C3}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -195,85 +199,126 @@ 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 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|Any CPU.ActiveCfg = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|Any CPU.Build.0 = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|Any CPU.Deploy.0 = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|ARM.ActiveCfg = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|ARM.Build.0 = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|ARM.Deploy.0 = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|ARM64.ActiveCfg = Debug|arm64 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|ARM64.Build.0 = Debug|arm64 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|ARM64.Deploy.0 = Debug|arm64 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|x64.ActiveCfg = Debug|x64 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|x64.Build.0 = Debug|x64 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|x64.Deploy.0 = Debug|x64 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|x86.ActiveCfg = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|x86.Build.0 = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Debug|x86.Deploy.0 = Debug|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|Any CPU.ActiveCfg = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|Any CPU.Build.0 = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|ARM.ActiveCfg = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|ARM.Build.0 = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|ARM.Deploy.0 = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|ARM64.ActiveCfg = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|ARM64.Build.0 = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|ARM64.Deploy.0 = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|x64.ActiveCfg = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|x64.Build.0 = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|x64.Deploy.0 = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|x86.ActiveCfg = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|x86.Build.0 = Release|x86 - {05AE3090-DAEE-4629-856B-120034CC4B1A}.Release|x86.Deploy.0 = Release|x86 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|Any CPU.ActiveCfg = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|Any CPU.Build.0 = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|Any CPU.Deploy.0 = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|ARM.ActiveCfg = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|ARM.Build.0 = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|ARM.Deploy.0 = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|ARM64.ActiveCfg = Debug|arm64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|ARM64.Build.0 = Debug|arm64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|ARM64.Deploy.0 = Debug|arm64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|x64.ActiveCfg = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|x64.Build.0 = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|x64.Deploy.0 = Debug|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|x86.ActiveCfg = Debug|x86 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|x86.Build.0 = Debug|x86 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Debug|x86.Deploy.0 = Debug|x86 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|Any CPU.ActiveCfg = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|Any CPU.Build.0 = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|Any CPU.Deploy.0 = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|ARM.ActiveCfg = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|ARM.Build.0 = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|ARM.Deploy.0 = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|ARM64.ActiveCfg = Release|arm64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|ARM64.Build.0 = Release|arm64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|ARM64.Deploy.0 = Release|arm64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|x64.ActiveCfg = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|x64.Build.0 = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|x64.Deploy.0 = Release|x64 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|x86.ActiveCfg = Release|x86 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|x86.Build.0 = Release|x86 - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B}.Release|x86.Deploy.0 = Release|x86 + {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 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|Any CPU.ActiveCfg = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|Any CPU.Build.0 = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|Any CPU.Deploy.0 = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|ARM.ActiveCfg = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|ARM.Build.0 = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|ARM.Deploy.0 = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|ARM64.ActiveCfg = Debug|arm64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|ARM64.Build.0 = Debug|arm64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|ARM64.Deploy.0 = Debug|arm64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|x64.ActiveCfg = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|x64.Build.0 = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|x64.Deploy.0 = Debug|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|x86.ActiveCfg = Debug|x86 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|x86.Build.0 = Debug|x86 + {53E592FC-E73C-474B-89C3-0570854CB160}.Debug|x86.Deploy.0 = Debug|x86 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|Any CPU.ActiveCfg = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|Any CPU.Build.0 = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|Any CPU.Deploy.0 = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|ARM.ActiveCfg = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|ARM.Build.0 = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|ARM.Deploy.0 = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|ARM64.ActiveCfg = Release|arm64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|ARM64.Build.0 = Release|arm64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|ARM64.Deploy.0 = Release|arm64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|x64.ActiveCfg = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|x64.Build.0 = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|x64.Deploy.0 = Release|x64 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|x86.ActiveCfg = Release|x86 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|x86.Build.0 = Release|x86 + {53E592FC-E73C-474B-89C3-0570854CB160}.Release|x86.Deploy.0 = Release|x86 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|Any CPU.ActiveCfg = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|Any CPU.Build.0 = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|Any CPU.Deploy.0 = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|ARM.ActiveCfg = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|ARM.Build.0 = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|ARM.Deploy.0 = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|ARM64.ActiveCfg = Debug|arm64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|ARM64.Build.0 = Debug|arm64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|ARM64.Deploy.0 = Debug|arm64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|x64.ActiveCfg = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|x64.Build.0 = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|x64.Deploy.0 = Debug|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|x86.ActiveCfg = Debug|x86 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|x86.Build.0 = Debug|x86 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Debug|x86.Deploy.0 = Debug|x86 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|Any CPU.ActiveCfg = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|Any CPU.Build.0 = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|Any CPU.Deploy.0 = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|ARM.ActiveCfg = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|ARM.Build.0 = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|ARM.Deploy.0 = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|ARM64.ActiveCfg = Release|arm64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|ARM64.Build.0 = Release|arm64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|ARM64.Deploy.0 = Release|arm64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|x64.ActiveCfg = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|x64.Build.0 = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|x64.Deploy.0 = Release|x64 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|x86.ActiveCfg = Release|x86 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|x86.Build.0 = Release|x86 + {25341845-BFEE-47E0-A6A9-16CADDF829C3}.Release|x86.Deploy.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -285,8 +330,10 @@ 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} - {3150A3C5-0DC3-4D6D-9598-1F767A7F323B} = {86E3CC4D-1359-4249-9C5B-C193BF65633D} + {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} + {25341845-BFEE-47E0-A6A9-16CADDF829C3} = {86E3CC4D-1359-4249-9C5B-C193BF65633D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1F0A4823-84EF-41AA-BBF9-A07B38DDC555}