-
Notifications
You must be signed in to change notification settings - Fork 19
Initial implementation of the ToolkitSampleButtonAttribute #308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -181,6 +181,15 @@ private async Task LoadData() | |
|
|
||
| var sampleControlInstance = (UIElement)Metadata.SampleControlFactory(); | ||
|
|
||
| // Bind button commands to the sample instance so they can invoke methods via reflection. | ||
| if (Metadata.SampleButtons is not null) | ||
| { | ||
| foreach (var button in Metadata.SampleButtons) | ||
| { | ||
| button.BindToInstance(sampleControlInstance); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure how we can hook these together without the reflection encapsulated in here yet. Wanted to get it working first, will see if can adjust this aspect or not from here. I would posit we maybe need more of a wrapping class where we create commands based off the information, but I think this is maybe different than the @Arlodotexe let me know if you have thoughts, not sure if we can carve out any of Sergio's time to pick his brain on this; I know he's been pretty busy lately. I do think this whole process/interface for sample devs though is super valuable to make it dirt-simple to have these more complex interactions. |
||
| } | ||
| } | ||
|
|
||
| // Custom control-based sample options. | ||
| if (Metadata.SampleOptionsPaneType is not null && Metadata.SampleOptionsPaneFactory is not null) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,258 @@ | ||
| // 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.Tooling.SampleGen.Diagnostics; | ||
| using CommunityToolkit.Tooling.SampleGen.Tests.Helpers; | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
| using System.Collections.Immutable; | ||
| using System.ComponentModel.DataAnnotations; | ||
|
|
||
| namespace CommunityToolkit.Tooling.SampleGen.Tests; | ||
|
|
||
| public partial class ToolkitSampleMetadataTests | ||
| { | ||
| [TestMethod] | ||
| public void SampleButtonAttributeOnNonSample() | ||
| { | ||
| var source = """ | ||
| using System.ComponentModel; | ||
| using CommunityToolkit.Tooling.SampleGen; | ||
| using CommunityToolkit.Tooling.SampleGen.Attributes; | ||
|
|
||
| namespace MyApp | ||
| { | ||
| public partial class Sample : Windows.UI.Xaml.Controls.UserControl | ||
| { | ||
| [ToolkitSampleButton(Title = "Click Me")] | ||
| private void OnButtonClick() | ||
| { | ||
| } | ||
| } | ||
| } | ||
|
|
||
| namespace Windows.UI.Xaml.Controls | ||
| { | ||
| public class UserControl { } | ||
| } | ||
| """; | ||
|
|
||
| var result = source.RunSourceGenerator<ToolkitSampleMetadataGenerator>(SAMPLE_ASM_NAME); | ||
|
|
||
| result.AssertDiagnosticsAre(DiagnosticDescriptors.SampleButtonAttributeOnNonSample); | ||
| result.AssertNoCompilationErrors(); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void SampleButtonAttributeValid() | ||
| { | ||
| var source = """ | ||
| using System.ComponentModel; | ||
| using CommunityToolkit.Tooling.SampleGen; | ||
| using CommunityToolkit.Tooling.SampleGen.Attributes; | ||
|
|
||
| namespace MyApp | ||
| { | ||
| [ToolkitSample(id: nameof(Sample), "Test Sample", description: "")] | ||
| public partial class Sample : Windows.UI.Xaml.Controls.UserControl | ||
| { | ||
| [ToolkitSampleButton(Title = "Click Me")] | ||
| private void OnButtonClick() | ||
| { | ||
| } | ||
| } | ||
| } | ||
|
|
||
| namespace Windows.UI.Xaml.Controls | ||
| { | ||
| public class UserControl { } | ||
| } | ||
| """; | ||
|
|
||
| var result = source.RunSourceGenerator<ToolkitSampleMetadataGenerator>(SAMPLE_ASM_NAME); | ||
|
|
||
| result.AssertDiagnosticsAre(DiagnosticDescriptors.SampleNotReferencedInMarkdown); | ||
| result.AssertNoCompilationErrors(); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void SampleButtonAttributeMultipleButtons() | ||
| { | ||
| var source = """ | ||
| using System.ComponentModel; | ||
| using CommunityToolkit.Tooling.SampleGen; | ||
| using CommunityToolkit.Tooling.SampleGen.Attributes; | ||
|
|
||
| namespace MyApp | ||
| { | ||
| [ToolkitSample(id: nameof(Sample), "Test Sample", description: "")] | ||
| public partial class Sample : Windows.UI.Xaml.Controls.UserControl | ||
| { | ||
| [ToolkitSampleButton(Title = "Add Item")] | ||
| private void AddItemClick() | ||
| { | ||
| } | ||
|
|
||
| [ToolkitSampleButton(Title = "Clear Items")] | ||
| private void ClearItemsClick() | ||
| { | ||
| } | ||
| } | ||
| } | ||
|
|
||
| namespace Windows.UI.Xaml.Controls | ||
| { | ||
| public class UserControl { } | ||
| } | ||
| """; | ||
|
|
||
| var result = source.RunSourceGenerator<ToolkitSampleMetadataGenerator>(SAMPLE_ASM_NAME); | ||
|
|
||
| result.AssertDiagnosticsAre(DiagnosticDescriptors.SampleNotReferencedInMarkdown); | ||
| result.AssertNoCompilationErrors(); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void SampleButtonCommand_GeneratedRegistryExecutesMethod() | ||
| { | ||
| // The sample registry is designed to be declared in the sample project, | ||
| // and generated in the project head. To test end-to-end execution of the | ||
| // generated button commands, we replicate this setup, verify the generated | ||
| // registry source, then execute the same command against a matching instance. | ||
| var sampleSource = """ | ||
| using System.ComponentModel; | ||
| using CommunityToolkit.Tooling.SampleGen; | ||
| using CommunityToolkit.Tooling.SampleGen.Attributes; | ||
|
|
||
| namespace MyApp | ||
| { | ||
| [ToolkitSample(id: nameof(Sample), "Test Sample", description: "")] | ||
| public partial class Sample : Windows.UI.Xaml.Controls.UserControl | ||
| { | ||
| public int Counter { get; set; } | ||
|
|
||
| public Sample() | ||
| { | ||
| } | ||
|
|
||
| [ToolkitSampleButton(Title = "Increment")] | ||
| private void IncrementCounter() | ||
| { | ||
| Counter++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| namespace Windows.UI.Xaml.Controls | ||
| { | ||
| public class UserControl { } | ||
| } | ||
| """; | ||
|
|
||
| // Compile sample project as a metadata reference for the generator | ||
| var sampleProjectAssembly = sampleSource.ToSyntaxTree() | ||
| .CreateCompilation("MyApp.Samples") | ||
| .ToMetadataReference(); | ||
|
|
||
| // Create application head that references the sample project | ||
| var headCompilation = string.Empty | ||
| .ToSyntaxTree() | ||
| .CreateCompilation("MyApp.Head") | ||
| .AddReferences(sampleProjectAssembly); | ||
|
|
||
| // Run source generator to produce the registry | ||
| var result = headCompilation.RunSourceGenerator<ToolkitSampleMetadataGenerator>(); | ||
|
|
||
| result.AssertDiagnosticsAre(); | ||
| result.AssertNoCompilationErrors(); | ||
|
|
||
| // Verify the generated registry contains the expected button command | ||
| var registrySource = result.Compilation.GetFileContentsByName("ToolkitSampleRegistry.g.cs"); | ||
| StringAssert.Contains(registrySource, @"new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleButtonCommand(""Increment"", ""IncrementCounter"")"); | ||
|
|
||
| // Now verify the generated command mechanism works end-to-end | ||
| // by creating the same command the registry would and binding it to an instance | ||
| var testInstance = new SampleButtonCommandTestTarget(); | ||
| Assert.AreEqual(0, testInstance.Counter); | ||
|
|
||
| var button = new Metadata.ToolkitSampleButtonCommand("Increment", "IncrementCounter"); | ||
| button.BindToInstance(testInstance); | ||
|
|
||
| Assert.AreEqual("Increment", button.Title); | ||
| Assert.AreEqual("IncrementCounter", button.MethodName); | ||
| Assert.IsTrue(button.CanExecute(null!)); | ||
|
|
||
| // Execute and verify the counter was incremented | ||
| button.Execute(null!); | ||
| Assert.AreEqual(1, testInstance.Counter); | ||
|
|
||
| button.Execute(null!); | ||
| Assert.AreEqual(2, testInstance.Counter); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void SampleButtonCommand_AssemblyAttributeBridge() | ||
| { | ||
| // Verifies that the sample project emits assembly-level ToolkitSampleButtonDataAttribute | ||
| // and that the head project can read them to populate button commands in the registry. | ||
| // This tests the mechanism that bridges private method visibility across PE references. | ||
| var sampleSource = """ | ||
| using System.ComponentModel; | ||
| using CommunityToolkit.Tooling.SampleGen; | ||
| using CommunityToolkit.Tooling.SampleGen.Attributes; | ||
|
|
||
| namespace MyApp | ||
| { | ||
| [ToolkitSample(id: nameof(Sample), "Test Sample", description: "")] | ||
| public partial class Sample : Windows.UI.Xaml.Controls.UserControl | ||
| { | ||
| [ToolkitSampleButton(Title = "Increment")] | ||
| private void IncrementCounter() | ||
| { | ||
| } | ||
| } | ||
| } | ||
|
|
||
| namespace Windows.UI.Xaml.Controls | ||
| { | ||
| public class UserControl { } | ||
| } | ||
| """; | ||
|
|
||
| // Step 1: Run the generator on the sample project to produce assembly-level button attributes | ||
| var sampleResult = sampleSource.RunSourceGenerator<ToolkitSampleMetadataGenerator>(SAMPLE_ASM_NAME); | ||
| sampleResult.AssertNoCompilationErrors(); | ||
|
|
||
| // Verify the sample project generated the assembly-level button metadata | ||
| var buttonMetadataSource = sampleResult.Compilation.GetFileContentsByName("ToolkitSampleButtonMetadata.g.cs"); | ||
| StringAssert.Contains(buttonMetadataSource, @"ToolkitSampleButtonDataAttribute(""MyApp.Sample"", ""IncrementCounter"", ""Increment"")"); | ||
|
|
||
| // Step 2: Compile the sample project WITH the generated source and create a reference | ||
| var sampleWithGenerated = sampleResult.Compilation.ToMetadataReference(); | ||
|
|
||
| // Step 3: Run the generator on the head project | ||
| var headCompilation = string.Empty | ||
| .ToSyntaxTree() | ||
| .CreateCompilation("MyApp.Head") | ||
| .AddReferences(sampleWithGenerated); | ||
|
|
||
| var headResult = headCompilation.RunSourceGenerator<ToolkitSampleMetadataGenerator>(); | ||
|
|
||
| headResult.AssertDiagnosticsAre(); | ||
| headResult.AssertNoCompilationErrors(); | ||
|
|
||
| // Verify the head project's registry includes the button command | ||
| var registrySource = headResult.Compilation.GetFileContentsByName("ToolkitSampleRegistry.g.cs"); | ||
| StringAssert.Contains(registrySource, @"new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleButtonCommand(""Increment"", ""IncrementCounter"")"); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Mirrors the generated sample class structure for testing <see cref="Metadata.ToolkitSampleButtonCommand"/> execution. | ||
| /// </summary> | ||
| internal class SampleButtonCommandTestTarget | ||
| { | ||
| public int Counter { get; set; } | ||
|
|
||
| private void IncrementCounter() => Counter++; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // 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.Tooling.SampleGen.Attributes; | ||
|
|
||
| [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] | ||
| public class ToolkitSampleButtonAttribute : Attribute | ||
| { | ||
| /// <summary> | ||
| /// The title to display on the button. | ||
| /// </summary> | ||
| public string? Title { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The name of the method this attribute is attached to. | ||
| /// Set during source generation. | ||
| /// </summary> | ||
| public string? MethodName { get; set; } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed that we maybe move this to be underneath the sample and then aligned to the right like a commandbar style sort of thing. Will see if @niels9001 has any suggestions. I think at least underneath may look a lot better for the page/doc view when inlined with the text for sure.