FmgLib.MauiMarkup

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:

C#
// 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:

C#
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:

C#
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:

C#
// 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):

C#
// 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:

C#
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:

C#
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:

C#
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:

C#
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)