Blazor Bindings for Avalonia: Attached Properties

⏱️ TL;DR

  • I started 2 attached-property experiments
  • They may exist side-by side

Previous article:

🌍 Community Response

A week ago I released a first experimental implementation of Blazor Bindings for Avalonia. As the first prototype was working earlier than expected, I got excited and shared it with you, the community, on Twitter and Mastodon.
I was hoping that some of you would share this excitement – but I didn’t expect to get such a positive response.

Thank you, Avalonia and Blazor communities!
Thank you for sharing and caring!

🗒️ Some Work To Do

In my previous post I briefly talked about next steps. But as the "EDIT" note there shows, I quickly realized that I was a little bit too excited when I wrote it. What I held in my hands had already surpassed some hurdles, sure, but there were/are still some challenges ahead.

So let’s get into today’s topic:
Attached Properties.

🏷️ Attached Properties

In XAML, attached properties are used ubiquitously. It allows to attach properties onto controls without them needing to know about those properties in advance.

For example, this is used to define in which row and column a control should be placed inside a Grid.

For those unfamiliar with the topic, a quick introduction.

🔬 Attached Properties In XAML

Imagine we have a Grid. Inside we place a TextBlock and a Button. We also provide the amount and height/width of rows and columns. In Avalonia this is done through RowDefinitions and ColumnDefinitions.

It looks like this:

<Grid ColumnDefinitions="100,200" RowDefinitions="20,30">
  <TextBlock>TextBlock: I'm in grid cell 0,1</TextBlock>
  <Button>Button: I'm in grid cell 1,0</Button>
</Grid>

If we run this, the TextBlock and Button overlap each other in the top left Grid cell, as this is the default behavior. As seen in the displayed text contents, this is not what we wanted.

It currently looks somewhat like this:

TextBlock: I’m in grid cell 0,1 Button: I’m in grid cell 1,0  
   

But actually we want:

  • TextBlock top right
  • Button bottom left.
  TextBlock: I’m in grid cell 0,1
Button: I’m in grid cell 1,0  

How can we tell the Grid to place the TextBlock and the Button?

This is where attached properties come into play.

🏷️ Grid’s Attached Properties

Grid defines (among others) 2 attached properties

  • Row
  • Column

If we attach them to TextBlock and Button, we can instruct Grid where to place them.

<Grid ColumnDefinitions="100,200" RowDefinitions="20,30">
  <TextBlock Grid.Row="0" Grid.Column="1">I'm in grid cell 0,1</TextBlock>
  <Button Grid.Row="1" Grid.Column="0">I'm in grid cell 1,0</Button>
</Grid>

Pretty neat, right?

But the question arises:

⚠️ Question
How to do it in Blazor?

🔎 Search For A Solution

Unfortunately, Blazor has no concept for attached properties. We need to find a way to let UI developers achieve the same result.

🖐️ Catch-All Parameter

Every component in Blazor can have parameters. Parameters can pass values from the parent Blazor component to the current component.

Passing a value to eg. a Grid‘s Width can look like this:

<Grid Width="50" ...>

Normally, a Blazor component defines it’s parameters explicitly, so it’s hardcoded which parameters are supported.

But contrarily, attached properties require that the target control does not know about their existence. Is this already a dead end?

No. Blazor supports a "catch everything else" parameter. This is something we can work with.
It even supports dots, so we are able to write them like previously in XAML:

<Grid ...>
  <TextBlock Grid.Row="1" Grid.Column="0">Hi!</TextBlock>
</Grid>

This is also the default approach used by the original BlazorBindings.Maui project.

ℹ️ Note
There is also another specialized approach for Grid attached properties by using GridCells.

✅ Pros

  • Syntax just like in XAML
  • Concise syntax

❌ Cons

  • No IntelliSense
  • No compile-time checks
  • Values (internally) are just strings

As I consider loosing IntelliSense and compile-time checks a huge impediment, let’s see what other solutions can be conceived.

🔌 Extension Methods

Extension methods and attached properties have 1 important trait in common:
The thing they’re attached to doesn’t need to know about them.

How would our sample look with extension methods?

First, we need a parameter that allows us to call an extension method. Let’s call this parameter Attached. This parameter let us define a lambda function which provides the current control in its parameter and allows us to use extension methods on it. These extension methods then set the actual attached properties.

<Grid ...>
  <TextBlock Attached="@((TextBlock tb) => tb.GridRow(1).GridColumn(0))"></TextBlock>
</Grid>

✅ Pros:

  • IntelliSense works
  • Compile-time checks

❌ Cons:

  • More verbose
    • Blazor cannot infer type name so we have to provide it ourselves
  • Passing RenderFragment is not so easy (but possible)

Except for the RenderFragments, which are used for data- or control-templates, this should work pretty well.

🔬 Extra: Using RenderFragments With Extension Methods
Using RenderFragments in extension methods is possible by doing the following:

<SomeControl Attached="@((SomeControl sc) => sc.AttachedTemplate(GetTemplate()))" />
@code
{
    private RenderFragment GetTemplate() =>
        @<TextBlock>From template</TextBlock>;
}

📦 Attached Elements

In Blazor Bindings for Avalonia we can also define elements which are no "real" elements in the UI tree. We can use this to our advantage to define attached elements.

Attached elements are placed inside the element which we want to configure.

<Grid ...>
  <TextBlock>
    <Grid_Attachment Row="1" Column="0" />
  </TextBlock>
</Grid>

_Attachment is just a suffix I chose. It ensures that the base element like Grid and its attached elements have different but related names.

📄 Templates

If we switch to a more sophisticated example which allows to define a template like ToolTip, then this approach really shines:

<Grid ...>
  <TextBlock>
    <ToolTip_Attachment Placement="PlacementMode.Bottom">
      @* Tip is a parameter expecting a RenderFragment *@
      <Tip>
        <Button>This is a tool tip as a Button</Button>
      </Tip>
    </ToolTip_Attachment>
  </TextBlock>

✅ Pros:

  • IntelliSense
  • Compile-time checks
  • Passing RenderFragment is easy

❌ Cons:

  • More verbose
  • Somewhat clunky for simple cases

🤔 Decisions

Blazor does not support attached properties out-of-the-box, but we saw we can work around it.

Prior art already demonstrates how to we could do it, but I want a solution which allows us to generate most of the Blazor glue code – also attached properties.

I really like the extension method approach as it allows everyone to easily add support for new attached properties as desired. This works almost always except if there you want to use RenderFragments. Here I really like the attached element style.

So: what should we take?

Let a meme answer this tough question:
file

I will try to make both work side-by-side.

🛣️ Next Steps

The attentive repository follower maybe has already noticed that I already implemented proof-of-concepts for both. The element approach already has generator support.

The next step is to extend the generator to also support code generation for extension methods. After that we will test and see how it fares in real-world usage.

Thanks for reading!
👋

Cookie Consent with Real Cookie Banner