Compiled Bindings in Code
String-path bindings (e.Path("Name")) are resolved at runtime with reflection. Compiled bindings replace the string with a typed expression, giving you:
- Performance — the binding expression is resolved at compile time; no reflection per update.
- Build-time safety — an invalid path is a compiler error, not a silent runtime failure.
- IntelliSense & refactoring — rename the VM property and the binding follows.
Getter — the compiled Path#
Wherever you would write Path, you can write Getter:
// before — string path, runtime reflection
new Label().Text(e => e.Path("Text").Source(entry));
// after — compiled
new Label().Text(e => e.Getter(static (Entry entry) => entry.Text).Source(entry));
With a view model:
new Label()
.Text(e => e.Getter(static (PersonViewModel vm) => vm.Name))
new Label()
.Text(e => e
.Getter(static (PersonViewModel vm) => vm.Address.City)
.StringFormat("City: {0}"))
All other builder methods (Source, BindingMode, StringFormat, Converter, Convert, FallbackValue, TargetNullValue…) combine with Getter exactly as with Path.
Mark the lambda
static— it prevents accidental closure captures and makes the intent (a pure property access) explicit.
Setter — enabling two-way updates#
For TwoWay/OneWayToSource compiled bindings, supply the reverse operation:
new Entry()
.Text(e => e
.Getter(static (PersonViewModel vm) => vm.Name)
.Setter(static (PersonViewModel vm, string value) => vm.Name = value)
.BindingMode(BindingMode.TwoWay))
What Expressions Are Valid?#
The getter must be a simple property access expression. Supported:
// Property access (incl. null-conditional chains)
static (PersonViewModel vm) => vm.Name;
static (PersonViewModel vm) => vm.Address?.Street;
// Array / indexer access
static (PersonViewModel vm) => vm.PhoneNumbers[0];
static (PersonViewModel vm) => vm.Config["Font"];
// Casts
static (Label label) => (label.BindingContext as PersonViewModel).Name;
static (Label label) => ((PersonViewModel)label.BindingContext).Name;
Not supported (these need a converter or a computed VM property instead):
// Method calls
static (PersonViewModel vm) => vm.GetAddress();
static (PersonViewModel vm) => vm.Address?.ToString();
// Complex expressions
static (PersonViewModel vm) => vm.Address?.Street + " " + vm.Address?.City;
static (PersonViewModel vm) => $"Name: {vm.Name}";
If you need a transformation, keep the getter simple and add Convert:
new Label()
.Text(e => e
.Getter(static (PersonViewModel vm) => vm.Name)
.Convert((string name) => $"Name: {name}"))
Compiled multi-bindings#
Getter may be called more than once. Each call opens its own compiled sub-binding, they may produce different types, and a closing MultiConvert combines them — the whole multi-binding stays reflection-free:
new Label()
.Text(e => e
.Getter(static (PersonViewModel vm) => vm.Name)
.Getter(static (PersonViewModel vm) => vm.Age)
.MultiConvert((string name, int age) => $"{name} ({age})"))
Compiled and string-path sub-bindings can be mixed in one multi-binding, and Setter still belongs to the Getter it follows. See MultiBinding for the full set of combining methods.
Binding.Create (MAUI 9+)#
.NET MAUI 9 added BindingBase.Create, which builds a typed binding object directly from a Func. It pairs perfectly with FmgLib's Bindings(...) for compiled multi-bindings:
public partial class MainPage : ContentPage, IFmgLibHotReload
{
private readonly MainPageViewModel viewModel;
public MainPage()
{
viewModel = new MainPageViewModel();
this.InitializeHotReload();
}
public void Build()
{
this
.BindingContext(viewModel)
.Content(
new VerticalStackLayout()
.Spacing(20)
.Children(
new CheckBox()
.IsChecked(e => e
.Bindings(
Binding.Create(static (MainPageViewModel m) => m.IsOver16),
Binding.Create(static (MainPageViewModel m) => m.HasPassedTest),
Binding.Create(static (MainPageViewModel m) => m.IsSuspended)
)
.Converter(new AllTrueMultiConverter())
.FallbackValue("Is Error.")
.TargetNullValue("Is Null.")
),
new Label()
.Text(e => e
.Bindings(
Binding.Create(static (MainPageViewModel m) => m.Id),
Binding.Create(static (MainPageViewModel m) => m.Name),
Binding.Create(static (MainPageViewModel m) => m.IsSuspended)
)
.StringFormat("{0} : {1} : {2}")
)
)
);
}
}
Compiled Bindings in Templates#
Inside item templates the binding context is the item, so type the lambda accordingly:
new CollectionView()
.ItemsSource(vm.Products)
.ItemTemplate(() =>
new VerticalStackLayout().Children(
new Label().Text(e => e.Getter(static (ProductVM p) => p.Name)),
new Label().Text(e => e
.Getter(static (ProductVM p) => p.Price)
.StringFormat("{0:C}"))
)
)
Migration Cheat Sheet#
| String binding | Compiled binding |
|---|---|
e.Path("Name") |
e.Getter(static (VM vm) => vm.Name) |
e.Path("Address.City") |
e.Getter(static (VM vm) => vm.Address.City) |
e.Path("Text").Source(entry) |
e.Getter(static (Entry x) => x.Text).Source(entry) |
e.Path("Name").BindingMode(TwoWay) |
add .Setter(static (VM vm, string v) => vm.Name = v) |
new Binding().Path("X") in Bindings(...) |
Binding.Create(static (VM vm) => vm.X) |