Thursday, 28 June 2018

Xamarin StackLayout

The stack layout container is the simplest way to organize your screens in Xamarin forms, it has two orientations: Vertical or Horizontal. Any controls placed in side of it are listed sequentially from top to bottom or left to right. The following is the xaml markup for a stacklayout container with some controls inside of it.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="pav.LayoutsExample.MainPage">
    <StackLayout>
        <Label Text="First name"/>
        <Entry Placeholder="John" />

        <Label Text="Last name"/>
        <Entry Placeholder="Doe"/>

        <Label Text="Birthdate"/>
        <DatePicker />

        <Label Text="Phone number"/>
        <Entry Placeholder="519 979 1337" Keyboard="Numeric"/>

        <Label Text="Email"/>
        <Entry Placeholder="first.last@domain.com" Keyboard="Email"/>
    </StackLayout>

</ContentPage>

below is how it renders


now you can see that if the orientation is not specified then the default is vertical. with that said you can also see that the elements do not space themselves out nicely, it's hard to tell which label corresponds to which textbox. the Stacklayout container has a property called Spacing which defines a decimal number the specifies the amount of space between elements, it defaults to 6. What we are going to do is group all of the complementary labels with their textbox's inside their own stacklayouts and give those nested stacklaouts a spacing of 0, then increase the root stacklayouts spacing to 10.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="pav.LayoutsExample.MainPage">
    <ContentPage.Resources>
        <Style TargetType="StackLayout">
            <Setter Property="Spacing" Value="0"/>
        </Style>
     
    </ContentPage.Resources>
    <StackLayout Spacing="10" Padding="5">
        <StackLayout>
            <Label Text="First name"  />
            <Entry Placeholder="John"  />
        </StackLayout>

        <StackLayout>
            <Label Text="Last name"/>
            <Entry Placeholder="Doe"/>
        </StackLayout>

        <StackLayout>
            <Label Text="Birthdate"/>
            <DatePicker />
        </StackLayout>

        <StackLayout>
            <Label Text="Phone number"/>
            <Entry Placeholder="519 979 1337" Keyboard="Numeric"/>
        </StackLayout>

        <StackLayout>
            <Label Text="Email"/>
            <Entry Placeholder="first.last@domain.com" Keyboard="Email"/>
        </StackLayout>
    </StackLayout>

</ContentPage>

we also added a padding of 5 to our root stack layout to keep the labels from being flush with the screen.


notice that our textbox's have what appears to be a margin, this is because each platform renders them slightly differently, the best solution for this is to create a custom render, a bit involved but MSDN has a great article on just that for entry controls; for now we'll turn a blind eye to that design offence.

let's focus on another issue, and that is our device's orientation. when looking at the horizontal orientation of our app we see that there's a lot of wasted vertical space, we really don't need our labels to be above their textboxs but could in fact just be to the left of them. Also not that the color choice for our labels isn't the greatest.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="pav.LayoutsExample.MainPage">
   
    <ContentPage.Resources>
        <!--Style to hook into that we override when device rotates-->
        <Style  x:Key="InnerStack_Style" TargetType="StackLayout" />

        <!--Style for when the device is vertical-->
        <Style  x:Key="StackVertical_Style" TargetType="StackLayout">
            <Setter Property="Spacing" Value="0"/>
            <Setter Property="Orientation" Value="Vertical"/>
        </Style>
       
        <!--Style for when the device is horizontal-->
        <Style  x:Key="StackHorisontal_Style" TargetType="StackLayout">
            <Setter Property="Spacing" Value="0"/>
            <Setter Property="Orientation" Value="Horizontal"/>
        </Style>
        
        <!--Style the labels to look better-->
        <Style TargetType="Label">
            <Setter Property="VerticalTextAlignment" Value="Center"/>
            <Setter Property="WidthRequest" Value="100"/>
            <Setter Property="TextColor" Value="CornflowerBlue" />
        </Style>
    </ContentPage.Resources>
   
    <StackLayout Spacing="10" Padding="5" Orientation="Vertical">
        <StackLayout Style="{DynamicResource InnerStack_Style}">
            <Label Text="First name" />
            <Entry Placeholder="John"  HorizontalOptions="FillAndExpand" />
        </StackLayout>

        <StackLayout Style="{DynamicResource InnerStack_Style}">
            <Label Text="Last name"/>
            <Entry Placeholder="Doe" HorizontalOptions="FillAndExpand"/>
        </StackLayout>

        <StackLayout Style="{DynamicResource InnerStack_Style}">
            <Label Text="Birthdate"/>
            <DatePicker HorizontalOptions="FillAndExpand"/>
        </StackLayout>

        <StackLayout Style="{DynamicResource InnerStack_Style}">
            <Label Text="Phone number"/>
            <Entry Placeholder="519 979 1337" Keyboard="Numeric" HorizontalOptions="FillAndExpand"/>
        </StackLayout>

        <StackLayout Style="{DynamicResource InnerStack_Style}">
            <Label Text="Email"/>
            <Entry Placeholder="first.last@domain.com" Keyboard="Email" HorizontalOptions="FillAndExpand"/>
        </StackLayout>
    </StackLayout>

</ContentPage>

so that was a leap, the main point of interest is the stacklayout styles, we created three of them, the first InnerStack_Style is a hook that the inner stacklayouts reference via a DynamicResouce "binding" the difference between a dynamicResouce vs a staticResouce is simply that if the style updates so will the controls look, whereas for a StaticResource it's look is only updated once on the initial rendering.

one other thing that should be called out is notice the HorizontalOptions property set to "FillAndExpand" on all of the entry controls (textboxs), this is because when the orientation is horizontal each control tries to take up as little horizontal space as possible.

Which bears the question, where do we updated our StackLayout_Style? and the answer is in the codebehind.

using Xamarin.Forms;

namespace pav.LayoutsExample
{
    public partial class MainPage : ContentPage
    {
        public MainPage() => InitializeComponent();

        protected override void OnSizeAllocated(double width, double height)
        {
            base.OnSizeAllocated(width, height);

            if (width > height)
                //landscape orientation
                this.Resources["InnerStack_Style"] = this.Resources["StackHorisontal_Style"];
            else
                //portarte orientation
                this.Resources["InnerStack_Style"] = this.Resources["StackVertical_Style"];
        }
    }

}

now everytime the device's orientation changes the OnSizeAllocated method fires, here we say that if the width is greater than the height then we are in landscape mode, otherwise portrait and we point the InnerStack_Style to the appropriate orientation.



now the finished product looks a bit more refined.