diff --git a/components/Segmented/OpenSolution.bat b/components/Segmented/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/Segmented/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/Segmented/samples/Assets/Segmented.png b/components/Segmented/samples/Assets/Segmented.png new file mode 100644 index 00000000..ba9ad789 Binary files /dev/null and b/components/Segmented/samples/Assets/Segmented.png differ diff --git a/components/Segmented/samples/Dependencies.props b/components/Segmented/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Segmented/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Segmented/samples/Segmented.Samples.csproj b/components/Segmented/samples/Segmented.Samples.csproj new file mode 100644 index 00000000..bb7b1ed2 --- /dev/null +++ b/components/Segmented/samples/Segmented.Samples.csproj @@ -0,0 +1,8 @@ + + + Segmented + + + + + diff --git a/components/Segmented/samples/Segmented.md b/components/Segmented/samples/Segmented.md new file mode 100644 index 00000000..39afa292 --- /dev/null +++ b/components/Segmented/samples/Segmented.md @@ -0,0 +1,28 @@ +--- +title: Segmented +author: niels9001 +description: A common UI control to configure a view or setting. +keywords: SegmentedControl, Control, Layout, Segmented +dev_langs: + - csharp +category: Controls +subcategory: Input +discussion-id: 314 +issue-id: 392 +icon: Assets/Segmented.png +--- + +## The basics + +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`. + +> [!Sample SegmentedBasicSample] + +## Selection +`Segmented` supports single and multi-selection. When `SelectionMode` is set to `Single` the first item will be selected by default. This can be overriden by settings `AutoSelection` to `false`. + +## Other styles + +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. + +> [!SAMPLE SegmentedStylesSample] diff --git a/components/Segmented/samples/SegmentedBasicSample.xaml b/components/Segmented/samples/SegmentedBasicSample.xaml new file mode 100644 index 00000000..47d31e96 --- /dev/null +++ b/components/Segmented/samples/SegmentedBasicSample.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Segmented/samples/SegmentedBasicSample.xaml.cs b/components/Segmented/samples/SegmentedBasicSample.xaml.cs new file mode 100644 index 00000000..9ebd0725 --- /dev/null +++ b/components/Segmented/samples/SegmentedBasicSample.xaml.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls; + +namespace SegmentedExperiment.Samples; + +/// +/// An example sample page of a Segmented control. +/// +[ToolkitSampleMultiChoiceOption("SelectionMode", "Single", "Multiple", Title = "Selection mode")] +[ToolkitSampleMultiChoiceOption("Alignment", "Left", "Center", "Right", "Stretch", Title = "Horizontal alignment")] + +[ToolkitSample(id: nameof(SegmentedBasicSample), "Basics", description: $"A sample for showing how to create and use a {nameof(Segmented)} custom control.")] +public sealed partial class SegmentedBasicSample : Page +{ + public SegmentedBasicSample() + { + this.InitializeComponent(); + } + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static ListViewSelectionMode ConvertStringToSelectionMode(string selectionMode) => selectionMode switch + { + "Single" => ListViewSelectionMode.Single, + "Multiple" => ListViewSelectionMode.Multiple, + _ => throw new System.NotImplementedException(), + }; + + public static HorizontalAlignment ConvertStringToHorizontalAlignment(string alignment) => alignment switch + { + "Left" => HorizontalAlignment.Left, + "Center" => HorizontalAlignment.Center, + "Right" => HorizontalAlignment.Right, + "Stretch" => HorizontalAlignment.Stretch, + _ => throw new System.NotImplementedException(), + }; +} + diff --git a/components/Segmented/samples/SegmentedStylesSample.xaml b/components/Segmented/samples/SegmentedStylesSample.xaml new file mode 100644 index 00000000..511c7eb2 --- /dev/null +++ b/components/Segmented/samples/SegmentedStylesSample.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + Item 1 + Item 2 + Item with long label + Item 4 + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Segmented/samples/SegmentedStylesSample.xaml.cs b/components/Segmented/samples/SegmentedStylesSample.xaml.cs new file mode 100644 index 00000000..f3fac18d --- /dev/null +++ b/components/Segmented/samples/SegmentedStylesSample.xaml.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace SegmentedExperiment.Samples; + +/// +/// An sample that shows how the Segmented control has multiple built-in styles. +/// +[ToolkitSampleMultiChoiceOption("SelectionMode", "Single", "Multiple", Title = "Selection mode")] + +[ToolkitSample(id: nameof(SegmentedStylesSample), "Additional styles", description: "A sample on how to use different built-in styles.")] +public sealed partial class SegmentedStylesSample : Page +{ + public SegmentedStylesSample() + { + this.InitializeComponent(); + } + public static ListViewSelectionMode ConvertStringToSelectionMode(string selectionMode) => selectionMode switch + { + "Single" => ListViewSelectionMode.Single, + "Multiple" => ListViewSelectionMode.Multiple, + _ => throw new System.NotImplementedException(), + }; +} diff --git a/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj b/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj new file mode 100644 index 00000000..5cc12018 --- /dev/null +++ b/components/Segmented/src/CommunityToolkit.WinUI.Controls.Segmented.csproj @@ -0,0 +1,20 @@ + + + Segmented + This package contains Segmented. + + + CommunityToolkit.WinUI.Controls.SegmentedRns + + + + + + + + + + + $(PackageIdPrefix).$(PackageIdVariant).Controls.$(ToolkitComponentName) + + diff --git a/components/Segmented/src/Dependencies.props b/components/Segmented/src/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Segmented/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Segmented/src/EqualPanel.cs b/components/Segmented/src/EqualPanel.cs new file mode 100644 index 00000000..9e2eb548 --- /dev/null +++ b/components/Segmented/src/EqualPanel.cs @@ -0,0 +1,101 @@ +// 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.Data; + +namespace CommunityToolkit.WinUI.Controls; + +public partial class EqualPanel : Panel +{ + private double _maxItemWidth = 0; + private double _maxItemHeight = 0; + private int _visibleItemsCount = 0; + public double Spacing + { + get { return (double)GetValue(SpacingProperty); } + set { SetValue(SpacingProperty, value); } + } + + /// + /// Identifies the Spacing dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register( + nameof(Spacing), + typeof(double), + typeof(EqualPanel), + new PropertyMetadata(default(double), OnSpacingChanged)); + + public EqualPanel() + { + RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnHorizontalAlignmentChanged); + } + + protected override Size MeasureOverride(Size availableSize) + { + _maxItemWidth = 0; + _maxItemHeight = 0; + + var elements = Children.Where(static e => e.Visibility == Visibility.Visible); + _visibleItemsCount = elements.Count(); + + foreach (var child in elements) + { + child.Measure(availableSize); + _maxItemWidth = Math.Max(_maxItemWidth, child.DesiredSize.Width); + _maxItemHeight = Math.Max(_maxItemHeight, child.DesiredSize.Height); + } + + if (_visibleItemsCount > 0) + { + // Return equal widths based on the widest item + // In very specific edge cases the AvailableWidth might be infinite resulting in a crash. + if (HorizontalAlignment != HorizontalAlignment.Stretch || double.IsInfinity(availableSize.Width)) + { + return new Size((_maxItemWidth * _visibleItemsCount) + (Spacing * (_visibleItemsCount - 1)), _maxItemHeight); + } + else + { + // Equal columns based on the available width, adjust for spacing + double totalWidth = availableSize.Width - (Spacing * (_visibleItemsCount - 1)); + _maxItemWidth = totalWidth / _visibleItemsCount; + return new Size(availableSize.Width, _maxItemHeight); + } + } + else + { + return new Size(0, 0); + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + double x = 0; + + // Check if there's more width available - if so, recalculate (e.g. whenever Grid.Column is set to Auto) + if (finalSize.Width > _visibleItemsCount * _maxItemWidth + (Spacing * (_visibleItemsCount - 1))) + { + MeasureOverride(finalSize); + } + + var elements = Children.Where(static e => e.Visibility == Visibility.Visible); + foreach (var child in elements) + { + child.Arrange(new Rect(x, 0, _maxItemWidth, _maxItemHeight)); + x += _maxItemWidth + Spacing; + } + return finalSize; + } + + private void OnHorizontalAlignmentChanged(DependencyObject sender, DependencyProperty dp) + { + InvalidateMeasure(); + } + + private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var panel = (EqualPanel)d; + panel.InvalidateMeasure(); + } +} diff --git a/components/Segmented/src/Helpers/ControlHelpers.cs b/components/Segmented/src/Helpers/ControlHelpers.cs new file mode 100644 index 00000000..28c3c8af --- /dev/null +++ b/components/Segmented/src/Helpers/ControlHelpers.cs @@ -0,0 +1,10 @@ +// 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.WinUI.Controls; + +internal static partial class ControlHelpers +{ + internal static bool IsXamlRootAvailable { get; } = Windows.Foundation.Metadata.ApiInformation.IsPropertyPresent("Windows.UI.Xaml.UIElement", "XamlRoot"); +} diff --git a/components/Segmented/src/Helpers/SegmentedMarginConverter.cs b/components/Segmented/src/Helpers/SegmentedMarginConverter.cs new file mode 100644 index 00000000..9de95ded --- /dev/null +++ b/components/Segmented/src/Helpers/SegmentedMarginConverter.cs @@ -0,0 +1,79 @@ +// 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.WinUI.Controls; + +public partial class SegmentedMarginConverter : DependencyObject, IValueConverter +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty LeftItemMarginProperty = + DependencyProperty.Register(nameof(LeftItemMargin), typeof(Thickness), typeof(SegmentedMarginConverter), new PropertyMetadata(null)); + + /// + /// Gets or sets the margin of the first item + /// + public Thickness LeftItemMargin + { + get { return (Thickness)GetValue(LeftItemMarginProperty); } + set { SetValue(LeftItemMarginProperty, value); } + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty MiddleItemMarginProperty = + DependencyProperty.Register(nameof(MiddleItemMargin), typeof(Thickness), typeof(SegmentedMarginConverter), new PropertyMetadata(null)); + + /// + /// Gets or sets the margin of any middle item + /// + public Thickness MiddleItemMargin + { + get { return (Thickness)GetValue(MiddleItemMarginProperty); } + set { SetValue(MiddleItemMarginProperty, value); } + } + + /// + /// Identifies the property. + /// + public static readonly DependencyProperty RightItemMarginProperty = + DependencyProperty.Register(nameof(RightItemMargin), typeof(Thickness), typeof(SegmentedMarginConverter), new PropertyMetadata(null)); + + /// + /// Gets or sets the margin of the last item + /// + public Thickness RightItemMargin + { + get { return (Thickness)GetValue(RightItemMarginProperty); } + set { SetValue(RightItemMarginProperty, value); } + } + + public object Convert(object value, Type targetType, object parameter, string language) + { + var segmentedItem = value as SegmentedItem; + var listView = ItemsControl.ItemsControlFromItemContainer(segmentedItem); + + int index = listView.IndexFromContainer(segmentedItem); + + if (index == 0) + { + return LeftItemMargin; + } + else if (index == listView.Items.Count - 1) + { + return RightItemMargin; + } + else + { + return MiddleItemMargin; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + return value; + } +} diff --git a/components/Segmented/src/MultiTarget.props b/components/Segmented/src/MultiTarget.props new file mode 100644 index 00000000..b11c1942 --- /dev/null +++ b/components/Segmented/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + + \ No newline at end of file diff --git a/components/Segmented/src/Segmented/Segmented.cs b/components/Segmented/src/Segmented/Segmented.cs new file mode 100644 index 00000000..75e0fb0f --- /dev/null +++ b/components/Segmented/src/Segmented/Segmented.cs @@ -0,0 +1,145 @@ +// 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 Windows.System; + +namespace CommunityToolkit.WinUI.Controls; + +public partial class Segmented : ListViewBase +{ + private int _internalSelectedIndex = -1; + private bool _hasLoaded = false; + + public Segmented() + { + this.DefaultStyleKey = typeof(Segmented); + + RegisterPropertyChangedCallback(SelectedIndexProperty, OnSelectedIndexChanged); + } + + protected override DependencyObject GetContainerForItemOverride() => new SegmentedItem(); + + protected override bool IsItemItsOwnContainerOverride(object item) + { + return item is SegmentedItem; + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + if (!_hasLoaded) + { + SelectedIndex = _internalSelectedIndex; + _hasLoaded = true; + } + PreviewKeyDown -= Segmented_PreviewKeyDown; + PreviewKeyDown += Segmented_PreviewKeyDown; + } + + protected override void PrepareContainerForItemOverride(DependencyObject element, object item) + { + base.PrepareContainerForItemOverride(element, item); + if (element is SegmentedItem segmentedItem) + { + segmentedItem.Loaded += SegmentedItem_Loaded; + } + } + + private void Segmented_PreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + switch (e.Key) + { + case VirtualKey.Left: e.Handled = MoveFocus(MoveDirection.Previous); break; + case VirtualKey.Right: e.Handled = MoveFocus(MoveDirection.Next); break; + } + } + + private void SegmentedItem_Loaded(object sender, RoutedEventArgs e) + { + if (sender is SegmentedItem segmentedItem) + { + segmentedItem.Loaded -= SegmentedItem_Loaded; + } + } + + protected override void OnItemsChanged(object e) + { + base.OnItemsChanged(e); + } + + private enum MoveDirection + { + Next, + Previous + } + + /// + /// Adjust the selected item and range based on keyboard input. + /// This is used to override the ListView behaviors for up/down arrow manipulation vs left/right for a horizontal control + /// + /// direction to move the selection + /// True if the focus was moved, false otherwise + private bool MoveFocus(MoveDirection direction) + { + bool retVal = false; + var currentContainerItem = GetCurrentContainerItem(); + + if (currentContainerItem != null) + { + var currentItem = ItemFromContainer(currentContainerItem); + var previousIndex = Items.IndexOf(currentItem); + var index = previousIndex; + + if (direction == MoveDirection.Previous) + { + if (previousIndex > 0) + { + index -= 1; + } + else + { + retVal = true; + } + } + else if (direction == MoveDirection.Next) + { + if (previousIndex < Items.Count - 1) + { + index += 1; + } + } + + // Only do stuff if the index is actually changing + if (index != previousIndex && ContainerFromIndex(index) is SegmentedItem newItem) + { + newItem.Focus(FocusState.Keyboard); + retVal = true; + } + } + + return retVal; + } + + private SegmentedItem? GetCurrentContainerItem() + { + if (ControlHelpers.IsXamlRootAvailable && XamlRoot != null) + { + return FocusManager.GetFocusedElement(XamlRoot) as SegmentedItem; + } + else + { + return FocusManager.GetFocusedElement() as SegmentedItem; + } + } + + private void OnSelectedIndexChanged(DependencyObject sender, DependencyProperty dp) + { + // This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/8257 + if (_internalSelectedIndex == -1 && SelectedIndex > -1) + { + // We catch the correct SelectedIndex and save it. + _internalSelectedIndex = SelectedIndex; + } + } +} diff --git a/components/Segmented/src/Segmented/Segmented.xaml b/components/Segmented/src/Segmented/Segmented.xaml new file mode 100644 index 00000000..ffc5460b --- /dev/null +++ b/components/Segmented/src/Segmented/Segmented.xaml @@ -0,0 +1,121 @@ + + + + + + + + + + 1 + + + + + 1 + + + + + 1 + + + + + 1 + 2 + + + + + + + diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.Properties.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.Properties.cs new file mode 100644 index 00000000..d7bf2824 --- /dev/null +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.Properties.cs @@ -0,0 +1,26 @@ +// 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.WinUI.Controls; + +public partial class SegmentedItem : ListViewItem +{ + /// + /// The backing for the property. + /// + public static readonly DependencyProperty IconProperty = DependencyProperty.Register( + nameof(Icon), + typeof(IconElement), + typeof(SegmentedItem), + new PropertyMetadata(defaultValue: null, (d, e) => ((SegmentedItem)d).OnIconPropertyChanged((IconElement)e.OldValue, (IconElement)e.NewValue))); + + /// + /// Gets or sets the icon. + /// + public IconElement Icon + { + get => (IconElement)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } +} diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.cs b/components/Segmented/src/SegmentedItem/SegmentedItem.cs new file mode 100644 index 00000000..94a9769f --- /dev/null +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +[ContentProperty(Name = nameof(Content))] +public partial class SegmentedItem : ListViewItem +{ + internal const string IconLeftState = "IconLeft"; + internal const string IconOnlyState = "IconOnly"; + internal const string ContentOnlyState = "ContentOnly"; + + public SegmentedItem() + { + this.DefaultStyleKey = typeof(SegmentedItem); + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + OnIconChanged(); + ContentChanged(); + } + + protected override void OnContentChanged(object oldContent, object newContent) + { + base.OnContentChanged(oldContent, newContent); + ContentChanged(); + } + + private void ContentChanged() + { + if (Content != null) + { + VisualStateManager.GoToState(this, IconLeftState, true); + } + else + { + VisualStateManager.GoToState(this, IconOnlyState, true); + } + } + + protected virtual void OnIconPropertyChanged(IconElement oldValue, IconElement newValue) + { + OnIconChanged(); + } + + private void OnIconChanged() + { + if (Icon != null) + { + VisualStateManager.GoToState(this, IconLeftState, true); + } + else + { + VisualStateManager.GoToState(this, ContentOnlyState, true); + } + } +} diff --git a/components/Segmented/src/SegmentedItem/SegmentedItem.xaml b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml new file mode 100644 index 00000000..f4cfc4d5 --- /dev/null +++ b/components/Segmented/src/SegmentedItem/SegmentedItem.xaml @@ -0,0 +1,1098 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0.55 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0.55 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0.55 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00:00:00.167 + + + + + + + + diff --git a/components/Segmented/src/Themes/Generic.xaml b/components/Segmented/src/Themes/Generic.xaml new file mode 100644 index 00000000..acf563f4 --- /dev/null +++ b/components/Segmented/src/Themes/Generic.xaml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/components/Segmented/tests/ExampleSegmentedTestClass.cs b/components/Segmented/tests/ExampleSegmentedTestClass.cs new file mode 100644 index 00000000..c2e040b9 --- /dev/null +++ b/components/Segmented/tests/ExampleSegmentedTestClass.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls; +using CommunityToolkit.Tooling.TestGen; +using CommunityToolkit.Tests; + +namespace SegmentedExperiment.Tests; + +[TestClass] +public partial class ExampleSegmentedTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(Segmented).Assembly; + var type = assembly.GetType(typeof(Segmented).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find Segmented control type."); + Assert.AreEqual(typeof(Segmented), type, "Type of Segmented control does not match expected type."); + } + + // If you don't need access to UI objects directly, use this pattern. + [TestMethod] + public async Task SimpleAsyncExampleTest() + { + await Task.Delay(250); + + Assert.IsTrue(true); + } + + // Example that shows how to check for exception throwing. + [TestMethod] + public void SimpleExceptionCheckTest() + { + // If you need to check exceptions occur for invalid inputs, etc... + // Use Assert.ThrowsException to limit the scope to where you expect the error to occur. + // Otherwise, using the ExpectedException attribute could swallow or + // catch other issues in setup code. + Assert.ThrowsException(() => throw new NotImplementedException()); + } + + // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects. + [UIThreadTestMethod] + public void SimpleUIAttributeExampleTest() + { + var component = new Segmented(); + Assert.IsNotNull(component); + } + + // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. + // This lets us actually test a control as it would behave within an actual application. + // The page will already be loaded by the time your test is called. + [UIThreadTestMethod] + public void SimpleUIExamplePageTest(ExampleSegmentedTestPage page) + { + // You can use the Toolkit Visual Tree helpers here to find the component by type or name: + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + + var componentByName = page.FindDescendant("SegmentedControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleSegmentedTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + } + + //// ----------------------------- ADVANCED TEST SCENARIOS ----------------------------- + + // If you need to use DataRow, you can use this pattern with the UI dispatch still. + // Otherwise, checkout the UIThreadTestMethod attribute above. + // See https://github.com/CommunityToolkit/Labs-Windows/issues/186 + [TestMethod] + public async Task ComplexAsyncUIExampleTest() + { + await EnqueueAsync(() => + { + var component = new Segmented(); + Assert.IsNotNull(component); + }); + } + + // If you want to load other content not within a XAML page using the UIThreadTestMethod above. + // Then you can do that using the Load/UnloadTestContentAsync methods. + [TestMethod] + public async Task ComplexAsyncLoadUIExampleTest() + { + await EnqueueAsync(async () => + { + var component = new Segmented(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + }); + } + + // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well: + [UIThreadTestMethod] + public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() + { + var component = new Segmented(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + } +} diff --git a/components/Segmented/tests/ExampleSegmentedTestPage.xaml b/components/Segmented/tests/ExampleSegmentedTestPage.xaml new file mode 100644 index 00000000..3223a11a --- /dev/null +++ b/components/Segmented/tests/ExampleSegmentedTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/Segmented/tests/ExampleSegmentedTestPage.xaml.cs b/components/Segmented/tests/ExampleSegmentedTestPage.xaml.cs new file mode 100644 index 00000000..23376e02 --- /dev/null +++ b/components/Segmented/tests/ExampleSegmentedTestPage.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace SegmentedExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleSegmentedTestPage : Page +{ + public ExampleSegmentedTestPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Segmented/tests/Segmented.Tests.projitems b/components/Segmented/tests/Segmented.Tests.projitems new file mode 100644 index 00000000..9c0e88b3 --- /dev/null +++ b/components/Segmented/tests/Segmented.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + AA9FB2D1-9A19-4442-A2FC-B62003F99558 + + + SegmentedExperiment.Tests + + + + + ExampleSegmentedTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/Segmented/tests/Segmented.Tests.shproj b/components/Segmented/tests/Segmented.Tests.shproj new file mode 100644 index 00000000..9c2cfd0e --- /dev/null +++ b/components/Segmented/tests/Segmented.Tests.shproj @@ -0,0 +1,13 @@ + + + + AA9FB2D1-9A19-4442-A2FC-B62003F99558 + 14.0 + + + + + + + +