如何将多个 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
,但是:
- ToolbarContent
DependencyProperty
需要什么类型? UIElementCollection
和 ItemsCollection
都不能实例化。
- 如何让 ItemsPresenter 水平显示其内容
StackPanel
?
当前使用情况:
<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
范式恕我直言,使客户端和自定义控件的关系更清晰,更易于维护。
我创建了一个表示磁贴的 CustomControl。它包含一个带有标题、工具栏和主要内容的 header,并且工作正常。但是,我想将 <StackPanel Orientation="Horizontal">
移动到控件的模板,因为我总是希望工具栏像这样显示。
因为我想将 children 添加到 DependencyProperty
而不是直接添加到控件,所以我无法从 ItemsControl
派生。我可以在不向我的控件添加大量样板代码的情况下解决这个问题吗?
我假设我必须用 ItemsPresenter
替换第二个 ContentPresenter
,但是:
- ToolbarContent
DependencyProperty
需要什么类型?UIElementCollection
和ItemsCollection
都不能实例化。 - 如何让 ItemsPresenter 水平显示其内容
StackPanel
?
当前使用情况:
<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
范式恕我直言,使客户端和自定义控件的关系更清晰,更易于维护。