diff --git a/components/SettingsControls/samples/SettingsExpanderSample.xaml b/components/SettingsControls/samples/SettingsExpanderSample.xaml index 4a3722792..328903a5b 100644 --- a/components/SettingsControls/samples/SettingsExpanderSample.xaml +++ b/components/SettingsControls/samples/SettingsExpanderSample.xaml @@ -1,4 +1,4 @@ - + diff --git a/components/SettingsControls/samples/SettingsExpanderSample.xaml.cs b/components/SettingsControls/samples/SettingsExpanderSample.xaml.cs index 7c4455185..605d13882 100644 --- a/components/SettingsControls/samples/SettingsExpanderSample.xaml.cs +++ b/components/SettingsControls/samples/SettingsExpanderSample.xaml.cs @@ -4,8 +4,9 @@ namespace SettingsControlsExperiment.Samples; -[ToolkitSampleBoolOption("IsCardEnabled", true, Title = "Is Enabled")] [ToolkitSampleBoolOption("IsCardExpanded", false, Title = "Is Expanded")] +// [ToolkitSampleBoolOption("IsCardEnabled", true, Title = "Is Enabled")] Disabled for now, see: https://github.com/CommunityToolkit/Labs-Windows/issues/362 + // Single values without a colon are used for both label and value. // To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). //[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")] diff --git a/components/SettingsControls/src/CommunityToolkit.Labs.WinUI.SettingsControls.csproj b/components/SettingsControls/src/CommunityToolkit.Labs.WinUI.SettingsControls.csproj index 036f78135..e1262f643 100644 --- a/components/SettingsControls/src/CommunityToolkit.Labs.WinUI.SettingsControls.csproj +++ b/components/SettingsControls/src/CommunityToolkit.Labs.WinUI.SettingsControls.csproj @@ -2,7 +2,7 @@ SettingsControls This package contains the SettingsCard and SettingsExpander controls. - 0.0.15 + 0.0.16 10.0 diff --git a/components/SettingsControls/src/Helpers/IsNullOrEmptyStateTrigger.cs b/components/SettingsControls/src/Helpers/IsNullOrEmptyStateTrigger.cs new file mode 100644 index 000000000..a66167daa --- /dev/null +++ b/components/SettingsControls/src/Helpers/IsNullOrEmptyStateTrigger.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Specialized; + +#if WINAPPSDK +using CommunityToolkit.WinUI.Helpers; +#else +using Microsoft.Toolkit.Uwp.Helpers; +#endif + + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Enables a state if an Object is null or a String/IEnumerable is empty +/// +public class IsNullOrEmptyStateTrigger : StateTriggerBase +{ + /// + /// Gets or sets the value used to check for null or empty. + /// + public object Value + { + get { return GetValue(ValueProperty); } + set { SetValue(ValueProperty, value); } + } + + /// + /// Identifies the DependencyProperty + /// + public static readonly DependencyProperty ValueProperty = + DependencyProperty.Register(nameof(Value), typeof(object), typeof(IsNullOrEmptyStateTrigger), new PropertyMetadata(null, OnValuePropertyChanged)); + + public IsNullOrEmptyStateTrigger() + { + UpdateTrigger(); + } + + private void UpdateTrigger() + { + var val = Value; + + SetActive(IsNullOrEmpty(val)); + + if (val == null) + { + return; + } + + // Try to listen for various notification events + // Starting with INorifyCollectionChanged +#pragma warning disable CS8622 // Nullability of reference types + var valNotifyCollection = val as INotifyCollectionChanged; + if (valNotifyCollection != null) + { + var weakEvent = new WeakEventListener(this) + { + OnEventAction = static (instance, source, args) => instance.SetActive(IsNullOrEmpty(source)), + OnDetachAction = (weakEventListener) => valNotifyCollection.CollectionChanged -= weakEventListener.OnEvent + }; + + valNotifyCollection.CollectionChanged += weakEvent.OnEvent; +#pragma warning restore CS8622 + return; + } + + // Not INotifyCollectionChanged, try IObservableVector + var valObservableVector = val as IObservableVector; + if (valObservableVector != null) + { + var weakEvent = new WeakEventListener(this) + { + OnEventAction = static (instance, source, args) => instance.SetActive(IsNullOrEmpty(source)), + OnDetachAction = (weakEventListener) => valObservableVector.VectorChanged -= weakEventListener.OnEvent + }; + + valObservableVector.VectorChanged += weakEvent.OnEvent; + return; + } + + // Not INotifyCollectionChanged, try IObservableMap + var valObservableMap = val as IObservableMap; + if (valObservableMap != null) + { + var weakEvent = new WeakEventListener>(this) + { + OnEventAction = static (instance, source, args) => instance.SetActive(IsNullOrEmpty(source)), + OnDetachAction = (weakEventListener) => valObservableMap.MapChanged -= weakEventListener.OnEvent + }; + + valObservableMap.MapChanged += weakEvent.OnEvent; + } + } + + private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var obj = (IsNullOrEmptyStateTrigger)d; + obj.UpdateTrigger(); + } + + private static bool IsNullOrEmpty(object val) + { + if (val == null) + { + return true; + } + + // Object is not null, check for an empty string + var valString = val as string; + if (valString != null) + { + return valString.Length == 0; + } + + // Object is not a string, check for an empty ICollection (faster) + var valCollection = val as ICollection; + if (valCollection != null) + { + return valCollection.Count == 0; + } + + // Object is not an ICollection, check for an empty IEnumerable + var valEnumerable = val as IEnumerable; + if (valEnumerable != null) + { + foreach (var item in valEnumerable) + { + // Found an item, not empty + return false; + } + + return true; + } + + // Not null and not a known type to test for emptiness + return false; + } +} diff --git a/components/SettingsControls/src/SettingsCard/SettingsCard.xaml b/components/SettingsControls/src/SettingsCard/SettingsCard.xaml index e49f11e67..550f76c43 100644 --- a/components/SettingsControls/src/SettingsCard/SettingsCard.xaml +++ b/components/SettingsControls/src/SettingsCard/SettingsCard.xaml @@ -294,22 +294,12 @@ + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -964,7 +988,6 @@ Grid.ColumnSpan="3" Margin="0,5" Background="{ThemeResource ToggleSwitchContainerBackground}" - Control.IsTemplateFocusTarget="True" CornerRadius="{TemplateBinding CornerRadius}" /> -/// A which also can restyle it's realized with a using the property. Currently, only checked once on loading. -/// -public sealed partial class StyledContentPresenter : ContentPresenter -{ - public StyleSelector ContentStyleSelector - { - get { return (StyleSelector)GetValue(ContentStyleSelectorProperty); } - set { SetValue(ContentStyleSelectorProperty, value); } - } - - public static readonly DependencyProperty ContentStyleSelectorProperty = - DependencyProperty.Register(nameof(ContentStyleSelector), typeof(StyleSelector), typeof(StyledContentPresenter), new PropertyMetadata(null)); - - public StyledContentPresenter() - { - // TODO: Not sure if we need to worry about content/template changing and restyling in response to that - // Not sure if we can detect that and hook in at the right spot regardless... - Loaded += this.StyledContentPresenter_Loaded; - } - - private void StyledContentPresenter_Loaded(object sender, RoutedEventArgs e) - { - // We need to wait and get the child element when the presenter is loaded, as Content is generally the data item itself. - var child = VisualTreeHelper.GetChild(this, 0); - - if (child is FrameworkElement element) - { - var style = ContentStyleSelector.SelectStyle(Content, element); - - // We don't want to blank out the style if we don't have a new one to provide. - if (style != null) - { - element.Style = style; - } - } - } -} diff --git a/components/SettingsControls/src/SettingsExpander/SettingsExpander.cs b/components/SettingsControls/src/SettingsExpander/SettingsExpander.cs index 37f0fc7db..02d60d228 100644 --- a/components/SettingsControls/src/SettingsExpander/SettingsExpander.cs +++ b/components/SettingsControls/src/SettingsExpander/SettingsExpander.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Labs.WinUI.Internal; - namespace CommunityToolkit.Labs.WinUI; //// Note: ItemsRepeater will request all the available horizontal space: https://github.com/microsoft/microsoft-ui-xaml/issues/3842 diff --git a/components/SettingsControls/src/SettingsExpander/SettingsExpander.xaml b/components/SettingsControls/src/SettingsExpander/SettingsExpander.xaml index 34c5b80e6..baf228a4b 100644 --- a/components/SettingsControls/src/SettingsExpander/SettingsExpander.xaml +++ b/components/SettingsControls/src/SettingsExpander/SettingsExpander.xaml @@ -16,6 +16,7 @@ 58,8,44,8 0,1,0,0 58,8,16,8 + 16 + diff --git a/components/Shimmer/src/Themes/Generic.xaml b/components/Shimmer/src/Themes/Generic.xaml new file mode 100644 index 000000000..727df06d0 --- /dev/null +++ b/components/Shimmer/src/Themes/Generic.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/components/Shimmer/tests/ExampleShimmerTestClass.cs b/components/Shimmer/tests/ExampleShimmerTestClass.cs new file mode 100644 index 000000000..7e5dc44dd --- /dev/null +++ b/components/Shimmer/tests/ExampleShimmerTestClass.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Tooling.TestGen; +using CommunityToolkit.Tests; + +namespace ShimmerExperiment.Tests; + +[TestClass] +public partial class ExampleShimmerTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(Shimmer).Assembly; + var type = assembly.GetType(typeof(Shimmer).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find Shimmer type."); + Assert.AreEqual(typeof(Shimmer), type, "Type of Shimmer 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 Shimmer(); + 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(ExampleShimmerTestPage 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("ShimmerControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleShimmerTestPage 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 Shimmer(); + 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 Shimmer(); + 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 Shimmer(); + 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/Shimmer/tests/ExampleShimmerTestPage.xaml b/components/Shimmer/tests/ExampleShimmerTestPage.xaml new file mode 100644 index 000000000..118dbd063 --- /dev/null +++ b/components/Shimmer/tests/ExampleShimmerTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/Shimmer/tests/ExampleShimmerTestPage.xaml.cs b/components/Shimmer/tests/ExampleShimmerTestPage.xaml.cs new file mode 100644 index 000000000..151241198 --- /dev/null +++ b/components/Shimmer/tests/ExampleShimmerTestPage.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 ShimmerExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleShimmerTestPage : Page +{ + public ExampleShimmerTestPage() + { + this.InitializeComponent(); + } +} diff --git a/components/Shimmer/tests/Shimmer.Tests.projitems b/components/Shimmer/tests/Shimmer.Tests.projitems new file mode 100644 index 000000000..a15d6ab2b --- /dev/null +++ b/components/Shimmer/tests/Shimmer.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 565F6A24-CB03-4FFA-A2D2-AD9E5EFE9485 + + + ShimmerExperiment.Tests + + + + + ExampleShimmerTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/Shimmer/tests/Shimmer.Tests.shproj b/components/Shimmer/tests/Shimmer.Tests.shproj new file mode 100644 index 000000000..94521372d --- /dev/null +++ b/components/Shimmer/tests/Shimmer.Tests.shproj @@ -0,0 +1,13 @@ + + + + 565F6A24-CB03-4FFA-A2D2-AD9E5EFE9485 + 14.0 + + + + + + + +