How to create a WPF UserControl with NAMED content
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?
The answer is to not use a UserControl to do it.
Create a class that extends ContentControl
public class MyFunkyControl : ContentControl
{
public static readonly DependencyProperty HeadingProperty =
DependencyProperty.Register("Heading", typeof(string),
typeof(MyFunkyControl), new PropertyMetadata(HeadingChanged));
private static void HeadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((MyFunkyControl) d).Heading = e.NewValue as string;
}
public string Heading { get; set; }
}
then use a style to specify the contents
<Style TargetType="control:MyFunkyControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="control:MyFunkyControl">
<Grid>
<ContentControl Content="{TemplateBinding Content}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
and finally - use it
<control:MyFunkyControl Heading="Some heading!">
<Label Name="WithAName">Some cool content</Label>
</control:MyFunkyControl>
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.
Another alternative I've used is to just set the Name
property in the Loaded
event.
In my case, I had a rather complex control which I didn't want to create in the code-behind, and it looked for an optional control with a specific name for certain behavior, and since I noticed I could set the name in a DataTemplate
I figured I could do it in the Loaded
event too.
private void Button_Loaded(object sender, RoutedEventArgs e)
{
Button b = sender as Button;
b.Name = "buttonName";
}
Sometimes you might just need to reference the element from C#. Depending on the use case, you can then set an x:Uid
instead of an x:Name
and access the elements by calling a Uid finder method like Get object by its Uid in WPF.
You can use this helper for set name inside the user control:
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Media;
namespace UI.Helpers
{
public class UserControlNameHelper
{
public static string GetName(DependencyObject d)
{
return (string)d.GetValue(UserControlNameHelper.NameProperty);
}
public static void SetName(DependencyObject d, string val)
{
d.SetValue(UserControlNameHelper.NameProperty, val);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.RegisterAttached("Name",
typeof(string),
typeof(UserControlNameHelper),
new FrameworkPropertyMetadata("",
FrameworkPropertyMetadataOptions.None,
(d, e) =>
{
if (!string.IsNullOrEmpty((string)e.NewValue))
{
string[] names = e.NewValue.ToString().Split(new char[] { ',' });
if (d is FrameworkElement)
{
((FrameworkElement)d).Name = names[0];
Type t = Type.GetType(names[1]);
if (t == null)
return;
var parent = FindVisualParent(d, t);
if (parent == null)
return;
var p = parent.GetType().GetProperty(names[0], BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
p.SetValue(parent, d, null);
}
}
}));
public static DependencyObject FindVisualParent(DependencyObject child, Type t)
{
// get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
// we’ve reached the end of the tree
if (parentObject == null)
{
var p = ((FrameworkElement)child).Parent;
if (p == null)
return null;
parentObject = p;
}
// check if the parent matches the type we’re looking for
DependencyObject parent = parentObject.GetType() == t ? parentObject : null;
if (parent != null)
{
return parent;
}
else
{
// use recursion to proceed with next level
return FindVisualParent(parentObject, t);
}
}
}
}
and your Window or Control Code Behind set you control by Property:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public Button BtnOK { get; set; }
}
your window xaml:
<Window x:Class="user_Control_Name.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:test="clr-namespace:user_Control_Name"
xmlns:helper="clr-namespace:UI.Helpers" x:Name="mainWindow"
Title="MainWindow" Height="350" Width="525">
<Grid>
<test:TestUserControl>
<Button helper:UserControlNameHelper.Name="BtnOK,user_Control_Name.MainWindow"/>
</test:TestUserControl>
<TextBlock Text="{Binding ElementName=mainWindow,Path=BtnOK.Name}"/>
</Grid>
</Window>
UserControlNameHelper get your control name and your Class name for set Control to Property.