Introducing ELXF: A UI Framework For Concise, Maintainable & Fast Programmatic UI's For Xamarin.Forms
EasyLayout.Forms - or ELXF - is a new UI framework to replace XAML with clean and concise layout code.
Today I’m happy to announce a new UI framework for Xamarin.Forms. It’s called EasyLayout.Forms (ELXF) and is an alternative to XAML and to programmatic nested view creation. Its goals are:
- Maximize UI performance by reducing excess render cycles associated with traditional view nesting
- Increase maintainability and readability by removing ceremony and keeping layout code concise
- Simplify usage of RelativeLayout while increasing its power and abstracting away its quirks
In this post I’ll briefly explain what it is, then get into why we need a new UI framework in the context of each of the above three goals. I'll finish with limitations, some history, and how to get started.
What Is ELXF?
EasyLayout.Forms (ELXF) is a C# Domain Specific Language that allows you to define the relationships between children in a RelativeLayout. For example to position one element 50px below another in XAML you would normally do this:
<RelativeLayout>
<Label
RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView, Property=X, ElementName=MainLabel, Constant=0}"
RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView, Property=Y, ElementName=MainLabel, Constant=50}"
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, Property=Width, ElementName=MainLabel}"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, Property=Height, ElementName=MainLabel}"
Text="RelativeLabel"
/>
</RelativeLayout>
With ELXF you can do the same thing like this:
_relativeLayout.ConstrainLayout(() =>
_relativeLabel.Bounds.Top == _mainLabel.Bounds.Top + 50 &&
_relativeLabel.Bounds.Left == _mainLabel.Bounds.Left &&
_relativeLabel.Bounds.Width == _mainLabel.Bounds.Width &&
_relativeLabel.Bounds.Height == _mainLabel.Bounds.Height
)
There's a handy self-documenting page that summerizes all of the options on github in LayoutExamplePage.cs.
But what's wrong with the regular way of doing layouts? Why do we need a new framework?
Maximize Performance
Today, regardless of whether you choose to layout UI with XAML or programmatically, the path of least resistance is to create nested view layouts with several levels of StackLayout’s, Grid’s, TableView’s, and custom views.
This creates a performance problem that Michael Ridland explains extremely well in his article Hacking the Xamarin.Forms Layout System for Fun and Profit. It’s worth reading the article a couple of times if you haven’t, but here is one of his key points:
A child of a stacklayout will always cause a full layout cycle, there’s no way a StackLayout can short circuit this cycle.
The solution is described on the Xamarin.Forms documentation on ListView performance:
AbsoluteLayout has the potential to perform layouts without a single measure call. This makes it very powerful for performance. If AbsoluteLayout cannot be used, consider RelativeLayout.
To better illustrate the problem and its solution I created two Xamarin Forms pages, one using view nesting, and one using a RelativeLayout with ELXF. The page simply shows some products (from Northwind) and you can tap one of them to select and then confirm the choice.
The nested view version goes four levels deep on the header and three levels deep in the ListView.
This may look complicated in a single image, but I honestly feel this is fairly typical if not actually simpler than what a real-world app might do.
To compare the performance, I counted the number of measure and draw cycles for each label after performing the same set of steps on each version of the page (scroll, select 3 products, update text in all labels 3 times aka click calculator button 4 times). I then gave a score to each label based on roughly how expensive it was to draw, and set colors to show a heat map.
Here's the traditional page:
And here’s the RelativeLayout with ELXF version:
The numbers in parenthesis are the number of measure operations and the number of draw operations. As you can see the 2nd one is roughly twice as fast.
If you want to check these out yourself* the main pages are at: TraditionalPerformancePage.xaml and ElxfPerformancePage.cs and the custom views are in the Controls folder. There's a lot more to this topic such as the importance of fully constraining your views that I'll save for a later post.
For now we've confirmed the Xamarin documentation and know RelativeLayout’s generally outperform nested views. But why not just use RelativeLayout’s in XAML or programmatically?
* fyi there's currently an issue in Xamarin.Android 7.3.1 for Visual Studio users that causes RelativeLayouts in ListViews to load extremely slowly on Android. The current workaround is to build from a Mac.
Increase Maintainability
Consider the following example in XAML:
<RelativeLayout>
<Label BackgroundColor="Aqua"
Text="Main Label"
x:Name="MainLabel"
RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToParent, Property=X, Constant=10}"
RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToParent, Property=Y, Constant=10}"
RelativeLayout.WidthConstraint="{ConstraintExpression Type=Constant, Constant=100}"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=Constant, Constant=40}"
/>
<Label BackgroundColor="OrangeRed"
RelativeLayout.XConstraint="{ConstraintExpression Type=RelativeToView, Property=X, ElementName=MainLabel, Constant=110}"
RelativeLayout.YConstraint="{ConstraintExpression Type=RelativeToView, Property=Y, ElementName=MainLabel, Constant=50}"
RelativeLayout.WidthConstraint="{ConstraintExpression Type=RelativeToView, Property=Width, ElementName=MainLabel}"
RelativeLayout.HeightConstraint="{ConstraintExpression Type=RelativeToView, Property=Height, ElementName=MainLabel}"
Text="RelativeLabel"
/>
</RelativeLayout>
This renders two labels like this:
I don’t know about you, but I find that code very hard to read. The ELXF version of that looks like this:
relativeLayout.ConstrainLayout(() =>
_mainLabel.Bounds.Top == relativeLayout.Bounds.Top + 10 &&
_mainLabel.Bounds.Left == relativeLayout.Bounds.Left + 10 &&
_mainLabel.Bounds.Width == 150 &&
_mainLabel.Bounds.Height == 40 &&
_relativeLabel.Bounds.Top == _mainLabel.Bounds.Bottom + 10 &&
_relativeLabel.Bounds.Left == _mainLabel.Bounds.Right + 10 &&
_relativeLabel.Bounds.Width == _mainLabel.Bounds.Width &&
_relativeLabel.Bounds.Height == _mainLabel.Bounds.Width
)
It’s concise, powerful, and the syntax is always verified by the compiler. It also fixes a duplication problem in that XAML example. Can you spot the issue?
Simplify RelativeLayout
While Xamarin.Forms RelativeLayouts aren’t exactly broken, they are far less powerful than iOS’s Autolayout or even Android’s RelativeLayout with its fairly extensive set of LayoutParams. The good news is Xamarin realizes this and has plans to introduce a more powerful version in Xamarin.Forms 3. The problem today, however, is that they essentially only allow you to control the top left pixel.
For example if you look again at the XAML above you’ll see that to align RelativeLabel to the right of MainLabel we had to add 110 (the width of MainLabel plus a margin) to RelativeLabel’s X. What we really want is an attribute like RelativeLayout.RightEdgeConstraint instead of the RelativeLayout.XConstraint attribute.
As it stands if we ever change MainLabel’s width, we must remember to update RelativeLabel’s XConstraint. That's the kind of duplication that hides bugs and complicates maintainability. However, even without ELXS we can do better.
If we write this in code it looks like this:
relativeLayout.Children.Add(mainLabel,
Constraint.RelativeToParent(rl => rl.X + 10),
Constraint.RelativeToParent(rl => rl.Y + 10),
Constraint.Constant(100),
Constraint.Constant(40)
);
relativeLayout.Children.Add(relativeLabel,
Constraint.RelativeToView(mainLabel, (rl, v) => v.X + v.Width + 10),
Constraint.RelativeToView(mainLabel, (rl, v) => v.Y + v.Height + 10),
Constraint.RelativeToView(mainLabel, (rl, v) => v.Width),
Constraint.RelativeToView(mainLabel, (rl, v) => v.Height)
);
Much better, right? RelativeLayouts in code have more power. So maybe we don't need a new framework after-all.
Except, even if you don't agree that the ELXF version of
_relativeLabel.Bounds.Left == _mainLabel.Bounds.Right + 10
is easier on the eyes than
Constraint.RelativeToView(mainLabel, (rl, v) => v.X + v.Width + 10)
the code-behind version still has serious limitations when it comes to Centering elements.
The Centering Problem
Suppose we want to center-align a 3rd view under the 2nd one. If we attempt something like this:
relativeLayout.Children.Add(centerLabel,
Constraint.RelativeToView(relativeLabel,
(rl, v) => v.X + (v.Width * .5f) – (centerLabel.Width * .5f)),
Constraint.RelativeToView(relativeLabel,
(rl, v) => v.Y + v.Height)
);
We’ll discover that it renders like this:
Why didn’t CenterLabel pull further left? It’s because when the XConstraint lambda was evaluated, 'centerLabel' hadn’t been rendered yet. A non-rendered view gives a Width or Height of -1. The solution, documented nicely in this StackOverflow post, is this:
Size GetSize(VisualElement ve, RelativeLayout rl) =>
ve.Measure(rl.Width, rl.Height).Request;
relativeLayout.Children.Add(centerLabel,
Constraint.RelativeToView(relativeLabel,
(rl, v) => v.X + (v.Width * .5f) - (GetSize(centerLabel, rl).Width * .5f)),
Constraint.RelativeToView(relativeLabel,
(rl, v) => v.Y + v.Height + 10)
);
That GetSize() local function (some C# 7 sugar) solves the problem by calculating what the width of centerLabel will be after it’s rendered. That renders nicely like this:
While that works, perhaps you’ll agree that it's difficult to discern intent among all that math. A complex page with a lot of this style code is liable to hide bugs and obfuscate intent.Worse, it’s not always this easy. What if we want CenterLabel to have a width relative to MainLabel. If we do this:
relativeLayout.Children.Add(centerLabel,
Constraint.RelativeToView(relativeLabel,
(rl, v) => v.X + (v.Width * .5f) - (GetSize(centerLabel, rl).Width / 2)),
Constraint.RelativeToView(relativeLabel,
(rl, v) => v.Y + v.Height + 10),
Constraint.RelativeToView(mainLabel,
(rl, v) => v.Width)
);
We end up with this:
The problem is our GetSize() method is calculating the width of the label prior to any RelativeLayout width constraints.This is the point at which we’re stuck with the solution of hard-coding (duplicating) MainLabel’s width.
ELXF to the Rescue
EasyLayout.Forms can solve the centering problem. It translates LINQ expressions into Children.Add() calls with the correct parameters, it incorporates calls to a GetSize() type function when necessary, and in many cases it can solve the GetCenter() problem from above by searching back through prior LINQ expressions to determine what height or width the current element should be. The final solution turns into this:
relativeLayout.ConstrainLayout(() =>
_mainLabel.Bounds.Top == relativeLayout.Bounds.Top + 10 &&
_mainLabel.Bounds.Left == relativeLayout.Bounds.Left + 10 &&
_mainLabel.Bounds.Width == 150 &&
_mainLabel.Bounds.Height == 40 &&
_relativeLabel.Bounds.Top == _mainLabel.Bounds.Bottom + 10 &&
_relativeLabel.Bounds.Left == _mainLabel.Bounds.Right + 10 &&
_relativeLabel.Bounds.Width == _mainLabel.Bounds.Width &&
_relativeLabel.Bounds.Height == _mainLabel.Bounds.Height &&
_centerLabel.Bounds.GetCenterX() == _relativeLabel.Bounds.GetCenterX() &&
_centerLabel.Bounds.Top == _relativeLabel.Bounds.Bottom + 10
);
Limitations
ELXF makes the RelativeLayout more powerful, but it can't patch over all of the issues. Until Xamarin.Forms 3 comes out the following are a few of the known issues:
- If you update the text of a view with Right or Center constraints, the relative layout doesn’t know to redraw it. To force the redraw you have to call relativeLayout.ForceLayout() twice
- You can’t currently constrain a Left edge to one view and a Right edge to another view the way you could with iOS Autolayout. The workaround is to set the width just like you would with a regular RelativeLayout
- Unlike the iOS version of EasyLayout, be aware that ELXF has no less than or greater than constraints
A Brief History
Speaking of the iOS version of EasyLayout, I must give credit where it's due and provide some context. EasyLayout is a UI framework originally developed by Frank Krueger (@praeclarum) to simplify doing programmatic Autolayout in Xamarin.iOS. It does this by creating a simple DSL using the awesome Expression Trees feature of C#. EasyLayout for iOS is so powerful that I wouldn’t start a Xamarin.iOS project without it, and I honestly feel sorry for traditional iOS developers for not having anything like it. But it was only for Xamarin iOS.
Then, earlier this year, my team decided to move away from AXML files on our Xamarin.Android project. They did this because on our large project AXML files take a very long time to generate and significantly slow development. I took the opportunity to port EasyLayout to Android in the form of EasyLayout.Droid. This turned out to be a fantastic solution for our team, and I now wouldn’t do a Xamarin Android project without EasyLayout.Droid.
While ELXF is not yet as mature as its predecessors, I did at least have the opportunity to bring in lessons learned from two mature projects.
Getting Started
If you'd like to give it a spin you can install it via nuget into a project with:Install-Package EasyLayout.Forms
The source code is all at GitHub and there are an extensive set of examples in the source code like here and here. And as mentioned there is a self documenting page. Also if you clone source there is a playground page where you can experiment.
What's nice is that if you like it you can adopt ELXF on your existing projects on a page by page basis. In fact, you can even use it for just a single view in a single relative layout. There's no obligation to adopt it everywhere. Just don't be surprised if, like me, you grow to like it enough to want it everywhere.
Summary
If you end up using and liking EasyLayout.Forms please shoot me a note on twitter @lprichar, I'd love to hear from you.