Skip to content

Commit

Permalink
- add tests and fix mapper scenarios
Browse files Browse the repository at this point in the history
  • Loading branch information
PureWeen committed May 14, 2024
1 parent bc4d4b4 commit 6f7fafb
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 35 deletions.
19 changes: 7 additions & 12 deletions src/Controls/src/Core/VisualElement/VisualElement.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,21 @@ internal static void RemapForControls(
viewMapper.ReplaceMapping<IView, IViewHandler>(SemanticProperties.DescriptionProperty.PropertyName, MapSemanticPropertiesDescriptionProperty);
viewMapper.ReplaceMapping<IView, IViewHandler>(SemanticProperties.HintProperty.PropertyName, MapSemanticPropertiesHintProperty);
viewMapper.ReplaceMapping<IView, IViewHandler>(SemanticProperties.HeadingLevelProperty.PropertyName, MapSemanticPropertiesHeadingLevelProperty);
viewMapper.ModifyMapping<IView, IViewHandler>(nameof(IView.Height), MapHeight);
viewMapper.ModifyMapping<IView, IViewHandler>(nameof(IView.Width), MapWidth);
viewMapper.ModifyMapping<IView, IViewHandler>(nameof(IView.Height), MapWidthOrHeight);
viewMapper.ModifyMapping<IView, IViewHandler>(nameof(IView.Width), MapWidthOrHeight);

viewMapper.AppendToMapping<VisualElement, IViewHandler>(nameof(IViewHandler.ContainerView), MapContainerView);

commandMapper.ModifyMapping<VisualElement, IViewHandler>(nameof(IView.Focus), MapFocus);
}

static void MapWidth(IViewHandler handler, IView view, Action<IElementHandler, IElement> action)
internal static void MapWidthOrHeight(IViewHandler handler, IView view, Action<IElementHandler, IElement> action)
{
if (view is VisualElement ve && ve.Batched)
return;

action?.Invoke(handler, view);
}

static void MapHeight(IViewHandler handler, IView view, Action<IElementHandler, IElement> action)
{
if (view is VisualElement ve && ve.Batched)
// If batched is set to true this means that VisualElement.Frame is being set from the platform layout pass
if (view is VisualElement ve && ve.Batched && !ve.DontSuppressHeightWidthRequestChangesToHandlers)
{
return;
}

action?.Invoke(handler, view);
}
Expand Down
14 changes: 14 additions & 0 deletions src/Controls/src/Core/VisualElement/VisualElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,17 @@ static void OnIsFocusedPropertyChanged(BindableObject bindable, object oldvalue,
element.ChangeVisualState();
}

// If we're in the midst of updating the Frame property on VisualElement
// we suppress WidthProperty and HeightProperty from propagating to the handler
// Updates to the Frame is really just the platform informing
// the xplat code of it's actual dimensions, these aren't user controlled properties.
// But, if there's a scenario where a user modifies the WidthRequest or HeightRequest
// during the SizeAllocated events, then, we still want to propagate those changes to the handler
// in order to not break any previous scenarios where the user was relying on that behavior.
// We could also do this by wiring up mappers to the WidthRequestProperty and HeightRequestProperty but
// that will somewhat change the order, so, this is least invasive.
bool DontSuppressHeightWidthRequestChangesToHandlers {get; set;}

static void OnRequestChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var constraint = LayoutConstraint.None;
Expand All @@ -1641,8 +1652,11 @@ static void OnRequestChanged(BindableObject bindable, object oldvalue, object ne

if (element is IView fe)
{
element.DontSuppressHeightWidthRequestChangesToHandlers = true;
fe.Handler?.UpdateValue(nameof(IView.Width));
fe.Handler?.UpdateValue(nameof(IView.Height));
element.DontSuppressHeightWidthRequestChangesToHandlers = false;

fe.Handler?.UpdateValue(nameof(IView.MinimumHeight));
fe.Handler?.UpdateValue(nameof(IView.MinimumWidth));
fe.Handler?.UpdateValue(nameof(IView.MaximumHeight));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BasicVisualElement, object>
{
public BasicVisualElementHandler(IPropertyMapper mapper, CommandMapper commandMapper = null) : base(mapper, commandMapper)
{
}

protected override object CreatePlatformView()
{
return new object();
}
}
}
61 changes: 61 additions & 0 deletions src/Controls/tests/Core.UnitTests/VisualElementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -237,5 +242,61 @@ public async Task ShadowDoesNotLeak()

Assert.False(reference.IsAlive, "VisualElement should not be alive!");
}

[Fact]
public void HandlerDoesntPropagateWidthChangesDuringBatchUpdates()
{
bool mapperCalled = false;

var mapper = new PropertyMapper<IView, ViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IView.Height)] = (_,_) => mapperCalled = true,
[nameof(IView.Width)] = (_,_) => mapperCalled = true,
};

VisualElement.RemapForControls(mapper, new CommandMapper<IView, IViewHandler>(ViewHandler.ViewCommandMapper));

var mauiApp1 = MauiApp.CreateBuilder()
.UseMauiApp<ApplicationStub>()
.ConfigureMauiHandlers(handlers => handlers.AddHandler<BasicVisualElement>((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()
{
// read comments for DontSuppressHeightWidthRequestChangesToHandlers
// to understand why this test is here
bool mapperCalled = false;

var mapper = new PropertyMapper<IView, ViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IView.Height)] = (_,_) => mapperCalled = true,
[nameof(IView.Width)] = (_,_) => mapperCalled = true,
};

VisualElement.RemapForControls(mapper, new CommandMapper<IView, IViewHandler>(ViewHandler.ViewCommandMapper));

var mauiApp1 = MauiApp.CreateBuilder()
.UseMauiApp<ApplicationStub>()
.ConfigureMauiHandlers(handlers => handlers.AddHandler<BasicVisualElement>((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);
}
}
}
26 changes: 24 additions & 2 deletions src/Core/src/Handlers/Border/BorderHandler.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,40 @@ static partial void UpdateContent(IBorderHandler handler)
handler.PlatformView.AddView(view.ToPlatform(handler.MauiContext));
}

public static partial void MapHeight(IBorderHandler handler, IBorderView border)
/// <summary>
/// Maps the abstract <see cref="IView.Height"/> property to the platform-specific implementations.
/// </summary>
/// <param name="handler">The associated handler.</param>
/// <param name="border">The associated <see cref="IBorderView"/> instance.</param>
public static void MapHeight(IBorderHandler handler, IBorderView border)
{
// These are no longer called from the Mapper the Mapper just uses
// MapHeightOrWidth
// TODO .NET9 obsolete this?
handler.PlatformView?.UpdateHeight(border);
handler.PlatformView?.InvalidateBorderStrokeBounds();
}

public static partial void MapWidth(IBorderHandler handler, IBorderView border)
/// <summary>
/// Maps the abstract <see cref="IView.Width"/> property to the platform-specific implementations.
/// </summary>
/// <param name="handler">The associated handler.</param>
/// <param name="border">The associated <see cref="IBorderView"/> instance.</param>
public static void MapWidth(IBorderHandler handler, IBorderView border)
{

// These are no longer called from the Mapper the Mapper just uses
// MapHeightOrWidth
// TODO .NET9 obsolete these?
handler.PlatformView?.UpdateWidth(border);
handler.PlatformView?.InvalidateBorderStrokeBounds();
}

static void MapHeightOrWidth(IBorderHandler handler, IBorderView view)
{
handler.PlatformView?.InvalidateBorderStrokeBounds();
}

protected override void DisconnectHandler(ContentViewGroup platformView)
{
// If we're being disconnected from the xplat element, then we should no longer be managing its children
Expand Down
26 changes: 5 additions & 21 deletions src/Core/src/Handlers/Border/BorderHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ public partial class BorderHandler : IBorderHandler
{
public static IPropertyMapper<IBorderView, IBorderHandler> Mapper = new PropertyMapper<IBorderView, IBorderHandler>(ViewMapper)
{
#if __ANDROID__
[nameof(IBorderView.Height)] = MapHeight,
[nameof(IBorderView.Width)] = MapWidth,
#endif
[nameof(IBorderView.Background)] = MapBackground,
[nameof(IBorderView.Content)] = MapContent,
[nameof(IBorderView.Shape)] = MapStrokeShape,
Expand All @@ -35,7 +31,11 @@ public partial class BorderHandler : IBorderHandler
[nameof(IBorderView.StrokeDashPattern)] = MapStrokeDashPattern,
[nameof(IBorderView.StrokeDashOffset)] = MapStrokeDashOffset,
[nameof(IBorderView.StrokeMiterLimit)] = MapStrokeMiterLimit
};
}
#if __ANDROID__
.Append(nameof(IBorderView.Height), MapHeightOrWidth)
#endif
;

public static CommandMapper<IBorderView, BorderHandler> CommandMapper = new(ViewCommandMapper)
{
Expand Down Expand Up @@ -164,21 +164,5 @@ public static void MapContent(IBorderHandler handler, IBorderView border)
}

static partial void UpdateContent(IBorderHandler handler);

#if __ANDROID__
/// <summary>
/// Maps the abstract <see cref="IView.Width"/> property to the platform-specific implementations.
/// </summary>
/// <param name="handler">The associated handler.</param>
/// <param name="border">The associated <see cref="IBorderView"/> instance.</param>
public static partial void MapWidth(IBorderHandler handler, IBorderView border);

/// <summary>
/// Maps the abstract <see cref="IView.Height"/> property to the platform-specific implementations.
/// </summary>
/// <param name="handler">The associated handler.</param>
/// <param name="border">The associated <see cref="IBorderView"/> instance.</param>
public static partial void MapHeight(IBorderHandler handler, IBorderView border);
#endif
}
}
15 changes: 15 additions & 0 deletions src/Core/src/PropertyMapperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ public static void ReplaceMapping<TVirtualView, TViewHandler>(this IPropertyMapp
propertyMapper.ModifyMapping<TVirtualView, TViewHandler>(key, (h, v, p) => method.Invoke(h, v));
}

internal static void ReplaceMapping<TVirtualView, TViewHandler>(this IPropertyMapper<TVirtualView, TViewHandler> propertyMapper,
string key, Action<TViewHandler, TVirtualView> method)
where TVirtualView : IElement where TViewHandler : IElementHandler
{
var previousMethod = propertyMapper.GetProperty(key);

if (previousMethod is null)
{
propertyMapper.Add(key, method);
return;
}

propertyMapper.ModifyMapping(key, (h, v, p) => method.Invoke(h, v));
}

/// <summary>
/// Specify a method to be run after an existing property mapping.
/// </summary>
Expand Down
84 changes: 84 additions & 0 deletions src/Core/src/PropertyMapperFluentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;

namespace Microsoft.Maui
{
internal static class PropertyMapperFluentExtensions
{
/// <summary>
/// Creates a new PropertyMapper based on an existing PropertyMapper, targeted at a new virtual view and handler type.
/// </summary>
internal static IPropertyMapper<TVirtualView, THandler> RemapFor<TVirtualView, THandler>(this IPropertyMapper<IElement, IElementHandler> innerMap)
where TVirtualView : IElement
where THandler : IElementHandler
{
return new PropertyMapper<TVirtualView, THandler>(innerMap);
}

/// <summary>
/// Creates a property mapping. If a previous property mapping exists, it is replaced.
/// </summary>
internal static IPropertyMapper<TVirtualView, THandler> Map<TVirtualView, THandler>(this IPropertyMapper<TVirtualView, THandler> propertyMapper,
string key, Action<THandler, TVirtualView> propertyUpdater)
where TVirtualView : IElement
where THandler : IElementHandler
{
propertyMapper.Add(key, propertyUpdater);
return propertyMapper;
}

/// <summary>
/// Modifies an existing mapping.
/// </summary>
internal static IPropertyMapper<TVirtualView, TViewHandler> Modify<TVirtualView, TViewHandler>(this IPropertyMapper<TVirtualView, TViewHandler> propertyMapper,
string key, Action<TViewHandler, TVirtualView, Action<IElementHandler, IElement>?> method)
where TVirtualView : IElement where TViewHandler : IElementHandler
{
propertyMapper.ModifyMapping(key, method);
return propertyMapper;
}

/// <summary>
/// Inserts a new mapping before an existing mapping.
/// </summary>
internal static IPropertyMapper<TVirtualView, TViewHandler> Prepend<TVirtualView, TViewHandler>(this IPropertyMapper<TVirtualView, TViewHandler> propertyMapper,
string key, Action<TViewHandler, TVirtualView> method)
where TVirtualView : IElement where TViewHandler : IElementHandler
{
propertyMapper.PrependToMapping(key, method);
return propertyMapper;
}

/// <summary>
/// Inserts a new mapping after an existing mapping.
/// </summary>
internal static IPropertyMapper<TVirtualView, TViewHandler> Append<TVirtualView, TViewHandler>(this IPropertyMapper<TVirtualView, TViewHandler> propertyMapper,
string key, Action<TViewHandler, TVirtualView> method)
where TVirtualView : IElement where TViewHandler : IElementHandler
{
propertyMapper.AppendToMapping(key, method);
return propertyMapper;
}

/// <summary>
/// Remaps a single property for the specified virtual view and handler types. If the virtual view or handler do not match the
/// specified type, the original mapping is used.
/// </summary>
internal static IPropertyMapper<IElement, IElementHandler> RemapFor<TVirtualView, TViewHandler>(this IPropertyMapper<IElement, IElementHandler> propertyMapper,
string key, Action<TViewHandler, TVirtualView> method)
where TVirtualView : IElement where TViewHandler : IElementHandler
{
var previousMethod = propertyMapper.GetProperty(key);

void newMethod(IElementHandler handler, IElement view)
{
if ((handler is null || handler is TViewHandler) && view is TVirtualView v)
method((TViewHandler)handler!, v);
else
previousMethod?.Invoke(handler!, view);
}

propertyMapper.Add(key, newMethod);
return propertyMapper;
}
}
}

0 comments on commit 6f7fafb

Please sign in to comment.