[Proposal] Typed Bindings
See original GitHub issueFeature name
Typed Bindings
Link to discussion
https://github.com/CommunityToolkit/Maui.Markup/discussions/154#discussioncomment-4210325
Progress tracker
- Android Implementation
- iOS Implementation
- MacCatalyst Implementation
- Windows Implementation
- Tizen Implementation
- Unit Tests
- Samples
- Documentation: https://github.com/MicrosoftDocs/CommunityToolkit/pull/204
Summary
Typed Bindings are used by XAML Compiled Bindings to improve performance and ensure Type safety.
This Proposal extends the .Bind()
extension method by providing the option of using TypedBinding
which is the binding engine used by XAML Compiled Bindings to improve performance and ensure Type safety.
// One-way (aka read-only) Binding
new Label().Row(Row.Description).Bind(Label.TextProperty, (StoryModel m) => m.Description)
// Two-way Binding
new Entry().Bind(Entry.TextProperty, (SettingsViewModel vm) => vm.NumberOfTopStoriesToFetch, (SettingsViewModel vm, int text) => vm.NumberOfTopStoriesToFetch = text)
Motivation
The current implementation of .Bind()
uses the Binding
class which requires reflection when a change to the binding is applied.
This updated implementation brings the option of using TypedBinding
with the .Bind()
extension method which does not require reflection providing a substantial performance improvement for bindings.
Bindings | TypedBinding | |
---|---|---|
Uses Reflection | Yes | No |
Type Safe | No | Yes |
Detailed Design
A POC of this can be found on the Compiled-Bindings
branch:
using Microsoft.Maui.Controls.Internals;
namespace CommunityToolkit.Maui.Markup;
/// <summary>
/// TypedBinding Extension Methods for Bindable Objects
/// </summary>
public static class TypedBindingExtensions
{
/// <summary>Bind to a specified property</summary>
public static TBindable Bind<TBindable, TBindingContext, TSource>(
this TBindable bindable,
BindableProperty targetProperty,
Func<TBindingContext, TSource> getter,
Action<TBindingContext, TSource>? setter = null,
BindingMode? mode = null,
string? stringFormat = null,
TBindingContext? source = default) where TBindable : BindableObject
{
bindable.SetBinding(targetProperty, new TypedBinding<TBindingContext, TSource>(result => (getter(result), true), setter, null)
{
Mode = (setter, mode) switch
{
(_, not null) => mode.Value, // Always use the provided mode when given
(null, null) => BindingMode.OneWay, // When setter is null, binding is read-only; use BindingMode.OneWay to improve performance
_ => BindingMode.Default // Default to BindingMode.Default
},
StringFormat = stringFormat,
Source = source,
});
return bindable;
}
/// <summary>Bind to a specified property with inline conversion</summary>
public static TBindable Bind<TBindable, TBindingContext, TSource, TDest>(
this TBindable bindable,
BindableProperty targetProperty,
Func<TBindingContext, TSource> getter,
Action<TBindingContext, TSource>? setter = null,
BindingMode? mode = null,
Func<TSource?, TDest>? convert = null,
Func<TDest?, TSource>? convertBack = null,
string? stringFormat = null,
TBindingContext? source = default,
TDest? targetNullValue = default,
TDest? fallbackValue = default) where TBindable : BindableObject
{
var converter = new FuncConverter<TSource, TDest, object>(convert, convertBack);
bindable.SetBinding(targetProperty, new TypedBinding<TBindingContext, TSource>(result => (getter(result), true), setter, null)
{
Mode = (setter, mode) switch
{
(_, not null) => mode.Value, // Always use the provided mode when given
(null, null) => BindingMode.OneWay, // When setter is null, binding is read-only; use BindingMode.OneWay to improve performance
_ => BindingMode.Default // Default to BindingMode.Default
},
Converter = converter,
StringFormat = stringFormat,
Source = source,
TargetNullValue = targetNullValue,
FallbackValue = fallbackValue
});
return bindable;
}
/// <summary>Bind to a specified property with inline conversion and conversion parameter</summary>
public static TBindable Bind<TBindable, TBindingContext, TSource, TParam, TDest>(
this TBindable bindable,
BindableProperty targetProperty,
Func<TBindingContext, TSource> getter,
Action<TBindingContext, TSource>? setter = null,
BindingMode? mode = null,
Func<TSource?, TParam?, TDest>? convert = null,
Func<TDest?, TParam?, TSource>? convertBack = null,
TParam? converterParameter = default,
string? stringFormat = null,
TBindingContext? source = default,
TDest? targetNullValue = default,
TDest? fallbackValue = default) where TBindable : BindableObject
{
var converter = new FuncConverter<TSource, TDest, TParam>(convert, convertBack);
bindable.SetBinding(targetProperty, new TypedBinding<TBindingContext, TSource>(result => (getter(result), true), setter, null)
{
Mode = (setter, mode) switch
{
(_, not null) => mode.Value, // Always use the provided mode when given
(null, null) => BindingMode.OneWay, // When setter is null, binding is read-only; use BindingMode.OneWay to improve performance
_ => BindingMode.Default // Default to BindingMode.Default
},
Converter = converter,
ConverterParameter = converterParameter,
StringFormat = stringFormat,
Source = source,
TargetNullValue = targetNullValue,
FallbackValue = fallbackValue
});
return bindable;
}
}
Usage Syntax
// One-way (aka read-only) Binding
new Label().Row(Row.Description).Bind(Label.TextProperty, (StoryModel m) => m.Description)
// Two-way Binding
new Entry().Bind(Entry.TextProperty, (SettingsViewModel vm) => vm.NumberOfTopStoriesToFetch, (SettingsViewModel vm, int text) => vm.NumberOfTopStoriesToFetch = text)
Drawbacks
This is an overload to the existing .Bind()
method, increasing the number of overloaded methods for .Bind()
to 16.
This implementation also ignores TypedBinding
’s string[] handler
constructor parameter. This parameter isn’t documented and I’m unsure how it is being used and what use-cases it covers. However, I’m confident we can add support for this parameter in a future update without breaking changes.
Alternatives
TypedBinding
can be used currently without C# Markup Extensions
// Two-way Binding
var entry = new Entry();
entry.SetBinding(Entry.TextProperty, new TypedBinding<SettingsViewModel, int>(vm => (vm.NumberOfTopStoriesToFetch, true), (vm, number) => vm.NumberOfTopStoriesToFetch = number, null));
Content = entry;
Unresolved Questions
Should we use a different name for this extension method, like .TypedBind()
?
Issue Analytics
- State:
- Created 10 months ago
- Reactions:8
- Comments:8 (3 by maintainers)
Top GitHub Comments
Oh I forgot to add in the proposal for the
BindCommand
extension! Let me add that now.I personally prefer the explicitness of defining the property being bound rather than the defaults but I don’t feel too strongly about it.
We believe we have updated all the relevant examples to use the new typed bindings. Once the next proposals are completed then we can update the rest of the docs. Unless you think we have missed some?
Good Question! I made
BindingMode? mode = null
default tonull
on purpose to slide in a small performance improvement.Since
TypedBinding
can have anull
setter, we can set theBindingMode
toBindingMode.OneWay
automatically for the user which improves performance overBindingMode.TwoWay
.Each
.Bind()
extension method in this proposal uses this logic to set the BindingMode of the binding:^ If the user doesn’t provide a setter and doesn’t provide a BindingMode, then we can safely assume the binding is read-only and set the BindingMode to
BindingMode.OneWay
.If the user does provide a setter, and doesn’t provide a
BindingMode
, then we’ll default toBindingMode.Default
.We can’t make the default
BindingMode.OneWay
because not every binding defaults toBindingMode.OneWay
.https://learn.microsoft.com/dotnet/maui/fundamentals/data-binding/binding-mode?view=net-maui-7.0#two-way-bindings