diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..4b3c5740
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+# The sample index is generated, byte-compared in CI, and regenerated by contributors on
+# whichever OS they happen to use. Checking it out with native line endings would make the
+# working copy differ from the committed bytes on Windows, so it is kept as LF everywhere.
+catalog/*.json text eol=lf
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 32b857d4..cb4ad4dd 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -54,6 +54,27 @@ jobs:
- name: Check XAML Styling
run: powershell -version 5.1 -command "./ApplyXamlStyling.ps1 -Passive" -ErrorAction Stop
+ # Verifies the published sample index still describes the samples in this branch. Runs on
+ # Linux and needs no workloads, because the exporter reads the samples as text rather than
+ # building them — so it reports a stale index in under a minute instead of after the matrix.
+ Sample-Index:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Install .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ global-json-file: global.json
+
+ - name: Check the sample index is up to date
+ run: dotnet run --project tools/SampleIndexExporter -- check
+
+ - name: Run sample index tests
+ run: dotnet test tools/SampleIndexExporter.Tests
+
# Build both Uno.UI/WinUI2/UWP and Uno.WinUI/WinUI3/WindowsAppSDK versions of our packages using a matrix
build:
needs: [Xaml-Style-Check]
diff --git a/catalog/README.md b/catalog/README.md
new file mode 100644
index 00000000..a274e412
--- /dev/null
+++ b/catalog/README.md
@@ -0,0 +1,74 @@
+# Sample index
+
+`toolkit-samples.json` is a machine-readable index of every sample in this repository. It exists so that tools outside the Toolkit — documentation sites, search, AI coding assistants — can offer Toolkit samples without scraping the repository and guessing at its layout.
+
+The file is generated. Do not edit it by hand.
+
+## What is in it
+
+One entry per documentation page under `components/*/samples/`, and one sample per `[!SAMPLE]` marker in that page, in the order the page presents them.
+
+```jsonc
+{
+ "schemaVersion": 1,
+ "source": "toolkit",
+ "controls": [
+ {
+ "id": "settingscard",
+ "name": "SettingsCard",
+ "description": "A card control that can be used to create Windows 11 style settings experiences.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.SettingsControls",
+ "curatedKeywords": ["SettingsCard", "Control", "Layout", "Settings"],
+ "usings": ["System.ComponentModel"],
+ "docs": [{ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/..." }],
+ "samples": [
+ {
+ "header": "SettingsCard",
+ "xaml": "…",
+ "xmlnsImports": ["xmlns:controls=\"using:CommunityToolkit.WinUI.Controls\""],
+ "toolkit": { "sampleId": "SettingsCardSample", "sourcePath": "components/…/SettingsCardSample.xaml" }
+ }
+ ]
+ }
+ ]
+}
+```
+
+Pages that document APIs with no markup to show — most of `Extensions` and `Helpers` — appear with an empty `samples` array, so the index carries the whole component surface rather than only the parts that happen to have XAML.
+
+Everything specific to this repository lives under a `toolkit` object, leaving the rest of each entry portable across sample sources.
+
+## The XAML is meant to be pasted
+
+Each `xaml` value is the sample's markup with the sample app removed from it:
+
+- The `` wrapper and its `x:Class` are gone, along with design-time namespaces.
+- Options that the sample app renders as sliders and toggles are replaced by the value the app starts with, so the snippet shows the sample in the state the gallery opens it in. An option whose value cannot be written as a literal has its attribute removed instead of guessed at, which leaves the control at its own default.
+- `` moves onto the element that survives, so referenced keys stay in scope.
+- Conditional `win:` prefixes are dropped, since on Windows they name the same elements as no prefix at all.
+- `xmlnsImports` lists only the namespace declarations the snippet actually uses.
+
+What changes is the environment, never what the sample demonstrates. A sample whose markup cannot be made pasteable is left out and reported rather than published broken.
+
+## The C# is the sample, not the page
+
+A sample that has code-behind also carries a `code` value: the handlers its markup calls and the types its markup binds to, as members to drop into a page. The license header, the sample app's namespace, the page class, the `[ToolkitSample…]` attributes and the `InitializeComponent` call are all scaffolding for an app the reader is not building, so none of them appear. Conditional branches are resolved for WinAppSDK, so the reader is not handed a choice that has already been made.
+
+Most samples have nothing left once that is removed, and those publish no `code` at all rather than a constructor that says nothing. A constructor that does something the sample needs is kept, named after the sample — rename it to your own page, the same adaptation the markup's `x:Class` already asks for.
+
+The namespaces that code needs are published once per entry, as `usings`, rather than repeated as `using` lines inside every snippet — consumers prepend them. The list is narrowed to what the published members actually use: a sample file imports whatever its whole page needed, and most of that page is scaffolding nobody is handed, so publishing the file's imports verbatim would ask you to reference packages for code you never got. Namespaces that exist only in this repository's sample app are never published, since they would not resolve anywhere a snippet is pasted.
+
+## Regenerating
+
+```shell
+dotnet run --project tools/SampleIndexExporter -- generate
+```
+
+Commit the result alongside the sample change that caused it. CI runs the same tool in `check` mode and fails if the committed file no longer matches the samples, so an index that drifts is caught in the pull request that caused the drift.
+
+```shell
+dotnet run --project tools/SampleIndexExporter -- check
+dotnet test tools/SampleIndexExporter.Tests
+```
+
+The tests are the guarantees consumers rely on: the committed file matches the samples, generation is deterministic, every published snippet parses and declares the prefixes and namespaces it uses, and anything excluded is listed by name rather than disappearing quietly.
diff --git a/catalog/toolkit-samples.json b/catalog/toolkit-samples.json
new file mode 100644
index 00000000..da6f587d
--- /dev/null
+++ b/catalog/toolkit-samples.json
@@ -0,0 +1,4146 @@
+{
+ "schemaVersion": 1,
+ "source": "toolkit",
+ "controls": [
+ {
+ "id": "animations",
+ "name": "Implicit Animations",
+ "description": "A collection of implicit Composition animations that can be grouped together",
+ "nugetPackage": "CommunityToolkit.WinUI.Animations",
+ "usings": [
+ "Microsoft.UI.Xaml.Hosting",
+ "System.Numerics"
+ ],
+ "curatedKeywords": [
+ "Animations",
+ "Effects",
+ "Layout",
+ "Composition",
+ "animationset"
+ ],
+ "keywords": [
+ "Animations",
+ "Effects"
+ ],
+ "docs": [
+ {
+ "title": "Implicit Animations",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Animations/samples/Animations.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Animations",
+ "documentPath": "components/Animations/samples/Animations.md",
+ "category": "Animations",
+ "subcategory": "Effects",
+ "author": "Sergio0694"
+ },
+ "samples": [
+ {
+ "header": "Implicit animations",
+ "details": "The ImplicitAnimationSet is mostly used implicitly when attaching implicit animations through the Implicit class, that exposes attached properties to set implicit animations to UI elements. As mentioned above, implicit composition animations can be set to trigger either when an element is shown or hidden, or whenever one of the targeted properties changes. This makes it very easy to create animations that dynamically react to changes in the visual state of UI elements, making it possible to nicely transition between different positions or layouts. Here is an example that shows how different animations can be attached to a UI element using the Implicit class: \u0060\u0060xaml \u003C!--Implicit show animations--\u003E \u003Canimations:Implicit.ShowAnimations\u003E \u003Canimations:TranslationAnimation Duration=\u00220:0:1\u0022 From=\u00220,-200,0\u0022 To=\u00220\u0022/\u003E \u003Canimations:OpacityAnimation Duration=\u00220:0:1\u0022 From=\u00220\u0022 To=\u00221.0\u0022/\u003E \u003C/animations:Implicit.ShowAnimations\u003E \u003C!--Implicit hide animations (using both default and custom animations)--\u003E \u003Canimations:Implicit.HideAnimations\u003E \u003Canimations:OpacityAnimation Duration=\u00220:0:1\u0022 To=\u00220\u0022/\u003E \u003Canimations:ScalarAnimation Target=\u0022Translation.Y\u0022 Duration=\u00220:0:1\u0022 To=\u0022-200\u0022\u003E \u003Canimations:ScalarKeyFrame Key=\u00220.1\u0022 Value=\u002230\u0022/\u003E \u003Canimations:ScalarKeyFrame Key=\u00220.5\u0022 Value=\u00220.0\u0022/\u003E \u003C/animations:ScalarAnimation\u003E \u003C/animations:Implicit.HideAnimations\u003E \u003C!--Implicit animations (using an expression keyframe as well). These animations can also bind to other properties as triggers: in this example we are animating the rotation whenever the Offset changes.--\u003E \u003Canimations:Implicit.Animations\u003E \u003Canimations:OffsetAnimation Duration=\u00220:0:1\u0022/\u003E \u003Canimations:RotationInDegreesAnimation ImplicitTarget=\u0022Offset\u0022 Duration=\u00220:0:1.5\u0022\u003E \u003Canimations:ScalarKeyFrame Key=\u00221.0\u0022 Expression=\u0022this.Target.Offset.X\u0022/\u003E \u003C/animations:RotationInDegreesAnimation\u003E \u003Canimations:ScaleAnimation Duration=\u00220:0:1\u0022/\u003E \u003C/animations:Implicit.Animations\u003E \u0060\u0060",
+ "xaml": "\u003CGrid Height=\u0022480\u0022\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022*\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n \u003CCanvas\u003E\n \u003CBorder x:Name=\u0022Element\u0022\n Canvas.Left=\u0022100\u0022\n Canvas.Top=\u0022100\u0022\n Width=\u0022100\u0022\n Height=\u0022100\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\n CornerRadius=\u0022{StaticResource ControlCornerRadius}\u0022\u003E\n\n \u003Canimations:Implicit.ShowAnimations\u003E\n \u003Canimations:TranslationAnimation From=\u00220, -200, 0\u0022\n To=\u00220\u0022\n Duration=\u00220:0:1\u0022 /\u003E\n \u003Canimations:OpacityAnimation From=\u00220\u0022\n To=\u00221.0\u0022\n Duration=\u00220:0:1\u0022 /\u003E\n \u003C/animations:Implicit.ShowAnimations\u003E\n\n \u003Canimations:Implicit.HideAnimations\u003E\n \u003Canimations:OpacityAnimation To=\u00220.0\u0022\n Duration=\u00220:0:1\u0022 /\u003E\n \u003Canimations:ScalarAnimation Target=\u0022Translation.Y\u0022\n To=\u0022-200\u0022\n Duration=\u00220:0:1\u0022\u003E\n \u003Canimations:ScalarKeyFrame Key=\u00220.1\u0022\n Value=\u002230\u0022 /\u003E\n \u003Canimations:ScalarKeyFrame Key=\u00220.5\u0022\n Value=\u00220.0\u0022 /\u003E\n \u003C/animations:ScalarAnimation\u003E\n \u003C/animations:Implicit.HideAnimations\u003E\n\n \u003Canimations:Implicit.Animations\u003E\n \u003Canimations:OffsetAnimation Duration=\u00220:0:1\u0022 /\u003E\n \u003Canimations:RotationInDegreesAnimation ImplicitTarget=\u0022Offset\u0022\n From=\u00220\u0022\n To=\u00220\u0022\n Duration=\u00220:0:1.2\u0022\u003E\n \u003Canimations:ScalarKeyFrame Key=\u00220.9\u0022\n Value=\u002280\u0022 /\u003E\n \u003C/animations:RotationInDegreesAnimation\u003E\n \u003Canimations:ScaleAnimation Duration=\u00220:0:1\u0022 /\u003E\n \u003C/animations:Implicit.Animations\u003E\n\n \u003C/Border\u003E\n \u003C/Canvas\u003E\n \u003CStackPanel Grid.Row=\u00221\u0022\n HorizontalAlignment=\u0022Center\u0022\n Orientation=\u0022Horizontal\u0022\n Spacing=\u00228\u0022\u003E\n \u003CButton Click=\u0022Visibility_Click\u0022\n Content=\u0022Toggle visibility\u0022 /\u003E\n \u003CButton Click=\u0022Move_Click\u0022\n Content=\u0022Move\u0022 /\u003E\n \u003CButton Click=\u0022Scale_Click\u0022\n Content=\u0022Scale\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "code": "private Random _random = new Random();\n\nprivate void Visibility_Click(object sender, RoutedEventArgs e)\n{\n Element.Visibility = Element.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;\n}\n\nprivate void Move_Click(object sender, RoutedEventArgs e)\n{\n Canvas.SetTop(Element, _random.NextDouble() * this.ActualHeight);\n Canvas.SetLeft(Element, _random.NextDouble() * this.ActualWidth);\n}\n\nprivate void Scale_Click(object sender, RoutedEventArgs e)\n{\n var visual = ElementCompositionPreview.GetElementVisual(Element);\n visual.Scale = new Vector3((float)_random.NextDouble() * 2, (float)_random.NextDouble() * 2, 1);\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:animations=\u0022using:CommunityToolkit.WinUI.Animations\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AnimationsImplicitSample",
+ "sourcePath": "components/Animations/samples/AnimationsImplicitSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "connectedanimations",
+ "name": "Connected Animations",
+ "description": "The Connected Animation XAML Attached Properties enable connected animations to be defined in your XAML code",
+ "nugetPackage": "CommunityToolkit.WinUI.Animations",
+ "curatedKeywords": [
+ "Animations",
+ "Effects",
+ "Layout",
+ "Composition",
+ "animationset",
+ "animation",
+ "coordinated animations"
+ ],
+ "keywords": [
+ "Animations",
+ "Effects"
+ ],
+ "docs": [
+ {
+ "title": "Connected Animations",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Animations/samples/ConnectedAnimations.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Animations",
+ "documentPath": "components/Animations/samples/ConnectedAnimations.md",
+ "category": "Animations",
+ "subcategory": "Effects",
+ "author": "nmetulev"
+ },
+ "samples": [
+ {
+ "header": "Connected Animations",
+ "details": "Connected animations let you create a dynamic and compelling navigation experience by animating the transition of an element between two different views. The Connected Animations XAML Attached Properties enable connected animations to be defined directly in your XAML code by simply adding a Key to the element that should animate. There are also attached properties to enable coordinated animations and animations in lists and grids.",
+ "xaml": "\u003CGrid\u003E\n \u003CBorder Background=\u0022{ThemeResource SolidBackgroundFillColorBaseBrush}\u0022\n BorderBrush=\u0022{ThemeResource SurfaceStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\n CornerRadius=\u0022{StaticResource OverlayCornerRadius}\u0022\u003E\n \u003CGrid\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022*\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n \u003CBorder Background=\u0022{StaticResource SolidBackgroundFillColorSecondaryBrush}\u0022\u003E\n \u003CStackPanel Height=\u002232\u0022\n VerticalAlignment=\u0022Top\u0022\n Orientation=\u0022Horizontal\u0022\u003E\n \u003CButton x:Name=\u0022BackButton\u0022\n Click=\u0022BackButton_Click\u0022\n Content=\u0022{ui:FontIcon Glyph=\u0026#xE72B;,\n FontSize=14}\u0022\n Visibility=\u0022Collapsed\u0022 /\u003E\n \u003CTextBlock Margin=\u002212,0\u0022\n VerticalAlignment=\u0022Center\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022My Connected Animations App\u0022\u003E\n \u003Canimations:Implicit.Animations\u003E\n \u003Canimations:OffsetAnimation Duration=\u00220:0:0.3\u0022 /\u003E\n \u003C/animations:Implicit.Animations\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n \u003C/Border\u003E\n \u003CFrame x:Name=\u0022RootFrame\u0022\n Grid.Row=\u00221\u0022\n Navigated=\u0022Frame_Navigated\u0022\n Navigating=\u0022RootFrame_Navigating\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/Border\u003E\n\u003C/Grid\u003E",
+ "code": "public ConnectedAnimationsSample()\n{\n RootFrame.Navigate(typeof(FirstPage));\n}\n\nprivate void Frame_Navigated(object sender, NavigationEventArgs e)\n{\n BackButton.Visibility = RootFrame.CanGoBack ? Visibility.Visible : Visibility.Collapsed;\n}\n\nprivate void RootFrame_Navigating(object sender, NavigatingCancelEventArgs e)\n{\n if (e.SourcePageType == RootFrame.SourcePageType)\n {\n e.Cancel = true;\n }\n}\n\nprivate void BackButton_Click(object sender, RoutedEventArgs e)\n{\n RootFrame.GoBack(new SuppressNavigationTransitionInfo());\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:animations=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ConnectedAnimationsSample",
+ "sourcePath": "components/Animations/samples/ConnectedAnimationsSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "animationset",
+ "name": "AnimationSet",
+ "description": "A collection of animations that can be grouped together.",
+ "nugetPackage": "CommunityToolkit.WinUI.Behaviors",
+ "curatedKeywords": [
+ "Behaviors",
+ "animations",
+ "animationset",
+ "xaml",
+ "visual",
+ "composition"
+ ],
+ "keywords": [
+ "Animations",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "AnimationSet",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Behaviors/samples/AnimationSet.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Behaviors",
+ "documentPath": "components/Behaviors/samples/AnimationSet.md",
+ "category": "Animations",
+ "subcategory": "Miscellaneous",
+ "author": "Sergio0694"
+ },
+ "samples": [
+ {
+ "header": "InvokeActionsActivity",
+ "details": "If you are also referencing the Behaviors package, it will be possible to also use behaviors and actions to better support the new APIs, such as by automatically triggering an animation when a given event is raised, entirely from XAML. There are four main types being introduced in this package that interoperate with the Animation APIs: - AnimationStartedTriggerBehavior and AnimationCompletedTriggerBehavior: these are custom triggers that can be used to execute IAction-s when an AnimationSet starts or completes. All the built-in IAction objects can be used from the Behaviors package, as well as custom ones as well. - StartAnimationAction: an IAction object that can be used within behaviors to easily start a target animation, either with an attached UI element or with an explicit target to animate. - StopAnimationAction: an IAction object that can be used within behaviors to easily stop a target animation, either with an attached UI element or with an explicit target to animate. Here is an example that shows how these new APIs can be used together: \u0060\u0060xaml \u003CButton\u003E \u003C!--Use StartAnimationAction to trigger the animation on click--\u003E \u003CInteractivity:Interaction.Behaviors\u003E \u003CInteractions:EventTriggerBehavior EventName=\u0022Click\u0022\u003E \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind ScaleAnimation}\u0022 /\u003E \u003C/Interactions:EventTriggerBehavior\u003E \u003C/Interactivity:Interaction.Behaviors\u003E \u003Canimations:Explicit.Animations\u003E \u003Canimations:AnimationSet x:Name=\u0022ScaleAnimation\u0022\u003E \u003Canimations:ScaleAnimation From=\u00221\u0022 To=\u00221.2\u0022/\u003E \u003C!--Use AnimationEndBehavior to invoke a command when the animation ends--\u003E \u003CInteractivity:Interaction.Behaviors\u003E \u003Cbehaviors:AnimationEndBehavior\u003E \u003CInteractions:InvokeCommandAction Command=\u0022{x:Bind ViewModel.MyCommand}\u0022/\u003E \u003C/behaviors:AnimationEndBehavior\u003E \u003C/Interactivity:Interaction.Behaviors\u003E \u003C/animations:AnimationSet\u003E \u003C/animations:Explicit.Animations\u003E \u003C/Button\u003E \u0060 This makes it possible to also not having to name the target UI element, to register the event handler in code behind, and in many cases to even name the AnimationSet\u0060 instance at all, if it doesn\u0027t need to be referenced by other animations at all. The resulting code is all in XAML, with no need for code behind at all!",
+ "xaml": "\u003CButton Width=\u0022140\u0022\n Height=\u0022140\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\u003E\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022MoveAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:StartAnimationActivity Animation=\u0022{Binding ElementName=FadeOutAnimation}\u0022 /\u003E\n \u003Cani:InvokeActionsActivity\u003E\n \u003Cinteractivity:ChangePropertyAction PropertyName=\u0022Foreground\u0022\n TargetObject=\u0022{Binding ElementName=MyText}\u0022\n Value=\u0022Purple\u0022 /\u003E\n \u003C!--\u003Cmediaactions:PlaySoundAction Source=\u0022Assets/Llama.mp3\u0022/\u003E--\u003E\n \u003C/ani:InvokeActionsActivity\u003E\n \u003Cani:StartAnimationActivity Animation=\u0022{Binding ElementName=FadeInAnimation}\u0022\n Delay=\u00220:0:2\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003CTextBlock x:Name=\u0022MyText\u0022\n Text=\u0022\uD83E\uDD99 Text\u0022\u003E\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022FadeOutAnimation\u0022\u003E\n \u003Cani:OpacityAnimation Delay=\u00220\u0022\n EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n From=\u00221\u0022\n To=\u00220\u0022\n Duration=\u00220:0:1\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003Cani:AnimationSet x:Name=\u0022FadeInAnimation\u0022\u003E\n \u003Cani:OpacityAnimation Delay=\u00220\u0022\n EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n From=\u00220\u0022\n To=\u00221\u0022\n Duration=\u00220:0:1\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n \u003C/TextBlock\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Click\u0022\u003E\n \u003Cinteractivity:ChangePropertyAction PropertyName=\u0022Foreground\u0022\n TargetObject=\u0022{Binding ElementName=MyText}\u0022\n Value=\u0022White\u0022 /\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{Binding ElementName=MoveAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Button\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "InvokeActionsActivitySample",
+ "sourcePath": "components/Behaviors/samples/InvokeActionsSample.xaml"
+ }
+ },
+ {
+ "header": "StartAnimationActivity",
+ "details": "A sample for showing how to create and use a InvokeActionsActivity behavior.",
+ "xaml": "\u003CButton Width=\u0022140\u0022\n Height=\u0022140\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\u003E\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022MoveAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:TranslationAnimation From=\u00220,0,0\u0022\n To=\u00220,32,0\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n \u003Cani:StartAnimationActivity Animation=\u0022{Binding ElementName=FadeOutAnimation}\u0022\n Delay=\u00220:0:3\u0022 /\u003E\n \u003Cani:StartAnimationActivity Animation=\u0022{Binding ElementName=FadeInAnimation}\u0022\n Delay=\u00220:0:3\u0022 /\u003E\n \u003Cani:TranslationAnimation From=\u00220,32,0\u0022\n To=\u00220,0,0\u0022\n Duration=\u00220:0:1\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003CImage Width=\u002268\u0022\n Height=\u002268\u0022\n Source=\u0022ms-appx:///Assets/ToolkitIcon.png\u0022\u003E\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022FadeOutAnimation\u0022\u003E\n \u003Cani:OpacityAnimation Delay=\u00220\u0022\n EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n From=\u00221\u0022\n To=\u00220\u0022\n Duration=\u00220:0:1\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003Cani:AnimationSet x:Name=\u0022FadeInAnimation\u0022\u003E\n \u003Cani:OpacityAnimation Delay=\u00220\u0022\n EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n From=\u00220\u0022\n To=\u00221\u0022\n Duration=\u00220:0:1\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n \u003C/Image\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Click\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{Binding ElementName=MoveAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Button\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StartAnimationActivitySample",
+ "sourcePath": "components/Behaviors/samples/StartAnimationActivitySample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "behaviors",
+ "name": "Behaviors",
+ "description": "A behavior is a class that attaches to a XAML control and invokes an Action when triggered.",
+ "nugetPackage": "CommunityToolkit.WinUI.Behaviors",
+ "curatedKeywords": [
+ "Behaviors"
+ ],
+ "keywords": [
+ "Xaml",
+ "Behaviors"
+ ],
+ "docs": [
+ {
+ "title": "Behaviors",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Behaviors/samples/Behaviors.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Behaviors",
+ "documentPath": "components/Behaviors/samples/Behaviors.md",
+ "category": "Xaml",
+ "subcategory": "Behaviors",
+ "author": "Arlodotexe"
+ },
+ "samples": [
+ {
+ "header": "KeyDownTriggerBehavior",
+ "details": "A behavior that listens to a key press event on the associated UIElement and triggers the set of actions.",
+ "xaml": "\u003CStackPanel MaxWidth=\u0022480\u0022\n Spacing=\u00228\u0022\u003E\n \u003CTextBox PlaceholderText=\u0022Set the focus to this TextBox and press enter\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:KeyDownTriggerBehavior Key=\u0022Enter\u0022\u003E\n \u003Cinteractivity:CallMethodAction MethodName=\u0022IncrementCount\u0022\n TargetObject=\u0022{x:Bind}\u0022 /\u003E\n \u003C/behaviors:KeyDownTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003C/TextBox\u003E\n\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Enter pressed\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{x:Bind Count, Mode=OneWay}\u0022 /\u003E\n \u003CRun Text=\u0022times.\u0022 /\u003E\n \u003C/TextBlock\u003E\n\u003C/StackPanel\u003E",
+ "code": "public int Count { get; set; }\n\npublic void IncrementCount()\n{\n Count\u002B\u002B;\n PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));\n}\n\npublic event PropertyChangedEventHandler? PropertyChanged;",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "KeyDownTriggerBehaviorSample",
+ "sourcePath": "components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml"
+ }
+ },
+ {
+ "header": "AutoSelectBehavior",
+ "details": "The AutoSelectBehavior automatically selects the entire content of its associated TextBox when it is loaded.",
+ "xaml": "\u003CStackPanel MaxWidth=\u0022480\u0022\n Spacing=\u00228\u0022\u003E\n \u003CTextBox Text=\u0022My content is not selected when loaded\u0022 /\u003E\n \u003CTextBox Text=\u0022My content is selected when loaded\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:AutoSelectBehavior /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003C/TextBox\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AutoSelectBehaviorSample",
+ "sourcePath": "components/Behaviors/samples/AutoSelectBehaviorSample.xaml"
+ }
+ },
+ {
+ "header": "ViewportBehavior",
+ "details": "This behavior allows you to listen an element enter or exit the ScrollViewer viewport.",
+ "xaml": "\u003CGrid RowSpacing=\u002212\u0022\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n IsHitTestVisible=\u0022False\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022Scroll to see the effect\u0022 /\u003E\n\n \u003CScrollViewer Grid.Row=\u00221\u0022\n MaxWidth=\u0022480\u0022\n MaxHeight=\u0022480\u0022\n BorderBrush=\u0022{ThemeResource ControlStrongStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\u003E\n \u003CGrid Height=\u00222000\u0022\u003E\n \u003CBorder Width=\u0022200\u0022\n Height=\u0022200\u0022\n Background=\u0022{ThemeResource AccentFillColorTertiaryBrush}\u0022\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:ViewportBehavior x:Name=\u0022ViewportBehavior\u0022\n IsAlwaysOn=\u0022True\u0022 /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\n \u003CRectangle Width=\u0022100\u0022\n Height=\u0022100\u0022\n Fill=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n \u003C/ScrollViewer\u003E\n\n \u003CStackPanel Grid.Row=\u00222\u0022\n HorizontalAlignment=\u0022Center\u0022\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022IsFullyInViewport:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{x:Bind ViewportBehavior.IsFullyInViewport, Mode=OneWay}\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022IsInViewport:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{x:Bind ViewportBehavior.IsInViewport, Mode=OneWay}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ViewportBehaviorSample",
+ "sourcePath": "components/Behaviors/samples/ViewportBehaviorSample.xaml",
+ "optionsResolved": [
+ "IsAlwaysOn=True"
+ ]
+ }
+ },
+ {
+ "header": "FocusBehavior: Disabled / Unloaded items",
+ "details": "Of the given targets, this behavior sets the focus on the first control which accepts it. A control only receives focus if it is enabled and loaded into the visual tree:",
+ "xaml": "\u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:FocusBehavior\u003E\n \u003Cbehaviors:FocusTarget Control=\u0022{x:Bind ButtonOne}\u0022 /\u003E\n \u003Cbehaviors:FocusTarget Control=\u0022{x:Bind ButtonTwo}\u0022 /\u003E\n \u003C/behaviors:FocusBehavior\u003E\n\u003C/interactivity:Interaction.Behaviors\u003E\n\n\u003CStackPanel Spacing=\u002212\u0022\u003E\n \u003CButton x:Name=\u0022ButtonOne\u0022\n x:Load=\u0022True\u0022\n Content=\u0022Button 1\u0022\n IsEnabled=\u0022True\u0022 /\u003E\n\n \u003CButton x:Name=\u0022ButtonTwo\u0022\n Content=\u0022Button 2\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "FocusBehaviorButtonSample",
+ "sourcePath": "components/Behaviors/samples/FocusBehaviorButtonSample.xaml",
+ "optionsResolved": [
+ "ControlLoaded=True",
+ "IsButtonEnabled=True"
+ ]
+ }
+ },
+ {
+ "header": "FocusBehavior: Lists",
+ "details": "Empty lists do not receive focus:",
+ "xaml": "\u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:FocusBehavior\u003E\n \u003Cbehaviors:FocusTarget Control=\u0022{x:Bind EmptyList}\u0022 /\u003E\n \u003Cbehaviors:FocusTarget Control=\u0022{x:Bind NonEmptyList}\u0022 /\u003E\n \u003C/behaviors:FocusBehavior\u003E\n\u003C/interactivity:Interaction.Behaviors\u003E\n\n\u003CStackPanel\u003E\n \u003CListView x:Name=\u0022EmptyList\u0022 /\u003E\n\n \u003CListView x:Name=\u0022NonEmptyList\u0022\u003E\n \u003CListView.Items\u003E\n \u003Cx:String\u003EItem 1\u003C/x:String\u003E\n \u003Cx:String\u003EItem 2\u003C/x:String\u003E\n \u003C/ListView.Items\u003E\n \u003C/ListView\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "FocusBehaviorListSample",
+ "sourcePath": "components/Behaviors/samples/FocusBehaviorListSample.xaml"
+ }
+ },
+ {
+ "header": "NavigateToUriAction",
+ "details": "This behavior allows you to define a Uri in XAML, similar to a Hyperlink or HyperlinkButton. This allows you to use a Button and still define the Uri in XAML without wiring up the Click event in code-behind, or restyling a HyperlinkButton.",
+ "xaml": "\u003CButton Content=\u0022Click to navigate\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Click\u0022\u003E\n \u003Cbehaviors:NavigateToUriAction NavigateUri=\u0022https://aka.ms/toolkit/windows\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Button\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "NavigateToUriActionSample",
+ "sourcePath": "components/Behaviors/samples/NavigateToUriActionSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "headerbehaviors",
+ "name": "Header Behaviors",
+ "description": "Behaviors for modifying an element\u0027s movement/response when scrolling within a ScrollViewer.",
+ "nugetPackage": "CommunityToolkit.WinUI.Behaviors",
+ "curatedKeywords": [
+ "Behaviors"
+ ],
+ "keywords": [
+ "Xaml",
+ "Behaviors"
+ ],
+ "docs": [
+ {
+ "title": "Header Behaviors",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Behaviors/samples/HeaderBehaviors.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Behaviors",
+ "documentPath": "components/Behaviors/samples/HeaderBehaviors.md",
+ "category": "Xaml",
+ "subcategory": "Behaviors",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "FadeHeaderBehavior",
+ "details": "The FadeHeaderBehavior causes the element in the scrolling collection to fade in and out as the user scrolls at the top of the collection.",
+ "xaml": "\u003C!-- We need to set height here to force scrolling in the sample --\u003E\n\u003CListView Height=\u0022300\u0022\n VerticalAlignment=\u0022Top\u0022\u003E\n \u003CListView.Header\u003E\n \u003CGrid MinHeight=\u0022100\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:FadeHeaderBehavior /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Foreground=\u0022{ThemeResource TextOnAccentFillColorPrimaryBrush}\u0022\n Text=\u0022Header\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/ListView.Header\u003E\n \u003CListView.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022x:String\u0022\u003E\n \u003CGrid MinHeight=\u002225\u0022\u003E\n \u003CTextBlock VerticalAlignment=\u0022Center\u0022\n Text=\u0022{Binding}\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/DataTemplate\u003E\n \u003C/ListView.ItemTemplate\u003E\n \u003CListView.Items\u003E\n \u003Cx:String\u003EOne\u003C/x:String\u003E\n \u003Cx:String\u003ETwo\u003C/x:String\u003E\n \u003Cx:String\u003EThree\u003C/x:String\u003E\n \u003Cx:String\u003EFour\u003C/x:String\u003E\n \u003Cx:String\u003EFive\u003C/x:String\u003E\n \u003Cx:String\u003ESix\u003C/x:String\u003E\n \u003Cx:String\u003ESeven\u003C/x:String\u003E\n \u003Cx:String\u003EEight\u003C/x:String\u003E\n \u003Cx:String\u003ENine\u003C/x:String\u003E\n \u003Cx:String\u003ETen\u003C/x:String\u003E\n \u003Cx:String\u003EEleven\u003C/x:String\u003E\n \u003Cx:String\u003ETwelve\u003C/x:String\u003E\n \u003Cx:String\u003EThirteen\u003C/x:String\u003E\n \u003Cx:String\u003EFourteen\u003C/x:String\u003E\n \u003Cx:String\u003EFifteen\u003C/x:String\u003E\n \u003Cx:String\u003ESixteen\u003C/x:String\u003E\n \u003Cx:String\u003ESeventeen\u003C/x:String\u003E\n \u003Cx:String\u003EEighteen\u003C/x:String\u003E\n \u003Cx:String\u003ENineteen\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty\u003C/x:String\u003E\n \u003C/ListView.Items\u003E\n\u003C/ListView\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "FadeHeaderBehaviorSample",
+ "sourcePath": "components/Behaviors/samples/Headers/FadeHeaderBehaviorSample.xaml"
+ }
+ },
+ {
+ "header": "QuickReturnHeaderBehavior",
+ "details": "The QuickReturnHeaderBehavior causes the element in the scrolling collection to return back into view as soon as the user scrolls up even if they are not near the top of the collection.",
+ "xaml": "\u003C!-- We need to set height here to force scrolling in the sample --\u003E\n\u003CGridView Height=\u0022300\u0022\n VerticalAlignment=\u0022Top\u0022\u003E\n \u003CGridView.Header\u003E\n \u003CGrid MinHeight=\u0022100\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:QuickReturnHeaderBehavior /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Foreground=\u0022{ThemeResource TextOnAccentFillColorPrimaryBrush}\u0022\n Text=\u0022Header\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/GridView.Header\u003E\n \u003CGridView.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022x:String\u0022\u003E\n \u003CBorder Width=\u0022200\u0022\n Height=\u0022200\u0022\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Text=\u0022{Binding}\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/DataTemplate\u003E\n \u003C/GridView.ItemTemplate\u003E\n \u003CGridView.Items\u003E\n \u003Cx:String\u003EOne\u003C/x:String\u003E\n \u003Cx:String\u003ETwo\u003C/x:String\u003E\n \u003Cx:String\u003EThree\u003C/x:String\u003E\n \u003Cx:String\u003EFour\u003C/x:String\u003E\n \u003Cx:String\u003EFive\u003C/x:String\u003E\n \u003Cx:String\u003ESix\u003C/x:String\u003E\n \u003Cx:String\u003ESeven\u003C/x:String\u003E\n \u003Cx:String\u003EEight\u003C/x:String\u003E\n \u003Cx:String\u003ENine\u003C/x:String\u003E\n \u003Cx:String\u003ETen\u003C/x:String\u003E\n \u003Cx:String\u003EEleven\u003C/x:String\u003E\n \u003Cx:String\u003ETwelve\u003C/x:String\u003E\n \u003Cx:String\u003EThirteen\u003C/x:String\u003E\n \u003Cx:String\u003EFourteen\u003C/x:String\u003E\n \u003Cx:String\u003EFifteen\u003C/x:String\u003E\n \u003Cx:String\u003ESixteen\u003C/x:String\u003E\n \u003Cx:String\u003ESeventeen\u003C/x:String\u003E\n \u003Cx:String\u003EEighteen\u003C/x:String\u003E\n \u003Cx:String\u003ENineteen\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-One\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-Two\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-Three\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-Four\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-Five\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-Six\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-Seven\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-Eight\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty-Nine\u003C/x:String\u003E\n \u003Cx:String\u003EThirty\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-One\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-Two\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-Three\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-Four\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-Five\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-Six\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-Seven\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-Eight\u003C/x:String\u003E\n \u003Cx:String\u003EThirty-Nine\u003C/x:String\u003E\n \u003Cx:String\u003EForty\u003C/x:String\u003E\n \u003Cx:String\u003EForty-One\u003C/x:String\u003E\n \u003Cx:String\u003EForty-Two\u003C/x:String\u003E\n \u003Cx:String\u003EForty-Three\u003C/x:String\u003E\n \u003Cx:String\u003EForty-Four\u003C/x:String\u003E\n \u003Cx:String\u003EForty-Five\u003C/x:String\u003E\n \u003Cx:String\u003EForty-Six\u003C/x:String\u003E\n \u003Cx:String\u003EForty-Seven\u003C/x:String\u003E\n \u003Cx:String\u003EForty-Eight\u003C/x:String\u003E\n \u003Cx:String\u003EForty-Nine\u003C/x:String\u003E\n \u003Cx:String\u003EFifty\u003C/x:String\u003E\n \u003C/GridView.Items\u003E\n\u003C/GridView\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "QuickReturnHeaderBehaviorSample",
+ "sourcePath": "components/Behaviors/samples/Headers/QuickReturnHeaderBehaviorSample.xaml"
+ }
+ },
+ {
+ "header": "QuickReturnHeaderBehavior in a ScrollViewer",
+ "details": "It can also be used to have content quickly re-appear in any ScrollViewer:",
+ "xaml": "\u003C!-- We need to set height here to force scrolling in the sample --\u003E\n\u003CScrollViewer Height=\u0022300\u0022\n VerticalAlignment=\u0022Top\u0022\u003E\n \u003CStackPanel MaxWidth=\u0022480\u0022\u003E\n \u003CGrid Height=\u0022100\u0022\n Padding=\u002212\u0022\n Background=\u0022{ThemeResource AcrylicBackgroundFillColorBaseBrush}\u0022\u003E\n \u003CImage Source=\u0022ms-appx:///Assets/ToolkitIcon.png\u0022 /\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:QuickReturnHeaderBehavior /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003C/Grid\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n Text=\u0022Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Faucibus vitae aliquet nec ullamcorper sit amet risus nullam eget. Quis vel eros donec ac odio. Nisi porta lorem mollis aliquam ut. At varius vel pharetra vel turpis nunc. Diam volutpat commodo sed egestas egestas fringilla phasellus faucibus scelerisque. Est pellentesque elit ullamcorper dignissim cras tincidunt. Amet consectetur adipiscing elit pellentesque habitant. Purus sit amet luctus venenatis lectus magna. Amet consectetur adipiscing elit ut aliquam purus. Felis eget nunc lobortis mattis aliquam faucibus purus in. Mattis rhoncus urna neque viverra justo nec ultrices. Ipsum nunc aliquet bibendum enim facilisis gravida. Nisi est sit amet facilisis magna. Suspendisse interdum consectetur libero id faucibus nisl. Malesuada bibendum arcu vitae elementum curabitur. Molestie ac feugiat sed lectus vestibulum mattis. Sem nulla pharetra diam sit amet nisl suscipit adipiscing bibendum. Quis enim lobortis scelerisque fermentum dui faucibus in. Sagittis purus sit amet volutpat consequat mauris nunc congue nisi. Leo duis ut diam quam nulla porttitor massa. Metus aliquam eleifend mi in. Fringilla urna porttitor rhoncus dolor. Sapien eget mi proin sed. A cras semper auctor neque. Id faucibus nisl tincidunt eget nullam non nisi. Pulvinar etiam non quam lacus suspendisse faucibus interdum. Urna id volutpat lacus laoreet non. Feugiat in ante metus dictum at tempor commodo ullamcorper a. Arcu non odio euismod lacinia at quis risus sed. In massa tempor nec feugiat. Enim blandit volutpat maecenas volutpat blandit aliquam etiam erat. Iaculis nunc sed augue lacus viverra vitae. Rhoncus est pellentesque elit ullamcorper dignissim. Ac tincidunt vitae semper quis lectus nulla at volutpat. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae. Tempor nec feugiat nisl pretium fusce id velit ut. Eget arcu dictum varius duis at consectetur lorem donec massa. A lacus vestibulum sed arcu non odio. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt. Turpis egestas integer eget aliquet nibh praesent tristique magna sit. Mauris vitae ultricies leo integer malesuada nunc vel risus commodo. Rhoncus dolor purus non enim praesent elementum facilisis leo. Vestibulum mattis ullamcorper velit sed ullamcorper morbi. Augue interdum velit euismod in pellentesque massa. Ullamcorper a lacus vestibulum sed. Penatibus et magnis dis parturient montes nascetur ridiculus. At risus viverra adipiscing at. Felis eget velit aliquet sagittis id. Venenatis tellus in metus vulputate eu. In hac habitasse platea dictumst vestibulum rhoncus. Luctus accumsan tortor posuere ac. Ullamcorper morbi tincidunt ornare massa. Tellus orci ac auctor augue mauris augue neque. Venenatis lectus magna fringilla urna porttitor. Pretium vulputate sapien nec sagittis aliquam malesuada. Aliquet nec ullamcorper sit amet. Consequat id porta nibh venenatis cras. Elementum nisi quis eleifend quam adipiscing. Netus et malesuada fames ac turpis egestas maecenas. Pellentesque eu tincidunt tortor aliquam. Ornare suspendisse sed nisi lacus sed viverra tellus in. Fusce ut placerat orci nulla pellentesque. Maecenas sed enim ut sem. Arcu cursus euismod quis viverra nibh cras pulvinar mattis. Proin fermentum leo vel orci porta non pulvinar neque. Eu non diam phasellus vestibulum lorem sed risus. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Dolor sed viverra ipsum nunc aliquet bibendum enim facilisis gravida. Nibh tortor id aliquet lectus proin nibh nisl condimentum id. Arcu cursus vitae congue mauris. Vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet. Faucibus turpis in eu mi bibendum. Faucibus nisl tincidunt eget nullam non nisi est sit. Turpis tincidunt id aliquet risus feugiat. Nulla at volutpat diam ut venenatis tellus in metus. Ultrices neque ornare aenean euismod. Suspendisse faucibus interdum posuere lorem ipsum dolor. Nisl rhoncus mattis rhoncus urna. Vitae semper quis lectus nulla at volutpat diam. Eu lobortis elementum nibh tellus molestie nunc. Nulla facilisi morbi tempus iaculis urna. Tortor at risus viverra adipiscing at in tellus integer. Nibh tortor id aliquet lectus proin nibh nisl. Feugiat vivamus at augue eget arcu dictum. Lectus proin nibh nisl condimentum. Tristique senectus et netus et malesuada fames. Elementum nibh tellus molestie nunc non blandit massa enim. Risus ultricies tristique nulla aliquet enim. Vel pretium lectus quam id leo in vitae turpis.\u0022\n TextWrapping=\u0022WrapWholeWords\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/ScrollViewer\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "QuickReturnScrollViewerSample",
+ "sourcePath": "components/Behaviors/samples/Headers/QuickReturnScrollViewerSample.xaml"
+ }
+ },
+ {
+ "header": "StickyHeaderBehavior",
+ "details": "The StickyHeaderBehavior causes the element in the scrolling collection to stay in view as the user scrolls up and down in the collection.",
+ "xaml": "\u003C!-- We need to set height here to force scrolling in the sample --\u003E\n\u003Ccontrols:HeaderedTreeView Height=\u0022300\u0022\n VerticalAlignment=\u0022Top\u0022\n ItemsSource=\u0022{x:Bind Items, Mode=OneWay}\u0022\u003E\n \u003Ccontrols:HeaderedTreeView.Header\u003E\n \u003CGrid MinHeight=\u0022100\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:StickyHeaderBehavior /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Foreground=\u0022{ThemeResource TextOnAccentFillColorPrimaryBrush}\u0022\n Text=\u0022Header\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/controls:HeaderedTreeView.Header\u003E\n \u003Ccontrols:HeaderedTreeView.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:ExplorerItem\u0022\u003E\n \u003Cmuxc:TreeViewItem Content=\u0022{x:Bind Name}\u0022\n IsExpanded=\u0022True\u0022\n ItemsSource=\u0022{x:Bind Children}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:HeaderedTreeView.ItemTemplate\u003E\n\u003C/controls:HeaderedTreeView\u003E",
+ "code": "public ObservableCollection\u003CExplorerItem\u003E Items { get; }\n\npublic StickyHeaderBehaviorSample()\n{\n Items = GetData();\n}\n\nprivate ObservableCollection\u003CExplorerItem\u003E GetData()\n{\n var list = new ObservableCollection\u003CExplorerItem\u003E();\n ExplorerItem folder1 = new ExplorerItem()\n {\n Name = \u0022Work Documents\u0022,\n Children =\n {\n new ExplorerItem()\n {\n Name = \u0022Functional Specifications\u0022,\n Children =\n {\n new ExplorerItem()\n {\n Name = \u0022TreeView spec\u0022,\n }\n }\n },\n new ExplorerItem()\n {\n Name = \u0022Feature Schedule\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Overall Project Plan\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Feature Resources Allocation\u0022,\n }\n }\n };\n ExplorerItem folder2 = new ExplorerItem()\n {\n Name = \u0022Personal Folder\u0022,\n Children =\n {\n new ExplorerItem()\n {\n Name = \u0022Home Remodel Folder\u0022,\n Children =\n {\n new ExplorerItem()\n {\n Name = \u0022Contractor Contact Info\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Paint Color Scheme\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Flooring Woodgrain type\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Kitchen Cabinet Style\u0022,\n }\n }\n }\n }\n };\n list.Add(folder1);\n list.Add(folder2);\n for (int i = 0; i \u003C 40; i\u002B\u002B)\n {\n list.Add(new() { Name = $\u0022Folder {i \u002B 1}\u0022 });\n }\n\n return list;\n}\n\npublic class ExplorerItem\n{\n public string? Name { get; set; }\n\n private ObservableCollection\u003CExplorerItem\u003E? _children;\n public ObservableCollection\u003CExplorerItem\u003E Children\n {\n get\n {\n if (_children == null)\n {\n _children = new ObservableCollection\u003CExplorerItem\u003E();\n }\n\n return _children;\n }\n\n set\n {\n _children = value;\n }\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StickyHeaderBehaviorSample",
+ "sourcePath": "components/Behaviors/samples/Headers/StickyHeaderBehaviorSample.xaml"
+ }
+ },
+ {
+ "header": "StickyHeaderBehavior with HeaderedItemsControl",
+ "details": "Or similarly, it can be used with a HeaderedItemsControl to maintain context at the top:",
+ "xaml": "\u003C!-- We need to set height here to force scrolling in the sample --\u003E\n\u003CScrollViewer Height=\u0022300\u0022\n VerticalAlignment=\u0022Top\u0022\u003E\n \u003Ccontrols:HeaderedItemsControl\u003E\n \u003Ccontrols:HeaderedItemsControl.Header\u003E\n \u003CGrid MinHeight=\u0022100\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:StickyHeaderBehavior /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Foreground=\u0022{ThemeResource TextOnAccentFillColorPrimaryBrush}\u0022\n Text=\u0022Header\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/controls:HeaderedItemsControl.Header\u003E\n \u003Ccontrols:HeaderedItemsControl.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022x:String\u0022\u003E\n \u003CGrid MinHeight=\u002225\u0022\n Margin=\u002210\u0022\u003E\n \u003CTextBlock VerticalAlignment=\u0022Center\u0022\n Text=\u0022{Binding}\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:HeaderedItemsControl.ItemTemplate\u003E\n \u003Ccontrols:HeaderedItemsControl.Items\u003E\n \u003Cx:String\u003EOne\u003C/x:String\u003E\n \u003Cx:String\u003ETwo\u003C/x:String\u003E\n \u003Cx:String\u003EThree\u003C/x:String\u003E\n \u003Cx:String\u003EFour\u003C/x:String\u003E\n \u003Cx:String\u003EFive\u003C/x:String\u003E\n \u003Cx:String\u003ESix\u003C/x:String\u003E\n \u003Cx:String\u003ESeven\u003C/x:String\u003E\n \u003Cx:String\u003EEight\u003C/x:String\u003E\n \u003Cx:String\u003ENine\u003C/x:String\u003E\n \u003Cx:String\u003ETen\u003C/x:String\u003E\n \u003Cx:String\u003EEleven\u003C/x:String\u003E\n \u003Cx:String\u003ETwelve\u003C/x:String\u003E\n \u003Cx:String\u003EThirteen\u003C/x:String\u003E\n \u003Cx:String\u003EFourteen\u003C/x:String\u003E\n \u003Cx:String\u003EFifteen\u003C/x:String\u003E\n \u003Cx:String\u003ESixteen\u003C/x:String\u003E\n \u003Cx:String\u003ESeventeen\u003C/x:String\u003E\n \u003Cx:String\u003EEighteen\u003C/x:String\u003E\n \u003Cx:String\u003ENineteen\u003C/x:String\u003E\n \u003Cx:String\u003ETwenty\u003C/x:String\u003E\n \u003C/controls:HeaderedItemsControl.Items\u003E\n \u003C/controls:HeaderedItemsControl\u003E\n\u003C/ScrollViewer\u003E",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StickyHeaderItemsControlSample",
+ "sourcePath": "components/Behaviors/samples/Headers/StickyHeaderItemsControlSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "stackednotificationsbehavior",
+ "name": "StackedNotificationsBehavior",
+ "description": "A behavior to add stacked notifications to a WinUI InfoBar control.",
+ "nugetPackage": "CommunityToolkit.WinUI.Behaviors",
+ "usings": [
+ "CommunityToolkit.WinUI.Behaviors"
+ ],
+ "curatedKeywords": [
+ "StackedNotificationsBehavior",
+ "Control",
+ "Layout",
+ "InfoBar",
+ "Behavior"
+ ],
+ "keywords": [
+ "Xaml",
+ "Behaviors"
+ ],
+ "docs": [
+ {
+ "title": "StackedNotificationsBehavior",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Behaviors/samples/StackedNotificationsBehavior.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Behaviors",
+ "documentPath": "components/Behaviors/samples/StackedNotificationsBehavior.md",
+ "category": "Xaml",
+ "subcategory": "Behaviors",
+ "author": "vgromfeld"
+ },
+ "samples": [
+ {
+ "header": "Stacked Notifications",
+ "details": "Clicking on the button multiple times will queue up multiple messages to be displayed one after another.",
+ "xaml": "\u003CGrid MinHeight=\u0022186\u0022\u003E\n \u003CButton HorizontalAlignment=\u0022Left\u0022\n VerticalAlignment=\u0022Top\u0022\n Click=\u0022Button_Click\u0022\n Content=\u0022Send notification\u0022 /\u003E\n\n \u003Cmuxc:InfoBar MaxWidth=\u0022480\u0022\n Margin=\u002224\u0022\n HorizontalAlignment=\u0022Right\u0022\n VerticalAlignment=\u0022Bottom\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:StackedNotificationsBehavior x:Name=\u0022NotificationQueue\u0022 /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003C/muxc:InfoBar\u003E\n\u003C/Grid\u003E",
+ "code": "private void Button_Click(object sender, RoutedEventArgs e)\n{\n var notification = new Notification\n {\n Title = $\u0022Notification {DateTimeOffset.Now}\u0022,\n Message = GetRandomText(),\n Severity = MUXC.InfoBarSeverity.Informational,\n };\n NotificationQueue.Show(notification);\n}\n\nprivate static int _current = 0;\n\nprivate static string GetRandomText() =\u003E (_current\u002B\u002B % 4) switch\n{\n 1 =\u003E \u0022Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sollicitudin bibendum enim at tincidunt. Praesent egestas ipsum ligula, nec tincidunt lacus semper non.\u0022,\n 2 =\u003E \u0022Pellentesque in risus eget leo rhoncus ultricies nec id ante.\u0022,\n 3 =\u003E \u0022Sed quis nisi quis nunc condimentum varius id consectetur metus. Duis mauris sapien, commodo eget erat ac, efficitur iaculis magna. Morbi eu velit nec massa pharetra cursus.\u0022,\n _ =\u003E \u0022Fusce non quam egestas leo finibus interdum eu ac massa. Quisque nec justo leo. Aenean scelerisque placerat ultrices. Sed accumsan lorem at arcu commodo tristique.\u0022,\n};",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StackedNotificationsBehaviorCustomSample",
+ "sourcePath": "components/Behaviors/samples/Notifications/StackedNotificationsBehaviorCustomSample.xaml"
+ }
+ },
+ {
+ "header": "Stacked Notification Migration",
+ "details": "This example shows sending simple text based notifications that will appear only for 2 seconds:",
+ "xaml": "\u003CStackPanel MinHeight=\u0022136\u0022\n Orientation=\u0022Vertical\u0022\n Spacing=\u002224\u0022\u003E\n \u003CButton HorizontalAlignment=\u0022Left\u0022\n VerticalAlignment=\u0022Top\u0022\n Click=\u0022Button_Click\u0022\n Content=\u0022Send notification\u0022 /\u003E\n\n \u003Cmuxc:InfoBar HorizontalAlignment=\u0022Left\u0022\n VerticalAlignment=\u0022Bottom\u0022\n IsIconVisible=\u0022False\u0022\u003E\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cbehaviors:StackedNotificationsBehavior x:Name=\u0022NotificationQueue\u0022 /\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n \u003C/muxc:InfoBar\u003E\n\u003C/StackPanel\u003E",
+ "code": "private void Button_Click(object sender, RoutedEventArgs e)\n{\n // Show our notification for 2 seconds\n NotificationQueue.Show(GetRandomText(), 2000);\n}\n\nprivate static int _current = 0;\n\nprivate static string GetRandomText() =\u003E (_current\u002B\u002B % 4) switch\n{\n 1 =\u003E \u0022Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sollicitudin bibendum enim at tincidunt. Praesent egestas ipsum ligula, nec tincidunt lacus semper non.\u0022,\n 2 =\u003E \u0022Pellentesque in risus eget leo rhoncus ultricies nec id ante.\u0022,\n 3 =\u003E \u0022Sed quis nisi quis nunc condimentum varius id consectetur metus. Duis mauris sapien, commodo eget erat ac, efficitur iaculis magna. Morbi eu velit nec massa pharetra cursus.\u0022,\n _ =\u003E \u0022Fusce non quam egestas leo finibus interdum eu ac massa. Quisque nec justo leo. Aenean scelerisque placerat ultrices. Sed accumsan lorem at arcu commodo tristique.\u0022,\n};",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StackedNotificationsBehaviorToolkitSample",
+ "sourcePath": "components/Behaviors/samples/Notifications/StackedNotificationsBehaviorToolkitSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "camerapreview",
+ "name": "CameraPreview",
+ "description": "The CameraPreview control allows to easily preview video in the MediaPlayerElement from available camera frame source groups. You can subscribe and get real time video frames and software bitmaps as they arrive from the selected camera source. It shows only frame sources that support color video preview or video record streams.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.CameraPreview",
+ "usings": [
+ "CommunityToolkit.WinUI.Controls",
+ "CommunityToolkit.WinUI.Helpers",
+ "Microsoft.UI.Xaml.Media.Imaging",
+ "Windows.ApplicationModel",
+ "Windows.Graphics.Imaging",
+ "Windows.Media"
+ ],
+ "curatedKeywords": [
+ "CameraPreview",
+ "Control",
+ "skommireddi"
+ ],
+ "keywords": [
+ "Controls",
+ "Media"
+ ],
+ "docs": [
+ {
+ "title": "CameraPreview",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/CameraPreview/samples/CameraPreview.md"
+ }
+ ],
+ "toolkit": {
+ "component": "CameraPreview",
+ "documentPath": "components/CameraPreview/samples/CameraPreview.md",
+ "category": "Controls",
+ "subcategory": "Media",
+ "author": "skommireddi"
+ },
+ "samples": [
+ {
+ "header": "CameraPreview",
+ "details": "A sample for showing how to create and use a CameraPreview control.",
+ "xaml": "\u003CStackPanel HorizontalAlignment=\u0022Center\u0022\n Orientation=\u0022Vertical\u0022\n Spacing=\u002212\u0022\u003E\n \u003CButton HorizontalAlignment=\u0022Center\u0022\n Click=\u0022CaptureButton_Click\u0022\n Content=\u0022Capture\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n \u003Ccontrols:CameraPreview x:Name=\u0022CameraPreviewControl\u0022\n Height=\u0022320\u0022\n IsFrameSourceGroupButtonVisible=\u0022True\u0022 /\u003E\n \u003Cmuxc:InfoBar x:Name=\u0022ErrorBar\u0022\n Title=\u0022Error\u0022\n IsOpen=\u0022False\u0022\n Severity=\u0022Error\u0022 /\u003E\n \u003CImage x:Name=\u0022CurrentFrameImage\u0022\n Height=\u0022120\u0022\n HorizontalAlignment=\u0022Center\u0022 /\u003E\n\n\u003C/StackPanel\u003E",
+ "code": "private static SemaphoreSlim? semaphoreSlim;\n\nprivate VideoFrame _currentVideoFrame;\n\nprivate SoftwareBitmapSource _softwareBitmapSource;\n\n#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.\n\npublic CameraPreviewSample()\n#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.\n\n{\n Loaded \u002B= this.CameraPreviewSample_Loaded;\n Unloaded \u002B= this.CameraPreviewSample_Unloaded;\n semaphoreSlim = new SemaphoreSlim(1);\n}\n\nprivate void CameraPreviewSample_Unloaded(object sender, RoutedEventArgs e)\n{\n Unloaded -= this.CameraPreviewSample_Unloaded;\n _softwareBitmapSource?.Dispose();\n}\n\nprivate void CameraPreviewSample_Loaded(object sender, RoutedEventArgs e)\n{\n Loaded -= this.CameraPreviewSample_Loaded;\n Load();\n}\n\nprivate async void Load()\n{\n // Using a semaphore lock for synchronization.\n // This method gets called multiple times when accessing the page from Latest Pages\n // and creates unused duplicate references to Camera and memory leaks.\n await semaphoreSlim!.WaitAsync();\n var cameraHelper = CameraPreviewControl?.CameraHelper;\n UnsubscribeFromEvents();\n if (CameraPreviewControl != null)\n {\n CameraPreviewControl.PreviewFailed \u002B= CameraPreviewControl_PreviewFailed!;\n await CameraPreviewControl.StartAsync(cameraHelper!);\n CameraPreviewControl.CameraHelper.FrameArrived \u002B= CameraPreviewControl_FrameArrived!;\n }\n\n _softwareBitmapSource = new SoftwareBitmapSource();\n CurrentFrameImage.Source = _softwareBitmapSource;\n semaphoreSlim.Release();\n}\n\nprotected override void OnNavigatedTo(NavigationEventArgs e)\n{\n base.OnNavigatedTo(e);\n}\n\nprotected async override void OnNavigatedFrom(NavigationEventArgs e)\n{\n base.OnNavigatedFrom(e);\n await CleanUpAsync();\n}\n\nprivate async void Application_Suspending(object? sender, SuspendingEventArgs e)\n{\n if (Frame?.CurrentSourcePageType == typeof(CameraPreviewSample))\n {\n var deferral = e.SuspendingOperation.GetDeferral();\n await CleanUpAsync();\n deferral.Complete();\n }\n}\n\nprivate async void Application_Resuming(object? sender, object e)\n{\n if (CameraPreviewControl != null)\n {\n var cameraHelper = CameraPreviewControl.CameraHelper;\n CameraPreviewControl.PreviewFailed \u002B= CameraPreviewControl_PreviewFailed!;\n await CameraPreviewControl.StartAsync(cameraHelper);\n CameraPreviewControl.CameraHelper.FrameArrived \u002B= CameraPreviewControl_FrameArrived!;\n }\n}\n\nprivate void CameraPreviewControl_FrameArrived(object? sender, FrameEventArgs e)\n{\n _currentVideoFrame = e.VideoFrame;\n}\n\nprivate void CameraPreviewControl_PreviewFailed(object? sender, PreviewFailedEventArgs e)\n{\n ErrorBar.Message = e.Error;\n ErrorBar.IsOpen = true;\n}\n\nprivate async void CaptureButton_Click(object? sender, RoutedEventArgs e)\n{\n var softwareBitmap = _currentVideoFrame?.SoftwareBitmap;\n if (softwareBitmap != null)\n {\n if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 || softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)\n {\n softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);\n }\n\n await _softwareBitmapSource!.SetBitmapAsync(softwareBitmap);\n }\n}\n\nprivate void UnsubscribeFromEvents()\n{\n if (CameraPreviewControl.CameraHelper != null)\n {\n CameraPreviewControl.CameraHelper.FrameArrived -= CameraPreviewControl_FrameArrived!;\n }\n\n CameraPreviewControl.PreviewFailed -= CameraPreviewControl_PreviewFailed!;\n}\n\nprivate async Task CleanUpAsync()\n{\n UnsubscribeFromEvents();\n CameraPreviewControl.Stop();\n await CameraPreviewControl.CameraHelper.CleanUpAsync();\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "CameraPreviewSample",
+ "sourcePath": "components/CameraPreview/samples/CameraPreviewSample.xaml",
+ "optionsResolved": [
+ "ShowCamera=True"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "advancedcollectionview",
+ "name": "AdvancedCollectionView",
+ "description": "The AdvancedCollectionView is a collection view implementation that support filtering, sorting and incremental loading. It\u0027s meant to be used in a view or viewmodel.",
+ "nugetPackage": "CommunityToolkit.WinUI.Collections",
+ "usings": [
+ "CommunityToolkit.WinUI.Collections",
+ "System.Diagnostics.CodeAnalysis"
+ ],
+ "curatedKeywords": [
+ "AdvancedCollectionView",
+ "CollectionViewSource",
+ "data",
+ "sorting",
+ "filtering",
+ "Collections"
+ ],
+ "keywords": [
+ "Helpers",
+ "Data"
+ ],
+ "docs": [
+ {
+ "title": "AdvancedCollectionView",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Collections/samples/AdvancedCollectionView.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Collections",
+ "documentPath": "components/Collections/samples/AdvancedCollectionView.md",
+ "category": "Helpers",
+ "subcategory": "Data",
+ "author": "nmetulev"
+ },
+ "samples": [
+ {
+ "header": "AdvancedCollectionView",
+ "details": "The following is a complete example of how to perform ...",
+ "xaml": "\u003CGrid ColumnSpacing=\u002212\u0022\n RowSpacing=\u00228\u0022\u003E\n \u003CGrid.Resources\u003E\n \u003CStyle x:Name=\u0022CardStyle\u0022\n TargetType=\u0022Grid\u0022\u003E\n \u003CStyle.Setters\u003E\n \u003CSetter Property=\u0022Background\u0022 Value=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderBrush\u0022 Value=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderThickness\u0022 Value=\u00221\u0022 /\u003E\n \u003CSetter Property=\u0022CornerRadius\u0022 Value=\u0022{StaticResource ControlCornerRadius}\u0022 /\u003E\n \u003C/Style.Setters\u003E\n \u003C/Style\u003E\n \u003CDataTemplate x:Key=\u0022EmployeeDataTemplate\u0022\n x:DataType=\u0022local:Employee\u0022\u003E\n \u003CTextBlock Text=\u0022{x:Bind Name}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/Grid.Resources\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition /\u003E\n \u003CColumnDefinition /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition /\u003E\n \u003C/Grid.RowDefinitions\u003E\n \u003CStackPanel Grid.Column=\u00220\u0022\n Grid.ColumnSpan=\u00222\u0022\n Margin=\u00220,24,0,24\u0022\n Orientation=\u0022Horizontal\u0022\n Spacing=\u00228\u0022\u003E\n \u003CTextBox x:Name=\u0022NewItemBox\u0022\n MinWidth=\u0022320\u0022\n PlaceholderText=\u0022Add new item\u0022 /\u003E\n \u003CButton x:Name=\u0022AddButton\u0022\n VerticalAlignment=\u0022Bottom\u0022\n Click=\u0022Add_Click\u0022\n Content=\u0022Add\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003CTextBlock Grid.Row=\u00221\u0022\n Text=\u0022Original list\u0022 /\u003E\n \u003CGrid Grid.Row=\u00222\u0022\n Style=\u0022{StaticResource CardStyle}\u0022\u003E\n \u003CListView x:Name=\u0022LeftList\u0022\n ItemTemplate=\u0022{StaticResource EmployeeDataTemplate}\u0022\n ItemsSource=\u0022{x:Bind EmployeeCollection}\u0022 /\u003E\n \u003C/Grid\u003E\n \u003CTextBlock Grid.Row=\u00221\u0022\n Grid.Column=\u00221\u0022\n Text=\u0022Sorted and filtered list (bound via AdvancedCollectionView)\u0022 /\u003E\n \u003CGrid Grid.Row=\u00222\u0022\n Grid.Column=\u00221\u0022\n Style=\u0022{StaticResource CardStyle}\u0022\u003E\n \u003CListView x:Name=\u0022RightList\u0022\n ItemTemplate=\u0022{StaticResource EmployeeDataTemplate}\u0022\n ItemsSource=\u0022{x:Bind CollectionView}\u0022 /\u003E\n \u003C/Grid\u003E\n\u003C/Grid\u003E",
+ "code": "public ObservableCollection\u003CEmployee\u003E EmployeeCollection { get; private set; }\n\npublic AdvancedCollectionView CollectionView { get; private set; }\n\npublic AdvancedCollectionViewSample()\n{\n Setup();\n}\n\n[MemberNotNull(nameof(EmployeeCollection))]\n[MemberNotNull(nameof(CollectionView))]\nprivate void Setup()\n{\n // left list\n EmployeeCollection = new()\n {\n new()\n {\n Name = \u0022Staff\u0022\n },\n new()\n {\n Name = \u002242\u0022\n },\n new()\n {\n Name = \u0022Swan\u0022\n },\n new()\n {\n Name = \u0022Orchid\u0022\n },\n new()\n {\n Name = \u002215\u0022\n },\n new()\n {\n Name = \u0022Flame\u0022\n },\n new()\n {\n Name = \u002216\u0022\n },\n new()\n {\n Name = \u0022Arrow\u0022\n },\n new()\n {\n Name = \u0022Tempest\u0022\n },\n new()\n {\n Name = \u002223\u0022\n },\n new()\n {\n Name = \u0022Pearl\u0022\n },\n new()\n {\n Name = \u0022Hydra\u0022\n },\n new()\n {\n Name = \u0022Lamp Post\u0022\n },\n new()\n {\n Name = \u00224\u0022\n },\n new()\n {\n Name = \u0022Looking Glass\u0022\n },\n new()\n {\n Name = \u00228\u0022\n },\n };\n // right list\n AdvancedCollectionView acv = new(EmployeeCollection);\n acv.Filter = x =\u003E !int.TryParse(((Employee)x).Name, out _);\n acv.SortDescriptions.Add(new SortDescription\u003CEmployee\u003E(nameof(Employee.Name), SortDirection.Ascending));\n CollectionView = acv;\n}\n\nprivate void Add_Click(object sender, RoutedEventArgs e)\n{\n if (!string.IsNullOrWhiteSpace(NewItemBox.Text))\n {\n EmployeeCollection.Insert(0, new Employee { Name = NewItemBox.Text });\n NewItemBox.Text = \u0022\u0022;\n }\n}\n\n/// \u003Csummary\u003E\n/// A sample class used to show how to use the \u003Csee cref = \u0022AdvancedCollectionView\u0022/\u003E class.\n/// \u003C/summary\u003E\npublic partial class Employee\n{\n /// \u003Csummary\u003E\n /// Gets or sets the name of the person.\n /// \u003C/summary\u003E\n public string? Name { get; set; }\n}",
+ "language": "csharp",
+ "toolkit": {
+ "sampleId": "AdvancedCollectionViewSample",
+ "sourcePath": "components/Collections/samples/AdvancedCollectionViewSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "incrementalloadingcollection",
+ "name": "IncrementalLoadingCollection",
+ "description": "The IncrementalLoadingCollection helpers greatly simplify the definition and usage of collections whose items can be loaded incrementally only when needed by the view (such as a ScrollViewer).",
+ "nugetPackage": "CommunityToolkit.WinUI.Collections",
+ "usings": [
+ "CommunityToolkit.WinUI.Collections"
+ ],
+ "curatedKeywords": [
+ "IncrementalLoadingCollection",
+ "Control",
+ "Data",
+ "Incremental",
+ "Loading",
+ "Collections"
+ ],
+ "keywords": [
+ "Helpers",
+ "Data"
+ ],
+ "docs": [
+ {
+ "title": "IncrementalLoadingCollection",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Collections/samples/IncrementalLoadingCollection.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Collections",
+ "documentPath": "components/Collections/samples/IncrementalLoadingCollection.md",
+ "category": "Helpers",
+ "subcategory": "Data",
+ "author": "nmetulev"
+ },
+ "samples": [
+ {
+ "header": "Incremental Loading Collection",
+ "details": "A sample for showing how to create and use a IncrementalLoadingCollection.",
+ "xaml": "\u003CGrid MaxWidth=\u0022460\u0022\n HorizontalAlignment=\u0022Left\u0022\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022*\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n\n \u003CStackPanel HorizontalAlignment=\u0022Left\u0022\u003E\n \u003CTextBlock Text=\u0022Items are loaded incrementally when the view needs to show them (i.e., when the user scrolls the ListView)\u0022\n TextWrapping=\u0022Wrap\u0022 /\u003E\n \u003CButton Margin=\u00220,12,0,12\u0022\n Click=\u0022{x:Bind PeopleSource.RefreshAsync}\u0022\n Content=\u0022Refresh collection\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Is loading:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{x:Bind PeopleSource.IsLoading, Mode=OneWay}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Has more items:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{x:Bind PeopleSource.HasMoreItems, Mode=OneWay}\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003C/StackPanel\u003E\n\n \u003CGrid Grid.Row=\u00222\u0022\n MaxHeight=\u0022420\u0022\n Margin=\u00220,24,0,0\u0022\n VerticalAlignment=\u0022Top\u0022\n Background=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022\n BorderBrush=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\n CornerRadius=\u00224\u0022\u003E\n \u003CListView x:Name=\u0022PeopleListView\u0022\n ItemsSource=\u0022{x:Bind PeopleSource, Mode=OneWay}\u0022\u003E\n \u003CListView.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:Person\u0022\u003E\n \u003CGrid\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition Width=\u0022Auto\u0022 /\u003E\n \u003CColumnDefinition /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n \u003CImage Width=\u002224\u0022\n Height=\u002224\u0022\n VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/AppIcon.png\u0022 /\u003E\n \u003CTextBlock Grid.Column=\u00221\u0022\n Margin=\u002212\u0022\n VerticalAlignment=\u0022Center\u0022\n Text=\u0022{x:Bind Name}\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/DataTemplate\u003E\n \u003C/ListView.ItemTemplate\u003E\n \u003C/ListView\u003E\n \u003C/Grid\u003E\n\u003C/Grid\u003E",
+ "code": "// IncrementalLoadingCollection can be bound to a GridView or a ListView. In this case it is a ListView called PeopleListView.\npublic IncrementalLoadingCollection\u003CPeopleSource, Person\u003E PeopleSource { get; set; } = new(new PeopleSource());\n\n/// \u003Csummary\u003E\n/// A sample implementation of the \u003Csee cref = \u0022IIncrementalSource{TSource}\u0022/\u003E interface.\n/// \u003C/summary\u003E\n/// \u003Cseealso cref = \u0022IIncrementalSource{TSource}\u0022/\u003E\npublic partial class PeopleSource : IIncrementalSource\u003CPerson\u003E\n{\n private readonly List\u003CPerson\u003E _people;\n /// \u003Csummary\u003E\n /// Initializes a new instance of the \u003Csee cref = \u0022PeopleSource\u0022/\u003E class.\n /// \u003C/summary\u003E\n public PeopleSource()\n {\n // Creates an example collection.\n _people = new List\u003CPerson\u003E();\n for (int i = 1; i \u003C= 200; i\u002B\u002B)\n {\n var p = new Person\n {\n Name = \u0022Person \u0022 \u002B i\n };\n _people.Add(p);\n }\n }\n\n /// \u003Csummary\u003E\n /// Retrieves items based on \u003Cparamref name = \u0022pageIndex\u0022/\u003E and \u003Cparamref name = \u0022pageSize\u0022/\u003E arguments.\n /// \u003C/summary\u003E\n /// \u003Cparam name = \u0022pageIndex\u0022\u003E\n /// The zero-based index of the page that corresponds to the items to retrieve.\n /// \u003C/param\u003E\n /// \u003Cparam name = \u0022pageSize\u0022\u003E\n /// The number of \u003Csee cref = \u0022Person\u0022/\u003E items to retrieve for the specified \u003Cparamref name = \u0022pageIndex\u0022/\u003E.\n /// \u003C/param\u003E\n /// \u003Cparam name = \u0022cancellationToken\u0022\u003E\n /// Used to propagate notification that operation should be canceled.\n /// \u003C/param\u003E\n /// \u003Creturns\u003E\n /// Returns a collection of \u003Csee cref = \u0022Person\u0022/\u003E.\n /// \u003C/returns\u003E\n public async Task\u003CIEnumerable\u003CPerson\u003E\u003E GetPagedItemsAsync(int pageIndex, int pageSize, CancellationToken cancellationToken = default(CancellationToken))\n {\n // Gets items from the collection according to pageIndex and pageSize parameters.\n var result = (\n from p in _people\n select p).Skip(pageIndex * pageSize).Take(pageSize);\n // Simulates a longer request...\n // Make sure the list is still in order after a refresh,\n // even if the first page takes longer to load\n if (pageIndex == 0)\n {\n await Task.Delay(2000);\n }\n else\n {\n await Task.Delay(1000);\n }\n\n return result;\n }\n}\n\n/// \u003Csummary\u003E\n/// A sample class used to show how to use the \u003Csee cref = \u0022IIncrementalSource{TSource}\u0022/\u003E interface.\n/// \u003C/summary\u003E\npublic partial class Person\n{\n /// \u003Csummary\u003E\n /// Gets or sets the name of the person.\n /// \u003C/summary\u003E\n public string? Name { get; set; }\n}",
+ "language": "csharp",
+ "toolkit": {
+ "sampleId": "IncrementalLoadingCollectionSample",
+ "sourcePath": "components/Collections/samples/IncrementalLoadingCollectionSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "colorpicker",
+ "name": "ColorPicker",
+ "description": "An extended color picker control that lets a user pick a color using a color spectrum, sliders, or text input.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.ColorPicker",
+ "curatedKeywords": [
+ "ColorPicker",
+ "Control",
+ "Layout"
+ ],
+ "keywords": [
+ "Controls",
+ "Input"
+ ],
+ "docs": [
+ {
+ "title": "ColorPicker",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/ColorPicker/samples/ColorPicker.md"
+ }
+ ],
+ "toolkit": {
+ "component": "ColorPicker",
+ "documentPath": "components/ColorPicker/samples/ColorPicker.md",
+ "category": "Controls",
+ "subcategory": "Input",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "ColorPicker",
+ "details": "The ColorPicker control lets a user pick a color using a color spectrum, palette, sliders, or text input.",
+ "xaml": "\u003CGrid\u003E\n \u003Ccontrols:ColorPicker HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Top\u0022\n IsAlphaEnabled=\u0022True\u0022\n IsAlphaSliderVisible=\u0022True\u0022\n IsColorChannelTextInputVisible=\u0022True\u0022\n IsColorPaletteVisible=\u0022True\u0022\n IsColorSliderVisible=\u0022True\u0022\n IsColorSpectrumVisible=\u0022True\u0022\n ShowAccentColors=\u0022True\u0022\n Color=\u0022LightBlue\u0022 /\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ColorPickerSample",
+ "sourcePath": "components/ColorPicker/samples/ColorPickerSample.xaml",
+ "optionsResolved": [
+ "AccentColors=True",
+ "AlphaEnabled=True",
+ "AlphaSlider=True",
+ "ColorChannel=True",
+ "ColorPalette=True",
+ "ColorSlider=True",
+ "SpectrumVisible=True"
+ ],
+ "optionBindingsDropped": [
+ "ColorSpectrumShape"
+ ]
+ }
+ },
+ {
+ "header": "ColorPickerButton",
+ "details": "A sample for showing how to create and use a ColorPickerButton control.",
+ "xaml": "\u003CGrid\u003E\n \u003Ccontrols:ColorPickerButton HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Top\u0022\n SelectedColor=\u0022LightBlue\u0022 /\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ColorPickerButtonSample",
+ "sourcePath": "components/ColorPicker/samples/ColorPickerButtonSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "converters",
+ "name": "Converters",
+ "description": "Commonly used converters that allow the data to be modified as it passes through the binding engine.",
+ "nugetPackage": "CommunityToolkit.WinUI.Converters",
+ "curatedKeywords": [
+ "Converters",
+ "Control",
+ "Layout"
+ ],
+ "keywords": [
+ "Xaml",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "Converters",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Converters/samples/Converters.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Converters",
+ "documentPath": "components/Converters/samples/Converters.md",
+ "category": "Xaml",
+ "subcategory": "Miscellaneous",
+ "author": "niels9001"
+ },
+ "samples": [
+ {
+ "header": "BoolNegationConverter",
+ "details": "Converts a boolean to the inverse value (True to False and vice versa)",
+ "xaml": "\u003CButton Content=\u0022Inverted value set as IsEnabled\u0022\n IsEnabled=\u0022True\u0022\u003E\n \u003CButton.Resources\u003E\n \u003Cconverters:BoolNegationConverter x:Key=\u0022BoolNegationConverter\u0022 /\u003E\n \u003C/Button.Resources\u003E\n\u003C/Button\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BoolNegationConverterSample",
+ "sourcePath": "components/Converters/samples/BoolNegationConverterSample.xaml",
+ "optionsResolved": [
+ "MyBooleanValue=True"
+ ]
+ }
+ },
+ {
+ "header": "BoolToObjectConverter",
+ "details": "Converts a boolean value into an other object. Can be used to convert true/false to e.g. visibility, color, or different images.",
+ "xaml": "\u003CBorder Width=\u002264\u0022\n Height=\u002264\u0022\n HorizontalAlignment=\u0022Left\u0022\n Background=\u0022False\u0022\n CornerRadius=\u00224\u0022\u003E\n \u003CBorder.Resources\u003E\n \u003Cconverters:BoolToObjectConverter x:Key=\u0022BoolToObjectConverter\u0022\n FalseValue=\u0022{ThemeResource ControlStrongFillColorDefaultBrush}\u0022\n TrueValue=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022 /\u003E\n \u003C/Border.Resources\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BoolToObjectConverterSample",
+ "sourcePath": "components/Converters/samples/BoolToObjectConverterSample.xaml",
+ "optionsResolved": [
+ "MyBooleanValue=False"
+ ]
+ }
+ },
+ {
+ "header": "BoolToVisibilityConverter",
+ "details": "Converts a boolean value into a Visibility enumeration. The ConverterParameter can be used to invert the logic.",
+ "xaml": "\u003CStackPanel Orientation=\u0022Horizontal\u0022\n Spacing=\u002212\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003CStyle x:Key=\u0022PlaceholderCardStyle\u0022\n TargetType=\u0022Border\u0022\u003E\n \u003CStyle.Setters\u003E\n \u003CSetter Property=\u0022Width\u0022 Value=\u002264\u0022 /\u003E\n \u003CSetter Property=\u0022Height\u0022 Value=\u002264\u0022 /\u003E\n \u003CSetter Property=\u0022Background\u0022 Value=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderBrush\u0022 Value=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderThickness\u0022 Value=\u00221\u0022 /\u003E\n \u003C/Style.Setters\u003E\n \u003C/Style\u003E\n\n \u003Cconverters:BoolToVisibilityConverter x:Key=\u0022BoolToVisibilityConverter\u0022 /\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CBorder Style=\u0022{StaticResource PlaceholderCardStyle}\u0022\u003E\n \u003CImage HorizontalAlignment=\u0022Left\u0022\n Source=\u0022/Assets/Converters.png\u0022\n Visibility=\u0022True\u0022 /\u003E\n \u003C/Border\u003E\n\n \u003CBorder Style=\u0022{StaticResource PlaceholderCardStyle}\u0022\u003E\n \u003CImage HorizontalAlignment=\u0022Left\u0022\n Source=\u0022/Assets/Converters.png\u0022\n Visibility=\u0022True\u0022 /\u003E\n \u003C/Border\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BoolToVisibilityConverterSample",
+ "sourcePath": "components/Converters/samples/BoolToVisibilityConverterSample.xaml",
+ "optionsResolved": [
+ "MyBooleanValue=True"
+ ]
+ }
+ },
+ {
+ "header": "ColorToDisplayNameConverter",
+ "details": "Converts a color to the approximated display name.",
+ "xaml": "\u003CStackPanel HorizontalAlignment=\u0022Left\u0022\n Spacing=\u002216\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003Cconverters:ColorToDisplayNameConverter x:Key=\u0022ColorToDisplayNameConverter\u0022 /\u003E\n \u003C/StackPanel.Resources\u003E\n\n \u003CColorPicker x:Name=\u0022colorPicker\u0022\n Height=\u002264\u0022\n VerticalAlignment=\u0022Top\u0022\n ColorSpectrumShape=\u0022Box\u0022\n IsAlphaEnabled=\u0022False\u0022\n IsAlphaSliderVisible=\u0022True\u0022\n IsAlphaTextInputVisible=\u0022True\u0022\n IsColorChannelTextInputVisible=\u0022False\u0022\n IsColorSliderVisible=\u0022False\u0022\n IsHexInputVisible=\u0022False\u0022\n IsMoreButtonVisible=\u0022False\u0022 /\u003E\n \u003CTextBlock Text=\u0022{Binding ElementName=colorPicker, Path=Color, Converter={StaticResource ColorToDisplayNameConverter}}\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ColorToDisplayNameConverterSample",
+ "sourcePath": "components/Converters/samples/ColorToDisplayNameConverterSample.xaml"
+ }
+ },
+ {
+ "header": "DoubleToObjectConverter",
+ "details": "Converts a double value into an other object. Can be used to convert doubles to e.g. visibility, colors , or different images. If GreaterThan and LessThan are both set, the logic looks for a value between the two values. Otherwise the logic looks for the value to be GreaterThan or LessThan the specified value.",
+ "xaml": "\u003CBorder Width=\u002264\u0022\n Height=\u002264\u0022\n HorizontalAlignment=\u0022Left\u0022\n Background=\u00220\u0022\n CornerRadius=\u00224\u0022\u003E\n \u003CBorder.Resources\u003E\n \u003Cconverters:DoubleToObjectConverter x:Key=\u0022DoubleToObjectConverter\u0022\n FalseValue=\u0022{ThemeResource SystemFillColorSuccessBrush}\u0022\n GreaterThan=\u00223\u0022\n LessThan=\u00227\u0022\n TrueValue=\u0022{ThemeResource SystemFillColorCriticalBrush}\u0022 /\u003E\n \u003C/Border.Resources\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "DoubleToObjectConverterSample",
+ "sourcePath": "components/Converters/samples/DoubleToObjectConverterSample.xaml",
+ "optionsResolved": [
+ "NumericValue=0"
+ ]
+ }
+ },
+ {
+ "header": "DoubleToVisibilityConverter",
+ "details": "Converts a double value into an other object. Can be used to convert doubles to e.g. visibility, colors , or different images. If GreaterThan and LessThan are both set, the logic looks for a value between the two values. Otherwise the logic looks for the value to be GreaterThan or LessThan the specified value.",
+ "xaml": "\u003CGrid Width=\u002264\u0022\n Height=\u002264\u0022\n HorizontalAlignment=\u0022Left\u0022\n Background=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022\n BorderBrush=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\u003E\n \u003CGrid.Resources\u003E\n \u003Cconverters:DoubleToVisibilityConverter x:Key=\u0022DoubleToVisibilityConverter\u0022\n GreaterThan=\u00225\u0022 /\u003E\n \u003C/Grid.Resources\u003E\n \u003CImage HorizontalAlignment=\u0022Left\u0022\n Source=\u0022/Assets/Converters.png\u0022\n Visibility=\u00220\u0022 /\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "DoubleToVisibilityConverterSample",
+ "sourcePath": "components/Converters/samples/DoubleToVisibilityConverterSample.xaml",
+ "optionsResolved": [
+ "NumericValue=0"
+ ]
+ }
+ },
+ {
+ "header": "CollectionVisibilityConverter",
+ "details": "Converts a collection size into an other object. Can be used to convert to bind a visibility, a color or an image to the size of the collection. The CollectionVisibilityConverter is derived from it and can be easily used to convert a collection into a Visibility enumeration (Collapsed if the given collection is empty or null). Note: this converter only checks the initial state of a collection and does not detect any updates. For that scenario, using a IsNullOrEmptyStateTrigger is more appropriate.",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.Resources\u003E\n \u003Cconverters:CollectionVisibilityConverter x:Key=\u0022CollectionVisibilityConverter\u0022 /\u003E\n \u003C/Grid.Resources\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n TextAlignment=\u0022Center\u0022\n Visibility=\u0022{x:Bind EmptyCollection, Mode=OneWay, Converter={StaticResource CollectionVisibilityConverter}, ConverterParameter=True}\u0022\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022All done for the day\u0022 /\u003E\n \u003CLineBreak /\u003E\n \u003CRun Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Text=\u0022Enjoy your empty inbox\u0022 /\u003E\n \u003C/TextBlock\u003E\n\u003C/Grid\u003E",
+ "code": "public ObservableCollection\u003Cstring\u003E EmptyCollection = new();",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "CollectionVisibilityConverterSample",
+ "sourcePath": "components/Converters/samples/CollectionVisibilityConverterSample.xaml"
+ }
+ },
+ {
+ "header": "EmptyStringToObjectConverter",
+ "details": "Converts an object value into a an object (if the value is null returns the false value). Can be used to bind a visibility, a color or an image to the value of an object. The EmptyStringToObjectConverter converts a string value into a an object (if the value is null or empty returns the false value). Can be used to bind a visibility, a color or an image to the value of a string.",
+ "xaml": "\u003CStackPanel\u003E\n \u003CStackPanel.Resources\u003E\n \u003Cconverters:EmptyStringToObjectConverter x:Key=\u0022EmptyStringToObjectConverter\u0022\n EmptyValue=\u0022False\u0022\n NotEmptyValue=\u0022True\u0022 /\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CButton Content=\u0022Submit\u0022\n IsEnabled=\u0022\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "EmptyStringToObjectConverterSample",
+ "sourcePath": "components/Converters/samples/EmptyStringToObjectConverterSample.xaml",
+ "optionsResolved": [
+ "MyTextStringValue="
+ ]
+ }
+ },
+ {
+ "header": "FileSizeToFriendlyStringConverter",
+ "details": "Converts a file size in bytes to a more human-readable friendly format using ToFileSizeString(Int64).",
+ "xaml": "\u003CStackPanel Orientation=\u0022Vertical\u0022\n Spacing=\u002212\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003Cconverters:FileSizeToFriendlyStringConverter x:Key=\u0022FileSizeToFriendlyStringConverter\u0022 /\u003E\n \u003C/StackPanel.Resources\u003E\n\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\u003E\n \u003CRun Text=\u0022Original value (Bytes):\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{x:Bind FileSizeInBytes}\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Converted string:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{x:Bind FileSizeInBytes, Converter={StaticResource FileSizeToFriendlyStringConverter}}\u0022 /\u003E\n \u003C/TextBlock\u003E\n\u003C/StackPanel\u003E",
+ "code": "public long FileSizeInBytes = 2400000;",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "FileSizeToFriendlyStringConverterSample",
+ "sourcePath": "components/Converters/samples/FileSizeToFriendlyStringConverterSample.xaml"
+ }
+ },
+ {
+ "header": "StringFormatConverter",
+ "details": "This allows you to format a string property upon binding wrapping string.Format. It only allows for a single input value (the binding string), but can be formatted with the regular string.Format methods.",
+ "xaml": "\u003CStackPanel Spacing=\u002216\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003Cconverters:StringFormatConverter x:Key=\u0022StringFormatConverter\u0022 /\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CTextBlock Text=\u0022True\u0022 /\u003E\n\n \u003CTextBlock Text=\u00221\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StringFormatConverterSample",
+ "sourcePath": "components/Converters/samples/StringFormatConverterSample.xaml",
+ "optionsResolved": [
+ "MyBooleanValue=True",
+ "MyDoubleValue=1"
+ ]
+ }
+ },
+ {
+ "header": "StringVisibilityConverter",
+ "details": "Converts a string value into a Visibility value (if the value is null or empty returns a collapsed value).",
+ "xaml": "\u003CTextBlock Text=\u0022This text is visible when the input TextBox is empty\u0022\n Visibility=\u0022\u0022\u003E\n \u003CTextBlock.Resources\u003E\n \u003Cconverters:StringVisibilityConverter x:Key=\u0022StringVisibilityConverter\u0022 /\u003E\n \u003C/TextBlock.Resources\u003E\n\u003C/TextBlock\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StringVisibilityConverterSample",
+ "sourcePath": "components/Converters/samples/StringVisibilityConverterSample.xaml",
+ "optionsResolved": [
+ "MyTextStringValue="
+ ]
+ }
+ },
+ {
+ "header": "TypeToObjectConverter",
+ "details": "Returns an object or another, depending on whether the type of the provided value matches another provided Type.",
+ "xaml": "\u003CStackPanel Spacing=\u00228\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003Cconverters:TypeToObjectConverter x:Key=\u0022TypeToObjectConverter\u0022\n FalseValue=\u0022Red\u0022\n TrueValue=\u0022Green\u0022\n Type=\u0022x:String\u0022 /\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CComboBox x:Name=\u0022Selection\u0022\n SelectedIndex=\u00220\u0022\u003E\n \u003CComboBoxItem\u003E\n \u003Cx:Int32\u003E64\u003C/x:Int32\u003E\n \u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003E\n \u003Cx:Double\u003E44.44\u003C/x:Double\u003E\n \u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EI\u0027m a String!\u003C/ComboBoxItem\u003E\n \u003C/ComboBox\u003E\n\n \u003CTextBlock Foreground=\u0022{x:Bind ((ComboBoxItem)Selection.SelectedItem).Content, Mode=OneWay, Converter={StaticResource TypeToObjectConverter}}\u0022\n Text=\u0022This text is Green when a string is selected\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "TypeToObjectConverterSample",
+ "sourcePath": "components/Converters/samples/TypeToObjectConverterSample.xaml"
+ }
+ },
+ {
+ "header": "VisibilityToBoolConverter",
+ "details": "Converts a Visibility enumeration to a boolean value.",
+ "xaml": "\u003CStackPanel Spacing=\u002212\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003CStyle x:Key=\u0022PlaceholderCardStyle\u0022\n TargetType=\u0022Border\u0022\u003E\n \u003CStyle.Setters\u003E\n \u003CSetter Property=\u0022Width\u0022 Value=\u002264\u0022 /\u003E\n \u003CSetter Property=\u0022Height\u0022 Value=\u002264\u0022 /\u003E\n \u003CSetter Property=\u0022Background\u0022 Value=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderBrush\u0022 Value=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderThickness\u0022 Value=\u00221\u0022 /\u003E\n \u003C/Style.Setters\u003E\n \u003C/Style\u003E\n\n \u003Cconverters:VisibilityToBoolConverter x:Key=\u0022VisibilityToBoolConverter\u0022 /\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CBorder Style=\u0022{StaticResource PlaceholderCardStyle}\u0022\u003E\n \u003CImage x:Name=\u0022image\u0022\n HorizontalAlignment=\u0022Left\u0022\n Source=\u0022/Assets/Converters.png\u0022\n Visibility=\u0022True\u0022 /\u003E\n \u003C/Border\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Image visibility:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding ElementName=image, Path=Visibility, Mode=OneWay, Converter={StaticResource VisibilityToBoolConverter}}\u0022 /\u003E\n \u003C/TextBlock\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "VisibilityToBoolConverterSample",
+ "sourcePath": "components/Converters/samples/VisibilityToBoolConverterSample.xaml",
+ "optionsResolved": [
+ "MyBooleanValue=True"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "developertools",
+ "name": "Developer Tools",
+ "description": "The FocusTracker and AlignmentGrid help you to get more information about and aligning UI elements.",
+ "nugetPackage": "CommunityToolkit.WinUI.DeveloperTools",
+ "curatedKeywords": [
+ "DeveloperTools",
+ "FocusTracker",
+ "AlignmentGrid",
+ "dev tools",
+ "controls"
+ ],
+ "keywords": [
+ "Helpers",
+ "Developer"
+ ],
+ "docs": [
+ {
+ "title": "Developer Tools",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/DeveloperTools/samples/DeveloperTools.md"
+ }
+ ],
+ "toolkit": {
+ "component": "DeveloperTools",
+ "documentPath": "components/DeveloperTools/samples/DeveloperTools.md",
+ "category": "Helpers",
+ "subcategory": "Developer",
+ "author": "nmetulev"
+ },
+ "samples": [
+ {
+ "header": "AlignmentGrid",
+ "details": "The AlignmentGrid Control can be used to display a grid to help with aligning controls. You can control the grid\u0027s steps with HorizontalStep and VerticalStep properties. Line color can be defined with LineBrush property.",
+ "xaml": "\u003CGrid\u003E\n \u003CdeveloperTools:AlignmentGrid HorizontalStep=\u002220\u0022\n Opacity=\u00220.2\u0022\n VerticalStep=\u002220\u0022 /\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:developerTools=\u0022using:CommunityToolkit.WinUI.DeveloperTools\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AlignmentGridSample",
+ "sourcePath": "components/DeveloperTools/samples/AlignmentGridSample.xaml",
+ "optionsResolved": [
+ "HorizontalStep=20",
+ "OpacitySetting=0.2",
+ "VerticalStep=20"
+ ]
+ }
+ },
+ {
+ "header": "FocusTracker",
+ "details": "The FocusTracker Control can be used to display information about the current focused XAML element (if any). FocusTracker will display the following information (when available) about the current focused XAML element: - Name - Type - AutomationProperties.Name - Name of the first parent in hierarchy with a name",
+ "xaml": "\u003CStackPanel Spacing=\u002212\u0022\u003E\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022Use the TAB key or mouse to set the focus on a UI element, and see the result in the Focus tracker below\u0022 /\u003E\n \u003CdeveloperTools:FocusTracker IsActive=\u0022True\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:developerTools=\u0022using:CommunityToolkit.WinUI.DeveloperTools\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "FocusTrackerSample",
+ "sourcePath": "components/DeveloperTools/samples/FocusTrackerSample.xaml",
+ "optionsResolved": [
+ "IsActive=True"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "arrayextensions",
+ "name": "Array Extensions",
+ "description": "Provides a few helpers for dealing with multidimensional and jagged arrays. Also, provides string helpers for debug output.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "Extensions",
+ "array"
+ ],
+ "keywords": [
+ "Extensions",
+ "Math"
+ ],
+ "docs": [
+ {
+ "title": "Array Extensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/ArrayExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/ArrayExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Math",
+ "author": "michael-hawker"
+ },
+ "samples": []
+ },
+ {
+ "id": "attacheddropshadow",
+ "name": "Attached Drop Shadow",
+ "description": "Allows many elements to share a common backdrop for casting shadows.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "shadow",
+ "shadows",
+ "dropshadow",
+ "dropshadowpanel",
+ "attachedshadow",
+ "attacheddropshadow",
+ "attachedcardshadow"
+ ],
+ "keywords": [
+ "Extensions",
+ "Shadows"
+ ],
+ "docs": [
+ {
+ "title": "Attached Drop Shadow",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/AttachedDropShadow.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/AttachedDropShadow.md",
+ "category": "Extensions",
+ "subcategory": "Shadows",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "Basic Attached Drop Shadow",
+ "details": "Creating a shadow is fairly straight-forward. First, you need to designate a _sibling_ element to sit behind all elements which you\u0027d like to have casing shadows. This single element can host as many shadows as required by other elements casting shadows. The parent element **cannot** be used, as otherwise your shadows will appear on top of your controls. With the target element setup, you can simply attach a shadow to any number of other elements within your panel you\u0027d like!",
+ "xaml": "\u003CGrid\u003E\n \u003C!--\n The ShadowTarget Border here is a *sibling* element behind where our elements which will cast\n shadows are located, this is important as otherwise if we used a parent element the\n shadows would appear on top of our elements instead!\n --\u003E\n \u003CBorder x:Name=\u0022ShadowTarget\u0022 /\u003E\n \u003CBorder Width=\u0022100\u0022\n Height=\u0022100\u0022\n BorderBrush=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\n CornerRadius=\u002232\u0022\u003E\n \u003CBorder.Background\u003E\n \u003CImageBrush ImageSource=\u0022ms-appx:///Assets/Llama.jpg\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003Cui:Effects.Shadow\u003E\n \u003Cui:AttachedDropShadow CastTo=\u0022{x:Bind ShadowTarget}\u0022\n CornerRadius=\u002232\u0022\n Offset=\u00224,4\u0022 /\u003E\n \u003C/ui:Effects.Shadow\u003E\n \u003C/Border\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AttachedDropShadowBasicSample",
+ "sourcePath": "components/Extensions/samples/Shadows/AttachedDropShadowBasicSample.xaml"
+ }
+ },
+ {
+ "header": "Attached Drop Shadow as Resource",
+ "details": "Creating a shadow definition for each Shadow can be a bit cumbersome. Fortunately, you can define a shadow definition as a resource and reuse the same shadow look-and-feel anywhere you need it!",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.Resources\u003E\n \u003C!-- We expect all our elements to bind to the same target with this resource --\u003E\n \u003Cui:AttachedDropShadow x:Key=\u0022CommonShadow\u0022\n CastTo=\u0022{x:Bind ShadowTarget}\u0022\n Offset=\u00224\u0022 /\u003E\n \u003C/Grid.Resources\u003E\n \u003CBorder x:Name=\u0022ShadowTarget\u0022 /\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\n Spacing=\u002232\u0022\u003E\n \u003CImage Width=\u0022100\u0022\n Height=\u0022100\u0022\n ui:Effects.Shadow=\u0022{StaticResource CommonShadow}\u0022\n Source=\u0022ms-appx:///Assets/Llama.jpg\u0022 /\u003E\n \u003CImage Width=\u0022100\u0022\n Height=\u0022100\u0022\n ui:Effects.Shadow=\u0022{StaticResource CommonShadow}\u0022\n Source=\u0022ms-appx:///Assets/Llama.jpg\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AttachedDropShadowResourceSample",
+ "sourcePath": "components/Extensions/samples/Shadows/AttachedDropShadowResourceSample.xaml"
+ }
+ },
+ {
+ "header": "Attached Drop Shadow as Style",
+ "details": "You can also use that same resource within a style on your page (for app-level see AttachedCardShadow). Or define your own definition within your style as we\u0027ve done here:",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.Resources\u003E\n \u003C!-- First define a shadow resource with a target as we did in the previous example --\u003E\n \u003Cui:AttachedDropShadow x:Key=\u0022CommonShadow\u0022\n CastTo=\u0022{x:Bind ShadowTarget}\u0022\n Offset=\u00224\u0022 /\u003E\n\n \u003C!-- Because we have to specify the shadow target, this style can at most be at the Page level --\u003E\n \u003CStyle BasedOn=\u0022{StaticResource AccentButtonStyle}\u0022\n TargetType=\u0022Button\u0022\u003E\n \u003C!-- We must use an existing resource (defined above) as a style value here --\u003E\n \u003CSetter Property=\u0022ui:Effects.Shadow\u0022 Value=\u0022{StaticResource CommonShadow}\u0022 /\u003E\n \u003CSetter Property=\u0022HorizontalAlignment\u0022 Value=\u0022Center\u0022 /\u003E\n \u003C/Style\u003E\n \u003C/Grid.Resources\u003E\n \u003CBorder x:Name=\u0022ShadowTarget\u0022 /\u003E\n \u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003C!--\n All buttons on this page have the shadow from the common style!\n The Shadow definition is Shared!\n --\u003E\n \u003CButton Content=\u0022I have a Shadow!\u0022 /\u003E\n \u003CButton Content=\u0022I also have a Shadow!\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AttachedDropShadowStyleSample",
+ "sourcePath": "components/Extensions/samples/Shadows/AttachedDropShadowStyleSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "attachedshadows",
+ "name": "Shadows overview",
+ "description": "Attached Shadows allow you to easily create shadow effects on elements.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "shadow",
+ "shadows",
+ "dropshadow",
+ "dropshadowpanel",
+ "attachedshadow",
+ "attacheddropshadow",
+ "attachedcardshadow"
+ ],
+ "keywords": [
+ "Extensions",
+ "Shadows"
+ ],
+ "docs": [
+ {
+ "title": "Shadows overview",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/AttachedShadows.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/AttachedShadows.md",
+ "category": "Extensions",
+ "subcategory": "Shadows",
+ "author": "michael-hawker"
+ },
+ "samples": []
+ },
+ {
+ "id": "dependencyobjectextensions",
+ "name": "DependencyObjectExtensions",
+ "description": "The DependencyObjectExtensions type provides a collection of extensions methods for DependencyObject objects to aid in using the VisualTreeHelper class.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "windows 10",
+ "uwp",
+ "windows community toolkit",
+ "uwp community toolkit",
+ "uwp toolkit",
+ "Visual Tree",
+ "extensions"
+ ],
+ "keywords": [
+ "Extensions",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "DependencyObjectExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/DependencyObjectExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/DependencyObjectExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Layout",
+ "author": "Sergio0694"
+ },
+ "samples": []
+ },
+ {
+ "id": "dispatcherqueueextensions",
+ "name": "DispatcherQueueExtensions",
+ "description": "Helpers for executing code on a specific UI thread through a DispatcherQueue instance.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "dispatcher",
+ "dispatcherqueue",
+ "DispatcherHelper",
+ "DispatcherQueueExtensions"
+ ],
+ "keywords": [
+ "Extensions",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "DispatcherQueueExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/DispatcherQueueExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/DispatcherQueueExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Miscellaneous",
+ "author": "Sergio0694"
+ },
+ "samples": []
+ },
+ {
+ "id": "dispatcherqueuetimerextensions",
+ "name": "DispatcherQueueTimerExtensions",
+ "description": "Helpers for executing code at specific times on a UI thread through a DispatcherQueue instance with a DispatcherQueueTimer.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "usings": [
+ "CommunityToolkit.WinUI",
+ "Microsoft.UI.Dispatching"
+ ],
+ "curatedKeywords": [
+ "dispatcher",
+ "dispatcherqueue",
+ "DispatcherHelper",
+ "DispatcherQueueExtensions",
+ "DispatcherQueueTimer",
+ "DispatcherQueueTimerExtensions"
+ ],
+ "keywords": [
+ "Extensions",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "DispatcherQueueTimerExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/DispatcherQueueTimerExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/DispatcherQueueTimerExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Miscellaneous",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "DispatcherQueueTimer Debounce Keyboard",
+ "details": "It can be used in a number of ways, but most simply like so as a keyboard limiter:",
+ "xaml": "\u003CStackPanel Spacing=\u00228\u0022\u003E\n \u003CTextBox PlaceholderText=\u0022Type here...\u0022\n TextChanged=\u0022TextBox_TextChanged\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022ResultText\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "code": "public DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();\n\nprivate void TextBox_TextChanged(object sender, TextChangedEventArgs e)\n{\n if (sender is TextBox textBox)\n {\n _debounceTimer.Debounce(() =\u003E\n {\n ResultText.Text = textBox.Text;\n }, //// i.e. if another keyboard press comes in within 120ms of the last, we\u0027ll wait before we fire off the request\n interval: TimeSpan.FromMilliseconds(Interval), //// If we\u0027re blanking out or the first character type, we\u0027ll start filtering immediately instead to appear more responsive.\n //// We want to switch back to trailing as the user types more so that we still capture all the input.\n immediate: textBox.Text.Length \u003C= 1);\n }\n}",
+ "language": "csharp",
+ "toolkit": {
+ "sampleId": "KeyboardDebounceSample",
+ "sourcePath": "components/Extensions/samples/DispatcherQueueTimerExtensions/KeyboardDebounceSample.xaml"
+ }
+ },
+ {
+ "header": "DispatcherQueueTimer Debounce Mouse",
+ "details": "Or for preventing multiple inputs from occuring accidentally (e.g. ignoring a double/multi-click):",
+ "xaml": "\u003CStackPanel Spacing=\u00228\u0022\u003E\n \u003CButton Click=\u0022Button_Click\u0022\n Content=\u0022Click Me\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022ResultText\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "code": "public DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();\n\nprivate int _count = 0;\n\nprivate void Button_Click(object sender, RoutedEventArgs e)\n{\n _debounceTimer.Debounce(() =\u003E\n {\n ResultText.Text = $\u0022You hit the button {\u002B\u002B_count} times!\u0022;\n }, interval: TimeSpan.FromMilliseconds(Interval), // By being on the leading edge, we ignore inputs past the first for the duration of the interval\n immediate: true);\n}",
+ "language": "csharp",
+ "toolkit": {
+ "sampleId": "MouseDebounceSample",
+ "sourcePath": "components/Extensions/samples/DispatcherQueueTimerExtensions/MouseDebounceSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "enumvaluesextension",
+ "name": "EnumValuesExtensions",
+ "description": "A markup extension that returns a collection of values of a specific enum type.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "XAML",
+ "markup",
+ "enum"
+ ],
+ "keywords": [
+ "Extensions",
+ "Markup"
+ ],
+ "docs": [
+ {
+ "title": "EnumValuesExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/EnumValuesExtension.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/EnumValuesExtension.md",
+ "category": "Extensions",
+ "subcategory": "Markup",
+ "author": "Sergio0694"
+ },
+ "samples": []
+ },
+ {
+ "id": "frameworkelementextensions",
+ "name": "FrameworkElement Extensions",
+ "description": "Provides attached dependency properties and extensions for the FrameworkElement type.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "FrameworkElement",
+ "extensions"
+ ],
+ "keywords": [
+ "Extensions",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "FrameworkElement Extensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/FrameworkElementExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/FrameworkElementExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Layout",
+ "author": "Sergio0694"
+ },
+ "samples": [
+ {
+ "header": "FrameworkElementAncestorSample",
+ "details": "The AncestorType attached property will walk the visual tree from the attached element for another element of the specified type. That value will be stored in the attached element\u0027s Ancestor property. This can then be used for binding to properties on the parent element. This is similar to the FindAncestor mode to RelativeSource data binding in WPF. Here is an example of how this can be used:",
+ "xaml": "\u003CStackPanel Padding=\u002216\u0022\n Spacing=\u0022{x:Bind ValueSlider.Value, Mode=OneWay}\u0022\u003E\n \u003CSlider x:Name=\u0022ValueSlider\u0022\n Maximum=\u002216\u0022\n Minimum=\u00224\u0022\n StepFrequency=\u00224\u0022\n Value=\u00228\u0022 /\u003E\n \u003CTextBlock Text=\u0022This is some text in a StackPanel with Spacing:\u0022 /\u003E\n \u003CTextBlock ui:FrameworkElementExtensions.AncestorType=\u0022StackPanel\u0022\n Text=\u0022{Binding (ui:FrameworkElementExtensions.Ancestor).Spacing, RelativeSource={RelativeSource Self}}\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "FrameworkElementAncestorSample",
+ "sourcePath": "components/Extensions/samples/FrameworkElementAncestorSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "hyperlinkextensions",
+ "name": "HyperlinkExtensions",
+ "description": "The HyperlinkExtensions allows for a Hyperlink element to invoke the execute method on a bound ICommand instance when clicked.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "windows 10",
+ "uwp",
+ "windows community toolkit",
+ "uwp community toolkit",
+ "uwp toolkit",
+ "Hyperlink",
+ "extensions"
+ ],
+ "keywords": [
+ "Extensions",
+ "Controls"
+ ],
+ "docs": [
+ {
+ "title": "HyperlinkExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/HyperlinkExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/HyperlinkExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Controls",
+ "author": "nmetulev"
+ },
+ "samples": []
+ },
+ {
+ "id": "iconmarkupextensions",
+ "name": "Icon extensions",
+ "description": "The FontIcon, FontIconSource and BitmapIcon markup extensions allow developers to easily declare these types of icons directly from XAML in a compact manner.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "markup extension",
+ "XAML",
+ "markup",
+ "fonticon",
+ "fonticonsource",
+ "bitmapicon"
+ ],
+ "keywords": [
+ "Extensions",
+ "Markup"
+ ],
+ "docs": [
+ {
+ "title": "Icon extensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/IconMarkupExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/IconMarkupExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Markup",
+ "author": "sergio0694"
+ },
+ "samples": []
+ },
+ {
+ "id": "listviewextensions",
+ "name": "ListViewExtensions",
+ "description": "ListViewExtensions extensions provide a lightweight way to extend every control that inherits the ListViewBase class with attached properties.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "usings": [
+ "CommunityToolkit.WinUI",
+ "Microsoft.UI.Xaml.Controls"
+ ],
+ "curatedKeywords": [
+ "ListViewBase",
+ "extensions"
+ ],
+ "keywords": [
+ "Extensions",
+ "Controls"
+ ],
+ "docs": [
+ {
+ "title": "ListViewExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/ListViewExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/ListViewExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Controls",
+ "author": "nmetulev"
+ },
+ "samples": [
+ {
+ "header": "ListViewExtensionsAlternateColorSample",
+ "details": "The AlternateColor property provides a way to assign a background color to every other item. Here is how this property can be used in XAML:",
+ "xaml": "\u003CListView ui:ListViewExtensions.AlternateColor=\u0022Silver\u0022\u003E\n \u003CListView.Items\u003E\n \u003Cx:String\u003EOne\u003C/x:String\u003E\n \u003Cx:String\u003ETwo\u003C/x:String\u003E\n \u003Cx:String\u003EThree\u003C/x:String\u003E\n \u003Cx:String\u003EFour\u003C/x:String\u003E\n \u003Cx:String\u003EFive\u003C/x:String\u003E\n \u003Cx:String\u003ESix\u003C/x:String\u003E\n \u003Cx:String\u003ESeven\u003C/x:String\u003E\n \u003Cx:String\u003EEight\u003C/x:String\u003E\n \u003Cx:String\u003ENine\u003C/x:String\u003E\n \u003Cx:String\u003ETen\u003C/x:String\u003E\n \u003C/ListView.Items\u003E\n\u003C/ListView\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ListViewExtensionsAlternateColorSample",
+ "sourcePath": "components/Extensions/samples/ListViewExtensionsAlternateColorSample.xaml"
+ }
+ },
+ {
+ "header": "SmoothScrollIntoView Extension",
+ "details": "Use SmoothScrollIntoView helps to scroll the item into the view with animation. Specify the ItemPosition property to align the item. \u0060\u0060csharp // Scrolling with index await MyGridView.SmoothScrollIntoViewWithIndexAsync(index: int, itemPlacement: ItemPlacement, disableAnimation: bool, scrollIfVisible: bool, additionalHorizontalOffset: int, additionalVerticalOffset: int); // Scrolling with item await MyGridView.SmoothScrollIntoViewWithItemAsync(item: object, itemPlacement: ItemPlacement, disableAnimation: bool, scrollIfVisible: bool, additionalHorizontalOffset: int, additionalVerticalOffset: int); \u0060\u0060 We can use this extension to make the selected item always centered:",
+ "xaml": "\u003CListView ItemsSource=\u0022{x:Bind Items, Mode=OneWay}\u0022\n ScrollViewer.HorizontalScrollBarVisibility=\u0022Visible\u0022\n ScrollViewer.HorizontalScrollMode=\u0022Enabled\u0022\n SelectionChanged=\u0022ListView_SelectionChanged\u0022\u003E\n \u003CListView.ItemsPanel\u003E\n \u003CItemsPanelTemplate\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022 /\u003E\n \u003C/ItemsPanelTemplate\u003E\n \u003C/ListView.ItemsPanel\u003E\n\u003C/ListView\u003E",
+ "code": "public ObservableCollection\u003Cstring\u003E Items { get; set; } = new();\n\npublic SmoothScrollIntoViewSample()\n{\n Items = GetOddEvenSource(201);\n}\n\nprivate async void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)\n{\n if (sender is ListView listView)\n {\n await listView.SmoothScrollIntoViewWithIndexAsync(listView.SelectedIndex, ScrollItemPlacement.Center, false, true);\n }\n}\n\nprivate ObservableCollection\u003Cstring\u003E GetOddEvenSource(int count)\n{\n ObservableCollection\u003Cstring\u003E oddEvenSource = new();\n for (int number = 0; number \u003C count; number\u002B\u002B)\n {\n var item = (number % 2) == 0 ? $\u0022{number} - Even\u0022 : $\u0022{number} - Odd\u0022;\n oddEvenSource.Add(item);\n }\n\n return oddEvenSource;\n}",
+ "language": "csharp",
+ "toolkit": {
+ "sampleId": "SmoothScrollIntoViewSample",
+ "sourcePath": "components/Extensions/samples/ListViewExtensions/SmoothScrollIntoViewSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "matrixextensions",
+ "name": "Matrix Extensions",
+ "description": "Learn about Matrix extensions, which provide methods to transform a Matrix. These methods are similar to methods from the System.Windows.Media.Matrix class.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "Extensions",
+ "matrix"
+ ],
+ "keywords": [
+ "Extensions",
+ "Math"
+ ],
+ "docs": [
+ {
+ "title": "Matrix Extensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/MatrixExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/MatrixExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Math",
+ "author": "michael-hawker"
+ },
+ "samples": []
+ },
+ {
+ "id": "nullableboolextension",
+ "name": "NullableBoolExtension",
+ "description": "The NullableBoolExtension type allows developers to specify default values in XAML for nullable bool dependency properties.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "nullable bool",
+ "dependency property",
+ "markup extension",
+ "XAML",
+ "markup"
+ ],
+ "keywords": [
+ "Extensions",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "NullableBoolExtension",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/NullableBoolExtension.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/NullableBoolExtension.md",
+ "category": "Extensions",
+ "subcategory": "Miscellaneous",
+ "author": "michael-hawker"
+ },
+ "samples": []
+ },
+ {
+ "id": "ondeviceextension",
+ "name": "OnDeviceExtension",
+ "description": "The OnDevice markup extension allows you to customize UI appearance on a per-DeviceFamily basis.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "markup extension",
+ "XAML",
+ "markup"
+ ],
+ "keywords": [
+ "Extensions",
+ "Markup"
+ ],
+ "docs": [
+ {
+ "title": "OnDeviceExtension",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/OnDeviceExtension.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/OnDeviceExtension.md",
+ "category": "Extensions",
+ "subcategory": "Markup",
+ "author": "sonnemaf"
+ },
+ "samples": []
+ },
+ {
+ "id": "scrollviewerextensions",
+ "name": "ScrollViewerExtensions",
+ "description": "ScrollViewerExtensions type provides a simple way to manage Margin for any ScrollBar inside any container.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "windows 10",
+ "uwp",
+ "windows community toolkit",
+ "uwp community toolkit",
+ "uwp toolkit",
+ "ScrollViewer",
+ "extensions"
+ ],
+ "keywords": [
+ "Extensions",
+ "Controls"
+ ],
+ "docs": [
+ {
+ "title": "ScrollViewerExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/ScrollViewerExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/ScrollViewerExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Controls",
+ "author": "ST-Apps"
+ },
+ "samples": []
+ },
+ {
+ "id": "shadowanimations",
+ "name": "Shadow animations",
+ "description": "Animating Attached Shadows",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "shadow",
+ "shadows",
+ "dropshadow",
+ "dropshadowpanel",
+ "attachedshadow",
+ "attacheddropshadow",
+ "attachedcardshadow"
+ ],
+ "keywords": [
+ "Animations",
+ "Shadows"
+ ],
+ "docs": [
+ {
+ "title": "Shadow animations",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/ShadowAnimations.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/ShadowAnimations.md",
+ "category": "Animations",
+ "subcategory": "Shadows",
+ "author": "michael-hawker"
+ },
+ "samples": []
+ },
+ {
+ "id": "stringextensions",
+ "name": "String Extensions",
+ "description": "Learn about string extension methods from the community toolkit. See code examples, requirements, and API information.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "windows 10",
+ "uwp",
+ "windows community toolkit",
+ "uwp community toolkit",
+ "uwp toolkit",
+ "Extensions",
+ "string"
+ ],
+ "keywords": [
+ "Extensions",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "String Extensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/StringExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/StringExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Miscellaneous",
+ "author": "avknaidu"
+ },
+ "samples": []
+ },
+ {
+ "id": "textboxextensions",
+ "name": "TextBoxExtensions",
+ "description": "The TextBoxExtensions class provides extensions and additional functionality for the TextBox control.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "TextBoxMask",
+ "XAML"
+ ],
+ "keywords": [
+ "Extensions",
+ "Controls"
+ ],
+ "docs": [
+ {
+ "title": "TextBoxExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/TextBoxExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/TextBoxExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Controls",
+ "author": "Sergio0694"
+ },
+ "samples": [
+ {
+ "header": "Regex Extension",
+ "details": "The Regex attached property allows text validation using a regular expression or using built in validation types. The developer adds a regular expression to validate the TextBox Text against the regular expression throw Regex property or from selecting ValidationType property on the TextBox. The validation has 3 modes (TextBoxExtensions.ValidationMode): 1) Normal (Default): this mode will set the IsValid attached property to false or true whether the TextBox text is a valid or not against the Regex property. 2) Forced: this mode sets the IsValid property and removes the TextBox text if not valid when the TextBox loses focus. 3) Dynamic: this mode extends Normal and if is the newest input of the TextBox is invalid, the character which is invalid will be deleted. Note that the TextBoxExtensions.ValidationType values Email and PhoneNumber don\u0027t support this validation mode. If you set the validation mode to Dynamic, Normal is selected automatically instead.",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.Resources\u003E\n \u003CStyle x:Key=\u0022TextBoxRegexStyle\u0022\n TargetType=\u0022TextBox\u0022\u003E\n \u003CSetter Property=\u0022VerticalAlignment\u0022 Value=\u0022Top\u0022 /\u003E\n \u003CSetter Property=\u0022TextWrapping\u0022 Value=\u0022Wrap\u0022 /\u003E\n \u003C/Style\u003E\n \u003CDataTemplate x:Key=\u0022HeaderTemplate\u0022\u003E\n \u003CStackPanel\u003E\n \u003CTextBlock Text=\u0022{Binding}\u0022\n TextWrapping=\u0022WrapWholeWords\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n \u003C/Grid.Resources\u003E\n \u003CGrid RowSpacing=\u002232\u0022\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition /\u003E\n \u003CRowDefinition /\u003E\n \u003CRowDefinition /\u003E\n \u003CRowDefinition /\u003E\n \u003CRowDefinition /\u003E\n \u003C/Grid.RowDefinitions\u003E\n\n \u003CStackPanel\u003E\n \u003CTextBox Name=\u0022PhoneNumberValidator\u0022\n ui:TextBoxExtensions.Regex=\u0022^\\s*\\\u002B?\\s*([0-9][\\s-]*){9,}$\u0022\n Header=\u0022Text box with Regex extension for phone number, validation occurs on TextChanged\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource TextBoxRegexStyle}\u0022 /\u003E\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\u003E\n \u003CRun Text=\u0022Is valid: \u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding (ui:TextBoxExtensions.IsValid), ElementName=PhoneNumberValidator}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n\n \u003CStackPanel Grid.Row=\u00221\u0022\u003E\n \u003CTextBox Name=\u0022CharactValidator\u0022\n ui:TextBoxExtensions.ValidationMode=\u0022Dynamic\u0022\n ui:TextBoxExtensions.ValidationType=\u0022Characters\u0022\n Header=\u0022Text box with ValidationType=Characters, validation occurs at input with ValidationMode=Dynamic and clear only single character when value is invalid\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource TextBoxRegexStyle}\u0022\n Text=\u0022abcdef\u0022 /\u003E\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\u003E\n \u003CRun Text=\u0022Is valid: \u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding (ui:TextBoxExtensions.IsValid), ElementName=CharactValidator}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n\n \u003CStackPanel Grid.Row=\u00222\u0022\u003E\n \u003CTextBox Name=\u0022EmailValidator\u0022\n ui:TextBoxExtensions.ValidationType=\u0022Email\u0022\n Header=\u0022Text box with ValidationType=Email, validation occurs on TextChanged\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource TextBoxRegexStyle}\u0022 /\u003E\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\u003E\n \u003CRun Text=\u0022Is valid: \u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding (ui:TextBoxExtensions.IsValid), ElementName=EmailValidator}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n\n \u003CStackPanel Grid.Row=\u00223\u0022\u003E\n \u003CTextBox Name=\u0022DecimalValidatorForce\u0022\n ui:TextBoxExtensions.ValidationMode=\u0022Forced\u0022\n ui:TextBoxExtensions.ValidationType=\u0022Decimal\u0022\n Header=\u0022Text box with ValidationType=Decimal, validation occurs on TextChanged and force occurs on lose focus with ValidationMode=Force (333,111 or 333.111)\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource TextBoxRegexStyle}\u0022 /\u003E\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\u003E\n \u003CRun Text=\u0022Is valid: \u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding (ui:TextBoxExtensions.IsValid), ElementName=DecimalValidatorForce}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n\n \u003CStackPanel Grid.Row=\u00224\u0022\u003E\n \u003CTextBox Name=\u0022NumberValidatorDynamic\u0022\n ui:TextBoxExtensions.ValidationMode=\u0022Dynamic\u0022\n ui:TextBoxExtensions.ValidationType=\u0022Number\u0022\n Header=\u0022Text box with ValidationType=Number, validation occurs at input with ValidationMode=Dynamic and clear only single character when value is invalid\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource TextBoxRegexStyle}\u0022 /\u003E\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\u003E\n \u003CRun Text=\u0022Is valid: \u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding (ui:TextBoxExtensions.IsValid), ElementName=NumberValidatorDynamic}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n \u003C/Grid\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "RegexSample",
+ "sourcePath": "components/Extensions/samples/TextBoxExtensions/RegexSample.xaml"
+ }
+ },
+ {
+ "header": "TextBoxMask Extension",
+ "details": "The Mask attached property allows a user to more easily enter fixed width text in TextBox control where you would like them to enter the data in a certain format, ex: phone number, postal code. The developer adds the mask property to prevent end user to enter any other format but the required one (eg. a postal code in the format \u0022aaa-9999\u0022). The Mask property provides 3 built-in variable characters that can be used to define a mask: 1) a which represents [a-Z] 2) 9 which represents [0-9] 3) * which represents a or 9 At the post code example the user will only be allowed to enter from a to Z in the first 3 characters, then there is a fixed character - which the user can change or remove, and the last part which the user can change by entering from 0 to 9 at each character. The Mask property prevents the TextBox from having non specified characters (eg. if you entered 1 or 2 into a mask like 9999). The Mask property also supports 2 type of characters: 1) Variable: which the user can change like a,9,* 2) Fixed: which the user can\u0027t change and it is any non variable character (eg. the - in the first example) Variable characters a represented to end user in form of placeholder so the user can know which characters he can change and which he can\u0027t, ex mask aaa-9999 will be presented to user as ___-____. The default placeholder is _, but you can change it using the MaskPlaceholder property. You can escape variable by using \\ (eg. the mask \u002B\\964 will be presented to the user as \u002B964). This way, the 9 in the mask is treated as a fixed character, not as a variable. If you needed \\ in the mask then you can write it as \\\\ (eg. 99\\\\99\\\\9999 will be presented to the user as __\\__\\__). In case you want to add a custom variable character you can use the CustomMask property. You can add a character that represents certain regex as c:[a-c] and once you use character c in the mask the mask will prevent any characters but from a to c inside the TextBox, also you specify multiple variable characters by adding comma , after every character and its representation. This feature is helpful if you want to allow certain language characters (eg. French or Arabic only TextBox).",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.Resources\u003E\n \u003CStyle x:Key=\u0022MaskedTextBoxStyle\u0022\n TargetType=\u0022TextBox\u0022\u003E\n \u003CSetter Property=\u0022VerticalAlignment\u0022 Value=\u0022Top\u0022 /\u003E\n \u003CSetter Property=\u0022TextWrapping\u0022 Value=\u0022Wrap\u0022 /\u003E\n \u003C/Style\u003E\n \u003CDataTemplate x:Key=\u0022HeaderTemplate\u0022\u003E\n \u003CStackPanel\u003E\n \u003CTextBlock Text=\u0022{Binding}\u0022\n TextWrapping=\u0022WrapWholeWords\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n \u003C/Grid.Resources\u003E\n \u003CStackPanel Spacing=\u002224\u0022\u003E\n \u003CTextBox ui:TextBoxExtensions.Mask=\u00229a9a-a9a*\u0022\n Header=\u0022Text box with Mask 9a9a-a9a* (9 allows from 0 to 9, a allow from a to Z and * allows both a and 9)\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource MaskedTextBoxStyle}\u0022\n Text=\u0022TextBoxMask\u0022 /\u003E\n\n \u003CTextBox ui:TextBoxExtensions.Mask=\u0022\u002B1999-9999\u0022\n ui:TextBoxExtensions.MaskPlaceholder=\u0022 \u0022\n Header=\u0022Text box with Mask \u002B1999-9999 and placeHolder as space (placeholder represents the characters the user can change on runtime)\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource MaskedTextBoxStyle}\u0022 /\u003E\n\n \u003CTextBox ui:TextBoxExtensions.Mask=\u0022\u002B\\964 799 999 9999\u0022\n Header=\u0022Text box with Mask \u002B964 799 999 9999 (Notice how we escape the first 9 with a backslash)\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource MaskedTextBoxStyle}\u0022 /\u003E\n\n \u003CTextBox ui:TextBoxExtensions.Mask=\u002299\\\\99\\\\9999\u0022\n Header=\u0022Text box with Mask 99\\99\\9999 (You can escape a backslash with another backslash)\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource MaskedTextBoxStyle}\u0022 /\u003E\n\n \u003CTextBox ui:TextBoxExtensions.CustomMask=\u00225:[1-5],c:[a-c]\u0022\n ui:TextBoxExtensions.Mask=\u0022a5c-5c*9\u0022\n Header=\u0022Text box with CustomMask in case you want to define your own variable character like a, 9 and *. Mask: a5c-5c*9, 5: [1-5], c: [a-c]\u0022\n HeaderTemplate=\u0022{StaticResource HeaderTemplate}\u0022\n Style=\u0022{StaticResource MaskedTextBoxStyle}\u0022 /\u003E\n\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "TextBoxMaskSample",
+ "sourcePath": "components/Extensions/samples/TextBoxExtensions/TextBoxMaskSample.xaml"
+ }
+ },
+ {
+ "header": "SurfaceDialOptions Extension",
+ "details": "The SurfaceDialOptions property adds features from the Surface Dial control to a numeric TextBox. This enables you to modify the content of the TextBox when rotating the Surface Dial (increasing or decreasing the value) and optionally go to the next focus element by tapping the Surface Dial click button. The various options are set through the SurfaceDialOptions type, which is declared in XAML and used to set all the values to use for a given TextBox from a single place. Here is an example of the visual result when scrolling on a Surface Dial over a TextBox:",
+ "xaml": "\u003CTextBox HorizontalAlignment=\u0022Left\u0022\n VerticalAlignment=\u0022Top\u0022\n Header=\u0022For this sample, you need a Surface Dial\u0022\n Text=\u00220\u0022\u003E\n \u003Cui:TextBoxExtensions.SurfaceDialOptions\u003E\n \u003Cui:SurfaceDialOptions EnableHapticFeedback=\u0022True\u0022\n EnableMinMaxValue=\u0022True\u0022\n EnableTapToNextControl=\u0022False\u0022\n Icon=\u0022Ruler\u0022\n MaxValue=\u0022100\u0022\n MinValue=\u00220\u0022\n RotationResolutionInDegrees=\u002212\u0022\n StepValue=\u00221\u0022 /\u003E\n \u003C/ui:TextBoxExtensions.SurfaceDialOptions\u003E\n\u003C/TextBox\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SurfaceDialOptionsSample",
+ "sourcePath": "components/Extensions/samples/TextBoxExtensions/SurfaceDialOptionsSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "transformextensions",
+ "name": "Transform Extensions",
+ "description": "Learn about Transform extensions. Transform extensions provide the ability to retrieve the Matrix of the transform.",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "Extensions",
+ "matrix",
+ "transform",
+ "rotate",
+ "skew",
+ "scale",
+ "RotateTransform",
+ "Value",
+ "ScaleTransform",
+ "SkewTransform",
+ "TranslateTransform"
+ ],
+ "keywords": [
+ "Extensions",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "Transform Extensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/TransformExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/TransformExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Miscellaneous",
+ "author": "michael-hawker"
+ },
+ "samples": []
+ },
+ {
+ "id": "uielementextensions",
+ "name": "UIElementExtensions",
+ "description": "UIElementExtensions provides a simple way to extend the UIElement class",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "windows 10",
+ "uwp",
+ "windows community toolkit",
+ "uwp community toolkit",
+ "uwp toolkit",
+ "UIElement",
+ "extensions"
+ ],
+ "keywords": [
+ "Extensions",
+ "Controls"
+ ],
+ "docs": [
+ {
+ "title": "UIElementExtensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/UIElementExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/UIElementExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Controls",
+ "author": "vgromfeld"
+ },
+ "samples": [
+ {
+ "header": "ClipToBounds",
+ "details": "The ClipToBounds property allows you to indicate whether to clip the content of this element (or content coming from the child elements of this element) to fit into the size of the containing element. Here is how it can be used in XAML to clip a target element:",
+ "xaml": "\u003CGrid Width=\u0022148\u0022\n Height=\u0022148\u0022\n ui:UIElementExtensions.ClipToBounds=\u0022True\u0022\n Background=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022\n BorderBrush=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\u003E\n \u003C!-- We translate the inner rectangles outside of the bounds of the container. --\u003E\n \u003CRectangle Width=\u0022100\u0022\n Height=\u0022100\u0022\n Fill=\u0022{ThemeResource SystemFillColorAttentionBrush}\u0022\u003E\n \u003CRectangle.RenderTransform\u003E\n \u003CTranslateTransform X=\u0022-50\u0022 Y=\u0022-50\u0022 /\u003E\n \u003C/Rectangle.RenderTransform\u003E\n \u003C/Rectangle\u003E\n \u003CRectangle Width=\u0022100\u0022\n Height=\u0022100\u0022\n Fill=\u0022{ThemeResource SystemFillColorSuccessBrush}\u0022\u003E\n \u003CRectangle.RenderTransform\u003E\n \u003CTranslateTransform X=\u002250\u0022 Y=\u002250\u0022 /\u003E\n \u003C/Rectangle.RenderTransform\u003E\n \u003C/Rectangle\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ClipToBoundsSample",
+ "sourcePath": "components/Extensions/samples/ClipToBoundsSample.xaml",
+ "optionsResolved": [
+ "IsClipped=True"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "visualextensions",
+ "name": "Composition Visual Extensions",
+ "description": "The Composition Visual Attached Properties Extension allow Composition Visual Properties to be modified directly in XAML",
+ "nugetPackage": "CommunityToolkit.WinUI.Extensions",
+ "curatedKeywords": [
+ "Visual",
+ "composition",
+ "xaml",
+ "attached property"
+ ],
+ "keywords": [
+ "Extensions",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "Composition Visual Extensions",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Extensions/samples/VisualExtensions.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Extensions",
+ "documentPath": "components/Extensions/samples/VisualExtensions.md",
+ "category": "Extensions",
+ "subcategory": "Layout",
+ "author": "nmetulev"
+ },
+ "samples": []
+ },
+ {
+ "id": "headeredcontentcontrol",
+ "name": "HeaderedContentControl",
+ "description": "The HeaderedContentControl allows content to be displayed with a specified header.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.HeaderedControls",
+ "curatedKeywords": [
+ "HeaderedContentControl",
+ "Control",
+ "headered"
+ ],
+ "keywords": [
+ "Controls",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "HeaderedContentControl",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/HeaderedControls/samples/HeaderedContentControl.md"
+ }
+ ],
+ "toolkit": {
+ "component": "HeaderedControls",
+ "documentPath": "components/HeaderedControls/samples/HeaderedContentControl.md",
+ "category": "Controls",
+ "subcategory": "Layout",
+ "author": "skendrot"
+ },
+ "samples": [
+ {
+ "header": "HeaderedContentControl",
+ "details": "The Header property can be any object and you can use the HeaderTemplate to specify a custom look to the header. Content for the HeaderedContentControl will align to the top left. This is to maintain the same functionality as the ContentControl.",
+ "xaml": "\u003Ccontrols:HeaderedContentControl Margin=\u00226\u0022\n HorizontalContentAlignment=\u0022Stretch\u0022\n VerticalContentAlignment=\u0022Stretch\u0022\n Header=\u0022Rectangle content\u0022\u003E\n \u003CRectangle MinHeight=\u0022160\u0022\n Fill=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022 /\u003E\n\u003C/controls:HeaderedContentControl\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "HeaderedContentControlSample",
+ "sourcePath": "components/HeaderedControls/samples/HeaderedContentControlSample.xaml"
+ }
+ },
+ {
+ "header": "HeaderedContentControl Text Sample",
+ "details": "A sample for showing how to create and use a HeaderedContentControl control with text content.",
+ "xaml": "\u003Ccontrols:HeaderedContentControl Margin=\u00226\u0022\n Header=\u0022TextBlock content header\u0022\u003E\n \u003CTextBlock FontSize=\u002212\u0022\n Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Text=\u0022Some more text about this content\u0022\n TextWrapping=\u0022Wrap\u0022 /\u003E\n\n\u003C/controls:HeaderedContentControl\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "HeaderedContentControlTextSample",
+ "sourcePath": "components/HeaderedControls/samples/HeaderedContentControlTextSample.xaml"
+ }
+ },
+ {
+ "header": "HeaderedContentControl Image Sample",
+ "details": "A sample for showing how to create and use a HeaderedContentControl control with image content.",
+ "xaml": "\u003Ccontrols:HeaderedContentControl Grid.Row=\u00221\u0022\n Grid.Column=\u00220\u0022\n Height=\u0022500\u0022\n Margin=\u00226\u0022\n Header=\u0022Image content\u0022\u003E\n \u003CImage Source=\u0022ms-appx:///Assets/BisonBadlandsChillin.jpg\u0022 /\u003E\n\u003C/controls:HeaderedContentControl\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "HeaderedContentControlImageSample",
+ "sourcePath": "components/HeaderedControls/samples/HeaderedContentControlImageSample.xaml"
+ }
+ },
+ {
+ "header": "HeaderedContentControl Complex Sample",
+ "details": "A sample for showing how to create and use a HeaderedContentControl control with complex content.",
+ "xaml": "\u003Ccontrols:HeaderedContentControl Margin=\u00226\u0022\n Header=\u0022Complex content\u0022\u003E\n \u003CRelativePanel\u003E\n \u003CImage x:Name=\u0022Icon\u0022\n Width=\u002250\u0022\n Height=\u002250\u0022\n Source=\u0022ms-appx:///Assets/Sunny.png\u0022 /\u003E\n\n \u003CStackPanel x:Name=\u0022Temp\u0022\n RelativePanel.RightOf=\u0022Icon\u0022\u003E\n \u003CTextBlock Text=\u002265\u0022 /\u003E\n \u003C/StackPanel\u003E\n\n \u003CTextBlock FontSize=\u002212\u0022\n Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n RelativePanel.AlignLeftWith=\u0022Temp\u0022\n RelativePanel.Below=\u0022Temp\u0022\n Text=\u0022Rainy\u0022 /\u003E\n \u003C/RelativePanel\u003E\n\u003C/controls:HeaderedContentControl\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "HeaderedContentControlComplexSample",
+ "sourcePath": "components/HeaderedControls/samples/HeaderedContentControlComplexSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "headereditemscontrol",
+ "name": "HeaderedItemsControl",
+ "description": "The HeaderedItemsControl allows items to be displayed with a specified header.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.HeaderedControls",
+ "curatedKeywords": [
+ "HeaderedItemsControl",
+ "Control",
+ "Layout"
+ ],
+ "keywords": [
+ "Controls",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "HeaderedItemsControl",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/HeaderedControls/samples/HeaderedItemsControl.md"
+ }
+ ],
+ "toolkit": {
+ "component": "HeaderedControls",
+ "documentPath": "components/HeaderedControls/samples/HeaderedItemsControl.md",
+ "category": "Controls",
+ "subcategory": "Layout",
+ "author": "skendrot"
+ },
+ "samples": [
+ {
+ "header": "HeaderedItemsControl",
+ "details": "The Header property can be any object and you can use the HeaderTemplate to specify a custom look to the header. Similar objects can be set for the Footer and FooterTemplate.",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition /\u003E\n \u003CColumnDefinition /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n \u003Ccontrols:HeaderedItemsControl Footer=\u0022Footer 1\u0022\n Header=\u0022Header 1\u0022\n ItemsSource=\u0022{x:Bind Items, Mode=OneWay}\u0022 /\u003E\n\n \u003Ccontrols:HeaderedItemsControl Grid.Column=\u00221\u0022\n Footer=\u0022Footer 2\u0022\n Header=\u0022Header 2\u0022\n ItemsSource=\u0022{x:Bind Items, Mode=OneWay}\u0022\u003E\n \u003Ccontrols:HeaderedItemsControl.HeaderTemplate\u003E\n \u003CDataTemplate\u003E\n \u003CTextBlock FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:HeaderedItemsControl.HeaderTemplate\u003E\n \u003Ccontrols:HeaderedItemsControl.FooterTemplate\u003E\n \u003CDataTemplate\u003E\n \u003CTextBlock FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:HeaderedItemsControl.FooterTemplate\u003E\n \u003CItemsControl.ItemTemplate\u003E\n \u003CDataTemplate\u003E\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Text=\u0022{Binding}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/ItemsControl.ItemTemplate\u003E\n \u003C/controls:HeaderedItemsControl\u003E\n\u003C/Grid\u003E",
+ "code": "public HeaderedItemsControlSample()\n{\n Items = \u0022The quick brown fox jumped over the lazy river\u0022.Split(\u0027 \u0027);\n}\n\npublic IEnumerable\u003Cstring\u003E Items { get; }",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "HeaderedItemsControlSample",
+ "sourcePath": "components/HeaderedControls/samples/HeaderedItemsControlSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "headeredtreeview",
+ "name": "HeaderedTreeView",
+ "description": "The HeaderedTreeView allows a treeview to be displayed with a specified header.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.HeaderedControls",
+ "curatedKeywords": [
+ "HeaderedTreeView",
+ "Control",
+ "Layout",
+ "treeview"
+ ],
+ "keywords": [
+ "Controls",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "HeaderedTreeView",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/HeaderedControls/samples/HeaderedTreeView.md"
+ }
+ ],
+ "toolkit": {
+ "component": "HeaderedControls",
+ "documentPath": "components/HeaderedControls/samples/HeaderedTreeView.md",
+ "category": "Controls",
+ "subcategory": "Layout",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "HeaderedTreeView",
+ "details": "The Header property can be any object and you can use the HeaderTemplate to specify a custom look to the header. Similar objects can be set for the Footer and FooterTemplate.",
+ "xaml": "\u003CGrid\u003E\n \u003Ccontrols:HeaderedTreeView Footer=\u0022Footer\u0022\n Header=\u0022Header\u0022\n ItemsSource=\u0022{x:Bind Items, Mode=OneWay}\u0022\u003E\n \u003Ccontrols:HeaderedTreeView.HeaderTemplate\u003E\n \u003CDataTemplate\u003E\n \u003CTextBlock FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:HeaderedTreeView.HeaderTemplate\u003E\n \u003Ccontrols:HeaderedTreeView.FooterTemplate\u003E\n \u003CDataTemplate\u003E\n \u003CTextBlock FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:HeaderedTreeView.FooterTemplate\u003E\n \u003Ccontrols:HeaderedTreeView.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:ExplorerItem\u0022\u003E\n \u003Cmuxc:TreeViewItem Content=\u0022{x:Bind Name}\u0022\n IsExpanded=\u0022True\u0022\n ItemsSource=\u0022{x:Bind Children}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:HeaderedTreeView.ItemTemplate\u003E\n \u003C/controls:HeaderedTreeView\u003E\n\u003C/Grid\u003E",
+ "code": "public HeaderedTreeViewSample()\n{\n Items = GetData();\n}\n\npublic ObservableCollection\u003CExplorerItem\u003E Items { get; }\n\nprivate ObservableCollection\u003CExplorerItem\u003E GetData()\n{\n var list = new ObservableCollection\u003CExplorerItem\u003E();\n ExplorerItem folder1 = new ExplorerItem()\n {\n Name = \u0022Work Documents\u0022,\n Children =\n {\n new ExplorerItem()\n {\n Name = \u0022Functional Specifications\u0022,\n Children =\n {\n new ExplorerItem()\n {\n Name = \u0022TreeView spec\u0022,\n }\n }\n },\n new ExplorerItem()\n {\n Name = \u0022Feature Schedule\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Overall Project Plan\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Feature Resources Allocation\u0022,\n }\n }\n };\n ExplorerItem folder2 = new ExplorerItem()\n {\n Name = \u0022Personal Folder\u0022,\n Children =\n {\n new ExplorerItem()\n {\n Name = \u0022Home Remodel Folder\u0022,\n Children =\n {\n new ExplorerItem()\n {\n Name = \u0022Contractor Contact Info\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Paint Color Scheme\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Flooring Woodgrain type\u0022,\n },\n new ExplorerItem()\n {\n Name = \u0022Kitchen Cabinet Style\u0022,\n }\n }\n }\n }\n };\n list.Add(folder1);\n list.Add(folder2);\n return list;\n}\n\npublic class ExplorerItem\n{\n public string? Name { get; set; }\n\n private ObservableCollection\u003CExplorerItem\u003E? _children;\n public ObservableCollection\u003CExplorerItem\u003E Children\n {\n get\n {\n if (_children == null)\n {\n _children = new ObservableCollection\u003CExplorerItem\u003E();\n }\n\n return _children;\n }\n\n set\n {\n _children = value;\n }\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "HeaderedTreeViewSample",
+ "sourcePath": "components/HeaderedControls/samples/HeaderedTreeViewSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "camerahelper",
+ "name": "CameraHelper",
+ "description": "The CameraHelper provides helper methods to easily use the available camera frame sources to preview video, capture video frames and software bitmaps.",
+ "nugetPackage": "CommunityToolkit.WinUI.Helpers",
+ "usings": [
+ "CommunityToolkit.WinUI.Helpers",
+ "Microsoft.UI.Xaml.Media.Imaging",
+ "Windows.ApplicationModel",
+ "Windows.Graphics.Imaging",
+ "Windows.Media",
+ "Windows.Media.Capture.Frames"
+ ],
+ "curatedKeywords": [
+ "Helpers",
+ "CameraHelper",
+ "Camera",
+ "Frame Source",
+ "Video Frame",
+ "Software Bitmap"
+ ],
+ "keywords": [
+ "Helpers",
+ "System"
+ ],
+ "docs": [
+ {
+ "title": "CameraHelper",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Helpers/samples/CameraHelper.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Helpers",
+ "documentPath": "components/Helpers/samples/CameraHelper.md",
+ "category": "Helpers",
+ "subcategory": "System",
+ "author": "skommireddi"
+ },
+ "samples": [
+ {
+ "header": "CameraHelper",
+ "details": "The helper currently shows camera frame sources that support color video preview or video record streams.",
+ "xaml": "\u003CStackPanel HorizontalAlignment=\u0022Center\u0022\n Orientation=\u0022Vertical\u0022\n Spacing=\u002212\u0022\u003E\n \u003CComboBox x:Name=\u0022FrameSourceGroupCombo\u0022\n MinWidth=\u0022200\u0022\n HorizontalAlignment=\u0022Center\u0022\n Header=\u0022Frame source group\u0022\u003E\n \u003CComboBox.ItemTemplate\u003E\n \u003CDataTemplate\u003E\n \u003CTextBlock Text=\u0022{Binding DisplayName}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/ComboBox.ItemTemplate\u003E\n \u003C/ComboBox\u003E\n \u003Cmuxc:InfoBar x:Name=\u0022ErrorBar\u0022\n Title=\u0022Error\u0022\n IsOpen=\u0022False\u0022\n Severity=\u0022Error\u0022 /\u003E\n \u003CBorder Background=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022\n BorderBrush=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\n CornerRadius=\u0022{StaticResource ControlCornerRadius}\u0022\u003E\n \u003CImage x:Name=\u0022CurrentFrameImage\u0022\n Height=\u0022300\u0022\n MinWidth=\u0022360\u0022 /\u003E\n \u003C/Border\u003E\n\n \u003CButton x:Name=\u0022CaptureButton\u0022\n HorizontalAlignment=\u0022Center\u0022\n Click=\u0022CaptureButton_Click\u0022\n Content=\u0022Capture video frame\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "code": "private CameraHelper _cameraHelper;\n\nprivate VideoFrame _currentVideoFrame;\n\nprivate SoftwareBitmapSource _softwareBitmapSource;\n\n#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.\n\npublic CameraHelperSample()\n#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.\n\n{\n Loaded \u002B= CameraHelperSample_Loaded;\n Unloaded \u002B= CameraHelperSample_Unloaded;\n}\n\nprivate async void CameraHelperSample_Loaded(object sender, RoutedEventArgs e)\n{\n Loaded -= CameraHelperSample_Loaded;\n Setup();\n await InitializeAsync();\n}\n\nprivate async void CameraHelperSample_Unloaded(object sender, RoutedEventArgs e)\n{\n Unloaded -= CameraHelperSample_Unloaded;\n await CleanUpAsync();\n}\n\nprivate void Setup()\n{\n _softwareBitmapSource = new SoftwareBitmapSource();\n CurrentFrameImage.Source = _softwareBitmapSource;\n}\n\nprivate async void Application_Suspending(object? sender, SuspendingEventArgs e)\n{\n if (IsLoaded)\n {\n var deferral = e.SuspendingOperation.GetDeferral();\n await CleanUpAsync();\n deferral.Complete();\n }\n}\n\nprivate async void Application_Resuming(object? sender, object e)\n{\n await InitializeAsync();\n}\n\nprivate void CameraHelper_FrameArrived(object sender, FrameEventArgs e)\n{\n _currentVideoFrame = e.VideoFrame;\n}\n\nprivate async Task InitializeAsync()\n{\n var frameSourceGroups = await CameraHelper.GetFrameSourceGroupsAsync();\n if (_cameraHelper == null)\n {\n _cameraHelper = new CameraHelper();\n }\n\n var result = await _cameraHelper.InitializeAndStartCaptureAsync();\n if (result == CameraHelperResult.Success)\n {\n // Subscribe to the video frame as they arrive\n _cameraHelper.FrameArrived \u002B= CameraHelper_FrameArrived!;\n FrameSourceGroupCombo.ItemsSource = frameSourceGroups;\n FrameSourceGroupCombo.SelectionChanged \u002B= FrameSourceGroupCombo_SelectionChanged;\n FrameSourceGroupCombo.SelectedIndex = 0;\n }\n\n SetUIControls(result);\n}\n\nprivate async void FrameSourceGroupCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)\n{\n if (FrameSourceGroupCombo.SelectedItem is MediaFrameSourceGroup selectedGroup \u0026\u0026 _cameraHelper != null)\n {\n _cameraHelper.FrameSourceGroup = selectedGroup;\n var result = await _cameraHelper.InitializeAndStartCaptureAsync();\n SetUIControls(result);\n }\n}\n\nprivate void SetUIControls(CameraHelperResult result)\n{\n var success = result == CameraHelperResult.Success;\n if (!success)\n {\n _currentVideoFrame = null !;\n }\n\n ErrorBar.Title = result.ToString();\n ErrorBar.IsOpen = !success;\n CaptureButton.IsEnabled = success;\n CurrentFrameImage.Opacity = success ? 1 : 0.5;\n}\n\nprivate async void CaptureButton_Click(object sender, RoutedEventArgs e)\n{\n var softwareBitmap = _currentVideoFrame?.SoftwareBitmap;\n if (softwareBitmap != null)\n {\n if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 || softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)\n {\n softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);\n }\n\n if (_softwareBitmapSource != null)\n {\n await _softwareBitmapSource.SetBitmapAsync(softwareBitmap);\n }\n }\n}\n\nprivate async Task CleanUpAsync()\n{\n if (FrameSourceGroupCombo != null)\n {\n FrameSourceGroupCombo.SelectionChanged -= FrameSourceGroupCombo_SelectionChanged;\n }\n\n if (_cameraHelper != null)\n {\n _cameraHelper.FrameArrived -= CameraHelper_FrameArrived!;\n await _cameraHelper.CleanUpAsync();\n _cameraHelper = null !;\n }\n\n _softwareBitmapSource?.Dispose();\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "CameraHelperSample",
+ "sourcePath": "components/Helpers/samples/CameraHelperSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "colorhelper",
+ "name": "ColorHelper",
+ "description": "Convert colors from text names, HTML hex, HSV, or HSL to Windows UI Colors (and back again).",
+ "nugetPackage": "CommunityToolkit.WinUI.Helpers",
+ "curatedKeywords": [
+ "Helpers",
+ "Theming",
+ "theme listener",
+ "themes",
+ "screenunithelper",
+ "colorhelper"
+ ],
+ "keywords": [
+ "Helpers",
+ "Converters"
+ ],
+ "docs": [
+ {
+ "title": "ColorHelper",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Helpers/samples/ColorHelper.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Helpers",
+ "documentPath": "components/Helpers/samples/ColorHelper.md",
+ "category": "Helpers",
+ "subcategory": "Converters",
+ "author": "niels9001"
+ },
+ "samples": []
+ },
+ {
+ "id": "designtimehelper",
+ "name": "DesignTimeHelpers",
+ "description": "Class used to provide helpers for design time.",
+ "nugetPackage": "CommunityToolkit.WinUI.Helpers",
+ "curatedKeywords": [
+ "Helpers",
+ "designtimehelpers"
+ ],
+ "keywords": [
+ "Helpers",
+ "Developer"
+ ],
+ "docs": [
+ {
+ "title": "DesignTimeHelpers",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Helpers/samples/DesignTimeHelper.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Helpers",
+ "documentPath": "components/Helpers/samples/DesignTimeHelper.md",
+ "category": "Helpers",
+ "subcategory": "Developer",
+ "author": "niels9001"
+ },
+ "samples": []
+ },
+ {
+ "id": "networkhelper",
+ "name": "NetworkHelper",
+ "description": "The NetworkHelper class is used to determine whether the app has Internet, and if it is on a metered Internet connection.",
+ "nugetPackage": "CommunityToolkit.WinUI.Helpers",
+ "usings": [
+ "CommunityToolkit.WinUI.Helpers"
+ ],
+ "curatedKeywords": [
+ "Helpers",
+ "NetworkHelper"
+ ],
+ "keywords": [
+ "Helpers",
+ "System"
+ ],
+ "docs": [
+ {
+ "title": "NetworkHelper",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Helpers/samples/NetworkHelper.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Helpers",
+ "documentPath": "components/Helpers/samples/NetworkHelper.md",
+ "category": "Helpers",
+ "subcategory": "System",
+ "author": "nmetulev"
+ },
+ "samples": [
+ {
+ "header": "Network Helper",
+ "details": "It exposes network information though a property called ConnectionInformation. The ConnectionInformation holds information about ConnectionType, ConnectivityLevel, ConnectionCost, SignalStrength, Internet Connectivity and more. **_What is a metered connection?_** A metered connection is an Internet connection that has a data limit or cost associated with it. Cellular data connections are set as metered by default. Wi-Fi network connections can be set to metered, but aren\u0027t by default. Application developers should take metered nature of connection into account and reduce data usage.",
+ "xaml": "\u003CStackPanel Orientation=\u0022Vertical\u0022\u003E\n \u003CTextBlock\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022Internet available: \u0022 /\u003E\n \u003CRun x:Name=\u0022IsInternetAvailableText\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022On metered connection: \u0022 /\u003E\n \u003CRun x:Name=\u0022IsInternetOnMeteredConnectionText\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022Connection type: \u0022 /\u003E\n \u003CRun x:Name=\u0022ConnectionTypeText\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022Signal bars: \u0022 /\u003E\n \u003CRun x:Name=\u0022SignalBarsText\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022Network names: \u0022 /\u003E\n \u003CRun x:Name=\u0022NetworkNamesText\u0022 /\u003E\n \u003C/TextBlock\u003E\n\u003C/StackPanel\u003E",
+ "code": "public NetworkHelperSample()\n{\n Load();\n}\n\nprivate void Load()\n{\n IsInternetAvailableText.Text = NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable ? \u0022Yes\u0022 : \u0022No\u0022;\n IsInternetOnMeteredConnectionText.Text = NetworkHelper.Instance.ConnectionInformation.IsInternetOnMeteredConnection ? \u0022Yes\u0022 : \u0022No\u0022;\n ConnectionTypeText.Text = NetworkHelper.Instance.ConnectionInformation.ConnectionType.ToString();\n SignalBarsText.Text = NetworkHelper.Instance.ConnectionInformation.SignalStrength.GetValueOrDefault(0).ToString();\n NetworkNamesText.Text = string.Join(\u0022, \u0022, NetworkHelper.Instance.ConnectionInformation.NetworkNames);\n}",
+ "language": "csharp",
+ "toolkit": {
+ "sampleId": "NetworkHelperSample",
+ "sourcePath": "components/Helpers/samples/NetworkHelperSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "screenunithelper",
+ "name": "ScreenUnitHelper",
+ "description": "Convert screen units to another screen unit.",
+ "nugetPackage": "CommunityToolkit.WinUI.Helpers",
+ "curatedKeywords": [
+ "Helpers",
+ "screenunithelper"
+ ],
+ "keywords": [
+ "Helpers",
+ "Converters"
+ ],
+ "docs": [
+ {
+ "title": "ScreenUnitHelper",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Helpers/samples/ScreenUnitHelper.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Helpers",
+ "documentPath": "components/Helpers/samples/ScreenUnitHelper.md",
+ "category": "Helpers",
+ "subcategory": "Converters",
+ "author": "niels9001"
+ },
+ "samples": []
+ },
+ {
+ "id": "themelistener",
+ "name": "Theme Listener",
+ "description": "The ThemeListener allows you to keep track of changes to the current Application Them, and when it is changed via System Theme changes.",
+ "nugetPackage": "CommunityToolkit.WinUI.Helpers",
+ "usings": [
+ "CommunityToolkit.WinUI.Helpers"
+ ],
+ "curatedKeywords": [
+ "Helpers",
+ "Theming",
+ "theme listener",
+ "themes"
+ ],
+ "keywords": [
+ "Helpers",
+ "System"
+ ],
+ "docs": [
+ {
+ "title": "Theme Listener",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Helpers/samples/ThemeListener.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Helpers",
+ "documentPath": "components/Helpers/samples/ThemeListener.md",
+ "category": "Helpers",
+ "subcategory": "System",
+ "author": "williamabradley"
+ },
+ "samples": [
+ {
+ "header": "Theme Listener",
+ "details": "KNOWN ISSUE: ThemeListener might not work in WinUI3 applications.",
+ "xaml": "\u003CStackPanel\u003E\n \u003CTextBlock\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022System theme: \u0022 /\u003E\n \u003CRun x:Name=\u0022SystemTheme\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022Sample theme: \u0022 /\u003E\n \u003CRun x:Name=\u0022CurrentTheme\u0022 /\u003E\n \u003C/TextBlock\u003E\n\u003C/StackPanel\u003E",
+ "code": "public ThemeListenerSample()\n{\n Listener = new ThemeListener();\n this.Loaded \u002B= ThemeListenerPage_Loaded;\n Listener.ThemeChanged \u002B= Listener_ThemeChanged;\n this.ActualThemeChanged \u002B= this.ThemeListenerSample_ActualThemeChanged;\n}\n\nprivate void ThemeListenerSample_ActualThemeChanged(FrameworkElement sender, object args)\n{\n UpdateThemeState();\n}\n\nprivate void ThemeListenerPage_Loaded(object sender, RoutedEventArgs e)\n{\n UpdateThemeState();\n}\n\nprivate void Listener_ThemeChanged(ThemeListener sender)\n{\n UpdateThemeState();\n}\n\nprivate void UpdateThemeState()\n{\n SystemTheme.Text = Listener.CurrentThemeName;\n CurrentTheme.Text = this.ActualTheme.ToString();\n}\n\npublic ThemeListener Listener { get; }",
+ "language": "csharp",
+ "toolkit": {
+ "sampleId": "ThemeListenerSample",
+ "sourcePath": "components/Helpers/samples/ThemeListenerSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "weakeventlistener",
+ "name": "WeakEventListener",
+ "description": "The WeakEventListener allows the owner to be garbage collected if its only remaining link is an event handler.",
+ "nugetPackage": "CommunityToolkit.WinUI.Helpers",
+ "curatedKeywords": [
+ "Helpers",
+ "WeakEventListener"
+ ],
+ "keywords": [
+ "Helpers",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "WeakEventListener",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Helpers/samples/WeakEventListener.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Helpers",
+ "documentPath": "components/Helpers/samples/WeakEventListener.md",
+ "category": "Helpers",
+ "subcategory": "Miscellaneous",
+ "author": "nmetulev"
+ },
+ "samples": []
+ },
+ {
+ "id": "imagecropper",
+ "name": "ImageCropper",
+ "description": "Control to crop rectangular and circular images.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.ImageCropper",
+ "usings": [
+ "CommunityToolkit.WinUI.Controls",
+ "Windows.Storage",
+ "Windows.Storage.Pickers"
+ ],
+ "curatedKeywords": [
+ "ImageCropper",
+ "Control",
+ "Layout"
+ ],
+ "keywords": [
+ "Controls",
+ "Media"
+ ],
+ "docs": [
+ {
+ "title": "ImageCropper",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/ImageCropper/samples/ImageCropper.md"
+ }
+ ],
+ "toolkit": {
+ "component": "ImageCropper",
+ "documentPath": "components/ImageCropper/samples/ImageCropper.md",
+ "category": "Controls",
+ "subcategory": "Media",
+ "author": "HHChaos"
+ },
+ "samples": [
+ {
+ "header": "ImageCropper",
+ "details": "The ImageCropper Control allows user to freely crop an image.",
+ "xaml": "\u003CGrid RowSpacing=\u002224\u0022\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n \u003Ccontrols:ImageCropper x:Name=\u0022imageCropper\u0022\n Width=\u0022520\u0022\n Height=\u0022520\u0022 /\u003E\n \u003CStackPanel Grid.Row=\u00221\u0022\n HorizontalAlignment=\u0022Center\u0022\n Orientation=\u0022Horizontal\u0022\n Spacing=\u00228\u0022\u003E\n \u003CButton Click=\u0022PickButton_Click\u0022\n Content=\u0022Pick image\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n \u003CButton Click=\u0022SaveButton_Click\u0022\n Content=\u0022Save\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n \u003CButton Click=\u0022ResetButton_Click\u0022\n Content=\u0022Reset\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "code": "public ImageCropperSample()\n{\n _ = Load();\n}\n\nprivate async Task Load()\n{\n var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(\u0022ms-appx:///Assets/Owl.jpg\u0022));\n await imageCropper.LoadImageFromFile(file);\n}\n\nprivate async Task PickImage()\n{\n var filePicker = new FileOpenPicker\n {\n ViewMode = PickerViewMode.Thumbnail,\n SuggestedStartLocation = PickerLocationId.PicturesLibrary,\n FileTypeFilter =\n {\n \u0022.png\u0022,\n \u0022.jpg\u0022,\n \u0022.jpeg\u0022\n }\n };\n var file = await filePicker.PickSingleFileAsync();\n if (file != null \u0026\u0026 imageCropper != null)\n {\n await imageCropper.LoadImageFromFile(file);\n }\n}\n\nprivate async Task SaveCroppedImage()\n{\n var savePicker = new FileSavePicker\n {\n SuggestedStartLocation = PickerLocationId.PicturesLibrary,\n SuggestedFileName = \u0022Cropped_Image\u0022,\n FileTypeChoices =\n {\n {\n \u0022PNG Picture\u0022,\n new List\u003Cstring\u003E\n {\n \u0022.png\u0022\n }\n },\n {\n \u0022JPEG Picture\u0022,\n new List\u003Cstring\u003E\n {\n \u0022.jpg\u0022\n }\n }\n }\n };\n var imageFile = await savePicker.PickSaveFileAsync();\n if (imageFile != null)\n {\n BitmapFileFormat bitmapFileFormat;\n switch (imageFile.FileType.ToLower())\n {\n case \u0022.png\u0022:\n bitmapFileFormat = BitmapFileFormat.Png;\n break;\n case \u0022.jpg\u0022:\n bitmapFileFormat = BitmapFileFormat.Jpeg;\n break;\n default:\n bitmapFileFormat = BitmapFileFormat.Png;\n break;\n }\n\n using (var fileStream = await imageFile.OpenAsync(FileAccessMode.ReadWrite, StorageOpenOptions.None))\n {\n await imageCropper.SaveAsync(fileStream, bitmapFileFormat);\n }\n }\n}\n\nprivate async void PickButton_Click(object sender, RoutedEventArgs e)\n{\n await PickImage();\n}\n\nprivate async void SaveButton_Click(object sender, RoutedEventArgs e)\n{\n await SaveCroppedImage();\n}\n\nprivate void ResetButton_Click(object sender, RoutedEventArgs e)\n{\n imageCropper.Reset();\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ImageCropperSample",
+ "sourcePath": "components/ImageCropper/samples/ImageCropperSample.xaml",
+ "optionBindingsDropped": [
+ "AspectRatio",
+ "CropShape",
+ "ThumbPlacement"
+ ]
+ }
+ },
+ {
+ "header": "ImageCropper Overlay",
+ "details": "The crop area can be overlaid with a brush. Here we use a RadialGradientBrush, but you can use any brush; backdrop Media brushes, Geometry brushes, bitmap and image brushes, and so on.",
+ "xaml": "\u003CGrid\u003E\n \u003Ccontrols:ImageCropper x:Name=\u0022ImageCropper\u0022\n Width=\u0022520\u0022\n Height=\u0022520\u0022\u003E\n \u003Ccontrols:ImageCropper.Overlay\u003E\n \u003Cmedia:RadialGradientBrush\u003E\n \u003CGradientStop Offset=\u00220.75\u0022 Color=\u0022Transparent\u0022 /\u003E\n \u003CGradientStop Offset=\u00221\u0022 Color=\u0022DimGray\u0022 /\u003E\n \u003C/media:RadialGradientBrush\u003E\n \u003C/controls:ImageCropper.Overlay\u003E\n \u003C/controls:ImageCropper\u003E\n\n\u003C/Grid\u003E",
+ "code": "public ImageCropperOverlaySample()\n{\n _ = Load();\n}\n\nprivate async Task Load()\n{\n var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(\u0022ms-appx:///Assets/Owl.jpg\u0022));\n await ImageCropper.LoadImageFromFile(file);\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:media=\u0022using:Microsoft.UI.Xaml.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ImageCropperOverlaySample",
+ "sourcePath": "components/ImageCropper/samples/ImageCropperOverlaySample.xaml",
+ "optionBindingsDropped": [
+ "AspectRatio",
+ "CropShape",
+ "ThumbPlacement"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "layouttransformcontrol",
+ "name": "LayoutTransformControl",
+ "description": "The LayoutTransformControl is a control that supports transformations on FrameworkElement as if applied by LayoutTransform.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.LayoutTransformControl",
+ "curatedKeywords": [
+ "LayoutTransformControl",
+ "Control",
+ "Layout",
+ "RenderTransform",
+ "RotateTransform",
+ "ScaleTransform",
+ "SkewTransform",
+ "Transform"
+ ],
+ "keywords": [
+ "Controls",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "LayoutTransformControl",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/LayoutTransformControl/samples/LayoutTransformControl.md"
+ }
+ ],
+ "toolkit": {
+ "component": "LayoutTransformControl",
+ "documentPath": "components/LayoutTransformControl/samples/LayoutTransformControl.md",
+ "category": "Controls",
+ "subcategory": "Layout",
+ "author": "odonno"
+ },
+ "samples": [
+ {
+ "header": "LayoutTransformControl",
+ "details": "The LayoutTransformControl is a control that applies Matrix transformations on any FrameworkElement of your application. The transformations that can be applied are one of the following: * RotateTransform * ScaleTransform * SkewTransform * MatrixTransform * TransformGroup",
+ "xaml": "\u003CGrid ColumnSpacing=\u002224\u0022\n RowSpacing=\u002224\u0022\u003E\n \u003CGrid.Resources\u003E\n \u003CStyle x:Key=\u0022BorderCardStyle\u0022\n TargetType=\u0022Border\u0022\u003E\n \u003CSetter Property=\u0022Background\u0022 Value=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderBrush\u0022 Value=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderThickness\u0022 Value=\u00221\u0022 /\u003E\n \u003CSetter Property=\u0022Padding\u0022 Value=\u002212\u0022 /\u003E\n \u003CSetter Property=\u0022CornerRadius\u0022 Value=\u0022{StaticResource ControlCornerRadius}\u0022 /\u003E\n \u003C/Style\u003E\n \u003C/Grid.Resources\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition /\u003E\n \u003CRowDefinition /\u003E\n \u003C/Grid.RowDefinitions\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition /\u003E\n \u003CColumnDefinition /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n\n \u003C!-- Layout Transform Fixed Size --\u003E\n \u003Ccontrols:LayoutTransformControl\u003E\n \u003Ccontrols:LayoutTransformControl.Transform\u003E\n \u003CTransformGroup\u003E\n \u003CRotateTransform Angle=\u00220\u0022 /\u003E\n \u003CScaleTransform ScaleX=\u00221\u0022 ScaleY=\u00221\u0022 /\u003E\n \u003CSkewTransform AngleX=\u00220\u0022 AngleY=\u00220\u0022 /\u003E\n \u003C/TransformGroup\u003E\n \u003C/controls:LayoutTransformControl.Transform\u003E\n \u003CBorder Width=\u0022200\u0022\n Height=\u002250\u0022\n Style=\u0022{StaticResource BorderCardStyle}\u0022\u003E\n \u003CTextBlock Text=\u0022Layout Fixed Size.\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/controls:LayoutTransformControl\u003E\n\n \u003C!-- Layout Transform Full Size --\u003E\n \u003Ccontrols:LayoutTransformControl Grid.Column=\u00221\u0022\u003E\n \u003Ccontrols:LayoutTransformControl.Transform\u003E\n \u003CTransformGroup\u003E\n \u003CRotateTransform Angle=\u00220\u0022 /\u003E\n \u003CScaleTransform ScaleX=\u00221\u0022 ScaleY=\u00221\u0022 /\u003E\n \u003CSkewTransform AngleX=\u00220\u0022 AngleY=\u00220\u0022 /\u003E\n \u003C/TransformGroup\u003E\n \u003C/controls:LayoutTransformControl.Transform\u003E\n \u003CBorder Style=\u0022{StaticResource BorderCardStyle}\u0022\u003E\n \u003CTextBlock Text=\u0022Layout Full Frame.\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/controls:LayoutTransformControl\u003E\n\n \u003C!-- Render Transform Fixed Size --\u003E\n \u003CBorder Grid.Row=\u00221\u0022\n Width=\u0022200\u0022\n Height=\u002250\u0022\n RenderTransformOrigin=\u00220.5,0.5\u0022\n Style=\u0022{StaticResource BorderCardStyle}\u0022\u003E\n \u003CBorder.RenderTransform\u003E\n \u003CTransformGroup\u003E\n \u003CRotateTransform Angle=\u00220\u0022 /\u003E\n \u003CScaleTransform ScaleX=\u00221\u0022 ScaleY=\u00221\u0022 /\u003E\n \u003CSkewTransform AngleX=\u00220\u0022 AngleY=\u00220\u0022 /\u003E\n \u003C/TransformGroup\u003E\n \u003C/Border.RenderTransform\u003E\n\n \u003CTextBlock Text=\u0022Render Fixed Size.\u0022 /\u003E\n \u003C/Border\u003E\n\n \u003C!-- Render Transform Full Size --\u003E\n \u003CBorder Grid.Row=\u00221\u0022\n Grid.Column=\u00221\u0022\n RenderTransformOrigin=\u00220.5,0.5\u0022\n Style=\u0022{StaticResource BorderCardStyle}\u0022\u003E\n \u003CBorder.RenderTransform\u003E\n \u003CTransformGroup\u003E\n \u003CRotateTransform Angle=\u00220\u0022 /\u003E\n \u003CScaleTransform ScaleX=\u00221\u0022 ScaleY=\u00221\u0022 /\u003E\n \u003CSkewTransform AngleX=\u00220\u0022 AngleY=\u00220\u0022 /\u003E\n \u003C/TransformGroup\u003E\n \u003C/Border.RenderTransform\u003E\n\n \u003CTextBlock Text=\u0022Render Full Frame.\u0022 /\u003E\n \u003C/Border\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "LayoutTransformControlSample",
+ "sourcePath": "components/LayoutTransformControl/samples/LayoutTransformControlSample.xaml",
+ "optionsResolved": [
+ "Angle=0",
+ "CustomScaleX=1",
+ "CustomScaleY=1",
+ "SkewX=0",
+ "SkewY=0"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "attachedcardshadow",
+ "name": "Attached Card Shadow",
+ "description": "Attach performant and easy to use shadows powered by Win2D.",
+ "nugetPackage": "CommunityToolkit.WinUI.Media",
+ "curatedKeywords": [
+ "shadow",
+ "shadows",
+ "dropshadow",
+ "dropshadowpanel",
+ "attachedshadow",
+ "attacheddropshadow",
+ "attachedcardshadow"
+ ],
+ "keywords": [
+ "Extensions",
+ "Shadows"
+ ],
+ "docs": [
+ {
+ "title": "Attached Card Shadow",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Media/samples/AttachedCardShadow.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Media",
+ "documentPath": "components/Media/samples/AttachedCardShadow.md",
+ "category": "Extensions",
+ "subcategory": "Shadows",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "Basic Attached Card Shadow",
+ "details": "The AttachedCardShadow is the easiest to use and most performant shadow. It is recommended to use it where possible, if taking a Win2D dependency is not a concern. Its only drawbacks are the extra dependency required and that it only supports rectangular and rounded-rectangular geometries (as described in the table above). The great benefit to the AttachedCardShadow is that no extra surface or element is required to add the shadow. This reduces the complexity required in development and allows shadows to easily be added at any point in the development process. It also supports transparent elements, without displaying the shadow behind them! The example shows how easy it is to not only apply an AttachedCardShadow to an element, but use it in a style to apply to multiple elements as well:",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003Cmedia:AttachedCardShadow x:Key=\u0022CommonShadow\u0022\n Offset=\u00224\u0022 /\u003E\n\n \u003CStyle BasedOn=\u0022{StaticResource DefaultButtonStyle}\u0022\n TargetType=\u0022Button\u0022\u003E\n \u003CSetter Property=\u0022ui:Effects.Shadow\u0022 Value=\u0022{StaticResource CommonShadow}\u0022 /\u003E\n \u003CSetter Property=\u0022HorizontalAlignment\u0022 Value=\u0022Center\u0022 /\u003E\n \u003C/Style\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CButton Content=\u0022I Have a Shadow!\u0022 /\u003E\n \u003CImage Width=\u0022100\u0022\n Height=\u0022100\u0022\n ui:Effects.Shadow=\u0022{StaticResource CommonShadow}\u0022\n Source=\u0022ms-appx:///Assets/OwlShadow.jpg\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AttachedCardShadowBasicSample",
+ "sourcePath": "components/Media/samples/Shadows/AttachedCardShadowBasicSample.xaml"
+ }
+ },
+ {
+ "header": "Untemplated Attached Card Shadow",
+ "details": "There can be cases, especially direct usage on untemplated elements, where the AttachedCardShadow may require a parent element to create the desired effect, as seen in this example:",
+ "xaml": "\u003C!--\n If you want to apply a shadow directly in your visual tree to an untemplated element\n You need to at least have a layer of depth as seen in this next example.\n--\u003E\n\u003CBorder Width=\u0022100\u0022\n Height=\u0022100\u0022\u003E\n \u003CBorder BorderBrush=\u0022White\u0022\n BorderThickness=\u00221\u0022\n CornerRadius=\u002232\u0022\u003E\n \u003CBorder.Background\u003E\n \u003CImageBrush ImageSource=\u0022ms-appx:///Assets/OwlShadow.jpg\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003C/Border\u003E\n \u003C!--\n We need to put the Shadow on a parent element here as otherwise the\n rounding of the border of the image above clips the shadow itself.\n This is easier to perform with the Composition only based shadow as the\n Shadow is projected to another element.\n --\u003E\n \u003Cui:Effects.Shadow\u003E\n \u003Cmedia:AttachedCardShadow CornerRadius=\u002232\u0022\n Offset=\u00224,4\u0022 /\u003E\n \u003C/ui:Effects.Shadow\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AttachedCardShadowUntemplatedSample",
+ "sourcePath": "components/Media/samples/Shadows/AttachedCardShadowUntemplatedSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "brushes",
+ "name": "Brushes",
+ "description": "Brush effects that can be applied to images and backgrounds.",
+ "nugetPackage": "CommunityToolkit.WinUI.Media",
+ "curatedKeywords": [
+ "brush",
+ "acrylic",
+ "acrylicbrush",
+ "backdrop",
+ "blur",
+ "gamma",
+ "transfer",
+ "invert",
+ "saturation",
+ "sepia",
+ "tiles",
+ "tile",
+ "image",
+ "blend",
+ "brushes"
+ ],
+ "keywords": [
+ "Xaml",
+ "Effects"
+ ],
+ "docs": [
+ {
+ "title": "Brushes",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Media/samples/Brushes.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Media",
+ "documentPath": "components/Media/samples/Brushes.md",
+ "category": "Xaml",
+ "subcategory": "Effects",
+ "author": "erinwoo"
+ },
+ "samples": [
+ {
+ "header": "AcrylicBrush",
+ "details": "Acrylic is a type of Brush that creates a translucent texture. You can apply acrylic to app surfaces to add depth and help establish a visual hierarchy. To learn more about the acrylic material in Windows: link",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u0022.5\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:AcrylicBrush BlurAmount=\u002215\u0022\n TintColor=\u0022Black\u0022\n TintOpacity=\u0022.6\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003C!-- Foreground image --\u003E\n \u003CImage Height=\u0022100\u0022\n VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/BrushAssets/Trex.png\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "AcrylicBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/AcrylicBrushSample.xaml"
+ }
+ },
+ {
+ "header": "BackdropBlurBrush",
+ "details": "A brush that blurs the background in the application.",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u0022.5\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:BackdropBlurBrush Amount=\u00225.0\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003C/Border\u003E\n \u003C!-- Foreground image --\u003E\n \u003CImage Height=\u0022100\u0022\n VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/BrushAssets/Trex.png\u0022 /\u003E\n \u003C/Grid\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BackdropBlurBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/BackdropBlurBrushSample.xaml"
+ }
+ },
+ {
+ "header": "BackdropGammaTransferBrush",
+ "details": "A brush which modifies the color values of the brush\u0027s background in the application. Map the color intensities of an image using a gamma function created using an amplitude, exponent, and offset you provide for each channel.",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u0022.5\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:BackdropGammaTransferBrush AlphaOffset=\u0022.2\u0022\n RedAmplitude=\u002210\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BackdropGammaTransferBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/BackdropGammaTransferBrushSample.xaml"
+ }
+ },
+ {
+ "header": "BackdropGammaTransferBrush",
+ "details": "A brush that inverts the colors of the brush\u0027s background in the application.",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u0022.5\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:BackdropInvertBrush /\u003E\n \u003C/Border.Background\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BackdropInvertBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/BackdropInvertBrushSample.xaml"
+ }
+ },
+ {
+ "header": "BackdropGammaTransferBrush",
+ "details": "A brush that can increase or decrease the saturation of the brush\u0027s background in the application. The Saturation property specifies a double value for the amount of Saturation to apply from 0.0 - 1.0. Zero being monochrome, and one being fully saturated. The default is 0.5.",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u0022.5\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:BackdropSaturationBrush Saturation=\u0022.2\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BackdropSaturationBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/BackdropSaturationBrushSample.xaml"
+ }
+ },
+ {
+ "header": "BackdropGammaTransferBrush",
+ "details": "A brush that applies the sepia effect to the brush\u0027s background. The Intensity property specifies a double value for the amount of Sepia to apply from 0.0 - 1.0. Zero being none, and one being full Sepia effect. The default is 0.5.",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u0022.5\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:BackdropSepiaBrush Intensity=\u0022.8\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BackdropSepiaBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/BackdropSepiaBrushSample.xaml"
+ }
+ },
+ {
+ "header": "BackdropGammaTransferBrush",
+ "details": "A brush that blends the provided image with whatever is behind it in the application with the provided blend mode. The ImageBlendMode property specifies how the image should be blended with the backdrop. More info about the effect modes can be found here.",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:ImageBlendBrush Mode=\u0022ColorDodge\u0022\n Source=\u0022ms-appx:///Assets/BrushAssets/Trex.png\u0022\n Stretch=\u0022None\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ImageBlendBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/ImageBlendBrushSample.xaml"
+ }
+ },
+ {
+ "header": "Tiles Brush",
+ "details": "A brush can be used to display a tiled image as a background.",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CBorder Width=\u0022500\u0022\n Height=\u0022500\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u00221\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:TilesBrush TextureUri=\u0022ms-appx:///Assets/BrushAssets/PolkaDotsBackground.jpg\u0022 /\u003E\n \u003C/Border.Background\u003E\n\n \u003CImage Height=\u0022100\u0022\n VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/BrushAssets/Trex.png\u0022 /\u003E\n \u003C/Border\u003E\n\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "TilesBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/TilesBrushSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "effectanimations",
+ "name": "EffectAnimations",
+ "description": "Animate a Win2D effect using XAML.",
+ "nugetPackage": "CommunityToolkit.WinUI.Media",
+ "curatedKeywords": [
+ "Media",
+ "effects",
+ "animations",
+ "pipeline"
+ ],
+ "keywords": [
+ "Animations",
+ "Media"
+ ],
+ "docs": [
+ {
+ "title": "EffectAnimations",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Media/samples/EffectAnimations.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Media",
+ "documentPath": "components/Media/samples/EffectAnimations.md",
+ "category": "Animations",
+ "subcategory": "Media",
+ "author": "arlodotexe"
+ },
+ "samples": [
+ {
+ "header": "EffectAnimation",
+ "details": "EffectAnimations are used to animate the Win2D effects in CommunityToolkit.WinUI.Media.Effects without code-behind. Combined with an AnimationSet, you can string together complex animated effects that run sequentially or simultaneously.",
+ "xaml": "\u003CGrid\u003E\n \u003CBorder Height=\u0022320\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C/Border\u003E\n\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n FontSize=\u002236\u0022\n FontWeight=\u0022SemiBold\u0022\n Foreground=\u0022White\u0022\n Text=\u0022This is sample text\u0022\n TextWrapping=\u0022Wrap\u0022 /\u003E\n\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory\u003E\n \u003Cmedia:LuminanceToAlphaEffect /\u003E\n\n \u003Cmedia:OpacityEffect Value=\u00220.4\u0022 /\u003E\n\n \u003Cmedia:BlendEffect Mode=\u0022Multiply\u0022\n Source=\u0022{media:BackdropSource}\u0022 /\u003E\n\n \u003Cmedia:BlurEffect x:Name=\u0022ImageBlurEffect\u0022\n Amount=\u002232\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n\n \u003Cmedia:SaturationEffect x:Name=\u0022ImageSaturationEffect\u0022\n IsAnimatable=\u0022True\u0022\n Value=\u00220\u0022 /\u003E\n\n \u003Cmedia:ExposureEffect x:Name=\u0022ImageExposureEffect\u0022\n Amount=\u00221\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n\n \u003Canimations:Explicit.Animations\u003E\n \u003Canimations:AnimationSet x:Name=\u0022ClipAnimation\u0022\u003E\n \u003Canimations:AnimationScope EasingMode=\u0022EaseOut\u0022\n Duration=\u00220:0:3\u0022\u003E\n\n \u003Canimations:ClipAnimation From=\u00220,0,1280,0\u0022\n To=\u00220\u0022 /\u003E\n\n \u003Canimations:TranslationAnimation From=\u002232,0,0\u0022\n To=\u00220\u0022 /\u003E\n\n \u003Canimations:ScaleAnimation From=\u00221.1\u0022\n To=\u00221\u0022 /\u003E\n\n \u003Canimations:BlurEffectAnimation Target=\u0022{x:Bind ImageBlurEffect}\u0022\n From=\u002232\u0022\n To=\u00220\u0022 /\u003E\n\n \u003Canimations:SaturationEffectAnimation Target=\u0022{x:Bind ImageSaturationEffect}\u0022\n From=\u00220\u0022\n To=\u00221.2\u0022 /\u003E\n\n \u003Canimations:ExposureEffectAnimation Target=\u0022{x:Bind ImageExposureEffect}\u0022\n From=\u00221\u0022\n To=\u00220\u0022 /\u003E\n \u003C/animations:AnimationScope\u003E\n \u003C/animations:AnimationSet\u003E\n \u003C/animations:Explicit.Animations\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Loaded\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind ClipAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:animations=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "EffectAnimationsSample",
+ "sourcePath": "components/Media/samples/EffectAnimationsSample.xaml"
+ }
+ },
+ {
+ "header": "BlurAnimationEffect",
+ "details": "Apply and animate a Win2D BlurEffect",
+ "xaml": "\u003CBorder Height=\u0022280\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory\u003E\n \u003Cmedia:BlurEffect x:Name=\u0022ImageBlurEffect\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022BlurAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:BlurEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageBlurEffect}\u0022\n From=\u00220\u0022\n To=\u00228\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n\n \u003Cani:BlurEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageBlurEffect}\u0022\n From=\u00228\u0022\n To=\u00220\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Loaded\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind BlurAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "BlurEffectAnimationSample",
+ "sourcePath": "components/Media/samples/BlurEffectAnimationSample.xaml"
+ }
+ },
+ {
+ "header": "ColorAnimationEffect",
+ "details": "Animate an overlaid color with a Win2D ColorEffect.",
+ "xaml": "\u003CBorder Height=\u0022280\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory\u003E\n \u003Cmedia:TintEffect x:Name=\u0022ImageColorEffect\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022ColorAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:ColorEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageColorEffect}\u0022\n From=\u0022Transparent\u0022\n To=\u0022#803300FF\u0022\n Duration=\u00220:0:2\u0022 /\u003E\n\n \u003Cani:ColorEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageColorEffect}\u0022\n From=\u0022#803300FF\u0022\n To=\u0022Transparent\u0022\n Duration=\u00220:0:2\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Loaded\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind ColorAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ColorEffectAnimationSample",
+ "sourcePath": "components/Media/samples/ColorEffectAnimationSample.xaml"
+ }
+ },
+ {
+ "header": "CrossFadeAnimationEffect",
+ "details": "Blends and animates any PipelineBuilder source with any Win2D effect. This sample blends an image with a BlurEffect and effect from CommunityToolkit.WinUI.Media.Effects.",
+ "xaml": "\u003CBorder Height=\u0022280\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory\u003E\n \u003Cmedia:BlurEffect Amount=\u00228\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n\n \u003Cmedia:CrossFadeEffect x:Name=\u0022ImageCrossFadeEffect\u0022\n Factor=\u00221\u0022\n IsAnimatable=\u0022True\u0022\n Source=\u0022{media:BackdropSource}\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022CrossFadeAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:CrossFadeEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageCrossFadeEffect}\u0022\n From=\u00220\u0022\n To=\u00221\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n\n \u003Cani:CrossFadeEffectAnimation EasingMode=\u0022EaseIn\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageCrossFadeEffect}\u0022\n From=\u00221\u0022\n To=\u00220\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Loaded\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind CrossFadeAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "CrossFadeEffectAnimationSample",
+ "sourcePath": "components/Media/samples/CrossFadeEffectAnimationSample.xaml"
+ }
+ },
+ {
+ "header": "ExposureAnimationEffect",
+ "details": "Animate the exposure with a Win2D ExposureEffect.",
+ "xaml": "\u003CBorder Height=\u0022280\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory\u003E\n \u003Cmedia:ExposureEffect x:Name=\u0022ImageExposureEffect\u0022\n Amount=\u00220\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022ExposureAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:ExposureEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageExposureEffect}\u0022\n From=\u00220\u0022\n To=\u00221\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n\n \u003Cani:ExposureEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageExposureEffect}\u0022\n From=\u00221\u0022\n To=\u00220\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Loaded\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind ExposureAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ExposureEffectAnimationSample",
+ "sourcePath": "components/Media/samples/ExposureEffectAnimationSample.xaml"
+ }
+ },
+ {
+ "header": "HueRotationAnimationEffect",
+ "details": "Animate Hue to a specific angle using a Win2D HueRotationEffect.",
+ "xaml": "\u003CBorder Height=\u0022280\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory\u003E\n \u003Cmedia:HueRotationEffect x:Name=\u0022ImageHueRotationEffect\u0022\n Angle=\u00220\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022HueRotationAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:HueRotationEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageHueRotationEffect}\u0022\n From=\u00220\u0022\n To=\u00226\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n\n \u003Cani:HueRotationEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageHueRotationEffect}\u0022\n From=\u00226\u0022\n To=\u00220\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Loaded\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind HueRotationAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "HueRotationEffectAnimationSample",
+ "sourcePath": "components/Media/samples/HueRotationEffectAnimationSample.xaml"
+ }
+ },
+ {
+ "header": "SaturationAnimationEffect",
+ "details": "Animate a Saturation effect",
+ "xaml": "\u003CBorder Height=\u0022280\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory\u003E\n \u003Cmedia:SaturationEffect x:Name=\u0022ImageSaturationEffect\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022SaturationAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:SaturationEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageSaturationEffect}\u0022\n From=\u00225\u0022\n To=\u00220\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n\n \u003Cani:SaturationEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageSaturationEffect}\u0022\n From=\u00220\u0022\n To=\u00225\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Loaded\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind SaturationAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SaturationEffectAnimationSample",
+ "sourcePath": "components/Media/samples/SaturationEffectAnimationSample.xaml"
+ }
+ },
+ {
+ "header": "SepiaAnimationEffect",
+ "details": "Animate a Sepia effect",
+ "xaml": "\u003CBorder Height=\u0022280\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory\u003E\n \u003Cmedia:SepiaEffect x:Name=\u0022ImageSepiaEffect\u0022\n IsAnimatable=\u0022True\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n\n \u003Cani:Explicit.Animations\u003E\n \u003Cani:AnimationSet x:Name=\u0022SepiaAnimation\u0022\n IsSequential=\u0022True\u0022\u003E\n \u003Cani:SepiaEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageSepiaEffect}\u0022\n From=\u00220\u0022\n To=\u00221\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n\n \u003Cani:SepiaEffectAnimation EasingMode=\u0022EaseOut\u0022\n EasingType=\u0022Linear\u0022\n Target=\u0022{x:Bind ImageSepiaEffect}\u0022\n From=\u00221\u0022\n To=\u00220\u0022\n Duration=\u00220:0:3\u0022 /\u003E\n \u003C/ani:AnimationSet\u003E\n \u003C/ani:Explicit.Animations\u003E\n\n \u003Cinteractivity:Interaction.Behaviors\u003E\n \u003Cinteractivity:EventTriggerBehavior EventName=\u0022Loaded\u0022\u003E\n \u003Cbehaviors:StartAnimationAction Animation=\u0022{x:Bind SepiaAnimation}\u0022 /\u003E\n \u003C/interactivity:EventTriggerBehavior\u003E\n \u003C/interactivity:Interaction.Behaviors\u003E\n\u003C/Border\u003E",
+ "xmlnsImports": [
+ "xmlns:ani=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:behaviors=\u0022using:CommunityToolkit.WinUI.Behaviors\u0022",
+ "xmlns:interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SepiaEffectAnimationSample",
+ "sourcePath": "components/Media/samples/SepiaEffectAnimationSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "pipelinebrush",
+ "name": "PipelineBrush",
+ "description": "A brush that renders a customizable Composition/Win2D effects pipeline. This allows for applying multiple effects into a single effect chain.",
+ "nugetPackage": "CommunityToolkit.WinUI.Media",
+ "curatedKeywords": [
+ "brush",
+ "pipeline",
+ "pipe"
+ ],
+ "keywords": [
+ "Xaml",
+ "Effects"
+ ],
+ "docs": [
+ {
+ "title": "PipelineBrush",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Media/samples/PipelineBrush.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Media",
+ "documentPath": "components/Media/samples/PipelineBrush.md",
+ "category": "Xaml",
+ "subcategory": "Effects",
+ "author": "erinwoo"
+ },
+ "samples": [
+ {
+ "header": "PipelineBrush",
+ "details": "Source gets or sets the source for the current pipeline (defaults to a BackdropSourceExtension with Microsoft.UI.Xaml.Media.AcrylicBackgroundSource.Backdrop source). Multiple effects can be nested within the PipelineBrush component.",
+ "xaml": "\u003CStackPanel VerticalAlignment=\u0022Center\u0022\n Spacing=\u002232\u0022\u003E\n \u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u0022.5\u0022\u003E\n \u003CBorder.Background\u003E\n \u003Cmedia:PipelineBrush Source=\u0022{media:BackdropSource}\u0022\u003E\n \u003Cmedia:SepiaEffect Intensity=\u00225\u0022 /\u003E\n \u003Cmedia:InvertEffect /\u003E\n \u003Cmedia:BlurEffect Amount=\u00225\u0022 /\u003E\n \u003C/media:PipelineBrush\u003E\n\n \u003C/Border.Background\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "PipelineBrushSample",
+ "sourcePath": "components/Media/samples/Brushes/PipelineBrushSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "pipelinevisualfactory",
+ "name": "PipelineVisualFactory",
+ "description": "A helper type that can be used to create sprite visuals with custom Win2D/Composition effects chains and attach them to UI elements.",
+ "nugetPackage": "CommunityToolkit.WinUI.Media",
+ "curatedKeywords": [
+ "pipeline",
+ "visual",
+ "factory",
+ "pipe"
+ ],
+ "keywords": [
+ "Xaml",
+ "Effects"
+ ],
+ "docs": [
+ {
+ "title": "PipelineVisualFactory",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Media/samples/PipelineVisualFactory.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Media",
+ "documentPath": "components/Media/samples/PipelineVisualFactory.md",
+ "category": "Xaml",
+ "subcategory": "Effects",
+ "author": "erinwoo"
+ },
+ "samples": [
+ {
+ "header": "BackdropBlurBrush",
+ "details": "PipelineVisualFactory can create the same visual brushes as the PipelineBrush type, but it can attach them directly on the underlying Visual instance backing a UI element. This can make the XAML code less verbose and more efficient, as there is no need to insert additional elements just so that a brush can be applied to them.",
+ "xaml": "\u003CGrid\u003E\n \u003C!-- Background image --\u003E\n \u003CImage Height=\u0022400\u0022\n Source=\u0022ms-appx:///Assets/Bloom.jpg\u0022 /\u003E\n \u003C!-- Brush area --\u003E\n \u003CBorder Width=\u0022550\u0022\n Height=\u0022300\u0022\n HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n BorderBrush=\u0022Black\u0022\n BorderThickness=\u0022.5\u0022\u003E\n \u003Cmedia:UIElementExtensions.VisualFactory\u003E\n \u003Cmedia:PipelineVisualFactory Source=\u0022{media:BackdropSource}\u0022\u003E\n \u003Cmedia:BlurEffect Amount=\u002215\u0022 /\u003E\n \u003Cmedia:LuminanceToAlphaEffect /\u003E\n \u003Cmedia:SaturationEffect Value=\u00221.4\u0022 /\u003E\n \u003Cmedia:BlendEffect Mode=\u0022Multiply\u0022\n Source=\u0022{media:BackdropSource}\u0022 /\u003E\n \u003C/media:PipelineVisualFactory\u003E\n \u003C/media:UIElementExtensions.VisualFactory\u003E\n \u003C/Border\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:media=\u0022using:CommunityToolkit.WinUI.Media\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "PipelineVisualFactorySample",
+ "sourcePath": "components/Media/samples/Brushes/PipelineVisualFactorySample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "metadatacontrol",
+ "name": "MetadataControl",
+ "description": "The MetadataControl control displays a list of labels and hyper-links separated by a bullet.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.MetadataControl",
+ "usings": [
+ "CommunityToolkit.WinUI.Controls",
+ "System.Windows.Input"
+ ],
+ "curatedKeywords": [
+ "MetadataControl",
+ "Control",
+ "metadata"
+ ],
+ "keywords": [
+ "Controls",
+ "StatusAndInfo"
+ ],
+ "docs": [
+ {
+ "title": "MetadataControl",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/MetadataControl/samples/MetadataControl.md"
+ }
+ ],
+ "toolkit": {
+ "component": "MetadataControl",
+ "documentPath": "components/MetadataControl/samples/MetadataControl.md",
+ "category": "Controls",
+ "subcategory": "StatusAndInfo",
+ "author": "vgromfeld"
+ },
+ "samples": [
+ {
+ "header": "MetadataControl",
+ "details": "The bullet separator can be customized using the Separator property. AccessibleSeparator is used as a replacement for Separator to generate the accessible string. The control needs a list of MetadataItem. Each item will be displayed either as a text or as an hyper-link (if the Commandproperty is set). The default control template is using on a TextBlock. The style of this TextBlock can be customized using the TextBlockStyle property.",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022*\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n\n \u003Ccontrols:MetadataControl x:Name=\u0022metadataControl\u0022\n AccessibleSeparator=\u0022, \u0022\n Separator=\u0022 • \u0022 /\u003E\n\n \u003CTextBlock x:Name=\u0022OutputTxt\u0022\n Grid.Row=\u00221\u0022\n Margin=\u00220,0,0,24\u0022\n FontWeight=\u0022SemiBold\u0022 /\u003E\n \u003CStackPanel Grid.Row=\u00222\u0022\n Orientation=\u0022Horizontal\u0022\n Spacing=\u00228\u0022\u003E\n \u003CButton Click=\u0022AddLabel_Click\u0022\n Content=\u0022Add label\u0022 /\u003E\n \u003CButton Click=\u0022AddCommand_Click\u0022\n Content=\u0022Add command\u0022 /\u003E\n \u003CButton Click=\u0022Clear_Click\u0022\n Content=\u0022Clear\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "code": "private static readonly string[] Labels = \u0022Lorem ipsum dolor sit amet consectetur adipiscing elit\u0022.Split(\u0027 \u0027);\n\nprivate readonly Random _random;\n\nprivate readonly ObservableCollection\u003CMetadataItem\u003E _units;\n\nprivate readonly DelegateCommand\u003Cobject\u003E _command;\n\npublic MetadataControlSample()\n{\n _random = new Random();\n _units = new ObservableCollection\u003CMetadataItem\u003E\n {\n new MetadataItem\n {\n Label = GetRandomLabel()\n },\n new MetadataItem\n {\n Label = GetRandomLabel()\n }\n };\n _command = new DelegateCommand\u003Cobject\u003E(OnExecuteCommand);\n metadataControl.Items = _units;\n}\n\nprivate string GetRandomLabel() =\u003E Labels[_random!.Next(Labels.Length)];\n\nprivate void OnExecuteCommand(object obj)\n{\n OutputTxt.Text = $\u0022Command invoked - parameter: {obj}\u0022;\n}\n\nprivate void AddLabel_Click(object sender, RoutedEventArgs e)\n{\n if (_units != null)\n {\n _units.Add(new MetadataItem { Label = GetRandomLabel() });\n }\n}\n\nprivate void AddCommand_Click(object sender, RoutedEventArgs e)\n{\n if (_units != null)\n {\n var label = GetRandomLabel();\n _units.Add(new MetadataItem { Label = label, Command = _command!, CommandParameter = label, });\n }\n}\n\nprivate void Clear_Click(object sender, RoutedEventArgs e)\n{\n if (_units != null)\n {\n OutputTxt.Text = \u0022\u0022;\n _units.Clear();\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "MetadataControlSample",
+ "sourcePath": "components/MetadataControl/samples/MetadataControlSample.xaml",
+ "optionsResolved": [
+ "AccessibleSeparator=, ",
+ "Separator= • "
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "constrainedbox",
+ "name": "ConstrainedBox",
+ "description": "The ConstrainedBox is a FrameworkElement which can allow a developer to constrain the aspect ratio, scale, or multiple boundary alignment of its content.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Primitives",
+ "curatedKeywords": [
+ "viewbox",
+ "content decorator",
+ "ConstrainedBox"
+ ],
+ "keywords": [
+ "Layouts",
+ "Media"
+ ],
+ "docs": [
+ {
+ "title": "ConstrainedBox",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Primitives/samples/ConstrainedBox.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Primitives",
+ "documentPath": "components/Primitives/samples/ConstrainedBox.md",
+ "category": "Layouts",
+ "subcategory": "Media",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "ConstrainedBox Aspect",
+ "details": "The most common use-case for the ConstrainedBox is to maintain the aspect ratio of an image. For instance the following example maintains a 16:3 aspect ratio of the image at the top of its container (like a page) and center on the image\u0027s content:",
+ "xaml": "\u003Ccontrols:ConstrainedBox VerticalAlignment=\u0022Top\u0022\n AspectRatio=\u002216:3\u0022\u003E\n \u003CImage VerticalAlignment=\u0022Center\u0022\n Source=\u0022ms-appx:///Assets/WestSeattleView.jpg\u0022\n Stretch=\u0022UniformToFill\u0022 /\u003E\n\u003C/controls:ConstrainedBox\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ConstrainedBoxAspectSample",
+ "sourcePath": "components/Primitives/samples/ConstrainedBox/ConstrainedBoxAspectSample.xaml"
+ }
+ },
+ {
+ "header": "ConstrainedBox Scale",
+ "details": "Another scenario may be for keeping a \u0027safe\u0027 margin around the content on your page. You may want this to not be a fixed margin but be proportional to a viewport. This sample demonstrates that using ScaleX/ScaleY:",
+ "xaml": "\u003C!-- Set MaxHeight here as sample has infinite height which doesn\u0027t make sense for this scenario --\u003E\n\u003Ccontrols:ConstrainedBox MaxHeight=\u0022512\u0022\n ScaleX=\u00220.8\u0022\n ScaleY=\u00220.5\u0022\u003E\n \u003CGrid Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022 /\u003E\n\u003C/controls:ConstrainedBox\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ConstrainedBoxScaleSample",
+ "sourcePath": "components/Primitives/samples/ConstrainedBox/ConstrainedBoxScaleSample.xaml"
+ }
+ },
+ {
+ "header": "ConstrainedBox Multiple",
+ "details": "The next sample shows how you can use the MultipleX property to snap the size of a component to the pattern of an image:",
+ "xaml": "\u003CStackPanel Orientation=\u0022Horizontal\u0022\u003E\n \u003Ccontrols:ConstrainedBox x:Name=\u0022CheckerPattern\u0022\n MinWidth=\u002264\u0022\n MaxWidth=\u0022512\u0022\n AspectRatio=\u00221:1\u0022\n MultipleX=\u002264\u0022\u003E\n \u003CImage ui:UIElementExtensions.ClipToBounds=\u0022True\u0022\n Source=\u0022ms-appx:///Assets/checker.png\u0022\n Stretch=\u0022None\u0022 /\u003E\n \u003C/controls:ConstrainedBox\u003E\n \u003Ccontrols:ContentSizer HorizontalAlignment=\u0022Right\u0022\n TargetControl=\u0022{x:Bind CheckerPattern}\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ConstrainedBoxMultipleSample",
+ "sourcePath": "components/Primitives/samples/ConstrainedBox/ConstrainedBoxMultipleSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "dockpanel",
+ "name": "DockPanel",
+ "description": "Defines an area where you can arrange child elements either horizontally or vertically, relative to each other.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Primitives",
+ "usings": [
+ "CommunityToolkit.WinUI.Controls",
+ "Windows.UI"
+ ],
+ "curatedKeywords": [
+ "DockPanel"
+ ],
+ "keywords": [
+ "Layouts",
+ "Panel"
+ ],
+ "docs": [
+ {
+ "title": "DockPanel",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Primitives/samples/DockPanel.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Primitives",
+ "documentPath": "components/Primitives/samples/DockPanel.md",
+ "category": "Layouts",
+ "subcategory": "Panel",
+ "author": "IbraheemOsama"
+ },
+ "samples": [
+ {
+ "header": "DockPanel",
+ "details": "The DockPanel position child controls based on the child Dock property, you have 4 options to Dock, left (Default), right, top, bottom. You can set DockPanel LastChildFill property to true if you want the last item added to the DockPanel to fill the rest empty space.",
+ "xaml": "\u003CGrid ColumnSpacing=\u002216\u0022\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition Width=\u0022Auto\u0022 /\u003E\n \u003CColumnDefinition Width=\u0022*\u0022 /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n \u003CStackPanel Orientation=\u0022Vertical\u0022\n Spacing=\u00224\u0022\u003E\n \u003CTextBlock Grid.Row=\u00221\u0022\n Margin=\u00224\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022In this demo you can\u0027t add a child after a Stretch child\u0022 /\u003E\n \u003CButton Click=\u0022AddTopDock\u0022\n Content=\u0022Add Top child\u0022 /\u003E\n \u003CButton Click=\u0022AddLeftDock\u0022\n Content=\u0022Add Left child\u0022 /\u003E\n \u003CButton Click=\u0022AddBottomDock\u0022\n Content=\u0022Add Bottom child\u0022 /\u003E\n \u003CButton Click=\u0022AddRightDock\u0022\n Content=\u0022Add Right child\u0022 /\u003E\n \u003CButton Click=\u0022AddStretchDock\u0022\n Content=\u0022Add Stretch child\u0022 /\u003E\n \u003CButton Click=\u0022ClearAllDock\u0022\n Content=\u0022Clear all\u0022 /\u003E\n \u003C/StackPanel\u003E\n\n \u003Ccontrols:DockPanel x:Name=\u0022SampleDockPanel\u0022\n Grid.Column=\u00221\u0022\n Background=\u0022{ThemeResource CardBackgroundFillColorSecondaryBrush}\u0022\n HorizontalSpacing=\u00225\u0022\n LastChildFill=\u0022False\u0022\n VerticalSpacing=\u00225\u0022\u003E\n \u003CStackPanel Height=\u0022100\u0022\n controls:DockPanel.Dock=\u0022Top\u0022\n Background=\u0022Black\u0022 /\u003E\n \u003CStackPanel Width=\u0022100\u0022\n controls:DockPanel.Dock=\u0022Left\u0022\n Background=\u0022Red\u0022 /\u003E\n \u003CStackPanel Height=\u0022100\u0022\n controls:DockPanel.Dock=\u0022Bottom\u0022\n Background=\u0022Green\u0022 /\u003E\n \u003CStackPanel Width=\u0022100\u0022\n controls:DockPanel.Dock=\u0022Right\u0022\n Background=\u0022Blue\u0022 /\u003E\n \u003C/controls:DockPanel\u003E\n\u003C/Grid\u003E",
+ "code": "private static readonly Random Rand = new Random();\n\nprivate void ClearAllDock(object sender, RoutedEventArgs e)\n{\n SampleDockPanel.Children.Clear();\n SampleDockPanel.LastChildFill = false;\n}\n\nprivate void AddStretchDock(object sender, RoutedEventArgs e)\n{\n AddChild(Dock.Bottom, false, false);\n SampleDockPanel.LastChildFill = true;\n}\n\nprivate void AddBottomDock(object sender, RoutedEventArgs e)\n{\n AddChild(Dock.Bottom, false, true);\n}\n\nprivate void AddTopDock(object sender, RoutedEventArgs e)\n{\n AddChild(Dock.Top, false, true);\n}\n\nprivate void AddLeftDock(object sender, RoutedEventArgs e)\n{\n AddChild(Dock.Left, true, false);\n}\n\nprivate void AddRightDock(object sender, RoutedEventArgs e)\n{\n AddChild(Dock.Right, true, false);\n}\n\nprivate void AddChild(Dock dock, bool setWidth = false, bool setHeight = false)\n{\n if (SampleDockPanel.LastChildFill)\n {\n return;\n }\n\n const int maxColor = 255;\n var childStackPanel = new StackPanel\n {\n Background = new SolidColorBrush(Color.FromArgb((byte)Rand.Next(0, maxColor), (byte)Rand.Next(0, maxColor), (byte)Rand.Next(0, maxColor), 1))\n };\n if (setHeight)\n {\n childStackPanel.Height = Rand.Next(50, 80);\n }\n\n if (setWidth)\n {\n childStackPanel.Width = Rand.Next(50, 80);\n }\n\n childStackPanel.SetValue(DockPanel.DockProperty, dock);\n childStackPanel.PointerPressed \u002B= this.ChildStackPanel_PointerPressed;\n SampleDockPanel.Children.Add(childStackPanel);\n}\n\nprivate void ChildStackPanel_PointerPressed(object sender, PointerRoutedEventArgs e)\n{\n SampleDockPanel.Children.Remove((StackPanel)sender);\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "DockPanelSample",
+ "sourcePath": "components/Primitives/samples/DockPanelSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "staggeredlayout",
+ "name": "StaggeredLayout",
+ "description": "The StaggeredLayout displays items in a column approach where an item will be added to whichever column has used the least amount of space.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Primitives",
+ "usings": [
+ "Windows.UI"
+ ],
+ "curatedKeywords": [
+ "StaggeredPanel",
+ "StaggeredLayout",
+ "Layout"
+ ],
+ "keywords": [
+ "Layouts",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "StaggeredLayout",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Primitives/samples/StaggeredLayout.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Primitives",
+ "documentPath": "components/Primitives/samples/StaggeredLayout.md",
+ "category": "Layouts",
+ "subcategory": "Layout",
+ "author": "skendrot"
+ },
+ "samples": [
+ {
+ "header": "StaggeredLayout",
+ "details": "The StaggeredLayout allows for layout of items in a column approach where an item will be added to whichever column has used the least amount of space. It is a Layout for ItemsRepeater which will provide better virtualization compared to the StaggeredPanel below.",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.Resources\u003E\n \u003CDataTemplate x:Key=\u0022StaggeredTemplate\u0022\n x:DataType=\u0022local:ColorItem\u0022\u003E\n \u003CGrid Background=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022\n BorderBrush=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\n CornerRadius=\u0022{StaticResource ControlCornerRadius}\u0022\u003E\n \u003CBorder Height=\u0022{x:Bind Height}\u0022\n Margin=\u00224\u0022\n CornerRadius=\u0022{StaticResource ControlCornerRadius}\u0022\u003E\n \u003CBorder.Background\u003E\n \u003CSolidColorBrush Color=\u0022{x:Bind Color}\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003CTextBlock Margin=\u00226,4,4,4\u0022\n FontSize=\u002216\u0022\n Text=\u0022{x:Bind Index}\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\n \u003C/DataTemplate\u003E\n \u003C/Grid.Resources\u003E\n \u003CScrollViewer\u003E\n \u003Cmuxc:ItemsRepeater x:Name=\u0022StaggeredRepeater\u0022\n ItemTemplate=\u0022{StaticResource StaggeredTemplate}\u0022\n ItemsSource=\u0022{x:Bind ColorsCollection, Mode=OneWay}\u0022\u003E\n \u003Cmuxc:ItemsRepeater.Layout\u003E\n \u003Ccontrols:StaggeredLayout ColumnSpacing=\u00225\u0022\n DesiredColumnWidth=\u0022250\u0022\n RowSpacing=\u00225\u0022 /\u003E\n \u003C/muxc:ItemsRepeater.Layout\u003E\n \u003C/muxc:ItemsRepeater\u003E\n \u003C/ScrollViewer\u003E\n\u003C/Grid\u003E",
+ "code": "public ObservableCollection\u003CColorItem\u003E ColorsCollection = new();\n\npublic Random random;\n\npublic StaggeredLayoutSample()\n{\n random = new Random(DateTime.Now.Millisecond);\n for (int i = 0; i \u003C random.Next(100, 200); i\u002B\u002B)\n {\n var item = new ColorItem\n {\n Index = i,\n Height = random.Next(50, 250),\n Color = Color.FromArgb(255, (byte)random.Next(0, 255), (byte)random.Next(0, 255), (byte)random.Next(0, 255))\n };\n ColorsCollection.Add(item);\n }\n}\n\npublic class ColorItem\n{\n public int Index { get; internal set; }\n public int Width { get; internal set; }\n public int Height { get; internal set; }\n public Color Color { get; internal set; }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StaggeredLayoutSample",
+ "sourcePath": "components/Primitives/samples/StaggeredLayoutSample.xaml",
+ "optionsResolved": [
+ "ColumnSpacing=5",
+ "DesiredColumnWidth=250",
+ "RowSpacing=5"
+ ],
+ "optionBindingsDropped": [
+ "ItemsStretch"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "staggeredpanel",
+ "name": "StaggeredPanel",
+ "description": "The StaggeredPanel displays items in a column approach where an item will be added to whichever column has used the least amount of space.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Primitives",
+ "usings": [
+ "Windows.UI"
+ ],
+ "curatedKeywords": [
+ "StaggeredPanel",
+ "StaggeredLayout",
+ "Layout"
+ ],
+ "keywords": [
+ "Layouts",
+ "Panel"
+ ],
+ "docs": [
+ {
+ "title": "StaggeredPanel",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Primitives/samples/StaggeredPanel.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Primitives",
+ "documentPath": "components/Primitives/samples/StaggeredPanel.md",
+ "category": "Layouts",
+ "subcategory": "Panel",
+ "author": "skendrot"
+ },
+ "samples": [
+ {
+ "header": "StaggeredPanel",
+ "details": "The StaggeredPanel allows for layout of items in a column approach where an item will be added to whichever column has used the least amount of space.",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.Resources\u003E\n \u003CDataTemplate x:Key=\u0022StaggeredTemplate\u0022\n x:DataType=\u0022local:ColorItem\u0022\u003E\n \u003CGrid Background=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022\n BorderBrush=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022\n BorderThickness=\u00221\u0022\n CornerRadius=\u0022{StaticResource ControlCornerRadius}\u0022\u003E\n \u003CBorder Width=\u0022{x:Bind Width}\u0022\n Height=\u0022{x:Bind Height}\u0022\n Margin=\u00224\u0022\n CornerRadius=\u0022{StaticResource ControlCornerRadius}\u0022\u003E\n \u003CBorder.Background\u003E\n \u003CSolidColorBrush Color=\u0022{x:Bind Color}\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003CTextBlock Margin=\u00226,4,4,4\u0022\n FontSize=\u002216\u0022\n Text=\u0022{x:Bind Index}\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n \u003C/DataTemplate\u003E\n \u003C/Grid.Resources\u003E\n \u003CGridView Name=\u0022GridView\u0022\n ItemTemplate=\u0022{StaticResource StaggeredTemplate}\u0022\n ItemsSource=\u0022{x:Bind ColorsCollection, Mode=OneWay}\u0022\u003E\n\n \u003CGridView.ItemsPanel\u003E\n \u003CItemsPanelTemplate\u003E\n \u003Ccontrols:StaggeredPanel ColumnSpacing=\u00225\u0022\n DesiredColumnWidth=\u0022250\u0022\n RowSpacing=\u00225\u0022 /\u003E\n \u003C/ItemsPanelTemplate\u003E\n \u003C/GridView.ItemsPanel\u003E\n \u003C/GridView\u003E\n\u003C/Grid\u003E",
+ "code": "public ObservableCollection\u003CColorItem\u003E ColorsCollection = new();\n\npublic Random random;\n\npublic StaggeredPanelSample()\n{\n random = new Random(DateTime.Now.Millisecond);\n for (int i = 0; i \u003C random.Next(100, 200); i\u002B\u002B)\n {\n var item = new ColorItem\n {\n Index = i,\n Width = random.Next(50, 250),\n Height = random.Next(50, 250),\n Color = Color.FromArgb(255, (byte)random.Next(0, 255), (byte)random.Next(0, 255), (byte)random.Next(0, 255))\n };\n ColorsCollection.Add(item);\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "StaggeredPanelSample",
+ "sourcePath": "components/Primitives/samples/StaggeredPanelSample.xaml",
+ "optionsResolved": [
+ "ColumnSpacing=5",
+ "DesiredColumnWidth=250",
+ "RowSpacing=5"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "switchpresenter",
+ "name": "SwitchPresenter",
+ "description": "A XAML ContentPresenter which can act like a switch statement for showing different UI based on a condition.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Primitives",
+ "curatedKeywords": [
+ "switch",
+ "logic",
+ "switchpresenter",
+ "contentpresenter",
+ "visibility",
+ "triggers",
+ "converters"
+ ],
+ "keywords": [
+ "Layouts",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "SwitchPresenter",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Primitives/samples/SwitchPresenter.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Primitives",
+ "documentPath": "components/Primitives/samples/SwitchPresenter.md",
+ "category": "Layouts",
+ "subcategory": "Miscellaneous",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "SwitchPresenter Layout",
+ "details": "SwitchPresenter can make it easier to follow complex layout changes or layouts with varying logic that are based on a property, for instance the type of ticketing mode for an airline:",
+ "xaml": "\u003CStackPanel\u003E\n \u003CComboBox x:Name=\u0022Lookup\u0022\n Margin=\u00220,0,0,8\u0022\n Header=\u0022Look up reservation\u0022\n SelectedIndex=\u00220\u0022\u003E\n \u003Cx:String\u003ESelect an option\u003C/x:String\u003E\n \u003Cx:String\u003EConfirmation Code\u003C/x:String\u003E\n \u003Cx:String\u003EE-ticket number\u003C/x:String\u003E\n \u003Cx:String\u003EMileage Plan number\u003C/x:String\u003E\n \u003C/ComboBox\u003E\n \u003C!-- SwitchPresenter binds to a value --\u003E\n \u003Ccontrols:SwitchPresenter Value=\u0022{Binding SelectedItem, ElementName=Lookup}\u0022\u003E\n \u003C!-- And then only dynamically displays the Case with the matching Value --\u003E\n \u003Ccontrols:Case Value=\u0022Confirmation Code\u0022\u003E\n \u003CStackPanel\u003E\n \u003CTextBox Name=\u0022ConfirmationCodeValidator\u0022\n ui:TextBoxExtensions.Regex=\u0022^[a-zA-Z]{6}$\u0022\n Header=\u0022Confirmation code\u0022\n PlaceholderText=\u00226 letters\u0022 /\u003E\n \u003CTextBlock Text=\u0022Thanks for entering a valid code!\u0022\n Visibility=\u0022{Binding (ui:TextBoxExtensions.IsValid), ElementName=ConfirmationCodeValidator}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022E-ticket number\u0022\u003E\n \u003CStackPanel\u003E\n \u003CTextBox Name=\u0022TicketValidator\u0022\n ui:TextBoxExtensions.Regex=\u0022(^\\d{10}$)|(^\\d{13}$)\u0022\n Header=\u0022E-ticket number\u0022\n PlaceholderText=\u002210 or 13 numbers\u0022 /\u003E\n \u003CTextBlock Text=\u0022Thanks for entering a valid code!\u0022\n Visibility=\u0022{Binding (ui:TextBoxExtensions.IsValid), ElementName=TicketValidator}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Mileage Plan number\u0022\u003E\n \u003CTextBox Name=\u0022PlanValidator\u0022\n Header=\u0022Mileage Plan #\u0022\n PlaceholderText=\u0022Mileage Plan #\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003C!-- You can also provide a default case if no match is found --\u003E\n \u003Ccontrols:Case IsDefault=\u0022True\u0022\u003E\n \u003CTextBlock Text=\u0022Please select a way to lookup your reservation above...\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003C/controls:SwitchPresenter\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SwitchPresenterLayoutSample",
+ "sourcePath": "components/Primitives/samples/SwitchPresenter/SwitchPresenterLayoutSample.xaml"
+ }
+ },
+ {
+ "header": "SwitchPresenter Value",
+ "details": "Or it can simply be used to clearly display different outcomes based on some state which can be useful for a NavigationView or with a simple enum as in the following example:",
+ "xaml": "\u003CStackPanel\u003E\n \u003CStackPanel.Resources\u003E\n \u003C!--\n If you reference an enum directly in UWP, you need to use it somewhere for the XamlTypeInfo reference to be generated...\n --\u003E\n \u003Clocal:Animal x:Key=\u0022MyAnimal\u0022\u003ECat\u003C/local:Animal\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CComboBox x:Name=\u0022AnimalPicker\u0022\n Header=\u0022Pick an Animal\u0022\n ItemsSource=\u0022{ui:EnumValues Type=local:Animal}\u0022\n SelectedIndex=\u00220\u0022 /\u003E\n \u003Ccontrols:SwitchPresenter Padding=\u002216\u0022\n TargetType=\u0022local:Animal\u0022\n Value=\u0022{x:Bind AnimalPicker.SelectedItem, Mode=OneWay}\u0022\u003E\n \u003Ccontrols:Case Value=\u0022Bunny\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83D\uDC07\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Cat\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83D\uDC08\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Dog\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83D\uDC15\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Giraffe\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83E\uDD92\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Llama\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83E\uDD99\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Otter\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83E\uDDA6\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Owl\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83E\uDD89\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Parrot\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83E\uDD9C\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Squirrel\u0022\u003E\n \u003CTextBlock FontSize=\u002232\u0022\n Text=\u0022\uD83D\uDC3F\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003C/controls:SwitchPresenter\u003E\n\u003C/StackPanel\u003E",
+ "code": "public enum Animal\n{\n Bunny,\n Cat,\n Dog,\n Giraffe,\n Llama,\n Otter,\n Owl,\n Parrot,\n Squirrel\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:local=\u0022using:PrimitivesExperiment.Samples.SwitchPresenter\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SwitchPresenterValueSample",
+ "sourcePath": "components/Primitives/samples/SwitchPresenter/SwitchPresenterValueSample.xaml"
+ }
+ },
+ {
+ "header": "SwitchPresenter Loader",
+ "details": "SwitchPresenter can also be used as a replacement for the deprecated Loading control. This provides more fine-grained control over animations and content within each state:",
+ "xaml": "\u003CGrid Padding=\u002216\u0022\u003E\n \u003CGrid.Resources\u003E\n \u003Canimations:ImplicitAnimationSet x:Name=\u0022ShowTransitions\u0022\u003E\n \u003Canimations:OffsetAnimation EasingMode=\u0022EaseOut\u0022\n From=\u00220,24,0\u0022\n To=\u00220\u0022\n Duration=\u00220:0:0.4\u0022 /\u003E\n \u003Canimations:OpacityAnimation EasingMode=\u0022EaseOut\u0022\n From=\u00220\u0022\n To=\u00221\u0022\n Duration=\u00220:0:0.2\u0022 /\u003E\n \u003C/animations:ImplicitAnimationSet\u003E\n \u003Canimations:ImplicitAnimationSet x:Name=\u0022HideTransitions\u0022\u003E\n \u003Canimations:OffsetAnimation EasingMode=\u0022EaseOut\u0022\n From=\u00220\u0022\n To=\u00220,24,0\u0022\n Duration=\u00220:0:0.2\u0022 /\u003E\n \u003Canimations:OpacityAnimation EasingMode=\u0022EaseOut\u0022\n From=\u00221\u0022\n To=\u00220\u0022\n Duration=\u00220:0:0.1\u0022 /\u003E\n \u003C/animations:ImplicitAnimationSet\u003E\n \u003C/Grid.Resources\u003E\n \u003Ccontrols:SwitchPresenter HorizontalAlignment=\u0022Center\u0022\n TargetType=\u0022x:Boolean\u0022\n Value=\u0022True\u0022\u003E\n \u003Ccontrols:Case Value=\u0022True\u0022\u003E\n \u003CStackPanel HorizontalAlignment=\u0022Center\u0022\n animations:Implicit.HideAnimations=\u0022{StaticResource HideTransitions}\u0022\n animations:Implicit.ShowAnimations=\u0022{StaticResource ShowTransitions}\u0022\n Orientation=\u0022Vertical\u0022\n Spacing=\u00228\u0022\u003E\n \u003Cmuxc:ProgressRing IsActive=\u0022True\u0022 /\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022Fetching data..\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022False\u0022\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n animations:Implicit.HideAnimations=\u0022{StaticResource HideTransitions}\u0022\n animations:Implicit.ShowAnimations=\u0022{StaticResource ShowTransitions}\u0022\n TextAlignment=\u0022Center\u0022\n TextWrapping=\u0022WrapWholeWords\u0022\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022Content has loaded\u0022 /\u003E\n \u003CLineBreak /\u003E\n \u003CRun Text=\u0022Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/controls:Case\u003E\n \u003C/controls:SwitchPresenter\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:animations=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SwitchPresenterLoaderSample",
+ "sourcePath": "components/Primitives/samples/SwitchPresenter/SwitchPresenterLoaderSample.xaml",
+ "optionsResolved": [
+ "LoadingState=True"
+ ]
+ }
+ },
+ {
+ "header": "SwitchPresenter Template",
+ "details": "We can also invert the paradigm a bit with a SwitchPresenter to do data transformations within XAML using a ContentTemplate. Imagine an alternate view of our first starting example:",
+ "xaml": "\u003CStackPanel\u003E\n \u003CComboBox x:Name=\u0022Lookup\u0022\n Margin=\u00220,0,0,8\u0022\n Header=\u0022Look up reservation\u0022\n SelectedIndex=\u00220\u0022\u003E\n \u003Cx:String\u003EConfirmation Code\u003C/x:String\u003E\n \u003Cx:String\u003EE-ticket number\u003C/x:String\u003E\n \u003Cx:String\u003EMileage Plan number\u003C/x:String\u003E\n \u003C/ComboBox\u003E\n \u003C!-- SwitchPresenter binds to a value --\u003E\n \u003Ccontrols:SwitchPresenter Value=\u0022{x:Bind Lookup.SelectedItem, Mode=OneWay}\u0022\u003E\n \u003C!-- We define a common UI template for the data we want to display --\u003E\n \u003Ccontrols:SwitchPresenter.ContentTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:TemplateInformation\u0022\u003E\n \u003CStackPanel\u003E\n \u003CTextBox Name=\u0022CodeValidator\u0022\n ui:TextBoxExtensions.Regex=\u0022{x:Bind Regex, Mode=OneWay}\u0022\n Header=\u0022{x:Bind Header, Mode=OneWay}\u0022\n PlaceholderText=\u0022{x:Bind PlaceholderText, Mode=OneWay}\u0022 /\u003E\n \u003CTextBlock Text=\u0022Thanks for entering a valid code!\u0022\n Visibility=\u0022{Binding (ui:TextBoxExtensions.IsValid), ElementName=CodeValidator}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:SwitchPresenter.ContentTemplate\u003E\n \u003C!-- And use the value to transform our data into an object we\u0027ll use as the context for our UI --\u003E\n \u003Ccontrols:Case IsDefault=\u0022True\u0022\n Value=\u0022Confirmation Code\u0022\u003E\n \u003Clocal:TemplateInformation Header=\u0022Confirmation code\u0022\n PlaceholderText=\u00226 letters\u0022\n Regex=\u0022^[a-zA-Z]{6}$\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022E-ticket number\u0022\u003E\n \u003Clocal:TemplateInformation Header=\u0022E-ticket number\u0022\n PlaceholderText=\u002210 or 13 numbers\u0022\n Regex=\u0022(^\\d{10}$)|(^\\d{13}$)\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022Mileage Plan number\u0022\u003E\n \u003Clocal:TemplateInformation Header=\u0022Mileage Plan #\u0022\n PlaceholderText=\u0022Mileage Plan (12 digits)\u0022\n Regex=\u0022(^\\d{12}$)\u0022 /\u003E\n \u003C/controls:Case\u003E\n \u003C/controls:SwitchPresenter\u003E\n\u003C/StackPanel\u003E",
+ "code": "public partial class TemplateInformation\n{\n public string? Header { get; set; }\n public string? Regex { get; set; }\n public string? PlaceholderText { get; set; }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:local=\u0022using:PrimitivesExperiment.Samples.SwitchPresenter\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SwitchPresenterTemplateSample",
+ "sourcePath": "components/Primitives/samples/SwitchPresenter/SwitchPresenterTemplateSample.xaml"
+ }
+ },
+ {
+ "header": "SwitchConverter Brush",
+ "details": "A new analog to SwitchPresenter is the SwitchConverter which can be used in bindings to translate values into resources:",
+ "xaml": "\u003CStackPanel Spacing=\u00228\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003C!--\n If you reference an enum directly in UWP, you need to use it somewhere for the XamlTypeInfo reference to be generated...\n --\u003E\n \u003Clocal:CheckStatus x:Key=\u0022MyChecks\u0022\u003EWarning\u003C/local:CheckStatus\u003E\n\n \u003C!-- SwitchConverter lets you easily convert general values to resources --\u003E\n \u003C!-- Note: This is in the converters namespace --\u003E\n \u003Cconverters:SwitchConverter x:Key=\u0022StatusToColorSwitchConverter\u0022\n TargetType=\u0022local:CheckStatus\u0022\u003E\n \u003C!-- Note: These are reused from the controls namespace from SwitchPresenter --\u003E\n \u003Ccontrols:Case Content=\u0022{ThemeResource SystemFillColorSuccessBrush}\u0022\n Value=\u0022Success\u0022 /\u003E\n \u003Ccontrols:Case Content=\u0022{ThemeResource SystemFillColorCautionBrush}\u0022\n Value=\u0022Warning\u0022 /\u003E\n \u003Ccontrols:Case Content=\u0022{ThemeResource SystemFillColorCriticalBrush}\u0022\n Value=\u0022Error\u0022 /\u003E\n \u003C/converters:SwitchConverter\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CComboBox x:Name=\u0022StatusPicker\u0022\n Header=\u0022Pick a status\u0022\n SelectedIndex=\u00220\u0022\u003E\n \u003Cx:String\u003ESuccess\u003C/x:String\u003E\n \u003Cx:String\u003EWarning\u003C/x:String\u003E\n \u003Cx:String\u003EError\u003C/x:String\u003E\n \u003C/ComboBox\u003E\n \u003CTextBlock Text=\u0022This is it, this is the demo:\u0022 /\u003E\n \u003CTextBlock FontWeight=\u0022SemiBold\u0022\n Foreground=\u0022{x:Bind StatusPicker.SelectedItem, Converter={StaticResource StatusToColorSwitchConverter}, Mode=OneWay}\u0022\n Text=\u0022{x:Bind StatusPicker.SelectedItem, Mode=OneWay}\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "code": "public enum CheckStatus\n{\n Error,\n Warning,\n Success,\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:converters=\u0022using:CommunityToolkit.WinUI.Converters\u0022",
+ "xmlns:local=\u0022using:PrimitivesExperiment.Samples.SwitchPresenter\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SwitchConverterBrushSample",
+ "sourcePath": "components/Primitives/samples/SwitchPresenter/SwitchConverterBrushSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "uniformgrid",
+ "name": "UniformGrid",
+ "description": "Presents items in a evenly-spaced set of rows or columns to fill the total available display space. It reacts to changes in the layout as well as the content so it can adapt to different form factors automatically.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Primitives",
+ "curatedKeywords": [
+ "UniformGrid",
+ "grid",
+ "layout",
+ "responsive"
+ ],
+ "keywords": [
+ "Layouts",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "UniformGrid",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Primitives/samples/UniformGrid.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Primitives",
+ "documentPath": "components/Primitives/samples/UniformGrid.md",
+ "category": "Layouts",
+ "subcategory": "Miscellaneous",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "UniformGrid",
+ "details": "Each cell in the grid, by default, will be the same size. If no value for Rows and Columns are provided, the UniformGrid will create a square layout based on the total number of visible items. If a fixed size is provided for Rows and Columns then additional children that can\u0027t fit in the number of cells provided won\u0027t be displayed. In addition, UniformGrid is a Panel instead of an ItemsControl. As such, it could be used as a Panel in such ItemsControls. The UniformGrid inherits from Grid and provides many additional features compared to its predecessor, see more below.",
+ "xaml": "\u003CGrid\u003E\n \u003Ccontrols:UniformGrid Columns=\u00220\u0022\n FirstColumn=\u00221\u0022\n Rows=\u00220\u0022\u003E\n\n \u003CBorder Grid.Row=\u00221\u0022\n Grid.RowSpan=\u00222\u0022\n Grid.Column=\u00221\u0022\n Grid.ColumnSpan=\u00222\u0022\n Background=\u0022AliceBlue\u0022\u003E\n \u003CTextBlock Text=\u00221\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022Cornsilk\u0022\u003E\n \u003CTextBlock Text=\u00222\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022DarkSalmon\u0022\u003E\n \u003CTextBlock Text=\u00223\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022Gainsboro\u0022\u003E\n \u003CTextBlock Text=\u00224\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022LightBlue\u0022\u003E\n \u003CTextBlock Text=\u00225\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022MediumAquamarine\u0022\u003E\n \u003CTextBlock Text=\u00226\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022MistyRose\u0022\u003E\n \u003CTextBlock Text=\u00227\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022LightCyan\u0022\u003E\n \u003CTextBlock Text=\u00228\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022Salmon\u0022\u003E\n \u003CTextBlock Text=\u00229\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022Goldenrod\u0022\u003E\n \u003CTextBlock Text=\u002210\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Background=\u0022Pink\u0022\u003E\n \u003CTextBlock Text=\u002211\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/controls:UniformGrid\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "UniformGridSample",
+ "sourcePath": "components/Primitives/samples/UniformGridSample.xaml",
+ "optionsResolved": [
+ "Columns=0",
+ "FirstColumn=1",
+ "Item1ColumnSpan=2",
+ "Item1RowSpan=2",
+ "Rows=0"
+ ],
+ "optionBindingsDropped": [
+ "Orientation"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "wraplayout",
+ "name": "WrapLayout",
+ "description": "The WrapLayout virtualizes child elements in sequential position from left to right, breaking content to the next line at the edge of the containing box.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Primitives",
+ "usings": [
+ "Windows.UI"
+ ],
+ "curatedKeywords": [
+ "WrapLayout"
+ ],
+ "keywords": [
+ "Layouts",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "WrapLayout",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Primitives/samples/WrapLayout.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Primitives",
+ "documentPath": "components/Primitives/samples/WrapLayout.md",
+ "category": "Layouts",
+ "subcategory": "Layout",
+ "author": "skendrot"
+ },
+ "samples": [
+ {
+ "header": "WrapLayout",
+ "details": "Subsequent ordering happens sequentially from top to bottom or from right to left, depending on the value of the Orientation property. The WrapLayout positions child controls based on orientation, horizontal orientation (default) positions controls from left to right and vertical orientation positions controls from top to bottom, and once the max-width or height is reached the control automatically creates rows or columns based on the orientation. Spacing can be automatically added between items using the HorizontalSpacing and VerticalSpacing properties. When the Orientation is Horizontal, HorizontalSpacing adds uniform horizontal spacing between each individual item, and VerticalSpacing adds uniform spacing between each row of items. When the Orientation is Vertical, HorizontalSpacing adds uniform spacing between each column of items, and VerticalSpacing adds uniform vertical spacing between individual items.",
+ "xaml": "\u003CScrollViewer\u003E\n \u003CScrollViewer.Resources\u003E\n \u003CDataTemplate x:Key=\u0022WrapTemplate\u0022\n x:DataType=\u0022local:ColorItem\u0022\u003E\n \u003CBorder Width=\u0022{x:Bind Width}\u0022\n Height=\u002248\u0022\n CornerRadius=\u0022{StaticResource ControlCornerRadius}\u0022\u003E\n \u003CBorder.Background\u003E\n \u003CSolidColorBrush Color=\u0022{x:Bind Color}\u0022 /\u003E\n \u003C/Border.Background\u003E\n \u003CTextBlock Margin=\u00226,4,4,4\u0022\n FontSize=\u002216\u0022\n Text=\u0022{x:Bind Index}\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/DataTemplate\u003E\n \u003C/ScrollViewer.Resources\u003E\n \u003Cmuxc:ItemsRepeater x:Name=\u0022WrapRepeater\u0022\n ItemTemplate=\u0022{StaticResource WrapTemplate}\u0022\n ItemsSource=\u0022{x:Bind ColorsCollection, Mode=OneWay}\u0022\u003E\n \u003Cmuxc:ItemsRepeater.Layout\u003E\n \u003Ccontrols:WrapLayout x:Name=\u0022Wrap\u0022\n HorizontalSpacing=\u00225\u0022\n VerticalSpacing=\u00225\u0022 /\u003E\n \u003C/muxc:ItemsRepeater.Layout\u003E\n \u003C/muxc:ItemsRepeater\u003E\n\u003C/ScrollViewer\u003E",
+ "code": "public ObservableCollection\u003CColorItem\u003E ColorsCollection = new();\n\npublic Random random;\n\npublic WrapLayoutSample()\n{\n random = new Random(DateTime.Now.Millisecond);\n for (int i = 0; i \u003C random.Next(1000, 5000); i\u002B\u002B)\n {\n var item = new ColorItem\n {\n Index = i,\n Width = random.Next(50, 250),\n Height = random.Next(50, 250),\n Color = Color.FromArgb(255, (byte)random.Next(0, 255), (byte)random.Next(0, 255), (byte)random.Next(0, 255))\n };\n ColorsCollection.Add(item);\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "WrapLayoutSample",
+ "sourcePath": "components/Primitives/samples/WrapLayoutSample.xaml",
+ "optionsResolved": [
+ "HorizontalSpacing=5",
+ "VerticalSpacing=5"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "wrappanel",
+ "name": "WrapPanel",
+ "description": "The WrapPanel Control positions child elements in sequential position from left to right, breaking content to the next line at the edge of the containing box.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Primitives",
+ "usings": [
+ "CommunityToolkit.WinUI",
+ "CommunityToolkit.WinUI.Controls"
+ ],
+ "curatedKeywords": [
+ "WrapPanel"
+ ],
+ "keywords": [
+ "Layouts",
+ "Panel"
+ ],
+ "docs": [
+ {
+ "title": "WrapPanel",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Primitives/samples/WrapPanel.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Primitives",
+ "documentPath": "components/Primitives/samples/WrapPanel.md",
+ "category": "Layouts",
+ "subcategory": "Panel",
+ "author": "nmetulev"
+ },
+ "samples": [
+ {
+ "header": "WrapPanel",
+ "details": "Subsequent ordering happens sequentially from top to bottom or from right to left, depending on the value of the Orientation property. The WrapPanel position child controls based on orientation, horizontal orientation (default) positions controls from left to right and vertical orientation positions controls from top to bottom, and once the max width or height is reached the control automatically create row or column based on the orientation. Spacing can be automatically added between items using the HorizontalSpacing and VerticalSpacing properties. When the Orientation is Horizontal, HorizontalSpacing adds uniform horizontal spacing between each individual item, and VerticalSpacing adds uniform spacing between each row of items. When the Orientation is Vertical, HorizontalSpacing adds uniform spacing between each column of items, and VerticalSpacing adds uniform vertical spacing between individual items.",
+ "xaml": "\u003CGrid\u003E\n \u003CGrid.Resources\u003E\n \u003CDataTemplate x:Key=\u0022PhotoTemplate\u0022\u003E\n \u003CGrid Width=\u0022{Binding Width}\u0022\n Height=\u0022{Binding Height}\u0022\n Margin=\u00220\u0022\u003E\n \u003CImage HorizontalAlignment=\u0022Center\u0022\n Stretch=\u0022UniformToFill\u0022\u003E\n \u003CImage.Source\u003E\n \u003CBitmapImage DecodePixelHeight=\u0022200\u0022\n UriSource=\u0022{Binding Thumbnail}\u0022 /\u003E\n \u003C/Image.Source\u003E\n \u003C/Image\u003E\n \u003CBorder VerticalAlignment=\u0022Bottom\u0022\u003E\n \u003CBorder.Background\u003E\n \u003CLinearGradientBrush EndPoint=\u00220,1\u0022\u003E\n \u003CGradientStop Offset=\u00220\u0022 Color=\u0022Transparent\u0022 /\u003E\n \u003CGradientStop Offset=\u00221\u0022 Color=\u0022#33000000\u0022 /\u003E\n \u003C/LinearGradientBrush\u003E\n \u003C/Border.Background\u003E\n \u003CTextBlock Margin=\u00225,20,5,5\u0022\n VerticalAlignment=\u0022Bottom\u0022\n Foreground=\u0022White\u0022\n Text=\u0022{Binding Category}\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n \u003C/DataTemplate\u003E\n \u003CStyle TargetType=\u0022ListViewItem\u0022\u003E\n \u003C!-- Change those values to change the WrapPanel\u0027s children alignment --\u003E\n \u003CSetter Property=\u0022VerticalContentAlignment\u0022 Value=\u0022Center\u0022 /\u003E\n \u003CSetter Property=\u0022HorizontalContentAlignment\u0022 Value=\u0022Center\u0022 /\u003E\n \u003CSetter Property=\u0022Margin\u0022 Value=\u00220\u0022 /\u003E\n \u003CSetter Property=\u0022Padding\u0022 Value=\u00220\u0022 /\u003E\n \u003CSetter Property=\u0022MinWidth\u0022 Value=\u00220\u0022 /\u003E\n \u003CSetter Property=\u0022MinHeight\u0022 Value=\u00220\u0022 /\u003E\n \u003C/Style\u003E\n \u003C/Grid.Resources\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022*\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\n Spacing=\u00224\u0022\u003E\n \u003CButton Click=\u0022AddButton_Click\u0022\n Content=\u0022Add random sized image\u0022 /\u003E\n \u003CButton Click=\u0022AddFixedBtn_Click\u0022\n Content=\u0022Add fixed sized image\u0022 /\u003E\n \u003CButton Click=\u0022SwitchBtn_Click\u0022\n Content=\u0022Switch Orientation\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003CListView Name=\u0022WrapPanelContainer\u0022\n Grid.Row=\u00221\u0022\n IsItemClickEnabled=\u0022True\u0022\n ItemClick=\u0022ItemControl_ItemClick\u0022\n ItemTemplate=\u0022{StaticResource PhotoTemplate}\u0022\n ItemsSource=\u0022{x:Bind WrapPanelCollection, Mode=OneWay}\u0022\u003E\n \u003CItemsControl.ItemsPanel\u003E\n \u003CItemsPanelTemplate\u003E\n \u003Ccontrols:WrapPanel x:Name=\u0022sampleWrapPanel\u0022\n Padding=\u002212\u0022\n HorizontalSpacing=\u00225\u0022\n VerticalSpacing=\u00225\u0022 /\u003E\n \u003C/ItemsPanelTemplate\u003E\n \u003C/ItemsControl.ItemsPanel\u003E\n \u003C/ListView\u003E\n\u003C/Grid\u003E",
+ "code": "private static readonly Random Rand = new Random();\n\nprivate ObservableCollection\u003CPhotoDataItemWithDimension\u003E WrapPanelCollection = new();\n\nprivate void ItemControl_ItemClick(object sender, ItemClickEventArgs e)\n{\n var item = e.ClickedItem as PhotoDataItemWithDimension;\n if (item == null)\n {\n return;\n }\n\n WrapPanelCollection.Remove(item);\n}\n\nprivate void AddButton_Click(object sender, RoutedEventArgs e)\n{\n WrapPanelCollection.Add(new PhotoDataItemWithDimension { Category = \u0022Remove\u0022, Thumbnail = \u0022ms-appx:///Assets/BigFourSummerHeat.jpg\u0022, Width = Rand.Next(60, 180), Height = Rand.Next(40, 140) });\n}\n\nprivate void AddFixedBtn_Click(object sender, RoutedEventArgs e)\n{\n WrapPanelCollection.Add(new PhotoDataItemWithDimension { Category = \u0022Remove\u0022, Thumbnail = \u0022ms-appx:///Assets/BigFourSummerHeat.jpg\u0022, Width = 150, Height = 100 });\n}\n\nprivate void SwitchBtn_Click(object sender, RoutedEventArgs e)\n{\n if (WrapPanelContainer.FindDescendant\u003CWrapPanel\u003E()is WrapPanel sampleWrapPanel)\n {\n if (sampleWrapPanel.Orientation == Orientation.Horizontal)\n {\n sampleWrapPanel.Orientation = Orientation.Vertical;\n ScrollViewer.SetVerticalScrollMode(WrapPanelContainer, ScrollMode.Disabled);\n ScrollViewer.SetVerticalScrollBarVisibility(WrapPanelContainer, ScrollBarVisibility.Disabled);\n ScrollViewer.SetHorizontalScrollMode(WrapPanelContainer, ScrollMode.Auto);\n ScrollViewer.SetHorizontalScrollBarVisibility(WrapPanelContainer, ScrollBarVisibility.Auto);\n }\n else\n {\n sampleWrapPanel.Orientation = Orientation.Horizontal;\n ScrollViewer.SetVerticalScrollMode(WrapPanelContainer, ScrollMode.Auto);\n ScrollViewer.SetVerticalScrollBarVisibility(WrapPanelContainer, ScrollBarVisibility.Auto);\n ScrollViewer.SetHorizontalScrollMode(WrapPanelContainer, ScrollMode.Disabled);\n ScrollViewer.SetHorizontalScrollBarVisibility(WrapPanelContainer, ScrollBarVisibility.Disabled);\n }\n }\n}\n\npublic class PhotoDataItemWithDimension : PhotoDataItem\n{\n public double Width { get; set; }\n public double Height { get; set; }\n}\n\npublic class PhotoDataItem\n{\n public string? Title { get; set; }\n public string? Category { get; set; }\n public string? Thumbnail { get; set; }\n\n public override string ToString()\n {\n return Title!;\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "WrapPanelSample",
+ "sourcePath": "components/Primitives/samples/WrapPanelSample.xaml",
+ "optionsResolved": [
+ "HorizontalSpacing=5",
+ "VerticalSpacing=5"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "radialgauge",
+ "name": "RadialGauge",
+ "description": "The Radial Gauge Control displays a value in a certain range using a needle on a circular face.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.RadialGauge",
+ "curatedKeywords": [
+ "RadialGauge",
+ "Control",
+ "Input"
+ ],
+ "keywords": [
+ "Controls",
+ "Input"
+ ],
+ "docs": [
+ {
+ "title": "RadialGauge",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/RadialGauge/samples/RadialGauge.md"
+ }
+ ],
+ "toolkit": {
+ "component": "RadialGauge",
+ "documentPath": "components/RadialGauge/samples/RadialGauge.md",
+ "category": "Controls",
+ "subcategory": "Input",
+ "author": "xamlbrewer"
+ },
+ "samples": [
+ {
+ "header": "RadialGauge",
+ "details": "This control will make data visualizations and dashboards more engaging with rich style and interactivity. The round gauges are powerful, easy to use, and highly configurable to present dashboards capable of displaying clocks, industrial panels, automotive dashboards, and even aircraft cockpits. The Radial Gauge supports animated transitions between configuration states. The control gradually animates as it redraws changes to the needle, needle position, scale range, color range, and more.",
+ "xaml": "\u003CStackPanel HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Orientation=\u0022Horizontal\u0022\u003E\n \u003Ccontrols:RadialGauge x:Name=\u0022RadialGauge\u0022\n Width=\u0022280\u0022\n IsEnabled=\u0022True\u0022\n IsInteractive=\u0022True\u0022\n MaxAngle=\u0022150\u0022\n Maximum=\u0022240\u0022\n MinAngle=\u0022-150\u0022\n Minimum=\u00220\u0022\n NeedleLength=\u002260\u0022\n NeedleWidth=\u00224\u0022\n ScalePadding=\u00220\u0022\n ScaleTickWidth=\u00220\u0022\n ScaleWidth=\u002212\u0022\n StepSize=\u002230\u0022\n TickLength=\u00226\u0022\n TickPadding=\u002224\u0022\n TickSpacing=\u002215\u0022\n TickWidth=\u00222\u0022\n ValueStringFormat=\u0022N0\u0022\n Value=\u0022120\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "RadialGaugeSample",
+ "sourcePath": "components/RadialGauge/samples/RadialGaugeSample.xaml",
+ "optionsResolved": [
+ "Enabled=True",
+ "IsInteractive=True",
+ "MaxAngle=150",
+ "MinAngle=-150",
+ "NeedleLength=60",
+ "NeedleWidth=4",
+ "ScalePadding=0",
+ "ScaleTickWidth=0",
+ "ScaleWidth=12",
+ "StepSize=30",
+ "TickLength=6",
+ "TickPadding=24",
+ "TickSpacing=15",
+ "TickWidth=2",
+ "Value=120"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "rangeselector",
+ "name": "RangeSelector",
+ "description": "The RangeSelector Control is a Double Slider control that allows the user to select a sub-range of values from a larger range of possible values. The user can slide from the left or right of the range.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.RangeSelector",
+ "curatedKeywords": [
+ "RangeSelector",
+ "Control",
+ "double slider",
+ "slider rangeslider"
+ ],
+ "keywords": [
+ "Controls",
+ "Input"
+ ],
+ "docs": [
+ {
+ "title": "RangeSelector",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/RangeSelector/samples/RangeSelector.md"
+ }
+ ],
+ "toolkit": {
+ "component": "RangeSelector",
+ "documentPath": "components/RangeSelector/samples/RangeSelector.md",
+ "category": "Controls",
+ "subcategory": "Input",
+ "author": "nmetulev"
+ },
+ "samples": [
+ {
+ "header": "RangeSelector",
+ "details": "A RangeSelector is pretty similar to a regular Slider, and shares some of its properties such as Minimum, Maximum and StepFrequency.",
+ "xaml": "\u003CGrid MinWidth=\u0022320\u0022\n MinHeight=\u002286\u0022\n MaxWidth=\u0022560\u0022\n HorizontalAlignment=\u0022Stretch\u0022\u003E\n \u003C!-- Use \u0027VerticalAlignment=\u0022Stretch\u0022\u0027 When \u0027Orientation=\u0022Vertical\u0022\u0027 --\u003E\n \u003Ccontrols:RangeSelector x:Name=\u0022rangeSelector\u0022\n VerticalAlignment=\u0022Center\u0022\n IsEnabled=\u0022True\u0022\n Maximum=\u0022100\u0022\n Minimum=\u00220\u0022\n Orientation=\u0022Horizontal\u0022\n RangeEnd=\u0022100\u0022\n RangeStart=\u00220\u0022\n StepFrequency=\u00221\u0022 /\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "RangeSelectorSample",
+ "sourcePath": "components/RangeSelector/samples/RangeSelectorSample.xaml",
+ "optionsResolved": [
+ "Enable=True",
+ "Maximum=100",
+ "Minimum=0",
+ "OrientationMode=Horizontal",
+ "StepFrequency=1"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "richsuggestbox",
+ "name": "RichSuggestBox",
+ "description": "A rich text input control that auto-suggests and stores token items in a document.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.RichSuggestBox",
+ "usings": [
+ "CommunityToolkit.WinUI.Controls",
+ "Microsoft.UI",
+ "Microsoft.UI.Dispatching",
+ "Microsoft.UI.Text"
+ ],
+ "curatedKeywords": [
+ "RichSuggestBox",
+ "Control"
+ ],
+ "keywords": [
+ "Controls",
+ "Text"
+ ],
+ "docs": [
+ {
+ "title": "RichSuggestBox",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/RichSuggestBox/samples/RichSuggestBox.md"
+ }
+ ],
+ "toolkit": {
+ "component": "RichSuggestBox",
+ "documentPath": "components/RichSuggestBox/samples/RichSuggestBox.md",
+ "category": "Controls",
+ "subcategory": "Text",
+ "author": "huynhsontung"
+ },
+ "samples": [
+ {
+ "header": "RichSuggestBox with multiple prefixes",
+ "details": "The RichSuggestBox is a combination of AutoSuggestBox and RichEditBox that can provide suggestions based on customizable prefixes. Selected suggestions are embedded and tracked in the document as tokens. RichSuggestBox resembles text controls commonly found in social applications where you type \u0022@\u0022 to mention people.",
+ "xaml": "\u003CStackPanel MinWidth=\u0022400\u0022\n HorizontalAlignment=\u0022Center\u0022\n Spacing=\u002224\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003CDataTemplate x:Key=\u0022EmailTemplate\u0022\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\u003E\n \u003CBorder Width=\u002220\u0022\n Height=\u002220\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\n CornerRadius=\u00229999\u0022\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n FontSize=\u002210\u0022\n FontWeight=\u0022Semibold\u0022\n Foreground=\u0022White\u0022\n Text=\u0022{Binding Initials}\u0022 /\u003E\n \u003C/Border\u003E\n \u003CTextBlock Padding=\u00228,0,0,0\u0022\n Text=\u0022{Binding DisplayName}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n\n \u003CDataTemplate x:Key=\u0022DataTemplate\u0022\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\u003E\n \u003CSymbolIcon Symbol=\u0022{Binding Icon}\u0022 /\u003E\n \u003CTextBlock Margin=\u00228,0,0,0\u0022\n Text=\u0022{Binding Text}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n\n \u003CDataTemplate x:Key=\u0022TokenTemplate\u0022\u003E\n \u003CStackPanel Margin=\u00220,4,0,12\u0022\n Orientation=\u0022Vertical\u0022\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Text:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding DisplayText}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Position:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding Position}\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Id:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding Id}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n\n \u003Clocal:SuggestionTemplateSelector x:Key=\u0022SuggestionTemplateSelector\u0022\n Data=\u0022{StaticResource DataTemplate}\u0022\n Person=\u0022{StaticResource EmailTemplate}\u0022 /\u003E\n\n \u003C/StackPanel.Resources\u003E\n \u003Ccontrols:RichSuggestBox x:Name=\u0022SuggestingBox\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n Header=\u0022RichSuggestBox that supports multiple prefixes (@ and #)\u0022\n ItemTemplateSelector=\u0022{StaticResource SuggestionTemplateSelector}\u0022\n Prefixes=\u0022@#\u0022\n SuggestionChosen=\u0022SuggestingBox_SuggestionChosen\u0022\n SuggestionRequested=\u0022SuggestingBox_SuggestionRequested\u0022 /\u003E\n \u003CListView x:Name=\u0022TokenListView\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n ItemTemplate=\u0022{StaticResource TokenTemplate}\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "code": "private readonly List\u003CSampleEmailDataType\u003E _emailSamples = new List\u003CSampleEmailDataType\u003E()\n{\n new SampleEmailDataType()\n {\n FirstName = \u0022Marcus\u0022,\n FamilyName = \u0022Perryman\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Michael\u0022,\n FamilyName = \u0022Hawker\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Matt\u0022,\n FamilyName = \u0022Lacey\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Alexandre\u0022,\n FamilyName = \u0022Chohfi\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Filip\u0022,\n FamilyName = \u0022Wallberg\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Shane\u0022,\n FamilyName = \u0022Weaver\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Vincent\u0022,\n FamilyName = \u0022Gromfeld\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Sergio\u0022,\n FamilyName = \u0022Pedri\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Alex\u0022,\n FamilyName = \u0022Wilber\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Allan\u0022,\n FamilyName = \u0022Deyoung\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Adele\u0022,\n FamilyName = \u0022Vance\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Grady\u0022,\n FamilyName = \u0022Archie\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Megan\u0022,\n FamilyName = \u0022Bowen\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Ben\u0022,\n FamilyName = \u0022Walters\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Debra\u0022,\n FamilyName = \u0022Berger\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Emily\u0022,\n FamilyName = \u0022Braun\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Christine\u0022,\n FamilyName = \u0022Cline\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Enrico\u0022,\n FamilyName = \u0022Catteneo\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Davit\u0022,\n FamilyName = \u0022Badalyan\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Diego\u0022,\n FamilyName = \u0022Siciliani\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Raul\u0022,\n FamilyName = \u0022Razo\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Miriam\u0022,\n FamilyName = \u0022Graham\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Lynne\u0022,\n FamilyName = \u0022Robbins\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Lydia\u0022,\n FamilyName = \u0022Holloway\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Nestor\u0022,\n FamilyName = \u0022Wilke\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Patti\u0022,\n FamilyName = \u0022Fernandez\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Pradeep\u0022,\n FamilyName = \u0022Gupta\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Joni\u0022,\n FamilyName = \u0022Sherman\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Isaiah\u0022,\n FamilyName = \u0022Langer\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Irvin\u0022,\n FamilyName = \u0022Sayers\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Tung\u0022,\n FamilyName = \u0022Huynh\u0022\n },\n};\n\nprivate readonly List\u003CSampleDataType\u003E _samples = new List\u003CSampleDataType\u003E()\n{\n new SampleDataType()\n {\n Text = \u0022Account\u0022,\n Icon = Symbol.Account\n },\n new SampleDataType()\n {\n Text = \u0022Add Friend\u0022,\n Icon = Symbol.AddFriend\n },\n new SampleDataType()\n {\n Text = \u0022Attach\u0022,\n Icon = Symbol.Attach\n },\n new SampleDataType()\n {\n Text = \u0022Attach Camera\u0022,\n Icon = Symbol.AttachCamera\n },\n new SampleDataType()\n {\n Text = \u0022Audio\u0022,\n Icon = Symbol.Audio\n },\n new SampleDataType()\n {\n Text = \u0022Block Contact\u0022,\n Icon = Symbol.BlockContact\n },\n new SampleDataType()\n {\n Text = \u0022Calculator\u0022,\n Icon = Symbol.Calculator\n },\n new SampleDataType()\n {\n Text = \u0022Calendar\u0022,\n Icon = Symbol.Calendar\n },\n new SampleDataType()\n {\n Text = \u0022Camera\u0022,\n Icon = Symbol.Camera\n },\n new SampleDataType()\n {\n Text = \u0022Contact\u0022,\n Icon = Symbol.Contact\n },\n new SampleDataType()\n {\n Text = \u0022Favorite\u0022,\n Icon = Symbol.Favorite\n },\n new SampleDataType()\n {\n Text = \u0022Link\u0022,\n Icon = Symbol.Link\n },\n new SampleDataType()\n {\n Text = \u0022Mail\u0022,\n Icon = Symbol.Mail\n },\n new SampleDataType()\n {\n Text = \u0022Map\u0022,\n Icon = Symbol.Map\n },\n new SampleDataType()\n {\n Text = \u0022Phone\u0022,\n Icon = Symbol.Phone\n },\n new SampleDataType()\n {\n Text = \u0022Pin\u0022,\n Icon = Symbol.Pin\n },\n new SampleDataType()\n {\n Text = \u0022Rotate\u0022,\n Icon = Symbol.Rotate\n },\n new SampleDataType()\n {\n Text = \u0022Rotate Camera\u0022,\n Icon = Symbol.RotateCamera\n },\n new SampleDataType()\n {\n Text = \u0022Send\u0022,\n Icon = Symbol.Send\n },\n new SampleDataType()\n {\n Text = \u0022Tags\u0022,\n Icon = Symbol.Tag\n },\n new SampleDataType()\n {\n Text = \u0022UnFavorite\u0022,\n Icon = Symbol.UnFavorite\n },\n new SampleDataType()\n {\n Text = \u0022UnPin\u0022,\n Icon = Symbol.UnPin\n },\n new SampleDataType()\n {\n Text = \u0022Zoom\u0022,\n Icon = Symbol.Zoom\n },\n new SampleDataType()\n {\n Text = \u0022ZoomIn\u0022,\n Icon = Symbol.ZoomIn\n },\n new SampleDataType()\n {\n Text = \u0022ZoomOut\u0022,\n Icon = Symbol.ZoomOut\n },\n};\n\npublic RichSuggestBoxMultiplePrefixesSample()\n{\n TokenListView.ItemsSource = SuggestingBox.Tokens;\n}\n\nprivate void SuggestingBox_SuggestionChosen(RichSuggestBox sender, SuggestionChosenEventArgs args)\n{\n if (args.Prefix == \u0022#\u0022)\n {\n args.Format!.BackgroundColor = Colors.LightSlateGray;\n args.Format.ForegroundColor = Colors.White;\n args.Format.Bold = FormatEffect.On;\n args.DisplayText = ((SampleDataType)args.SelectedItem!).Text;\n }\n else\n {\n args.DisplayText = ((SampleEmailDataType)args.SelectedItem!).DisplayName;\n }\n}\n\nprivate void SuggestingBox_SuggestionRequested(RichSuggestBox sender, SuggestionRequestedEventArgs args)\n{\n if (args.Prefix == \u0022#\u0022)\n {\n sender.ItemsSource = this._samples.Where(x =\u003E x.Text.Contains(args.QueryText!, StringComparison.OrdinalIgnoreCase));\n }\n else\n {\n sender.ItemsSource = this._emailSamples.Where(x =\u003E x.DisplayName.Contains(args.QueryText!, StringComparison.OrdinalIgnoreCase));\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:local=\u0022using:RichSuggestBoxExperiment.Samples\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "RichSuggestBoxMultiplePrefixesSample",
+ "sourcePath": "components/RichSuggestBox/samples/RichSuggestBoxMultiplePrefixesSample.xaml"
+ }
+ },
+ {
+ "header": "RichSuggestBox with plain text",
+ "details": "The example below creates a RichSuggestBox that only allows users to enter plain text. The only formatted texts in the document are tokens. \u0060\u0060xaml \u003Ccontrols:RichSuggestBox ClipboardCopyFormat=\u0022PlainText\u0022 ClipboardPasteFormat=\u0022PlainText\u0022 DisabledFormattingAccelerators=\u0022All\u0022 /\u003E \u0060\u0060",
+ "xaml": "\u003CStackPanel MinWidth=\u0022400\u0022\n HorizontalAlignment=\u0022Center\u0022\n Spacing=\u002224\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003CDataTemplate x:Key=\u0022EmailTemplate\u0022\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\u003E\n \u003CBorder Width=\u002220\u0022\n Height=\u002220\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\n CornerRadius=\u00229999\u0022\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n FontSize=\u002210\u0022\n FontWeight=\u0022Semibold\u0022\n Foreground=\u0022White\u0022\n Text=\u0022{Binding Initials}\u0022 /\u003E\n \u003C/Border\u003E\n \u003CTextBlock Padding=\u00228,0,0,0\u0022\n Text=\u0022{Binding DisplayName}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n\n \u003CDataTemplate x:Key=\u0022TokenTemplate\u0022\u003E\n \u003CStackPanel Margin=\u00220,4,0,12\u0022\n Orientation=\u0022Vertical\u0022\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Text:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding DisplayText}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Position:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding Position}\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Id:\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding Id}\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n\n \u003Clocal:SuggestionTemplateSelector x:Key=\u0022SuggestionTemplateSelector\u0022\n Data=\u0022{StaticResource DataTemplate}\u0022\n Person=\u0022{StaticResource EmailTemplate}\u0022 /\u003E\n\n \u003CFlyout x:Key=\u0022TokenSelectedFlyout\u0022\u003E\n \u003CContentPresenter x:Name=\u0022FlyoutPresenter\u0022\n ContentTemplate=\u0022{StaticResource EmailTemplate}\u0022 /\u003E\n \u003C/Flyout\u003E\n\n \u003C/StackPanel.Resources\u003E\n\n \u003Ccontrols:RichSuggestBox x:Name=\u0022SuggestingBox\u0022\n MaxHeight=\u0022400\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n ClipboardCopyFormat=\u0022PlainText\u0022\n ClipboardPasteFormat=\u0022PlainText\u0022\n DisabledFormattingAccelerators=\u0022All\u0022\n FlyoutBase.AttachedFlyout=\u0022{StaticResource TokenSelectedFlyout}\u0022\n Header=\u0022Plain text RichSuggestBox with on token pointer over flyout\u0022\n ItemTemplate=\u0022{StaticResource EmailTemplate}\u0022\n Prefixes=\u0022@\u0022\n SuggestionChosen=\u0022SuggestingBox_SuggestionChosen\u0022\n SuggestionRequested=\u0022SuggestingBox_SuggestionRequested\u0022\n TokenPointerOver=\u0022SuggestingBox_TokenPointerOver\u0022 /\u003E\n\n \u003CListView x:Name=\u0022TokenListView\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n ItemTemplate=\u0022{StaticResource TokenTemplate}\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "code": "private DispatcherQueue _dispatcherQueue;\n\nprivate readonly List\u003CSampleEmailDataType\u003E _emailSamples = new List\u003CSampleEmailDataType\u003E()\n{\n new SampleEmailDataType()\n {\n FirstName = \u0022Marcus\u0022,\n FamilyName = \u0022Perryman\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Michael\u0022,\n FamilyName = \u0022Hawker\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Matt\u0022,\n FamilyName = \u0022Lacey\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Alexandre\u0022,\n FamilyName = \u0022Chohfi\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Filip\u0022,\n FamilyName = \u0022Wallberg\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Shane\u0022,\n FamilyName = \u0022Weaver\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Vincent\u0022,\n FamilyName = \u0022Gromfeld\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Sergio\u0022,\n FamilyName = \u0022Pedri\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Alex\u0022,\n FamilyName = \u0022Wilber\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Allan\u0022,\n FamilyName = \u0022Deyoung\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Adele\u0022,\n FamilyName = \u0022Vance\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Grady\u0022,\n FamilyName = \u0022Archie\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Megan\u0022,\n FamilyName = \u0022Bowen\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Ben\u0022,\n FamilyName = \u0022Walters\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Debra\u0022,\n FamilyName = \u0022Berger\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Emily\u0022,\n FamilyName = \u0022Braun\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Christine\u0022,\n FamilyName = \u0022Cline\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Enrico\u0022,\n FamilyName = \u0022Catteneo\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Davit\u0022,\n FamilyName = \u0022Badalyan\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Diego\u0022,\n FamilyName = \u0022Siciliani\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Raul\u0022,\n FamilyName = \u0022Razo\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Miriam\u0022,\n FamilyName = \u0022Graham\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Lynne\u0022,\n FamilyName = \u0022Robbins\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Lydia\u0022,\n FamilyName = \u0022Holloway\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Nestor\u0022,\n FamilyName = \u0022Wilke\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Patti\u0022,\n FamilyName = \u0022Fernandez\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Pradeep\u0022,\n FamilyName = \u0022Gupta\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Joni\u0022,\n FamilyName = \u0022Sherman\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Isaiah\u0022,\n FamilyName = \u0022Langer\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Irvin\u0022,\n FamilyName = \u0022Sayers\u0022\n },\n new SampleEmailDataType()\n {\n FirstName = \u0022Tung\u0022,\n FamilyName = \u0022Huynh\u0022\n },\n};\n\npublic RichSuggestBoxPlainTextSample()\n{\n _dispatcherQueue = DispatcherQueue.GetForCurrentThread();\n TokenListView.ItemsSource = SuggestingBox.Tokens;\n}\n\nprivate void SuggestingBox_SuggestionChosen(RichSuggestBox sender, SuggestionChosenEventArgs args)\n{\n args.DisplayText = ((SampleEmailDataType)args.SelectedItem!).DisplayName;\n}\n\nprivate void SuggestingBox_SuggestionRequested(RichSuggestBox sender, SuggestionRequestedEventArgs args)\n{\n sender.ItemsSource = this._emailSamples.Where(x =\u003E x.DisplayName.Contains(args.QueryText!, StringComparison.OrdinalIgnoreCase));\n}\n\nprivate void SuggestingBox_TokenPointerOver(RichSuggestBox sender, RichSuggestTokenPointerOverEventArgs args)\n{\n var flyout = (Flyout)FlyoutBase.GetAttachedFlyout(sender);\n var pointerPosition = args.CurrentPoint!.Position;\n if (flyout?.Content is ContentPresenter cp \u0026\u0026 sender.TextDocument!.Selection.Type != SelectionType.Normal \u0026\u0026 (!flyout.IsOpen || cp.Content != args.Token!.Item))\n {\n this._dispatcherQueue.TryEnqueue(() =\u003E\n {\n cp.Content = args.Token!.Item;\n flyout.ShowAt(sender, new FlyoutShowOptions { Position = pointerPosition, ExclusionRect = sender.GetRectFromRange(args.Range!), ShowMode = FlyoutShowMode.TransientWithDismissOnPointerMoveAway, });\n });\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:local=\u0022using:RichSuggestBoxExperiment.Samples\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "RichSuggestBoxPlainTextSample",
+ "sourcePath": "components/RichSuggestBox/samples/RichSuggestBoxPlainText.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "segmented",
+ "name": "Segmented",
+ "description": "A common UI control to configure a view or setting.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Segmented",
+ "curatedKeywords": [
+ "SegmentedControl",
+ "Control",
+ "Layout",
+ "Segmented"
+ ],
+ "keywords": [
+ "Controls",
+ "Input"
+ ],
+ "docs": [
+ {
+ "title": "Segmented",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Segmented/samples/Segmented.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Segmented",
+ "documentPath": "components/Segmented/samples/Segmented.md",
+ "category": "Controls",
+ "subcategory": "Input",
+ "author": "niels9001"
+ },
+ "samples": [
+ {
+ "header": "Basics",
+ "details": "The Segmented control is best used with 2-5 items and does not support overflow. The Icon and Content property can be set on the SegmentedItems.",
+ "xaml": "\u003CStackPanel x:Name=\u0022Panel\u0022\n Spacing=\u00228\u0022\u003E\n \u003CTextBlock Style=\u0022{StaticResource BodyStrongTextBlockStyle}\u0022\n Text=\u0022Icon \u002B content\u0022 /\u003E\n \u003Ccontrols:Segmented\n SelectedIndex=\u00220\u0022\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Item 1\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xEA3A;}\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Item 2\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xEA3A;}\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Item 3 with a long label\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xEA3A;}\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Item 4\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xEA3A;}\u0022 /\u003E\n \u003C/controls:Segmented\u003E\n\n \u003CTextBlock Margin=\u00220,24,0,0\u0022\n Style=\u0022{StaticResource BodyStrongTextBlockStyle}\u0022\n Text=\u0022Icon only\u0022 /\u003E\n \u003Ccontrols:Segmented\n SelectedIndex=\u00222\u0022\u003E\n \u003Ccontrols:SegmentedItem Icon=\u0022{ui:FontIcon Glyph=\u0026#xE8BF;}\u0022\n ToolTipService.ToolTip=\u0022Day view\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Icon=\u0022{ui:FontIcon Glyph=\u0026#xE8C0;}\u0022\n ToolTipService.ToolTip=\u0022Week view\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Icon=\u0022{ui:FontIcon Glyph=\u0026#xE787;}\u0022\n ToolTipService.ToolTip=\u0022Month view\u0022 /\u003E\n \u003C/controls:Segmented\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SegmentedBasicSample",
+ "sourcePath": "components/Segmented/samples/SegmentedBasicSample.xaml",
+ "optionBindingsDropped": [
+ "HorizontalAlignment",
+ "Orientation",
+ "SelectionMode"
+ ]
+ }
+ },
+ {
+ "header": "Additional styles",
+ "details": "The Segmented control contains various additional styles, to match the look and feel of your application. The PivotSegmentedStyle matches a modern Pivot style while the ButtonSegmentedStyle represents buttons. To load these styles, make sure to add the ResourceDictionary as a resource (see Page.Resources sample below).",
+ "xaml": "\u003CStackPanel Width=\u0022480\u0022\n Spacing=\u00228\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003CResourceDictionary\u003E\n \u003CResourceDictionary.MergedDictionaries\u003E\n \u003CResourceDictionary Source=\u0022ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml\u0022 /\u003E\n \u003C/ResourceDictionary.MergedDictionaries\u003E\n \u003C/ResourceDictionary\u003E\n \u003C/StackPanel.Resources\u003E\n \u003CTextBlock Style=\u0022{StaticResource BodyStrongTextBlockStyle}\u0022\n Text=\u0022PivotSegmentedStyle\u0022 /\u003E\n \u003Ccontrols:Segmented\n SelectedIndex=\u00221\u0022\n Style=\u0022{StaticResource PivotSegmentedStyle}\u0022\u003E\n \u003Ccontrols:SegmentedItem\u003EItem 1\u003C/controls:SegmentedItem\u003E\n \u003Ccontrols:SegmentedItem\u003EItem 2\u003C/controls:SegmentedItem\u003E\n \u003Ccontrols:SegmentedItem\u003EItem with long label\u003C/controls:SegmentedItem\u003E\n \u003Ccontrols:SegmentedItem\u003EItem 4\u003C/controls:SegmentedItem\u003E\n \u003C/controls:Segmented\u003E\n\n \u003CTextBlock Margin=\u00220,24,0,0\u0022\n Style=\u0022{StaticResource BodyStrongTextBlockStyle}\u0022\n Text=\u0022ButtonSegmentedStyle\u0022 /\u003E\n \u003Ccontrols:Segmented\n SelectedIndex=\u00220\u0022\n Style=\u0022{StaticResource ButtonSegmentedStyle}\u0022\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Day\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xE8BF;}\u0022\n ToolTipService.ToolTip=\u0022Day view\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Week\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xE8C0;}\u0022\n ToolTipService.ToolTip=\u0022Week view\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Month\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xE787;}\u0022\n ToolTipService.ToolTip=\u0022Month view\u0022 /\u003E\n \u003C/controls:Segmented\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SegmentedStylesSample",
+ "sourcePath": "components/Segmented/samples/SegmentedStylesSample.xaml",
+ "optionBindingsDropped": [
+ "Orientation",
+ "SelectionMode"
+ ]
+ }
+ },
+ {
+ "header": "Segmented \u002B SwitchPresenter",
+ "details": "The Segmented control can be combined with e.g. a SwitchPresenter to provide easy navigation and with limited XAML and no code-behind!",
+ "xaml": "\u003CStackPanel Width=\u0022360\u0022\n VerticalAlignment=\u0022Top\u0022\n Orientation=\u0022Vertical\u0022\n Spacing=\u00228\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003CStyle x:Key=\u0022PanelStyle\u0022\n TargetType=\u0022StackPanel\u0022\u003E\n \u003CSetter Property=\u0022CornerRadius\u0022 Value=\u00228\u0022 /\u003E\n \u003CSetter Property=\u0022Padding\u0022 Value=\u002216\u0022 /\u003E\n \u003CSetter Property=\u0022Background\u0022 Value=\u0022{ThemeResource CardBackgroundFillColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022BorderThickness\u0022 Value=\u00221\u0022 /\u003E\n \u003CSetter Property=\u0022BorderBrush\u0022 Value=\u0022{ThemeResource CardStrokeColorDefaultBrush}\u0022 /\u003E\n \u003CSetter Property=\u0022Orientation\u0022 Value=\u0022Horizontal\u0022 /\u003E\n \u003CSetter Property=\u0022Spacing\u0022 Value=\u00228\u0022 /\u003E\n \u003CSetter Property=\u0022animations:Implicit.HideAnimations\u0022 Value=\u0022{StaticResource ShowTransitions}\u0022 /\u003E\n \u003C/Style\u003E\n\n \u003Canimations:ImplicitAnimationSet x:Name=\u0022ShowTransitions\u0022\u003E\n \u003Canimations:OffsetAnimation EasingMode=\u0022EaseOut\u0022\n From=\u00220,24,0\u0022\n To=\u00220\u0022\n Duration=\u00220:0:0.4\u0022 /\u003E\n \u003Canimations:OpacityAnimation EasingMode=\u0022EaseOut\u0022\n From=\u00220\u0022\n To=\u00221\u0022\n Duration=\u00220:0:0.2\u0022 /\u003E\n \u003C/animations:ImplicitAnimationSet\u003E\n \u003Canimations:ImplicitAnimationSet x:Name=\u0022HideTransitions\u0022\u003E\n \u003Canimations:OffsetAnimation EasingMode=\u0022EaseOut\u0022\n From=\u00220\u0022\n To=\u00220,24,0\u0022\n Duration=\u00220:0:0.2\u0022 /\u003E\n \u003Canimations:OpacityAnimation EasingMode=\u0022EaseOut\u0022\n From=\u00221\u0022\n To=\u00220\u0022\n Duration=\u00220:0:0.1\u0022 /\u003E\n \u003C/animations:ImplicitAnimationSet\u003E\n \u003C/StackPanel.Resources\u003E\n \u003Ccontrols:Segmented x:Name=\u0022segmentedControl\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n SelectedIndex=\u00220\u0022\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Square\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xE739;}\u0022\n Tag=\u0022square\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Circle\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xEA3A;}\u0022\n Tag=\u0022circle\u0022 /\u003E\n \u003Ccontrols:SegmentedItem Content=\u0022Rectangle\u0022\n Icon=\u0022{ui:FontIcon Glyph=\u0026#xE7FB;}\u0022\n Tag=\u0022rect\u0022 /\u003E\n \u003C/controls:Segmented\u003E\n \u003Ccontrols:SwitchPresenter Value=\u0022{Binding SelectedItem.Tag, ElementName=segmentedControl}\u0022\u003E\n \u003Ccontrols:Case Value=\u0022square\u0022\u003E\n \u003CStackPanel animations:Implicit.HideAnimations=\u0022{StaticResource HideTransitions}\u0022\n animations:Implicit.ShowAnimations=\u0022{StaticResource ShowTransitions}\u0022\n Style=\u0022{StaticResource PanelStyle}\u0022\u003E\n\n \u003CBorder Width=\u002224\u0022\n Height=\u002224\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022 /\u003E\n \u003CTextBlock VerticalAlignment=\u0022Center\u0022\n Text=\u0022This is the Square panel\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022circle\u0022\u003E\n \u003CStackPanel animations:Implicit.HideAnimations=\u0022{StaticResource HideTransitions}\u0022\n animations:Implicit.ShowAnimations=\u0022{StaticResource ShowTransitions}\u0022\n Style=\u0022{StaticResource PanelStyle}\u0022\u003E\n\n \u003CEllipse Width=\u002224\u0022\n Height=\u002224\u0022\n Fill=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022 /\u003E\n \u003CTextBlock VerticalAlignment=\u0022Center\u0022\n Text=\u0022This is the Circle panel\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/controls:Case\u003E\n \u003Ccontrols:Case Value=\u0022rect\u0022\u003E\n \u003CStackPanel animations:Implicit.HideAnimations=\u0022{StaticResource HideTransitions}\u0022\n animations:Implicit.ShowAnimations=\u0022{StaticResource ShowTransitions}\u0022\n Style=\u0022{StaticResource PanelStyle}\u0022\u003E\n \u003CRectangle Width=\u002248\u0022\n Height=\u002224\u0022\n Fill=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022 /\u003E\n \u003CTextBlock VerticalAlignment=\u0022Center\u0022\n Text=\u0022This is the Rectangle panel\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/controls:Case\u003E\n \u003C/controls:SwitchPresenter\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:animations=\u0022using:CommunityToolkit.WinUI.Animations\u0022",
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SegmentedSwitchPresenterSample",
+ "sourcePath": "components/Segmented/samples/SegmentedSwitchPresenterSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "settingscard",
+ "name": "SettingsCard",
+ "description": "A card control that can be used to create Windows 11 style settings experiences.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.SettingsControls",
+ "usings": [
+ "System.ComponentModel"
+ ],
+ "curatedKeywords": [
+ "SettingsCard",
+ "Control",
+ "Layout",
+ "Settings"
+ ],
+ "keywords": [
+ "Controls",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "SettingsCard",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/SettingsControls/samples/SettingsCard.md"
+ }
+ ],
+ "toolkit": {
+ "component": "SettingsControls",
+ "documentPath": "components/SettingsControls/samples/SettingsCard.md",
+ "category": "Controls",
+ "subcategory": "Layout",
+ "author": "niels9001"
+ },
+ "samples": [
+ {
+ "header": "SettingsCard",
+ "details": "SettingsCard is a control that can be used to display settings in your experience. It uses the default styling found in Windows 11 and is easy to use, meets all accessibility standards and will make your settings page look great! You can set the Header, Description, HeaderIcon and Content properties to create an easy to use experience, like so:",
+ "xaml": "\u003CStackPanel Spacing=\u00224\u0022\u003E\n\n \u003Ccontrols:SettingsCard x:Name=\u0022settingsCard\u0022\n Description=\u0022This is a default card, with the Header, HeaderIcon, Description and Content set.\u0022\n Header=\u0022This is the Header\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE799;}\u0022\n IsEnabled=\u0022True\u0022\u003E\n \u003CComboBox SelectedIndex=\u00220\u0022\u003E\n \u003CComboBoxItem\u003EOption 1\u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EOption 2\u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EOption 3\u003C/ComboBoxItem\u003E\n \u003C/ComboBox\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsCard Description=\u0022You can use a FontIcon, SymbolIcon or BitmapIcon to set the cards HeaderIcon.\u0022\n Header=\u0022Icon options\u0022\n HeaderIcon=\u0022{ui:BitmapIcon Source=ms-appx:///Assets/AppTitleBar.scale-200.png}\u0022\n IsEnabled=\u0022True\u0022\u003E\n \u003CToggleSwitch /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsCard Header=\u0022A card with custom objects as its Description\u0022\n IsEnabled=\u0022True\u0022\u003E\n \u003Ccontrols:SettingsCard.Description\u003E\n \u003CHyperlinkButton Content=\u0022Learn more about Phone Link\u0022 /\u003E\n \u003C/controls:SettingsCard.Description\u003E\n \u003CButton Content=\u0022Open Phone Link\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsCard Description=\u0022When resizing a SettingsCard, the Content will wrap vertically. You can override this breakpoint by setting the SettingsCardWrapThreshold resource. For edge cases, you can also hide the icon by setting SettingsCardWrapNoIconThreshold.\u0022\n Header=\u0022Adaptive layouts\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE745;}\u0022\n IsEnabled=\u0022True\u0022\u003E\n \u003Ccontrols:SettingsCard.Resources\u003E\n \u003Cx:Double x:Key=\u0022SettingsCardWrapThreshold\u0022\u003E800\u003C/x:Double\u003E\n \u003Cx:Double x:Key=\u0022SettingsCardWrapNoIconThreshold\u0022\u003E600\u003C/x:Double\u003E\n \u003C/controls:SettingsCard.Resources\u003E\n \u003CButton Content=\u0022This control will wrap vertically!\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsCard Header=\u0022This is a card with a Header only\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SettingsCardSample",
+ "sourcePath": "components/SettingsControls/samples/SettingsCardSample.xaml",
+ "optionsResolved": [
+ "IsCardEnabled=True"
+ ]
+ }
+ },
+ {
+ "header": "ClickableSettingsCardSample",
+ "details": "SettingsCard can also be turned into a button, by setting the IsClickEnabled property. This can be useful whenever you want your settings component to navigate to a detail page or open an external link. You can set a custom icon by setting the ActionIcon, or hiding it completely by setting the IsActionIconVisible to false.",
+ "xaml": "\u003CStackPanel Spacing=\u00224\u0022\u003E\n \u003Ccontrols:SettingsCard x:Name=\u0022settingsCard\u0022\n Click=\u0022OnCardClicked\u0022\n Description=\u0022A SettingsCard can be made clickable and you can leverage the Command property or Click event.\u0022\n Header=\u0022A clickable SettingsCard\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE799;}\u0022\n IsClickEnabled=\u0022True\u0022\n IsEnabled=\u0022True\u0022\u003E\n \u003CTextBlock Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Text=\u0022This is content\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsCard ActionIcon=\u0022{ui:FontIcon Glyph=\u0026#xE8A7;}\u0022\n ActionIconToolTip=\u0022Open in new window\u0022\n Click=\u0022OnCardClicked\u0022\n Description=\u0022You can customize the ActionIcon and ActionIconToolTip.\u0022\n Header=\u0022Customizing the ActionIcon\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE774;}\u0022\n IsClickEnabled=\u0022True\u0022\n IsEnabled=\u0022True\u0022 /\u003E\n\n \u003Ccontrols:SettingsCard Click=\u0022OnCardClicked\u0022\n Header=\u0022Hiding the ActionIcon\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE72E;}\u0022\n IsActionIconVisible=\u0022False\u0022\n IsClickEnabled=\u0022True\u0022\n IsEnabled=\u0022True\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "code": "private async void OnCardClicked(object sender, RoutedEventArgs e)\n{\n await Windows.System.Launcher.LaunchUriAsync(new Uri(\u0022https://www.microsoft.com\u0022));\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ClickableSettingsCardSample",
+ "sourcePath": "components/SettingsControls/samples/ClickableSettingsCardSample.xaml",
+ "optionsResolved": [
+ "IsCardEnabled=True"
+ ]
+ }
+ },
+ {
+ "header": "Settings Page Example",
+ "details": "The following sample provides a typical design page, following the correct Windows 11 design specifications for things like spacing, section headers and animations.",
+ "xaml": "\u003CScrollViewer\u003E\n \u003CScrollViewer.Resources\u003E\n \u003C!-- These styles can be referenced to create a consistent SettingsPage layout --\u003E\n\n \u003C!-- Spacing between cards --\u003E\n \u003Cx:Double x:Key=\u0022SettingsCardSpacing\u0022\u003E4\u003C/x:Double\u003E\n\n \u003C!-- Style (inc. the correct spacing) of a section header --\u003E\n \u003CStyle x:Key=\u0022SettingsSectionHeaderTextBlockStyle\u0022\n BasedOn=\u0022{StaticResource BodyStrongTextBlockStyle}\u0022\n TargetType=\u0022TextBlock\u0022\u003E\n \u003CStyle.Setters\u003E\n \u003CSetter Property=\u0022Margin\u0022 Value=\u00221,30,0,6\u0022 /\u003E\n \u003C/Style.Setters\u003E\n \u003C/Style\u003E\n \u003C/ScrollViewer.Resources\u003E\n \u003CGrid\u003E\n \u003CStackPanel MaxWidth=\u00221000\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n Spacing=\u0022{StaticResource SettingsCardSpacing}\u0022\u003E\n \u003CStackPanel.ChildrenTransitions\u003E\n \u003CEntranceThemeTransition FromVerticalOffset=\u002250\u0022 /\u003E\n \u003CRepositionThemeTransition IsStaggeringEnabled=\u0022False\u0022 /\u003E\n \u003C/StackPanel.ChildrenTransitions\u003E\n \u003CTextBlock Style=\u0022{StaticResource SettingsSectionHeaderTextBlockStyle}\u0022\n Text=\u0022Section 1\u0022 /\u003E\n \u003Ccontrols:SettingsCard Description=\u0022This is a default card, with the Header, HeaderIcon, Description and Content set\u0022\n Header=\u0022This is the Header\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE716;}\u0022\u003E\n \u003CToggleSwitch IsOn=\u0022True\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsExpander Description=\u0022The SettingsExpander has the same properties as a SettingsCard\u0022\n Header=\u0022SettingsExpander\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE91B;}\u0022\u003E\n \u003CButton Content=\u0022Content\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n\n \u003Ccontrols:SettingsExpander.Items\u003E\n \u003Ccontrols:SettingsCard Header=\u0022A basic SettingsCard within an SettingsExpander\u0022\u003E\n \u003CButton Content=\u0022Button\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003Ccontrols:SettingsCard Description=\u0022SettingsCard within an Expander can be made clickable too!\u0022\n Header=\u0022This item can be clicked\u0022\n IsClickEnabled=\u0022True\u0022 /\u003E\n\n \u003Ccontrols:SettingsCard ContentAlignment=\u0022Left\u0022\u003E\n \u003CCheckBox Content=\u0022Here the ContentAlignment is set to Left. This is great for e.g. CheckBoxes or RadioButtons\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/controls:SettingsExpander.Items\u003E\n \u003C/controls:SettingsExpander\u003E\n\n \u003CTextBlock Style=\u0022{StaticResource SettingsSectionHeaderTextBlockStyle}\u0022\n Text=\u0022Section 2\u0022 /\u003E\n \u003Ccontrols:SettingsCard Description=\u0022Another card to show grouping of cards\u0022\n Header=\u0022Another SettingsCard\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE799;}\u0022\u003E\n \u003CComboBox SelectedIndex=\u00220\u0022\u003E\n \u003CComboBoxItem\u003EOption 1\u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EOption 2\u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EOption 3\u003C/ComboBoxItem\u003E\n \u003C/ComboBox\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsCard Description=\u0022Another card to show grouping of cards\u0022\n Header=\u0022Yet another SettingsCard\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE768;}\u0022\u003E\n \u003CButton Content=\u0022Content\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003C!-- Example \u0027About\u0027 section --\u003E\n \u003CTextBlock Style=\u0022{StaticResource SettingsSectionHeaderTextBlockStyle}\u0022\n Text=\u0022About\u0022 /\u003E\n\n \u003Ccontrols:SettingsExpander Description=\u0022© 2023. All rights reserved.\u0022\n Header=\u0022Community Toolkit Gallery\u0022\n HeaderIcon=\u0022{ui:BitmapIcon Source=ms-appx:///Assets/AppTitleBar.scale-200.png}\u0022\u003E\n \u003CTextBlock IsTextSelectionEnabled=\u0022True\u0022\n Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Text=\u0022Version 8.0.0\u0022 /\u003E\n \u003Ccontrols:SettingsExpander.Items\u003E\n \u003Ccontrols:SettingsCard HorizontalContentAlignment=\u0022Left\u0022\n ContentAlignment=\u0022Left\u0022\u003E\n \u003CStackPanel Margin=\u0022-12,0,0,0\u0022\n Orientation=\u0022Vertical\u0022\u003E\n \u003CHyperlinkButton Content=\u0022Link 1\u0022 /\u003E\n \u003CHyperlinkButton Content=\u0022Link 2\u0022 /\u003E\n \u003CHyperlinkButton Content=\u0022Link 3\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/controls:SettingsExpander.Items\u003E\n \u003C/controls:SettingsExpander\u003E\n \u003CHyperlinkButton Margin=\u00220,8,0,0\u0022\n Content=\u0022Send feedback\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/Grid\u003E\n\u003C/ScrollViewer\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SettingsPageExample",
+ "sourcePath": "components/SettingsControls/samples/SettingsPageExample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "settingsexpander",
+ "name": "SettingsExpander",
+ "description": "An expander control that can be used to create Windows 11 style settings experiences.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.SettingsControls",
+ "usings": [
+ "CommunityToolkit.Mvvm.ComponentModel",
+ "Windows.System"
+ ],
+ "curatedKeywords": [
+ "SettingsCard",
+ "SettingsExpander",
+ "Expander",
+ "Control",
+ "Layout",
+ "Settings"
+ ],
+ "keywords": [
+ "Controls",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "SettingsExpander",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/SettingsControls/samples/SettingsExpander.md"
+ }
+ ],
+ "toolkit": {
+ "component": "SettingsControls",
+ "documentPath": "components/SettingsControls/samples/SettingsExpander.md",
+ "category": "Controls",
+ "subcategory": "Layout",
+ "author": "niels9001"
+ },
+ "samples": [
+ {
+ "header": "SettingsExpander",
+ "details": "The SettingsExpander can be used to group multiple SettingsCards into a single collapsible group. A SettingsExpander can have it\u0027s own content to display a setting on the right, just like a SettingsCard, but in addition can have any number of extra Items to include as additional settings. These items are SettingsCards themselves, which means you can easily move a setting into or out of Expanders just by cutting and pasting their XAML!",
+ "xaml": "\u003Ccontrols:SettingsExpander x:Name=\u0022settingsCard\u0022\n VerticalAlignment=\u0022Top\u0022\n Description=\u0022The SettingsExpander has the same properties as a Card, and you can set SettingsCard as part of the Items collection.\u0022\n Header=\u0022SettingsExpander\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE91B;}\u0022\n IsEnabled=\u0022True\u0022\n IsExpanded=\u0022False\u0022\u003E\n \u003C!-- TODO: This should be TwoWay bound but throws compile error in Uno. --\u003E\n \u003CComboBox SelectedIndex=\u00220\u0022\u003E\n \u003CComboBoxItem\u003EOption 1\u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EOption 2\u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EOption 3\u003C/ComboBoxItem\u003E\n \u003C/ComboBox\u003E\n\n \u003Ccontrols:SettingsExpander.Items\u003E\n \u003Ccontrols:SettingsCard Header=\u0022A basic SettingsCard within an SettingsExpander\u0022\u003E\n \u003CButton Content=\u0022Button\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003Ccontrols:SettingsCard Description=\u0022SettingsCard within an Expander can be made clickable too!\u0022\n Header=\u0022This item can be clicked\u0022\n IsClickEnabled=\u0022True\u0022 /\u003E\n\n \u003Ccontrols:SettingsCard ContentAlignment=\u0022Left\u0022\u003E\n \u003CCheckBox Content=\u0022Here the ContentAlignment is set to Left. This is great for e.g. CheckBoxes or RadioButtons.\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsCard HorizontalContentAlignment=\u0022Left\u0022\n ContentAlignment=\u0022Vertical\u0022\n Description=\u0022You can also align your content vertically. Make sure to set the HorizontalAlignment to Left when you do!\u0022\n Header=\u0022Vertically aligned\u0022\u003E\n \u003CGridView SelectedIndex=\u00221\u0022\u003E\n \u003CGridViewItem\u003E\n \u003CBorder Width=\u002264\u0022\n Height=\u002264\u0022\n Background=\u0022#0078D4\u0022\n CornerRadius=\u00224\u0022 /\u003E\n \u003C/GridViewItem\u003E\n \u003CGridViewItem\u003E\n \u003CBorder Width=\u002264\u0022\n Height=\u002264\u0022\n Background=\u0022#005EB7\u0022\n CornerRadius=\u00224\u0022 /\u003E\n \u003C/GridViewItem\u003E\n \u003CGridViewItem\u003E\n \u003CBorder Width=\u002264\u0022\n Height=\u002264\u0022\n Background=\u0022#003D92\u0022\n CornerRadius=\u00224\u0022 /\u003E\n \u003C/GridViewItem\u003E\n \u003CGridViewItem\u003E\n \u003CBorder Width=\u002264\u0022\n Height=\u002264\u0022\n Background=\u0022#001968\u0022\n CornerRadius=\u00224\u0022 /\u003E\n \u003C/GridViewItem\u003E\n \u003C/GridView\u003E\n \u003C/controls:SettingsCard\u003E\n \u003Ccontrols:SettingsCard Description=\u0022You can override the Left indention of a SettingsCard by overriding the SettingsCardLeftIndention\u0022\n Header=\u0022Customization\u0022\u003E\n \u003Ccontrols:SettingsCard.Resources\u003E\n \u003Cx:Double x:Key=\u0022SettingsCardLeftIndention\u0022\u003E40\u003C/x:Double\u003E\n \u003C/controls:SettingsCard.Resources\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/controls:SettingsExpander.Items\u003E\n\u003C/controls:SettingsExpander\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SettingsExpanderSample",
+ "sourcePath": "components/SettingsControls/samples/SettingsExpanderSample.xaml",
+ "optionsResolved": [
+ "IsCardEnabled=True",
+ "IsCardExpanded=False"
+ ]
+ }
+ },
+ {
+ "header": "SettingsExpanderItemsSource",
+ "details": "You can easily override certain properties to create custom experiences. For instance, you can customize the ContentAlignment of a SettingsCard, to align your content to the Right (default), Left (hiding the HeaderIcon, Header and Description) or Vertically (usually best paired with changing the HorizontalContentAlignment to Stretch). SettingsExpander is also an ItemsControl, so its items can be driven by a collection and the ItemsSource property. You can use the ItemTemplate to define how your data object is represented as a SettingsCard, as shown below. The ItemsHeader and ItemsFooter property can be used to host custom content at the start or end of the items list.",
+ "xaml": "\u003CStackPanel Spacing=\u00224\u0022\u003E\n \u003CStackPanel.Resources\u003E\n \u003CResourceDictionary\u003E\n \u003CResourceDictionary.MergedDictionaries\u003E\n \u003CResourceDictionary Source=\u0022ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsExpander/SettingsExpander.xaml\u0022 /\u003E\n \u003C/ResourceDictionary.MergedDictionaries\u003E\n \u003C/ResourceDictionary\u003E\n \u003C/StackPanel.Resources\u003E\n \u003Ccontrols:SettingsExpander Description=\u0022The SettingsExpander can use ItemsSource to define its Items.\u0022\n Header=\u0022Settings Expander with ItemsSource\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xEA37;}\u0022\n ItemsSource=\u0022{x:Bind MyDataSet}\u0022\u003E\n \u003Ccontrols:SettingsExpander.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:MyDataModel\u0022\u003E\n \u003Ccontrols:SettingsCard Description=\u0022{x:Bind Info}\u0022\n Header=\u0022{x:Bind Name}\u0022\u003E\n \u003CHyperlinkButton Content=\u0022{x:Bind LinkDescription}\u0022\n NavigateUri=\u0022{x:Bind Url}\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:SettingsExpander.ItemTemplate\u003E\n \u003Ccontrols:SettingsExpander.ItemsHeader\u003E\n \u003Cmuxc:InfoBar Title=\u0022This is the ItemsHeader\u0022\n BorderThickness=\u00220\u0022\n CornerRadius=\u00220\u0022\n IsIconVisible=\u0022False\u0022\n IsOpen=\u0022True\u0022\n Severity=\u0022Success\u0022\u003E\n \u003Cmuxc:InfoBar.ActionButton\u003E\n \u003CHyperlinkButton Content=\u0022It can host custom content\u0022 /\u003E\n \u003C/muxc:InfoBar.ActionButton\u003E\n \u003C/muxc:InfoBar\u003E\n \u003C/controls:SettingsExpander.ItemsHeader\u003E\n \u003Ccontrols:SettingsExpander.ItemsFooter\u003E\n \u003Ccontrols:SettingsCard Header=\u0022This is the ItemsFooter\u0022\n Style=\u0022{StaticResource DefaultSettingsExpanderItemStyle}\u0022\u003E\n \u003CButton Content=\u0022Add a device\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/controls:SettingsExpander.ItemsFooter\u003E\n \u003C/controls:SettingsExpander\u003E\n\n \u003Ccontrols:SettingsExpander Description=\u0022SettingsExpander can use a DataTemplate, DataTemplateSelector, or IElementFactory for its ItemTemplate.\u0022\n Header=\u0022Settings Expander with a custom ItemTemplate\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE8FD;}\u0022\n ItemsSource=\u0022{x:Bind MyDataSet}\u0022\u003E\n \u003Ccontrols:SettingsExpander.ItemTemplate\u003E\n \u003Clocal:MyDataModelTemplateSelector\u003E\n \u003Clocal:MyDataModelTemplateSelector.ButtonTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:MyDataModel\u0022\u003E\n \u003Ccontrols:SettingsCard Description=\u0022{x:Bind ItemType}\u0022\n Header=\u0022{x:Bind Name}\u0022\u003E\n \u003CButton Click=\u0022Button_Click\u0022\n Content=\u0022{x:Bind LinkDescription}\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/DataTemplate\u003E\n \u003C/local:MyDataModelTemplateSelector.ButtonTemplate\u003E\n\n \u003Clocal:MyDataModelTemplateSelector.LinkButtonTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:MyDataModel\u0022\u003E\n \u003Ccontrols:SettingsCard Description=\u0022{x:Bind ItemType}\u0022\n Header=\u0022{x:Bind Name}\u0022\u003E\n \u003CHyperlinkButton Content=\u0022{x:Bind LinkDescription}\u0022\n NavigateUri=\u0022{x:Bind Url}\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/DataTemplate\u003E\n \u003C/local:MyDataModelTemplateSelector.LinkButtonTemplate\u003E\n\n \u003Clocal:MyDataModelTemplateSelector.NoButtonTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:MyDataModel\u0022\u003E\n \u003Ccontrols:SettingsCard Description=\u0022{x:Bind ItemType}\u0022\n Header=\u0022{x:Bind Name}\u0022 /\u003E\n \u003C/DataTemplate\u003E\n \u003C/local:MyDataModelTemplateSelector.NoButtonTemplate\u003E\n \u003C/local:MyDataModelTemplateSelector\u003E\n \u003C/controls:SettingsExpander.ItemTemplate\u003E\n \u003C/controls:SettingsExpander\u003E\n\u003C/StackPanel\u003E",
+ "code": "public ObservableCollection\u003CMyDataModel\u003E MyDataSet = new()\n{\n new()\n {\n Name = \u0022First Item\u0022,\n Info = \u0022More about first item.\u0022,\n ItemType = \u0022Item type: Button\u0022,\n LinkDescription = \u0022Click here for more on first item.\u0022,\n Url = \u0022https://microsoft.com/\u0022,\n },\n new()\n {\n Name = \u0022Second Item\u0022,\n Info = \u0022More about second item.\u0022,\n ItemType = \u0022Item type: Link button\u0022,\n LinkDescription = \u0022Click here for more on second item.\u0022,\n Url = \u0022https://xbox.com/\u0022,\n },\n new()\n {\n Name = \u0022Third Item\u0022,\n Info = \u0022More about third item.\u0022,\n ItemType = \u0022Item type: No button\u0022,\n LinkDescription = \u0022Click here for more on third item.\u0022,\n Url = \u0022https://toolkitlabs.dev/\u0022,\n },\n};\n\nprivate async void Button_Click(object sender, RoutedEventArgs e)\n{\n _ = await Launcher.LaunchUriAsync(new(\u0022https://microsoft.com/\u0022));\n}\n\npublic class MyDataModel\n{\n public string? Name { get; set; }\n public string? Info { get; set; }\n public string? ItemType { get; set; }\n public string? LinkDescription { get; set; }\n public string? Url { get; set; }\n}\n\npublic partial class MyDataModelTemplateSelector : DataTemplateSelector\n{\n public DataTemplate? ButtonTemplate { get; set; }\n public DataTemplate? LinkButtonTemplate { get; set; }\n public DataTemplate? NoButtonTemplate { get; set; }\n\n protected override DataTemplate SelectTemplateCore(object item)\n {\n var itm = (MyDataModel)item;\n if (itm.ItemType?.EndsWith(\u0022Button\u0022) == true)\n {\n return ButtonTemplate!;\n }\n else if (itm.ItemType?.EndsWith(\u0022Link button\u0022) == true)\n {\n return LinkButtonTemplate!;\n }\n else\n {\n return NoButtonTemplate!;\n }\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:local=\u0022using:SettingsControlsExperiment.Samples\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SettingsExpanderItemsSourceSample",
+ "sourcePath": "components/SettingsControls/samples/SettingsExpanderItemsSourceSample.xaml"
+ }
+ },
+ {
+ "header": "SettingsExpanderDragHandle",
+ "details": "You may use a list of SettingsCard or SettingsExpander to represent configurable items within a tool. The order of these may be something you want to track. In this case there is a conflict between the interactions with the settings card and the drag and drop interactions of a parent containing ListView, for instance. Therefore, it is recommended to use the drag-handle type UI approach for this scenario in having a dedicated space for re-ordering manipulation vs. interaction with the Settings control. You can see how to do this with SettingsExpander in the example below, however it equally works with a collection of SettingsCard as the main data template component as well.",
+ "xaml": "\u003C!--\n Enabled the List view to drag its items and reorder them around,\n plus need SelectionMode None so the indicator doesn\u0027t appear when clicking on the drag region\n--\u003E\n\u003CListView AllowDrop=\u0022True\u0022\n CanDragItems=\u0022True\u0022\n CanReorderItems=\u0022True\u0022\n ItemsSource=\u0022{x:Bind MyDataSet}\u0022\n SelectionMode=\u0022None\u0022\u003E\n \u003CListView.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022local:ExpandedCardInfo\u0022\u003E\n \u003CGrid\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition Width=\u0022Auto\u0022 /\u003E\n \u003CColumnDefinition Width=\u0022*\u0022 /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n \u003C!-- Provide a custom area that can be manipulated by the user, this could be before or after the card. --\u003E\n \u003CBorder Width=\u002236\u0022\n Height=\u002275\u0022\n Margin=\u00221,1,1,1\u0022\n VerticalAlignment=\u0022Top\u0022\n Background=\u0022Transparent\u0022\n CornerRadius=\u0022{ThemeResource ControlCornerRadius}\u0022\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n FontFamily=\u0022Segoe UI Symbol\u0022\n FontSize=\u002220\u0022\n Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Text=\u0022\u0026#x283F;\u0022 /\u003E\n \u003C!-- Use Behaviors to hover on pointer --\u003E\n \u003CInteractivity:Interaction.Behaviors\u003E\n \u003CInteractivity:EventTriggerBehavior EventName=\u0022PointerEntered\u0022\u003E\n \u003CInteractivity:ChangePropertyAction PropertyName=\u0022Background\u0022\u003E\n \u003CInteractivity:ChangePropertyAction.Value\u003E\n \u003CSolidColorBrush Color=\u0022{ThemeResource ControlFillColorDefault}\u0022 /\u003E\n \u003C/Interactivity:ChangePropertyAction.Value\u003E\n \u003C/Interactivity:ChangePropertyAction\u003E\n \u003C/Interactivity:EventTriggerBehavior\u003E\n \u003CInteractivity:EventTriggerBehavior EventName=\u0022PointerExited\u0022\u003E\n \u003CInteractivity:ChangePropertyAction PropertyName=\u0022Background\u0022\u003E\n \u003CInteractivity:ChangePropertyAction.Value\u003E\n \u003CSolidColorBrush Color=\u0022Transparent\u0022 /\u003E\n \u003C/Interactivity:ChangePropertyAction.Value\u003E\n \u003C/Interactivity:ChangePropertyAction\u003E\n \u003C/Interactivity:EventTriggerBehavior\u003E\n \u003C/Interactivity:Interaction.Behaviors\u003E\n \u003C/Border\u003E\n \u003C!-- Standard Settings Expander (could also just be a Card) --\u003E\n \u003Ccontrols:SettingsExpander Grid.Column=\u00221\u0022\n Description=\u0022{x:Bind Info}\u0022\n Header=\u0022{x:Bind Name}\u0022\n IsExpanded=\u0022{x:Bind IsExpanded, Mode=TwoWay}\u0022\u003E\n\n \u003CToggleSwitch IsOn=\u0022{x:Bind IsExpanded, Mode=TwoWay}\u0022\n OffContent=\u0022Off\u0022\n OnContent=\u0022On\u0022 /\u003E\n\n \u003Ccontrols:SettingsExpander.Items\u003E\n \u003Ccontrols:SettingsCard Header=\u0022{x:Bind LinkDescription}\u0022\u003E\n \u003CHyperlinkButton Content=\u0022{x:Bind Url}\u0022\n NavigateUri=\u0022{x:Bind Url}\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/controls:SettingsExpander.Items\u003E\n \u003C/controls:SettingsExpander\u003E\n \u003C/Grid\u003E\n \u003C/DataTemplate\u003E\n \u003C/ListView.ItemTemplate\u003E\n \u003C!-- Customize Size of Item Container from ListView --\u003E\n \u003CListView.ItemContainerStyle\u003E\n \u003CStyle BasedOn=\u0022{StaticResource DefaultListViewItemStyle}\u0022\n TargetType=\u0022ListViewItem\u0022\u003E\n \u003CSetter Property=\u0022Margin\u0022 Value=\u00220\u0022 /\u003E\n \u003CSetter Property=\u0022Padding\u0022 Value=\u00224\u0022 /\u003E\n \u003C/Style\u003E\n \u003C/ListView.ItemContainerStyle\u003E\n \u003C!-- Hides the overall highlight from the ListView Container --\u003E\n \u003CListView.Resources\u003E\n \u003CSolidColorBrush x:Key=\u0022ListViewItemBackgroundPointerOver\u0022\n Color=\u0022Transparent\u0022 /\u003E\n \u003C/ListView.Resources\u003E\n\u003C/ListView\u003E",
+ "code": "public ObservableCollection\u003CExpandedCardInfo\u003E MyDataSet = new()\n{\n new()\n {\n Name = \u0022First Item\u0022,\n Info = \u0022More about first item.\u0022,\n LinkDescription = \u0022Click the link for more on first item.\u0022,\n Url = \u0022https://microsoft.com/\u0022,\n },\n new()\n {\n Name = \u0022Second Item\u0022,\n Info = \u0022More about second item.\u0022,\n LinkDescription = \u0022Click the link for more on second item.\u0022,\n Url = \u0022https://xbox.com/\u0022,\n },\n new()\n {\n Name = \u0022Third Item\u0022,\n Info = \u0022More about third item.\u0022,\n LinkDescription = \u0022Click the link for more on third item.\u0022,\n Url = \u0022https://toolkitlabs.dev/\u0022,\n },\n};\n\npublic partial class ExpandedCardInfo : ObservableObject\n{\n public string? Name { get; set; }\n public string? Info { get; set; }\n public string? LinkDescription { get; set; }\n public string? Url { get; set; }\n\n [ObservableProperty]\n public partial bool IsExpanded { get; set; } = false;\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:Interactivity=\u0022using:Microsoft.Xaml.Interactivity\u0022",
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SettingsExpanderDragHandleSample",
+ "sourcePath": "components/SettingsControls/samples/SettingsExpanderDragHandleSample.xaml"
+ }
+ },
+ {
+ "header": "Settings Page Example",
+ "details": "The following sample provides a typical design page, following the correct Windows 11 design specifications for things like spacing, section headers and animations.",
+ "xaml": "\u003CScrollViewer\u003E\n \u003CScrollViewer.Resources\u003E\n \u003C!-- These styles can be referenced to create a consistent SettingsPage layout --\u003E\n\n \u003C!-- Spacing between cards --\u003E\n \u003Cx:Double x:Key=\u0022SettingsCardSpacing\u0022\u003E4\u003C/x:Double\u003E\n\n \u003C!-- Style (inc. the correct spacing) of a section header --\u003E\n \u003CStyle x:Key=\u0022SettingsSectionHeaderTextBlockStyle\u0022\n BasedOn=\u0022{StaticResource BodyStrongTextBlockStyle}\u0022\n TargetType=\u0022TextBlock\u0022\u003E\n \u003CStyle.Setters\u003E\n \u003CSetter Property=\u0022Margin\u0022 Value=\u00221,30,0,6\u0022 /\u003E\n \u003C/Style.Setters\u003E\n \u003C/Style\u003E\n \u003C/ScrollViewer.Resources\u003E\n \u003CGrid\u003E\n \u003CStackPanel MaxWidth=\u00221000\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n Spacing=\u0022{StaticResource SettingsCardSpacing}\u0022\u003E\n \u003CStackPanel.ChildrenTransitions\u003E\n \u003CEntranceThemeTransition FromVerticalOffset=\u002250\u0022 /\u003E\n \u003CRepositionThemeTransition IsStaggeringEnabled=\u0022False\u0022 /\u003E\n \u003C/StackPanel.ChildrenTransitions\u003E\n \u003CTextBlock Style=\u0022{StaticResource SettingsSectionHeaderTextBlockStyle}\u0022\n Text=\u0022Section 1\u0022 /\u003E\n \u003Ccontrols:SettingsCard Description=\u0022This is a default card, with the Header, HeaderIcon, Description and Content set\u0022\n Header=\u0022This is the Header\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE716;}\u0022\u003E\n \u003CToggleSwitch IsOn=\u0022True\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsExpander Description=\u0022The SettingsExpander has the same properties as a SettingsCard\u0022\n Header=\u0022SettingsExpander\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE91B;}\u0022\u003E\n \u003CButton Content=\u0022Content\u0022\n Style=\u0022{StaticResource AccentButtonStyle}\u0022 /\u003E\n\n \u003Ccontrols:SettingsExpander.Items\u003E\n \u003Ccontrols:SettingsCard Header=\u0022A basic SettingsCard within an SettingsExpander\u0022\u003E\n \u003CButton Content=\u0022Button\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003Ccontrols:SettingsCard Description=\u0022SettingsCard within an Expander can be made clickable too!\u0022\n Header=\u0022This item can be clicked\u0022\n IsClickEnabled=\u0022True\u0022 /\u003E\n\n \u003Ccontrols:SettingsCard ContentAlignment=\u0022Left\u0022\u003E\n \u003CCheckBox Content=\u0022Here the ContentAlignment is set to Left. This is great for e.g. CheckBoxes or RadioButtons\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/controls:SettingsExpander.Items\u003E\n \u003C/controls:SettingsExpander\u003E\n\n \u003CTextBlock Style=\u0022{StaticResource SettingsSectionHeaderTextBlockStyle}\u0022\n Text=\u0022Section 2\u0022 /\u003E\n \u003Ccontrols:SettingsCard Description=\u0022Another card to show grouping of cards\u0022\n Header=\u0022Another SettingsCard\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE799;}\u0022\u003E\n \u003CComboBox SelectedIndex=\u00220\u0022\u003E\n \u003CComboBoxItem\u003EOption 1\u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EOption 2\u003C/ComboBoxItem\u003E\n \u003CComboBoxItem\u003EOption 3\u003C/ComboBoxItem\u003E\n \u003C/ComboBox\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003Ccontrols:SettingsCard Description=\u0022Another card to show grouping of cards\u0022\n Header=\u0022Yet another SettingsCard\u0022\n HeaderIcon=\u0022{ui:FontIcon Glyph=\u0026#xE768;}\u0022\u003E\n \u003CButton Content=\u0022Content\u0022 /\u003E\n \u003C/controls:SettingsCard\u003E\n\n \u003C!-- Example \u0027About\u0027 section --\u003E\n \u003CTextBlock Style=\u0022{StaticResource SettingsSectionHeaderTextBlockStyle}\u0022\n Text=\u0022About\u0022 /\u003E\n\n \u003Ccontrols:SettingsExpander Description=\u0022© 2023. All rights reserved.\u0022\n Header=\u0022Community Toolkit Gallery\u0022\n HeaderIcon=\u0022{ui:BitmapIcon Source=ms-appx:///Assets/AppTitleBar.scale-200.png}\u0022\u003E\n \u003CTextBlock IsTextSelectionEnabled=\u0022True\u0022\n Foreground=\u0022{ThemeResource TextFillColorSecondaryBrush}\u0022\n Text=\u0022Version 8.0.0\u0022 /\u003E\n \u003Ccontrols:SettingsExpander.Items\u003E\n \u003Ccontrols:SettingsCard HorizontalContentAlignment=\u0022Left\u0022\n ContentAlignment=\u0022Left\u0022\u003E\n \u003CStackPanel Margin=\u0022-12,0,0,0\u0022\n Orientation=\u0022Vertical\u0022\u003E\n \u003CHyperlinkButton Content=\u0022Link 1\u0022 /\u003E\n \u003CHyperlinkButton Content=\u0022Link 2\u0022 /\u003E\n \u003CHyperlinkButton Content=\u0022Link 3\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/controls:SettingsCard\u003E\n \u003C/controls:SettingsExpander.Items\u003E\n \u003C/controls:SettingsExpander\u003E\n \u003CHyperlinkButton Margin=\u00220,8,0,0\u0022\n Content=\u0022Send feedback\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/Grid\u003E\n\u003C/ScrollViewer\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SettingsPageExample",
+ "sourcePath": "components/SettingsControls/samples/SettingsPageExample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "contentsizer",
+ "name": "ContentSizer",
+ "description": "The ContentSizer is a control which can be used to resize any element, usually its parent.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Sizers",
+ "curatedKeywords": [
+ "ContentSizer",
+ "SizerBase",
+ "Control",
+ "Layout",
+ "Expander",
+ "Splitter"
+ ],
+ "keywords": [
+ "Controls",
+ "Sizers"
+ ],
+ "docs": [
+ {
+ "title": "ContentSizer",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Sizers/samples/ContentSizer.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Sizers",
+ "documentPath": "components/Sizers/samples/ContentSizer.md",
+ "category": "Controls",
+ "subcategory": "Sizers",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "Top Shelf",
+ "details": "If you are using a Grid, use GridSplitter instead. The main use-case for a ContentSizer is to create an expandable shelf for your application. This allows the Expander itself to remember its opening/closing sizes. A GridSplitter would be insufficient as it would force the grid to remember the row size and maintain its position when the Expander collapses.",
+ "xaml": "\u003CGrid MinHeight=\u0022300\u0022\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022Auto\u0022 /\u003E\n \u003CRowDefinition Height=\u0022*\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n\n \u003C!-- Bottom \u0027Shelf\u0027, In this case you cannot use GridSplitter as row would maintain its size when Expander gets collapsed --\u003E\n \u003Cmuxc:Expander x:Name=\u0022TopExpander\u0022\n VerticalAlignment=\u0022Top\u0022\n HorizontalContentAlignment=\u0022Stretch\u0022\n ExpandDirection=\u0022Up\u0022\n Header=\u0022This is some Shelf\u0022\n IsExpanded=\u0022True\u0022\u003E\n \u003CGrid x:Name=\u0022ExpandContent\u0022\n Height=\u0022128\u0022\n MinHeight=\u002232\u0022\n MaxHeight=\u0022256\u0022\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Text=\u0022This is the expanded content\u0022\n TextWrapping=\u0022Wrap\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/muxc:Expander\u003E\n \u003C!-- We expand the inner content size here so that Expander maintains it\u0027s size properly. --\u003E\n \u003Ccontrols:ContentSizer Grid.Row=\u00221\u0022\n Height=\u002216\u0022\n VerticalAlignment=\u0022Top\u0022\n Orientation=\u0022Horizontal\u0022\n TargetControl=\u0022{x:Bind ExpandContent}\u0022\n Visibility=\u0022{x:Bind TopExpander.IsExpanded, Mode=OneWay}\u0022 /\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ContentSizerTopShelfPage",
+ "sourcePath": "components/Sizers/samples/ContentSizerTopShelfPage.xaml"
+ }
+ },
+ {
+ "header": "Left-side Shelf",
+ "details": "The following example shows how to use the ContentSizer to create a left-side shelf; however, this scenario can also be accomplished with a GridSplitter.",
+ "xaml": "\u003CGrid MinWidth=\u0022400\u0022\n MinHeight=\u0022300\u0022\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition Width=\u0022Auto\u0022 /\u003E\n \u003CColumnDefinition Width=\u0022Auto\u0022 /\u003E\n \u003CColumnDefinition /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n\n \u003C!-- Left-side \u0027Shelf\u0027, In this case you could also use a GridSplitter --\u003E\n \u003CBorder x:Name=\u0022SideContent\u0022\n MinWidth=\u0022200\u0022\n MaxWidth=\u0022600\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Foreground=\u0022{ThemeResource TextOnAccentFillColorPrimaryBrush}\u0022\n Style=\u0022{ThemeResource BodyStrongTextBlockStyle}\u0022\n Text=\u0022Side Content\u0022 /\u003E\n \u003C/Border\u003E\n \u003Ccontrols:ContentSizer Grid.Column=\u00221\u0022\n TargetControl=\u0022{x:Bind SideContent}\u0022 /\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ContentSizerLeftShelfPage",
+ "sourcePath": "components/Sizers/samples/ContentSizerLeftShelfPage.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "gridsplitter",
+ "name": "GridSplitter",
+ "description": "The GridSplitter control provides an easy-to-use Splitter that redistributes space between columns or rows of a Grid Control.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Sizers",
+ "curatedKeywords": [
+ "ContentSizer",
+ "SizerBase",
+ "Control",
+ "Layout",
+ "Expander"
+ ],
+ "keywords": [
+ "Controls",
+ "Sizers"
+ ],
+ "docs": [
+ {
+ "title": "GridSplitter",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Sizers/samples/GridSplitter.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Sizers",
+ "documentPath": "components/Sizers/samples/GridSplitter.md",
+ "category": "Controls",
+ "subcategory": "Sizers",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "GridSplitter Example",
+ "details": "The control automatically detects the targeted columns/rows to resize, while dragging the control it starts to resize the columns/rows and redistributes space between columns/rows, you can manually specify the ResizeDirection (Auto / Column / Row) and the ResizeBehavior to select which columns/rows to resize. GridSplitter control will resize the targeted rows or columns",
+ "xaml": "\u003CGrid x:Name=\u0022RootGrid\u0022\n Height=\u0022300\u0022\n VerticalAlignment=\u0022Top\u0022\n BorderBrush=\u0022{ThemeResource SystemControlHighlightChromeHighBrush}\u0022\n BorderThickness=\u00220,0,1,1\u0022\u003E\n \u003CGrid.Resources\u003E\n \u003CStyle TargetType=\u0022Border\u0022\u003E\n \u003CSetter Property=\u0022BorderThickness\u0022 Value=\u00221,1,0,0\u0022 /\u003E\n \u003CSetter Property=\u0022Padding\u0022 Value=\u002216\u0022 /\u003E\n \u003CSetter Property=\u0022BorderBrush\u0022 Value=\u0022{ThemeResource SystemControlHighlightChromeHighBrush}\u0022 /\u003E\n \u003C/Style\u003E\n\n \u003CStyle TargetType=\u0022TextBlock\u0022\u003E\n \u003CSetter Property=\u0022TextWrapping\u0022 Value=\u0022Wrap\u0022 /\u003E\n \u003C/Style\u003E\n \u003C/Grid.Resources\u003E\n \u003CGrid.RowDefinitions\u003E\n \u003CRowDefinition MinHeight=\u0022100\u0022\n MaxHeight=\u0022300\u0022 /\u003E\n \u003CRowDefinition Height=\u0022200\u0022 /\u003E\n \u003C/Grid.RowDefinitions\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition MinWidth=\u0022100\u0022\n MaxWidth=\u0022300\u0022 /\u003E\n \u003CColumnDefinition /\u003E\n \u003CColumnDefinition Width=\u0022200\u0022\n MinWidth=\u0022100\u0022 /\u003E\n \u003CColumnDefinition Width=\u0022200\u0022\n MinWidth=\u0022100\u0022 /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n\n \u003CVisualStateManager.VisualStateGroups\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022Full\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003CAdaptiveTrigger MinWindowWidth=\u0022600\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003C/VisualState\u003E\n \u003CVisualState x:Name=\u0022Small\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003CAdaptiveTrigger MinWindowWidth=\u00220\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022RootGrid.Padding\u0022 Value=\u002212\u0022 /\u003E\n \u003CSetter Target=\u0022RootGrid.FontSize\u0022 Value=\u002212\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n\n \u003CBorder Grid.Row=\u00220\u0022\n Grid.Column=\u00220\u0022\u003E\n \u003CTextBlock Text=\u0022This is a Column with a Min 100 / Max 300 width - This text to simulate the resizing feature of the Grid Splitter Control, try to move the splitter to see the effect RowDefinition MinHeight=\u0027100\u0027\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Grid.Row=\u00220\u0022\n Grid.Column=\u00221\u0022\u003E\n \u003CTextBlock Text=\u0022This text to simulate the resizing feature of the Grid Splitter Control, try to move the splitter to see the effect\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Grid.Row=\u00220\u0022\n Grid.Column=\u00222\u0022\u003E\n \u003CTextBlock Text=\u0022This text to simulate the resizing feature of the Grid Splitter Control, try to move the splitter to see the effect\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Grid.Row=\u00220\u0022\n Grid.Column=\u00223\u0022\u003E\n \u003CTextBlock Text=\u0022This text to simulate the resizing feature of the Grid Splitter Control, try to move the splitter to see the effect\u0022 /\u003E\n \u003C/Border\u003E\n\n \u003CBorder Grid.Row=\u00221\u0022\n Grid.Column=\u00220\u0022\u003E\n \u003CTextBlock Text=\u0022This text to simulate the resizing feature of the Grid Splitter Control, try to move the splitter to see the effect\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Grid.Row=\u00221\u0022\n Grid.Column=\u00221\u0022\u003E\n \u003CTextBlock Text=\u0022This text to simulate the resizing feature of the Grid Splitter Control, try to move the splitter to see the effect\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Grid.Row=\u00221\u0022\n Grid.Column=\u00222\u0022\u003E\n \u003CTextBlock Text=\u0022This is a fixed width column - This text to simulate the resizing feature of the Grid Splitter Control, try to move the splitter to see the effect\u0022 /\u003E\n \u003C/Border\u003E\n \u003CBorder Grid.Row=\u00221\u0022\n Grid.Column=\u00223\u0022\u003E\n \u003CTextBlock Text=\u0022This is a fixed width column - This text to simulate the resizing feature of the Grid Splitter Control, try to move the splitter to see the effect\u0022 /\u003E\n \u003C/Border\u003E\n\n \u003C!-- Column Grid Splitter --\u003E\n \u003Ccontrols:GridSplitter Grid.Column=\u00221\u0022\n Width=\u002216\u0022\n HorizontalAlignment=\u0022Left\u0022\n ResizeBehavior=\u0022BasedOnAlignment\u0022\n ResizeDirection=\u0022Auto\u0022\u003E\n \u003Ccontrols:GridSplitter.RenderTransform\u003E\n \u003CTranslateTransform X=\u0022-7\u0022 /\u003E\n \u003C/controls:GridSplitter.RenderTransform\u003E\n \u003C/controls:GridSplitter\u003E\n\n \u003C!-- Row Grid Splitter --\u003E\n \u003Ccontrols:GridSplitter Grid.Row=\u00221\u0022\n Grid.ColumnSpan=\u00224\u0022\n Height=\u002216\u0022\n VerticalAlignment=\u0022Top\u0022\u003E\n \u003Ccontrols:GridSplitter.RenderTransform\u003E\n \u003CTranslateTransform Y=\u0022-7\u0022 /\u003E\n \u003C/controls:GridSplitter.RenderTransform\u003E\n \u003C/controls:GridSplitter\u003E\n\n \u003C!-- Last 2 columns splitter --\u003E\n \u003Ccontrols:GridSplitter Grid.Row=\u00220\u0022\n Grid.RowSpan=\u00222\u0022\n Grid.Column=\u00223\u0022\n Width=\u002216\u0022\n HorizontalAlignment=\u0022Left\u0022\n ResizeBehavior=\u0022BasedOnAlignment\u0022\n ResizeDirection=\u0022Auto\u0022\u003E\n \u003Ccontrols:GridSplitter.RenderTransform\u003E\n \u003CTranslateTransform X=\u0022-7\u0022 /\u003E\n \u003C/controls:GridSplitter.RenderTransform\u003E\n \u003C/controls:GridSplitter\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "GridSplitterPage",
+ "sourcePath": "components/Sizers/samples/GridSplitterPage.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "propertysizer",
+ "name": "PropertySizer",
+ "description": "The PropertySizer is a control which can be used to manipulate the value of another double based property.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Sizers",
+ "curatedKeywords": [
+ "PropertySizer",
+ "SizerBase",
+ "Control",
+ "Layout",
+ "NavigationView",
+ "Splitter"
+ ],
+ "keywords": [
+ "Controls",
+ "Sizers"
+ ],
+ "docs": [
+ {
+ "title": "PropertySizer",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Sizers/samples/PropertySizer.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Sizers",
+ "documentPath": "components/Sizers/samples/PropertySizer.md",
+ "category": "Controls",
+ "subcategory": "Sizers",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "NavigationView Shelf",
+ "details": "The main use-case is for PropertySizer to allow you to manipulate the OpenPaneLength property of a NavigationView control to create a user customizable size shelf. This is handy when using NavigationView with a tree of items that represents some project or folder structure for your application. Both GridSplitter and ContentSizer are insufficient as they would force the NavigationView to a specific size and not allow it to remember its size when it expands or collapses.",
+ "xaml": "\u003Cmuxc:NavigationView x:Name=\u0022ViewPanel\u0022\n Width=\u0022500\u0022\n MinHeight=\u0022300\u0022\n HorizontalAlignment=\u0022Left\u0022\n IsPaneOpen=\u0022True\u0022\n OpenPaneLength=\u0022300\u0022\n PaneDisplayMode=\u0022Left\u0022\u003E\n \u003Cmuxc:NavigationView.AutoSuggestBox\u003E\n \u003CAutoSuggestBox AutomationProperties.Name=\u0022Search\u0022\n QueryIcon=\u0022Find\u0022 /\u003E\n \u003C/muxc:NavigationView.AutoSuggestBox\u003E\n \u003Cmuxc:NavigationView.MenuItems\u003E\n \u003Cmuxc:NavigationViewItem Content=\u0022Menu Item1\u0022\n Tag=\u0022SamplePage1\u0022\u003E\n \u003Cmuxc:NavigationViewItem.Icon\u003E\n \u003CSymbolIcon Symbol=\u0022Play\u0022 /\u003E\n \u003C/muxc:NavigationViewItem.Icon\u003E\n \u003C/muxc:NavigationViewItem\u003E\n \u003Cmuxc:NavigationViewItem Content=\u0022Menu Item2\u0022\n Tag=\u0022SamplePage2\u0022\u003E\n \u003Cmuxc:NavigationViewItem.Icon\u003E\n \u003CSymbolIcon Symbol=\u0022Save\u0022 /\u003E\n \u003C/muxc:NavigationViewItem.Icon\u003E\n \u003C/muxc:NavigationViewItem\u003E\n \u003Cmuxc:NavigationViewItem Content=\u0022Menu Item3 with Really Long Name and Such...\u0022\n Tag=\u0022SamplePage3\u0022\u003E\n \u003Cmuxc:NavigationViewItem.Icon\u003E\n \u003CSymbolIcon Symbol=\u0022Refresh\u0022 /\u003E\n \u003C/muxc:NavigationViewItem.Icon\u003E\n \u003C/muxc:NavigationViewItem\u003E\n \u003Cmuxc:NavigationViewItem Content=\u0022Menu Item4\u0022\n Tag=\u0022SamplePage4\u0022\u003E\n \u003Cmuxc:NavigationViewItem.Icon\u003E\n \u003CSymbolIcon Symbol=\u0022Download\u0022 /\u003E\n \u003C/muxc:NavigationViewItem.Icon\u003E\n \u003C/muxc:NavigationViewItem\u003E\n \u003C/muxc:NavigationView.MenuItems\u003E\n \u003CGrid\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition Width=\u0022Auto\u0022 /\u003E\n \u003CColumnDefinition Width=\u0022*\u0022 /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n \u003C!-- Note the use of a TwoWay binding here, this is required for this control to work. --\u003E\n \u003Ccontrols:PropertySizer HorizontalAlignment=\u0022Left\u0022\n Binding=\u0022{x:Bind ViewPanel.OpenPaneLength, Mode=TwoWay}\u0022\n Maximum=\u0022440\u0022\n Minimum=\u002252\u0022\n Visibility=\u0022{x:Bind ViewPanel.IsPaneOpen, Mode=OneWay}\u0022 /\u003E\n \u003C!--In An Application, put your host frame here: \u003CFrame Grid.Column=\u00221\u0022/\u003E--\u003E\n \u003C!-- Here we show the content as a border for a visual aid --\u003E\n \u003CBorder Grid.Column=\u00221\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n VerticalAlignment=\u0022Stretch\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\n VerticalAlignment=\u0022Center\u0022\n Foreground=\u0022{ThemeResource TextOnAccentFillColorPrimaryBrush}\u0022\n Text=\u0022{x:Bind ViewPanel.OpenPaneLength, Mode=OneWay}\u0022 /\u003E\n \u003C/Border\u003E\n \u003C/Grid\u003E\n\u003C/muxc:NavigationView\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "PropertySizerNavigationViewPage",
+ "sourcePath": "components/Sizers/samples/PropertySizerNavigationViewPage.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "sizercontrols",
+ "name": "Sizer overview",
+ "description": "The Sizer controls allow users to resize various parts of your UI easily in a consistent fashion.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.Sizers",
+ "curatedKeywords": [
+ "GridSplitter",
+ "ContentSizer",
+ "PropertySizer",
+ "SizerBase",
+ "Control",
+ "Layout",
+ "Expander",
+ "Grid",
+ "Splitter"
+ ],
+ "keywords": [
+ "Controls",
+ "Sizers"
+ ],
+ "docs": [
+ {
+ "title": "Sizer overview",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Sizers/samples/SizerControls.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Sizers",
+ "documentPath": "components/Sizers/samples/SizerControls.md",
+ "category": "Controls",
+ "subcategory": "Sizers",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "Custom Mouse Cursor",
+ "details": "You may want to change the cursor that is shown when hovering over your element like this:",
+ "xaml": "\u003CGrid MinWidth=\u0022400\u0022\n MinHeight=\u0022300\u0022\u003E\n \u003CGrid.ColumnDefinitions\u003E\n \u003CColumnDefinition Width=\u0022Auto\u0022 /\u003E\n \u003CColumnDefinition Width=\u0022Auto\u0022 /\u003E\n \u003CColumnDefinition /\u003E\n \u003C/Grid.ColumnDefinitions\u003E\n\n \u003CBorder x:Name=\u0022SomeContent\u0022\n MinWidth=\u0022200\u0022\n MaxWidth=\u0022600\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022 /\u003E\n \u003Ccontrols:ContentSizer Grid.Column=\u00221\u0022\n Cursor=\u0022Hand\u0022\n TargetControl=\u0022{x:Bind SomeContent}\u0022 /\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "SizerCursorPage",
+ "sourcePath": "components/Sizers/samples/SizerCursorPage.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "tabbedcommandbar",
+ "name": "TabbedCommandBar",
+ "description": "A control for displaying multiple CommandBars in the same space, like Microsoft Office\u0027s ribbon.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.TabbedCommandBar",
+ "curatedKeywords": [
+ "TabbedCommandBar",
+ "Control",
+ "Layout",
+ "commandbar",
+ "ribbon"
+ ],
+ "keywords": [
+ "Controls",
+ "Layout"
+ ],
+ "docs": [
+ {
+ "title": "TabbedCommandBar",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/TabbedCommandBar/samples/TabbedCommandBar.md"
+ }
+ ],
+ "toolkit": {
+ "component": "TabbedCommandBar",
+ "documentPath": "components/TabbedCommandBar/samples/TabbedCommandBar.md",
+ "category": "Controls",
+ "subcategory": "Layout",
+ "author": "yoshiask"
+ },
+ "samples": [
+ {
+ "header": "TabbedCommandBar",
+ "details": "The TabbedCommandBar displays a set of TabbedCommandBarItem in a shared container found in many productivity type apps. It is based off of NavigationView. TabbedCommandBarItem can be used to display certain items, and its IsContextual property can be set to change the default style into an item that is known from the Office apps to highlight to a user that certain context options are available.",
+ "xaml": "\u003CGrid VerticalAlignment=\u0022Top\u0022\u003E\n \u003Ccontrols:TabbedCommandBar\u003E\n \u003Ccontrols:TabbedCommandBar.AutoSuggestBox\u003E\n \u003CAutoSuggestBox AutomationProperties.Name=\u0022Search\u0022\n QueryIcon=\u0022Find\u0022 /\u003E\n \u003C/controls:TabbedCommandBar.AutoSuggestBox\u003E\n \u003Ccontrols:TabbedCommandBar.PaneFooter\u003E\n \u003CCommandBar Background=\u0022Transparent\u0022\n DefaultLabelPosition=\u0022Right\u0022\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE72D;}\u0022\n Label=\u0022Share\u0022 /\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE8BD;}\u0022\n Label=\u0022Comments\u0022 /\u003E\n \u003C/CommandBar\u003E\n \u003C/controls:TabbedCommandBar.PaneFooter\u003E\n \u003Ccontrols:TabbedCommandBar.MenuItems\u003E\n \u003Ccontrols:TabbedCommandBarItem Header=\u0022Home\u0022\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE7A7;}\u0022\n Label=\u0022Undo\u0022 /\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE7A6;}\u0022\n Label=\u0022Redo\u0022 /\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE77F;}\u0022\n Label=\u0022Paste\u0022 /\u003E\n \u003CAppBarSeparator /\u003E\n \u003C!-- TO DO: Enable when ColorPicker is merged --\u003E\n \u003C!--\u003CAppBarElementContainer\u003E\n \u003Ccontrols:ColorPickerButton SelectedColor=\u0022{ThemeResource Brand-Color}\u0022/\u003E\n \u003C/AppBarElementContainer\u003E--\u003E\n \u003CAppBarElementContainer\u003E\n \u003CComboBox MinWidth=\u0022175\u0022\n SelectedIndex=\u00220\u0022\u003E\n \u003CComboBoxItem Content=\u0022Arial\u0022 /\u003E\n \u003CComboBoxItem Content=\u0022Calibri\u0022 /\u003E\n \u003CComboBoxItem Content=\u0022JetBrains Mono\u0022 /\u003E\n \u003CComboBoxItem Content=\u0022Roboto\u0022 /\u003E\n \u003CComboBoxItem Content=\u0022Segoe UI\u0022 /\u003E\n \u003CComboBoxItem Content=\u0022Segoe UI Semibold\u0022 /\u003E\n \u003C/ComboBox\u003E\n \u003C/AppBarElementContainer\u003E\n \u003CAppBarElementContainer\u003E\n \u003CTextBox PlaceholderText=\u0022Size\u0022 /\u003E\n \u003C/AppBarElementContainer\u003E\n \u003CAppBarToggleButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE8DD;}\u0022\n Label=\u0022Bold\u0022 /\u003E\n \u003CAppBarToggleButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE8DB;}\u0022\n Label=\u0022Italic\u0022 /\u003E\n \u003CAppBarToggleButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE8DC;}\u0022\n Label=\u0022Underline\u0022 /\u003E\n \u003C/controls:TabbedCommandBarItem\u003E\n \u003Ccontrols:TabbedCommandBarItem Header=\u0022Insert\u0022\u003E\n \u003CAppBarButton Icon=\u0022Pictures\u0022\n Label=\u0022Pictures\u0022\u003E\n \u003CAppBarButton.Flyout\u003E\n \u003CMenuFlyout Placement=\u0022BottomEdgeAlignedLeft\u0022\u003E\n \u003CMenuFlyoutItem Icon=\u0022{ui:FontIcon Glyph=\u0026#xEC4E;}\u0022\n Text=\u0022This device\u0022 /\u003E\n \u003CMenuFlyoutItem Icon=\u0022{ui:FontIcon Glyph=\u0026#xE721;}\u0022\n Text=\u0022Stock images\u0022 /\u003E\n \u003CMenuFlyoutItem Icon=\u0022{ui:FontIcon Glyph=\u0026#xE774;}\u0022\n Text=\u0022Online pictures\u0022 /\u003E\n \u003C/MenuFlyout\u003E\n \u003C/AppBarButton.Flyout\u003E\n \u003C/AppBarButton\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xF156;}\u0022\n Label=\u0022Shapes\u0022 /\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xED58;}\u0022\n Label=\u0022Icons\u0022 /\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xF158;}\u0022\n Label=\u00223D Models\u0022 /\u003E\n \u003CAppBarSeparator /\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xECAA;}\u0022\n Label=\u0022Add-ins\u0022 /\u003E\n \u003Ccontrols:TabbedCommandBarItem.SecondaryCommands\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE710;}\u0022\n Label=\u0022New item\u0022 /\u003E\n \u003C/controls:TabbedCommandBarItem.SecondaryCommands\u003E\n \u003C/controls:TabbedCommandBarItem\u003E\n \u003Ccontrols:TabbedCommandBarItem x:Name=\u0022PictureFormat\u0022\n Header=\u0022Picture Format\u0022\n IsContextual=\u0022True\u0022\n Visibility=\u0022True\u0022\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE706;}\u0022\n Label=\u0022Remove background\u0022 /\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xF4A5;}\u0022\n Label=\u0022Picture effects\u0022 /\u003E\n \u003CAppBarButton Icon=\u0022{ui:FontIcon Glyph=\u0026#xE7AD;}\u0022\n Label=\u0022Rotate\u0022 /\u003E\n \u003CAppBarElementContainer\u003E\n \u003Cmuxc:SplitButton\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\n Spacing=\u002212\u0022\u003E\n \u003CFontIcon FontFamily=\u0022{ThemeResource SymbolThemeFontFamily}\u0022\n FontSize=\u002216\u0022\n Glyph=\u0022\u0026#xE7A8;\u0022 /\u003E\n \u003CTextBlock FontSize=\u002212\u0022\n Text=\u0022Crop\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003Cmuxc:SplitButton.Flyout\u003E\n \u003CMenuFlyout\u003E\n \u003CMenuFlyoutItem Icon=\u0022{ui:FontIcon Glyph=\u0026#xE7A8;}\u0022\n Text=\u0022Crop\u0022 /\u003E\n \u003CMenuFlyoutItem Icon=\u0022{ui:FontIcon Glyph=\u0026#xF407;}\u0022\n Text=\u0022Crop to Shape\u0022 /\u003E\n \u003CMenuFlyoutItem Text=\u0022Aspect Ratio\u0022 /\u003E\n \u003CMenuFlyoutSeparator /\u003E\n \u003CMenuFlyoutItem Text=\u0022Fill\u0022 /\u003E\n \u003CMenuFlyoutItem Text=\u0022Fit\u0022 /\u003E\n \u003C/MenuFlyout\u003E\n \u003C/muxc:SplitButton.Flyout\u003E\n \u003C/muxc:SplitButton\u003E\n \u003C/AppBarElementContainer\u003E\n \u003C/controls:TabbedCommandBarItem\u003E\n \u003C/controls:TabbedCommandBar.MenuItems\u003E\n \u003C/controls:TabbedCommandBar\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:muxc=\u0022using:Microsoft.UI.Xaml.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "TabbedCommandBarSample",
+ "sourcePath": "components/TabbedCommandBar/samples/TabbedCommandBarSample.xaml",
+ "optionsResolved": [
+ "ContextualItem=True"
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "tokenizingtextbox",
+ "name": "TokenizingTextBox",
+ "description": "A text input control that auto-suggests and displays token items.",
+ "nugetPackage": "CommunityToolkit.WinUI.Controls.TokenizingTextBox",
+ "usings": [
+ "CommunityToolkit.WinUI.Controls"
+ ],
+ "curatedKeywords": [
+ "TokenizingTextBox",
+ "control",
+ "tokens"
+ ],
+ "keywords": [
+ "Controls",
+ "Text"
+ ],
+ "docs": [
+ {
+ "title": "TokenizingTextBox",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/TokenizingTextBox/samples/TokenizingTextBox.md"
+ }
+ ],
+ "toolkit": {
+ "component": "TokenizingTextBox",
+ "documentPath": "components/TokenizingTextBox/samples/TokenizingTextBox.md",
+ "category": "Controls",
+ "subcategory": "Text",
+ "author": "michael-hawker"
+ },
+ "samples": [
+ {
+ "header": "Basic sample",
+ "details": "The TokenizingTextBox is an advanced AutoSuggestBox which will display selected items as tokens within the textbox. A user can easily see the picked items or remove them easily.",
+ "xaml": "\u003CStackPanel Orientation=\u0022Vertical\u0022\n Spacing=\u00224\u0022\u003E\n \u003Ccontrols:TokenizingTextBox x:Name=\u0022TokenBox\u0022\n MaxWidth=\u0022620\u0022\n HorizontalAlignment=\u0022Left\u0022\n ItemClick=\u0022TokenBox_ItemClick\u0022\n ItemsSource=\u0022{x:Bind SelectedTokens, Mode=TwoWay}\u0022\n Loaded=\u0022TokenBox_Loaded\u0022\n MaximumTokens=\u00225\u0022\n PlaceholderText=\u0022Add actions\u0022\n QueryIcon=\u0022{ui:FontIconSource Glyph=\u0026#xE721;,\n FontSize=12}\u0022\n SuggestedItemsSource=\u0022{x:Bind _samples, Mode=OneWay}\u0022\n TextChanged=\u0022TextChanged\u0022\n TextMemberPath=\u0022Text\u0022\n TokenDelimiter=\u0022,\u0022\n TokenItemAdding=\u0022TokenItemCreating\u0022\u003E\n \u003Ccontrols:TokenizingTextBox.Header\u003E\n \u003CTextBlock\u003E\n \u003CRun Text=\u0022Start typing and select up to\u0022 /\u003E\n \u003CRun FontWeight=\u0022SemiBold\u0022\n Text=\u0022{Binding MaximumTokens, ElementName=TokenBox, Mode=OneWay}\u0022 /\u003E\n \u003CRun Text=\u0022actions\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/controls:TokenizingTextBox.Header\u003E\n \u003Ccontrols:TokenizingTextBox.SuggestedItemTemplate\u003E\n \u003CDataTemplate\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\u003E\n \u003CViewbox Width=\u002216\u0022\u003E\n \u003CSymbolIcon Symbol=\u0022{Binding Icon}\u0022 /\u003E\n \u003C/Viewbox\u003E\n \u003CTextBlock Padding=\u00228,0,0,0\u0022\n Text=\u0022{Binding Text}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:TokenizingTextBox.SuggestedItemTemplate\u003E\n \u003Ccontrols:TokenizingTextBox.TokenItemTemplate\u003E\n \u003CDataTemplate\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\u003E\n \u003CViewbox Width=\u002216\u0022\u003E\n \u003CSymbolIcon Symbol=\u0022{Binding Icon}\u0022 /\u003E\n \u003C/Viewbox\u003E\n\n \u003CTextBlock Padding=\u00228,0,0,0\u0022\n Text=\u0022{Binding Text}\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003C/DataTemplate\u003E\n \u003C/controls:TokenizingTextBox.TokenItemTemplate\u003E\n \u003C/controls:TokenizingTextBox\u003E\n\n \u003CTextBlock Margin=\u00220,24,0,0\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u0022Text:\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022currentEdit\u0022 /\u003E\n\n \u003CTextBlock Margin=\u00220,24,0,0\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u0022SelectedTokenText:\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022selectedItemsString\u0022 /\u003E\n\n \u003CTextBlock Margin=\u00220,24,0,0\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u0022Items:\u0022 /\u003E\n \u003CItemsControl ItemsSource=\u0022{x:Bind SelectedTokens, Mode=OneWay}\u0022 /\u003E\n\n \u003CTextBlock Margin=\u00220,24,0,0\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u0022Clicked item:\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022clickedItem\u0022 /\u003E\n\u003C/StackPanel\u003E",
+ "code": "public readonly List\u003CSampleDataType\u003E _samples = new List\u003CSampleDataType\u003E()\n{\n new SampleDataType()\n {\n Text = \u0022Account\u0022,\n Icon = Symbol.Account\n },\n new SampleDataType()\n {\n Text = \u0022Add friend\u0022,\n Icon = Symbol.AddFriend\n },\n new SampleDataType()\n {\n Text = \u0022Attach\u0022,\n Icon = Symbol.Attach\n },\n new SampleDataType()\n {\n Text = \u0022Attach camera\u0022,\n Icon = Symbol.AttachCamera\n },\n new SampleDataType()\n {\n Text = \u0022Audio\u0022,\n Icon = Symbol.Audio\n },\n new SampleDataType()\n {\n Text = \u0022Block contact\u0022,\n Icon = Symbol.BlockContact\n },\n new SampleDataType()\n {\n Text = \u0022Calculator\u0022,\n Icon = Symbol.Calculator\n },\n new SampleDataType()\n {\n Text = \u0022Calendar\u0022,\n Icon = Symbol.Calendar\n },\n new SampleDataType()\n {\n Text = \u0022Camera\u0022,\n Icon = Symbol.Camera\n },\n new SampleDataType()\n {\n Text = \u0022Contact\u0022,\n Icon = Symbol.Contact\n },\n new SampleDataType()\n {\n Text = \u0022Favorite\u0022,\n Icon = Symbol.Favorite\n },\n new SampleDataType()\n {\n Text = \u0022Link\u0022,\n Icon = Symbol.Link\n },\n new SampleDataType()\n {\n Text = \u0022Mail\u0022,\n Icon = Symbol.Mail\n },\n new SampleDataType()\n {\n Text = \u0022Map\u0022,\n Icon = Symbol.Map\n },\n new SampleDataType()\n {\n Text = \u0022Phone\u0022,\n Icon = Symbol.Phone\n },\n new SampleDataType()\n {\n Text = \u0022Pin\u0022,\n Icon = Symbol.Pin\n },\n new SampleDataType()\n {\n Text = \u0022Rotate\u0022,\n Icon = Symbol.Rotate\n },\n new SampleDataType()\n {\n Text = \u0022Rotate camera\u0022,\n Icon = Symbol.RotateCamera\n },\n new SampleDataType()\n {\n Text = \u0022Send\u0022,\n Icon = Symbol.Send\n },\n new SampleDataType()\n {\n Text = \u0022Tags\u0022,\n Icon = Symbol.Tag\n },\n new SampleDataType()\n {\n Text = \u0022UnFavorite\u0022,\n Icon = Symbol.UnFavorite\n },\n new SampleDataType()\n {\n Text = \u0022UnPin\u0022,\n Icon = Symbol.UnPin\n },\n new SampleDataType()\n {\n Text = \u0022Zoom\u0022,\n Icon = Symbol.Zoom\n },\n new SampleDataType()\n {\n Text = \u0022ZoomIn\u0022,\n Icon = Symbol.ZoomIn\n },\n new SampleDataType()\n {\n Text = \u0022ZoomOut\u0022,\n Icon = Symbol.ZoomOut\n },\n};\n\npublic ObservableCollection\u003CSampleDataType\u003E SelectedTokens { get; set; }\n\npublic TokenizingTextBoxSample()\n{\n SelectedTokens = new()\n {\n _samples[0],\n _samples[1]\n };\n}\n\nprivate void TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)\n{\n currentEdit.Text = TokenBox.Text;\n SetSelectedTokenText();\n}\n\nprivate void SetSelectedTokenText()\n{\n selectedItemsString.Text = TokenBox.SelectedTokenText;\n}\n\nprivate void TokenItemCreating(object sender, TokenItemAddingEventArgs e)\n{\n // Take the user\u0027s text and convert it to our data type (if we have a matching one).\n e.Item = _samples.FirstOrDefault((item) =\u003E item.Text!.Contains(e.TokenText, StringComparison.CurrentCultureIgnoreCase));\n // Otherwise, create a new version of our data type\n if (e.Item == null)\n {\n e.Item = new SampleDataType()\n {\n Text = e.TokenText,\n Icon = Symbol.OutlineStar\n };\n }\n}\n\nprivate void TokenBox_ItemClick(object sender, ItemClickEventArgs e)\n{\n if (e.ClickedItem is SampleDataType selectedItem)\n {\n clickedItem.Text = selectedItem.Text!;\n }\n}\n\nprivate void TokenBox_Loaded(object sender, RoutedEventArgs e)\n{\n SetSelectedTokenText();\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:controls=\u0022using:CommunityToolkit.WinUI.Controls\u0022",
+ "xmlns:ui=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "TokenizingTextBoxSample",
+ "sourcePath": "components/TokenizingTextBox/samples/TokenizingTextBoxSample.xaml"
+ }
+ }
+ ]
+ },
+ {
+ "id": "triggers",
+ "name": "Triggers",
+ "description": "A collection of custom visual State Triggers",
+ "nugetPackage": "CommunityToolkit.WinUI.Triggers",
+ "curatedKeywords": [
+ "CompareStateTrigger",
+ "ControlSizeTrigger",
+ "FullScreenModeStateTrigger",
+ "IsEqualStateTrigger",
+ "IsNotEqualStateTrigger",
+ "IsNullOrEmptyStateTriggers",
+ "NetworkConnectionStateTrigger",
+ "RegexStateTrigger",
+ "UserHandPreferenceStateTrigger",
+ "UserInteractionModeStateTrigger"
+ ],
+ "keywords": [
+ "Xaml",
+ "Miscellaneous"
+ ],
+ "docs": [
+ {
+ "title": "Triggers",
+ "uri": "https://github.com/CommunityToolkit/Windows/blob/main/components/Triggers/samples/Triggers.md"
+ }
+ ],
+ "toolkit": {
+ "component": "Triggers",
+ "documentPath": "components/Triggers/samples/Triggers.md",
+ "category": "Xaml",
+ "subcategory": "Miscellaneous",
+ "author": "dotMorten"
+ },
+ "samples": [
+ {
+ "header": "CompareStateTrigger",
+ "details": "Enables a state if the value is equal to, greater than, or less than another value.",
+ "xaml": "\u003CGrid\u003E\n \u003CVisualStateManager.VisualStateGroups\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022Less\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:CompareStateTrigger Comparison=\u0022LessThan\u0022\n Value=\u0022{Binding Value, ElementName=Slider, Mode=OneWay}\u0022\n To=\u00223\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022SliderStatus.Text\u0022 Value=\u0022Slider value is less than 3\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003CVisualState x:Name=\u0022Equal\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:CompareStateTrigger Comparison=\u0022Equal\u0022\n Value=\u0022{Binding Value, ElementName=Slider, Mode=OneWay}\u0022\n To=\u00223\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022SliderStatus.Text\u0022 Value=\u0022Slider value is 3\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003CVisualState x:Name=\u0022Greater\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:CompareStateTrigger Comparison=\u0022GreaterThan\u0022\n Value=\u0022{Binding Value, ElementName=Slider, Mode=OneWay}\u0022\n To=\u00223\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022SliderStatus.Text\u0022 Value=\u0022Slider value is greater than 3\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022GreaterThanOrEqual\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:CompareStateTrigger Comparison=\u0022GreaterThanOrEqual\u0022\n Value=\u0022{Binding Value, ElementName=Slider2, Mode=OneWay}\u0022\n To=\u00224\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022SliderStatus2.Text\u0022 Value=\u0022Slider value is 4 or greater\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003CVisualState x:Name=\u0022LessThanOrEqual\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:CompareStateTrigger Comparison=\u0022LessThanOrEqual\u0022\n Value=\u0022{Binding Value, ElementName=Slider2, Mode=OneWay}\u0022\n To=\u00222\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022SliderStatus2.Text\u0022 Value=\u0022Slider value is 2 or less\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n\n \u003CStackPanel MaxWidth=\u0022400\u0022\u003E\n \u003CSlider x:Name=\u0022Slider\u0022\n Maximum=\u00225\u0022\n Minimum=\u00220\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022SliderStatus\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022If you see this, trigger isn\u0027t working\u0022 /\u003E\n \u003CSlider x:Name=\u0022Slider2\u0022\n Margin=\u00220,36,0,0\u0022\n Maximum=\u00225\u0022\n Minimum=\u00220\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022SliderStatus2\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022Slider value is 3\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:triggers=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "CompareStateTriggerSample",
+ "sourcePath": "components/Triggers/samples/CompareStateTriggerSample.xaml"
+ }
+ },
+ {
+ "header": "ControlStateTrigger",
+ "details": "Enables a state if the target control meets the specified size",
+ "xaml": "\u003CGrid\u003E\n \u003CVisualStateManager.VisualStateGroups\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:ControlSizeTrigger MinWidth=\u0022400\u0022\n MaxWidth=\u0022501\u0022\n TargetElement=\u0022{Binding ElementName=ParentGrid}\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022ResizingText.Text\u0022 Value=\u0022more than 400\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n\n \u003CStackPanel MaxWidth=\u0022400\u0022\u003E\n\n \u003CSlider x:Name=\u0022Slider\u0022\n Maximum=\u0022500\u0022\n Minimum=\u00221\u0022 /\u003E\n \u003CGrid x:Name=\u0022ParentGrid\u0022\n Width=\u0022{Binding Value, ElementName=Slider, Mode=OneWay}\u0022\n Height=\u002232\u0022\n Background=\u0022{ThemeResource AccentFillColorDefaultBrush}\u0022\n CornerRadius=\u0022{StaticResource ControlCornerRadius}\u0022 /\u003E\n\n \u003CTextBlock Margin=\u00220,12,0,0\u0022\n HorizontalAlignment=\u0022Center\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\u003E\n \u003CRun Text=\u0022Control size is\u0022 /\u003E\n \u003CRun x:Name=\u0022ResizingText\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u0022less than 400\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:triggers=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "ControlSizeTriggerSample",
+ "sourcePath": "components/Triggers/samples/ControlSizeTriggerSample.xaml"
+ }
+ },
+ {
+ "header": "IsEqualStateTrigger",
+ "details": "Enables a state if the value is equal to another value.",
+ "xaml": "\u003CGrid\u003E\n \u003CVisualStateManager.VisualStateGroups\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022checkstate\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003C!-- Checkbox is null when indeterminate --\u003E\n \u003Ctriggers:IsEqualStateTrigger Value=\u0022{Binding IsChecked, ElementName=checkbox, Mode=OneWay}\u0022\n To=\u0022{x:Null}\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022checkStatus.Text\u0022 Value=\u0022indeterminate\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022sliderstate\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:IsEqualStateTrigger Value=\u0022{Binding Value, ElementName=slider, Mode=OneWay}\u0022\n To=\u00220\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022sliderStatus.Text\u0022 Value=\u00220\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n\n \u003CStackPanel MaxWidth=\u0022400\u0022\u003E\n \u003CCheckBox x:Name=\u0022checkbox\u0022\n Content=\u0022Check me\u0022\n IsChecked=\u0022True\u0022\n IsThreeState=\u0022True\u0022 /\u003E\n \u003CTextBlock Margin=\u00220,8,0,0\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\u003E\n \u003CRun Text=\u0022Checkbox value is\u0022 /\u003E\n \u003CRun x:Name=\u0022checkStatus\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u0022not indeterminate\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CSlider x:Name=\u0022slider\u0022\n Margin=\u00220,36,0,0\u0022\n Maximum=\u00225\u0022\n Minimum=\u00220\u0022 /\u003E\n \u003CTextBlock Margin=\u00220,8,0,0\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\u003E\n\n \u003CRun Text=\u0022Slider value is\u0022 /\u003E\n \u003CRun x:Name=\u0022sliderStatus\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u0022more than 0\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:triggers=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "IsEqualStateTriggerSample",
+ "sourcePath": "components/Triggers/samples/IsEqualStateTriggerSample.xaml"
+ }
+ },
+ {
+ "header": "IsNotEqualStateTrigger",
+ "details": "Enables a state if the value is not equal to another value.",
+ "xaml": "\u003CGrid\u003E\n \u003CVisualStateManager.VisualStateGroups\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022checkstate\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003C!-- Checkbox is null when indeterminate --\u003E\n \u003Ctriggers:IsNotEqualStateTrigger Value=\u0022{Binding IsChecked, ElementName=checkbox, Mode=OneWay}\u0022\n To=\u0022{x:Null}\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022checkStatus.Text\u0022 Value=\u0022not indeterminate\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022sliderstate\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:IsNotEqualStateTrigger Value=\u0022{Binding Value, ElementName=slider, Mode=OneWay}\u0022\n To=\u00220\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022sliderStatus.Text\u0022 Value=\u0022not 0\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n\n \u003CStackPanel MaxWidth=\u0022400\u0022\u003E\n \u003CCheckBox x:Name=\u0022checkbox\u0022\n Content=\u0022Check me\u0022\n IsChecked=\u0022True\u0022\n IsThreeState=\u0022True\u0022 /\u003E\n \u003CTextBlock Margin=\u00220,8,0,0\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\u003E\n \u003CRun Text=\u0022Checkbox value is\u0022 /\u003E\n \u003CRun x:Name=\u0022checkStatus\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u0022indeterminate\u0022 /\u003E\n \u003C/TextBlock\u003E\n\n \u003CSlider x:Name=\u0022slider\u0022\n Margin=\u00220,36,0,0\u0022\n Maximum=\u00225\u0022\n Minimum=\u00220\u0022 /\u003E\n \u003CTextBlock Margin=\u00220,8,0,0\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\u003E\n\n \u003CRun Text=\u0022Slider value is\u0022 /\u003E\n \u003CRun x:Name=\u0022sliderStatus\u0022\n FontWeight=\u0022SemiBold\u0022\n Text=\u00220\u0022 /\u003E\n \u003C/TextBlock\u003E\n \u003C/StackPanel\u003E\n\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:triggers=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "IsNotEqualStateTriggerSample",
+ "sourcePath": "components/Triggers/samples/IsNotEqualStateTriggerSample.xaml"
+ }
+ },
+ {
+ "header": "IsNullOrEmptyStateTrigger",
+ "details": "Enables a state if an Object is null or a String/IEnumerable is empty.",
+ "xaml": "\u003CGrid\u003E\n \u003CVisualStateManager.VisualStateGroups\u003E\n\n \u003CVisualStateGroup x:Name=\u0022TextBoxStates\u0022\u003E\n \u003CVisualState x:Name=\u0022TextBoxNotEmptyState\u0022 /\u003E\n \u003CVisualState x:Name=\u0022TextBoxEmptyState\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:IsNullOrEmptyStateTrigger Value=\u0022{Binding Text, ElementName=OurTextBox, Mode=OneWay}\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022OurTextBox.BorderBrush\u0022 Value=\u0022Red\u0022 /\u003E\n \u003CSetter Target=\u0022OurTextBoxError.Visibility\u0022 Value=\u0022Visible\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003CVisualStateGroup x:Name=\u0022ListBoxStates\u0022\u003E\n \u003CVisualState x:Name=\u0022ListNotEmptyState\u0022 /\u003E\n \u003CVisualState x:Name=\u0022ListEmptyState\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:IsNullOrEmptyStateTrigger Value=\u0022{Binding Items, ElementName=OurList, Mode=OneWay}\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022RemoveButton.IsEnabled\u0022 Value=\u0022False\u0022 /\u003E\n \u003CSetter Target=\u0022ListEmptyMessage.Visibility\u0022 Value=\u0022Visible\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n\n \u003CStackPanel HorizontalAlignment=\u0022Center\u0022\n Spacing=\u002212\u0022\u003E\n \u003CTextBox x:Name=\u0022OurTextBox\u0022\n Header=\u0022The TextBox below will warn if it is empty.\u0022\n Text=\u0022\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022OurTextBoxError\u0022\n VerticalAlignment=\u0022Center\u0022\n Foreground=\u0022{ThemeResource SystemFillColorCriticalBrush}\u0022\n Text=\u0022* required\u0022\n Visibility=\u0022Collapsed\u0022 /\u003E\n\n \u003CTextBlock x:Name=\u0022ListEmptyMessage\u0022\n Margin=\u00220,24,0,0\u0022\n Text=\u0022List is empty, add some items\u0022\n Visibility=\u0022Collapsed\u0022 /\u003E\n \u003CStackPanel Orientation=\u0022Horizontal\u0022\u003E\n \u003CButton x:Name=\u0022AddButton\u0022\n Margin=\u00220,0,4,0\u0022\n Click=\u0022AddButton_Click\u0022\n Content=\u0022Add\u0022 /\u003E\n \u003CButton x:Name=\u0022RemoveButton\u0022\n Click=\u0022RemoveButton_Click\u0022\n Content=\u0022Remove\u0022 /\u003E\n \u003C/StackPanel\u003E\n \u003CListView x:Name=\u0022OurList\u0022\n HorizontalAlignment=\u0022Left\u0022\u003E\n \u003CListView.ItemTemplate\u003E\n \u003CDataTemplate x:DataType=\u0022x:String\u0022\u003E\n \u003CGrid\u003E\n \u003CTextBlock Text=\u0022{Binding}\u0022 /\u003E\n \u003C/Grid\u003E\n \u003C/DataTemplate\u003E\n \u003C/ListView.ItemTemplate\u003E\n \u003C/ListView\u003E\n \u003C/StackPanel\u003E\n\n\u003C/Grid\u003E",
+ "code": "private void AddButton_Click(object sender, RoutedEventArgs e)\n{\n if (OurList != null)\n {\n OurList.Items.Add(\u0022Item\u0022);\n }\n}\n\nprivate void RemoveButton_Click(object sender, RoutedEventArgs e)\n{\n if (OurList != null)\n {\n OurList.Items.RemoveAt(0);\n }\n}",
+ "language": "csharp",
+ "xmlnsImports": [
+ "xmlns:triggers=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "IsNullOrEmptyStateTriggerSample",
+ "sourcePath": "components/Triggers/samples/IsNullOrEmptyStateTriggerSample.xaml"
+ }
+ },
+ {
+ "header": "NetworkConnectionStateTrigger",
+ "details": "Trigger for switching when the network availability changes.",
+ "xaml": "\u003CGrid\u003E\n \u003CVisualStateManager.VisualStateGroups\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022connected\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:NetworkConnectionStateTrigger ConnectionState=\u0022Connected\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022connectionStatus.Text\u0022 Value=\u0022Internet is available\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003CVisualState x:Name=\u0022disconnected\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:NetworkConnectionStateTrigger ConnectionState=\u0022Disconnected\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022connectionStatus.Text\u0022 Value=\u0022No internet connection\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n \u003CTextBlock HorizontalAlignment=\u0022Center\u0022\u003E\n \u003CRun Text=\u0022Connection status:\u0022 /\u003E\n \u003CRun x:Name=\u0022connectionStatus\u0022\n FontWeight=\u0022SemiBold\u0022 /\u003E\n \u003C/TextBlock\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:triggers=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "NetworkConnectionStateTriggerSample",
+ "sourcePath": "components/Triggers/samples/NetworkConnectionStateTriggerSample.xaml"
+ }
+ },
+ {
+ "header": "RegexStateTrigger",
+ "details": "Enables a state if the regex expression is true for a given string value.",
+ "xaml": "\u003CGrid\u003E\n \u003CVisualStateManager.VisualStateGroups\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022validEmail\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003C!-- Note: Simple example RegEx, see our IsEmail string extension using emailregex.com for official RFC 5322 support --\u003E\n \u003Ctriggers:RegexStateTrigger Expression=\u0022^[A-Z0-9._%\u002B-]\u002B@[A-Z0-9.-]\u002B\\.[A-Z]{2,4}$\u0022\n Options=\u0022IgnoreCase\u0022\n Value=\u0022{Binding Text, ElementName=emailTextBox, Mode=OneWay}\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022emailStatus.Text\u0022 Value=\u0022This is a valid email\u0022 /\u003E\n \u003CSetter Target=\u0022emailStatus.Foreground\u0022 Value=\u0022{ThemeResource SystemFillColorSuccessBrush}\u0022 /\u003E\n \u003CSetter Target=\u0022submitButton.IsEnabled\u0022 Value=\u0022true\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n\n \u003CStackPanel MaxWidth=\u0022400\u0022\u003E\n \u003CTextBox x:Name=\u0022emailTextBox\u0022\n HorizontalAlignment=\u0022Stretch\u0022\n Header=\u0022Enter an email\u0022 /\u003E\n \u003CTextBlock x:Name=\u0022emailStatus\u0022\n Margin=\u00220,4,0,24\u0022\n Foreground=\u0022{ThemeResource SystemFillColorCriticalBrush}\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022Not a valid email\u0022 /\u003E\n \u003CButton x:Name=\u0022submitButton\u0022\n Content=\u0022Submit\u0022\n IsEnabled=\u0022False\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:triggers=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "RegexStateTriggerSample",
+ "sourcePath": "components/Triggers/samples/RegexStateTriggerSample.xaml"
+ }
+ },
+ {
+ "header": "UserHandPreferenceStateTrigger",
+ "details": "Trigger for switching UI based on whether the user favors their left or right hand.",
+ "xaml": "\u003CGrid\u003E\n \u003CVisualStateManager.VisualStateGroups\u003E\n \u003CVisualStateGroup\u003E\n \u003CVisualState x:Name=\u0022leftHanded\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:UserHandPreferenceStateTrigger HandPreference=\u0022LeftHanded\u0022 /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022handPreferenceStatus.Text\u0022 Value=\u0022You are left-handed\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003CVisualState x:Name=\u0022rightHanded\u0022\u003E\n \u003CVisualState.StateTriggers\u003E\n \u003Ctriggers:UserHandPreferenceStateTrigger /\u003E\n \u003C/VisualState.StateTriggers\u003E\n \u003CVisualState.Setters\u003E\n \u003CSetter Target=\u0022handPreferenceStatus.Text\u0022 Value=\u0022You are right-handed\u0022 /\u003E\n \u003C/VisualState.Setters\u003E\n \u003C/VisualState\u003E\n \u003C/VisualStateGroup\u003E\n \u003C/VisualStateManager.VisualStateGroups\u003E\n\n \u003CStackPanel HorizontalAlignment=\u0022Center\u0022\u003E\n \u003CTextBlock x:Name=\u0022handPreferenceStatus\u0022\n HorizontalAlignment=\u0022Center\u0022\n FontWeight=\u0022SemiBold\u0022 /\u003E\n \u003CTextBlock Margin=\u00220,12\u0022\n HorizontalAlignment=\u0022Center\u0022\n Style=\u0022{StaticResource CaptionTextBlockStyle}\u0022\n Text=\u0022Set your hand preference in Windows settings (Devices -\u0026gt; Pen) then restart the app.\u0022\n TextWrapping=\u0022WrapWholeWords\u0022 /\u003E\n \u003C/StackPanel\u003E\n\u003C/Grid\u003E",
+ "xmlnsImports": [
+ "xmlns:triggers=\u0022using:CommunityToolkit.WinUI\u0022"
+ ],
+ "toolkit": {
+ "sampleId": "UserHandPreferenceStateTriggerSample",
+ "sourcePath": "components/Triggers/samples/UserHandPreferenceStateTriggerSample.xaml"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/tools/Directory.Build.props b/tools/Directory.Build.props
new file mode 100644
index 00000000..8912fe8f
--- /dev/null
+++ b/tools/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
diff --git a/tools/SampleIndexExporter.Tests/ContractConformanceTests.cs b/tools/SampleIndexExporter.Tests/ContractConformanceTests.cs
new file mode 100644
index 00000000..e1b422cb
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/ContractConformanceTests.cs
@@ -0,0 +1,224 @@
+// 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.Text.Json;
+using System.Text.RegularExpressions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.SampleIndex.Tests;
+
+///
+/// The contract gate: the published file has to stay usable by consumers that already read it.
+///
+///
+/// The index is a public artifact fetched from this repository's main branch. Anything checked
+/// here is something a consumer is entitled to rely on without defensive code: ids that are
+/// stable and safe in a URL, names that are present, a version it can branch on.
+///
+[TestClass]
+public class ContractConformanceTests
+{
+ [TestMethod]
+ public void IndexDeclaresItsSourceAndVersion()
+ {
+ // Read back from the committed file rather than the object that produced it. A consumer
+ // branches on these two fields before it reads anything else, so what matters is that
+ // they survive serialization under the field names the contract publishes — asserting
+ // against the in-memory defaults would only restate their initializers.
+ using var document = JsonDocument.Parse(RepositoryIndex.CommittedJson);
+
+ Assert.AreEqual(1, document.RootElement.GetProperty("schemaVersion").GetInt32());
+ Assert.AreEqual("toolkit", document.RootElement.GetProperty("source").GetString());
+ }
+
+ [TestMethod]
+ public void EntryIdsAreUniqueAndUrlSafe()
+ {
+ var ids = RepositoryIndex.Index.Controls.Select(c => c.Id).ToList();
+
+ CollectionAssert.AllItemsAreUnique(ids, "Entry ids must be unique; a consumer keys on them.");
+
+ var unsafeIds = ids
+ .Where(id => !id.All(c => char.IsAsciiLetterLower(c) || char.IsAsciiDigit(c) || c == '-'))
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ unsafeIds.Count,
+ "Entry ids must be lowercase letters, digits and hyphens:\n " + string.Join("\n ", unsafeIds));
+ }
+
+ [TestMethod]
+ public void EntryIdDependsOnlyOnItsOwnDocumentFileName()
+ {
+ // An id is published as stable, so it has to be a function of the entry's own document
+ // and nothing else. Deriving it from the set of entries present — qualifying whichever
+ // of two colliding file names happened to be read second — would silently rename an
+ // entry that already shipped the day an unrelated component was added. A collision is
+ // reported as an error instead, so this invariant holds by construction.
+ var unexpected = RepositoryIndex.Index.Controls
+ .Where(c => c.Toolkit?.DocumentPath is { } path && c.Id != ExpectedId(path))
+ .Select(c => $"{c.Id} (expected '{ExpectedId(c.Toolkit!.DocumentPath!)}' from {c.Toolkit!.DocumentPath})")
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ unexpected.Count,
+ "Entry ids must be derived from their own documentation file name alone:\n "
+ + string.Join("\n ", unexpected));
+ }
+
+ private static string ExpectedId(string documentPath) =>
+ Regex.Replace(Path.GetFileNameWithoutExtension(documentPath).ToLowerInvariant(), "[^a-z0-9]+", "-").Trim('-');
+
+ [TestMethod]
+ public void EveryEntryHasANameAndSourceDocument()
+ {
+ var incomplete = RepositoryIndex.Index.Controls
+ .Where(c => string.IsNullOrWhiteSpace(c.Name) || string.IsNullOrWhiteSpace(c.Toolkit?.DocumentPath))
+ .Select(c => c.Id)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ incomplete.Count,
+ "Entries missing a name or source document:\n " + string.Join("\n ", incomplete));
+ }
+
+ [TestMethod]
+ public void EveryEntryLinksToItsDocumentation()
+ {
+ var unlinked = RepositoryIndex.Index.Controls
+ .Where(c => c.Docs is null || c.Docs.Count == 0 || c.Docs.Any(d => string.IsNullOrWhiteSpace(d.Uri)))
+ .Select(c => c.Id)
+ .ToList();
+
+ Assert.AreEqual(0, unlinked.Count, "Entries with no documentation link:\n " + string.Join("\n ", unlinked));
+ }
+
+ [TestMethod]
+ public void EverySampleHasAHeader()
+ {
+ // The header is what a consumer shows in a picker. Without it the entry is a body of
+ // markup the reader has to decode before they can tell whether they want it.
+ var headerless = RepositoryIndex.Samples
+ .Where(s => string.IsNullOrWhiteSpace(s.Sample.Header))
+ .Select(s => s.Sample.Toolkit!.SourcePath)
+ .ToList();
+
+ Assert.AreEqual(0, headerless.Count, "Samples with no header:\n " + string.Join("\n ", headerless));
+ }
+
+ [TestMethod]
+ public void EverySampleRecordsWhereItCameFrom()
+ {
+ var untraceable = RepositoryIndex.Samples
+ .Where(s => string.IsNullOrWhiteSpace(s.Sample.Toolkit?.SourcePath)
+ || string.IsNullOrWhiteSpace(s.Sample.Toolkit?.SampleId))
+ .Select(s => $"{s.Control.Id}: {s.Sample.Header}")
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ untraceable.Count,
+ "Samples that do not say which file they came from:\n " + string.Join("\n ", untraceable));
+ }
+
+ [TestMethod]
+ public void EveryNuGetPackageNameLooksLikeAPackage()
+ {
+ var suspicious = RepositoryIndex.Index.Controls
+ .Where(c => c.NuGetPackage is { } package
+ && !package.StartsWith("CommunityToolkit.", StringComparison.Ordinal))
+ .Select(c => $"{c.Id}: {c.NuGetPackage}")
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ suspicious.Count,
+ "Entries naming a package that is not a toolkit package:\n " + string.Join("\n ", suspicious));
+ }
+
+ [TestMethod]
+ public void EveryPublishedUsingIsANamespaceAConsumerCanWriteOut()
+ {
+ // Read back from the committed file: a consumer builds 'using {value};' lines straight
+ // from this array, so anything that is not a namespace name becomes a syntax error in
+ // the reader's file rather than a missing import they could work around.
+ using var document = JsonDocument.Parse(RepositoryIndex.CommittedJson);
+
+ var malformed = new List();
+
+ foreach (var control in document.RootElement.GetProperty("controls").EnumerateArray())
+ {
+ if (!control.TryGetProperty("usings", out var usings))
+ {
+ continue;
+ }
+
+ malformed.AddRange(usings
+ .EnumerateArray()
+ .Select(u => u.GetString())
+ .Where(u => u is null || !Regex.IsMatch(u, @"^[A-Za-z_]\w*(\.[A-Za-z_]\w*)*$"))
+ .Select(u => $"{control.GetProperty("id").GetString()}: '{u}'"));
+ }
+
+ Assert.AreEqual(
+ 0,
+ malformed.Count,
+ "Published usings that are not namespace names:\n " + string.Join("\n ", malformed));
+ }
+
+ [TestMethod]
+ public void KeywordsAreTrimmedAndNonEmpty()
+ {
+ // Consumers weight curated keywords above generated ones, so a stray empty string or
+ // untrimmed entry becomes a search term that matches nothing. Read back from the
+ // committed file, and covering both lists: the generated keywords come from the
+ // category frontmatter by a different path than the curated ones, and were not checked
+ // at all.
+ using var document = JsonDocument.Parse(RepositoryIndex.CommittedJson);
+
+ var malformed = new List();
+
+ foreach (var control in document.RootElement.GetProperty("controls").EnumerateArray())
+ {
+ foreach (var field in new[] { "curatedKeywords", "keywords" })
+ {
+ if (!control.TryGetProperty(field, out var keywords))
+ {
+ continue;
+ }
+
+ malformed.AddRange(keywords
+ .EnumerateArray()
+ .Select(k => k.GetString())
+ .Where(k => string.IsNullOrWhiteSpace(k) || k != k.Trim())
+ .Select(k => $"{control.GetProperty("id").GetString()}.{field}: '{k}'"));
+ }
+ }
+
+ Assert.AreEqual(
+ 0,
+ malformed.Count,
+ "Blank or untrimmed keywords:\n " + string.Join("\n ", malformed));
+ }
+
+ [TestMethod]
+ public void KeywordSplittingDiscardsPaddingAndEmptyTerms()
+ {
+ // Pins the behaviour the gate above relies on. Frontmatter is hand-written, so a
+ // trailing comma or a space after one is a matter of time rather than a hypothetical.
+ var keywords = MarkdownDocument.SplitKeywords("WrapPanel, Layout ,, Panel,");
+
+ CollectionAssert.AreEqual(new[] { "WrapPanel", "Layout", "Panel" }, keywords);
+ }
+
+ [TestMethod]
+ public void KeywordSplittingTreatsAnAbsentFieldAsNoKeywords()
+ {
+ Assert.AreEqual(0, MarkdownDocument.SplitKeywords(null).Count);
+ Assert.AreEqual(0, MarkdownDocument.SplitKeywords(" ").Count);
+ }
+}
diff --git a/tools/SampleIndexExporter.Tests/CoverageTests.cs b/tools/SampleIndexExporter.Tests/CoverageTests.cs
new file mode 100644
index 00000000..92f923ca
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/CoverageTests.cs
@@ -0,0 +1,140 @@
+// 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.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.SampleIndex.Tests;
+
+///
+/// The coverage gate: anything the index leaves out is listed here on purpose.
+///
+///
+/// A generator that quietly drops what it cannot handle looks healthy while it is losing data.
+/// Pinning the exclusions means a sample that stops being published fails a test with its own
+/// name in the message, instead of just disappearing from the file.
+///
+[TestClass]
+public class CoverageTests
+{
+ ///
+ /// Documentation pages that present no runnable markup.
+ ///
+ ///
+ /// These are real pages and they stay in the index, because a consumer searching the toolkit
+ /// should still find ArrayExtensions. They carry an empty samples array because the
+ /// APIs they document are called from code, not declared in markup. If a page that used to
+ /// show samples turns up here, its [!SAMPLE] markers broke.
+ ///
+ private static readonly string[] DocumentsWithoutSamples =
+ [
+ "components/Extensions/samples/ArrayExtensions.md",
+ "components/Extensions/samples/AttachedShadows.md",
+ "components/Extensions/samples/DependencyObjectExtensions.md",
+ "components/Extensions/samples/DispatcherQueueExtensions.md",
+ "components/Extensions/samples/EnumValuesExtension.md",
+ "components/Extensions/samples/HyperlinkExtensions.md",
+ "components/Extensions/samples/IconMarkupExtensions.md",
+ "components/Extensions/samples/MatrixExtensions.md",
+ "components/Extensions/samples/NullableBoolExtension.md",
+ "components/Extensions/samples/OnDeviceExtension.md",
+ "components/Extensions/samples/ScrollViewerExtensions.md",
+ "components/Extensions/samples/ShadowAnimations.md",
+ "components/Extensions/samples/StringExtensions.md",
+ "components/Extensions/samples/TransformExtensions.md",
+ "components/Extensions/samples/VisualExtensions.md",
+ "components/Helpers/samples/ColorHelper.md",
+ "components/Helpers/samples/DesignTimeHelper.md",
+ "components/Helpers/samples/ScreenUnitHelper.md",
+ "components/Helpers/samples/WeakEventListener.md",
+ ];
+
+ [TestMethod]
+ public void NothingIsWithheld()
+ {
+ // A sample is withheld when its markup cannot be made pasteable. There is nothing in
+ // that state today, and this test is here so that the first one is noticed and either
+ // fixed at the source or listed deliberately.
+ var withheld = RepositoryIndex.Result.Withheld
+ .Select(w => $"{w.SampleId}: {w.Reason}")
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ withheld.Count,
+ "Samples excluded from the index:\n " + string.Join("\n ", withheld));
+ }
+
+ [TestMethod]
+ public void OnlyTheExpectedDocumentsHaveNoSamples()
+ {
+ var actual = RepositoryIndex.Index.Controls
+ .Where(c => c.Samples.Count == 0)
+ .Select(c => c.Toolkit!.DocumentPath)
+ .OrderBy(p => p, StringComparer.Ordinal)
+ .ToList();
+
+ var expected = DocumentsWithoutSamples.OrderBy(p => p, StringComparer.Ordinal).ToList();
+
+ var lost = actual.Except(expected).ToList();
+ Assert.AreEqual(
+ 0,
+ lost.Count,
+ "These pages used to publish samples and no longer do. Check their [!SAMPLE] markers:\n "
+ + string.Join("\n ", lost));
+
+ var gained = expected.Except(actual).ToList();
+ Assert.AreEqual(
+ 0,
+ gained.Count,
+ "These pages now publish samples. Remove them from DocumentsWithoutSamples:\n "
+ + string.Join("\n ", gained));
+ }
+
+ [TestMethod]
+ public void EverySampleDeclarationIsPublished()
+ {
+ // The sample app renders one control per [ToolkitSample]. Any declaration missing from
+ // the index is a sample a reader can see in the gallery and not find here.
+ var declared = Directory
+ .EnumerateFiles(Path.Combine(RepositoryIndex.Root, "components"), "*.xaml.cs", SearchOption.AllDirectories)
+ .Where(path => path.Contains($"{Path.DirectorySeparatorChar}samples{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
+ .SelectMany(path => SampleDeclaration.Parse(path, Path.GetRelativePath(RepositoryIndex.Root, path)))
+ .Select(declaration => declaration.Id)
+ .ToHashSet(StringComparer.Ordinal);
+
+ var published = RepositoryIndex.Samples
+ .Select(s => s.Sample.Toolkit!.SampleId)
+ .ToHashSet(StringComparer.Ordinal);
+
+ var missing = declared.Except(published).OrderBy(id => id, StringComparer.Ordinal).ToList();
+
+ Assert.AreEqual(
+ 0,
+ missing.Count,
+ "Samples the app renders but the index omits. A [!SAMPLE] marker is probably missing "
+ + "from the matching documentation page:\n " + string.Join("\n ", missing));
+ }
+
+ [TestMethod]
+ public void EveryComponentWithSamplesIsRepresented()
+ {
+ var components = Directory
+ .EnumerateDirectories(Path.Combine(RepositoryIndex.Root, "components"))
+ .Where(directory => Directory.Exists(Path.Combine(directory, "samples")))
+ .Select(Path.GetFileName)
+ .ToList();
+
+ var represented = RepositoryIndex.Index.Controls
+ .Select(c => c.Toolkit!.Component)
+ .ToHashSet(StringComparer.Ordinal);
+
+ var missing = components.Where(c => !represented.Contains(c!)).ToList();
+
+ Assert.AreEqual(
+ 0,
+ missing.Count,
+ "Components with a samples folder that produced no index entries:\n "
+ + string.Join("\n ", missing));
+ }
+}
diff --git a/tools/SampleIndexExporter.Tests/Fixture.cs b/tools/SampleIndexExporter.Tests/Fixture.cs
new file mode 100644
index 00000000..609bde84
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/Fixture.cs
@@ -0,0 +1,83 @@
+// 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.SampleIndex.Tests;
+
+///
+/// Builds a miniature repository on disk for the tests that are about the exporter rather than
+/// about this repository's samples.
+///
+///
+/// Small on purpose. It has to satisfy and carry one
+/// component with one documented sample; anything more would make the tests that use it harder
+/// to read without making them test more.
+///
+internal static class Fixture
+{
+ public const string SampleXaml =
+ """
+
+
+
+ """;
+
+ /// Create a fixture repository and return its root.
+ ///
+ /// Component folder names, created in the order given. The generated index must not depend
+ /// on that order.
+ ///
+ public static string Create(params string[] components)
+ {
+ if (components.Length == 0)
+ {
+ components = ["Widgets"];
+ }
+
+ var root = Directory.CreateTempSubdirectory("sample-index-").FullName;
+
+ // What FindRepoRoot looks for.
+ File.WriteAllText(Path.Combine(root, "Directory.Build.props"), "");
+ Directory.CreateDirectory(Path.Combine(root, "components"));
+
+ foreach (var component in components)
+ {
+ var samples = Directory.CreateDirectory(
+ Path.Combine(root, "components", component, "samples")).FullName;
+
+ File.WriteAllText(Path.Combine(samples, $"{component}.md"), Document(component));
+ File.WriteAllText(Path.Combine(samples, "WidgetSample.xaml.cs"), Declaration("WidgetSample", "Widget"));
+ File.WriteAllText(Path.Combine(samples, "WidgetSample.xaml"), SampleXaml);
+ }
+
+ return root;
+ }
+
+ public static string Declaration(string id, string displayName) =>
+ $$"""
+ using CommunityToolkit.Tooling.SampleGen.Attributes;
+
+ namespace Widgets;
+
+ [ToolkitSample(id: nameof({{id}}), "{{displayName}}", description: "Shows a widget.")]
+ public sealed partial class {{id}} : Page;
+ """;
+
+ private static string Document(string component) =>
+ $"""
+ ---
+ title: {component}
+ author: nobody
+ description: A component used by the exporter's own tests.
+ keywords: {component}
+ category: Controls
+ subcategory: Layout
+ ---
+
+ Prose introducing the sample.
+
+ > [!SAMPLE WidgetSample]
+ """;
+}
diff --git a/tools/SampleIndexExporter.Tests/IndexUpToDateTests.cs b/tools/SampleIndexExporter.Tests/IndexUpToDateTests.cs
new file mode 100644
index 00000000..6fbfdfc8
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/IndexUpToDateTests.cs
@@ -0,0 +1,119 @@
+// 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.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.SampleIndex.Tests;
+
+///
+/// The staleness gate: the committed index has to match what the samples produce right now.
+///
+///
+/// Without this, the index rots quietly. Someone edits a sample, the committed JSON keeps
+/// describing the old one, and nothing anywhere says so — consumers keep serving markup that no
+/// longer exists in the repository. Running this in CI turns that into a failed build with a
+/// one-line fix.
+///
+[TestClass]
+public class IndexUpToDateTests
+{
+ [TestMethod]
+ public void CommittedIndexExists()
+ {
+ var path = Path.Combine(RepositoryIndex.Root, IndexGenerator.IndexRelativePath);
+
+ Assert.IsTrue(
+ File.Exists(path),
+ $"{IndexGenerator.IndexRelativePath} is missing. Run "
+ + "'dotnet run --project tools/SampleIndexExporter -- generate' and commit the result.");
+ }
+
+ [TestMethod]
+ public void CommittedIndexMatchesTheSamples()
+ {
+ var path = Path.Combine(RepositoryIndex.Root, IndexGenerator.IndexRelativePath);
+ var committed = File.ReadAllText(path).Replace("\r\n", "\n");
+ var regenerated = IndexGenerator.Serialize(RepositoryIndex.Index);
+
+ Assert.AreEqual(
+ regenerated,
+ committed,
+ $"{IndexGenerator.IndexRelativePath} does not match the samples in this branch. Run "
+ + "'dotnet run --project tools/SampleIndexExporter -- generate' and commit the result.");
+ }
+
+ [TestMethod]
+ public void GeneratingTwiceProducesTheSameBytes()
+ {
+ // Catches in-process non-determinism only: a dictionary's iteration order, a hash set
+ // reaching the output, a stray DateTime. It cannot see the filesystem-dependent kind,
+ // which is what the test below is for.
+ var first = IndexGenerator.Serialize(IndexGenerator.Generate(RepositoryIndex.Root).Index);
+ var second = IndexGenerator.Serialize(IndexGenerator.Generate(RepositoryIndex.Root).Index);
+
+ Assert.AreEqual(first, second, "Generating the index twice produced different output.");
+ }
+
+ [TestMethod]
+ public void EntriesAreOrderedOrdinallyRatherThanByTheHostsIdeaOfOrder()
+ {
+ // The index is generated by contributors on Windows and byte-compared by a Linux CI
+ // job, so every ordering has to mean the same thing on both. NTFS compares names
+ // case-insensitively and ext4 does not; a culture-aware or case-insensitive comparer
+ // would therefore agree with itself on one machine and disagree across the two,
+ // producing a CI failure nobody could reproduce locally.
+ //
+ // These three names are chosen so ordinal order (uppercase first) and case-insensitive
+ // order disagree: ordinal gives Beta, Gamma, alpha; case-insensitive gives alpha, Beta,
+ // Gamma. Comparing two fixtures built in different orders would not catch this, because
+ // NTFS hands directory entries back in name order whatever order they were created in.
+ var root = Fixture.Create("Beta", "alpha", "Gamma");
+
+ try
+ {
+ var entries = IndexGenerator.Generate(root).Index.Controls
+ .Select(c => c.Toolkit!.DocumentPath)
+ .ToList();
+
+ CollectionAssert.AreEqual(
+ new[]
+ {
+ "components/Beta/samples/Beta.md",
+ "components/Gamma/samples/Gamma.md",
+ "components/alpha/samples/alpha.md",
+ },
+ entries,
+ "Entries must be ordered ordinally, which is the only order both CI and a "
+ + "contributor's machine agree on.");
+ }
+ finally
+ {
+ Directory.Delete(root, recursive: true);
+ }
+ }
+
+ [TestMethod]
+ public void EntriesAreOrderedByDocumentPath()
+ {
+ var paths = RepositoryIndex.Index.Controls.Select(c => c.Toolkit!.DocumentPath).ToList();
+ var sorted = paths.OrderBy(p => p, StringComparer.Ordinal).ToList();
+
+ CollectionAssert.AreEqual(
+ sorted,
+ paths,
+ "Entries must be ordered by document path so that adding a sample produces a small diff.");
+ }
+
+ [TestMethod]
+ public void SourceDataHasNoErrors()
+ {
+ var errors = RepositoryIndex.Result.Errors.Select(e => e.ToString()).ToList();
+
+ Assert.AreEqual(
+ 0,
+ errors.Count,
+ "The sample sources have errors that stop the index being published:\n "
+ + string.Join("\n ", errors));
+ }
+}
diff --git a/tools/SampleIndexExporter.Tests/PasteabilityTests.cs b/tools/SampleIndexExporter.Tests/PasteabilityTests.cs
new file mode 100644
index 00000000..46bc0a62
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/PasteabilityTests.cs
@@ -0,0 +1,215 @@
+// 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.Text.RegularExpressions;
+using System.Xml.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.SampleIndex.Tests;
+
+///
+/// The pasteability gate: every published snippet has to work when pasted into a page.
+///
+///
+/// This is the whole point of the index. A consumer offers the markup to someone who pastes it
+/// into their own app, so a snippet that references the sample app's page, or a namespace prefix
+/// the consumer was never told to declare, is worse than no snippet at all — it fails after the
+/// paste, in their file, with an error that points at their code.
+///
+[TestClass]
+public class PasteabilityTests
+{
+ [TestMethod]
+ public void EverySampleHasMarkup()
+ {
+ var empty = RepositoryIndex.Samples
+ .Where(s => string.IsNullOrWhiteSpace(s.Sample.Xaml))
+ .Select(s => s.Sample.Toolkit!.SourcePath)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ empty.Count,
+ "Published samples with no markup:\n " + string.Join("\n ", empty));
+ }
+
+ [TestMethod]
+ public void EverySampleIsWellFormedXml()
+ {
+ var malformed = new List();
+
+ foreach (var (_, sample) in RepositoryIndex.Samples)
+ {
+ var declarations = string.Join(
+ ' ',
+ (sample.XmlnsImports ?? []).Select(i => i));
+
+ var document =
+ "" + sample.Xaml + "";
+
+ try
+ {
+ XDocument.Parse(document);
+ }
+ catch (Exception ex)
+ {
+ malformed.Add($"{sample.Toolkit!.SourcePath}: {ex.Message}");
+ }
+ }
+
+ Assert.AreEqual(
+ 0,
+ malformed.Count,
+ "Published samples that do not parse under their own declared imports:\n "
+ + string.Join("\n ", malformed));
+ }
+
+ [TestMethod]
+ public void NoSampleCarriesSampleAppBindings()
+ {
+ // The sample app binds its option controls into the markup. Those bindings resolve
+ // against a generated view model that only exists in the sample app, so anything left
+ // unsettled here would paste in and then fail to compile. The options have to come from
+ // the declarations: checking against an empty list would only catch dangling element
+ // names and would quietly pass everything else.
+ var options = SampleOptionsBySampleId();
+ var unsettled = new List();
+
+ foreach (var (_, sample) in RepositoryIndex.Samples)
+ {
+ var declared = options.GetValueOrDefault(sample.Toolkit!.SampleId, []);
+ var leftovers = XamlFragment.UnsettledBindings(sample.Xaml!, declared);
+ if (leftovers.Count > 0)
+ {
+ unsettled.Add($"{sample.Toolkit.SourcePath}: {string.Join(", ", leftovers)}");
+ }
+ }
+
+ Assert.AreEqual(
+ 0,
+ unsettled.Count,
+ "Published samples still referencing the sample app:\n " + string.Join("\n ", unsettled));
+ }
+
+ [TestMethod]
+ public void EverySampleWithOptionsActuallyHadThemChecked()
+ {
+ // Guards the test above. If the source paths and sample ids ever stopped lining up,
+ // every lookup would miss, every sample would be checked against no options, and the
+ // gate would pass while checking nothing.
+ var options = SampleOptionsBySampleId();
+ var matched = RepositoryIndex.Samples.Count(s => options.ContainsKey(s.Sample.Toolkit!.SampleId));
+
+ Assert.AreEqual(
+ RepositoryIndex.Samples.Count(),
+ matched,
+ "Some published samples could not be matched back to their declaration.");
+ }
+
+ private static Dictionary> SampleOptionsBySampleId()
+ {
+ return Directory
+ .EnumerateFiles(Path.Combine(RepositoryIndex.Root, "components"), "*.xaml.cs", SearchOption.AllDirectories)
+ .Where(path => path.Contains($"{Path.DirectorySeparatorChar}samples{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
+ .SelectMany(path => SampleDeclaration.Parse(path, Path.GetRelativePath(RepositoryIndex.Root, path)))
+ .ToDictionary(d => d.Id, d => d.Options, StringComparer.Ordinal);
+ }
+
+ [TestMethod]
+ public void NoSampleCarriesThePageItWasExtractedFrom()
+ {
+ var wrappers = RepositoryIndex.Samples
+ .Where(s => s.Sample.Xaml!.Contains("x:Class", StringComparison.Ordinal)
+ || s.Sample.Xaml!.StartsWith(" s.Sample.Toolkit!.SourcePath)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ wrappers.Count,
+ "Published samples that still include the sample page wrapper:\n "
+ + string.Join("\n ", wrappers));
+ }
+
+ [TestMethod]
+ public void EveryPrefixUsedIsDeclared()
+ {
+ // A snippet that uses controls: without saying what controls: means sends the reader
+ // hunting through the repository for an xmlns they were supposed to be handed.
+ var undeclared = new List();
+
+ foreach (var (_, sample) in RepositoryIndex.Samples)
+ {
+ var declared = (sample.XmlnsImports ?? [])
+ .Select(import => import["xmlns:".Length..import.IndexOf('=')])
+ .ToHashSet(StringComparer.Ordinal);
+
+ foreach (var prefix in PrefixesUsed(sample.Xaml!))
+ {
+ if (prefix is "x" or "xmlns" || declared.Contains(prefix))
+ {
+ continue;
+ }
+
+ undeclared.Add($"{sample.Toolkit!.SourcePath}: {prefix}");
+ }
+ }
+
+ Assert.AreEqual(
+ 0,
+ undeclared.Count,
+ "Published samples using a prefix they do not declare:\n "
+ + string.Join("\n ", undeclared.Distinct()));
+ }
+
+ [TestMethod]
+ public void NoSampleUsesDesignTimeMarkup()
+ {
+ // Design-time attributes (d:, mc:) are Visual Studio scaffolding. They carry two more
+ // xmlns declarations and do nothing at run time, so shipping them makes the snippet
+ // longer and the paste more fragile for no benefit.
+ var designTime = RepositoryIndex.Samples
+ .Where(s => s.Sample.Xaml!.Contains(" d:", StringComparison.Ordinal)
+ || s.Sample.Xaml!.Contains(" mc:", StringComparison.Ordinal))
+ .Select(s => s.Sample.Toolkit!.SourcePath)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ designTime.Count,
+ "Published samples carrying design-time markup:\n " + string.Join("\n ", designTime));
+ }
+
+ ///
+ /// Every prefix the markup relies on: element names, attached property names and markup
+ /// extensions all fail the same way when the prefix was never declared.
+ ///
+ ///
+ /// Comments are removed first. Samples sometimes keep a commented-out alternative — the
+ /// Behaviors sample keeps a PlaySoundAction that way — and a prefix inside a comment
+ /// is never resolved, so counting it would report a problem that cannot happen.
+ ///
+ private static IEnumerable PrefixesUsed(string xaml)
+ {
+ var text = Regex.Replace(xaml, "", string.Empty, RegexOptions.Singleline);
+
+ foreach (Match match in Regex.Matches(text, @"?(?[A-Za-z_][\w.-]*):"))
+ {
+ yield return match.Groups["prefix"].Value;
+ }
+
+ foreach (Match match in Regex.Matches(text, @"\s(?[A-Za-z_][\w.-]*):[A-Za-z_][\w.]*\s*="))
+ {
+ yield return match.Groups["prefix"].Value;
+ }
+
+ foreach (Match match in Regex.Matches(text, @"\{\s*(?[A-Za-z_][\w.-]*):"))
+ {
+ yield return match.Groups["prefix"].Value;
+ }
+ }
+}
diff --git a/tools/SampleIndexExporter.Tests/ProgramTests.cs b/tools/SampleIndexExporter.Tests/ProgramTests.cs
new file mode 100644
index 00000000..c2758bd2
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/ProgramTests.cs
@@ -0,0 +1,183 @@
+// 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.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.SampleIndex.Tests;
+
+///
+/// The command-line contract: what CI actually depends on.
+///
+///
+/// The Sample-Index job is a gate, and a gate is only worth having if it fails when it
+/// should. Everything here runs against a fixture repository built in a temporary directory,
+/// because the questions are about the exporter's own behaviour — exit codes, what it writes,
+/// what it refuses to write — rather than about this repository's samples.
+///
+[TestClass]
+public class ProgramTests
+{
+ private string _root = string.Empty;
+
+ [TestInitialize]
+ public void CreateFixture()
+ {
+ _root = Fixture.Create();
+ }
+
+ [TestCleanup]
+ public void DeleteFixture()
+ {
+ if (Directory.Exists(_root))
+ {
+ Directory.Delete(_root, recursive: true);
+ }
+ }
+
+ [TestMethod]
+ public void NoArgumentsIsAUsageError()
+ {
+ Assert.AreEqual(2, Run(out var error));
+ StringAssert.Contains(error, "Usage:");
+ }
+
+ [TestMethod]
+ public void AnUnknownCommandIsAUsageError()
+ {
+ // Distinct from 1: a mistyped command is the caller's mistake, not a stale index, and a
+ // CI step that confused the two would report drift that is not there.
+ Assert.AreEqual(2, Run(out _, "verify", "--repo-root", _root));
+ }
+
+ [TestMethod]
+ public void CheckFailsWhenTheIndexHasNeverBeenGenerated()
+ {
+ Assert.AreEqual(1, Run(out var error, "check", "--repo-root", _root));
+ StringAssert.Contains(error, "does not exist");
+ Assert.IsFalse(File.Exists(IndexPath), "check must not write anything.");
+ }
+
+ [TestMethod]
+ public void GenerateWritesTheIndex()
+ {
+ Assert.AreEqual(0, Run(out _, "generate", "--repo-root", _root));
+ Assert.IsTrue(File.Exists(IndexPath));
+ }
+
+ [TestMethod]
+ public void CheckPassesOnAFreshlyGeneratedIndex()
+ {
+ Run(out _, "generate", "--repo-root", _root);
+
+ Assert.AreEqual(0, Run(out _, "check", "--repo-root", _root));
+ }
+
+ [TestMethod]
+ public void CheckFailsWhenASampleChangedAfterGenerating()
+ {
+ // The whole point of the gate. If this passed, the published index could describe
+ // markup that no longer exists and nothing would say so.
+ Run(out _, "generate", "--repo-root", _root);
+
+ File.WriteAllText(
+ Path.Combine(_root, "components", "Widgets", "samples", "WidgetSample.xaml"),
+ Fixture.SampleXaml.Replace("Hello", "Goodbye", StringComparison.Ordinal));
+
+ Assert.AreEqual(1, Run(out var error, "check", "--repo-root", _root));
+ StringAssert.Contains(error, "out of date");
+ }
+
+ [TestMethod]
+ public void CheckIgnoresLineEndingsTheWorkingCopyMayHave()
+ {
+ // The index is generated with LF and byte-compared by a Linux CI job, but a contributor
+ // regenerates it on Windows. Without this normalization the check would report every
+ // Windows working copy as stale, and .gitattributes alone cannot be relied on to have
+ // been applied to a copy that was already on disk.
+ Run(out _, "generate", "--repo-root", _root);
+
+ var lf = File.ReadAllText(IndexPath);
+ Assert.IsFalse(lf.Contains('\r'), "The generator must write LF.");
+
+ File.WriteAllText(IndexPath, lf.Replace("\n", "\r\n", StringComparison.Ordinal));
+
+ Assert.AreEqual(0, Run(out _, "check", "--repo-root", _root));
+ }
+
+ [TestMethod]
+ public void AnErrorInTheSourcesFailsAndWritesNothing()
+ {
+ // Publishing a half-built index is worse than publishing none: a consumer cannot tell
+ // the difference between a control that was dropped and one that never existed.
+ File.WriteAllText(
+ Path.Combine(_root, "components", "Widgets", "samples", "Broken.md"),
+ "---\nauthor: nobody\n---\n\n> [!SAMPLE WidgetSample]\n");
+
+ Assert.AreEqual(1, Run(out var error, "generate", "--repo-root", _root));
+ StringAssert.Contains(error, "error:");
+ Assert.IsFalse(File.Exists(IndexPath), "Nothing may be written when the sources have errors.");
+ }
+
+ [TestMethod]
+ public void AWarningDoesNotFailTheBuild()
+ {
+ // An unreferenced sample is worth saying out loud and not worth blocking a merge over.
+ // If warnings failed, the gate would be turned off.
+ File.WriteAllText(
+ Path.Combine(_root, "components", "Widgets", "samples", "OrphanSample.xaml.cs"),
+ Fixture.Declaration("OrphanSample", "Orphan"));
+
+ Assert.AreEqual(0, Run(out _, "generate", "--repo-root", _root));
+ }
+
+ [TestMethod]
+ public void TheRepositoryRootIsFoundByWalkingUp()
+ {
+ var nested = Directory.CreateDirectory(Path.Combine(_root, "components", "Widgets", "samples")).FullName;
+
+ Assert.AreEqual(
+ new DirectoryInfo(_root).FullName,
+ new DirectoryInfo(IndexGenerator.FindRepoRoot(nested)).FullName);
+ }
+
+ [TestMethod]
+ public void AnUnrecognizableRootIsReportedRatherThanGuessedAt()
+ {
+ var stray = Directory.CreateTempSubdirectory("sample-index-stray-").FullName;
+
+ try
+ {
+ Assert.ThrowsException(() => IndexGenerator.FindRepoRoot(stray));
+ }
+ finally
+ {
+ Directory.Delete(stray, recursive: true);
+ }
+ }
+
+ private string IndexPath =>
+ Path.Combine(_root, IndexGenerator.IndexRelativePath.Replace('/', Path.DirectorySeparatorChar));
+
+ /// Run the exporter in-process, capturing what it wrote to stderr.
+ private static int Run(out string error, params string[] args)
+ {
+ var originalOut = Console.Out;
+ var originalError = Console.Error;
+ var captured = new StringWriter(new StringBuilder());
+
+ try
+ {
+ Console.SetOut(TextWriter.Null);
+ Console.SetError(captured);
+ return Program.Main(args);
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ Console.SetError(originalError);
+ error = captured.ToString();
+ }
+ }
+}
diff --git a/tools/SampleIndexExporter.Tests/RepositoryIndex.cs b/tools/SampleIndexExporter.Tests/RepositoryIndex.cs
new file mode 100644
index 00000000..e00b4fd4
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/RepositoryIndex.cs
@@ -0,0 +1,39 @@
+// 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.SampleIndex.Tests;
+
+///
+/// Generates the index once for the whole test run, against the real repository.
+///
+///
+/// The gates below all ask questions about the actual sample data rather than about fixtures,
+/// because the failure they exist to catch is a change to the samples, not a change to this
+/// code. Generating is fast enough to do once and share.
+///
+internal static class RepositoryIndex
+{
+ private static readonly Lazy LazyResult = new(() => IndexGenerator.Generate(Root));
+
+ private static readonly Lazy LazyRoot =
+ new(() => IndexGenerator.FindRepoRoot(AppContext.BaseDirectory));
+
+ public static string Root => LazyRoot.Value;
+
+ public static GenerationResult Result => LazyResult.Value;
+
+ public static SampleIndex Index => Result.Index;
+
+ /// The committed index exactly as a consumer fetches it.
+ ///
+ /// For the contract gates. A test that asks the in-memory object whether a field is correct
+ /// is asking the code that produced it, and gets the answer that code was written to give;
+ /// the artifact is the only place the serialized field names and values can be observed.
+ ///
+ public static string CommittedJson =>
+ File.ReadAllText(Path.Combine(Root, IndexGenerator.IndexRelativePath.Replace('/', Path.DirectorySeparatorChar)));
+
+ public static IEnumerable<(IndexedControl Control, IndexedSample Sample)> Samples =>
+ Index.Controls.SelectMany(control => control.Samples.Select(sample => (control, sample)));
+}
diff --git a/tools/SampleIndexExporter.Tests/SampleCodeTests.cs b/tools/SampleIndexExporter.Tests/SampleCodeTests.cs
new file mode 100644
index 00000000..cad98bb4
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/SampleCodeTests.cs
@@ -0,0 +1,340 @@
+// 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.Text.Json;
+using System.Text.RegularExpressions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.SampleIndex.Tests;
+
+///
+/// Gates for the published C#.
+///
+///
+/// A snippet that carries the sample app with it is worse than no snippet: the reader pastes
+/// it, it does not compile, and the error names a generator package and a namespace they have
+/// never heard of.
+///
+[TestClass]
+public partial class SampleCodeTests
+{
+ private static IEnumerable<(string Path, string Code)> PublishedCode =>
+ RepositoryIndex.Samples
+ .Where(s => s.Sample.Code is not null)
+ .Select(s => (s.Sample.Toolkit!.SourcePath, s.Sample.Code!));
+
+ [TestMethod]
+ public void SomeSamplesPublishCode()
+ {
+ // Guards every test below. If extraction silently started returning nothing, they
+ // would all pass while checking an empty set.
+ Assert.IsTrue(
+ PublishedCode.Count() > 20,
+ $"Only {PublishedCode.Count()} samples published code, which suggests extraction is failing.");
+ }
+
+ [TestMethod]
+ public void CodeIsAlwaysTaggedAsCSharp()
+ {
+ // Read back from the committed file: the language tag is what a consumer hands to a
+ // syntax highlighter, so what matters is the value in the artifact. Asserting against
+ // the in-memory object would only restate the line in IndexGenerator that sets it.
+ using var document = JsonDocument.Parse(RepositoryIndex.CommittedJson);
+
+ var mistagged = new List();
+
+ foreach (var control in document.RootElement.GetProperty("controls").EnumerateArray())
+ {
+ foreach (var sample in control.GetProperty("samples").EnumerateArray())
+ {
+ var hasCode = sample.TryGetProperty("code", out var code) && code.GetString() is not null;
+ var language = sample.TryGetProperty("language", out var tag) ? tag.GetString() : null;
+
+ if (hasCode != (language is not null) || language is not (null or "csharp"))
+ {
+ mistagged.Add($"{control.GetProperty("id").GetString()}: language '{language ?? "(none)"}'");
+ }
+ }
+ }
+
+ Assert.AreEqual(
+ 0,
+ mistagged.Count,
+ "Samples whose code and language fields disagree:\n " + string.Join("\n ", mistagged));
+ }
+
+ [TestMethod]
+ public void NoPublishedCodeCarriesSampleBrowserAttributes()
+ {
+ var leaked = PublishedCode
+ .Where(c => c.Code.Contains("[ToolkitSample", StringComparison.Ordinal))
+ .Select(c => c.Path)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ leaked.Count,
+ "Published code still carrying [ToolkitSample…] attributes:\n " + string.Join("\n ", leaked));
+ }
+
+ [TestMethod]
+ public void NoPublishedCodeCarriesTheSampleAppNamespaceOrLicense()
+ {
+ var leaked = PublishedCode
+ .Where(c => c.Code.Contains("namespace ", StringComparison.Ordinal)
+ || c.Code.Contains("Licensed to the .NET Foundation", StringComparison.Ordinal))
+ .Select(c => c.Path)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ leaked.Count,
+ "Published code still carrying a namespace or license header:\n " + string.Join("\n ", leaked));
+ }
+
+ [TestMethod]
+ public void NoPublishedCodeCarriesUnresolvedConditionals()
+ {
+ // The reader is on WinAppSDK. Publishing #if WINAPPSDK / #else asks them to decide
+ // which half is theirs, and publishing the UWP half would not compile at all.
+ var leaked = PublishedCode
+ .Where(c => c.Code.Contains("#if", StringComparison.Ordinal)
+ || c.Code.Contains("#else", StringComparison.Ordinal)
+ || c.Code.Contains("#endif", StringComparison.Ordinal))
+ .Select(c => c.Path)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ leaked.Count,
+ "Published code still carrying preprocessor branches:\n " + string.Join("\n ", leaked));
+ }
+
+ [TestMethod]
+ public void NoPublishedCodeIsJustThePageWakingUp()
+ {
+ // InitializeComponent loads a different page's markup, and the reader's own page
+ // already calls it. A constructor that does more than that is kept - that part is the
+ // sample - but the call itself never is.
+ var scaffolding = PublishedCode
+ .Where(c => c.Code.Contains("InitializeComponent", StringComparison.Ordinal))
+ .Select(c => c.Path)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ scaffolding.Count,
+ "Published code that is page scaffolding rather than the sample:\n "
+ + string.Join("\n ", scaffolding));
+ }
+
+ [TestMethod]
+ public void NoPublishedCodeCarriesTheOptionsPaneConverters()
+ {
+ // ConvertStringTo… turns an options-pane string into a real value. With the pane gone
+ // nothing calls them, so publishing them is dead code the reader has to read and
+ // discard.
+ var leaked = PublishedCode
+ .Where(c => c.Code.Contains("ConvertString", StringComparison.Ordinal))
+ .Select(c => c.Path)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ leaked.Count,
+ "Published code still carrying option-pane converters:\n " + string.Join("\n ", leaked));
+ }
+
+ [TestMethod]
+ public void PublishedCodeParsesAsCSharpMembers()
+ {
+ var malformed = new List();
+
+ foreach (var (path, code) in PublishedCode)
+ {
+ // Wrapped the way a reader would paste it: into a class in their own page.
+ var wrapped = $"class Host {{\n{code}\n}}";
+ var tree = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(wrapped);
+
+ var errors = tree.GetDiagnostics()
+ .Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)
+ .ToList();
+
+ // A type declared beside the sample is published alongside its members, and a type
+ // cannot nest inside the host class in every form, so those are parsed standalone.
+ if (errors.Count > 0)
+ {
+ var standalone = Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(code);
+ errors = standalone.GetDiagnostics()
+ .Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)
+ .ToList();
+ }
+
+ if (errors.Count > 0)
+ {
+ malformed.Add($"{path}: {errors[0].GetMessage()}");
+ }
+ }
+
+ Assert.AreEqual(
+ 0,
+ malformed.Count,
+ "Published code that does not parse:\n " + string.Join("\n ", malformed));
+ }
+
+ [TestMethod]
+ public void TypesDeclaredBesideASampleArePublishedWithIt()
+ {
+ // SwitchPresenterValueSample's markup switches on an enum declared in the same file.
+ // Dropping it would publish markup that binds to a type the reader does not have.
+ var sample = RepositoryIndex.Samples
+ .Single(s => s.Sample.Toolkit!.SampleId == "SwitchPresenterValueSample")
+ .Sample;
+
+ Assert.IsNotNull(sample.Code);
+ StringAssert.Contains(sample.Code, "enum Animal");
+ }
+
+ [TestMethod]
+ public void SomeControlsPublishTheNamespacesTheirCodeNeeds()
+ {
+ // Guards every import test below, and the field itself: consumers prepend this list to
+ // each sample's code, so an extractor that quietly stopped filling it would publish C#
+ // that names toolkit types and says nowhere which namespace they come from.
+ var publishing = RepositoryIndex.Index.Controls.Count(c => c.Usings is { Count: > 0 });
+
+ Assert.IsTrue(
+ publishing > 10,
+ $"Only {publishing} entries published any usings, which suggests imports are being dropped.");
+ }
+
+ [TestMethod]
+ public void AControlPublishesTheNamespaceItsCodeCallsInto()
+ {
+ // NetworkHelperSample's code is NetworkHelper.Instance.ConnectionInformation and
+ // nothing else, so the one namespace its file imports is the one the reader needs.
+ var control = RepositoryIndex.Index.Controls.Single(c => c.Id == "networkhelper");
+
+ CollectionAssert.AreEqual(new[] { "CommunityToolkit.WinUI.Helpers" }, control.Usings);
+ }
+
+ [TestMethod]
+ public void AMultiTargetAliasIsPublishedAsTheNamespaceItNames()
+ {
+ // Samples alias a type to name one platform's version of it while multi-targeting. With
+ // the WinAppSDK branch already resolved, what the reader needs is the namespace that
+ // type lives in — nothing in the published code mentions the alias is an alias.
+ var timers = RepositoryIndex.Index.Controls.Single(c => c.Id == "dispatcherqueuetimerextensions");
+ var wrapPanel = RepositoryIndex.Index.Controls.Single(c => c.Id == "wrappanel");
+
+ CollectionAssert.Contains(timers.Usings, "Microsoft.UI.Dispatching");
+ CollectionAssert.Contains(wrapPanel.Usings, "CommunityToolkit.WinUI.Controls");
+
+ // And the extension method the same sample calls, which enters scope through its
+ // namespace without the code ever naming the class that declares it.
+ CollectionAssert.Contains(timers.Usings, "CommunityToolkit.WinUI");
+ CollectionAssert.Contains(wrapPanel.Usings, "CommunityToolkit.WinUI");
+ }
+
+ [TestMethod]
+ public void ImportsOnlyTheScaffoldingNeededAreNotPublished()
+ {
+ // IsNullOrEmptyStateTriggerSample's file imports CommunityToolkit.WinUI for the trigger
+ // its markup uses, but the code that survives extraction is two click handlers that
+ // touch nothing in that namespace. Publishing the import anyway would tell the reader
+ // to add a using for code they were never handed.
+ var control = RepositoryIndex.Index.Controls.Single(c => c.Id == "triggers");
+
+ Assert.IsNull(
+ control.Usings,
+ "Imports that only the discarded page scaffolding needed must not be published: "
+ + string.Join(", ", control.Usings ?? []));
+ }
+
+ [TestMethod]
+ public void NoControlPublishesANamespaceOnlyTheSampleAppHas()
+ {
+ // The Connected Animations sample navigates to pages declared in a namespace of this
+ // repository's sample app. That using resolves here and nowhere else, so prepending it
+ // to a snippet would break the paste in the reader's project.
+ var sampleApp = SampleAppNamespaces();
+
+ var leaked = RepositoryIndex.Index.Controls
+ .SelectMany(c => (c.Usings ?? []).Select(u => (Control: c.Id, Using: u)))
+ .Where(u => sampleApp.Contains(u.Using))
+ .Select(u => $"{u.Control}: {u.Using}")
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ leaked.Count,
+ "Published usings naming a sample-app namespace:\n " + string.Join("\n ", leaked));
+ }
+
+ [TestMethod]
+ public void TheSampleAppNamespacesWereActuallyFound()
+ {
+ // Guards the test above: an empty set would let it pass while checking nothing.
+ Assert.IsTrue(
+ SampleAppNamespaces().Contains("AnimationsExperiment.Samples.ConnectedAnimations"),
+ "The sample app's namespaces could not be read, so the gate above checked nothing.");
+ }
+
+ [TestMethod]
+ public void NoPublishedCodeRepeatsTheImportsTheControlDeclares()
+ {
+ // The contract puts the imports on the control and has consumers prepend them, so a
+ // snippet that carries its own would hand the reader the same using twice.
+ var leaked = PublishedCode
+ .Where(c => UsingDirectiveRegex().IsMatch(c.Code))
+ .Select(c => c.Path)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ leaked.Count,
+ "Published code carrying its own using directives:\n " + string.Join("\n ", leaked));
+ }
+
+ [TestMethod]
+ public void OnlyControlsThatPublishCodePublishImports()
+ {
+ // Imports exist to make the C# compile. An entry that publishes none but still carries
+ // a usings list would have a consumer prepending using lines to pure markup.
+ var orphaned = RepositoryIndex.Index.Controls
+ .Where(c => c.Usings is { Count: > 0 } && c.Samples.All(s => s.Code is null))
+ .Select(c => c.Id)
+ .ToList();
+
+ Assert.AreEqual(
+ 0,
+ orphaned.Count,
+ "Entries publishing usings with no code to import for:\n " + string.Join("\n ", orphaned));
+ }
+
+ [GeneratedRegex(@"^\s*using\s+[\w.]+\s*;", RegexOptions.Multiline)]
+ private static partial Regex UsingDirectiveRegex();
+
+ /// Every namespace this repository's sample app declares.
+ ///
+ /// Read from the sources rather than matched against the …Experiment.Samples naming
+ /// convention, so the gate keeps working if a component ever names its sample pages
+ /// differently.
+ ///
+ private static HashSet SampleAppNamespaces()
+ {
+ var samples = $"{Path.DirectorySeparatorChar}samples{Path.DirectorySeparatorChar}";
+
+ return Directory
+ .EnumerateFiles(Path.Combine(RepositoryIndex.Root, "components"), "*.cs", SearchOption.AllDirectories)
+ .Where(path => path.Contains(samples, StringComparison.Ordinal))
+ .SelectMany(path => Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree
+ .ParseText(File.ReadAllText(path))
+ .GetRoot()
+ .DescendantNodes()
+ .OfType()
+ .Select(declaration => declaration.Name.ToString()))
+ .ToHashSet(StringComparer.Ordinal);
+ }
+}
diff --git a/tools/SampleIndexExporter.Tests/SampleDeclarationTests.cs b/tools/SampleIndexExporter.Tests/SampleDeclarationTests.cs
new file mode 100644
index 00000000..0086c82c
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/SampleDeclarationTests.cs
@@ -0,0 +1,233 @@
+// 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.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.SampleIndex.Tests;
+
+///
+/// Unit tests for reading a [ToolkitSample] declaration.
+///
+///
+/// These are the forms the samples actually use. Each one was a way the index could have
+/// disagreed with the sample app about what a sample is called or what its options default to,
+/// which is the one thing a reader has no way to notice.
+///
+[TestClass]
+public class SampleDeclarationTests
+{
+ [TestMethod]
+ public void DisplayNameIsReadWhenArgumentsMixNamedAndPositional()
+ {
+ // The most common form in the repository. The display name is the second parameter but
+ // the first positional argument, so reading arguments by position alone loses it.
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(MySample), "My Sample", description: "Shows a thing.")]
+ public sealed partial class MySample : Page;
+ """);
+
+ Assert.AreEqual("MySample", declaration.Id);
+ Assert.AreEqual("My Sample", declaration.DisplayName);
+ Assert.AreEqual("Shows a thing.", declaration.Description);
+ }
+
+ [TestMethod]
+ public void DisplayNameIsReadWhenEveryArgumentIsPositional()
+ {
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(nameof(MySample), "My Sample", "Shows a thing.")]
+ public sealed partial class MySample : Page;
+ """);
+
+ Assert.AreEqual("My Sample", declaration.DisplayName);
+ }
+
+ [TestMethod]
+ public void DisplayNameIsReadWhenEveryArgumentIsNamed()
+ {
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(description: "Shows a thing.", displayName: "My Sample", id: nameof(MySample))]
+ public sealed partial class MySample : Page;
+ """);
+
+ Assert.AreEqual("My Sample", declaration.DisplayName);
+ }
+
+ [TestMethod]
+ public void DisplayNameIsReadFromNameOf()
+ {
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(SettingsCardSample), nameof(SettingsCard), description: "")]
+ public sealed partial class SettingsCardSample : Page;
+ """);
+
+ Assert.AreEqual("SettingsCard", declaration.DisplayName);
+ }
+
+ [TestMethod]
+ public void DisplayNameIsReadFromAnInterpolatedString()
+ {
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(FocusSample), $"{nameof(FocusBehavior)}: Lists", description: "")]
+ public sealed partial class FocusSample : Page;
+ """);
+
+ Assert.AreEqual("FocusBehavior: Lists", declaration.DisplayName);
+ }
+
+ [TestMethod]
+ public void AnUnreadableArgumentIsReportedRatherThanGuessed()
+ {
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(MySample), BuildName(), description: "")]
+ public sealed partial class MySample : Page;
+ """);
+
+ Assert.IsNull(declaration.DisplayName);
+ Assert.AreEqual(1, declaration.UnreadableArguments.Count);
+ StringAssert.Contains(declaration.UnreadableArguments[0], "displayName");
+ }
+
+ [TestMethod]
+ public void BoolOptionTakesItsDefaultState()
+ {
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(MySample), "My Sample", description: "")]
+ [ToolkitSampleBoolOption("IsCardEnabled", true, Title = "Is card enabled")]
+ public sealed partial class MySample : Page;
+ """);
+
+ var option = declaration.Options.Single();
+ Assert.AreEqual("IsCardEnabled", option.Name);
+ Assert.AreEqual("True", option.DefaultValue);
+ }
+
+ [TestMethod]
+ public void NumericOptionTakesItsInitialValue()
+ {
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(MySample), "My Sample", description: "")]
+ [ToolkitSampleNumericOption("Spacing", 8, 0, 48, 1, false, Title = "Spacing")]
+ public sealed partial class MySample : Page;
+ """);
+
+ var option = declaration.Options.Single();
+ Assert.AreEqual("Spacing", option.Name);
+ Assert.AreEqual("8", option.DefaultValue);
+ }
+
+ [TestMethod]
+ public void NumericOptionWithOnlyNamedRangeArgumentsStillHasADefault()
+ {
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(MySample), "My Sample", description: "")]
+ [ToolkitSampleNumericOption("Spacing", min: 0, max: 48, initial: 8)]
+ public sealed partial class MySample : Page;
+ """);
+
+ Assert.AreEqual("8", declaration.Options.Single().DefaultValue);
+ }
+
+ [TestMethod]
+ public void MultiChoiceOptionTakesItsFirstChoice()
+ {
+ // The sample app selects the first choice, so anything else here would publish markup
+ // showing a state the gallery never opens in.
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(MySample), "My Sample", description: "")]
+ [ToolkitSampleMultiChoiceOption("Orientation", "Horizontal", "Vertical", Title = "Orientation")]
+ public sealed partial class MySample : Page;
+ """);
+
+ Assert.AreEqual("Horizontal", declaration.Options.Single().DefaultValue);
+ }
+
+ [TestMethod]
+ public void MultiChoiceLabelsAreStrippedFromTheValue()
+ {
+ // "Label : Value" is display text on the left and the real value on the right. Pasting
+ // the label would produce markup that does not compile.
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(MySample), "My Sample", description: "")]
+ [ToolkitSampleMultiChoiceOption("Stretch", "Fill the box : UniformToFill", "Fit : Uniform")]
+ public sealed partial class MySample : Page;
+ """);
+
+ Assert.AreEqual("UniformToFill", declaration.Options.Single().DefaultValue);
+ }
+
+ [TestMethod]
+ public void TextOptionDefaultsToItsPlaceholder()
+ {
+ // The sample app seeds the text box with the placeholder and binds Text to it, so the
+ // placeholder is what a reader sees the sample doing, not just prompt text.
+ var declaration = ParseSingle(
+ """
+ [ToolkitSample(id: nameof(MySample), "My Sample", description: "")]
+ [ToolkitSampleTextOption("Caption", placeholderText: "Enter a caption")]
+ public sealed partial class MySample : Page;
+ """);
+
+ Assert.AreEqual("Enter a caption", declaration.Options.Single().DefaultValue);
+ }
+
+ [TestMethod]
+ public void AttributesOnOtherClassesAreNotMixedTogether()
+ {
+ var declarations = ParseAll(
+ """
+ [ToolkitSample(id: nameof(First), "First", description: "")]
+ [ToolkitSampleBoolOption("IsOn", true)]
+ public sealed partial class First : Page;
+
+ [ToolkitSample(id: nameof(Second), "Second", description: "")]
+ public sealed partial class Second : Page;
+ """);
+
+ Assert.AreEqual(2, declarations.Count);
+ Assert.AreEqual(1, declarations[0].Options.Count);
+ Assert.AreEqual(0, declarations[1].Options.Count);
+ }
+
+ [TestMethod]
+ public void AClassWithNoSampleAttributeIsIgnored()
+ {
+ var declarations = ParseAll("public sealed partial class NotASample : Page;");
+
+ Assert.AreEqual(0, declarations.Count);
+ }
+
+ private SampleDeclaration ParseSingle(string source) => ParseAll(source).Single();
+
+ private static List ParseAll(string source)
+ {
+ // Deliberately outside the repository: a .xaml.cs written anywhere under the test
+ // project would be picked up by the SDK's implicit compile glob on the next build, and
+ // these fragments are not compilable C# files.
+ var directory = Path.Combine(Path.GetTempPath(), "toolkit-sample-index-tests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+ var path = Path.Combine(directory, "Test.xaml.cs");
+
+ try
+ {
+ File.WriteAllText(path, source);
+ return SampleDeclaration.Parse(path, "components/Test/samples/Test.xaml.cs");
+ }
+ finally
+ {
+ Directory.Delete(directory, recursive: true);
+ }
+ }
+}
diff --git a/tools/SampleIndexExporter.Tests/SampleIndexExporter.Tests.csproj b/tools/SampleIndexExporter.Tests/SampleIndexExporter.Tests.csproj
new file mode 100644
index 00000000..ee68e390
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/SampleIndexExporter.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+
+ net9.0
+ CommunityToolkit.SampleIndex.Tests
+ CommunityToolkit.SampleIndexExporter.Tests
+ enable
+ nullable
+ latest
+ enable
+ false
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/SampleIndexExporter.Tests/XamlFragmentTests.cs b/tools/SampleIndexExporter.Tests/XamlFragmentTests.cs
new file mode 100644
index 00000000..e6a45e93
--- /dev/null
+++ b/tools/SampleIndexExporter.Tests/XamlFragmentTests.cs
@@ -0,0 +1,361 @@
+// 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.VisualStudio.TestTools.UnitTesting;
+
+namespace CommunityToolkit.SampleIndex.Tests;
+
+///
+/// Unit tests for turning a sample page into a pasteable fragment.
+///
+[TestClass]
+public class XamlFragmentTests
+{
+ private const string PageHeader =
+ """
+
+ """;
+
+ [TestMethod]
+ public void ThePageWrapperIsRemoved()
+ {
+ var result = Extract("");
+
+ Assert.IsNull(result.Error);
+ Assert.AreEqual("", result.Xaml);
+ }
+
+ [TestMethod]
+ public void OnlyTheImportsTheBodyUsesAreKept()
+ {
+ // A snippet that hands over six xmlns declarations for one control makes the reader
+ // work out which of them they need. Keeping only what the body uses removes that work.
+ var result = Extract("");
+
+ CollectionAssert.AreEqual(
+ new[] { "xmlns:controls=\"using:CommunityToolkit.WinUI.Controls\"" },
+ result.XmlnsImports);
+ }
+
+ [TestMethod]
+ public void DesignTimeImportsAreNeverKept()
+ {
+ var result = Extract("");
+
+ Assert.AreEqual(0, result.XmlnsImports.Count(i => i.Contains("blend") || i.Contains("markup-compatibility")));
+ }
+
+ [TestMethod]
+ public void AnOptionBindingBecomesItsDefaultValue()
+ {
+ var result = Extract(
+ "",
+ new SampleOption("IsCardEnabled", "True"));
+
+ StringAssert.Contains(result.Xaml!, "IsEnabled=\"True\"");
+ CollectionAssert.Contains(result.OptionsResolved, "IsCardEnabled=True");
+ }
+
+ [TestMethod]
+ public void AnAttributeNamedLikeAnOptionIsNotMistakenForABinding()
+ {
+ // IsAlwaysOn="{x:Bind IsAlwaysOn}" mentions the option name twice: once as the
+ // attribute and once as the binding path. Matching on raw text flagged the attribute
+ // name and reported a perfectly resolvable sample as unpasteable.
+ var result = Extract(
+ "",
+ new SampleOption("IsAlwaysOn", "True"));
+
+ Assert.IsNull(result.Error);
+ StringAssert.Contains(result.Xaml!, "IsAlwaysOn=\"True\"");
+ }
+
+ [TestMethod]
+ public void AnOptionBindingWithNoStaticValueDropsTheWholeAttribute()
+ {
+ // Leaving the attribute with an unresolved value would not compile, and inventing a
+ // value would show markup the author never wrote. Removing the attribute gives the
+ // control its own default, which is the honest answer.
+ var result = Extract(
+ "",
+ new SampleOption("Shape", null));
+
+ Assert.IsNull(result.Error);
+ Assert.IsFalse(result.Xaml!.Contains("Shape", StringComparison.Ordinal));
+ StringAssert.Contains(result.Xaml!, "Content=\"Hi\"");
+ CollectionAssert.Contains(result.OptionBindingsDropped, "Shape");
+ }
+
+ [TestMethod]
+ public void ABindingToTheSamplePageIsSettled()
+ {
+ // ElementName points at the Page root, which is exactly the element being discarded.
+ var result = Extract(
+ """
+
+
+
+ """,
+ new SampleOption("IsOn", "True"));
+
+ Assert.IsNull(result.Error);
+ Assert.IsFalse(result.Xaml!.Contains("ElementName", StringComparison.Ordinal));
+ }
+
+ [TestMethod]
+ public void ABindingBetweenTwoElementsInTheFragmentIsLeftAlone()
+ {
+ // This one works after a paste, because both ends of it come along.
+ var result = Extract(
+ """
+
+
+
+
+ """);
+
+ Assert.IsNull(result.Error);
+ StringAssert.Contains(result.Xaml!, "ElementName=Toggle");
+ }
+
+ [TestMethod]
+ public void CharacterReferencesSurviveExtraction()
+ {
+ // Glyphs are written as and mean nothing once decoded into a private-use
+ // character, so the fragment is taken as source text rather than re-serialized.
+ var result = Extract("");
+
+ StringAssert.Contains(result.Xaml!, "");
+ }
+
+ [TestMethod]
+ public void AnEmptyPageIsWithheldRatherThanPublishedBlank()
+ {
+ var result = Extract(" \n ");
+
+ Assert.IsNotNull(result.Error);
+ Assert.IsNull(result.Xaml);
+ }
+
+ [TestMethod]
+ public void MalformedMarkupIsWithheldRatherThanPublishedBroken()
+ {
+ var result = XamlFragment.Extract("", []);
+
+ Assert.IsNotNull(result.Error);
+ Assert.IsNull(result.Xaml);
+ }
+
+ [TestMethod]
+ public void UnsettledBindingsFindsASampleAppReference()
+ {
+ var leftovers = XamlFragment.UnsettledBindings(
+ "",
+ [new SampleOption("IsOn", "True")]);
+
+ Assert.AreEqual(1, leftovers.Count);
+ }
+
+ [TestMethod]
+ public void UnsettledBindingsFindsABindingToAMissingElement()
+ {
+ var leftovers = XamlFragment.UnsettledBindings(
+ "",
+ []);
+
+ Assert.AreEqual(1, leftovers.Count);
+ }
+
+ [TestMethod]
+ public void AConditionalWindowsPrefixIsRemoved()
+ {
+ // xmlns:win="...presentation" is Uno's "Windows only" prefix, and on Windows it names
+ // the same elements as no prefix at all.
+ var result = XamlFragment.Extract(
+ """
+
+
+
+ """,
+ []);
+
+ Assert.IsNull(result.Error);
+ Assert.AreEqual("", result.Xaml);
+ Assert.AreEqual(0, result.XmlnsImports.Count);
+ }
+
+ [TestMethod]
+ public void PageResourcesMoveOntoTheElementThatSurvives()
+ {
+ // belongs to the page the fragment discards. Left as it is, the
+ // snippet fails to compile the moment it is pasted anywhere that is not a Page.
+ var result = Extract(
+ """
+
+
+
+
+
+
+
+ """);
+
+ Assert.IsNull(result.Error);
+ Assert.IsFalse(result.Xaml!.Contains("Page.", StringComparison.Ordinal));
+ StringAssert.Contains(result.Xaml!, "");
+ StringAssert.StartsWith(result.Xaml!, "
+
+
+
+
+ """);
+
+ Assert.IsNull(result.Error);
+ StringAssert.Contains(result.Xaml!, "");
+ StringAssert.Contains(result.Xaml!, "");
+ }
+
+ [TestMethod]
+ public void ACastInABindingPathDoesNotHideTheOption()
+ {
+ // x:Bind writes a cast as a parenthesised type ahead of the path. Reading the cast as
+ // part of the path made the lookup miss, so a binding with a readable default was
+ // treated as having none and UniformGrid published without its spans or first column.
+ var result = Extract(
+ "",
+ new SampleOption("Item1RowSpan", "2"));
+
+ Assert.IsNull(result.Error);
+ StringAssert.Contains(result.Xaml!, "Grid.RowSpan=\"2\"");
+ CollectionAssert.Contains(result.OptionsResolved, "Item1RowSpan=2");
+ Assert.AreEqual(0, result.OptionBindingsDropped.Count);
+ }
+
+ [TestMethod]
+ public void AnAttachedPropertyPathIsNotMistakenForACast()
+ {
+ // {Binding (Grid.Row)} is a path, not a cast: the parentheses are the whole path and
+ // there is nothing after them. Stripping them would turn the path into the empty
+ // string and lose the binding.
+ var result = Extract("");
+
+ Assert.IsNull(result.Error);
+ StringAssert.Contains(result.Xaml!, "{Binding (Grid.Row)}");
+ }
+
+ [TestMethod]
+ public void MarkupInsideACommentIsNotRewritten()
+ {
+ // A comment ends at '-->', not at the first '>'. Stopping at the first one resumed the
+ // scan inside the comment — here, straight after the start tag — and
+ // edited the author's commented-out alternative as though it were live markup.
+ var result = Extract(
+ """
+
+
+ """,
+ new SampleOption("IsCardEnabled", "True"));
+
+ Assert.IsNull(result.Error);
+ StringAssert.Contains(result.Xaml!, "");
+ Assert.AreEqual(0, result.OptionsResolved.Count);
+ Assert.AreEqual(0, result.OptionBindingsDropped.Count);
+ }
+
+ [TestMethod]
+ public void AnElementNameInsideACommentDoesNotCountAsDeclared()
+ {
+ // The commented-out element does not exist, so the binding that names it is dangling
+ // and its attribute has to go. Reading the comment as markup would publish a binding
+ // pointing at nothing.
+ var result = Extract(
+ """
+
+
+ """);
+
+ Assert.IsNull(result.Error);
+ Assert.IsFalse(result.Xaml!.Contains("ElementName=Source", StringComparison.Ordinal));
+ }
+
+ [TestMethod]
+ public void APrefixUsedOnlyInsideACommentIsNotPublishedAsAnImport()
+ {
+ // An import tells the reader to reference a package. One needed only by markup the
+ // author commented out is a package the published fragment does not use.
+ var result = Extract(
+ """
+
+
+ """);
+
+ Assert.AreEqual(0, result.XmlnsImports.Count);
+ }
+
+ [TestMethod]
+ public void AnApostropheInACommentDoesNotSwallowTheRestOfTheFragment()
+ {
+ // The prefix stripper tracks quotes so it does not stop at a '>' inside an attribute
+ // value. Prose in a comment is not attribute values, and reading it as such opened a
+ // quote that never closed, abandoning the rest of the fragment.
+ var result = XamlFragment.Extract(
+ """
+
+
+
+
+ """,
+ []);
+
+ Assert.IsNull(result.Error);
+ StringAssert.Contains(result.Xaml!, "");
+ }
+
+ [TestMethod]
+ public void UnsettledBindingsAcceptsMarkupWithNoBindings()
+ {
+ var leftovers = XamlFragment.UnsettledBindings("", []);
+
+ Assert.AreEqual(0, leftovers.Count);
+ }
+
+ [TestMethod]
+ public void UnsettledBindingsAcceptsStaticResourceLookups()
+ {
+ // StaticResource resolves against the reader's own resource dictionary, and the ones
+ // used here are system styles that every WinUI app has.
+ var leftovers = XamlFragment.UnsettledBindings(
+ "",
+ []);
+
+ Assert.AreEqual(0, leftovers.Count);
+ }
+
+ private static XamlExtraction Extract(string body, params SampleOption[] options) =>
+ XamlFragment.Extract($"{PageHeader}\n{body}\n", options);
+}
diff --git a/tools/SampleIndexExporter/IndexGenerator.cs b/tools/SampleIndexExporter/IndexGenerator.cs
new file mode 100644
index 00000000..eba576d4
--- /dev/null
+++ b/tools/SampleIndexExporter/IndexGenerator.cs
@@ -0,0 +1,423 @@
+// 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.Text.RegularExpressions;
+
+namespace CommunityToolkit.SampleIndex;
+
+///
+/// Builds the published index from the repository's own sample sources.
+///
+///
+/// The index is keyed by documentation file. Each components/*/samples/*.md becomes one
+/// entry carrying its frontmatter, and its [!SAMPLE] markers are resolved, in document
+/// order, to the [ToolkitSample] declarations and XAML files they name.
+///
+/// This is not a source generator. ToolkitSampleMetadataGenerator is an
+/// incremental generator, and AddSource only emits C# into a compilation — there is no
+/// way for it to write a JSON file into the repository. The frontmatter and marker patterns are
+/// shared with it deliberately, so the index cannot disagree with the sample app about what a
+/// document presents.
+///
+internal static partial class IndexGenerator
+{
+ /// Where the generated index is committed, relative to the repository root.
+ public const string IndexRelativePath = "catalog/toolkit-samples.json";
+
+ private const string RepositoryBlobUrl = "https://github.com/CommunityToolkit/Windows/blob/main/";
+
+ /// Characters that are not safe in an identifier a consumer may put in a URL.
+ [GeneratedRegex("[^a-z0-9]+")]
+ private static partial Regex NonSlugRegex();
+
+ ///
+ /// Walk up from a starting directory to the repository root.
+ ///
+ public static string FindRepoRoot(string start)
+ {
+ var directory = new DirectoryInfo(start);
+
+ while (directory is not null)
+ {
+ if (Directory.Exists(Path.Combine(directory.FullName, "components"))
+ && File.Exists(Path.Combine(directory.FullName, "Directory.Build.props")))
+ {
+ return directory.FullName;
+ }
+
+ directory = directory.Parent;
+ }
+
+ throw new DirectoryNotFoundException(
+ $"Could not find the repository root above '{start}'. Pass --repo-root explicitly.");
+ }
+
+ /// Generate the index from the tree rooted at .
+ public static GenerationResult Generate(string repoRoot)
+ {
+ var issues = new List();
+ var withheld = new List();
+ var index = new SampleIndex();
+ var usedIds = new Dictionary(StringComparer.Ordinal);
+
+ var componentsRoot = Path.Combine(repoRoot, "components");
+ var components = Directory.GetDirectories(componentsRoot)
+ .OrderBy(Path.GetFileName, StringComparer.Ordinal);
+
+ foreach (var componentPath in components)
+ {
+ var component = Path.GetFileName(componentPath);
+ var samplesRoot = Path.Combine(componentPath, "samples");
+ if (!Directory.Exists(samplesRoot))
+ {
+ continue;
+ }
+
+ var nugetPackage = NuGetPackage(componentPath);
+ var declarations = ReadDeclarations(repoRoot, samplesRoot, issues);
+ var referenced = new HashSet(StringComparer.Ordinal);
+
+ var documents = Directory.GetFiles(samplesRoot, "*.md", SearchOption.AllDirectories)
+ .OrderBy(p => Relative(repoRoot, p), StringComparer.Ordinal);
+
+ foreach (var documentPath in documents)
+ {
+ var relativePath = Relative(repoRoot, documentPath);
+ var document = MarkdownDocument.Parse(documentPath, relativePath);
+ if (document is null)
+ {
+ // No frontmatter: a plain markdown file that happens to live in samples/.
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(document.Title))
+ {
+ issues.Add(new IndexIssue(
+ IssueSeverity.Error,
+ relativePath,
+ "the frontmatter has no 'title' field, so the entry would have no name."));
+ continue;
+ }
+
+ var entry = BuildEntry(
+ repoRoot, component, nugetPackage, document, declarations, referenced, issues, withheld, usedIds);
+
+ if (entry is not null)
+ {
+ index.Controls.Add(entry);
+ }
+ }
+
+ foreach (var declaration in declarations.Values.OrderBy(d => d.Id, StringComparer.Ordinal))
+ {
+ if (!referenced.Contains(declaration.Id))
+ {
+ issues.Add(new IndexIssue(
+ IssueSeverity.Warning,
+ declaration.RelativePath,
+ $"sample '{declaration.Id}' is not referenced by any '> [!SAMPLE ...]' marker, so it is "
+ + "not published and does not appear in the sample app."));
+ }
+ }
+ }
+
+ withheld.Sort((a, b) => string.CompareOrdinal(a.SampleId, b.SampleId));
+
+ return new GenerationResult(index, issues, withheld);
+ }
+
+ /// Serialize an index exactly as it is committed.
+ public static string Serialize(SampleIndex index) => IndexJsonContext.Serialize(index);
+
+ ///
+ /// Keywords the documentation implies rather than states.
+ ///
+ ///
+ /// A reader searching for "layout" or "input" is describing what they want to do, not
+ /// naming a control, and the category a component is filed under is the repository's own
+ /// answer to that question. It is kept separate from the author's keywords line,
+ /// which consumers weight more heavily precisely because a person chose those words.
+ ///
+ private static List GeneratedKeywords(MarkdownDocument document)
+ {
+ var keywords = new List();
+
+ foreach (var value in new[] { document.Category, document.Subcategory })
+ {
+ if (!string.IsNullOrWhiteSpace(value) && !keywords.Contains(value, StringComparer.OrdinalIgnoreCase))
+ {
+ keywords.Add(value.Trim());
+ }
+ }
+
+ return keywords;
+ }
+
+ private static IndexedControl? BuildEntry(
+ string repoRoot,
+ string component,
+ string? nugetPackage,
+ MarkdownDocument document,
+ Dictionary declarations,
+ HashSet referenced,
+ List issues,
+ List withheld,
+ Dictionary usedIds)
+ {
+ var id = UniqueId(document, issues, usedIds);
+ if (id is null)
+ {
+ return null;
+ }
+
+ var entry = new IndexedControl
+ {
+ Id = id,
+ Name = document.Title!,
+ Description = document.Description,
+ NuGetPackage = nugetPackage,
+ CuratedKeywords = NullIfEmpty(MarkdownDocument.SplitKeywords(document.Keywords)),
+ Keywords = NullIfEmpty(GeneratedKeywords(document)),
+ Docs =
+ [
+ new IndexedDocLink
+ {
+ Title = document.Title,
+ Uri = RepositoryBlobUrl + document.RelativePath,
+ },
+ ],
+ Toolkit = new ControlExtension
+ {
+ Component = component,
+ DocumentPath = document.RelativePath,
+ Category = document.Category,
+ Subcategory = document.Subcategory,
+ Author = document.Author,
+ Experimental = document.Experimental,
+ },
+ };
+
+ // Imports are published once per entry, unioned over the samples that carry code: the
+ // contract has consumers prepend them to every sample of the control, so collecting
+ // them here is what lets each sample's code leave them out.
+ var usings = new List();
+
+ foreach (var sampleId in document.SampleIds)
+ {
+ if (!declarations.TryGetValue(sampleId, out var declaration))
+ {
+ issues.Add(new IndexIssue(
+ IssueSeverity.Error,
+ document.RelativePath,
+ $"'> [!SAMPLE {sampleId}]' names a sample that does not exist. No class in this component "
+ + $"declares [ToolkitSample(id: nameof({sampleId}), ...)], so the sample app renders nothing here."));
+ continue;
+ }
+
+ referenced.Add(sampleId);
+
+ foreach (var unreadable in declaration.UnreadableArguments)
+ {
+ issues.Add(new IndexIssue(
+ IssueSeverity.Error,
+ declaration.RelativePath,
+ $"the [ToolkitSample] {unreadable} argument is not a literal, nameof or interpolated string, "
+ + "so its value cannot be read without compiling. Rewrite it in one of those forms."));
+ }
+
+ var sample = BuildSample(repoRoot, declaration, document, issues, withheld, usings);
+ if (sample is not null)
+ {
+ entry.Samples.Add(sample);
+ }
+ }
+
+ usings.Sort(StringComparer.Ordinal);
+ entry.Usings = NullIfEmpty(usings);
+
+ // Documentation pages that present no samples are kept. Several components — Extensions
+ // and Helpers especially — document APIs that have no markup to show, and dropping them
+ // would make the index disagree with the promise that it carries one entry per
+ // documentation file. A consumer that only wants pasteable markup filters on a
+ // non-empty samples array; one building search over the whole toolkit keeps them.
+ return entry;
+ }
+
+ private static IndexedSample? BuildSample(
+ string repoRoot,
+ SampleDeclaration declaration,
+ MarkdownDocument document,
+ List issues,
+ List withheld,
+ List usings)
+ {
+ var xamlPath = Path.ChangeExtension(Path.Combine(repoRoot, declaration.RelativePath.Replace('/', Path.DirectorySeparatorChar)), null);
+ if (!File.Exists(xamlPath))
+ {
+ withheld.Add(new WithheldSample(declaration.Id, "the sample declares no .xaml file"));
+ return null;
+ }
+
+ var extraction = XamlFragment.Extract(File.ReadAllText(xamlPath), declaration.Options);
+ if (extraction.Xaml is null)
+ {
+ withheld.Add(new WithheldSample(declaration.Id, extraction.Error ?? "its XAML could not be extracted"));
+ return null;
+ }
+
+ // The contract promises pasteable markup, so a binding left pointing at something only
+ // the sample app has is a defect in this exporter rather than something to publish.
+ foreach (var problem in XamlFragment.UnsettledBindings(extraction.Xaml, declaration.Options))
+ {
+ issues.Add(new IndexIssue(
+ IssueSeverity.Error,
+ Relative(repoRoot, xamlPath),
+ $"the published XAML is not pasteable: {problem}. The extractor must resolve or remove it."));
+ return null;
+ }
+
+ var code = SampleCode.Extract(
+ Path.Combine(repoRoot, declaration.RelativePath.Replace('/', Path.DirectorySeparatorChar)),
+ declaration.TypeName,
+ ToolkitApi.ForRepository(repoRoot));
+
+ foreach (var import in code.Usings)
+ {
+ if (!usings.Contains(import, StringComparer.Ordinal))
+ {
+ usings.Add(import);
+ }
+ }
+
+ return new IndexedSample
+ {
+ Header = declaration.DisplayName,
+ Details = document.SampleProse.TryGetValue(declaration.Id, out var prose) ? prose : declaration.Description,
+ Xaml = extraction.Xaml,
+ Code = code.Code,
+ Language = code.Code is null ? null : "csharp",
+ XmlnsImports = NullIfEmpty(extraction.XmlnsImports),
+ Toolkit = new SampleExtension
+ {
+ SampleId = declaration.Id,
+ SourcePath = Relative(repoRoot, xamlPath),
+ OptionsResolved = NullIfEmpty(extraction.OptionsResolved),
+ OptionBindingsDropped = NullIfEmpty(extraction.OptionBindingsDropped),
+ },
+ };
+ }
+
+ ///
+ /// Read every [ToolkitSample] declaration in a component, keyed by sample id.
+ ///
+ private static Dictionary ReadDeclarations(
+ string repoRoot,
+ string samplesRoot,
+ List issues)
+ {
+ var declarations = new Dictionary(StringComparer.Ordinal);
+ var displayNames = new Dictionary(StringComparer.Ordinal);
+
+ var files = Directory.GetFiles(samplesRoot, "*.xaml.cs", SearchOption.AllDirectories)
+ .OrderBy(p => Relative(repoRoot, p), StringComparer.Ordinal);
+
+ foreach (var file in files)
+ {
+ foreach (var declaration in SampleDeclaration.Parse(file, Relative(repoRoot, file)))
+ {
+ if (declarations.TryGetValue(declaration.Id, out var existing))
+ {
+ issues.Add(new IndexIssue(
+ IssueSeverity.Error,
+ declaration.RelativePath,
+ $"sample id '{declaration.Id}' is already declared by {existing.RelativePath}. "
+ + "Ids must be unique within a component so a marker can name exactly one sample."));
+ continue;
+ }
+
+ declarations[declaration.Id] = declaration;
+
+ if (declaration.DisplayName is { Length: > 0 } displayName)
+ {
+ if (displayNames.TryGetValue(displayName, out var duplicate))
+ {
+ issues.Add(new IndexIssue(
+ IssueSeverity.Warning,
+ declaration.RelativePath,
+ $"[ToolkitSample] display name \"{displayName}\" is already used by "
+ + $"{duplicate.RelativePath}. Both samples render under that one title in the sample "
+ + "app, and both are published under it here."));
+ }
+ else
+ {
+ displayNames[displayName] = declaration;
+ }
+ }
+ }
+ }
+
+ return declarations;
+ }
+
+ ///
+ /// The NuGet package a component ships as, taken from its source project's file name.
+ ///
+ private static string? NuGetPackage(string componentPath)
+ {
+ var sourceRoot = Path.Combine(componentPath, "src");
+ if (!Directory.Exists(sourceRoot))
+ {
+ return null;
+ }
+
+ var projects = Directory.GetFiles(sourceRoot, "*.csproj", SearchOption.TopDirectoryOnly)
+ .OrderBy(p => p, StringComparer.Ordinal)
+ .ToList();
+
+ return projects.Count == 1 ? Path.GetFileNameWithoutExtension(projects[0]) : null;
+ }
+
+ ///
+ /// Derive the entry's identifier from the documentation file name.
+ ///
+ ///
+ /// The file name rather than the title, because consumers key sample ids off this and a
+ /// title is prose that gets reworded.
+ ///
+ /// Two components documenting the same file name is reported rather than resolved.
+ /// Qualifying one of them with its component name would have to pick which one, and any
+ /// rule for picking depends on the set of components present — so adding a component could
+ /// change the id of an entry that already shipped, which is the one thing an identifier
+ /// documented as stable must never do. A collision is a build failure a contributor settles
+ /// by renaming, and every existing id stays where it is.
+ ///
+ private static string? UniqueId(
+ MarkdownDocument document,
+ List issues,
+ Dictionary usedIds)
+ {
+ var id = Slug(Path.GetFileNameWithoutExtension(document.Path));
+
+ if (!usedIds.TryGetValue(id, out var owner))
+ {
+ usedIds[id] = document.RelativePath;
+ return id;
+ }
+
+ issues.Add(new IndexIssue(
+ IssueSeverity.Error,
+ document.RelativePath,
+ $"entry id '{id}' is already used by {owner}. Rename one of the documentation files."));
+
+ return null;
+ }
+
+ private static string Slug(string value) =>
+ NonSlugRegex().Replace(value.ToLowerInvariant(), "-").Trim('-');
+
+ private static List? NullIfEmpty(List values) => values.Count == 0 ? null : values;
+
+ private static string Relative(string repoRoot, string path) =>
+ Path.GetRelativePath(repoRoot, path).Replace('\\', '/');
+}
diff --git a/tools/SampleIndexExporter/IndexIssue.cs b/tools/SampleIndexExporter/IndexIssue.cs
new file mode 100644
index 00000000..5244ddbf
--- /dev/null
+++ b/tools/SampleIndexExporter/IndexIssue.cs
@@ -0,0 +1,46 @@
+// 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.SampleIndex;
+
+/// How serious a finding is.
+internal enum IssueSeverity
+{
+ /// The index is still publishable, but something upstream needs attention.
+ Warning,
+
+ /// The index cannot be published as-is.
+ Error,
+}
+
+/// One finding about the sample data, tied to the file that caused it.
+/// Whether this blocks publishing.
+/// Repository-relative path of the file at fault.
+/// What is wrong, in terms the file's author would recognize.
+internal sealed record IndexIssue(IssueSeverity Severity, string File, string Message)
+{
+ public override string ToString() => $"{File}: {Message}";
+}
+
+/// A sample that was left out of the index, and why.
+/// The sample's [ToolkitSample] id.
+/// Why it could not be published.
+internal sealed record WithheldSample(string SampleId, string Reason)
+{
+ public override string ToString() => $"{SampleId}: {Reason}";
+}
+
+/// Everything one generation run produced.
+/// The index, ready to serialize.
+/// Findings, in file order.
+/// Samples deliberately left out, in id order.
+internal sealed record GenerationResult(
+ SampleIndex Index,
+ IReadOnlyList Issues,
+ IReadOnlyList Withheld)
+{
+ public IEnumerable Errors => Issues.Where(i => i.Severity == IssueSeverity.Error);
+
+ public IEnumerable Warnings => Issues.Where(i => i.Severity == IssueSeverity.Warning);
+}
diff --git a/tools/SampleIndexExporter/IndexJsonContext.cs b/tools/SampleIndexExporter/IndexJsonContext.cs
new file mode 100644
index 00000000..a31afe9b
--- /dev/null
+++ b/tools/SampleIndexExporter/IndexJsonContext.cs
@@ -0,0 +1,48 @@
+// 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.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Unicode;
+
+namespace CommunityToolkit.SampleIndex;
+
+///
+/// Source-generated serialization for the index. The project disables reflection-based
+/// serialization, so a type reached by the serializer that is not listed here fails loudly at
+/// runtime rather than being written with default conventions.
+///
+[JsonSourceGenerationOptions(
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonSerializable(typeof(SampleIndex))]
+internal sealed partial class IndexJsonContext : JsonSerializerContext
+{
+ ///
+ /// Serialize the index exactly as it is committed.
+ ///
+ ///
+ /// The encoder is widened past the default so that the characters that actually occur in
+ /// this corpus — Segoe icon glyphs, typographic quotes, non-breaking spaces — are written
+ /// as themselves instead of as \uXXXX escapes. The file is read by people as well as
+ /// by tools, and the default encoder would render a page of XAML unreadable to both.
+ ///
+ /// Line endings are forced to \n. The indenting writer uses the running
+ /// machine's newline, so leaving it alone would mean a Windows contributor and a Linux CI
+ /// agent generate byte-different files from identical sources, and the staleness check
+ /// would fail on one of them for no reason a reader could act on. Output ends with a single
+ /// newline so the file is well-formed for text tooling.
+ ///
+ public static string Serialize(SampleIndex index)
+ {
+ var options = new JsonSerializerOptions(Default.Options)
+ {
+ Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
+ NewLine = "\n",
+ };
+
+ return JsonSerializer.Serialize(index, typeof(SampleIndex), new IndexJsonContext(options)) + "\n";
+ }
+}
diff --git a/tools/SampleIndexExporter/IndexModels.cs b/tools/SampleIndexExporter/IndexModels.cs
new file mode 100644
index 00000000..f8cde664
--- /dev/null
+++ b/tools/SampleIndexExporter/IndexModels.cs
@@ -0,0 +1,245 @@
+// 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.Text.Json.Serialization;
+
+namespace CommunityToolkit.SampleIndex;
+
+///
+/// The published index document.
+///
+///
+/// The shape is the shared WinUI sample index contract, so a consumer that already reads
+/// another source's index reads this one with the same parser. Anything specific to this
+/// repository lives under the toolkit extension objects, which the contract allows and
+/// which a consumer may ignore entirely.
+///
+/// Every collection is serialized in a deterministic order and no timestamp is emitted,
+/// so regenerating on an unchanged tree produces a byte-identical file. That is what lets CI
+/// compare the committed file against a fresh generate.
+///
+internal sealed class SampleIndex
+{
+ [JsonPropertyName("schemaVersion")]
+ public int SchemaVersion { get; set; } = 1;
+
+ [JsonPropertyName("source")]
+ public string Source { get; set; } = "toolkit";
+
+ ///
+ /// Documented entries, one per components/*/samples/*.md.
+ ///
+ ///
+ /// Named controls by the contract. Keyed by documentation file rather than by
+ /// component because a component routinely documents several controls — SettingsControls
+ /// documents SettingsCard and SettingsExpander, Sizers documents ContentSizer, GridSplitter
+ /// and PropertySizer — and the documentation file is where an author already recorded which
+ /// is which. Keying by component would push that split onto every consumer.
+ ///
+ [JsonPropertyName("controls")]
+ public List Controls { get; set; } = [];
+
+ [JsonIgnore]
+ public int ControlCount => Controls.Count;
+
+ [JsonIgnore]
+ public int SampleCount => Controls.Sum(c => c.Samples.Count);
+}
+
+/// One documentation file and the samples it presents, in document order.
+internal sealed class IndexedControl
+{
+ ///
+ /// Stable, URL-safe identifier, derived from the documentation file name
+ /// (SettingsCard.md becomes settingscard).
+ ///
+ ///
+ /// Derived from the file name, not the title, because consumers key sample ids off this and
+ /// a title is prose that gets reworded. Uniqueness across the repository is enforced when
+ /// the index is generated.
+ ///
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ /// Display name: the documentation file's title frontmatter field.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// One-line summary: the documentation file's description frontmatter field.
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ ///
+ /// NuGet package that must be referenced, read from the component's
+ /// src/<Package>.csproj file name.
+ ///
+ [JsonPropertyName("nugetPackage")]
+ public string? NuGetPackage { get; set; }
+
+ ///
+ /// Namespaces this entry's published C# needs imported, unioned over its samples.
+ ///
+ ///
+ /// Per control rather than per sample because that is where the contract puts it: a
+ /// consumer prepends the whole list as using X; lines to each of the entry's
+ /// samples, which is also why no sample's repeats them.
+ ///
+ /// Narrowed to the imports the published members still use. A sample file imports
+ /// what its whole page needed, and most of that page is scaffolding that is not published,
+ /// so publishing the file's imports verbatim would ask the reader to reference packages for
+ /// code they were never handed.
+ ///
+ [JsonPropertyName("usings")]
+ public List? Usings { get; set; }
+
+ ///
+ /// The documentation file's keywords frontmatter field, split on commas.
+ ///
+ ///
+ /// Published as curatedKeywords rather than keywords because the contract
+ /// reserves that field for terms the sample's own author wrote, which is exactly what
+ /// frontmatter keywords are.
+ ///
+ [JsonPropertyName("curatedKeywords")]
+ public List? CuratedKeywords { get; set; }
+
+ ///
+ /// Keywords derived from the documentation rather than written by its author: the category
+ /// and subcategory the component is filed under. Consumers weight these below
+ /// , which is correct — nobody chose them for this control.
+ ///
+ [JsonPropertyName("keywords")]
+ public List? Keywords { get; set; }
+
+ [JsonPropertyName("docs")]
+ public List? Docs { get; set; }
+
+ [JsonPropertyName("toolkit")]
+ public ControlExtension? Toolkit { get; set; }
+
+ [JsonPropertyName("samples")]
+ public List Samples { get; set; } = [];
+}
+
+internal sealed class IndexedDocLink
+{
+ [JsonPropertyName("title")]
+ public string? Title { get; set; }
+
+ [JsonPropertyName("uri")]
+ public string Uri { get; set; } = string.Empty;
+}
+
+///
+/// Repository-specific metadata that has no place in the shared contract. Consumers may ignore
+/// all of it; nothing here is required to use a sample.
+///
+internal sealed class ControlExtension
+{
+ /// Component directory name, e.g. SettingsControls.
+ [JsonPropertyName("component")]
+ public string Component { get; set; } = string.Empty;
+
+ /// Repository-relative path of the documentation file this entry was built from.
+ [JsonPropertyName("documentPath")]
+ public string DocumentPath { get; set; } = string.Empty;
+
+ [JsonPropertyName("category")]
+ public string? Category { get; set; }
+
+ [JsonPropertyName("subcategory")]
+ public string? Subcategory { get; set; }
+
+ [JsonPropertyName("author")]
+ public string? Author { get; set; }
+
+ /// True when the documentation file marks the component experimental.
+ [JsonPropertyName("experimental")]
+ public bool? Experimental { get; set; }
+}
+
+/// One sample: a [!SAMPLE] marker resolved to its declaration and its XAML.
+internal sealed class IndexedSample
+{
+ /// The displayName argument of the sample's [ToolkitSample] attribute.
+ [JsonPropertyName("header")]
+ public string? Header { get; set; }
+
+ ///
+ /// Prose describing this sample: the paragraphs the documentation file places immediately
+ /// before the sample's marker, falling back to the attribute's description argument.
+ ///
+ [JsonPropertyName("details")]
+ public string? Details { get; set; }
+
+ ///
+ /// The sample's XAML as a pasteable fragment: the body of its root element, with the
+ /// sample-app-only bindings resolved. Never the verbatim file, which carries an
+ /// x:Class and a <Page> root that belong to the sample app.
+ ///
+ [JsonPropertyName("xaml")]
+ public string? Xaml { get; set; }
+
+ ///
+ /// The C# the sample demonstrates: the handlers its markup calls and the types its markup
+ /// binds to, as members a reader drops into their own page. Omitted when the sample's
+ /// code-behind is only the page scaffolding, which is the common case.
+ ///
+ ///
+ /// Carries no using directives. The namespaces it needs are published once on the
+ /// owning control as , which the contract has consumers
+ /// prepend to every sample of that control.
+ ///
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+
+ /// Language of . Always csharp when code is present.
+ [JsonPropertyName("language")]
+ public string? Language { get; set; }
+
+ ///
+ /// The XAML namespace declarations this fragment actually uses, emitted verbatim.
+ ///
+ ///
+ /// Per sample rather than per control: sibling samples of one control legitimately pull in
+ /// different namespaces, and an import a sample does not use is an instruction to add a
+ /// package reference it does not need.
+ ///
+ [JsonPropertyName("xmlnsImports")]
+ public List? XmlnsImports { get; set; }
+
+ [JsonPropertyName("toolkit")]
+ public SampleExtension? Toolkit { get; set; }
+}
+
+///
+/// Per-sample extraction metadata. Informational: the published xaml is already
+/// pasteable, and these fields only explain what had to change to make it so.
+///
+internal sealed class SampleExtension
+{
+ /// The sample's [ToolkitSample] id, which is also its [!SAMPLE] marker.
+ [JsonPropertyName("sampleId")]
+ public string SampleId { get; set; } = string.Empty;
+
+ /// Repository-relative path of the sample's .xaml file.
+ [JsonPropertyName("sourcePath")]
+ public string SourcePath { get; set; } = string.Empty;
+
+ ///
+ /// Sample-app option members whose bindings were replaced with the option's default value,
+ /// as Name=Value.
+ ///
+ [JsonPropertyName("optionsResolved")]
+ public List? OptionsResolved { get; set; }
+
+ ///
+ /// Attributes removed because they bound to a sample-app option through an expression with
+ /// no static value — a function binding, for instance. The contract requires published XAML
+ /// to be pasteable, and an attribute is the one construct whose absence yields the target
+ /// control's own default.
+ ///
+ [JsonPropertyName("optionBindingsDropped")]
+ public List? OptionBindingsDropped { get; set; }
+}
diff --git a/tools/SampleIndexExporter/MarkdownDocument.cs b/tools/SampleIndexExporter/MarkdownDocument.cs
new file mode 100644
index 00000000..5909155f
--- /dev/null
+++ b/tools/SampleIndexExporter/MarkdownDocument.cs
@@ -0,0 +1,241 @@
+// 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.Text.RegularExpressions;
+
+namespace CommunityToolkit.SampleIndex;
+
+///
+/// One components/*/samples/*.md file: its YAML frontmatter, the [!SAMPLE]
+/// markers it contains in document order, and the prose introducing each of them.
+///
+///
+/// The frontmatter and marker patterns are deliberately the ones
+/// ToolkitSampleMetadataGenerator uses to build the sample app. Matching it exactly is
+/// the point: an index that disagreed with the app about which samples a document presents
+/// would be worse than no index, because nothing would reveal the disagreement.
+///
+internal sealed partial class MarkdownDocument
+{
+ private MarkdownDocument(string path, string relativePath)
+ {
+ Path = path;
+ RelativePath = relativePath;
+ }
+
+ /// Absolute path on disk.
+ public string Path { get; }
+
+ /// Repository-relative path, always with forward slashes.
+ public string RelativePath { get; }
+
+ public string? Title { get; private init; }
+
+ public string? Author { get; private init; }
+
+ public string? Description { get; private init; }
+
+ public string? Keywords { get; private init; }
+
+ public string? Category { get; private init; }
+
+ public string? Subcategory { get; private init; }
+
+ public bool? Experimental { get; private init; }
+
+ /// Sample ids referenced by [!SAMPLE] markers, in document order.
+ public IReadOnlyList SampleIds { get; private init; } = [];
+
+ /// Prose introducing each sample id, where the document has any.
+ public IReadOnlyDictionary SampleProse { get; private init; } =
+ new Dictionary(StringComparer.Ordinal);
+
+ [GeneratedRegex(@"^title:\s*(?.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex TitleRegex();
+
+ [GeneratedRegex(@"^author:\s*(?.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex AuthorRegex();
+
+ [GeneratedRegex(@"^description:\s*(?.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex DescriptionRegex();
+
+ [GeneratedRegex(@"^keywords:\s*(?.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex KeywordsRegex();
+
+ [GeneratedRegex(@"^category:\s*(?.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex CategoryRegex();
+
+ [GeneratedRegex(@"^subcategory:\s*(?.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex SubcategoryRegex();
+
+ [GeneratedRegex(@"^experimental:\s*(?.*)$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex ExperimentalRegex();
+
+ /// The generator's own marker pattern, verbatim.
+ [GeneratedRegex(@"^>\s*\[!SAMPLE\s*(?.*)\s*\]\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex SampleMarkerRegex();
+
+ /// A markdown link, reduced to its text.
+ [GeneratedRegex(@"\[([^\]]+)\]\([^)]+\)")]
+ private static partial Regex MarkdownLinkRegex();
+
+ /// Inline code, reduced to its contents.
+ [GeneratedRegex(@"`([^`]+)`")]
+ private static partial Regex InlineCodeRegex();
+
+ [GeneratedRegex(@"\s+")]
+ private static partial Regex WhitespaceRegex();
+
+ ///
+ /// Read and parse a documentation file. Returns when the file has no
+ /// frontmatter section, which is how the generator distinguishes a documentation file from
+ /// an ordinary markdown file that happens to sit in the samples folder.
+ ///
+ public static MarkdownDocument? Parse(string path, string relativePath)
+ {
+ // Normalize line endings once. Every pattern below is anchored with Multiline and the
+ // prose splitter looks for blank lines, both of which behave differently under CRLF.
+ var content = File.ReadAllText(path).Replace("\r\n", "\n").Replace('\r', '\n');
+
+ var sections = content.Split(["---"], StringSplitOptions.RemoveEmptyEntries);
+ if (sections.Length <= 1)
+ {
+ return null;
+ }
+
+ var frontmatter = sections[0];
+ var experimentalText = Field(frontmatter, ExperimentalRegex());
+
+ return new MarkdownDocument(path, relativePath)
+ {
+ Title = Field(frontmatter, TitleRegex()),
+ Author = Field(frontmatter, AuthorRegex()),
+ Description = Field(frontmatter, DescriptionRegex()),
+ Keywords = Field(frontmatter, KeywordsRegex()),
+ Category = Field(frontmatter, CategoryRegex()),
+ Subcategory = Field(frontmatter, SubcategoryRegex()),
+ Experimental = bool.TryParse(experimentalText, out var experimental) ? experimental : null,
+ SampleIds = ReadSampleIds(content),
+ SampleProse = ReadSampleProse(content),
+ };
+ }
+
+ /// Split the keywords frontmatter field into individual terms.
+ public static List SplitKeywords(string? keywords) =>
+ string.IsNullOrWhiteSpace(keywords)
+ ? []
+ : [.. keywords.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
+
+ private static string? Field(string frontmatter, Regex pattern)
+ {
+ var match = pattern.Match(frontmatter);
+ if (!match.Success)
+ {
+ return null;
+ }
+
+ var value = match.Groups["value"].Value.Trim();
+ return value.Length == 0 ? null : value;
+ }
+
+ private static List ReadSampleIds(string content)
+ {
+ var ids = new List();
+ foreach (Match match in SampleMarkerRegex().Matches(content))
+ {
+ var id = match.Groups["sampleid"].Value.Trim();
+ if (id.Length > 0)
+ {
+ ids.Add(id);
+ }
+ }
+
+ return ids;
+ }
+
+ ///
+ /// Associate each marker with the prose that introduces it: the paragraphs between the
+ /// previous marker and this one, starting after the most recent section heading.
+ ///
+ ///
+ /// The heading boundary matters for documents that present several controls in one file —
+ /// SizerControls.md, for instance — where without it the introduction to one section
+ /// would be attached to the first sample of the next.
+ ///
+ private static Dictionary ReadSampleProse(string content)
+ {
+ var body = content;
+ var sections = content.Split(["---"], StringSplitOptions.RemoveEmptyEntries);
+ if (sections.Length > 1)
+ {
+ // Drop the frontmatter block so its fields cannot be mistaken for prose.
+ var frontmatterEnd = content.IndexOf("---", content.IndexOf("---", StringComparison.Ordinal) + 3, StringComparison.Ordinal);
+ if (frontmatterEnd >= 0)
+ {
+ body = content[(frontmatterEnd + 3)..];
+ }
+ }
+
+ var prose = new Dictionary(StringComparer.Ordinal);
+ var cursor = 0;
+
+ foreach (Match match in SampleMarkerRegex().Matches(body))
+ {
+ var id = match.Groups["sampleid"].Value.Trim();
+ var preceding = body[cursor..match.Index];
+ cursor = match.Index + match.Length;
+
+ if (id.Length == 0)
+ {
+ continue;
+ }
+
+ var paragraphs = preceding
+ .Split("\n\n", StringSplitOptions.RemoveEmptyEntries)
+ .Select(p => p.Trim())
+ .Where(p => p.Length > 0)
+ .ToList();
+
+ var start = 0;
+ for (var i = paragraphs.Count - 1; i >= 0; i--)
+ {
+ if (paragraphs[i].StartsWith('#'))
+ {
+ start = i + 1;
+ break;
+ }
+ }
+
+ var local = paragraphs
+ .Skip(start)
+ .Where(p => !p.StartsWith('#') && !p.StartsWith('>'))
+ .ToList();
+
+ if (local.Count == 0)
+ {
+ continue;
+ }
+
+ var text = CleanProse(string.Join(" ", local));
+ if (text.Length > 0)
+ {
+ prose[id] = text;
+ }
+ }
+
+ return prose;
+ }
+
+ ///
+ /// Reduce markdown prose to plain text. Links keep their text and inline code its contents,
+ /// because a consumer renders this as a sentence rather than as markdown.
+ ///
+ private static string CleanProse(string text)
+ {
+ var result = MarkdownLinkRegex().Replace(text, "$1");
+ result = InlineCodeRegex().Replace(result, "$1");
+ result = WhitespaceRegex().Replace(result, " ");
+ return result.Trim();
+ }
+}
diff --git a/tools/SampleIndexExporter/Program.cs b/tools/SampleIndexExporter/Program.cs
new file mode 100644
index 00000000..2824b642
--- /dev/null
+++ b/tools/SampleIndexExporter/Program.cs
@@ -0,0 +1,120 @@
+// 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.SampleIndex;
+
+///
+/// Regenerates or verifies the committed sample index.
+///
+///
+///
+/// dotnet run --project tools/SampleIndexExporter -- generate [--repo-root <path>]
+/// dotnet run --project tools/SampleIndexExporter -- check [--repo-root <path>]
+///
+///
+/// generate writes the index. check rebuilds it in memory and fails when
+/// the committed file is missing or out of date, without touching anything on disk. CI runs
+/// check, which is what stops the published index drifting away from the samples it
+/// describes.
+///
+internal static class Program
+{
+ internal static int Main(string[] args)
+ {
+ if (args.Length == 0 || (args[0] != "generate" && args[0] != "check"))
+ {
+ Console.Error.WriteLine(
+ "Usage: dotnet run --project tools/SampleIndexExporter -- [--repo-root ]");
+ return 2;
+ }
+
+ var command = args[0];
+ string repoRoot;
+
+ try
+ {
+ repoRoot = ParseRepoRoot(args) ?? IndexGenerator.FindRepoRoot(Directory.GetCurrentDirectory());
+ }
+ catch (DirectoryNotFoundException ex)
+ {
+ Console.Error.WriteLine(ex.Message);
+ return 2;
+ }
+
+ var result = IndexGenerator.Generate(repoRoot);
+
+ foreach (var warning in result.Warnings)
+ {
+ Console.WriteLine($"warning: {warning}");
+ }
+
+ foreach (var withheld in result.Withheld)
+ {
+ Console.WriteLine($"withheld: {withheld}");
+ }
+
+ var errors = result.Errors.ToList();
+ if (errors.Count > 0)
+ {
+ foreach (var error in errors)
+ {
+ Console.Error.WriteLine($"error: {error}");
+ }
+
+ Console.Error.WriteLine(
+ $"{errors.Count} error(s) in the sample sources. The index was not written.");
+ return 1;
+ }
+
+ var content = IndexGenerator.Serialize(result.Index);
+ var absolutePath = Path.Combine(
+ repoRoot,
+ IndexGenerator.IndexRelativePath.Replace('/', Path.DirectorySeparatorChar));
+
+ if (command == "generate")
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(absolutePath)!);
+ File.WriteAllText(absolutePath, content);
+
+ Console.WriteLine(
+ $"Wrote {result.Index.ControlCount} entries and {result.Index.SampleCount} samples to "
+ + IndexGenerator.IndexRelativePath + ".");
+ return 0;
+ }
+
+ if (!File.Exists(absolutePath))
+ {
+ Console.Error.WriteLine(
+ $"{IndexGenerator.IndexRelativePath} does not exist. Run "
+ + "'dotnet run --project tools/SampleIndexExporter -- generate' and commit the result.");
+ return 1;
+ }
+
+ if (File.ReadAllText(absolutePath).Replace("\r\n", "\n") != content)
+ {
+ Console.Error.WriteLine(
+ $"{IndexGenerator.IndexRelativePath} is out of date. Run "
+ + "'dotnet run --project tools/SampleIndexExporter -- generate' and commit the result.");
+ return 1;
+ }
+
+ Console.WriteLine(
+ $"{IndexGenerator.IndexRelativePath} is up to date "
+ + $"({result.Index.ControlCount} entries, {result.Index.SampleCount} samples).");
+ return 0;
+ }
+
+ private static string? ParseRepoRoot(string[] args)
+ {
+ for (var i = 1; i < args.Length - 1; i++)
+ {
+ if (args[i] == "--repo-root")
+ {
+ return Path.GetFullPath(args[i + 1]);
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/tools/SampleIndexExporter/SampleCode.cs b/tools/SampleIndexExporter/SampleCode.cs
new file mode 100644
index 00000000..9a86f0e0
--- /dev/null
+++ b/tools/SampleIndexExporter/SampleCode.cs
@@ -0,0 +1,321 @@
+// 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;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace CommunityToolkit.SampleIndex;
+
+/// What extracting one sample's C# produced.
+/// The publishable members, or null when the sample has none.
+///
+/// Namespaces the published members need imported, in ordinal order. Published on the owning
+/// control rather than inside : the contract has consumers prepend them
+/// as using X; lines, and asks that no sample's code repeat them.
+///
+internal sealed record SampleCodeExtraction(string? Code, List Usings);
+
+///
+/// Extracts the C# a sample actually demonstrates from the page that hosts it.
+///
+///
+/// A sample's .xaml.cs is mostly scaffolding: a license header, a namespace belonging
+/// to the sample app, a page class, and a constructor that calls InitializeComponent.
+/// None of that is the sample. What is the sample is the handlers its markup calls and the
+/// types its markup binds to, so those are what gets published — the same shape the WinUI
+/// Gallery index publishes, which is a body of members a reader drops into their own page.
+///
+/// Most samples have nothing left once the scaffolding is removed, and those publish no
+/// code at all rather than a constructor that tells the reader nothing.
+///
+internal static class SampleCode
+{
+ ///
+ /// Symbols that make the parser keep the WinAppSDK branch of a conditional.
+ ///
+ ///
+ /// Samples multi-target WinAppSDK, UWP and Uno. The reader is on WinAppSDK, and a snippet
+ /// carrying all three branches asks them to work out which one is theirs — so the parser
+ /// is told which platform this is and the branches for the others never enter the tree.
+ ///
+ /// Shared with , which reads the component sources for the
+ /// same platform so that what a namespace is known to supply matches what a published
+ /// sample is allowed to name.
+ ///
+ public static readonly string[] WinAppSdkSymbols = ["WINAPPSDK", "WINUI3", "NET"];
+
+ ///
+ /// Read the publishable C# for one sample, together with the namespaces it needs imported.
+ ///
+ public static SampleCodeExtraction Extract(string path, string sampleTypeName, ToolkitApi api)
+ {
+ var text = File.ReadAllText(path).Replace("\r\n", "\n");
+ var tree = CSharpSyntaxTree.ParseText(
+ text,
+ new CSharpParseOptions(preprocessorSymbols: WinAppSdkSymbols),
+ path: path);
+
+ var root = (CompilationUnitSyntax)tree.GetRoot();
+ var published = new List();
+
+ foreach (var type in root.DescendantNodes().OfType())
+ {
+ // Nested types travel with the member that declares them.
+ if (type.Parent is not (CompilationUnitSyntax or BaseNamespaceDeclarationSyntax))
+ {
+ continue;
+ }
+
+ if (type.Identifier.ValueText == sampleTypeName)
+ {
+ if (type is ClassDeclarationSyntax sample)
+ {
+ published.AddRange(sample.Members.Where(IsWorthPublishing).Select(Clean));
+ }
+
+ continue;
+ }
+
+ // A type declared beside the sample - the enum a SwitchPresenter switches on, the
+ // model an ItemsRepeater binds to - is part of what the markup needs to work.
+ published.Add(Clean(type));
+ }
+
+ var parts = published.Select(Render).Where(part => part.Length > 0);
+ var code = string.Join("\n\n", parts).Trim();
+
+ // Nothing published needs nothing imported. Most samples land here: their code-behind
+ // was only page scaffolding, and an import list for code that was never published would
+ // be prepended by a consumer to samples that are pure markup.
+ return code.Length == 0
+ ? new SampleCodeExtraction(null, [])
+ : new SampleCodeExtraction(code, Imports(root, published, api));
+ }
+
+ ///
+ /// The namespaces the published members still need imported.
+ ///
+ ///
+ /// A sample file imports what the whole page needed, and most of the page is scaffolding
+ /// that is not published. Publishing its imports unfiltered would hand the reader packages
+ /// to reference for code they were never given, and — for the sample app's own namespaces —
+ /// a using that cannot resolve in any project but this repository's.
+ ///
+ private static List Imports(
+ CompilationUnitSyntax root,
+ List published,
+ ToolkitApi api)
+ {
+ var names = published
+ .SelectMany(node => node.DescendantNodesAndSelf().OfType())
+ .Select(name => name.Identifier.ValueText)
+ .ToHashSet(StringComparer.Ordinal);
+
+ var pageNamespace = PageNamespace(root);
+ var imports = new List();
+
+ foreach (var directive in root.DescendantNodes().OfType())
+ {
+ if (Import(directive, names) is not { } import
+ || BelongsToTheSampleApp(import, pageNamespace)
+ || (api.Declares(import) && !api.Supplies(import, names))
+ || imports.Contains(import, StringComparer.Ordinal))
+ {
+ continue;
+ }
+
+ imports.Add(import);
+ }
+
+ imports.Sort(StringComparer.Ordinal);
+ return imports;
+ }
+
+ ///
+ /// The namespace one using directive asks a consumer to import, or
+ /// when it asks for something a namespace import cannot express.
+ ///
+ private static string? Import(UsingDirectiveSyntax directive, HashSet names)
+ {
+ // 'using static X;' imports one type's members rather than a namespace, and the field
+ // this feeds is a list of namespaces a consumer writes out as 'using X;'. There is no
+ // form for it, so it is not published.
+ if (directive.StaticKeyword != default || directive.Name is null)
+ {
+ return null;
+ }
+
+ if (directive.Alias is null)
+ {
+ return directive.Name.ToString();
+ }
+
+ // An alias is how a sample names one platform's type while multi-targeting:
+ // 'using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;'. The branch has
+ // already been resolved to WinAppSDK by the time anything is published, so what the
+ // reader needs is the type's own namespace - but only when the alias renames the type
+ // to itself, which is what makes the published code read the same either way. An alias
+ // to a different name is not expressible as a namespace import and is left out.
+ var aliasName = directive.Alias.Name.Identifier.ValueText;
+
+ return directive.Name is QualifiedNameSyntax qualified
+ && qualified.Right.Identifier.ValueText == aliasName
+ && names.Contains(aliasName)
+ ? qualified.Left.ToString()
+ : null;
+ }
+
+ /// The namespace the sample page itself is declared in.
+ private static string PageNamespace(CompilationUnitSyntax root) =>
+ root.DescendantNodes().OfType().FirstOrDefault()?.Name.ToString()
+ ?? string.Empty;
+
+ ///
+ /// True when an import names the sample app rather than the toolkit or the platform.
+ ///
+ ///
+ /// A sample page sometimes imports a sibling of its own namespace — the Connected Animations
+ /// sample navigates to pages declared one folder down. Those namespaces exist only in this
+ /// repository's sample app, so publishing one would be publishing a using that cannot
+ /// resolve anywhere the snippet is actually pasted.
+ ///
+ private static bool BelongsToTheSampleApp(string import, string pageNamespace) =>
+ pageNamespace.Length > 0
+ && (import == pageNamespace
+ || import.StartsWith(pageNamespace + ".", StringComparison.Ordinal)
+ || pageNamespace.StartsWith(import + ".", StringComparison.Ordinal));
+
+ private static bool IsWorthPublishing(MemberDeclarationSyntax member)
+ {
+ // A constructor that only calls InitializeComponent is the page waking up, not the
+ // sample doing anything. One that does more is kept whole.
+ if (member is ConstructorDeclarationSyntax constructor)
+ {
+ var statements = constructor.Body?.Statements ?? default;
+ return statements.Count != 1 || !IsInitializeComponent(statements[0]);
+ }
+
+ // ConvertStringTo… exists to turn an option-pane string into a real value. The options
+ // pane is part of the sample browser, so with it gone nothing calls these.
+ if (member is MethodDeclarationSyntax method
+ && method.Identifier.ValueText.StartsWith("ConvertString", StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static bool IsInitializeComponent(StatementSyntax statement) =>
+ statement is ExpressionStatementSyntax
+ {
+ Expression: InvocationExpressionSyntax
+ {
+ Expression: IdentifierNameSyntax { Identifier.ValueText: "InitializeComponent" }
+ or MemberAccessExpressionSyntax
+ {
+ Expression: ThisExpressionSyntax,
+ Name.Identifier.ValueText: "InitializeComponent",
+ },
+ },
+ };
+
+ ///
+ /// Remove from one declaration everything that belongs to the sample app rather than to the
+ /// sample.
+ ///
+ private static SyntaxNode Clean(SyntaxNode node)
+ {
+ var cleaned = StripSampleAttributes(node);
+ cleaned = StripPageWakeUp(cleaned);
+ return StripConditionals(cleaned);
+ }
+
+ ///
+ /// Render one cleaned declaration as standalone source.
+ ///
+ private static string Render(SyntaxNode cleaned) =>
+ cleaned.NormalizeWhitespace(indentation: " ", eol: "\n").ToFullString().Trim();
+
+ ///
+ /// Remove the InitializeComponent call from a constructor that does more than make
+ /// it.
+ ///
+ ///
+ /// The reader's own page already calls it. A constructor is only published when it also
+ /// does something the sample needs — navigating a frame, wiring a collection — and that
+ /// part is worth keeping, while the call that loads a different page's markup is not.
+ ///
+ private static SyntaxNode StripPageWakeUp(SyntaxNode node)
+ {
+ var calls = node
+ .DescendantNodesAndSelf()
+ .OfType()
+ .SelectMany(constructor => constructor.Body?.Statements ?? default)
+ .Where(IsInitializeComponent)
+ .ToList();
+
+ return calls.Count == 0
+ ? node
+ : node.RemoveNodes(calls, SyntaxRemoveOptions.KeepNoTrivia) ?? node;
+ }
+
+ ///
+ /// Remove the conditional directives left behind around a branch the parser already took.
+ ///
+ ///
+ /// Telling the parser which platform this is keeps the other platforms' code out of the
+ /// tree, but the #if and #endif lines themselves are trivia and survive that.
+ /// Published, they read as a choice the reader has to make, when the choice has already
+ /// been made for them.
+ ///
+ private static SyntaxNode StripConditionals(SyntaxNode node) =>
+ node.ReplaceTrivia(
+ node.DescendantTrivia(descendIntoTrivia: true).Where(IsConditional),
+ (_, _) => default);
+
+ private static bool IsConditional(SyntaxTrivia trivia) => trivia.Kind() is
+ SyntaxKind.IfDirectiveTrivia or
+ SyntaxKind.ElifDirectiveTrivia or
+ SyntaxKind.ElseDirectiveTrivia or
+ SyntaxKind.EndIfDirectiveTrivia or
+ SyntaxKind.DisabledTextTrivia;
+
+ ///
+ /// Remove the attributes that exist for the sample browser rather than for the code.
+ ///
+ ///
+ /// [ToolkitSample] and its option attributes are read by a source generator that
+ /// the reader's project does not reference, so leaving them in publishes code that does
+ /// not compile and describes a UI the reader never sees.
+ ///
+ private static SyntaxNode StripSampleAttributes(SyntaxNode node)
+ {
+ var lists = node
+ .DescendantNodesAndSelf()
+ .OfType()
+ .Where(list => list.Attributes.All(IsSampleAttribute))
+ .ToList();
+
+ if (lists.Count == 0)
+ {
+ return node;
+ }
+
+ return node.RemoveNodes(lists, SyntaxRemoveOptions.KeepNoTrivia) ?? node;
+ }
+
+ private static bool IsSampleAttribute(AttributeSyntax attribute)
+ {
+ var name = attribute.Name switch
+ {
+ IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
+ QualifiedNameSyntax qualified => qualified.Right.Identifier.ValueText,
+ _ => attribute.Name.ToString(),
+ };
+
+ return name.StartsWith("ToolkitSample", StringComparison.Ordinal);
+ }
+}
diff --git a/tools/SampleIndexExporter/SampleDeclaration.cs b/tools/SampleIndexExporter/SampleDeclaration.cs
new file mode 100644
index 00000000..14d3bf2a
--- /dev/null
+++ b/tools/SampleIndexExporter/SampleDeclaration.cs
@@ -0,0 +1,445 @@
+// 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;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace CommunityToolkit.SampleIndex;
+
+///
+/// Evaluates the small set of C# expressions that [ToolkitSample] arguments are written
+/// with, without a compilation.
+///
+///
+/// Sample authors write display names three ways: a plain literal
+/// ("Implicit animations"), a nameof (nameof(AutoSelectBehavior)) and an
+/// interpolated string mixing the two ($"{nameof(FocusBehavior)}: Lists"). All three
+/// resolve from syntax alone, because nameof yields the final identifier of the
+/// expression it is given.
+///
+/// Anything outside that set returns rather than a guess. The
+/// caller reports it, so a new form shows up as a build failure instead of as a sample
+/// published under the wrong name.
+///
+internal static class ConstantText
+{
+ /// Evaluate an expression to a string, or return null when it is not a form we model.
+ public static string? Evaluate(ExpressionSyntax? expression)
+ {
+ switch (expression)
+ {
+ case null:
+ return null;
+
+ case LiteralExpressionSyntax literal when literal.IsKind(SyntaxKind.StringLiteralExpression):
+ return literal.Token.ValueText;
+
+ case InvocationExpressionSyntax { Expression: IdentifierNameSyntax { Identifier.ValueText: "nameof" } } nameOf:
+ return NameOfValue(nameOf);
+
+ case InterpolatedStringExpressionSyntax interpolated:
+ return Interpolated(interpolated);
+
+ case BinaryExpressionSyntax binary when binary.IsKind(SyntaxKind.AddExpression):
+ var left = Evaluate(binary.Left);
+ var right = Evaluate(binary.Right);
+ return left is null || right is null ? null : left + right;
+
+ case ParenthesizedExpressionSyntax parenthesized:
+ return Evaluate(parenthesized.Expression);
+
+ default:
+ return null;
+ }
+ }
+
+ /// Evaluate a boolean literal argument.
+ public static bool? EvaluateBool(ExpressionSyntax? expression) => expression switch
+ {
+ LiteralExpressionSyntax literal when literal.IsKind(SyntaxKind.TrueLiteralExpression) => true,
+ LiteralExpressionSyntax literal when literal.IsKind(SyntaxKind.FalseLiteralExpression) => false,
+ ParenthesizedExpressionSyntax parenthesized => EvaluateBool(parenthesized.Expression),
+ _ => null,
+ };
+
+ /// Evaluate a numeric literal argument, including a negated one.
+ public static double? EvaluateNumber(ExpressionSyntax? expression) => expression switch
+ {
+ LiteralExpressionSyntax literal when literal.IsKind(SyntaxKind.NumericLiteralExpression) =>
+ Convert.ToDouble(literal.Token.Value, System.Globalization.CultureInfo.InvariantCulture),
+ PrefixUnaryExpressionSyntax unary when unary.IsKind(SyntaxKind.UnaryMinusExpression) =>
+ EvaluateNumber(unary.Operand) is { } value ? -value : null,
+ ParenthesizedExpressionSyntax parenthesized => EvaluateNumber(parenthesized.Expression),
+ _ => null,
+ };
+
+ /// nameof(A.B.C) is "C": the last identifier wins.
+ private static string? NameOfValue(InvocationExpressionSyntax invocation)
+ {
+ if (invocation.ArgumentList.Arguments.Count != 1)
+ {
+ return null;
+ }
+
+ return invocation.ArgumentList.Arguments[0].Expression switch
+ {
+ IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
+ MemberAccessExpressionSyntax member => member.Name.Identifier.ValueText,
+ GenericNameSyntax generic => generic.Identifier.ValueText,
+ _ => null,
+ };
+ }
+
+ private static string? Interpolated(InterpolatedStringExpressionSyntax interpolated)
+ {
+ var builder = new System.Text.StringBuilder();
+
+ foreach (var content in interpolated.Contents)
+ {
+ switch (content)
+ {
+ case InterpolatedStringTextSyntax text:
+ builder.Append(text.TextToken.ValueText);
+ break;
+
+ case InterpolationSyntax { AlignmentClause: null, FormatClause: null } interpolation:
+ var value = Evaluate(interpolation.Expression);
+ if (value is null)
+ {
+ return null;
+ }
+
+ builder.Append(value);
+ break;
+
+ default:
+ return null;
+ }
+ }
+
+ return builder.ToString();
+ }
+}
+
+/// A sample-app option declared by a [ToolkitSample*Option] attribute.
+/// The binding name, which is the generated member's name.
+///
+/// The value the generated member holds before anyone touches the options pane, formatted as
+/// XAML would write it, or when the declaration gives no usable default.
+///
+internal sealed record SampleOption(string Name, string? DefaultValue);
+
+///
+/// A [ToolkitSample] declaration: the sample id the documentation refers to, the name
+/// and description the sample app shows, and any options its markup can bind to.
+///
+internal sealed class SampleDeclaration
+{
+ public required string Id { get; init; }
+
+ public required string TypeName { get; init; }
+
+ /// Repository-relative path of the declaring .xaml.cs file.
+ public required string RelativePath { get; init; }
+
+ public string? DisplayName { get; init; }
+
+ public string? Description { get; init; }
+
+ public IReadOnlyList Options { get; init; } = [];
+
+ ///
+ /// Argument forms encountered that does not model. Non-empty
+ /// means a field was left unset rather than guessed.
+ ///
+ public IReadOnlyList UnreadableArguments { get; init; } = [];
+
+ ///
+ /// Read every sample declaration in one .xaml.cs file.
+ ///
+ public static List Parse(string path, string relativePath)
+ {
+ var tree = CSharpSyntaxTree.ParseText(File.ReadAllText(path), path: path);
+ var root = tree.GetRoot();
+ var declarations = new List();
+
+ foreach (var type in root.DescendantNodes().OfType())
+ {
+ var attributes = type.AttributeLists.SelectMany(list => list.Attributes).ToList();
+ var sampleAttribute = attributes.FirstOrDefault(a => AttributeNameIs(a, "ToolkitSample"));
+ if (sampleAttribute is null)
+ {
+ continue;
+ }
+
+ var unreadable = new List();
+ var arguments = sampleAttribute.ArgumentList?.Arguments ?? default;
+ var bound = BindArguments(arguments, ["id", "displayName", "description"]);
+
+ var id = ReadArgument(bound, "id", unreadable);
+ var displayName = ReadArgument(bound, "displayName", unreadable);
+ var description = ReadArgument(bound, "description", unreadable);
+
+ // The id argument is always nameof(TheSampleClass), so the class name is the
+ // authoritative fallback when the expression is one we do not model.
+ id ??= type.Identifier.ValueText;
+
+ declarations.Add(new SampleDeclaration
+ {
+ Id = id,
+ TypeName = type.Identifier.ValueText,
+ RelativePath = relativePath,
+ DisplayName = displayName,
+ Description = description,
+ Options = ReadOptions(attributes),
+ UnreadableArguments = unreadable,
+ });
+ }
+
+ return declarations;
+ }
+
+ private static bool AttributeNameIs(AttributeSyntax attribute, string name)
+ {
+ var text = attribute.Name switch
+ {
+ IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
+ QualifiedNameSyntax qualified => qualified.Right.Identifier.ValueText,
+ _ => attribute.Name.ToString(),
+ };
+
+ // Attribute syntax allows the "Attribute" suffix to be written out in full.
+ return string.Equals(text, name, StringComparison.Ordinal)
+ || string.Equals(text, name + "Attribute", StringComparison.Ordinal);
+ }
+
+ private static string? AttributeName(AttributeSyntax attribute) => attribute.Name switch
+ {
+ IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
+ QualifiedNameSyntax qualified => qualified.Right.Identifier.ValueText,
+ _ => attribute.Name.ToString(),
+ };
+
+ private static string? ReadArgument(
+ Dictionary bound,
+ string name,
+ List unreadable)
+ {
+ if (!bound.TryGetValue(name, out var argument))
+ {
+ return null;
+ }
+
+ var value = ConstantText.Evaluate(argument.Expression);
+ if (value is null)
+ {
+ unreadable.Add($"{name}: {argument.Expression}");
+ }
+
+ return value;
+ }
+
+ ///
+ /// Match attribute arguments to the parameters they fill.
+ ///
+ ///
+ /// Sample authors mix the two calling styles freely — most write
+ /// [ToolkitSample(id: nameof(X), "Display name", description: "...")], where the
+ /// display name is the only positional argument even though it is the second parameter.
+ /// Binding by position alone would therefore miss it, so named arguments claim their
+ /// parameter first and the positional ones fill whatever slots are left, in order. That is
+ /// what the compiler does, and matching it is why the index agrees with the sample app.
+ ///
+ private static Dictionary BindArguments(
+ SeparatedSyntaxList arguments,
+ string[] parameters)
+ {
+ var bound = new Dictionary(StringComparer.Ordinal);
+ var positional = new List();
+
+ foreach (var argument in arguments)
+ {
+ // NameEquals is the property-initializer form (Title = "..."), which sets a
+ // property rather than filling a constructor parameter.
+ if (argument.NameEquals is not null)
+ {
+ continue;
+ }
+
+ var name = argument.NameColon?.Name.Identifier.ValueText;
+ if (name is null)
+ {
+ positional.Add(argument);
+ }
+ else if (Array.IndexOf(parameters, name) >= 0)
+ {
+ bound[name] = argument;
+ }
+ }
+
+ var next = 0;
+ foreach (var parameter in parameters)
+ {
+ if (bound.ContainsKey(parameter))
+ {
+ continue;
+ }
+
+ if (next < positional.Count)
+ {
+ bound[parameter] = positional[next++];
+ }
+ }
+
+ return bound;
+ }
+
+ ///
+ /// Read the option attributes on a sample class, in declaration order.
+ ///
+ ///
+ /// Each one causes a member to be generated on the sample's page, which its XAML then binds
+ /// to. Those members exist only inside the sample app, so the default recorded here is what
+ /// lets the published XAML say what the reader would have seen on first opening the sample.
+ ///
+ private static List ReadOptions(List attributes)
+ {
+ var options = new List();
+
+ foreach (var attribute in attributes)
+ {
+ var name = AttributeName(attribute);
+ if (name is null || !name.StartsWith("ToolkitSample", StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ var arguments = attribute.ArgumentList?.Arguments ?? default;
+ var positional = arguments.Where(a => a.NameColon is null && a.NameEquals is null).ToList();
+ if (positional.Count == 0 && arguments.Count == 0)
+ {
+ continue;
+ }
+
+ string? defaultValue;
+
+ if (AttributeNameIs(attribute, "ToolkitSampleBoolOption"))
+ {
+ // (string bindingName, bool defaultState)
+ var bound = BindArguments(arguments, ["bindingName", "defaultState"]);
+ if (!TryBindingName(bound, out var boolName))
+ {
+ continue;
+ }
+
+ defaultValue = bound.TryGetValue("defaultState", out var state)
+ && ConstantText.EvaluateBool(state.Expression) is { } value
+ ? (value ? "True" : "False")
+ : null;
+
+ options.Add(new SampleOption(boolName, defaultValue));
+ continue;
+ }
+
+ if (AttributeNameIs(attribute, "ToolkitSampleNumericOption"))
+ {
+ // (string bindingName, double initial = 0, ...)
+ var bound = BindArguments(arguments, ["bindingName", "initial", "min", "max", "step", "showAsNumberBox"]);
+ if (!TryBindingName(bound, out var numericName))
+ {
+ continue;
+ }
+
+ var initial = bound.TryGetValue("initial", out var argument)
+ ? ConstantText.EvaluateNumber(argument.Expression)
+ : 0d;
+
+ options.Add(new SampleOption(
+ numericName,
+ initial?.ToString(System.Globalization.CultureInfo.InvariantCulture)));
+ continue;
+ }
+
+ if (AttributeNameIs(attribute, "ToolkitSampleTextOption"))
+ {
+ // (string bindingName, string? placeholderText = null). The generated member
+ // starts out holding the placeholder, so that is what the reader first sees.
+ var bound = BindArguments(arguments, ["bindingName", "placeholderText"]);
+ if (!TryBindingName(bound, out var textName))
+ {
+ continue;
+ }
+
+ defaultValue = bound.TryGetValue("placeholderText", out var placeholder)
+ ? ConstantText.Evaluate(placeholder.Expression)
+ : null;
+
+ options.Add(new SampleOption(textName, defaultValue));
+ continue;
+ }
+
+ if (AttributeNameIs(attribute, "ToolkitSampleMultiChoiceOption"))
+ {
+ // (string bindingName, params string[] choices), where a choice may be written
+ // "Label : Value" and the first choice is the initial selection. The choices are
+ // a params array, so they are always positional.
+ if (positional.Count == 0)
+ {
+ continue;
+ }
+
+ var choiceName = ConstantText.Evaluate(positional[0].Expression);
+ if (string.IsNullOrEmpty(choiceName))
+ {
+ continue;
+ }
+
+ defaultValue = positional.Count > 1
+ ? ChoiceValue(ConstantText.Evaluate(positional[1].Expression))
+ : null;
+
+ options.Add(new SampleOption(choiceName, defaultValue));
+ }
+
+ // Anything else is not an option attribute (ToolkitSampleOptionsPane, for instance).
+ }
+
+ return options;
+ }
+
+ /// A multi-choice entry is either "Value" or "Label : Value".
+ private static string? ChoiceValue(string? choice)
+ {
+ if (choice is null)
+ {
+ return null;
+ }
+
+ var separator = choice.IndexOf(" : ", StringComparison.Ordinal);
+ return separator < 0 ? choice : choice[(separator + 3)..].TrimStart();
+ }
+
+ /// Read an option attribute's binding name, which is also the generated member's name.
+ private static bool TryBindingName(
+ Dictionary bound,
+ out string name)
+ {
+ name = string.Empty;
+
+ if (!bound.TryGetValue("bindingName", out var argument))
+ {
+ return false;
+ }
+
+ var value = ConstantText.Evaluate(argument.Expression);
+ if (string.IsNullOrEmpty(value))
+ {
+ return false;
+ }
+
+ name = value;
+ return true;
+ }
+}
diff --git a/tools/SampleIndexExporter/SampleIndexExporter.csproj b/tools/SampleIndexExporter/SampleIndexExporter.csproj
new file mode 100644
index 00000000..7439ee2e
--- /dev/null
+++ b/tools/SampleIndexExporter/SampleIndexExporter.csproj
@@ -0,0 +1,43 @@
+
+
+
+
+ Exe
+
+ net9.0
+ CommunityToolkit.SampleIndex
+ CommunityToolkit.SampleIndexExporter
+ enable
+ nullable
+ latest
+ enable
+ true
+ false
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/SampleIndexExporter/ToolkitApi.cs b/tools/SampleIndexExporter/ToolkitApi.cs
new file mode 100644
index 00000000..009739b6
--- /dev/null
+++ b/tools/SampleIndexExporter/ToolkitApi.cs
@@ -0,0 +1,173 @@
+// 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;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace CommunityToolkit.SampleIndex;
+
+///
+/// What each namespace this repository ships puts in scope.
+///
+///
+/// A sample's imports have to be narrowed to the ones its published members still need, and
+/// deciding that means knowing which namespace supplies a name the code uses. Binding the code
+/// would answer it outright, but binding needs the built toolkit assemblies and this tool
+/// deliberately never builds anything.
+///
+/// The repository's own sources answer it for the namespaces where the answer matters. A
+/// toolkit namespace stands for a package the reader has to reference, so publishing one the
+/// sample does not use is an instruction to take on a dependency for nothing. Everything else a
+/// sample imports is a platform namespace — System, Windows, Microsoft.UI —
+/// which this repository does not declare and cannot enumerate, and which the reader's project
+/// already has. Those are kept: an import that cannot be shown to be unused is not one to drop,
+/// and the cost of being wrong runs one way. A missing import is a compile error in the reader's
+/// file; a spare one is a hint the compiler discards.
+///
+internal sealed class ToolkitApi
+{
+ ///
+ /// One index per repository root. Generation runs several times over one tree — the tests
+ /// alone generate for every gate — and the sources do not change underneath it.
+ ///
+ private static readonly Dictionary Cache = new(StringComparer.Ordinal);
+
+ private readonly Dictionary> _namesByNamespace;
+
+ private ToolkitApi(Dictionary> namesByNamespace) =>
+ _namesByNamespace = namesByNamespace;
+
+ /// Read, once, what the components under declare.
+ public static ToolkitApi ForRepository(string repoRoot)
+ {
+ lock (Cache)
+ {
+ if (!Cache.TryGetValue(repoRoot, out var api))
+ {
+ api = Build(repoRoot);
+ Cache[repoRoot] = api;
+ }
+
+ return api;
+ }
+ }
+
+ /// True when a component in this repository declares .
+ public bool Declares(string namespaceName) => _namesByNamespace.ContainsKey(namespaceName);
+
+ /// True when that namespace puts one of in scope.
+ public bool Supplies(string namespaceName, IReadOnlySet names) =>
+ _namesByNamespace.TryGetValue(namespaceName, out var declared) && declared.Overlaps(names);
+
+ private static ToolkitApi Build(string repoRoot)
+ {
+ var namesByNamespace = new Dictionary>(StringComparer.Ordinal);
+ var componentsRoot = Path.Combine(repoRoot, "components");
+
+ if (!Directory.Exists(componentsRoot))
+ {
+ return new ToolkitApi(namesByNamespace);
+ }
+
+ foreach (var componentPath in Directory.GetDirectories(componentsRoot))
+ {
+ var sourceRoot = Path.Combine(componentPath, "src");
+ if (!Directory.Exists(sourceRoot))
+ {
+ continue;
+ }
+
+ foreach (var file in Directory.EnumerateFiles(sourceRoot, "*.cs", SearchOption.AllDirectories))
+ {
+ if (IsBuildOutput(file))
+ {
+ continue;
+ }
+
+ // Read for the same platform the samples are published for, so a type that only
+ // exists on WinAppSDK is in the index exactly when a published sample can name it.
+ var tree = CSharpSyntaxTree.ParseText(
+ File.ReadAllText(file),
+ new CSharpParseOptions(preprocessorSymbols: SampleCode.WinAppSdkSymbols));
+
+ Index(tree.GetRoot(), namesByNamespace);
+ }
+ }
+
+ return new ToolkitApi(namesByNamespace);
+ }
+
+ private static bool IsBuildOutput(string path) =>
+ path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
+ || path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal);
+
+ private static void Index(SyntaxNode root, Dictionary> namesByNamespace)
+ {
+ foreach (var node in root.DescendantNodes())
+ {
+ switch (node)
+ {
+ case BaseTypeDeclarationSyntax type:
+ Add(namesByNamespace, Namespace(type), type.Identifier.ValueText);
+ break;
+
+ case DelegateDeclarationSyntax @delegate:
+ Add(namesByNamespace, Namespace(@delegate), @delegate.Identifier.ValueText);
+ break;
+
+ // An extension method enters scope through its namespace rather than through the
+ // class declaring it: a sample that calls .Debounce() names the method and never
+ // names DispatcherQueueTimerExtensions, so indexing types alone would read that
+ // sample as having no use for CommunityToolkit.WinUI.
+ case MethodDeclarationSyntax method
+ when method.ParameterList.Parameters is [{ } first, ..]
+ && first.Modifiers.Any(SyntaxKind.ThisKeyword):
+ Add(namesByNamespace, Namespace(method), method.Identifier.ValueText);
+ break;
+ }
+ }
+ }
+
+ private static void Add(
+ Dictionary> namesByNamespace,
+ string namespaceName,
+ string name)
+ {
+ if (namespaceName.Length == 0)
+ {
+ return;
+ }
+
+ if (!namesByNamespace.TryGetValue(namespaceName, out var names))
+ {
+ names = new HashSet(StringComparer.Ordinal);
+ namesByNamespace[namespaceName] = names;
+ }
+
+ names.Add(name);
+
+ // An attribute is written without its suffix, so the name in the code and the name of
+ // the type never match for the one construct where the type is never spelled out.
+ if (name.EndsWith("Attribute", StringComparison.Ordinal) && name.Length > "Attribute".Length)
+ {
+ names.Add(name[..^"Attribute".Length]);
+ }
+ }
+
+ private static string Namespace(SyntaxNode node)
+ {
+ var parts = new List();
+
+ for (var ancestor = node.Parent; ancestor is not null; ancestor = ancestor.Parent)
+ {
+ if (ancestor is BaseNamespaceDeclarationSyntax declaration)
+ {
+ parts.Insert(0, declaration.Name.ToString());
+ }
+ }
+
+ return string.Join('.', parts);
+ }
+}
diff --git a/tools/SampleIndexExporter/XamlFragment.cs b/tools/SampleIndexExporter/XamlFragment.cs
new file mode 100644
index 00000000..3974d7de
--- /dev/null
+++ b/tools/SampleIndexExporter/XamlFragment.cs
@@ -0,0 +1,1166 @@
+// 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.Text;
+using System.Text.RegularExpressions;
+using System.Xml;
+using System.Xml.Linq;
+
+namespace CommunityToolkit.SampleIndex;
+
+/// What extracting one sample's XAML produced.
+/// The pasteable fragment, or null when the sample was withheld.
+/// Namespace declarations the fragment actually uses.
+/// Option bindings replaced with their default, as Name=Value.
+/// Attributes removed because their binding had no static value.
+/// Why the sample was withheld, when it was.
+internal sealed record XamlExtraction(
+ string? Xaml,
+ List XmlnsImports,
+ List OptionsResolved,
+ List OptionBindingsDropped,
+ string? Error);
+
+///
+/// Turns a sample's .xaml file into something a reader can paste into their own app.
+///
+///
+/// A sample file is a page in the sample app: it has an x:Class naming a type in that
+/// app, a <Page> root, and bindings to members the sample generator adds to that
+/// page. None of those exist in the reader's project, so publishing the file verbatim would
+/// publish something that cannot compile. What is published instead is the body of the root
+/// element, with the sample-app bindings settled.
+///
+/// What changes here is environment, never substance. The markup still demonstrates
+/// exactly what its author wrote it to demonstrate.
+///
+internal static partial class XamlFragment
+{
+ /// The XAML language namespace, declared by every XAML file as x.
+ private const string XamlNamespace = "http://schemas.microsoft.com/winfx/2006/xaml";
+
+ /// The default presentation namespace, declared by every XAML file with no prefix.
+ private const string PresentationNamespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
+
+ /// Design-time namespaces, which mean nothing outside a designer.
+ private static readonly HashSet DesignTimeNamespaces =
+ [
+ "http://schemas.microsoft.com/expression/blend/2008",
+ "http://schemas.openxmlformats.org/markup-compatibility/2006",
+ ];
+
+ /// A markup extension's prefix, e.g. the ui of {ui:FontIcon ...}.
+ [GeneratedRegex(@"\{\s*(?[A-Za-z_][\w.-]*):")]
+ private static partial Regex MarkupExtensionPrefixRegex();
+
+ /// Two or more consecutive blank lines, left behind when an attribute is removed.
+ [GeneratedRegex(@"\n[ \t]*\n[ \t]*\n")]
+ private static partial Regex BlankLineRunRegex();
+
+ ///
+ /// Extract the pasteable fragment from a sample file.
+ ///
+ /// The verbatim contents of the sample's .xaml file.
+ /// Options declared on the sample, whose generated members its markup may bind to.
+ public static XamlExtraction Extract(string xamlText, IReadOnlyList options)
+ {
+ var text = xamlText.Replace("\r\n", "\n").Replace('\r', '\n');
+
+ XDocument document;
+ try
+ {
+ document = XDocument.Parse(text, LoadOptions.PreserveWhitespace);
+ }
+ catch (XmlException ex)
+ {
+ return Withheld($"the file is not well-formed XML: {ex.Message}");
+ }
+
+ var root = document.Root;
+ if (root is null)
+ {
+ return Withheld("the file has no root element");
+ }
+
+ var body = RootBody(text, root.Name.LocalName);
+ if (body is null)
+ {
+ return Withheld("the root element's body could not be located");
+ }
+
+ if (body.Trim().Length == 0)
+ {
+ return Withheld("the root element is empty");
+ }
+
+ var (resolved, resolvedNames, droppedAttributes) = ResolveSampleAppBindings(body, options);
+
+ // Removing an attribute leaves the blank line it occupied. Collapsing runs keeps the
+ // published markup looking like the styled source it came from.
+ resolved = BlankLineRunRegex().Replace(resolved, "\n\n");
+
+ var fragment = Dedent(resolved);
+ if (fragment.Length == 0)
+ {
+ return Withheld("nothing remained after removing sample-app-only markup");
+ }
+
+ var relocation = RelocateRootProperties(fragment, root.Name.LocalName);
+ if (relocation.Error is not null)
+ {
+ return Withheld(relocation.Error);
+ }
+
+ fragment = relocation.Xaml!;
+
+ var declarations = NamespaceDeclarations(root);
+ fragment = StripAliasPrefixes(fragment, declarations, out declarations);
+
+ XElement wrapper;
+ try
+ {
+ wrapper = ParseFragment(fragment, declarations);
+ }
+ catch (XmlException ex)
+ {
+ return Withheld($"the extracted fragment is not well-formed XML: {ex.Message}");
+ }
+
+ var imports = UsedImports(wrapper, fragment, declarations);
+
+ return new XamlExtraction(fragment, imports, resolvedNames, droppedAttributes, Error: null);
+ }
+
+ private static XamlExtraction Withheld(string reason) => new(null, [], [], [], reason);
+
+ ///
+ /// Find bindings in a published fragment whose source does not exist outside the sample app.
+ ///
+ ///
+ /// This is the check that keeps the contract's promise: published XAML has to be pasteable,
+ /// so a binding left pointing at an options-pane member or at the discarded page element is
+ /// a defect in this extractor rather than something to publish with a warning attached.
+ /// It reads the binding expressions only — an attribute may legitimately be *named* after
+ /// an option, as in IsAlwaysOn="{x:Bind IsAlwaysOn}", where resolving the binding
+ /// leaves IsAlwaysOn="True" and nothing is wrong.
+ ///
+ public static List UnsettledBindings(string xaml, IReadOnlyList options)
+ {
+ var declaredNames = DeclaredElementNames(xaml);
+ var problems = new List();
+
+ foreach (var attribute in Attributes(xaml))
+ {
+ foreach (var binding in Bindings(attribute.Value))
+ {
+ foreach (var option in options)
+ {
+ if (MentionsIdentifier(binding.Arguments, option.Name))
+ {
+ problems.Add(
+ $"{attribute.Name}=\"{attribute.Value}\" binds to the sample-app option "
+ + $"'{option.Name}'");
+ }
+ }
+
+ if (binding.ElementName is { Length: > 0 } element && !declaredNames.Contains(element))
+ {
+ problems.Add(
+ $"{attribute.Name}=\"{attribute.Value}\" binds to element '{element}', which the "
+ + "fragment does not declare");
+ }
+ }
+ }
+
+ return problems;
+ }
+
+ ///
+ /// Return the raw text between the root element's start and end tags.
+ ///
+ ///
+ /// Taken from the text rather than rebuilt from the parsed tree so the author's formatting
+ /// survives. Re-serializing would reflow attributes and expand character references, which
+ /// turns a styled, readable sample into something that no longer matches its own source.
+ ///
+ private static string? RootBody(string text, string rootName)
+ {
+ var start = text.IndexOf("<" + rootName, StringComparison.Ordinal);
+ if (start < 0)
+ {
+ return null;
+ }
+
+ // Walk to the end of the start tag, ignoring '>' inside attribute values.
+ var quote = '\0';
+ var index = start;
+ for (; index < text.Length; index++)
+ {
+ var c = text[index];
+ if (quote != '\0')
+ {
+ if (c == quote)
+ {
+ quote = '\0';
+ }
+
+ continue;
+ }
+
+ if (c is '"' or '\'')
+ {
+ quote = c;
+ }
+ else if (c == '>')
+ {
+ break;
+ }
+ }
+
+ if (index >= text.Length)
+ {
+ return null;
+ }
+
+ // A self-closing root has no body.
+ if (text[index - 1] == '/')
+ {
+ return string.Empty;
+ }
+
+ var end = text.LastIndexOf("" + rootName, StringComparison.Ordinal);
+ return end < 0 || end <= index ? null : text[(index + 1)..end];
+ }
+
+ ///
+ /// Settle every binding whose source is part of the sample app rather than the sample.
+ ///
+ ///
+ /// Two kinds of binding do not survive extraction, and both point at the page the sample
+ /// file declares:
+ ///
+ ///
+ /// - An options-pane member. The options pane belongs to the sample app;
+ /// the sample generator emits its members onto the page class, so markup binding to one
+ /// names something the reader's project does not have.
+ /// - An ElementName naming the root element. The root is the
+ /// <Page> itself, which extraction discards, so the binding has no source
+ /// left once the fragment stands alone.
+ ///
+ ///
+ /// A binding whose whole path is an option becomes that option's default value, which
+ /// is what the sample showed before anyone touched the options pane. Anything else — a
+ /// function binding through a converter, a property of the discarded root — has no static
+ /// value, so the attribute carrying it is removed and the target control's own default
+ /// applies. Removing an attribute is the only edit with that property; there is no
+ /// equivalent for a value embedded in an expression.
+ ///
+ private static (string Text, List Resolved, List Dropped) ResolveSampleAppBindings(
+ string body,
+ IReadOnlyList options)
+ {
+ var resolved = new List();
+ var dropped = new List();
+
+ var byName = options.ToDictionary(o => o.Name, StringComparer.Ordinal);
+ var declaredNames = DeclaredElementNames(body);
+ var builder = new StringBuilder();
+ var cursor = 0;
+
+ foreach (var attribute in Attributes(body))
+ {
+ var bindings = Bindings(attribute.Value);
+ if (bindings.Count == 0)
+ {
+ continue;
+ }
+
+ var settlement = Settle(bindings, byName, declaredNames);
+ if (settlement is null)
+ {
+ continue;
+ }
+
+ builder.Append(body, cursor, attribute.Start - cursor);
+
+ if (settlement.Value.Replacement is { } value)
+ {
+ builder.Append(attribute.Name).Append("=\"").Append(EscapeAttributeValue(value)).Append('"');
+ resolved.Add(settlement.Value.Note!);
+ }
+ else
+ {
+ // Drop the attribute. The whitespace that separated it goes too, otherwise the
+ // line it occupied survives as trailing indentation.
+ dropped.Add(attribute.Name);
+ TrimTrailingInlineWhitespace(builder);
+ }
+
+ cursor = attribute.End;
+ }
+
+ builder.Append(body, cursor, body.Length - cursor);
+
+ resolved.Sort(StringComparer.Ordinal);
+ dropped.Sort(StringComparer.Ordinal);
+
+ return (builder.ToString(), Distinct(resolved), Distinct(dropped));
+ }
+
+ ///
+ /// Decide what to do with one attribute's bindings: substitute a default, drop the
+ /// attribute, or leave it alone.
+ ///
+ ///
+ /// Null when the attribute binds only to things that survive extraction. Otherwise a
+ /// replacement value, or null replacement meaning the attribute must go.
+ ///
+ private static (string? Replacement, string? Note)? Settle(
+ List bindings,
+ Dictionary options,
+ HashSet declaredNames)
+ {
+ var affected = false;
+
+ foreach (var binding in bindings)
+ {
+ var danglingSource = binding.ElementName is { Length: > 0 } element && !declaredNames.Contains(element);
+ var mentionsOption = options.Keys.Any(name => MentionsIdentifier(binding.Arguments, name));
+
+ if (!danglingSource && !mentionsOption)
+ {
+ continue;
+ }
+
+ affected = true;
+
+ // Only a lone binding whose entire path is an option has a value to substitute.
+ // Anything else leaves part of the expression unaccounted for.
+ if (bindings.Count == 1
+ && binding.Path is { } path
+ && options.TryGetValue(path, out var option)
+ && option.DefaultValue is { } value)
+ {
+ return (value, $"{option.Name}={value}");
+ }
+ }
+
+ return affected ? (null, null) : null;
+ }
+
+ /// Every x:Name declared inside the fragment.
+ ///
+ /// Used to tell a binding that still has a source from one that lost it. The root element's
+ /// own name is absent by construction — the fragment is the root's body — which is exactly
+ /// how an ElementName pointing at the discarded page is detected.
+ ///
+ private static HashSet DeclaredElementNames(string body)
+ {
+ var names = new HashSet(StringComparer.Ordinal);
+
+ foreach (var attribute in Attributes(body))
+ {
+ if (attribute.Name is "x:Name" or "Name")
+ {
+ names.Add(attribute.Value);
+ }
+ }
+
+ return names;
+ }
+
+ private static List Distinct(List values) =>
+ [.. values.Distinct(StringComparer.Ordinal)];
+
+ /// Remove the run of spaces and tabs just written, and a newline if that is all that is left.
+ private static void TrimTrailingInlineWhitespace(StringBuilder builder)
+ {
+ var index = builder.Length - 1;
+ while (index >= 0 && (builder[index] == ' ' || builder[index] == '\t'))
+ {
+ index--;
+ }
+
+ if (index >= 0 && builder[index] == '\n')
+ {
+ index--;
+ }
+
+ builder.Length = index + 1;
+ }
+
+ /// An attribute found in the raw text, with the span it occupies.
+ private readonly record struct RawAttribute(string Name, string Value, int Start, int End);
+
+ ///
+ /// Index just past the tag beginning at , for the tags that carry
+ /// no attributes.
+ ///
+ ///
+ /// A comment ends at --> and a CDATA section at ]]>, not at the first
+ /// >. Stopping at the first one would resume the scan inside the comment and read
+ /// markup the author commented out as though it were live: an x:Name in a comment
+ /// would count as a declared element, and an option binding in a comment would be rewritten
+ /// or deleted in the published text.
+ ///
+ private static int EndOfNonAttributeTag(string text, int tagStart)
+ {
+ var tail = text.AsSpan(tagStart);
+
+ var terminator =
+ tail.StartsWith(""
+ : tail.StartsWith(""
+ : tail.StartsWith("", StringComparison.Ordinal) ? "?>"
+ : ">";
+
+ var end = text.IndexOf(terminator, tagStart, StringComparison.Ordinal);
+ return end < 0 ? -1 : end + terminator.Length;
+ }
+
+ /// True when a tag carries no attributes: an end tag, comment, CDATA section or PI.
+ private static bool IsNonAttributeTag(string text, int tagStart) =>
+ tagStart + 1 < text.Length && text[tagStart + 1] is '/' or '!' or '?';
+
+ /// True when a tag is a comment, CDATA section, declaration or processing instruction.
+ /// Unlike an end tag, its contents are not markup and must never be rewritten.
+ private static bool IsCommentLikeTag(string text, int tagStart) =>
+ tagStart + 1 < text.Length && text[tagStart + 1] is '!' or '?';
+
+ /// The text with comments, CDATA sections and processing instructions removed.
+ ///
+ /// For the scans that read the raw text looking for markup. What an author commented out is
+ /// prose as far as this tool is concerned, and reading it as markup makes the tool report
+ /// requirements the published fragment does not have.
+ ///
+ private static string WithoutComments(string text)
+ {
+ var builder = new StringBuilder();
+ var index = 0;
+
+ while (index < text.Length)
+ {
+ var open = text.IndexOf('<', index);
+ if (open < 0 || !IsCommentLikeTag(text, open))
+ {
+ if (open < 0)
+ {
+ break;
+ }
+
+ builder.Append(text, index, open + 1 - index);
+ index = open + 1;
+ continue;
+ }
+
+ var end = EndOfNonAttributeTag(text, open);
+ if (end < 0)
+ {
+ return builder.Append(text, index, open - index).ToString();
+ }
+
+ builder.Append(text, index, open - index);
+ index = end;
+ }
+
+ return builder.Append(text, index, text.Length - index).ToString();
+ }
+
+ ///
+ /// Walk the attributes of every start tag in the fragment, in source order.
+ ///
+ ///
+ /// Done over the text because the edits are textual: the parsed tree knows which attributes
+ /// exist but not where they sit in the author's formatting, and rebuilding the text from
+ /// the tree is exactly what this extraction avoids.
+ ///
+ private static IEnumerable Attributes(string text)
+ {
+ var index = 0;
+
+ while (index < text.Length)
+ {
+ var tagStart = text.IndexOf('<', index);
+ if (tagStart < 0)
+ {
+ yield break;
+ }
+
+ // Skip comments, CDATA, processing instructions and end tags: none carry attributes.
+ if (IsNonAttributeTag(text, tagStart))
+ {
+ var skipTo = EndOfNonAttributeTag(text, tagStart);
+ if (skipTo < 0)
+ {
+ yield break;
+ }
+
+ index = skipTo;
+ continue;
+ }
+
+ var position = tagStart + 1;
+
+ // Element name.
+ while (position < text.Length && !char.IsWhiteSpace(text[position]) && text[position] != '>' && text[position] != '/')
+ {
+ position++;
+ }
+
+ while (position < text.Length && text[position] != '>')
+ {
+ if (char.IsWhiteSpace(text[position]) || text[position] == '/')
+ {
+ position++;
+ continue;
+ }
+
+ var nameStart = position;
+ while (position < text.Length && text[position] != '=' && text[position] != '>' && !char.IsWhiteSpace(text[position]))
+ {
+ position++;
+ }
+
+ var name = text[nameStart..position];
+
+ while (position < text.Length && char.IsWhiteSpace(text[position]))
+ {
+ position++;
+ }
+
+ if (position >= text.Length || text[position] != '=')
+ {
+ continue;
+ }
+
+ position++;
+ while (position < text.Length && char.IsWhiteSpace(text[position]))
+ {
+ position++;
+ }
+
+ if (position >= text.Length || (text[position] != '"' && text[position] != '\''))
+ {
+ continue;
+ }
+
+ var quote = text[position];
+ var valueStart = ++position;
+ while (position < text.Length && text[position] != quote)
+ {
+ position++;
+ }
+
+ if (position >= text.Length)
+ {
+ yield break;
+ }
+
+ yield return new RawAttribute(name, text[valueStart..position], nameStart, position + 1);
+ position++;
+ }
+
+ index = position + 1;
+ }
+ }
+
+ /// One binding markup extension found in an attribute value.
+ /// Everything between the extension name and its closing brace.
+ /// The binding path, from the positional argument or Path=.
+ /// The ElementName argument, when the binding names a source element.
+ internal readonly record struct BindingExpression(string Arguments, string? Path, string? ElementName);
+
+ ///
+ /// Read every {x:Bind} and {Binding} extension in an attribute value.
+ ///
+ ///
+ /// Both forms matter. x:Bind resolves against the page class, so it reaches the
+ /// generated option members; Binding with an ElementName reaches the page
+ /// element itself. Nested extensions such as Converter={StaticResource ...} are
+ /// stepped over rather than treated as bindings of their own.
+ ///
+ internal static List Bindings(string attributeValue)
+ {
+ var bindings = new List();
+
+ for (var index = 0; index < attributeValue.Length; index++)
+ {
+ if (attributeValue[index] != '{')
+ {
+ continue;
+ }
+
+ var nameStart = index + 1;
+ while (nameStart < attributeValue.Length && char.IsWhiteSpace(attributeValue[nameStart]))
+ {
+ nameStart++;
+ }
+
+ var nameEnd = nameStart;
+ while (nameEnd < attributeValue.Length
+ && !char.IsWhiteSpace(attributeValue[nameEnd])
+ && attributeValue[nameEnd] != '}'
+ && attributeValue[nameEnd] != ',')
+ {
+ nameEnd++;
+ }
+
+ var name = attributeValue[nameStart..nameEnd];
+ if (name is not ("x:Bind" or "Binding"))
+ {
+ continue;
+ }
+
+ var arguments = ExtensionArguments(attributeValue, nameEnd, out var end);
+ bindings.Add(Parse(arguments));
+
+ // Skip past this extension so a nested one is not read as a second binding.
+ index = end;
+ }
+
+ return bindings;
+ }
+
+ /// The text of an extension's arguments, with braces balanced.
+ private static string ExtensionArguments(string value, int start, out int end)
+ {
+ var depth = 0;
+ var index = start;
+
+ for (; index < value.Length; index++)
+ {
+ var c = value[index];
+ if (c == '{')
+ {
+ depth++;
+ }
+ else if (c == '}')
+ {
+ if (depth == 0)
+ {
+ break;
+ }
+
+ depth--;
+ }
+ }
+
+ end = index;
+ return value[start..index];
+ }
+
+ private static BindingExpression Parse(string arguments)
+ {
+ string? path = null;
+ string? elementName = null;
+
+ foreach (var argument in SplitTopLevel(arguments))
+ {
+ var separator = TopLevelEquals(argument);
+ if (separator < 0)
+ {
+ // The positional argument is the path, e.g. {x:Bind IsCardEnabled, Mode=OneWay}.
+ path ??= StripCast(argument.Trim()) is { Length: > 0 } value ? value : null;
+ continue;
+ }
+
+ var key = argument[..separator].Trim();
+ var argumentValue = argument[(separator + 1)..].Trim();
+
+ if (key == "Path")
+ {
+ path = StripCast(argumentValue);
+ }
+ else if (key == "ElementName")
+ {
+ elementName = argumentValue;
+ }
+ }
+
+ return new BindingExpression(arguments, path, elementName);
+ }
+
+ /// Remove a cast from the front of a binding path.
+ ///
+ /// x:Bind spells a cast as a parenthesised type ahead of the path, as in
+ /// (x:Int32)Columns, which still binds to Columns. Leaving the cast attached
+ /// means the path matches no option, so a binding that has a perfectly readable default to
+ /// substitute is treated as having none and its attribute is removed instead.
+ ///
+ /// A parenthesised group with nothing after it is an attached property rather than a
+ /// cast — {Binding (Grid.Row)} — and is part of the path, so it is left alone.
+ ///
+ private static string StripCast(string path)
+ {
+ var trimmed = path.TrimStart();
+ if (trimmed.Length == 0 || trimmed[0] != '(')
+ {
+ return path;
+ }
+
+ var close = trimmed.IndexOf(')');
+ if (close < 0)
+ {
+ return path;
+ }
+
+ var rest = trimmed[(close + 1)..].TrimStart();
+ return rest.Length == 0 ? path : rest;
+ }
+
+ /// Split extension arguments on commas that are not inside a nested extension.
+ private static List SplitTopLevel(string arguments)
+ {
+ var parts = new List();
+ var depth = 0;
+ var start = 0;
+
+ for (var index = 0; index < arguments.Length; index++)
+ {
+ switch (arguments[index])
+ {
+ case '{':
+ depth++;
+ break;
+ case '}':
+ depth--;
+ break;
+ case ',' when depth == 0:
+ parts.Add(arguments[start..index]);
+ start = index + 1;
+ break;
+ }
+ }
+
+ parts.Add(arguments[start..]);
+ return parts;
+ }
+
+ /// Index of the argument's =, ignoring any inside a nested extension.
+ private static int TopLevelEquals(string argument)
+ {
+ var depth = 0;
+
+ for (var index = 0; index < argument.Length; index++)
+ {
+ switch (argument[index])
+ {
+ case '{':
+ depth++;
+ break;
+ case '}':
+ depth--;
+ break;
+ case '=' when depth == 0:
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ ///
+ /// True when the text contains the identifier as a whole word.
+ ///
+ ///
+ /// Whole-word matching keeps an option named Color from claiming
+ /// ColorsCollection, which binds to something else entirely. A preceding dot also
+ /// disqualifies a match: in ViewPanel.OpenPaneLength the trailing name is a property
+ /// of a named element, not the option member.
+ ///
+ internal static bool MentionsIdentifier(string text, string identifier)
+ {
+ var index = text.IndexOf(identifier, StringComparison.Ordinal);
+
+ while (index >= 0)
+ {
+ var beforeOk = index == 0 || (!IsIdentifierChar(text[index - 1]) && text[index - 1] != '.');
+ var after = index + identifier.Length;
+ var afterOk = after >= text.Length || !IsIdentifierChar(text[after]);
+
+ if (beforeOk && afterOk)
+ {
+ return true;
+ }
+
+ index = text.IndexOf(identifier, index + 1, StringComparison.Ordinal);
+ }
+
+ return false;
+ }
+
+ private static bool IsIdentifierChar(char c) => char.IsLetterOrDigit(c) || c == '_';
+
+ private static string EscapeAttributeValue(string value) =>
+ value.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """);
+
+ /// Remove the indentation the fragment carried inside its root element.
+ private static string Dedent(string text)
+ {
+ var lines = text.Split('\n');
+ var indent = int.MaxValue;
+
+ foreach (var line in lines)
+ {
+ if (line.Trim().Length == 0)
+ {
+ continue;
+ }
+
+ var count = 0;
+ while (count < line.Length && (line[count] == ' ' || line[count] == '\t'))
+ {
+ count++;
+ }
+
+ indent = Math.Min(indent, count);
+ }
+
+ if (indent is 0 or int.MaxValue)
+ {
+ return text.Trim('\n', ' ', '\t');
+ }
+
+ var builder = new StringBuilder();
+ for (var i = 0; i < lines.Length; i++)
+ {
+ var line = lines[i];
+ builder.Append(line.Length >= indent ? line[indent..] : line.TrimStart());
+ if (i < lines.Length - 1)
+ {
+ builder.Append('\n');
+ }
+ }
+
+ return builder.ToString().Trim('\n', ' ', '\t');
+ }
+
+ /// The namespace declarations the sample file's root element carries.
+ private static List<(string Prefix, string Uri)> NamespaceDeclarations(XElement root)
+ {
+ var declarations = new List<(string Prefix, string Uri)>();
+
+ foreach (var attribute in root.Attributes())
+ {
+ if (!attribute.IsNamespaceDeclaration)
+ {
+ continue;
+ }
+
+ var prefix = attribute.Name.LocalName == "xmlns" ? string.Empty : attribute.Name.LocalName;
+ declarations.Add((prefix, attribute.Value));
+ }
+
+ return declarations;
+ }
+
+ ///
+ /// Remove prefixes that are only another spelling of the default namespace.
+ ///
+ ///
+ /// Several samples declare xmlns:win="…/presentation" and write <win:TextBox>.
+ /// That is Uno's conditional XAML, where the prefix means "only on Windows"; on Windows it
+ /// resolves to the same namespace as no prefix at all, so <win:TextBox> and
+ /// <TextBox> are the same element. Publishing the prefix would hand the reader
+ /// a multi-platform concept they did not ask about, together with an xmlns they have to
+ /// carry to make it parse. Dropping it leaves markup that is identical where it will run.
+ ///
+ private static string StripAliasPrefixes(
+ string fragment,
+ List<(string Prefix, string Uri)> declarations,
+ out List<(string Prefix, string Uri)> remaining)
+ {
+ var aliases = declarations
+ .Where(d => d.Prefix.Length > 0 && d.Uri == PresentationNamespace)
+ .Select(d => d.Prefix)
+ .ToHashSet(StringComparer.Ordinal);
+
+ remaining = declarations.Where(d => !aliases.Contains(d.Prefix)).ToList();
+
+ if (aliases.Count == 0)
+ {
+ return fragment;
+ }
+
+ var pattern = "(" + string.Join('|', aliases.Select(Regex.Escape)) + ")";
+ var builder = new StringBuilder();
+ var index = 0;
+
+ while (index < fragment.Length)
+ {
+ var open = fragment.IndexOf('<', index);
+ if (open < 0)
+ {
+ builder.Append(fragment, index, fragment.Length - index);
+ break;
+ }
+
+ // A comment or CDATA section declares no prefixes and must not be rewritten. It also
+ // has to be stepped over as a unit: EndOfStartTag reads an apostrophe in prose as an
+ // opening quote, which would swallow the rest of the fragment.
+ if (IsCommentLikeTag(fragment, open))
+ {
+ var skipTo = EndOfNonAttributeTag(fragment, open);
+ if (skipTo < 0)
+ {
+ builder.Append(fragment, index, fragment.Length - index);
+ break;
+ }
+
+ builder.Append(fragment, index, skipTo - index);
+ index = skipTo;
+ continue;
+ }
+
+ var close = EndOfStartTag(fragment, open);
+ if (close < 0)
+ {
+ builder.Append(fragment, index, fragment.Length - index);
+ break;
+ }
+
+ var tag = fragment[open..(close + 1)];
+ var stripped = Regex.Replace(tag, $@"(?<=^?){pattern}:", string.Empty);
+ stripped = Regex.Replace(stripped, $@"(?<=\s){pattern}:(?=[A-Za-z_])", string.Empty);
+
+ builder.Append(fragment, index, open - index);
+ builder.Append(stripped == tag ? tag : Realign(stripped, IndentOf(fragment, open)));
+ index = close + 1;
+ }
+
+ return builder.ToString();
+ }
+
+ /// Width of the whitespace run that begins the line containing .
+ private static int IndentOf(string text, int position)
+ {
+ var lineStart = text.LastIndexOf('\n', Math.Max(position - 1, 0)) + 1;
+ return position - lineStart;
+ }
+
+ ///
+ /// Re-indent a start tag's continuation lines so its attributes line up again.
+ ///
+ ///
+ /// Removing a prefix shortens the element name, which leaves every attribute on the lines
+ /// below it indented to a column that no longer exists. The repository formats XAML with
+ /// attributes aligned one space past the element name, so that is the column they are put
+ /// back at — the reader sees the house style, not the fact that something was edited.
+ ///
+ private static string Realign(string tag, int baseIndent)
+ {
+ var lines = tag.Split('\n');
+ if (lines.Length == 1)
+ {
+ return tag;
+ }
+
+ var name = Regex.Match(lines[0], @"^?(?[\w.:]+)");
+ if (!name.Success)
+ {
+ return tag;
+ }
+
+ var column = new string(' ', baseIndent + 1 + name.Groups["name"].Value.Length + 1);
+
+ for (var i = 1; i < lines.Length; i++)
+ {
+ // Only lines that begin with an attribute are the tag's own layout. A line that
+ // starts mid-value belongs to the value and is left exactly as the author wrote it.
+ if (!Regex.IsMatch(lines[i], @"^\s*[\w.:]+\s*=|^\s*/?>$"))
+ {
+ return tag;
+ }
+
+ lines[i] = column + lines[i].TrimStart();
+ }
+
+ return string.Join('\n', lines);
+ }
+
+ /// What relocating a root property element produced.
+ private readonly record struct Relocation(string? Xaml, string? Error);
+
+ ///
+ /// Move property elements belonging to the discarded root onto an element that survives.
+ ///
+ ///
+ /// A sample that defines a converter writes <Page.Resources>, which belongs to
+ /// the <Page> the fragment drops. Pasted into a <Grid> that markup
+ /// does not compile, because a Grid has no Page property — so the snippet would fail on the
+ /// reader's first build, in their file. Attaching the resources to the sample's own root
+ /// element instead keeps every key in scope for the markup that uses it, and is what the
+ /// author would have written had there been no page.
+ ///
+ private static Relocation RelocateRootProperties(string fragment, string rootName)
+ {
+ var propertyElement = Regex.Match(
+ fragment,
+ $@"^[ \t]*<{Regex.Escape(rootName)}\.(?\w+)>.*?{Regex.Escape(rootName)}\.\k>[ \t]*\n?",
+ RegexOptions.Singleline | RegexOptions.Multiline);
+
+ if (!propertyElement.Success)
+ {
+ return new Relocation(fragment, null);
+ }
+
+ var block = propertyElement.Value.Trim('\n');
+ var property = propertyElement.Groups["property"].Value;
+ var rest = fragment.Remove(propertyElement.Index, propertyElement.Length).Trim('\n');
+
+ if (Regex.IsMatch(rest, $@"<{Regex.Escape(rootName)}\.\w+>"))
+ {
+ return new Relocation(
+ null,
+ $"the sample declares more than one <{rootName}.*> block, which cannot be attached "
+ + "to a single element");
+ }
+
+ var host = Regex.Match(rest, @"^<(?[\w.:]+)");
+ if (!host.Success)
+ {
+ return new Relocation(
+ null,
+ $"the sample's <{rootName}.{property}> has no sibling element to attach to");
+ }
+
+ var hostName = host.Groups["name"].Value;
+ var startTagEnd = EndOfStartTag(rest, 0);
+ if (startTagEnd < 0)
+ {
+ return new Relocation(null, $"the start tag of <{hostName}> could not be located");
+ }
+
+ var selfClosing = rest[startTagEnd - 1] == '/';
+ var moved = Indent(
+ block.Replace($"<{rootName}.{property}>", $"<{hostName}.{property}>")
+ .Replace($"{rootName}.{property}>", $"{hostName}.{property}>"),
+ " ");
+
+ if (selfClosing)
+ {
+ // has to become to hold the resources.
+ var openTag = rest[..(startTagEnd - 1)].TrimEnd() + ">";
+ var after = rest[(startTagEnd + 1)..];
+ return new Relocation($"{openTag}\n{moved}\n{hostName}>{after}", null);
+ }
+
+ return new Relocation(
+ $"{rest[..(startTagEnd + 1)]}\n{moved}{rest[(startTagEnd + 1)..]}",
+ null);
+ }
+
+ /// Index of the '>' closing the start tag beginning at .
+ private static int EndOfStartTag(string text, int start)
+ {
+ var quote = '\0';
+
+ for (var i = start; i < text.Length; i++)
+ {
+ var c = text[i];
+
+ if (quote != '\0')
+ {
+ if (c == quote)
+ {
+ quote = '\0';
+ }
+
+ continue;
+ }
+
+ if (c is '"' or '\'')
+ {
+ quote = c;
+ }
+ else if (c == '>')
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private static string Indent(string text, string prefix) =>
+ string.Join(
+ '\n',
+ text.Split('\n').Select(line => line.Length == 0 ? line : prefix + line));
+
+ ///
+ /// Parse the fragment on its own, under the declarations its file provided, so that a
+ /// fragment referencing an undeclared prefix is caught here rather than by the reader.
+ ///
+ private static XElement ParseFragment(string fragment, List<(string Prefix, string Uri)> declarations)
+ {
+ var builder = new StringBuilder("').Append(fragment).Append("");
+
+ return XElement.Parse(builder.ToString(), LoadOptions.PreserveWhitespace);
+ }
+
+ ///
+ /// The declarations the fragment actually needs, written out as they appear in the source.
+ ///
+ ///
+ /// Only what is used: an import a sample does not need reads as an instruction to reference
+ /// a package it does not need. The default presentation namespace and the x language
+ /// namespace are left out because every XAML file the reader could paste into already
+ /// declares both, and the design-time namespaces are left out because they mean nothing
+ /// outside a designer.
+ ///
+ private static List UsedImports(
+ XElement fragment,
+ string fragmentText,
+ List<(string Prefix, string Uri)> declarations)
+ {
+ var usedUris = new HashSet(StringComparer.Ordinal);
+
+ foreach (var element in fragment.DescendantsAndSelf())
+ {
+ usedUris.Add(element.Name.NamespaceName);
+
+ foreach (var attribute in element.Attributes())
+ {
+ if (!attribute.IsNamespaceDeclaration)
+ {
+ usedUris.Add(attribute.Name.NamespaceName);
+ }
+ }
+ }
+
+ // Markup extensions live inside attribute values, where the XML parser sees only text,
+ // so their prefixes have to be read from the text. Commented-out markup is excluded:
+ // a prefix used only there is not one the reader has to declare.
+ var usedPrefixes = new HashSet(StringComparer.Ordinal);
+ foreach (Match match in MarkupExtensionPrefixRegex().Matches(WithoutComments(fragmentText)))
+ {
+ usedPrefixes.Add(match.Groups["prefix"].Value);
+ }
+
+ var imports = new List();
+
+ foreach (var (prefix, uri) in declarations)
+ {
+ if (prefix.Length == 0 || uri == PresentationNamespace || uri == XamlNamespace || DesignTimeNamespaces.Contains(uri))
+ {
+ continue;
+ }
+
+ if (usedUris.Contains(uri) || usedPrefixes.Contains(prefix))
+ {
+ imports.Add($"xmlns:{prefix}=\"{uri}\"");
+ }
+ }
+
+ imports.Sort(StringComparer.Ordinal);
+ return imports;
+ }
+}