Skip to content
4 changes: 2 additions & 2 deletions src/Controls/src/Core/Items/CarouselLayoutTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c
var strValue = value?.ToString();

if (strValue == "HorizontalList")
return LinearItemsLayout.CarouselDefault;
return LinearItemsLayout.CreateCarouselHorizontalDefault();

if (strValue == "VerticalList")
return LinearItemsLayout.CarouselVertical;
return LinearItemsLayout.CreateCarouselVerticalDefault();

throw new InvalidOperationException($"Cannot convert \"{strValue}\" into {typeof(LinearItemsLayout)}");
}
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Items/CarouselView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public object PositionChangedCommandParameter
/// <summary>Bindable property for <see cref="ItemsLayout"/>.</summary>
public static readonly BindableProperty ItemsLayoutProperty =
BindableProperty.Create(nameof(ItemsLayout), typeof(LinearItemsLayout), typeof(ItemsView),
LinearItemsLayout.CarouselDefault);
null, defaultValueCreator: (b) => LinearItemsLayout.CreateCarouselHorizontalDefault());

/// <include file="../../../docs/Microsoft.Maui.Controls/CarouselView.xml" path="//Member[@MemberName='ItemsLayout']/Docs/*" />
[System.ComponentModel.TypeConverter(typeof(CarouselLayoutTypeConverter))]
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/src/Core/Items/ItemsLayoutTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo c
int identifierLength = 0;

if (strValue == "VerticalList")
return LinearItemsLayout.Vertical;
return LinearItemsLayout.CreateVerticalDefault();
else if (strValue == "HorizontalList")
return LinearItemsLayout.Horizontal;
return LinearItemsLayout.CreateHorizontalDefault();
else if (strValue.StartsWith("VerticalGrid", StringComparison.Ordinal))
{
orientation = ItemsLayoutOrientation.Vertical;
Expand Down
3 changes: 2 additions & 1 deletion src/Controls/src/Core/Items/ItemsView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ public int RemainingItemsThreshold

internal static readonly BindableProperty InternalItemsLayoutProperty =
BindableProperty.Create(nameof(ItemsLayout), typeof(IItemsLayout), typeof(ItemsView),
LinearItemsLayout.Vertical, propertyChanged: OnInternalItemsLayoutPropertyChanged);
null, propertyChanged: OnInternalItemsLayoutPropertyChanged,
defaultValueCreator: (b) => LinearItemsLayout.CreateVerticalDefault());

static void OnInternalItemsLayoutPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
Expand Down
36 changes: 24 additions & 12 deletions src/Controls/src/Core/Items/LinearItemsLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,14 @@ public LinearItemsLayout([Parameter("Orientation")] ItemsLayoutOrientation orien
}

/// <include file="../../../docs/Microsoft.Maui.Controls/LinearItemsLayout.xml" path="//Member[@MemberName='Vertical']/Docs/*" />
public static readonly IItemsLayout Vertical = new LinearItemsLayout(ItemsLayoutOrientation.Vertical);
public static readonly IItemsLayout Vertical = CreateVerticalDefault();
/// <include file="../../../docs/Microsoft.Maui.Controls/LinearItemsLayout.xml" path="//Member[@MemberName='Horizontal']/Docs/*" />
public static readonly IItemsLayout Horizontal = new LinearItemsLayout(ItemsLayoutOrientation.Horizontal);
public static readonly IItemsLayout Horizontal = CreateHorizontalDefault();

/// <include file="../../../docs/Microsoft.Maui.Controls/LinearItemsLayout.xml" path="//Member[@MemberName='CarouselVertical']/Docs/*" />
public static readonly IItemsLayout CarouselVertical = new LinearItemsLayout(ItemsLayoutOrientation.Vertical)
{
SnapPointsType = SnapPointsType.MandatorySingle,
SnapPointsAlignment = SnapPointsAlignment.Center
};
public static readonly IItemsLayout CarouselVertical = CreateCarouselVerticalDefault();

internal static readonly LinearItemsLayout CarouselDefault = new LinearItemsLayout(ItemsLayoutOrientation.Horizontal)
{
SnapPointsType = SnapPointsType.MandatorySingle,
SnapPointsAlignment = SnapPointsAlignment.Center
};
internal static readonly LinearItemsLayout CarouselDefault = CreateCarouselHorizontalDefault();

/// <summary>Bindable property for <see cref="ItemSpacing"/>.</summary>
public static readonly BindableProperty ItemSpacingProperty =
Expand All @@ -40,5 +32,25 @@ public double ItemSpacing
get => (double)GetValue(ItemSpacingProperty);
set => SetValue(ItemSpacingProperty, value);
}

internal static LinearItemsLayout CreateVerticalDefault()
=> new LinearItemsLayout(ItemsLayoutOrientation.Vertical);

internal static LinearItemsLayout CreateHorizontalDefault()
=> new LinearItemsLayout(ItemsLayoutOrientation.Horizontal);

internal static LinearItemsLayout CreateCarouselVerticalDefault()
=> new LinearItemsLayout(ItemsLayoutOrientation.Vertical)
{
SnapPointsType = SnapPointsType.MandatorySingle,
SnapPointsAlignment = SnapPointsAlignment.Center
};

internal static LinearItemsLayout CreateCarouselHorizontalDefault()
=> new LinearItemsLayout(ItemsLayoutOrientation.Horizontal)
{
SnapPointsType = SnapPointsType.MandatorySingle,
SnapPointsAlignment = SnapPointsAlignment.Center
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ public void HorizontalListShouldReturnLinearItemsLayout()
{
var converter = new ItemsLayoutTypeConverter();
var result = converter.ConvertFromInvariantString("HorizontalList");
Assert.Same(LinearItemsLayout.Horizontal, result);

Assert.IsType<LinearItemsLayout>(result);
var linearLayout = (LinearItemsLayout)result;
Assert.Equal(ItemsLayoutOrientation.Horizontal, linearLayout.Orientation);
}

[Fact]
public void VerticalListShouldReturnLinearItemsLayout()
{
var converter = new ItemsLayoutTypeConverter();
var result = converter.ConvertFromInvariantString("VerticalList");
Assert.Same(LinearItemsLayout.Vertical, result);

Assert.IsType<LinearItemsLayout>(result);
var linearLayout = (LinearItemsLayout)result;
Assert.Equal(ItemsLayoutOrientation.Vertical, linearLayout.Orientation);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,54 @@
namespace Microsoft.Maui.DeviceTests
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Handlers.Items2;
using Microsoft.Maui.Handlers;
using Xunit;

namespace Microsoft.Maui.DeviceTests
{
public partial class CarouselViewTests
{
[Fact(DisplayName = "CarouselView Does Not Leak With Default ItemsLayout")]
public async Task CarouselViewDoesNotLeakWithDefaultItemsLayout()
{
SetupBuilder();

WeakReference weakCarouselView = null;
WeakReference weakHandler = null;

await InvokeOnMainThreadAsync(async () =>
{
var carouselView = new CarouselView
{
ItemsSource = new List<string> { "Item 1", "Item 2", "Item 3" },
ItemTemplate = new DataTemplate(() => new Label())
// Note: Not setting ItemsLayout - using the default
};

weakCarouselView = new WeakReference(carouselView);

var handler = await CreateHandlerAsync<CarouselViewHandler2>(carouselView);

// Verify handler is created
Assert.NotNull(handler);

// Store weak reference to the handler
weakHandler = new WeakReference(handler);

// Disconnect the handler
((IElementHandler)handler).DisconnectHandler();
});

// Force garbage collection
await AssertionExtensions.WaitForGC(weakCarouselView, weakHandler);

// Verify the CarouselView was collected
Assert.False(weakCarouselView.IsAlive, "CarouselView should have been garbage collected");

// Verify the handler was collected
Assert.False(weakHandler.IsAlive, "CarouselViewHandler2 should have been garbage collected");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,47 @@ public void IndexPathValidTest()
Assert.False(source.IsIndexPathValid(invalidSection));
}

[Fact(DisplayName = "CollectionView Does Not Leak With Default ItemsLayout")]
public async Task CollectionViewDoesNotLeakWithDefaultItemsLayout()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While CollectionView has this GC test, CarouselView lacks a similar explicit test, could create a similar one for CarouselView?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the way it handles layout is the same no ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added CarouselViewDoesNotLeakWithDefaultItemsLayout test in CarouselViewTests.iOS.cs that mirrors the CollectionView test. It verifies both CarouselView and CarouselViewHandler2 are properly garbage collected when using the default ItemsLayout. Changes in commit a3f8b41.

{
SetupBuilder();

WeakReference weakCollectionView = null;
WeakReference weakHandler = null;

await InvokeOnMainThreadAsync(async () =>
{
var collectionView = new CollectionView
{
ItemsSource = new List<string> { "Item 1", "Item 2", "Item 3" },
ItemTemplate = new DataTemplate(() => new Label())
// Note: Not setting ItemsLayout - using the default
};

weakCollectionView = new WeakReference(collectionView);

var handler = await CreateHandlerAsync<CollectionViewHandler2>(collectionView);

// Verify handler is created
Assert.NotNull(handler);

// Store weak reference to the handler
weakHandler = new WeakReference(handler);

// Disconnect the handler
((IElementHandler)handler).DisconnectHandler();
});

// Force garbage collection
await AssertionExtensions.WaitForGC(weakCollectionView, weakHandler);

// Verify the CollectionView was collected
Assert.False(weakCollectionView.IsAlive, "CollectionView should have been garbage collected");

// Verify the handler was collected
Assert.False(weakHandler.IsAlive, "CollectionViewHandler2 should have been garbage collected");
}

/// <summary>
/// Simulates what a developer might do with a Page/View
/// </summary>
Expand Down
Loading