I'm trying to have a custom WPF control whose content is defined in XAML but avoiding the use of a UserControl
. This is because a UserControl makes it impossible to add x:Name
to its content: https://stackoverflow.com/questions/751325/how-to-create-a-wpf-usercontrol-with-named-content/3413382
My files are laid out like this:
.
│ App.config
│ App.xaml
│ App.xaml.cs
│ MainWindow.xaml
│ MainWindow.xaml.cs
│ WpfScratch.csproj
│
├───bin
│ ├───Debug
│ │ │ WpfScratch.exe
│ │ │ WpfScratch.exe.config
│ │ │ WpfScratch.pdb
│ │ │
│ │ └───Controls
│ └───Release
├───Controls
. MyPanel.cs
. MyPanel.xaml
and the source of the relevant ones is:
MyPanel.cs
public class MyPanel : Control
{
static MyPanel()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyPanel), new FrameworkPropertyMetadata(typeof(MyPanel)));
}
public MyPanel()
{
var templateUri = new Uri("/WpfScratch;component/Controls/MyPanel.xaml", UriKind.Relative);
Template = (ControlTemplate) Application.LoadComponent(templateUri);
}
public static readonly DependencyProperty BodyProperty = DependencyProperty.Register("Body", typeof(object), typeof(MyPanel), new PropertyMetadata(default(object)));
public object Body
{
get => GetValue(BodyProperty);
set => SetValue(BodyProperty, value);
}
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register("Footer", typeof(object), typeof(MyPanel), new PropertyMetadata(default(object)));
public object Footer
{
get => GetValue(FooterProperty);
set => SetValue(FooterProperty, value);
}
}
MyPanel.xaml
<ControlTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfScratch.Controls"
TargetType="local:MyPanel" >
<DockPanel LastChildFill="True" x:Name="Content">
<Border BorderThickness="2"
BorderBrush="Aqua"
DockPanel.Dock="Bottom">
<ContentPresenter x:Name="Footer"
Content="{Binding Footer, RelativeSource={RelativeSource TemplatedParent}}"
Margin="0, 10" />
</Border>
<Border BorderThickness="2"
BorderBrush="Magenta">
<ContentPresenter x:Name="Body"
Content="{Binding Body, RelativeSource={RelativeSource TemplatedParent}}" />
</Border>
</DockPanel>
</ControlTemplate>
MainWindow.xaml
<Window x:Class="WpfScratch.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfScratch"
xmlns:controls="clr-namespace:WpfScratch.Controls"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<controls:MyPanel x:Name="HelloWorldPanel">
<controls:MyPanel.Body>
<Label x:Name="HelloLabel">Hello!</Label>
</controls:MyPanel.Body>
<controls:MyPanel.Footer>
<Label x:Name="WorldLabel">World!</Label>
</controls:MyPanel.Footer>
</controls:MyPanel>
</Window>
My main concern is the loading of the XAML file, i.e.:
var templateUri = new Uri("/WpfScratch;component/Controls/MyPanel.xaml", UriKind.Relative);
Template = (ControlTemplate) Application.LoadComponent(templateUri);
- Is
Application.Load()
the way to go? UWP seems to deprecate it in favour ofXAMLReader.Load()
, but I'm not sure how I would get the correct stream for that. I tried digging into the sources of Application.LoadComponent and there's some logic involved with processing pack URIs - The URI is a hardcoded mouthful and seems not very resilient to refactoring. Is there a terse way of saying "hey my XAML file is right next to this one"?
-
\$\begingroup\$ I can't see where the name is being used. Am I overlooking something? Is it used in code which you haven't included in the question? \$\endgroup\$Peter Taylor– Peter Taylor2018年07月30日 11:01:13 +00:00Commented Jul 30, 2018 at 11:01
-
\$\begingroup\$ @Peter - which name? \$\endgroup\$millimoose– millimoose2018年07月30日 11:03:59 +00:00Commented Jul 30, 2018 at 11:03
-
\$\begingroup\$ Any of them. You say that the reason for avoiding UserControl is that it prevents you using names, but I can't see why any of the names in the code is actually necessary. All of the binding seems to be to properties rather than elements. \$\endgroup\$Peter Taylor– Peter Taylor2018年07月30日 11:06:20 +00:00Commented Jul 30, 2018 at 11:06
-
\$\begingroup\$ @PeterTaylor - code clarity and being able to navigate the visual tree easier when debugging \$\endgroup\$millimoose– millimoose2018年07月30日 11:07:09 +00:00Commented Jul 30, 2018 at 11:07
-
\$\begingroup\$ And yes, obviously it would be useful if I needed to identify the control in codebehind, but that’s not my use case yet and I was motivated by the previous reasons to look into this, since it’s a vexing restriction with a confusing error message \$\endgroup\$millimoose– millimoose2018年07月30日 11:08:51 +00:00Commented Jul 30, 2018 at 11:08
1 Answer 1
The conventional way of attaching default template to custom control is to specify default style inside a special resource dictionary. For this approach to work three conditions should be met:
1) Resource dictionary should be located at Themes/Generic.xaml
. There you should place a default style for your custom control (that in turn should set Template
property to default value).
2) Your AssemblyInfo.cs
file should contain ThemeInfoAttribute
.
3) Your custom control should override default style key inside a static constructor, using DefaultStyleKeyProperty.OverrideMetadata
method.
Alternatively, if you are not planning on reusing your custom control, you can just drop the default style into App.xaml
resources and avoid above shenanigans.
For more info see: https://michaelscodingspot.com/2016/12/24/explicit-implicit-and-default-styles-in-wpf/
-
\$\begingroup\$ Will this work is this control is in a subproject that’s referenced by the main app’s project in a solution? That is, is the theme tied to the assembly rather than the app itself? \$\endgroup\$millimoose– millimoose2018年07月30日 11:06:03 +00:00Commented Jul 30, 2018 at 11:06
-
2\$\begingroup\$ @millimoose, the three points are for using it as a subproject. The line beginning "Alternatively" is the simpler approach which suffices if you just want to tie the theme to the app. \$\endgroup\$Peter Taylor– Peter Taylor2018年07月30日 11:07:30 +00:00Commented Jul 30, 2018 at 11:07
-
\$\begingroup\$ @millimoose, yes, first approach will work "out of the box": you will be able to use your custom controls the same way you use default ones - just by referencing them in your xaml. Second approach will technically work as well, but you would have to manually specify path to resource dictionary inside
App.xaml
every time you create a new application that depends on your sub-project. For that reason I wouldn't recommend using it across different assemblies. \$\endgroup\$Nikita B– Nikita B2018年07月30日 11:17:03 +00:00Commented Jul 30, 2018 at 11:17