Introducing Bindable Property Source Generators

Introducing Bindable Property Source Generators

I am excited to announce two new source generators introduced in CommunityToolkit.Maui v14.0.0 :

  • [BindableProperty]
  • [AttachedBindableProperty<T>]

These new source generators make it easier than ever to create a BindableProperty in your .NET MAUI apps. In fact, all Bindable Properties in CommunityToolkit.Maui are now automatically generated using these new source generators.

Using these attributes, the .NET MAUI Community Toolkit will generate all of the boiler-plate code required for Bindable Properties and Attached Properties.

Opt-into this Experimental Feature

We have decided to release this feature using the [Experimental] attribute. This allows us to let the community test its features and provide feedback while giving us the flexibility to modify the API as requested.

  1. In the csproj file of your .NET MAUI app, suppress the MCTEXP001 :
<!-- Opt into Binbdable Property Source Generators -->
<PropertyGroup>
    <NoWarn>MCTEXP001</NoWarn>
</PropertyGroup>

In the mean-time, I will be writing more comprehensive documentation and writing Analyzers, similar to CommunityToolkit.MVVM, to help provide you the best coding experience!

Using [BindableProperty]

To leverage the Bindable Property Source Generator, first ensure you using a partial class. Then, add the partial keyword and attach the [CommunityToolkit.Maui.BindableProperty] attribute to your Property:

  1. Add the partial keyword to the class
  2. Add the partial keyword the Property for which to generate an associated Bindable Property
  3. Add the [CommunityToolkit.Maui.BindableProperty] attribute to the Property for which to generate an associated Bindable Property

Here is an example:

using CommunityToolkit.Maui;

namespace BindablePropertySourceGeneratorSample;

public partial class CustomButton : Button
{
    [CommunityToolkit.Maui.BindableProperty] 
    public partial int? Number { get; set; }
}

The above example generates the following Bindable Property:

public partial class CustomButton
{
    /// <summary>
    /// BindableProperty for the <see cref = "Number"/> property.
    /// </summary>
    public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Number", typeof(int?), typeof(BindablePropertySourceGeneratorSample.CustomButton), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
    public partial int? Number { get => (int)GetValue(NumberProperty); set => SetValue(NumberProperty, value); }
}

Setting Default Value

To set the default value for [BindableProperty], simply set the initializer on the partial property:

[CommunityToolkit.Maui.BindableProperty] 
public partial int Number { get; set; } = 10;

Setting Default BindingMode

To set the default BindingMode for [BindableProperty], assign the value to the attribute's DefaultBindingMode property:

[CommunityToolkit.Maui.BindableProperty(DefaultBindingMode = BindingMode.TwoWay)] 
public partial int Number { get; set; }

Using ValidateValue

To leverage the ValidateValueDelegate, use the nameof() operator to assign the name of the validation method to the the attribute's ValidateValueMethodName property:

[CommunityToolkit.Maui.BindableProperty(ValidateValueMethodName = nameof(ValidateNumber))] 
public partial int Number { get; set; }

static bool ValidateNumber(BindableObject bindable, object value)
{
    var updatedValue = (int)value;
    return updatedValue > 0;
}
Important: The method used for ValidateValueMethodName must adhere to the ValidateValueDelegate method signature: static bool MethodName(BindableObject, object)

Using PropertyChanging

To leverage the BindingPropertyChangingDelegate, use the nameof() operator to assign the name of the property changing method to the the attribute's PropertyChangingMethodName property:

[CommunityToolkit.Maui.BindableProperty(PropertyChangingMethodName = nameof(OnNumberChanging))] 
public partial int Number { get; set; }

static void OnNumberChanging(BindableObject bindable, object oldValue, object newValue)
{
  Trace.WriteLine($"Old Value: {oldValue}");
  Trace.WriteLine($"New Value: {newValue}");
}
Important: The method used for PropertyChangingMethodName must adhere to the BindingPropertyChangingDelegate method signature: static void MethodName(BindableObject, object, object)

Using PropertyChanged

To leverage the BindingPropertyChangedDelegate, use the nameof() operator to assign the name of the property changed method to the the attribute's PropertyChangedMethodName property:

[CommunityToolkit.Maui.BindableProperty(PropertyChangedMethodName = nameof(OnNumberChanged))] 
public partial int Number { get; set; }


static void OnNumberChanged(BindableObject bindable, object oldValue, object newValue)
{
  Trace.WriteLine($"Old Value: {oldValue}");
  Trace.WriteLine($"New Value: {newValue}");
}
Important: The method used for PropertyChangedMethodName must adhere to the BindingPropertyChangedDelegate method signature: static void MethodName(BindableObject, object, object)

Using CoreceValue

To leverage the CoerceValueDelegate, use the nameof() operator to assign the name of the coerce value changed method to the the attribute's CoerceValueMethodName property:

[CommunityToolkit.Maui.BindableProperty(CoerceValueMethodName = nameof(CoerceNumber))] 
public partial int Number { get; set; }

static object CoerceNumber(BindableObject bindable, object value)
{
    var updatedValue = (int)value;
    return Math.Clamp(updatedValue, 0, int.MaxValue);
}
Important: The method used for CoerceValueMethodName must adhere to the CoreceValueDelegate method signature: static object MethodName(BindableObject, object)

Creating a Readonly Bindable Property

The [BindableProperty] attribute will automatically generate a Readonly Bindable Property (eg BindableProperty.CreateReadOnly()) when the accessibility modifier of the Property's setter is protected, private protected or private.

For example, a Readonly Bindable Property will be generated for the following properties whose setter cannot be accessed outside of its class:

public partial class CustomButton : Button
{
    [CommunityToolkit.Maui.BindableProperty] 
    public partial int Number { get; protected set; }
    
    [CommunityToolkit.Maui.BindableProperty] 
    public partial char Letter { get; private protected set; }
    
    [CommunityToolkit.Maui.BindableProperty] 
    public partial string Text { get; private set; }
}

Using [AttachedBindableProperty<T>]

Using [AttachedBindableProperty<T>] is a bit different. This attribute is applied to either the Class or the Constructor, not a Property, as Attached Properties don't have an associated Property:

.NET Multi-platform App UI (.NET MAUI) attached properties enable an object to assign a value for a property that its own class doesn't define

To leverage the Attached Bindable Property Source Generator, first ensure you using a partial class. Then, add [CommunityToolkit.Maui.AttachedBindableProperty<T>] attribute to either the class or constructor:

  1. Add the partial keyword to the class
  2. Add the [CommunityToolkit.Maui.AttachedBindableProperty<T>] attribute to the class or constructor for which to generate an associated Attached Bindable Property

Here is an example:

namespace BindablePropertySourceGeneratorSample;

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number")] 
public partial class CustomButton : Button
{
  public CustomButton()
  {
  }
}

Alternatively, the attribute can be attached to the constructor:

namespace BindablePropertySourceGeneratorSample;
 
public partial class CustomButton : Button
{
  [CommunityToolkit.Maui.AttachedBindableProperty<int>("Number")]
  public CustomButton()
  {
  }
}

Both examples above generate the following Attached Bindable Property along with its associated Get/Set methods

public partial class CustomButton
{
    /// <summary>
    /// Attached BindableProperty for the Number property.
    /// </summary>
    public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Number", typeof(int), typeof(BindablePropertySourceGeneratorSample.CustomButton), null, (global::Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null);
    /// <summary>
    /// Gets Number for the <paramref name = "bindable"/> child element.
    /// </summary>
    public static int GetNumber(global::Microsoft.Maui.Controls.BindableObject bindable) => (int)bindable.GetValue(NumberProperty);
    /// <summary>
    /// Sets Number for the <paramref name = "bindable"/> child element.
    /// </summary>
    public static void SetNumber(global::Microsoft.Maui.Controls.BindableObject bindable, int value) => bindable.SetValue(NumberProperty, value);
}

Nullability

In C#, Attribute<T> does not yet support nullability for T. For example, [AttachedBindableProperty<string?>] will generate a compiler error.

To generate an Attached Property for a nullable type, set IsNullable to true:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", IsNullable = true)] 
public partial class CustomButton : Button
{
}

Setting IsNullable = true will generate an Attached Property using a nullable T?:

public partial class CustomButton
{
    /// <summary>
    /// Attached BindableProperty for the Number property.
    /// </summary>
    public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Number", typeof(int), typeof(BindablePropertySourceGeneratorSample.CustomButton), null, (global::Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null);
    /// <summary>
    /// Gets Number for the <paramref name = "bindable"/> child element.
    /// </summary>
    public static int? GetNumber(global::Microsoft.Maui.Controls.BindableObject bindable) => (int? )bindable.GetValue(NumberProperty);
    /// <summary>
    /// Sets Number for the <paramref name = "bindable"/> child element.
    /// </summary>
    public static void SetNumber(global::Microsoft.Maui.Controls.BindableObject bindable, int? value) => bindable.SetValue(NumberProperty, value);
}

Setting Default Value

To set the default value for [AttachedBindableProperty<T>], assign a compile-time constant to the attribute's DefaultValue property:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", DefaultValue = 10)]
public partial class CustomButton : Button
{
}

Setting Default BindingMode

To set the default BindingMode for [BindableProperty], assign the value to the attribute's DefaultBindingMode property:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", DefaultBindingMode = BindingMode.TwoWay)]
public partial class CustomButton : Button
{
}

Using ValidateValue

To leverage the ValidateValueDelegate, use the nameof() operator to assign the name of the validation method to the the attribute's ValidateValueMethodName property:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", ValidateValueMethodName = nameof(ValidateNumber))]
public partial class CustomButton : Button
{
  static bool ValidateNumber(BindableObject bindable, object value)
  {
      var updatedValue = (int)value;
      return updatedValue > 0;
  }
}
Important: The method used for ValidateValueMethodName must adhere to the ValidateValueDelegate method signature: static bool MethodName(BindableObject, object)

Using PropertyChanging

To leverage the BindingPropertyChangingDelegate, use the nameof() operator to assign the name of the property changing method to the the attribute's PropertyChangingMethodName property:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", PropertyChangingMethodName = nameof(OnNumberChanging))]
public partial class CustomButton : Button
{
  static void OnNumberChanging(BindableObject bindable, object oldValue, object newValue)
  {
    Trace.WriteLine($"Old Value: {oldValue}");
    Trace.WriteLine($"New Value: {newValue}");
  }
}
Important: The method used for PropertyChangingMethodName must adhere to the BindingPropertyChangingDelegate method signature: static void MethodName(BindableObject, object, object)

Using PropertyChanged

To leverage the BindingPropertyChangedDelegate, use the nameof() operator to assign the name of the property changed method to the the attribute's PropertyChangedMethodName property:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", PropertyChangedMethodName = nameof(OnNumberChanged))]
public partial class CustomButton : Button
{
  static void OnNumberChanged(BindableObject bindable, object oldValue, object newValue)
  {
    Trace.WriteLine($"Old Value: {oldValue}");
    Trace.WriteLine($"New Value: {newValue}");
  }
}
Important: The method used for PropertyChangedMethodName must adhere to the BindingPropertyChangedDelegate method signature: static void MethodName(BindableObject, object, object)

Using CoreceValue

To leverage the CoerceValueDelegate, use the nameof() operator to assign the name of the coerce value method to the the attribute's CoerceValueMethodName property:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", CoerceValueMethodName = nameof(CoerceNumber))]
public partial class CustomButton : Button
{
  static object CoerceNumber(BindableObject bindable, object value)
  {
      var updatedValue = (int)value;
      return Math.Clamp(updatedValue, 0, int.MaxValue);
  }
}
Important: The method used for CoerceValueMethodName must adhere to the CoreceValueDelegate method signature: static object MethodName(BindableObject, object)

Using CreateDefaultValue

To leverage the CreateDefaultValueDelegate, use the nameof() operator to assign the name of the create default value method to the the attribute's DefaultValueCreatorMethodName property:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", DefaultValueCreatorMethodName = nameof(CreateDefaultNumber))]
public partial class CustomButton : Button
{
    static object CreateDefaultNumber() => 10;
}
Important: The method used for DefaultValueCreatorMethodName must adhere to the CreateDefaultValueDelegate method signature: static object MethodName()

Modifying Accessibility

By default, [AttachedBindableProperty<T>] will generate a public Bindable Property, a public Getter method and a public Setter method . To modify the accessibility of the Attached Property, set the value of the attribute's BindablePropertyAccessibility, GetterAccessibility and SetterAccessibility properties:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", BindablePropertyAccessibility = AccessModifier.Internal, GetterAccessibility = AccessModifier.Internal, SetterAccessibility = AccessModifier.Internal)]
public partial class CustomButton : Button
{
}

This example will generate the following code:

public partial class CustomButton
{
    /// <summary>
    /// Attached BindableProperty for the Number property.
    /// </summary>
    internal static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Number", typeof(int), typeof(BindablePropertySourceGeneratorSample.CustomButton), null, (global::Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null);
    /// <summary>
    /// Gets Number for the <paramref name = "bindable"/> child element.
    /// </summary>
    internal static int GetNumber(global::Microsoft.Maui.Controls.BindableObject bindable) => (int)bindable.GetValue(NumberProperty);
    /// <summary>
    /// Sets Number for the <paramref name = "bindable"/> child element.
    /// </summary>
    internal static void SetNumber(global::Microsoft.Maui.Controls.BindableObject bindable, int value) => bindable.SetValue(NumberProperty, value);
}

Creating a Readonly Attached Property

The [AttachedBindableProperty<T>] attribute will automatically generate a Readonly Attached Bindable Property (eg BindableProperty.CreateAttachedReadOnly()) when the SetterAccessibility value is set to AccessModifier.Protected, AccessModifier.PrivateProtected or AccessModifier.Private.

For example, a Readonly Attached Property will be generated for the following:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", SetterAccessibility = AccessModifier.Protected)]
[CommunityToolkit.Maui.AttachedBindableProperty<char>("Letter", SetterAccessibility = AccessModifier.PrivateProtected)]
[CommunityToolkit.Maui.AttachedBindableProperty<string>("Text", SetterAccessibility = AccessModifier.Private)]
public partial class CustomButton : Button
{
}

Modifying XML Documentation

By default, [AttachedBindableProperty<T>] will generate the boiler-plate XML Documentation seen in examples above. To customize the XML Documentation generated, pass in the XML Documentation as a string to the attribute's BindablePropertyXmlDocumentation property, GetterMethodXmlDocumentation property and SetterMethodXmlDocumentation property accordingly:

[CommunityToolkit.Maui.AttachedBindableProperty<int>("Number", BindablePropertyXmlDocumentation = NumberBindablePropertyXmlDocumentation, GetterMethodXmlDocumentation = NumberGetterXmlDocumentation, SetterMethodXmlDocumentation = NumberSetterXmlDocumentation)]
public partial class CustomButton : Button
{
    const string NumberBindablePropertyXmlDocumentation =
        """
        ///<Summary>The Bindable Property Number for a <see cref="CustomButton"/></Summary> 
        """;

    const string NumberGetterXmlDocumentation =
        """
        ///<Summary>Sets the value for a <see cref="CustomButton"/></Summary>
        """;

    const string NumberSetterXmlDocumentation =
        """
        ///<Summary>Gets the value for a <see cref="CustomButton"/></Summary>
        ///<Remarks>Number cannot be less than zero</Remarks> 
        """;
}
Important: The string used for the XML Documentation must contain both ///

Conclusion

I hope you enjoy using these new source generators! They were a ton of work to create, but I'm excited to see these bring a giant productivity boost to all developers in the .NET MAUI Community.

Please give us your feedback by opening a Discussion on the .NET MAUI Community Toolkit GitHub repository.

To see more examples of `[BindableProperty] and [AttachedBindableProperty<T>], check out the source code for CommunityToolkit.Maui to see how we're now using it to generate all of the Bindable Properties in our library.