How to create a WPF UserControl with NAMED content

Viewed 45389

I have a set of controls with attached commands and logic that are constantly reused in the same way. I decided to create a user control that holds all the common controls and logic.

However I also need the control to be able to hold content that can be named. I tried the following:

<UserControl.ContentTemplate>
    <DataTemplate>
        <Button>a reused button</Button>
        <ContentPresenter Content="{TemplateBinding Content}"/>
        <Button>a reused button</Button>
    </DataTemplate>
</UserControl.ContentTemplate>

However it seems any content placed inside the user control cannot be named. For example if I use the control in the following way:

<lib:UserControl1>
     <Button Name="buttonName">content</Button>
</lib:UserControl1>

I receive the following error:

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

If I remove the buttonName, then it compiles, however I need to be able to name the content. How can I achieve this?

10 Answers

It seems this is not possible when XAML is used. Custom controls seem to be a overkill when I actually have all the controls I need, but just need to group them together with a small bit of logic and allow named content.

The solution on JD's blog as mackenir suggests, seems to have the best compromise. A way to extend JD's solution to allow controls to still be defined in XAML could be as follows:

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

        var grid = new Grid();
        var content = new ContentPresenter
                          {
                              Content = Content
                          };

        var userControl = new UserControlDefinedInXAML();
        userControl.aStackPanel.Children.Add(content);

        grid.Children.Add(userControl);
        Content = grid;           
    }

In my example above I have created a user control called UserControlDefinedInXAML which is define like any normal user controls using XAML. In my UserControlDefinedInXAML I have a StackPanel called aStackPanel within which I want my named content to appear.

I ran into this problem and found a workaround that lets you design custom controls using Xaml. Its still has a bit of a hack, but one that solved all of my problems without any obvious compromises.

Basically, you do everything the way you normally would with the xaml, but you also include some of the header declarations on the control template itself and Base64 encode that template to be loaded in the code constructor. Not shown in this Xaml excerpt, but the namespace my full Xaml used is actually targeting a XamlTemplates instead of the Controls namespace. This was on purpose because the "Release" build moves that developmental Debug reference out of the way from my production controls namespace. More on that below.

<ControlTemplate TargetType="{x:Type TabControl}" 
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid x:Name="templateRoot" 
          ClipToBounds="True" 
          SnapsToDevicePixels="True" 
          Background="Transparent"
          KeyboardNavigation.TabNavigation="Local">
        <Grid.ColumnDefinitions>
            <ColumnDefinition x:Name="ColumnDefinition0"/>
            <ColumnDefinition x:Name="ColumnDefinition1" Width="0"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition x:Name="RowDefinition0" Height="Auto"/>
            <RowDefinition x:Name="RowDefinition1" Height="*"/>
        </Grid.RowDefinitions>
        <TabPanel x:Name="HeaderPanel"
                  Panel.ZIndex="1"                          
                  Margin="{Binding MarginHeaderPanel, RelativeSource={RelativeSource AncestorType=TabControl}}"
                  Background="{Binding Background, RelativeSource={RelativeSource AncestorType=TabControl}}"
                  IsItemsHost="True"                          
                  KeyboardNavigation.TabIndex="2"/>
        <Border x:Name="blankregion" Panel.ZIndex="1" Margin="0" Padding="0" 
                Background="{Binding Background, RelativeSource={RelativeSource AncestorType=TabControl}}">
            <ContentPresenter x:Name="blankpresenter"                                      
                              KeyboardNavigation.TabIndex="1"    
                              Content="{Binding TabBlankSpaceContent, RelativeSource={RelativeSource AncestorType=TabControl}}"                                          
                              ContentSource="TabBlankSpaceContent" 
                              SnapsToDevicePixels="True"/>
        </Border>

        <Grid x:Name="ContentPanel">
            <Border 
                BorderBrush="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=TabControl}}"
                BorderThickness="{Binding BorderThickness, RelativeSource={RelativeSource AncestorType=TabControl}}"                       
                Background="{Binding SelectedItem.Background, RelativeSource={RelativeSource AncestorType=TabControl}}"
                KeyboardNavigation.DirectionalNavigation="Contained" 
                KeyboardNavigation.TabNavigation="Local"                                           
                CornerRadius="{Binding BorderRadius, RelativeSource={RelativeSource AncestorType=TabControl}}"
                KeyboardNavigation.TabIndex="3">
                <ContentControl x:Name="PART_SelectedContentHost" 
                                ContentTemplate="{Binding SelectedContentTemplate, RelativeSource={RelativeSource AncestorType=TabControl}}"
                                Content="{Binding SelectedContent, RelativeSource={RelativeSource AncestorType=TabControl}}"
                                ContentStringFormat="{Binding SelectedContentStringFormat, RelativeSource={RelativeSource AncestorType=TabControl}}" 
                                Margin="{Binding Padding, RelativeSource={RelativeSource AncestorType=TabControl}}"
                                SnapsToDevicePixels="{Binding SnapsToDevicePixels, RelativeSource={RelativeSource AncestorType=TabControl}}"/>
            </Border>

        </Grid>
    </Grid>
    <ControlTemplate.Triggers>
        <!--Triggers were removed for clarity-->
    </ControlTemplate.Triggers>
</ControlTemplate>

I'll point out that the above XAML didn't name the control it derived from and everything within the template used relative lookups to bind its properties; even the custom ones.

On the C# side, I used the Base64 encoded version of the control template from my Xaml and directives to shuffle around the development/release versions of the controls. The theory being that my controls in the development space wouldn't run into the problem this topic is about, but would give me a way to test/develop them. The release DLL versions seem to be working really well and the controls built do have great design time support just like they did on the Debug/Development side.

#if DEBUG
namespace AgileBIM.Controls
{
    public class AgileTabControl : AgileBIM.XamlTemplates.AgileTabControlDesigner { }
}

namespace AgileBIM.XamlTemplates
#else
namespace AgileBIM.Controls
#endif
{
#if DEBUG    
    public partial class AgileTabControlDesigner : TabControl
#else
    public class AgileTabControl : TabControl
#endif
    {

        

#if DEBUG
        private static Type ThisControl = typeof(AgileTabControlDesigner);
#else
        private static Type ThisControl = typeof(AgileTabControl);
        private string Template64 = "Base64 encoded template removed for clarity"
#endif


#if DEBUG
        public AgileTabControlDesigner() { InitializeComponent(); }
#else
        public AgileTabControl()
        {
            string decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(Template64));
            System.IO.StringReader sr = new System.IO.StringReader(decoded);
            System.Xml.XmlReader xr = System.Xml.XmlReader.Create(sr);
            ControlTemplate ct = (ControlTemplate)System.Windows.Markup.XamlReader.Load(xr);

            DefaultStyleKey = ThisControl;
            Template = ct;
        }
#endif

        public Thickness MarginHeaderPanel 
        {
            get { return (Thickness)GetValue(MarginHeaderPanelProperty); } 
            set { SetValue(MarginHeaderPanelProperty, value); } 
        }
        public static readonly DependencyProperty MarginHeaderPanelProperty =
            DependencyProperty.Register("MarginHeaderPanel", typeof(Thickness), ThisControl, new PropertyMetadata(new Thickness(0)));

        public CornerRadius BorderRadius 
        { 
            get { return (CornerRadius)GetValue(BorderRadiusProperty); } 
            set { SetValue(BorderRadiusProperty, value); }
        }
        public static readonly DependencyProperty BorderRadiusProperty =
            DependencyProperty.Register("BorderRadius", typeof(CornerRadius), ThisControl, new PropertyMetadata(new CornerRadius(0)));

        public object TabBlankSpaceContent 
        { 
            get { return (object)GetValue(TabBlankSpaceContentProperty); } 
            set { SetValue(TabBlankSpaceContentProperty, value); } 
        }
        public static readonly DependencyProperty TabBlankSpaceContentProperty =
            DependencyProperty.Register("TabBlankSpaceContent", typeof(object), ThisControl, new PropertyMetadata());
    }
}

The critical thing to remember before creating a "release" control DLL to be used in your primary application is to update your base64 encoded string with your latest and greatest version of its control template. This is because the Release build is completely detached from the original Xaml and entirely dependent on the encoded one.

The above control and others like it can be found on GitHub. Which is a library I am making intended to "unlock" many of the things I want to style that standard controls don't expose. That and adding some features that don't exist. For example, the above TabControl has an additional content property for utilizing the "unused" area of the tab headers.

Important Notes:

  • Basic styling gets lost using this method, but you get it all back if your styles for the Custom Control uses the BasedOn="{StaticResource {x:Type TabControl}}" mechanism.
  • I need to find time to research if this will cause any noteworthy memory leaks and whether I can do anything to combat them, if anyone has any thoughts on this let me know in the comments.
Related