如何将多个 UI children 添加到 CustomControl 的 DependencyProperty?

How can I add multiple UI children to a CustomControl's DependencyProperty?

我创建了一个表示磁贴的 CustomControl。它包含一个带有标题、工具栏和主要内容的 header,并且工作正常。但是,我想将 <StackPanel Orientation="Horizontal"> 移动到控件的模板,因为我总是希望工具栏像这样显示。

因为我想将 children 添加到 DependencyProperty 而不是直接添加到控件,所以我无法从 ItemsControl 派生。我可以在不向我的控件添加大量样板代码的情况下解决这个问题吗?

我假设我必须用 ItemsPresenter 替换第二个 ContentPresenter,但是:

当前使用情况:

<controls:Tile Title="Projects" Margin="6,6,0,6">
    <controls:Tile.ToolbarContent>
        <StackPanel Orientation="Horizontal">
            <controls:ToolbarButton Text="Add" ImageSource="pack://application:,,,/MyAssembly;Component/Resources/plus-16.png" />
            <controls:ToolbarButton Text="Remove" ImageSource="pack://application:,,,/MyAssembly;Component/Resources/delete-16.png" />
            <controls:ToolbarButton ... />
            <controls:ToolbarButton ... />
        </StackPanel>
    </controls:Tile.ToolbarContent>
    <ListView x:Name="MainContent" BorderThickness="0" ItemsSource="{Binding Projects}" />
</controls:Tile>

要求的用法:

<controls:Tile Title="Projects" Margin="6,6,0,6">
    <controls:Tile.ToolbarContent>
        <controls:ToolbarButton Text="Add" ImageSource="pack://application:,,,/MyAssembly;Component/Resources/plus-16.png" />
        <controls:ToolbarButton Text="Remove" ImageSource="pack://application:,,,/MyAssembly;Component/Resources/delete-16.png" />
        <controls:ToolbarButton ... />
        <controls:ToolbarButton ... />
    </controls:Tile.ToolbarContent>
    <ListView x:Name="MainContent" BorderThickness="0" ItemsSource="{Binding Projects}" />
</controls:Tile>

风格:

<Style TargetType="controls:Tile">
    <Setter Property="UseLayoutRounding" Value="True" />
    <Setter Property="SnapsToDevicePixels" Value="True" />
    <Setter Property="BorderBrush" Value="{DynamicResource PrimaryColor_100}" />
    <Setter Property="Background" Value="White" />
    <Setter Property="FontSize" Value="12" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="controls:Tile">
                <Border BorderBrush="{DynamicResource PrimaryColor_40}" BorderThickness="1" Background="{TemplateBinding Background}" UseLayoutRounding="{TemplateBinding UseLayoutRounding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition />
                        </Grid.RowDefinitions>
                        <Border x:Name="Header" Background="{TemplateBinding BorderBrush}">
                            <TextBlock VerticalAlignment="Center" FontSize="{TemplateBinding FontSize}" Margin="3" Text="{TemplateBinding Title}" Foreground="White" />
                        </Border>
                        <Border x:Name="Toolbar" Grid.Row="1" Background="{DynamicResource PrimaryColor_40}" Padding="1">
                            <ContentPresenter x:Name="ToolbarContent" Content="{TemplateBinding ToolbarContent}" />
                        </Border>
                        <ContentPresenter x:Name="MainContent" Grid.Row="2" />
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

代码:

public class Tile : ContentControl
{
    public static readonly DependencyProperty ToolbarContentProperty =
        DependencyProperty.Register("ToolbarContent", typeof(FrameworkElement), typeof(Tile), new PropertyMetadata());

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

    public FrameworkElement ToolbarContent
    {
        get { return (FrameworkElement)GetValue(ToolbarContentProperty); }
        set { SetValue(ToolbarContentProperty, value); }
    }

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

    static Tile()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(Tile), new FrameworkPropertyMetadata(typeof(Tile)));
    }
}

我不知道有什么好方法可以按字面意思完成您的要求。您正在以恕我直言的方式重载 ContentControl 类型,这看起来很笨拙,并且与该特定控件的预期设计不符。也就是说,有可能按照这些思路进行一些工作。

需要了解的重要一点是,您将无法创建一个集合,然后将其分配给 StackPanel children。这不是 Panel object 的工作方式。相反,您需要一些其他控件,您可以将控件集合分配给这些控件并使其工作。用于此目的的常用控件是 ItemsControl object.

您可以将您的 ToolbarContent 属性 设为 ObservableCollection<UIElement>,然后将其值绑定到 ItemsControl.ItemsSource 属性。在 ItemsControl 中,您可以设置 ItemsPanel 属性 让 ItemsControl 使用具有您想要的布局的 StackPanel(例如 Orientation="Horizontal")。

例如:

      <ControlTemplate TargetType="controls:Tile">
        <Border BorderBrush="{DynamicResource PrimaryColor_40}" BorderThickness="1" Background="{TemplateBinding Background}" UseLayoutRounding="{TemplateBinding UseLayoutRounding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="Auto" />
              <RowDefinition Height="Auto" />
              <RowDefinition />
            </Grid.RowDefinitions>
            <Border x:Name="Header" Background="{TemplateBinding BorderBrush}">
              <TextBlock VerticalAlignment="Center" FontSize="{TemplateBinding FontSize}" Margin="3" Text="{TemplateBinding Title}" Foreground="White" />
            </Border>
            <Border x:Name="Toolbar" Grid.Row="1" Background="{DynamicResource PrimaryColor_40}" Padding="1">
              <ItemsControl ItemsSource="{TemplateBinding ToolbarContent}">
                <ItemsControl.ItemsPanel>
                  <ItemsPanelTemplate>
                    <StackPanel IsItemsHost="True" Orientation="Horizontal"/>
                  </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
              </ItemsControl>
            </Border>
            <ContentPresenter x:Name="MainContent" Grid.Row="2" />
          </Grid>
        </Border>
      </ControlTemplate>

并且:

class Tile : ContentControl
{
    private static readonly DependencyPropertyKey ToolbarContentKey =
         DependencyProperty.RegisterReadOnly("ToolbarContent", typeof(ObservableCollection<UIElement>), typeof(Tile), new PropertyMetadata());

    public static readonly DependencyProperty ToolbarContentProperty = ToolbarContentKey.DependencyProperty;

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

    public ObservableCollection<UIElement> ToolbarContent
    {
        get { return (ObservableCollection<UIElement>)GetValue(ToolbarContentProperty); }
    }

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

    public Tile()
    {
        SetValue(ToolbarContentKey, new ObservableCollection<UIElement>());
    }
}

然后当你使用你想要的语法时,元素将被添加到集合中并且该集合绑定到 ItemsSource 属性 以便它们可以使用 [=15= 显示]作为主持人。


现在,虽然这会起作用,但对我来说,您定义的模板确实具有更多 "composite control" 的感觉。这通常意味着实施 UserControl。使用 UserControl,您只是在声明一个控件,该控件本身由其他控件的集合组成,其布局和其他特征在 XAML 中定义,类似于 [=30] 的方式=].

在这种方法中,您可以将内部元素的集合公开为 public 接口的一部分,而不是创建外部集合并绑定到内部元素的 属性 28=] 直接往里面加东西。这也将允许您以更直接的方式定义自定义控件的 layout/composition(例如,具有实际的 StackPanel 元素,而不必将其包装在 ItemsControl 中)。

可能看起来像这样:

<UserControl x:Class="TestSO37929673CollectionContent.Tile2"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             x:Name="root"
             d:DesignHeight="300" d:DesignWidth="300">

  <Border BorderBrush="Black" BorderThickness="1"
          Background="{Binding Background, ElementName=root}"
          UseLayoutRounding="{Binding UseLayoutRounding, ElementName=root}"
          SnapsToDevicePixels="{Binding SnapsToDevicePixels, ElementName=root}">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition />
      </Grid.RowDefinitions>
      <Border x:Name="Header" Background="{Binding BorderBrush, ElementName=root}">
        <TextBlock VerticalAlignment="Center"
                   FontSize="{Binding FontSize, ElementName=root}"
                   Margin="3"
                   Text="{Binding Title, ElementName=root}" Foreground="White" />
      </Border>
      <Border x:Name="Toolbar" Grid.Row="1" Background="White" Padding="1">
        <StackPanel x:Name="stackPanel1" Orientation="Horizontal"/>
      </Border>
      <ContentPresenter Content="{Binding MainContent, ElementName=root}" Grid.Row="2" />
    </Grid>
  </Border>
</UserControl>

并且:

[ContentPropertyAttribute("MainContent")]
public partial class Tile2 : UserControl
{
    private static readonly DependencyPropertyKey ToolbarContentKey =
         DependencyProperty.RegisterReadOnly("ToolbarContent", typeof(UIElementCollection), typeof(Tile2), new PropertyMetadata());

    public static readonly DependencyProperty ToolbarContentProperty = ToolbarContentKey.DependencyProperty;

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

    public static readonly DependencyProperty MainContentProperty =
        DependencyProperty.Register("MainContent", typeof(object), typeof(Tile2));

    public UIElementCollection ToolbarContent
    {
        get { return (UIElementCollection)GetValue(ToolbarContentProperty); }
    }

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

    public object MainContent
    {
        get { return GetValue(MainContentProperty); }
        set { SetValue(MainContentProperty, value); }
    }

    public Tile2()
    {
        InitializeComponent();
        SetValue(ToolbarContentKey, stackPanel1.Children);
    }
}

使用语法相同(即完全符合您在 post 中的要求),您也可以用相同的方式设置控件的样式。但是它将控件的布局封装在 UserControl 定义本身中,而不是依赖在样式中分配的 ControlTemplate 来实现。

请注意 UserControl 本身就是 ContentControl 的子类。因此,如果您认为 ContentControl 的某些功能很重要,您仍然可以利用它们。只是 UserControl 范式恕我直言,使客户端和自定义控件的关系更清晰,更易于维护。