diff --git a/src/Controls/src/Core/Element/Element.cs b/src/Controls/src/Core/Element/Element.cs index 8d18b3e2458c..bf3dada20406 100644 --- a/src/Controls/src/Core/Element/Element.cs +++ b/src/Controls/src/Core/Element/Element.cs @@ -571,7 +571,7 @@ protected override void OnPropertyChanged([CallerMemberName] string propertyName { base.OnPropertyChanged(propertyName); - Handler?.UpdateValue(propertyName); + UpdateHandlerValue(propertyName); if (_effects?.Count > 0) { @@ -583,6 +583,9 @@ protected override void OnPropertyChanged([CallerMemberName] string propertyName } } + private protected virtual void UpdateHandlerValue(string property) => + Handler?.UpdateValue(property); + internal IEnumerable Descendants() => Descendants(); diff --git a/src/Controls/src/Core/VisualElement/VisualElement.cs b/src/Controls/src/Core/VisualElement/VisualElement.cs index 243e4a49eda8..fd9d905470fe 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement.cs @@ -1942,6 +1942,19 @@ static double EnsurePositive(double value) return value; } + private protected override void UpdateHandlerValue(string property) + { + // The HeightProperty and WidthProperty are not designed to propagate back to the handler. + // Instead, we use WidthRequestProperty and HeightRequestProperty to propagate changes to the handler. + // HeightProperty and WidthProperty are readonly and only update when the VisualElement.Frame property is set + // during an arrange pass, which indicates the actual width and height of the platform element. + // Changes to WidthRequestProperty and HeightRequestProperty will propagate to the handler via the `OnRequestChanged` method. + if (this.Batched && (property == HeightProperty.PropertyName || property == WidthProperty.PropertyName)) + return; + + base.UpdateHandlerValue(property); + } + /// double IView.Width { diff --git a/src/Controls/tests/Core.UnitTests/TestClasses/BasicVisualElement.cs b/src/Controls/tests/Core.UnitTests/TestClasses/BasicVisualElement.cs new file mode 100644 index 000000000000..bfffbd55f44a --- /dev/null +++ b/src/Controls/tests/Core.UnitTests/TestClasses/BasicVisualElement.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Microsoft.Maui.Animations; + +using Microsoft.Maui.Handlers; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class BasicVisualElement : VisualElement + { + } + + public class BasicVisualElementHandler : ViewHandler + { + public BasicVisualElementHandler(IPropertyMapper mapper, CommandMapper commandMapper = null) : base(mapper, commandMapper) + { + } + + protected override object CreatePlatformView() + { + return new object(); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/Core.UnitTests/VisualElementTests.cs b/src/Controls/tests/Core.UnitTests/VisualElementTests.cs index ba86e9bd57fc..76cd9e8fc7c6 100644 --- a/src/Controls/tests/Core.UnitTests/VisualElementTests.cs +++ b/src/Controls/tests/Core.UnitTests/VisualElementTests.cs @@ -2,7 +2,12 @@ using System.Data.Common; using System.Threading.Tasks; using Microsoft.Maui.Controls.Shapes; + +using Microsoft.Maui.Controls.Hosting; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Hosting; +using Microsoft.Maui.Platform; + using Microsoft.Maui.Handlers; using Microsoft.Maui.Primitives; using Xunit; @@ -237,5 +242,86 @@ public async Task ShadowDoesNotLeak() Assert.False(reference.IsAlive, "VisualElement should not be alive!"); } + + [Fact] + public void HandlerDoesntPropagateWidthChangesDuringBatchUpdates() + { + bool mapperCalled = false; + + var mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IView.Height)] = (_,_) => mapperCalled = true, + [nameof(IView.Width)] = (_,_) => mapperCalled = true, + }; + + var mauiApp1 = MauiApp.CreateBuilder() + .UseMauiApp() + .ConfigureMauiHandlers(handlers => handlers.AddHandler((services) => new BasicVisualElementHandler(mapper))) + .Build(); + + var element = new BasicVisualElement(); + var platformView = element.ToPlatform(new MauiContext(mauiApp1.Services)); + + mapperCalled = false; + element.Frame = new Rect(0,0,100,100); + Assert.False(mapperCalled); + } + + [Fact] + public void HandlerDoesPropagateWidthChangesWhenUpdatedDuringSizedChanged() + { + bool mapperCalled = false; + + var mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IView.Height)] = (_,_) => mapperCalled = true, + [nameof(IView.Width)] = (_,_) => mapperCalled = true, + }; + + var mauiApp1 = MauiApp.CreateBuilder() + .UseMauiApp() + .ConfigureMauiHandlers(handlers => handlers.AddHandler((services) => new BasicVisualElementHandler(mapper))) + .Build(); + + var element = new BasicVisualElement(); + var platformView = element.ToPlatform(new MauiContext(mauiApp1.Services)); + + element.SizeChanged += (_,_) => element.HeightRequest = 100; + mapperCalled = false; + element.Frame = new Rect(0,0,100,100); + + Assert.True(mapperCalled); + } + + [Fact] + public void WidthAndHeightRequestPropagateToHandler() + { + int heightMapperCalled = 0; + int widthMapperCalled = 0; + + var mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IView.Height)] = (_,_) => heightMapperCalled++, + [nameof(IView.Width)] = (_,_) => widthMapperCalled++, + }; + + var mauiApp1 = MauiApp.CreateBuilder() + .UseMauiApp() + .ConfigureMauiHandlers(handlers => handlers.AddHandler((services) => new BasicVisualElementHandler(mapper))) + .Build(); + + var element = new BasicVisualElement(); + var platformView = element.ToPlatform(new MauiContext(mauiApp1.Services)); + + heightMapperCalled = 0; + widthMapperCalled = 0; + element.WidthRequest = 99; + Assert.Equal(1, heightMapperCalled); + Assert.Equal(1, widthMapperCalled); + + element.HeightRequest = 99; + Assert.Equal(2, heightMapperCalled); + Assert.Equal(2, widthMapperCalled); + } } }