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()
{
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