WPF で子要素を持つ UserControl を作る

◆ はじめに

現在のプロジェクト案件で WPF を使っておりまして、レビューやらで Xaml をみる機会が増えてきました。
その Xaml 内に同じような記述が繰り返しでてきたりすると、共通化や部品化したくなるのが人の性…。
せっせこ部品化していたときに、子要素を持つコントロールってどうやって作るんだっけなーとふと思って調べたのでメモ。

◆ どんなコントロールを作るか

タイトル行付きの StackPanel を作ってみる。イメージは以下。
タイトルとカラーはプロパティで指定できるようにする。

表紙

<Window
    x:Class="CustomStackPanel.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"
    Title="MainWindow"
    Width="450"
    Height="450"
    mc:Ignorable="d">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <!--ここから-->
        <Border
            Grid.Row="0"
            BorderBrush="SkyBlue"
            BorderThickness="3">
            <StackPanel>
                <Label Content="グループ1" Foreground="SkyBlue" />
                <Border
                    Margin="5"
                    BorderBrush="SkyBlue"
                    BorderThickness="0,0,0,2" />
                <StackPanel Margin="10">
                    <!--ここは可変-->
                    <TextBlock x:Name="text1" Text="あいうえお" />
                    <TextBlock x:Name="text2" Text="あいうえお" />
                    <TextBlock x:Name="text3" Text="あいうえお" />
                    <TextBlock x:Name="text4" Text="あいうえお" />
                    <TextBlock x:Name="text5" Text="あいうえお" />
                    <!--ここは可変-->
                </StackPanel>
            </StackPanel>
        </Border>
        <!--ここまで部品化-->
    </Grid>
</Window>

◆ 手順

1. UserControl を追加

プロジェクト -> ユーザコントロールの追加 で UserControl を新規作成する。
Xaml ファイルとそれに紐づく C# のソースが生成される。UserControl 名は「GroupPanel」としている。

2. Xaml 修正

作りたいコントロールに合わせて、Xaml を修正していく。
今回は子要素を持つ必要があるので、Template と ContentPresenter を使用している。以下を参考にした。
https://stackoverflow.com/questions/36352612/fill-stackpanel-inside-usercontrol

Xaml はこんな感じになる。

GroupPanel.xaml

<UserControl
    x:Class="CustomStackPanel.GroupPanel"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="GroupPanelRoot">
    <UserControl.Template>
        <ControlTemplate TargetType="UserControl">
            <Border BorderBrush="{Binding StyleColor, ElementName=GroupPanelRoot}" BorderThickness="3">
                <StackPanel>
                    <Label Content="{Binding Title, ElementName=GroupPanelRoot}" Foreground="{Binding StyleColor, ElementName=GroupPanelRoot}" />
                    <Border
                        Margin="5"
                        BorderBrush="{Binding StyleColor, ElementName=GroupPanelRoot}"
                        BorderThickness="0,0,0,2" />
                    <StackPanel Margin="10">
                        <ContentPresenter Content="{TemplateBinding Content}" />
                    </StackPanel>
                </StackPanel>
            </Border>
        </ControlTemplate>
    </UserControl.Template>
</UserControl>

3. プロパティの追加

コントロールの見た目はできたので、あとは独自プロパティを追加する。
独自プロパティは、追加した UserControl の C# 側に記載すれば OK。
詳細は、依存関係プロパティとかでググれば出てくる。

GroupPanel.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace CustomStackPanel
{
    /// <summary>
    /// GroupPanel.xaml の相互作用ロジック
    /// </summary>
    public partial class GroupPanel : UserControl
    {
        public static DependencyProperty StyleColorProperty =
            DependencyProperty.Register(
                "StyleColor",
                typeof(Brush),
                typeof(GroupPanel),
                new PropertyMetadata(new SolidColorBrush(Colors.Transparent)));

        public Brush StyleColor
        {
            get => (Brush)GetValue(StyleColorProperty);
            set { SetValue(StyleColorProperty, value); }
        }

        public static DependencyProperty TitleProperty =
            DependencyProperty.Register(
                "Title",
                typeof(string),
                typeof(GroupPanel),
                new PropertyMetadata(""));

        public string Title
        {
            get => (string)GetValue(TitleProperty);
            set { SetValue(TitleProperty, value); }
        }

        public GroupPanel()
        {
            InitializeComponent();
        }
    }
}

4. 子要素に Name 属性が持てない!

以上までで良し良し楽勝だなとか思っていたら、問題発生。
作成した UserControl の子要素に Name 属性を持たせたらエラーが発生した。
嘘だと言ってよバーニー。

こんな感じの Xaml をかくと、

<local:GroupPanel
    Title="グループ2"
    Grid.Row="1"
    StyleColor="IndianRed">
    <StackPanel>
        <TextBlock x:Name="text1" Text="あいうえお" />
    </StackPanel>
</local:GroupPanel>

こんなエラーが出てくる。

重大度レベル	コード	説明	プロジェクト	ファイル	行	抑制状態
エラー	XEC0030	名前 "text1" は既にこのスコープで定義されています。	CustomStackPanel	MainWindow.xaml	41

なんでや。

5. 解決策

意味が分からなさ過ぎて、ネットの海をさまよっていたら、以下サイトたちに行き着いた。

記事によると、Xaml で画面作るとダメっぽいので、画面も C# 側で書け、以上。まじかよ。

Xaml なしの UserControl クラス(GroupPanel2) を作成してみたら、解決した。全然納得できないけど、しょうがないね。

GroupPanel2.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace CustomStackPanel
{
    public class GroupPanel2 : UserControl
    {
        public static DependencyProperty StyleColorProperty =
            DependencyProperty.Register(
                "StyleColor",
                typeof(Brush),
                typeof(GroupPanel2),
                new PropertyMetadata(new SolidColorBrush(Colors.Transparent)));

        public Brush StyleColor
        {
            get => (Brush)GetValue(StyleColorProperty);
            set { SetValue(StyleColorProperty, value); }
        }

        public static DependencyProperty TitleProperty =
            DependencyProperty.Register(
                "Title",
                typeof(string),
                typeof(GroupPanel2),
                new PropertyMetadata(""));

        public string Title
        {
            get => (string)GetValue(TitleProperty);
            set { SetValue(TitleProperty, value); }
        }

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

            Border parentBorder = new Border
            {
                BorderBrush = StyleColor,
                BorderThickness = new Thickness(3),
            };

            StackPanel mainStack = new StackPanel();

            Label titleLabel = new Label
            {
                Content = Title,
                Foreground = StyleColor,
            };

            Border lineBorder = new Border
            {
                Margin = new Thickness(5),
                BorderBrush = StyleColor,
                BorderThickness = new Thickness(0, 0, 0, 2)
            };

            StackPanel contentStack = new StackPanel
            {
                Margin = new Thickness(10)
            };

            ContentPresenter contentP = new ContentPresenter
            {
                Content = Content
            };

            contentStack.Children.Add(contentP);

            mainStack.Children.Add(titleLabel);
            mainStack.Children.Add(lineBorder);
            mainStack.Children.Add(contentStack);

            parentBorder.Child = mainStack;

            Content = parentBorder;
        }
    }
}

6. その他

サンプルソースは以下。見たい方はどうぞ。
https://github.com/shuntaro4/WPFSampleCustomStackPanel

◆ まとめ

無理やり感あるけど、なんとか解決できて良かった。
カスタムコントロール+スタイルで作る方がもしかしてスタンダードなやり方なのかもしれない。
そっちのやり方も試したらのっける。


WPF

2018-09-30 15:00 +0000