Content-Unterstützung via XAML für UserControls

UserControls sind modulare bzw. fertige (Programm)-Komponenten die nach außen gekapselt sind. Manchmal währe es praktisch beim Verwenden eines UserControls, neben den üblichen Properties, den Inhalt direkt im XAML-Code zu definieren und einzubetten.

Ich habe zur Demonstration ein UserControl erstellt, welches ein Grid mit blauen Hintergrund enthält. In diesem Grid soll der Inhalt gezeigt werden, welcher vom Verwender definiert wird (<ContentPresenter />).

<UserControl 
    x:Class="ContentWPF.MyUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
    <Grid Background="Blue">
        <ContentPresenter />
    </Grid>
</UserControl>

Die Verwendung von dem UserControl wird wie üblich in XAML deklariert mit dem Unterschied das man diesmal direkt im XAML den Content angibt.

<local:MyUserControl>
    Hello World
</local:MyUserControl>

 Das Problem

Nun ist es so das am Ende nichts von der Definition des Controls (‚MyUserControl.xaml‘) gezeigt wird. Es wird nur noch „Hello World“ ohne Grid und ohne Blau dargestellt.

Der Grund ist, dass das Content-Property von UserControl mit dem Content des Verwenders überschrieben wird und dabei die eigentliche Definition verloren geht.

Mit diesem Wissen ist der nächste Schritt nun zu verhindern, dass das Content-Property überschrieben wird. Das machen wir einfach indem wir im CodeBehind von ‚MyUserControl‘ ein separates Content-Property anlegen.

public static readonly DependencyProperty MyContentProperty 
    = DependencyProperty.Register("MyContent", typeof(object), typeof(MyUserControl), null);
public object MyContent
{
    get { return (object)GetValue(MyContentProperty); }
    set { SetValue(MyContentProperty, value); }
}
<UserControl 
    x:Class="ContentWPF.MyUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
    <Grid Background="Blue">
        <ContentPresenter Content="{Binding Path=MyContent,
          RelativeSource={RelativeSource  AncestorType=UserControl}}" />
    </Grid>
</UserControl>

Mit dem neuen Property kann 'MyUserControl' folgenderweiße verwendet werden:

<local:MyUserControl MyContent="Hello World" />

Oder

<local:MyUserControl>
    <local:MyUserControl.MyContent>
        Hello World
    </local:MyUserControl.MyContent>
</local:MyUserControl>

(zweite Variante ist besser wenn kein Text sondern ein kompletter Control-Tree eingefügt wird).

Jetzt funktioniert es zwar, aber nur indem man beim Verwenden des Control mehr Code schreibt als nötig. Wer (so wie ich) sich daran stört unnötigen Code zu produzieren für den habe ich folgende ...

 Lösung 😀

Die Lösung ist so Simpel, allerdings wenn man es nicht weiß kann man nicht darauf kommen.

Man muss lediglich im CodeBehind von ‚MyUserControl‘ das Klassen-Attribute 'ContentPropertyAttribute' deklarieren und als Parameter das gewünschte Property als string angeben.

namespace ContentWPF
{
    [ContentProperty("MyContent")] 
    public partial class MyUserControl : UserControl
    {
        public MyUserControl()
        {
            InitializeComponent();
        }
        
        public static readonly DependencyProperty MyContentProperty
            = DependencyProperty.Register("MyContent", typeof(object), typeof(MyUserControl), null);
        public object MyContent
        {
            get { return (object)GetValue(MyContentProperty); }
            set { SetValue(MyContentProperty, value); }
        }
    }
}

Damit kann man nun im XAML-Code das Einbetten von Sub-Elementen (wie anfangs demonstriert) verwenden:

<local:MyUserControl>
    Hello World
</local:MyUserControl>

Böser Beigeschmack

Leider gibt es selbst jetzt noch einen Nachteil, den man wohl nicht umgehen kann, wenn man ein UserControl verwendet. Wenn ihr ‚MyUserControl‘ nun verwendet und einem Element darin einem Namen gebt werdet ihr vom Compiler informiert das dies nicht erlaubt ist.

<local:MyUserControl>
    <TextBlock x:Name="CompilerErrorIfItemIsNamed">Hello World</TextBlock>
</local:MyUserControl>

Folgender Fehler wird ausgegeben:

Cannot set Name attribute value 'CompilerErrorIfItemIsNamed' on element 'TextBlock'. 'TextBlock' is under the scope of element 'MyUserControl', which already had a name registered when it was defined in another scope.

Den Namen selbst kann man zwar nachträglich über CodeBehind (oder sonstwo) angeben aber zur Designzeit kann man dieses Element im CodeBehind nicht direkt ansprechen.

Wenn man damit nicht Leben kann führt kein weg an einem CustomControl vorbei. Bei diesem können eingebettete Elemente mit einem Namen versehen und vom CodeBehind direkt angesprochen werden.

Achtung: In Silverlight ist dieses Verhalten identisch, allerdings versucht euch der Compiler zu täuschen. Die Fehlermeldung wird dort nicht gezeigt und das Projekt lässt sich compilieren. Allerdings wenn Ihr zur Laufzeit im CodeBehind direkt auf das Element zugreift wird eine NullReferenceException fliegen da das Property immer leer ist.