User experience experts constantly emphasize that simplicity is key to a well-designed application. Horizontally or vertically arranged elements are essential for creating clean and clear views. Mastering these layout techniques is crucial to avoid unexpected issues on a user’s device.
Though this topic might seem straightforward, many developers run into issues due to the specific nuances of elements such as HorizontalStackLayout/VerticalStackLayout.
Let’s create several horizontal/vertical layout types to get a basic understanding of how the .NET MAUI layout system works and how to avoid potential issues.
Getting ready
To follow the steps described in this recipe, we just need to create a blank .NET MAUI application. The default template includes sample code in the MainPage.xaml
and MainPage.xaml.cs
files, but you can remove this code and leave only a blank ContentPage
in XAML and a constructor with the InitializeComponent
method in the page class. When copying code snippets with namespaces, don’t forget to replace them with the namespaces in your project.
The code for this recipe is available at https://github.com/PacktPublishing/.NET-MAUI-Cookbook/tree/main/Chapter01/c1-HorizontalAndVerticalLayouts.
How to do it…
We’ll create four linear layouts with buttons using the following panels:
HorizontalStackLayout
VerticalStackLayout
Grid
FlexLayout
Figure 1.1 – Linear layouts
- Add
HorizontalStackLayout
with four buttons to arrange elements horizontally:
MainPage.xaml
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="c1_HorizontalAndVerticalLayouts.MainPage">
<HorizontalStackLayout Spacing="50">
<Button Text="B 1"/>
<Button Text="B 2"/>
<Button Text="B 3"/>
<Button Text="B 4"/>
</HorizontalStackLayout
>
</ContentPage>
If you run the project, you should see the result shown on the left-hand side of Figure 1.1.
- Replace
HorizontalStackLayout
with VerticalStackLayout
:<VerticalStackLayout
Spacing="50">
<Button Text="B 1"/>
<Button Text="B 2"/>
<Button Text="B 3"/>
<Button Text="B 4"/>
</VerticalStackLayout
>
Run the project to see the result.
- Do the same as we did in the previous step, but now, replace
VerticalStackLayout
with Grid
, with four rows. Assign the Grid.Row
attached property to each button:<Grid RowDefinitions="*,*,*,*" RowSpacing="50">
<Button Text="B 1"/>
<Button Text="B 2" Grid.Row="1"/>
<Button Text="B 3" Grid.Row="2"/>
<Button Text="B 4" Grid.Row="3"/>
</Grid>
Run the project to see the result.
- Do the same as we did in the previous step, but now, replace
Grid
with FlexLayout
. Add one more button to the panel:<FlexLayout Wrap="Wrap">
<Button Text="B 1" Margin="25"/>
<Button Text="B 2" Margin="25"/>
<Button Text="B 3" Margin="25"/>
<Button Text="B 4" Margin="25"/>
<Button Text="B 5" Margin="25"/>
</FlexLayout>
Run the project to see the result.
How it works…
All panels in .NET MAUI use the same algorithm to arrange elements. Here’s a broad overview of how it works:
- The panel asks its child elements how much space they need to display their content by calling the
Measure
method. This process is called measuring. During this, the panel informs the child elements about the available space, allowing them to return their optimal size based on these constraints. In other words, the panel communicates the size limits to its elements.
- Based on the measurements from the first step, the panel then calls
Arrange
for each child to position them. This process is called arranging. While the panel considers each element’s desired size, it doesn’t always give them as much space as they request. If all the child elements demand more space than the panel has available, the panel may reduce some of its children.
For a simple linear arrangement task, we used four panel types available in the standard .NET MAUI suite, and all of them have a unique measuring and arranging logic:
HorizontalStackLayout
: When measuring its children, the HorizontalStackLayout
does so without any horizontal constraints. Essentially, it asks each child, “How wide would you like to be if you had infinite width available?” The height
constraint, however, is determined by a panel’s height. In the scenario from the first step, buttons return the width needed to display their text. The panel then arranges the buttons horizontally in a single row, giving each button as much space as requested. Each button is separated by the distance specified in the Spacing
property. If the panel doesn’t have enough space to display an element, that element gets cut off (as seen in Figure 1.1, where the fourth button is not displayed in the first layout).
Key point
HorizontalStackLayout
provides its child elements with as much width as they require to display all content.
VerticalStackLayout
: This panel works exactly like HorizontalStackLayout
, but all the logic is rotated by 90 degrees.
Key point
VerticalStackLayout
provides its child elements with as much height as they require to display all content.
Grid
: The grid panel has a more complex measuring/arranging logic since it may have multiple rows and columns, but in the scenario demonstrated in step 3 in the How to do it section, it does the following:- All the space available for the grid is divided into four equal parts because we defined four rows.
- When measuring the children, the grid provides each child with as much height as available in a corresponding row. Their width is limited by the width of the grid itself.
- When arranging, each element is placed in its row.
FlexLayout
: While this panel also has a complicated measuring/arranging logic because of various settings, in the configuration demonstrated previously, the panel moves elements to the next line when they don’t fit the current row.
There’s more…
What could go wrong in such straightforward scenarios? It might not be obvious at first, but let’s consider the following code example, where CollectionView
, displaying items vertically, is added to VerticalStackLayout
:
<VerticalStackLayout>
<CollectionView>
<CollectionView.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Item1</x:String>
<x:String>Item2</x:String>
<x:String>Item3</x:String>
<!--...-->
<x:String>Item100</x:String>
</x:Array>
</CollectionView.ItemsSource>
</CollectionView>
<Button Text="Some Button"/>
</VerticalStackLayout>
Many developers would expect to get the result demonstrated on the left of the following figure, but instead, they would get the output illustrated on the right:
Figure 1.2 – An issue with CollectionView in VerticalStackLayout
The reason for this is that VerticalStackLayout
provides infinite height to CollectionView
during the measuring cycle and CollectionView
arranges its elements based on the size required to display all items. Since CollectionView
has 100 items, it returns a larger desired size than VerticalStackLayout
has. But since VerticalStackLayout
doesn’t constrain its children, the button element is shifted by CollectionView
beyond the screen. Besides this layout issue, this results in performance problems because CollectionView
creates its elements even if they are not visible on the screen.
To achieve the result demonstrated on the left-hand side of Figure 1.2, use the Grid
panel with two rows:
<Grid RowDefinitions="*,Auto">
<CollectionView>
<CollectionView.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Item1</x:String>
<x:String>Item2</x:String>
<x:String>Item3</x:String>
<!--more items here-->
<x:String>Item100</x:String>
</x:Array>
</CollectionView.ItemsSource>
</CollectionView>
<Button Grid.Row="1" Text="Some Button"/>
</Grid>
Note that RowDefinitions
is set to "*, Auto"
, which means that the second row gets as much space as required by the button and the first row gets all the remaining space.