diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index 537fbb0698d3..a51cf6a2c0a9 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -1410,6 +1410,25 @@ static IEnumerable SetBinding(VariableDefinition parent, FieldRefer static bool CanSetValue(FieldReference bpRef, bool attached, INode node, IXmlLineInfo iXmlLineInfo, ILContext context) { + static bool CanSetValue (TypeReference bpTypeRef, VariableDefinition varValue, ILContext context, IXmlLineInfo iXmlLineInfo) + { + // If it's an attached BP, there's no second chance to handle IMarkupExtensions, so we try here. + // Worst case scenario ? InvalidCastException at runtime + if (varValue.VariableType.FullName == "System.Object") + return true; + var implicitOperator = varValue.VariableType.GetImplicitOperatorTo(context.Cache, bpTypeRef, context.Body.Method.Module); + if (implicitOperator != null) + return true; + + //as we're in the SetValue Scenario, we can accept value types, they'll be boxed + if (varValue.VariableType.IsValueType && bpTypeRef.FullName == "System.Object") + return true; + + if (varValue.VariableType.InheritsFromOrImplements(context.Cache, bpTypeRef) || varValue.VariableType.FullName == "System.Object") + return true; + return false; + } + var module = context.Body.Method.Module; if (bpRef == null) @@ -1424,22 +1443,23 @@ static bool CanSetValue(FieldReference bpRef, bool attached, INode node, IXmlLin if (!context.Variables.TryGetValue(elementNode, out VariableDefinition varValue)) return false; + var bpTypeRef = bpRef.GetBindablePropertyType(context.Cache, iXmlLineInfo, module); - // If it's an attached BP, there's no second chance to handle IMarkupExtensions, so we try here. - // Worst case scenario ? InvalidCastException at runtime - if (varValue.VariableType.FullName == "System.Object") - return true; - var implicitOperator = varValue.VariableType.GetImplicitOperatorTo(context.Cache, bpTypeRef, module); - if (implicitOperator != null) - return true; - //as we're in the SetValue Scenario, we can accept value types, they'll be boxed - if (varValue.VariableType.IsValueType && bpTypeRef.FullName == "System.Object") + if (CanSetValue(bpTypeRef, varValue, context, iXmlLineInfo)) return true; - return varValue.VariableType.InheritsFromOrImplements(context.Cache, bpTypeRef) || varValue.VariableType.FullName == "System.Object"; + if (bpTypeRef.ResolveCached(context.Cache).FullName == "System.Nullable`1") + { + bpTypeRef = ((GenericInstanceType)bpTypeRef).GenericArguments[0]; + if (CanSetValue(bpTypeRef, varValue, context, iXmlLineInfo)) + return true; + } + + return false; } + static bool CanGetValue(VariableDefinition parent, FieldReference bpRef, bool attached, IXmlLineInfo iXmlLineInfo, ILContext context, out TypeReference propertyType) { var module = context.Body.Method.Module; @@ -1483,32 +1503,37 @@ static IEnumerable SetValue(VariableDefinition parent, FieldReferen var @else = Create(OpCodes.Nop); var endif = Create(OpCodes.Nop); - if (context.Variables[elementNode].VariableType.FullName == "System.Object") + + var varValue = context.Variables[elementNode]; + if (varValue.VariableType.FullName == "System.Object") { //if(value != null && value.GetType().IsAssignableFrom(typeof(BindingBase))) - yield return Create(Ldloc, context.Variables[elementNode]); + yield return Create(Ldloc, varValue); yield return Create(Brfalse, @else); - yield return Create(Ldtoken, module.ImportReference(context.Cache, ("Microsoft.Maui.Controls", "Microsoft.Maui.Controls", "BindingBase"))); yield return Create(Call, module.ImportMethodReference(context.Cache, ("mscorlib", "System", "Type"), methodName: "GetTypeFromHandle", parameterTypes: new[] { ("mscorlib", "System", "RuntimeTypeHandle") }, isStatic: true)); - yield return Create(Ldloc, context.Variables[elementNode]); + yield return Create(Ldloc, varValue); yield return Create(Callvirt, module.ImportMethodReference(context.Cache, ("mscorlib", "System", "Object"), methodName: "GetType", paramCount: 0)); yield return Create(Callvirt, module.ImportMethodReference(context.Cache, ("mscorlib", "System", "Type"), methodName: "IsAssignableFrom", parameterTypes: new[] { ("mscorlib", "System", "Type") })); yield return Create(Brfalse, @else); //then - yield return Create(Ldloc, context.Variables[elementNode]); + yield return Create(Ldloc, varValue); yield return Create(Br, endif); //else yield return @else; } var bpTypeRef = bpRef.GetBindablePropertyType(context.Cache, iXmlLineInfo, module); - foreach (var instruction in context.Variables[elementNode].LoadAs(context.Cache, bpTypeRef, module)) + foreach (var instruction in varValue.LoadAs(context.Cache, bpTypeRef, module)) yield return instruction; if (bpTypeRef.IsValueType) + { + if ( bpTypeRef.ResolveCached(context.Cache).FullName == "System.Nullable`1" + && TypeRefComparer.Default.Equals(varValue.VariableType, ((GenericInstanceType)bpTypeRef).GenericArguments[0])) + bpTypeRef = ((GenericInstanceType)bpTypeRef).GenericArguments[0]; yield return Create(Box, module.ImportReference(bpTypeRef)); - + } //endif - if (context.Variables[elementNode].VariableType.FullName == "System.Object") + if (varValue.VariableType.FullName == "System.Object") yield return endif; } diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/SearchHandlerAppearanceTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/SearchHandlerAppearanceTracker.cs index 6b21640b0ca2..52fadb15e8fe 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/SearchHandlerAppearanceTracker.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/SearchHandlerAppearanceTracker.cs @@ -54,6 +54,10 @@ protected virtual void SearchHandlerPropertyChanged(object sender, System.Compon { UpdateTextTransform(); } + else if (e.Is(SearchHandler.PlaceholderProperty)) + { + UpdatePlaceholder(); + } else if (e.Is(SearchHandler.PlaceholderColorProperty)) { UpdatePlaceholderColor(); @@ -82,6 +86,28 @@ protected virtual void SearchHandlerPropertyChanged(object sender, System.Compon { UpdateAutomationId(); } + else if (e.Is(SearchHandler.QueryProperty)) + { + UpdateText(); + } + } + + void UpdateText() + { + int cursorPosition = _editText.SelectionStart; + bool selectionExists = _editText.HasSelection; + + _editText.Text = _searchHandler.Query ?? string.Empty; + + UpdateTextTransform(); + + // If we had a selection, place the cursor at the end of text + // Otherwise try to maintain the cursor at its previous position + int textLength = _editText.Text?.Length ?? 0; + int newPosition = selectionExists ? textLength : Math.Min(cursorPosition, textLength); + + // Prevents the cursor from resetting to position zero when text is set programmatically + _editText.SetSelection(newPosition); } void EditTextFocusChange(object s, AView.FocusChangeEventArgs args) @@ -115,6 +141,11 @@ void UpdateFont() _editText.SetTextSize(ComplexUnitType.Sp, (float)_searchHandler.FontSize); } + void UpdatePlaceholder() + { + _editText.Hint = _searchHandler.Placeholder; + } + void UpdatePlaceholderColor() { _editText.UpdatePlaceholderColor(_searchHandler.PlaceholderColor); diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SearchHandlerAppearanceTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SearchHandlerAppearanceTracker.cs index ecea8c950e70..7d8e11076e63 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SearchHandlerAppearanceTracker.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SearchHandlerAppearanceTracker.cs @@ -115,6 +115,19 @@ void SearchHandlerPropertyChanged(object sender, System.ComponentModel.PropertyC { UpdateSearchBarVerticalTextAlignment(_uiSearchBar.FindDescendantView()); } + else if (e.Is(SearchHandler.QueryProperty)) + { + UpdateText(_uiSearchBar.FindDescendantView()); + } + } + + void UpdateText(UITextField uiTextField) + { + if (uiTextField is null) + return; + + uiTextField.Text = _searchHandler.Query; + UpdateTextTransform(uiTextField); } void GetDefaultSearchBarColors(UISearchBar searchBar) diff --git a/src/Controls/src/Core/Handlers/Items/iOS/CarouselViewController.cs b/src/Controls/src/Core/Handlers/Items/iOS/CarouselViewController.cs index ca07824e4a11..afa9ea62f139 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/CarouselViewController.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/CarouselViewController.cs @@ -32,6 +32,20 @@ public class CarouselViewController : ItemsViewController bool _isRotating; + public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection) + { + if (previousTraitCollection.VerticalSizeClass == TraitCollection.VerticalSizeClass) + { + return; + } + + if (ItemsView?.Loop == false || _carouselViewLoopManager is null) + { + CollectionView.ReloadData(); + InitialPositionSet = false; + } + } + public CarouselViewController(CarouselView itemsView, ItemsViewLayout layout) : base(itemsView, layout) { CollectionView.AllowsSelection = false; diff --git a/src/Controls/src/Core/Handlers/Items/iOS/ReorderableItemsViewController.cs b/src/Controls/src/Core/Handlers/Items/iOS/ReorderableItemsViewController.cs index 8878d36194b6..63dc064fd595 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/ReorderableItemsViewController.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/ReorderableItemsViewController.cs @@ -21,6 +21,11 @@ public ReorderableItemsViewController(TItemsView reorderableItemsView, ItemsView // For some reason it only seemed to work when the CollectionView was inside the Flyout section of a FlyoutPage. // The UILongPressGestureRecognizer is simple enough to set up so let's just add our own. InstallsStandardGestureForInteractiveMovement = false; +#if MACCATALYST + // On Mac Catalyst, the default normal press and drag interactions occur, causing the CanMixGroups = false logic to not work. + // Since all reordering logic is handled exclusively by UILongPressGestureRecognizer, we can set DragInteractionEnabled to false, ensuring that only the long press gesture is used. + CollectionView.DragInteractionEnabled = false; +#endif } public override bool CanMoveItem(UICollectionView collectionView, NSIndexPath indexPath) diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/ReorderableItemsViewController2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/ReorderableItemsViewController2.cs index 12e33e65ef62..2114d4eecc35 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/ReorderableItemsViewController2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/ReorderableItemsViewController2.cs @@ -21,6 +21,11 @@ public ReorderableItemsViewController2(TItemsView reorderableItemsView, UICollec // For some reason it only seemed to work when the CollectionView was inside the Flyout section of a FlyoutPage. // The UILongPressGestureRecognizer is simple enough to set up so let's just add our own. InstallsStandardGestureForInteractiveMovement = false; +#if MACCATALYST + // On Mac Catalyst, the default normal press and drag interactions occur, causing the CanMixGroups = false logic to not work. + // Since all reordering logic is handled exclusively by UILongPressGestureRecognizer, we can set DragInteractionEnabled to false, ensuring that only the long press gesture is used. + CollectionView.DragInteractionEnabled = false; +#endif } public override bool CanMoveItem(UICollectionView collectionView, NSIndexPath indexPath) diff --git a/src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs b/src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs index e6cc1113cb59..cb679d4fc62c 100644 --- a/src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs +++ b/src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs @@ -180,7 +180,11 @@ void UpdateBackButton() // Set this before BackButtonVisible triggers an update to the handler // This way all useful information is present - if (Parent is FlyoutPage flyout && flyout.ShouldShowToolbarButton() && !anyPagesPushed.Value) + if (Parent is FlyoutPage flyout && flyout.ShouldShowToolbarButton() +#if !WINDOWS // TODO NET 10 : Move this logic to ShouldShowToolbarButton + && !anyPagesPushed.Value +#endif + ) _drawerToggleVisible = true; else _drawerToggleVisible = false; diff --git a/src/Controls/src/Core/Platform/Windows/Extensions/ToolbarExtensions.cs b/src/Controls/src/Core/Platform/Windows/Extensions/ToolbarExtensions.cs index 1fe23332fa72..7348dbd80992 100644 --- a/src/Controls/src/Core/Platform/Windows/Extensions/ToolbarExtensions.cs +++ b/src/Controls/src/Core/Platform/Windows/Extensions/ToolbarExtensions.cs @@ -87,9 +87,9 @@ public static void UpdateToolbarDynamicOverflowEnabled(this MauiToolbar platform private static void UpdateBackButtonVisibility(MauiToolbar platformToolbar, Toolbar toolbar) { - platformToolbar.IsBackButtonVisible = - toolbar.BackButtonVisible - ? NavigationViewBackButtonVisible.Visible + platformToolbar.IsBackButtonVisible = + toolbar.BackButtonVisible + ? NavigationViewBackButtonVisible.Visible : NavigationViewBackButtonVisible.Collapsed; } } diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index e33dcb76f5a2..9089f402c400 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -71,6 +71,7 @@ Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget(T! target) -> ~Microsoft.Maui.Controls.Xaml.IXamlTypeResolver.Resolve(string qualifiedTypeName, System.IServiceProvider serviceProvider = null, bool expandToExtension = true) -> System.Type ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.RequireServiceAttribute(System.Type[] serviceTypes) -> void ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] +~override Microsoft.Maui.Controls.Handlers.Items.CarouselViewController.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void ~override Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2.CreateDelegator() -> UIKit.UICollectionViewDelegateFlowLayout ~override Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2.CreateItemsViewSource() -> Microsoft.Maui.Controls.Handlers.Items.IItemsViewSource ~override Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2.DetermineCellReuseId(Foundation.NSIndexPath indexPath) -> string diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 6950a4815676..44b640bc304f 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -71,6 +71,7 @@ Microsoft.Maui.Controls.HybridWebView.SetInvokeJavaScriptTarget(T! target) -> ~Microsoft.Maui.Controls.Xaml.IXamlTypeResolver.Resolve(string qualifiedTypeName, System.IServiceProvider serviceProvider = null, bool expandToExtension = true) -> System.Type ~Microsoft.Maui.Controls.WebViewProcessTerminatedEventArgs.PlatformArgs.get -> Microsoft.Maui.Controls.PlatformWebViewProcessTerminatedEventArgs ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.RequireServiceAttribute(System.Type[] serviceTypes) -> void +~override Microsoft.Maui.Controls.Handlers.Items.CarouselViewController.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~override Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2.CreateDelegator() -> UIKit.UICollectionViewDelegateFlowLayout ~override Microsoft.Maui.Controls.Handlers.Items2.CarouselViewController2.CreateItemsViewSource() -> Microsoft.Maui.Controls.Handlers.Items.IItemsViewSource diff --git a/src/Controls/src/Core/ShellToolbar.cs b/src/Controls/src/Core/ShellToolbar.cs index 2b3ebfb63c6a..beb4e6374a62 100644 --- a/src/Controls/src/Core/ShellToolbar.cs +++ b/src/Controls/src/Core/ShellToolbar.cs @@ -91,7 +91,11 @@ internal void ApplyChanges() } var flyoutBehavior = (_shell as IFlyoutView).FlyoutBehavior; +#if WINDOWS + _drawerToggleVisible = flyoutBehavior is FlyoutBehavior.Flyout; +#else _drawerToggleVisible = stack.Count <= 1 && flyoutBehavior is FlyoutBehavior.Flyout; +#endif BackButtonVisible = backButtonVisible && stack.Count > 1; BackButtonEnabled = _backButtonBehavior?.IsEnabled ?? true; ToolbarItems = _toolbarTracker.ToolbarItems; diff --git a/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.Android.cs index 0d84d49eb8e3..1add2de04889 100644 --- a/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.Android.cs @@ -2,11 +2,13 @@ using System.ComponentModel; using System.Threading.Tasks; using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Handlers; using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; using Xunit; + namespace Microsoft.Maui.DeviceTests { public partial class BoxViewTests @@ -96,6 +98,25 @@ public async Task RotationConsistent() var platformRotation = await InvokeOnMainThreadAsync(() => platformBoxView.Rotation); Assert.Equal(expected, platformRotation); } + + [Fact] + [Description("The IsEnabled property of a BoxView should match with native IsEnabled")] + public async Task VerifyBoxViewIsEnabledProperty() + { + var boxView = new BoxView + { + IsEnabled = false + }; + var expectedValue = boxView.IsEnabled; + + var handler = await CreateHandlerAsync(boxView); + var nativeView = GetNativeBoxView(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.Enabled; + Assert.Equal(expectedValue, isEnabled); + }); + } Task GetPlatformIsVisible(ShapeViewHandler boxViewViewHandler) { diff --git a/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.Windows.cs index 88e0735833b1..da087c8ae9d7 100644 --- a/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.Windows.cs @@ -3,6 +3,10 @@ using Microsoft.Maui.Graphics.Platform; using Microsoft.Maui.Graphics.Win2D; using Microsoft.Maui.Handlers; +using Xunit; +using System.ComponentModel; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Handlers; namespace Microsoft.Maui.DeviceTests { @@ -20,6 +24,25 @@ Task GetPlatformOpacity(ShapeViewHandler handler) }); } + [Fact] + [Description("The IsEnabled property of a BoxView should match with native IsEnabled")] + public async Task BoxViewIsEnabled() + { + var boxView = new BoxView + { + IsEnabled = false + }; + var expectedValue = boxView.IsEnabled; + + var handler = await CreateHandlerAsync(boxView); + var nativeView = GetNativeBoxView(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.IsEnabled; + Assert.Equal(expectedValue, isEnabled); + }); + } + Task GetPlatformIsVisible(ShapeViewHandler boxViewHandler) { return InvokeOnMainThreadAsync(() => diff --git a/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.cs b/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.cs index cf574048faf1..5f271391ad18 100644 --- a/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/BoxView/BoxViewTests.cs @@ -4,9 +4,9 @@ using Microsoft.Maui.Controls; using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Controls.Handlers; using Microsoft.Maui.Hosting; using Xunit; -using Microsoft.Maui.Controls.Handlers; namespace Microsoft.Maui.DeviceTests { @@ -47,6 +47,21 @@ public async Task BoxViewBackgroundColorConsistent() await ValidateHasColor(boxView, expected, typeof(ShapeViewHandler)); } + [Fact] + [Description("The Background of a BoxView should match with native Background")] + public async Task BoxViewBackgroundConsistent() + { + var boxView = new BoxView + { + HeightRequest = 100, + WidthRequest = 200, + Background = Brush.Red + }; + var expected = (boxView.Background as SolidColorBrush)?.Color; + + await ValidateHasColor(boxView, expected, typeof(BoxViewHandler)); + } + [Fact] [Description("The Opacity property of a BoxView should match with native Opacity")] public async Task VerifyBoxViewOpacityProperty() diff --git a/src/Controls/tests/DeviceTests/Elements/Button/ButtonTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Button/ButtonTests.iOS.cs index 0b1f27eef54b..b16315e26605 100644 --- a/src/Controls/tests/DeviceTests/Elements/Button/ButtonTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Button/ButtonTests.iOS.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.ComponentModel; using System.Threading.Tasks; using Microsoft.Maui.Controls; using Microsoft.Maui.Graphics; @@ -87,5 +88,24 @@ await InvokeOnMainThreadAsync(async () => Assert.True(button.Width < gridWidth, $"Button shouldn't occupy entire layout width. Expected: {gridWidth}<, was {button.Width}"); Assert.True(button.Height < gridHeight, $"Button shouldn't occupy entire layout height. Expected: {gridHeight}<, was {button.Height}"); } + + [Fact] + [Description("The CornerRadius of a Button should match with native CornerRadius")] + public async Task ButtonCornerRadius() + { + var button = new Button + { + CornerRadius = 15, + }; + var expectedValue = button.CornerRadius; + + var handler = await CreateHandlerAsync(button); + var nativeView = GetPlatformButton(handler); + await InvokeOnMainThreadAsync(() => + { + var platformCornerRadius = nativeView.Layer.CornerRadius; + Assert.Equal(expectedValue, platformCornerRadius); + }); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.Android.cs index 7403625f6268..e31c3eab2fa5 100644 --- a/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.Android.cs @@ -98,6 +98,22 @@ public async Task RotationConsistent() Assert.Equal(expected, platformRotation); } + [Fact("The IsEnabled of a CheckBox should match with native IsEnabled")] + public async Task CheckBoxIsEnabled() + { + var checkBox = new CheckBox(); + checkBox.IsEnabled = false; + var expectedValue = checkBox.IsEnabled; + + var handler = await CreateHandlerAsync(checkBox); + var nativeView = GetNativeCheckBox(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.Enabled; + Assert.Equal(expectedValue, isEnabled); + }); + } + Task GetPlatformIsVisible(CheckBoxHandler checkBoxHandler) { return InvokeOnMainThreadAsync(() => diff --git a/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.Windows.cs index b37682a406a4..da7a3b43ae4f 100644 --- a/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.Windows.cs @@ -3,6 +3,7 @@ using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; using Microsoft.UI.Xaml.Controls; +using Xunit; namespace Microsoft.Maui.DeviceTests { @@ -19,6 +20,22 @@ Task GetPlatformOpacity(CheckBoxHandler checkBoxHandler) return (float)nativeView.Opacity; }); } + + [Fact("The IsEnabled of a CheckBox should match with native IsEnabled")] + public async Task CheckBoxIsEnabled() + { + var checkBox = new Microsoft.Maui.Controls.CheckBox(); + checkBox.IsEnabled = false; + var expectedValue = checkBox.IsEnabled; + + var handler = await CreateHandlerAsync(checkBox); + var nativeView = GetNativeCheckBox(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.IsEnabled; + Assert.Equal(expectedValue, isEnabled); + }); + } Task GetPlatformIsVisible(CheckBoxHandler checkBoxHandler) { diff --git a/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.iOS.cs index c907dcf606ff..c1cbbfe2a5a5 100644 --- a/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/CheckBox/CheckBoxTests.iOS.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; using Xunit; @@ -21,6 +22,22 @@ Task GetPlatformOpacity(CheckBoxHandler checkBoxHandler) return (float)nativeView.Alpha; }); } + + [Fact("The IsEnabled of a CheckBox should match with native IsEnabled")] + public async Task CheckBoxIsEnabled() + { + var checkBox = new CheckBox(); + checkBox.IsEnabled = false; + var expectedValue = checkBox.IsEnabled; + + var handler = await CreateHandlerAsync(checkBox); + var nativeView = GetNativeCheckBox(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.Enabled; + Assert.Equal(expectedValue, isEnabled); + }); + } Task GetPlatformIsVisible(CheckBoxHandler checkBoxHandler) { diff --git a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Android.cs index 00b457c78179..0df2cb8c285a 100644 --- a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Android.cs @@ -150,5 +150,25 @@ public async Task RotationConsistent() var platformRotation = await InvokeOnMainThreadAsync(() => PlatformEditor.Rotation); Assert.Equal(expected, platformRotation); } + + [Fact] + [Description("The IsEnabled property of a Editor should match with native IsEnabled")] + public async Task VerifyEditorIsEnabledProperty() + { + var editor = new Editor + { + IsEnabled = false + }; + var expectedValue = editor.IsEnabled; + + var handler = await CreateHandlerAsync(editor); + var nativeView = GetPlatformControl(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.Enabled; + + Assert.Equal(expectedValue, isEnabled); + }); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Windows.cs index e9bb56dc2acf..6dc9ab2f9d7c 100644 --- a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Windows.cs @@ -1,7 +1,14 @@ #nullable enable +using Xunit; +using System; +using System.ComponentModel; using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Dispatching; using Microsoft.Maui.Handlers; using Microsoft.UI.Xaml.Controls; +using WFlowDirection = Microsoft.UI.Xaml.FlowDirection; +using WTextAlignment = Microsoft.UI.Xaml.TextAlignment; namespace Microsoft.Maui.DeviceTests { @@ -41,5 +48,35 @@ Task GetPlatformIsVisible(EditorHandler editorHandler) return nativeView.Visibility == Microsoft.UI.Xaml.Visibility.Visible; }); } + //src/Compatibility/Core/tests/WinUI/FlowDirectionTests.cs + [Theory] + [InlineData(true, FlowDirection.RightToLeft, WTextAlignment.Left, WFlowDirection.RightToLeft)] + [InlineData(true, FlowDirection.LeftToRight, WTextAlignment.Left, WFlowDirection.LeftToRight)] + [InlineData(false, FlowDirection.LeftToRight, WTextAlignment.Left, WFlowDirection.LeftToRight)] + [Description("The Editor's text alignment and flow direction should match the expected values when FlowDirection is applied explicitly or implicitly.")] + public async Task EditorAlignmentMatchesFlowDirection(bool isExplicit, FlowDirection flowDirection, WTextAlignment expectedAlignment, WFlowDirection expectedFlowDirection) + { + var editor = new Editor { Text = " تسجيل الدخول" }; + var contentPage = new ContentPage { Title = "Flow Direction", Content = editor }; + + if (isExplicit) + { + editor.FlowDirection = flowDirection; + } + else + { + contentPage.FlowDirection = flowDirection; + } + + var handler = await CreateHandlerAsync(editor); + var (nativeAlignment, nativeFlowDirection) = await contentPage.Dispatcher.DispatchAsync(() => + { + var textField = GetPlatformControl(handler); + return (textField.TextAlignment, textField.FlowDirection); + }); + + Assert.Equal(expectedAlignment, nativeAlignment); + Assert.Equal(expectedFlowDirection, nativeFlowDirection); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs index 6be8fb8fb7b4..6f89153da58c 100644 --- a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs @@ -1,11 +1,14 @@ -using System.Linq; +using UIKit; +using Xunit; +using System.Linq; using System.Threading.Tasks; using Microsoft.Maui.Controls; +using Microsoft.Maui.Dispatching; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform; -using Xunit; using static Microsoft.Maui.DeviceTests.AssertHelpers; +using System.ComponentModel; namespace Microsoft.Maui.DeviceTests { @@ -48,7 +51,7 @@ Task GetPlatformOpacity(EditorHandler editorHandler) return InvokeOnMainThreadAsync(() => { var nativeView = GetPlatformControl(editorHandler); - return (float)nativeView.Alpha; + return (float)nativeView.Alpha; }); } @@ -103,5 +106,35 @@ await CreateHandlerAndAddToWindow(contentPage, async () => }); } } + + //src/Compatibility/Core/tests/iOS/FlowDirectionTests.cs + [Theory] + [InlineData(true, FlowDirection.LeftToRight, UITextAlignment.Left)] + [InlineData(true, FlowDirection.RightToLeft, UITextAlignment.Right)] + [InlineData(false, FlowDirection.LeftToRight, UITextAlignment.Left)] + [Description("The Editor's text alignment should match the expected alignment when FlowDirection is applied explicitly or implicitly")] + public async Task EditorAlignmentMatchesFlowDirection(bool isExplicit, FlowDirection flowDirection, UITextAlignment expectedAlignment) + { + var editor = new Editor { Text = "Checking flow direction" }; + var contentPage = new ContentPage { Title = "Flow Direction", Content = editor }; + + if (isExplicit) + { + editor.FlowDirection = flowDirection; + } + else + { + contentPage.FlowDirection = flowDirection; + } + + var handler = await CreateHandlerAsync(editor); + var nativeAlignment = await contentPage.Dispatcher.DispatchAsync(() => + { + var textField = GetPlatformControl(handler); + return textField.TextAlignment; + }); + + Assert.Equal(expectedAlignment, nativeAlignment); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Windows.cs index f76b6526f23b..a41c816bea69 100644 --- a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Windows.cs @@ -1,7 +1,12 @@ #nullable enable +using Xunit; +using System.ComponentModel; using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Dispatching; using Microsoft.Maui.Handlers; using Microsoft.UI.Xaml.Controls; +using WTextAlignment = Microsoft.UI.Xaml.TextAlignment; namespace Microsoft.Maui.DeviceTests { @@ -41,5 +46,35 @@ Task GetPlatformIsVisible(EntryHandler entryHandler) return nativeView.Visibility == Microsoft.UI.Xaml.Visibility.Visible; }); } + //src/Compatibility/Core/tests/WinUI/FlowDirectionTests.cs + [Theory] + [InlineData(true, FlowDirection.LeftToRight, WTextAlignment.Left)] + [InlineData(true, FlowDirection.RightToLeft, WTextAlignment.Left)] + [InlineData(false, FlowDirection.LeftToRight, WTextAlignment.Left)] + [InlineData(false, FlowDirection.RightToLeft, WTextAlignment.Left)] + [Description("The Entry's text alignment should match the expected alignment when FlowDirection is applied explicitly or implicitly.")] + public async Task EntryAlignmentMatchesFlowDirection(bool isExplicit, FlowDirection flowDirection, WTextAlignment expectedAlignment) + { + var entry = new Entry { Text = "Checking flow direction", HorizontalTextAlignment = TextAlignment.Start }; + var contentPage = new ContentPage { Title = "Flow Direction", Content = entry }; + + if (isExplicit) + { + entry.FlowDirection = flowDirection; + } + else + { + contentPage.FlowDirection = flowDirection; + } + + var handler = await CreateHandlerAsync(entry); + var nativeAlignment = await contentPage.Dispatcher.DispatchAsync(() => + { + var textField = GetPlatformControl(handler); + return textField.TextAlignment; + }); + + Assert.Equal(expectedAlignment, nativeAlignment); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs index 4a2e9e48c957..282b8a5647ef 100644 --- a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs @@ -1,13 +1,15 @@ -using System.Linq; +using UIKit; +using Xunit; +using System.ComponentModel; +using System.Linq; using System.Threading.Tasks; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Handlers.Compatibility; using Microsoft.Maui.DeviceTests.TestCases; +using Microsoft.Maui.Dispatching; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform; -using UIKit; -using Xunit; using static Microsoft.Maui.DeviceTests.AssertHelpers; namespace Microsoft.Maui.DeviceTests @@ -44,13 +46,13 @@ static int GetPlatformSelectionLength(EntryHandler entryHandler) return -1; } - + Task GetPlatformOpacity(EntryHandler entryHandler) { return InvokeOnMainThreadAsync(() => { var nativeView = GetPlatformControl(entryHandler); - return (float)nativeView.Alpha; + return (float)nativeView.Alpha; }); } @@ -332,5 +334,35 @@ await CreateHandlerAndAddToWindow(contentPage, async () => }); } } + + //src/Compatibility/Core/tests/iOS/FlowDirectionTests.cs + [Theory] + [InlineData(true, FlowDirection.LeftToRight, UITextAlignment.Left)] + [InlineData(true, FlowDirection.RightToLeft, UITextAlignment.Right)] + [InlineData(false, FlowDirection.LeftToRight, UITextAlignment.Left)] + [Description("The Entry's text alignment should match the expected alignment when FlowDirection is applied explicitly or implicitly")] + public async Task EntryAlignmentMatchesFlowDirection(bool isExplicit, FlowDirection flowDirection, UITextAlignment expectedAlignment) + { + var entry = new Entry { Text = "Checking flow direction", HorizontalTextAlignment = TextAlignment.Start }; + var contentPage = new ContentPage { Title = "Flow Direction", Content = entry }; + + if (isExplicit) + { + entry.FlowDirection = flowDirection; + } + else + { + contentPage.FlowDirection = flowDirection; + } + + var handler = await CreateHandlerAsync(entry); + var nativeAlignment = await contentPage.Dispatcher.DispatchAsync(() => + { + var textField = GetPlatformControl(handler); + return textField.TextAlignment; + }); + + Assert.Equal(expectedAlignment, nativeAlignment); + } } } \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/Label/LabelTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/Label/LabelTests.Android.cs index 03e1aadb80aa..e584366a7174 100644 --- a/src/Controls/tests/DeviceTests/Elements/Label/LabelTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/Label/LabelTests.Android.cs @@ -162,6 +162,26 @@ public async Task RotationConsistent() var platformRotation = await InvokeOnMainThreadAsync(() => platformLabel.Rotation); Assert.Equal(expected, platformRotation); } + + [Fact] + [Description("The IsEnabled property of a Label should match with native IsEnabled")] + public async Task VerifyLabelIsEnabledProperty() + { + var label = new Label + { + IsEnabled = false + }; + var expectedValue = label.IsEnabled; + + var handler = await CreateHandlerAsync(label); + var nativeView = GetPlatformLabel(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.Enabled; + Assert.Equal(expectedValue, isEnabled); + }); + } + TextView GetPlatformLabel(LabelHandler labelHandler) => labelHandler.PlatformView; diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.iOS.cs index eca6b8284954..89cd1b7ed98b 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.iOS.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -7,6 +8,7 @@ using Microsoft.Maui.Controls.Handlers.Compatibility; using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Dispatching; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform; @@ -65,5 +67,29 @@ public async Task TranslucentNavigationBar(bool enabled) var translucent = await GetValueAsync(navPage, (handler) => (handler.ViewController as UINavigationController).NavigationBar.Translucent); Assert.Equal(enabled, translucent); } + + //src/Compatibility/Core/tests/iOS/NavigationTests.cs + [Fact] + [Description("Multiple calls to NavigationRenderer.Dispose shouldn't crash")] + public async Task NavigationRendererDoubleDisposal() + { + SetupBuilder(); + + var root = new ContentPage() + { + Title = "root", + Content = new Label { Text = "Hello" } + }; + + await root.Dispatcher.DispatchAsync(() => + { + var navPage = new NavigationPage(root); + var handler = CreateHandler(navPage); + + // Calling Dispose more than once should be fine + (handler as NavigationRenderer).Dispose(); + (handler as NavigationRenderer).Dispose(); + }); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Page/PageTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/Page/PageTests.Android.cs new file mode 100644 index 000000000000..8ee2bf4a9524 --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/Page/PageTests.Android.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Dispatching; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; +using Xunit; +using AView = Android.Views.View; +using AndroidX.Fragment.App; + +namespace Microsoft.Maui.DeviceTests +{ + public partial class PageTests : ControlsHandlerTestBase + { + //src/Compatibility/Core/tests/Android/EmbeddingTests.cs + [Fact(DisplayName = "Can Create Platform View From ContentPage")] + public async Task CanCreatePlatformViewFromContentPage() + { + + var contentPage = new ContentPage { Title = "Embedded Page" }; + var handler = CreateHandler(contentPage); + var mauiContext = handler.MauiContext; + + await contentPage.Dispatcher.DispatchAsync(() => + { + + AView platformView = contentPage.ToPlatform(mauiContext); + if (platformView is FragmentContainerView containerView) + { + var activity = mauiContext.Context as AndroidX.Fragment.App.FragmentActivity; + var fragmentManager = activity.SupportFragmentManager; + var fragment = fragmentManager.FindFragmentById(containerView.Id); + Assert.NotNull(fragment); + } + }); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/Page/PageTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Page/PageTests.iOS.cs index 1c71d20140cb..045e31fb2c72 100644 --- a/src/Controls/tests/DeviceTests/Elements/Page/PageTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Page/PageTests.iOS.cs @@ -3,9 +3,12 @@ using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.PlatformConfiguration; using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; +using Microsoft.Maui.Dispatching; +using Microsoft.Maui.Platform; using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; +using UIKit; using Xunit; namespace Microsoft.Maui.DeviceTests @@ -40,5 +43,23 @@ await CreateHandlerAndAddToWindow(shell, (handler) => } }); } + + //src/Compatibility/Core/tests/iOS/EmbeddingTests.cs + [Fact(DisplayName = "Can Create Platform View From ContentPage")] + public async Task CanCreateViewControllerFromContentPage() + { + var contentPage = new ContentPage { Title = "Embedded Page" }; + await contentPage.Dispatcher.DispatchAsync(async () => + { + var handler = CreateHandler(contentPage); + var mauiContext = handler.MauiContext; + + await contentPage.Dispatcher.DispatchAsync(() => + { + UIViewController viewController = contentPage.ToUIViewController(mauiContext); + Assert.NotNull(viewController); + }); + }); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.Windows.cs index 30853568b48e..84e80f647bcd 100644 --- a/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/RadioButton/RadioButtonTests.Windows.cs @@ -1,8 +1,8 @@ using System.ComponentModel; +using Xunit; using System.Threading.Tasks; using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; -using Xunit; namespace Microsoft.Maui.DeviceTests { @@ -27,14 +27,56 @@ public async Task VerifyRadioButtonOpacityProperty() var handler = await CreateHandlerAsync(radioButton); var nativeView = GetNativeRadioButton(handler); await InvokeOnMainThreadAsync(() => - { + { var nativeOpacityValue = (float)nativeView.Opacity; Assert.Equal(expectedValue, nativeOpacityValue); }); } [Fact] - [Description("The IsVisible property of a RadioButton should match with native IsVisible")] + [Description("The CornerRadius of a RadioButton should match with native CornerRadius")] + public async Task RadioButtonCornerRadius() + { + var radioButton = new RadioButton(); + radioButton.CornerRadius = 15; + var expectedValue = radioButton.CornerRadius; + + var handler = await CreateHandlerAsync(radioButton); + var nativeView = GetNativeRadioButton(handler); + await InvokeOnMainThreadAsync(() => + { + var cornerRadiusTopLeft = (float)nativeView.CornerRadius.TopLeft; + var cornerRadiusTopRight = (float)nativeView.CornerRadius.TopRight; + var cornerRadiusBottomLeft = (float)nativeView.CornerRadius.BottomLeft; + var cornerRadiusBottomRight = (float)nativeView.CornerRadius.BottomRight; + Assert.Equal(expectedValue, cornerRadiusTopLeft); + Assert.Equal(expectedValue, cornerRadiusTopRight); + Assert.Equal(expectedValue, cornerRadiusBottomLeft); + Assert.Equal(expectedValue, cornerRadiusBottomRight); + }); + } + + [Fact] + [Description("The IsEnabled of a RadioButton should match with native IsEnabled")] + public async Task VerifyRadioButtonIsEnabledProperty() + { + var radioButton = new RadioButton + { + IsEnabled = false + }; + var expectedValue = radioButton.IsEnabled; + + var handler = await CreateHandlerAsync(radioButton); + var nativeView = GetNativeRadioButton(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.IsEnabled; + Assert.Equal(expectedValue, isEnabled); + }); + } + + [Fact] + [Description("The IsVisible property of a RadioButton should match with native IsVisible")] public async Task VerifyRadioButtonIsVisibleProperty() { var radioButton = new RadioButton(); @@ -47,7 +89,7 @@ await InvokeOnMainThreadAsync(() => { var isVisible = nativeView.Visibility == Microsoft.UI.Xaml.Visibility.Visible; Assert.Equal(expectedValue, isVisible); - }); + }); } } -} +} \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Android.cs index 335b55cebb19..8d573064ac4b 100644 --- a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Android.cs @@ -117,6 +117,26 @@ public async Task RotationConsistent() Assert.Equal(expected, platformRotation); } + [Fact] + [Description("The IsEnabled of a SearchBar should match with native IsEnabled")] + public async Task VerifySearchBarIsEnabledProperty() + { + var searchBar = new SearchBar + { + IsEnabled = false + }; + var expectedValue = searchBar.IsEnabled; + + var handler = await CreateHandlerAsync(searchBar); + var nativeView = GetPlatformControl(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.Enabled; + + Assert.Equal(expectedValue, isEnabled); + }); + } + Task GetPlatformIsVisible(SearchBarHandler searchBarHandler) { return InvokeOnMainThreadAsync(() => diff --git a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Windows.cs index 30a2ed3c6d6a..3fbf5f0cdfdf 100644 --- a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Windows.cs @@ -1,8 +1,11 @@ #nullable enable +using System.ComponentModel; using System.Threading.Tasks; +using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; using Microsoft.UI.Xaml.Controls; +using Xunit; namespace Microsoft.Maui.DeviceTests { @@ -52,6 +55,25 @@ Task GetPlatformOpacity(SearchBarHandler searchBarHandler) return (float)nativeView.Opacity; }); } + + [Fact] + [Description("The IsEnabled of a SearchBar should match with native IsEnabled")] + public async Task VerifySearchBarIsEnabledProperty() + { + var searchBar = new SearchBar + { + IsEnabled = false + }; + var expectedValue = searchBar.IsEnabled; + + var handler = await CreateHandlerAsync(searchBar); + var nativeView = GetPlatformControl(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.IsEnabled; + Assert.Equal(expectedValue, isEnabled); + }); + } Task GetPlatformIsVisible(SearchBarHandler searchBarHandler) { diff --git a/src/Controls/tests/DeviceTests/Elements/SwipeView/SwipeViewTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/SwipeView/SwipeViewTests.Android.cs index 37ed82a30149..da925a7dfb15 100644 --- a/src/Controls/tests/DeviceTests/Elements/SwipeView/SwipeViewTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/SwipeView/SwipeViewTests.Android.cs @@ -189,5 +189,24 @@ await InvokeOnMainThreadAsync(() => Assert.Equal(expectedValue, isVisible); }); } + + [Fact] + [Description("The IsEnabled of a SwipeView should match with native IsEnabled")] + public async Task VerifySwipeViewIsEnabledProperty() + { + var swipeView = new SwipeView + { + IsEnabled = false + }; + var expectedValue = swipeView.IsEnabled; + + var handler = await CreateHandlerAsync(swipeView); + var nativeView = GetPlatformControl(handler); + await InvokeOnMainThreadAsync(() => + { + var isEnabled = nativeView.Enabled; + Assert.Equal(expectedValue, isEnabled); + }); + } } } \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CarouselViewItemShouldScaleProperly.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CarouselViewItemShouldScaleProperly.png new file mode 100644 index 000000000000..daf55575bde6 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/CarouselViewItemShouldScaleProperly.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ReorderBetweenGroupsShouldNotOccurWhenCanMixGroupsIsFalse.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ReorderBetweenGroupsShouldNotOccurWhenCanMixGroupsIsFalse.png new file mode 100644 index 000000000000..8825c19a19b9 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ReorderBetweenGroupsShouldNotOccurWhenCanMixGroupsIsFalse.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySearchHandlerPlaceholderText.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySearchHandlerPlaceholderText.png new file mode 100644 index 000000000000..3065fce58707 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySearchHandlerPlaceholderText.png differ diff --git a/src/Controls/tests/TestCases.HostApp/CoreViews/CorePageView.cs b/src/Controls/tests/TestCases.HostApp/CoreViews/CorePageView.cs index 9f2fc6c54f1a..61d2bfa29c0c 100644 --- a/src/Controls/tests/TestCases.HostApp/CoreViews/CorePageView.cs +++ b/src/Controls/tests/TestCases.HostApp/CoreViews/CorePageView.cs @@ -77,6 +77,7 @@ public override string ToString() new GalleryPageFactory(() => new TimePickerCoreGalleryPage(), "Time Picker Gallery"), new GalleryPageFactory(() => new WebViewCoreGalleryPage(), "WebView Gallery"), new GalleryPageFactory(() => new SliderControlPage(), "Slider Feature Matrix"), + new GalleryPageFactory(() => new HeaderFooterMainPage(), "CollectionView Feature Matrix"), }; public CorePageView(Page rootPage) diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewControlPage.xaml b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewControlPage.xaml new file mode 100644 index 000000000000..5c49a597e156 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewControlPage.xaml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewControlPage.xaml.cs b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewControlPage.xaml.cs new file mode 100644 index 000000000000..27e08b4408c5 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewControlPage.xaml.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Maui.Controls; +using System.Collections.ObjectModel; + +namespace Maui.Controls.Sample +{ + public partial class CollectionViewControlPage : ContentPage + { + private CollectionViewViewModel _viewModel; + + public CollectionViewControlPage() + { + InitializeComponent(); + _viewModel = new CollectionViewViewModel(); + _viewModel.ItemsSourceType = ItemsSourceType.ObservableCollection5T; + BindingContext = _viewModel; + } + + private async void NavigateToOptionsPage_Clicked(object sender, EventArgs e) + { + BindingContext = _viewModel = new CollectionViewViewModel(); + _viewModel.ItemsSourceType = ItemsSourceType.ObservableCollection5T; + await Navigation.PushAsync(new CollectionViewOptionsPage(_viewModel)); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewOptionsPage.xaml b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewOptionsPage.xaml new file mode 100644 index 000000000000..7460999f5a1e --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewOptionsPage.xaml @@ -0,0 +1,275 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewOptionsPage.xaml.cs b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewOptionsPage.xaml.cs new file mode 100644 index 000000000000..ccf26c04cc54 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewOptionsPage.xaml.cs @@ -0,0 +1,278 @@ +using System; +using Microsoft.Maui.Controls; +using System.Collections.ObjectModel; +using Maui.Controls.Sample.CollectionViewGalleries; + +namespace Maui.Controls.Sample +{ + public partial class CollectionViewOptionsPage : ContentPage + { + private CollectionViewViewModel _viewModel; + + public CollectionViewOptionsPage(CollectionViewViewModel viewModel) + { + InitializeComponent(); + _viewModel = viewModel; + BindingContext = _viewModel; + } + + private void ApplyButton_Clicked(object sender, EventArgs e) + { + Navigation.PopAsync(); + } + + private void OnEmptyViewChanged(object sender, CheckedChangedEventArgs e) + { + if (EmptyViewNone.IsChecked) + { + _viewModel.EmptyView = null; + } + else if (EmptyViewString.IsChecked) + { + _viewModel.EmptyView = "No Items Available(String)"; + } + } + + private void OnHeaderChanged(object sender, CheckedChangedEventArgs e) + { + if (HeaderNone.IsChecked) + { + _viewModel.Header = null; + } + else if (HeaderString.IsChecked) + { + _viewModel.Header = "CollectionView Header(String)"; + } + else if (HeaderGrid.IsChecked) + { + Grid grid = new Grid + { + BackgroundColor = Colors.LightGray, + Padding = new Thickness(10) + }; + grid.Children.Add(new Label + { + Text = "CollectionView Header(Grid View)", + FontSize = 18, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + TextColor = Colors.Blue, + AutomationId = "HeaderViewLabel" + }); + _viewModel.Header = grid; + } + } + + private void OnFooterChanged(object sender, CheckedChangedEventArgs e) + { + if (FooterNone.IsChecked) + { + _viewModel.Footer = null; + } + else if (FooterString.IsChecked) + { + _viewModel.Footer = "CollectionView Footer(String)"; + } + else if (FooterGrid.IsChecked) + { + Grid grid = new Grid + { + BackgroundColor = Colors.LightGray, + Padding = new Thickness(10) + }; + grid.Children.Add(new Label + { + Text = "CollectionView Footer(Grid View)", + FontSize = 18, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + TextColor = Colors.Red, + AutomationId = "FooterViewLabel" + }); + _viewModel.Footer = grid; + } + } + + private void OnHeaderTemplateChanged(object sender, CheckedChangedEventArgs e) + { + if (HeaderTemplateNone.IsChecked) + { + _viewModel.HeaderTemplate = null; + } + else if (HeaderTemplateGrid.IsChecked) + { + _viewModel.HeaderTemplate = new DataTemplate(() => + { + Grid grid = new Grid + { + BackgroundColor = Colors.LightGray, + Padding = new Thickness(10) + }; + grid.Children.Add(new Label + { + Text = "Header Template(Grid View)", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + TextColor = Colors.Blue, + AutomationId = "HeaderTemplateLabel" + }); + return grid; + }); + } + } + + private void OnFooterTemplateChanged(object sender, CheckedChangedEventArgs e) + { + if (FooterTemplateNone.IsChecked) + { + _viewModel.FooterTemplate = null; + } + else if (FooterTemplateGrid.IsChecked) + { + _viewModel.FooterTemplate = new DataTemplate(() => + { + Grid grid = new Grid + { + BackgroundColor = Colors.LightGray, + Padding = new Thickness(10) + }; + grid.Children.Add(new Label + { + Text = "Footer Template(Grid View)", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + TextColor = Colors.Green, + AutomationId = "FooterTemplateLabel" + }); + return grid; + }); + } + } + + private void OnGroupHeaderTemplateChanged(object sender, CheckedChangedEventArgs e) + { + if (GroupHeaderTemplateNone.IsChecked) + { + _viewModel.GroupHeaderTemplate = null; + } + else if (GroupHeaderTemplateGrid.IsChecked) + { + _viewModel.GroupHeaderTemplate = new DataTemplate(() => + { + Grid grid = new Grid + { + BackgroundColor = Colors.LightGray, + Padding = new Thickness(10) + }; + grid.Children.Add(new Label + { + Text = "Group Header Template(Grid View)", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + TextColor = Colors.Green, + AutomationId = "GroupHeaderTemplateLabel" + }); + return grid; + }); + } + } + private void OnGroupFooterTemplateChanged(object sender, CheckedChangedEventArgs e) + { + if (GroupFooterTemplateNone.IsChecked) + { + _viewModel.GroupFooterTemplate = null; + } + else if (GroupFooterTemplateGrid.IsChecked) + { + _viewModel.GroupFooterTemplate = new DataTemplate(() => + { + Grid grid = new Grid + { + BackgroundColor = Colors.LightGray, + Padding = new Thickness(10) + }; + grid.Children.Add(new Label + { + Text = "Group Footer Template(Grid View)", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + TextColor = Colors.Red, + AutomationId = "GroupFooterTemplateLabel" + }); + return grid; + }); + } + } + private void OnIsGroupedChanged(object sender, CheckedChangedEventArgs e) + { + if (IsGroupedFalse.IsChecked) + { + _viewModel.IsGrouped = false; + } + else if (IsGroupedTrue.IsChecked) + { + _viewModel.IsGrouped = true; + } + } + + private void OnItemTemplateChanged(object sender, CheckedChangedEventArgs e) + { + if (ItemTemplateNone.IsChecked) + { + _viewModel.ItemTemplate = null; + } + else if (ItemTemplateBasic.IsChecked) + { + _viewModel.ItemTemplate = new DataTemplate(() => + { + var label = new Label(); + label.SetBinding(Label.TextProperty, new Binding("Caption")); + + return label; + }); + } + } + + private void OnItemsLayoutChanged(object sender, CheckedChangedEventArgs e) + { + if (ItemsLayoutVerticalList.IsChecked) + { + _viewModel.ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical); + } + else if (ItemsLayoutHorizontalList.IsChecked) + { + _viewModel.ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Horizontal); + } + else if (ItemsLayoutVerticalGrid.IsChecked) + { + _viewModel.ItemsLayout = new GridItemsLayout(2, ItemsLayoutOrientation.Vertical); // 2 columns + } + else if (ItemsLayoutHorizontalGrid.IsChecked) + { + _viewModel.ItemsLayout = new GridItemsLayout(2, ItemsLayoutOrientation.Horizontal); // 2 rows + } + } + + private void OnItemsSourceChanged(object sender, CheckedChangedEventArgs e) + { + if (!(sender is RadioButton radioButton) || !e.Value) + return; + if (radioButton == ItemsSourceObservableCollection25) + _viewModel.ItemsSourceType = ItemsSourceType.ObservableCollection25T; + else if (radioButton == ItemsSourceObservableCollection5) + _viewModel.ItemsSourceType = ItemsSourceType.ObservableCollection5T; + else if (radioButton == ItemsSourceGroupedList) + _viewModel.ItemsSourceType = ItemsSourceType.GroupedListT; + else if (radioButton == ItemsSourceNone) + _viewModel.ItemsSourceType = ItemsSourceType.None; + } + } +} diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewViewModel.cs b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewViewModel.cs new file mode 100644 index 000000000000..76d61f1ed6f3 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/CollectionViewViewModel.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using Maui.Controls.Sample.CollectionViewGalleries; + +namespace Maui.Controls.Sample +{ + public class Grouping : ObservableCollection + { + public TKey Key { get; } + + public Grouping(TKey key, IEnumerable items) : base(items) + { + Key = key; + } + } + + public enum ItemsSourceType + { + None, + ObservableCollection25T, + ObservableCollection5T, + GroupedListT, + EmptyGroupedListT, + EmptyObservableCollectionT + } + public class CollectionViewViewModel : INotifyPropertyChanged + { + private object _emptyView; + private object _header; + private object _footer; + private DataTemplate _emptyViewTemplate; + private DataTemplate _headerTemplate; + private DataTemplate _footerTemplate; + private DataTemplate _groupHeaderTemplate; + private DataTemplate _groupFooterTemplate; + private DataTemplate _itemTemplate; + private IItemsLayout _itemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical); + private ItemsSourceType _itemsSourceType = ItemsSourceType.None; + private bool _isGrouped = false; + private ObservableCollection _observableCollection25; + private ObservableCollection _observableCollection5; + private ObservableCollection _emptyObservableCollection; + private List> _groupedList; + private List> _emptyGroupedList; + public event PropertyChangedEventHandler PropertyChanged; + + public CollectionViewViewModel() + { + LoadItems(); + ItemTemplate = new DataTemplate(() => + { + var stackLayout = new StackLayout + { + Padding = new Thickness(10), + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center + }; + + var label = new Label + { + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center + }; + label.SetBinding(Label.TextProperty, "Caption"); + stackLayout.Children.Add(label); + return stackLayout; + }); + + GroupHeaderTemplate = new DataTemplate(() => + { + var stackLayout = new StackLayout + { + BackgroundColor = Colors.LightGray + }; + var label = new Label + { + FontAttributes = FontAttributes.Bold, + FontSize = 18 + }; + label.SetBinding(Label.TextProperty, "Key"); + stackLayout.Children.Add(label); + return stackLayout; + }); + } + + public object EmptyView + { + get => _emptyView; + set { _emptyView = value; OnPropertyChanged(); } + } + + public object Header + { + get => _header; + set { _header = value; OnPropertyChanged(); } + } + + public object Footer + { + get => _footer; + set { _footer = value; OnPropertyChanged(); } + } + + public DataTemplate EmptyViewTemplate + { + get => _emptyViewTemplate; + set { _emptyViewTemplate = value; OnPropertyChanged(); } + } + + public DataTemplate HeaderTemplate + { + get => _headerTemplate; + set { _headerTemplate = value; OnPropertyChanged(); } + } + + public DataTemplate FooterTemplate + { + get => _footerTemplate; + set { _footerTemplate = value; OnPropertyChanged(); } + } + + public DataTemplate GroupHeaderTemplate + { + get => _groupHeaderTemplate; + set { _groupHeaderTemplate = value; OnPropertyChanged(); } + } + + public DataTemplate GroupFooterTemplate + { + get => _groupFooterTemplate; + set { _groupFooterTemplate = value; OnPropertyChanged(); } + } + + public DataTemplate ItemTemplate + { + get => _itemTemplate; + set { _itemTemplate = value; OnPropertyChanged(); } + } + + public IItemsLayout ItemsLayout + { + get => _itemsLayout; + set + { + if (_itemsLayout != value) + { + _itemsLayout = value; + OnPropertyChanged(); + } + } + } + + public ItemsSourceType ItemsSourceType + { + get => _itemsSourceType; + set + { + if (_itemsSourceType != value) + { + _itemsSourceType = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ItemsSource)); + } + } + } + + public bool IsGrouped + { + get => _isGrouped; + set + { + if (_isGrouped != value) + { + _isGrouped = value; + OnPropertyChanged(); + } + } + } + + public object ItemsSource + { + get + { + return ItemsSourceType switch + { + ItemsSourceType.ObservableCollection25T => _observableCollection25, + ItemsSourceType.ObservableCollection5T => _observableCollection5, + ItemsSourceType.GroupedListT => _groupedList, + ItemsSourceType.EmptyGroupedListT => _emptyGroupedList, + ItemsSourceType.EmptyObservableCollectionT => _emptyObservableCollection, + ItemsSourceType.None => null, + _ => null + }; + } + } + + + private void LoadItems() + { + _observableCollection25 = new ObservableCollection(); + _observableCollection5 = new ObservableCollection(); + AddItems(_observableCollection5, 5, "Fruits"); + AddItems(_observableCollection25, 10, "Fruits"); + AddItems(_observableCollection25, 10, "Vegetables"); + + _emptyObservableCollection = new ObservableCollection(); + + _groupedList = new List> + { + new Grouping("Fruits", new List()), + new Grouping("Vegetables", new List()) + }; + + AddItems(_groupedList[0], 4, "Fruits"); + AddItems(_groupedList[1], 4, "Vegetables"); + + _emptyGroupedList = new List>(); + } + + + private void AddItems(IList list, int count, string category) + { + string[] fruits = + { + "Apple", "Banana", "Orange", "Grapes", "Mango", + "Pineapple", "Strawberry", "Blueberry", "Peach", "Cherry", + "Watermelon", "Papaya", "Kiwi", "Pear", "Plum", + "Avocado", "Fig", "Guava", "Lychee", "Pomegranate", + "Lime", "Lemon", "Coconut", "Apricot", "Blackberry" + }; + + string[] vegetables = + { + "Carrot", "Broccoli", "Spinach", "Potato", "Tomato", + "Cucumber", "Lettuce", "Onion", "Garlic", "Pepper", + "Zucchini", "Pumpkin", "Radish", "Beetroot", "Cabbage", + "Sweet Potato", "Turnip", "Cauliflower", "Celery", "Asparagus", + "Eggplant", "Chili", "Corn", "Peas", "Mushroom" + }; + + string[] items = category == "Fruits" ? fruits : vegetables; + + for (int n = 0; n < count; n++) + { + list.Add(new CollectionViewTestItem(items[n % items.Length], n)); // Pass the index + } + } + + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + if (propertyName == nameof(IsGrouped)) + { + OnPropertyChanged(nameof(ItemsSource)); + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public class CustomDataTemplateSelector : DataTemplateSelector + { + public DataTemplate Template1 { get; set; } + public DataTemplate Template2 { get; set; } + + protected override DataTemplate OnSelectTemplate(object item, BindableObject container) + { + if (item is CollectionViewTestItem testItem) + { + return testItem.Index % 2 == 0 ? Template1 : Template2; + } + + return Template1; + } + } + + public class CollectionViewTestItem + { + public string Caption { get; set; } + public int Index { get; set; } + + public CollectionViewTestItem(string caption, int index) + { + Caption = caption; + Index = index; + } + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/HeaderFooterMainPage.xaml b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/HeaderFooterMainPage.xaml new file mode 100644 index 000000000000..c98e25007ce5 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/CollectionView/HeaderFooterMainPage.xaml @@ -0,0 +1,15 @@ + + + +