How to make a UI-MarkupExtension
Asked Answered
D

2

7

I have a simple UIElement that I would like to turn into a MarkupExtension:

[MarkupExtensionReturnType(typeof(FrameworkElement))]
public class PinkRectangle : MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    { 
        return new Rectangle {Height = 100, Width = 300, Fill = Brushes.HotPink };
    }
}

It works really well in most cases. The only exception is in lists:

<local:WindowEx x:Class="WpfApp1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.winfx/200x/xaml"
    xmlns:local="clr-namespace:WpfApp1"
    DataContext="{Binding RelativeSource={RelativeSource Self}}"
    MyProperty="{Binding local:PinkRectangle}"> <!--this one works.-->
    <local:WindowsEx.MyList>
        <!--<Grid/> If I comment this line in, it works-->
        <local:PinkRectangle/>
    </local:WindowsEx.MyList>

    <ContentPresenter Content="{Binding MyProperty}"/>
</local:WindowEx>

In Collection Syntax, it says:

If the type of a property is a collection, then the inferred collection type does not need to be specified in the markup as an object element. Instead, the elements that are intended to become the items in the collection are specified as one or more child elements of the property element. Each such item is evaluated to an object during loading and added to the collection by calling the Add method of the implied collection.

However, xaml interprets the syntax above as MyList = PinkRectangle rather than MyList.Add(PinkRectangle) But if I put in a Grid first, it calls MyList.Add() for both correctly. What is the correct syntax for telling xaml to call MyList.Add() for both situations?

Here's the rest of the code to create a Minimal, Reproducable Example:

namespace WpfApp1
{
    // I use this class to directly set a few unusual properties directly in xaml.
    public class WindowEx : Window
    {
        //If I remove the set property, the error goes away, but I need the setter.
        public ObservableCollection<object> MyList {get; set; } = new ObservableCollection();

        public object MyProperty
        {
            get { return GetValue(MyPropertyProperty); }
            set { SetValue(MyPropertyProperty, value); }
        }
        public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register(nameof(MyProperty), typeof(object), typeof(MainWindow), new PropertyMetaData(0));
     }

    public partial class MainWindow : WindowEx
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

- Edit -

I found that if I removed the set{ } from MyList, the problem went away because xaml no longer thought there was a setter, but ultimately I need to be able to set MyList....

Decarbonize answered 19/7, 2019 at 20:51 Comment(3)
This is a little bit strange. Can you elaborate why you need that List<object> inside of a Window (especially having a public setter) and why you want to fill it with UI elements? All this seems to be bad design from my point of view.Erichericha
Should you take a look with ILSpy how this similar behavior is implemented in lets say a Grid? Seems there is some IAddChild interface that handles this, possibly.Bethink
@Erichericha - This is just a simplified example. In my project, I have a Custom UserControl that is essentially comprised of Listboxes - the UserControl defines how they look and work together. I want to be able to set what is in these lists from outside the actual control.Decarbonize
C
2

Poor XAML parser is just really confused about all this...:O) Help it by eliminating ambiguity : instantiate MyList explicitly in your XAML.

enter image description here

XAML:

<local:UserControlEx x:Class="WpfApp14.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp14"
             DataContext="{Binding RelativeSource={RelativeSource Self}}"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="450">

    <local:UserControlEx.MyList>
        <local:ObjectCollection>
            <local:CoolBlueRectangle/>
            <local:CoolBlueRectangle/>
            <local:CoolBlueRectangle/>
            <local:CoolBlueRectangle/>
            <local:CoolBlueRectangle/>
        </local:ObjectCollection>
    </local:UserControlEx.MyList>

    <Grid>
        <ItemsControl HorizontalAlignment="Left" 
                      ItemsSource="{Binding MyList}"/>
    </Grid>

</local:UserControlEx>

Where,

public class ObjectCollection : ObservableCollection<object>
{
}

BTW, the naming convention is that your markup class definition should use the Extension suffix.

public class CoolBlueRectangleExtension : MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
    }
}
Clerihew answered 30/7, 2019 at 22:32 Comment(0)
E
0

If MyProperty will only be initialized in XAML and you'll never need or want to bind it, you can do this more simply, and without having to clutter up the XAML with the collection type. To do this with an attached property, you would store the actual collection reference in a dependency property private to the static extension class, with the dependency property name decorated with a leading underscore or something. In that case, you'd have to initialize the collection in GetMyProperty(), naturally: Just check if the private dependency property is null for the target object, and initialize as needed.

Note that GetMyProperty must be static. The naming convention is that the 'Get' prefix must be present, and the rest of the method name is the "property" name.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    public static StringCollection GetMyProperty(MainWindow wnd)
    {
        return wnd._myProperty;
    }

    private StringCollection _myProperty = new StringCollection();
}

public class StringCollection : ObservableCollection<String>
{
}
<local:MainWindow.MyProperty>
    <sys:String>Foo</sys:String>
</local:MainWindow.MyProperty>

Ettore answered 31/7, 2019 at 3:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.