Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allowing named elements in a multi-content custom control

Requirement

Let's start with what I am trying to achieve. I want to have a grid with 2 columns and a grid splitter (there is a little more to it that that, but let's keep it simple). I want to be able to use this grid in a lot of different places, so instead of creating it each time I want to make a custom control that contain two ContentPresenters.

The end goal is effectively to be able to write XAML like this:

<MyControls:MyGrid>
    <MyControls:MyGrid.Left>
        <Label x:Name="MyLabel">Something unimportant</Label>
    </MyControls:MyGrid.Left>
    <MyControls:MyGrid.Right>
        <Label>Whatever</Label>
    </MyControls:MyGrid.Right>
</MyControls:MyGrid>

IMPORTANT: Notice that I want to apply a Name to my Label element.

Attempt 1

I did a lot of searching for solutions, and the best way I found was to create a UserControl along with a XAML file that defined my grid. This XAML file contained the 2 ContentPresenter elements, and with the magic of binding I was able to get something working which was great. However, the problem with that approach is not being able to Name the nested controls, which results in the following build error:

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

With that error in hand, I went back to Dr. Google...

Attempt 2 (current)

After a lot more searching I found some information here on SO that suggested the problem was due to having an associated XAML file with the MyGrid class, and the problem should be solvable by removing the XAML and creating all the controls via code in the OnInitialized method.

So I headed off down that path and got it all coded and compiling. The good news is that I can now add a Name to my nested Label control, the bad news is nothing renders! Not in design mode, and not when running the application. No errors are thrown either.

So, my question is: What am I missing? What am I doing wrong?

I am also open to suggestions for other ways to meet my requirements.

Current code

public class MyGrid : UserControl
{
    public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(object), typeof(MyGrid), new PropertyMetadata(null));
    public object Left
    {
        get { return (object)GetValue(LeftProperty); }
        set { SetValue(LeftProperty, value); }
    }

    public static readonly DependencyProperty RightProperty = DependencyProperty.Register("Right", typeof(object), typeof(MyGrid), new PropertyMetadata(null));
    public object Right
    {
        get { return (object)GetValue(RightProperty); }
        set { SetValue(RightProperty, value); }
    }

    Grid MainGrid;

    static MyGrid()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyGrid), new FrameworkPropertyMetadata(typeof(MyGrid)));
    }

    protected override void OnInitialized(EventArgs e)
    {
        base.OnInitialized(e);

        //Create control elements
        MainGrid = new Grid();

        //add column definitions
        ColumnDefinition leftColumn = new ColumnDefinition()
        {
            Name = "LeftColumn",
            Width = new GridLength(300)
        };
        MainGrid.ColumnDefinitions.Add(leftColumn);

        MainGrid.ColumnDefinitions.Add(new ColumnDefinition()
        {
            Width = GridLength.Auto
        });

        //add grids and splitter
        Grid leftGrid = new Grid();
        Grid.SetColumn(leftGrid, 0);
        MainGrid.Children.Add(leftGrid);

        GridSplitter splitter = new GridSplitter()
        {
            Name = "Splitter",
            Width = 5,
            BorderBrush = new SolidColorBrush(Color.FromArgb(255, 170, 170, 170)),
            BorderThickness = new Thickness(1, 0, 1, 0)
        };
        MainGrid.Children.Add(splitter);

        Grid rightGrid = new Grid();
        Grid.SetColumn(rightGrid, 1);
        MainGrid.Children.Add(rightGrid);

        //add content presenters
        ContentPresenter leftContent = new ContentPresenter();
        leftContent.SetBinding(ContentPresenter.ContentProperty, new Binding("Left") { Source = this });
        leftGrid.Children.Add(leftContent);

        ContentPresenter rightContent = new ContentPresenter();
        rightContent.SetBinding(ContentPresenter.ContentProperty, new Binding("Right") { Source = this });
        rightGrid.Children.Add(rightContent);

        //Set this content of this user control
        this.Content = MainGrid;
    }
}
like image 627
musefan Avatar asked Oct 22 '25 13:10

musefan


1 Answers

After some discussion via comments, it quickly became clear that neither of my attempted solutions was the correct way to go about it. So I set out on a third adventure hoping this one would be the final solution... and it seems it is!


Disclaimer: I do not yet have enough experience with WPF to confidently say that my solution is the best and/or recommended way to do this, only that it definitely works.


First of all create a new custom control: "Add" > "New Item" > "Custom Control (WPF)". This will create a new class that inherits from Control.

In here we put our dependency properties for bind to out content presenters:

public class MyGrid : Control
{
    public static readonly DependencyProperty LeftProperty = DependencyProperty.Register("Left", typeof(object), typeof(MyGrid), new PropertyMetadata(null));
    public object Left
    {
        get { return (object)GetValue(LeftProperty); }
        set { SetValue(LeftProperty, value); }
    }

    public static readonly DependencyProperty RightProperty = DependencyProperty.Register("Right", typeof(object), typeof(MyGrid), new PropertyMetadata(null));
    public object Right
    {
        get { return (object)GetValue(RightProperty); }
        set { SetValue(RightProperty, value); }
    }

    static MyGrid()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyGrid), new FrameworkPropertyMetadata(typeof(MyGrid)));
    }
}

When you add this class file in Visual Studio, it will automatically create a new "Generic.xaml" file in the project containing a Style for this control, along with a Control Template within that style - this is where we define our control elements...

<Style TargetType="{x:Type MyControls:MyGrid}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type MyControls:MyGrid}">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="500" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Grid Grid.Column="0">
                        <ContentPresenter x:Name="LeftContent" />
                    </Grid>
                    <GridSplitter Width="5" BorderBrush="#FFAAAAAA" BorderThickness="1,0,1,0">
                    </GridSplitter>
                    <Grid Grid.Column="1">
                        <ContentPresenter x:Name="RightContent" />
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The final step is to hook up the bindings for the 2 content presenters, so back to the class file.

Add the following override method to the MyGrid class:

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    //Apply bindings and events
    ContentPresenter leftContent = GetTemplateChild("LeftContent") as ContentPresenter;
    leftContent.SetBinding(ContentPresenter.ContentProperty, new Binding("Left") { Source = this });

    ContentPresenter rightContent = GetTemplateChild("RightContent") as ContentPresenter;
    rightContent.SetBinding(ContentPresenter.ContentProperty, new Binding("Right") { Source = this });
}

And that's it! The control can now be used in other XAML code like so:

<MyControls:MyGrid>
    <MyControls:MyGrid.Left>
        <Label x:Name="MyLabel">Something unimportant</Label>
    </MyControls:MyGrid.Left>
    <MyControls:MyGrid.Right>
        <Label>Whatever</Label>
    </MyControls:MyGrid.Right>
</MyControls:MyGrid>

Thanks to @NovitchiS for your input, your suggestions were vital in getting this approach to work

like image 102
musefan Avatar answered Oct 24 '25 03:10

musefan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!