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:
HorizontalOptions
, of typeLayoutOptions
VerticalOptions
, of typeLayoutOptions
WidthRequest
, of typedouble
HeightRequest
, of typedouble
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,LayoutOptions> get_opt_main, Func<View,double> get_req_main, Func<View,LayoutOptions> get_opt_cross, Func<View,double> get_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(); } } }