diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml
new file mode 100644
index 0000000000..ec58f4fb7a
--- /dev/null
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml.cs
new file mode 100644
index 0000000000..c3f1b5b18a
--- /dev/null
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/UniformGridPage.xaml.cs
@@ -0,0 +1,9 @@
+namespace CommunityToolkit.Maui.Sample.Pages.Views;
+
+public partial class UniformGridPage : BasePage
+{
+ public UniformGridPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/ViewsGalleryPage.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ViewsGalleryPage.cs
new file mode 100644
index 0000000000..5aac09bec0
--- /dev/null
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/ViewsGalleryPage.cs
@@ -0,0 +1,11 @@
+using System;
+using CommunityToolkit.Maui.Sample.ViewModels.Views;
+
+namespace CommunityToolkit.Maui.Sample.Pages.Views;
+
+public class ViewsGalleryPage : BaseGalleryPage
+{
+ public ViewsGalleryPage() : base("Views")
+ {
+ }
+}
diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/MainViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/MainViewModel.cs
index 971e1c2f32..ac484c70ea 100644
--- a/samples/CommunityToolkit.Maui.Sample/ViewModels/MainViewModel.cs
+++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/MainViewModel.cs
@@ -3,6 +3,7 @@
using CommunityToolkit.Maui.Sample.Pages.Alerts;
using CommunityToolkit.Maui.Sample.Pages.Behaviors;
using CommunityToolkit.Maui.Sample.Pages.Converters;
+using CommunityToolkit.Maui.Sample.Pages.Views;
using CommunityToolkit.Maui.Sample.Pages.Extensions;
using Microsoft.Maui.Graphics;
@@ -15,6 +16,12 @@ protected override IEnumerable CreateItems() => new[]
new SectionModel(typeof(BehaviorsGalleryPage), "Behaviors", Color.FromArgb("#8E8CD8"),
"Behaviors lets you add functionality to user interface controls without having to subclass them. Behaviors are written in code and added to controls in XAML or code"),
+ new SectionModel(typeof(ConvertersGalleryPage), "Converters", Color.FromArgb("#EA005E"),
+ "Converters let you convert bindings of a certain type to a different value, based on custom logic"),
+
+ new SectionModel(typeof(ViewsGalleryPage), "Views", Color.FromArgb("#EA005E"),
+ "Views"),
+ };
new SectionModel(typeof(ConvertersGalleryPage), "Converters", Color.FromArgb("#EA005E"),
"Converters let you convert bindings of a certain type to a different value, based on custom logic"),
diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs
new file mode 100644
index 0000000000..38fd87e3d5
--- /dev/null
+++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using CommunityToolkit.Maui.Sample.Models;
+using CommunityToolkit.Maui.Sample.Pages.Views;
+using CommunityToolkit.Maui.Views;
+
+namespace CommunityToolkit.Maui.Sample.ViewModels.Views;
+
+public class ViewsGalleryViewModel : BaseGalleryViewModel
+{
+ protected override IEnumerable CreateItems() => new[]
+ {
+ new SectionModel(
+ typeof(UniformGridPage),
+ nameof(UniformGrid),
+ "UniformGrid")
+ };
+}
diff --git a/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj b/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj
index 6a3179ad02..44f73fd6a2 100644
--- a/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj
+++ b/src/CommunityToolkit.Maui.UnitTests/CommunityToolkit.Maui.UnitTests.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/UniformGrid_Tests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/UniformGrid_Tests.cs
new file mode 100644
index 0000000000..073d1dac1a
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Views/UniformGrid_Tests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CommunityToolkit.Maui.UnitTests.Mocks;
+using CommunityToolkit.Maui.Views;
+using Microsoft.Maui;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Graphics;
+using NSubstitute;
+using Xunit;
+
+namespace CommunityToolkit.Maui.UnitTests.Views;
+
+public class UniformGridTests : BaseTest
+{
+ UniformGrid uniformGrid;
+ IView uniformChild;
+ const double childCount = 3;
+ const double childWidth = 20;
+ const double childHeight = 10;
+ public UniformGridTests()
+ {
+ uniformChild = Substitute.For();
+ uniformGrid = new UniformGrid()
+ {
+ Children =
+ {
+ uniformChild,
+ uniformChild,
+ uniformChild
+ }
+ };
+ }
+
+ [Fact]
+ public void MeasureUniformGrid_NoWrap()
+ {
+ var expectedSize = new Size(childWidth * childCount, childHeight);
+ var childSize = new Size(childWidth, childHeight);
+ SetupChildrenSize(childSize);
+ var actualSize = uniformGrid.Measure(childWidth * childCount, childHeight * childCount);
+ Assert.Equal(expectedSize, actualSize);
+ }
+
+ [Fact]
+ public void MeasureUniformGrid_WrapOnNextRow()
+ {
+ var expectedSize = new Size(childWidth, childHeight * childCount);
+ var childSize = new Size(childWidth, childHeight);
+ SetupChildrenSize(childSize);
+ var actualSize = uniformGrid.Measure(childWidth, childHeight * childCount);
+ Assert.Equal(expectedSize, actualSize);
+ }
+
+ [Fact]
+ public void ArrangeChildrenUniformGrid()
+ {
+ var expectedSize = new Size(childWidth, childHeight);
+ SetupChildrenSize(expectedSize);
+ var actualSize = uniformGrid.ArrangeChildren(new Rectangle(0, 0, childWidth * childCount, childHeight * childCount));
+ Assert.Equal(expectedSize, actualSize);
+ }
+
+ [Fact]
+ public void MaxRowsArrangeChildrenUniformGrid()
+ {
+ var expectedSize = new Size(childWidth, childHeight);
+ SetupChildrenSize(expectedSize);
+ uniformGrid.MaxColumns = 1;
+ uniformGrid.MaxRows = 1;
+ var actualSize = uniformGrid.Measure(double.PositiveInfinity, double.PositiveInfinity);
+ Assert.Equal(expectedSize, actualSize);
+ }
+
+ void SetupChildrenSize(Size size)
+ {
+ uniformChild.Measure(double.PositiveInfinity, double.PositiveInfinity).ReturnsForAnyArgs(size);
+ uniformGrid.Measure(childWidth * childCount, childHeight * childCount);
+ }
+}
diff --git a/src/CommunityToolkit.Maui/Views/UniformGrid.shared.cs b/src/CommunityToolkit.Maui/Views/UniformGrid.shared.cs
new file mode 100644
index 0000000000..891935c9fe
--- /dev/null
+++ b/src/CommunityToolkit.Maui/Views/UniformGrid.shared.cs
@@ -0,0 +1,117 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Graphics;
+using Microsoft.Maui.Layouts;
+
+namespace CommunityToolkit.Maui.Views;
+
+///
+/// The UniformGrid is just like the Grid, with the possibility of multiple rows and columns, but with one important difference:
+/// All rows and columns will have the same size.
+/// Use this when you need the Grid behavior without the need to specify different sizes for the rows and columns.
+///
+public class UniformGrid : Grid, ILayoutManager
+{
+ double childWidth;
+
+ double childHeight;
+
+ ///
+ /// Assign this as a LayoutManager
+ ///
+ ///
+ protected override ILayoutManager CreateLayoutManager()
+ {
+ return this;
+ }
+
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty MaxRowsProperty = BindableProperty.Create(nameof(MaxRows), typeof(int), typeof(UniformGrid), int.MaxValue);
+
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty MaxColumnsProperty = BindableProperty.Create(nameof(MaxColumns), typeof(int), typeof(UniformGrid), int.MaxValue);
+
+ ///
+ /// Max rows
+ ///
+ public int MaxRows
+ {
+ get { return (int)GetValue(MaxRowsProperty); }
+ set { SetValue(MaxRowsProperty, value); }
+ }
+
+ ///
+ /// Max columns
+ ///
+ public int MaxColumns
+ {
+ get { return (int)GetValue(MaxColumnsProperty); }
+ set { SetValue(MaxColumnsProperty, value); }
+ }
+
+ ///
+ /// Arrange children
+ ///
+ /// Grid rectangle
+ /// Child size
+ public Size ArrangeChildren(Rectangle rectangle)
+ {
+ Measure(rectangle.Width, rectangle.Height, MeasureFlags.None);
+ var columns = GetColumnsCount(Children.Count, rectangle.Width, childWidth);
+ var rows = GetRowsCount(Children.Count, columns);
+ var boundsWidth = rectangle.Width / columns;
+ var boundsHeight = childHeight;
+ var bounds = new Rectangle(0, 0, boundsWidth, boundsHeight);
+ var count = 0;
+
+ for (var i = 0; i < rows; i++)
+ {
+ for (var j = 0; j < columns && count < Children.Count; j++)
+ {
+ var item = Children[count];
+ bounds.X = j * boundsWidth;
+ bounds.Y = i * boundsHeight;
+ item.Arrange(bounds);
+ count++;
+ }
+ }
+
+ return new Size(boundsWidth, boundsHeight);
+ }
+
+ ///
+ /// Measure grid size
+ ///
+ /// Width constraint
+ /// Height constraint
+ /// Grid size
+ public Size Measure(double widthConstraint, double heightConstraint)
+ {
+ foreach (var child in Children)
+ {
+ if (child.Visibility != Visibility.Visible)
+ continue;
+
+ var sizeRequest = child.Measure(double.PositiveInfinity, double.PositiveInfinity);
+ childHeight = sizeRequest.Height;
+ childWidth = sizeRequest.Width;
+ }
+
+ var columns = GetColumnsCount(Children.Count, widthConstraint, childWidth);
+ var rows = GetRowsCount(Children.Count, columns);
+ return new Size(columns * childWidth, rows * childHeight);
+ }
+
+ int GetColumnsCount(int visibleChildrenCount, double widthConstraint, double maxChildWidth)
+ => Math.Min(double.IsPositiveInfinity(widthConstraint)
+ ? visibleChildrenCount
+ : Math.Min((int)(widthConstraint / maxChildWidth), visibleChildrenCount), MaxColumns);
+
+ int GetRowsCount(int visibleChildrenCount, int columnsCount)
+ => Math.Min((int)Math.Ceiling((double)visibleChildrenCount / columnsCount), MaxRows);
+}