4
\$\begingroup\$

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);
  1. Is Application.Load() the way to go? UWP seems to deprecate it in favour of XAMLReader.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
  2. 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"?
\$\endgroup\$
  • \$\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 Jul 30 '18 at 11:01
  • \$\begingroup\$ @Peter - which name? \$\endgroup\$ – millimoose Jul 30 '18 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 Jul 30 '18 at 11:06
  • \$\begingroup\$ @PeterTaylor - code clarity and being able to navigate the visual tree easier when debugging \$\endgroup\$ – millimoose Jul 30 '18 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 Jul 30 '18 at 11:08
5
\$\begingroup\$

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/

\$\endgroup\$
  • \$\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 Jul 30 '18 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 Jul 30 '18 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 Jul 30 '18 at 11:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.