30

I'm reading Avalonia source code and I came across this sentence:

return new MenuFlyoutPresenter
{
    [!ItemsControl.ItemsProperty] = this[!ItemsProperty],
    [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty]
};

I've never seen a syntax like that. What does those bracket do if there is no indexed property or this[] accessor?, and why are they negated with the exclamation mark if the property they are referring to is not a bool?, maybe some kind of null-check?

The code itself is contained in the following cs file:

https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/Flyouts/MenuFlyout.cs

I've tracked the code but I was unable to understand what that syntax does.

user3840170
  • 26,597
  • 4
  • 30
  • 62
  • 1
    It's a [collection initializer](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#collection-initializers) – canton7 Jan 28 '22 at 13:28
  • But collection initializers have curly braces don't they?, also, why are they negated? – Aleksander Stukov Jan 28 '22 at 13:30
  • 3
    No they don't, please read the section I linked to – canton7 Jan 28 '22 at 13:31
  • 2
    @AleksanderStukov: Collection initializers come in two broad categories: for lists and for dictionaries. This is the syntax used with dictionaries. Also, the UED shouldn't have messed with Raynor. – Ben Voigt Jan 28 '22 at 23:14

2 Answers2

35

There are a couple of things going on here.

First, the syntax:

var menu = new MenuFlyoutPresenter
{
    [key] = value,
};

Is a collection initializer, and is shorthand for:

var menu = new MenuFlyoutPresenter();
menu[key] = value;

That indexer is defined here as:

public IBinding this[IndexerDescriptor binding]
{
    get { return new IndexerBinding(this, binding.Property!, binding.Mode); }
    set { this.Bind(binding.Property!, value); }
}

So the key there is an IndexerDescriptor, and the value is an IBinding.

So, what's going on with this thing?

!ItemsControl.ItemsProperty

We can see from your link that ItemsProperty is a DirectProperty<TOwner, TValue>, and that ultimately implements the ! operator here:

public static IndexerDescriptor operator !(AvaloniaProperty property)
{
    return new IndexerDescriptor
    {
        Priority = BindingPriority.LocalValue,
        Property = property,
    };
}

Avalonia seems to like overloading operators such as ! and ~ to do things you might not expect (and would normally use a method for). In this case, they use ! on an AvaloniaProperty as a shorthand for accessing that property's binding.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • As far as I understand the collection initializer will be transformed into series of `.Add()` calls. The indexer is a property, so the the *first* must be a an *object initializer*, not collection initializer. – Sinatr Jul 18 '23 at 08:43
  • @Sinatr See my first link please. The syntax `new Foo() { { .., .. } }`, would be transformed into a series of `Add` calls, but the syntax `new Foo() { [..] = .. }` is transformed into a series of calls to the indexer. The documentation page I linked to documents this under "Collection initializers" – canton7 Jul 18 '23 at 08:48
  • Although, it's true that the spec document this under object initializers. And the type doesn't need to implement `IEnumerable`, as it does for collection initializers. Eh – canton7 Jul 18 '23 at 08:57
11

A relatively simple class that demonstrates a way of allowing this syntax is:

public sealed class Demo
{
    public Demo this[Demo index] // Indexer
    {
        get => !index;
        set {} // Not needed to demonstrate syntax. 
    }

    public static Demo operator !(Demo item) => item;

    public Demo ItemsProperty        => _empty;
    public Demo ItemTemplateProperty => _empty;

    public Demo SomeMethod(Demo ItemsControl)
    {
        return new Demo
        {
            [!ItemsControl.ItemsProperty] = this[!ItemsProperty],
            [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty],
        };
    }

    static Demo _empty = new();
}

Some things to note:

  • Demo implements operator! which allows the ! operator to be used on values of that type (e.g. the !ItemsProperty in the SomeMethod() initialisation).
  • Demo implements an indexer which is what allows the use of the indexing (via this) on the right-hand side of the collection initialiser.
  • The indexer also enables the use of the [x] = y collection initialisation syntax used in SomeMethod().

It's the combination of the operator()! operator along with the this[Demo] indexer that enables the syntax.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • that ! operator use is totall not instinctive. (I know not your idea). '!' to me means 'not' so `!Foo` means 'not foo'. Yet here it means foo. Why is it needed at all? – pm100 Jan 28 '22 at 22:39
  • @pm100 The code in my answer is just an example to show how the syntax works - obviously in real code `operator!()` should be implemented to actually do something. However, I personally think the Avalonia code is abusing these operators and making the code very unintuitive and hard to read. – Matthew Watson Jan 29 '22 at 09:46