Home About Eric Topics SourceGear

2019-10-02 11:00:00

Xamarin.Forms: LayoutOptions

Things with LayoutOptions in Xamarin.Forms can get weird.

Background

When using a StackLayout, there are four properties on each child view that determine how that child is positioned:

LayoutOptions encapsulates two things, an Alignment, and a flag which indicates whether the child view Expands to use extra space.

A StackLayout has an Orientation which can be either Horizontal or Vertical. Let's call that the main axis, and the perpendicular direction the cross axis.

The Alignment only affects the cross axis. Specifying an Alignment for the main axis has no effect. However, the possible values for Alignment are Fill, Start, Center, and End. There is no value for None. The best we can do is to just leave the Alignment at its default value, which is Fill.

The Expands flag only affects the main axis. Specifying Expands = true for the cross axis has no effect.

Also, a WidthRequest or HeightRequest conflicts with a Alignment = Fill or Expands = true on the corresponding axis. In other words, if you say "I want X to be 40 pixels height" and then you also say "I want X to fill all available vertical space", you are saying two probably-conflicting things about the height of X. Which of your instructions is Xamarin.Forms supposed to obey?

Futility

So this API presents us with several ways to specify things that have no effect.

Suppose I create a horizontal StackLayout and add a child view with VerticalOptions = FillAndExpand. The -AndExpand part doesn't make sense on the cross axis. Changing it to Fill would make the code look more correct (IMHO), but it won't affect the behavior of the app at all.

Similarly, it makes no sense to use VerticalOptions = Center for a child of a vertical StackLayout. That's a line of code that might as well be removed, but again, the user will never know or care.

If stuff like this deserves to be called a bug, it is a bug that the user will never see.

To fix or not to fix

At the bottom of this blog post is a tidbit of code that walks through the children of a StackLayout and gripes about the various problems described above.

When I run that checker on every StackLayout in our app, it finds hundreds of gripes.

(Note also that LayoutOptions.Expands only applies for children of a StackLayout. So I have another snippet (not shown here, but it's trivial) that complains whenever it finds a child of a Grid with a LayoutOptions with Expands = true. Our app has lots of those gripes as well.)

I believe code should be correct, so these gripes bother me.

But I also believe that EVERY code change involves risk, and taking risk that offers little or no benefit to the user is a bad idea.

I wrote about this kind of quandary 14 years ago. Often there are no easy answers. As I said back then, "Figuring out exactly how your product is going to be imperfect is hard. It should be hard.".

Bottom line

One thing I CAN (and often do) recommend is to arm yourself with lots of knowledge about how stuff works "under the hood".

Trying random code changes until "it seems to work" is a lousy approach to software development. Avoid the need to say, "I don't know what was going on, but I made this change and the problem went away." Dig deeper.

Things with LayoutOptions in Xamarin.Forms can get weird, and as with any other API, the best way to deal with that is to read the docs (and the code!) and gain a solid understanding of how things work.

Here's that gripe code

using System;
using Xamarin.Forms;
 
static class GripeLayoutOptions
{
    static void Gripe(string s)
    {
        Console.WriteLine(s);
    }
 
    static void CheckChild(
        Func<View,LayoutOptionsget_opt_main,
        Func<View,doubleget_req_main,

        Func<View,LayoutOptionsget_opt_cross,
        Func<View,doubleget_req_cross,

        View child
        )
    {
        var opt_main = get_opt_main(child);
        if (opt_main.Alignment != LayoutAlignment.Fill)
        {
            Gripe("Alignment along main axis");
        }
        if (opt_main.Expands)
        {
            if (get_req_main(child) >= 0)
            {
                Gripe("Width/Height request with Expands");
            }
        }

        var opt_cross = get_opt_cross(child);
        if (opt_cross.Expands)
        {
            if (!(child is ListView))
            {
                Gripe("Expands along cross axis");
            }
        }
        if (opt_cross.Alignment == LayoutAlignment.Fill)
        {
            if (get_req_cross(child) >= 0)
            {
                Gripe("Width/Height request with Fill");
            }
        }
    }

    public static void CheckStackLayout(StackLayout stk)
    {
        switch (stk.Orientation)
        {
            case StackOrientation.Horizontal:
                foreach (var child in stk.Children)
                {
                    CheckChild(
                        x => x.HorizontalOptions,
                        x => x.WidthRequest,
 
                        x => x.VerticalOptions,
                        x => x.HeightRequest,
 
                        child
                        );
                }
                break;
            case StackOrientation.Vertical:
                foreach (var child in stk.Children)
                {
                    CheckChild(
                        x => x.VerticalOptions,
                        x => x.HeightRequest,
 
                        x => x.HorizontalOptions,
                        x => x.WidthRequest,
 
                        child
                        );
                }
                break;
            default:
                throw new NotImplementedException();
        }
    }
}