WPF:带有 2 个(或更多!)ContentPresenters 的模板或 UserControl 以在“插槽”中呈现内容
Posted
技术标签:
【中文标题】WPF:带有 2 个(或更多!)ContentPresenters 的模板或 UserControl 以在“插槽”中呈现内容【英文标题】:WPF: template or UserControl with 2 (or more!) ContentPresenters to present content in 'slots' 【发布时间】:2010-11-05 00:32:58 【问题描述】:我正在开发 LOB 应用程序,我需要多个对话框窗口(在一个窗口中显示所有内容不是一种选择/没有意义)。
我希望我的窗口有一个用户控件,它可以定义一些样式等,并且会有 几个 可以插入内容的插槽 - 例如,模态对话框窗口的模板将有一个内容槽和按钮槽(这样用户就可以提供一个内容和一组带有绑定 ICommand 的按钮)。
我想要这样的东西(但这不起作用):
用户控件 xaml:
<UserControl x:Class="TkMVVMContainersSample.Services.Common.GUI.DialogControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="DynamicResource x:Static SystemColors.ControlBrushKey"
>
<DockPanel>
<DockPanel
LastChildFill="False"
HorizontalAlignment="Stretch"
DockPanel.Dock="Bottom">
<ContentPresenter ContentSource="Binding Buttons"/>
</DockPanel>
<Border
Background="DynamicResource x:Static SystemColors.WindowBrushKey"
Padding="8"
>
<ContentPresenter ContentSource="Binding Controls"/>
</Border>
</DockPanel>
</UserControl>
这样的事情可能吗?我应该如何告诉 VS 我的控件公开了两个内容占位符,以便我可以这样使用它?
<Window ... DataContext="MyViewModel">
<gui:DialogControl>
<gui:DialogControl.Controls>
<!-- My dialog content - grid with textboxes etc...
inherits the Window's DC - DialogControl just passes it through -->
</gui:DialogControl.Controls>
<gui:DialogControl.Buttons>
<!-- My dialog's buttons with wiring, like
<Button Command="Binding HelpCommand">Help</Button>
<Button Command="Binding CancelCommand">Cancel</Button>
<Button Command="Binding OKCommand">OK</Button>
- they inherit DC from the Window, so the OKCommand binds to MyViewModel.OKCommand
-->
</gui:DialogControl.Buttons>
</gui:DialogControl>
</Window>
或者也许我可以为窗口 like here 使用 ControlTemplate,但话又说回来:窗口只有一个内容槽,因此它的模板只能有一个演示者,但我需要两个(如果在这种情况下它可能会与一个一起使用,还有其他用例会出现多个内容槽,只需考虑文章的模板 - 控件的用户将提供标题、(结构化)内容、作者姓名、图像...... )。
谢谢!
PS:如果我只想并排放置按钮,如何将多个控件(按钮)放到 StackPanel 中? ListBox 有 ItemsSource,但 StackPanel 没有,而且它的 Children 属性是只读的 - 所以这不起作用(在用户控件内部):
<StackPanel
Orientation="Horizontal"
Children="Binding Buttons"/>
编辑:我不想使用绑定,因为我想将 DataContext (ViewModel) 分配给整个窗口(等于 View),然后从插入到控件“插槽”中的按钮绑定到它的命令 - 所以在层次结构中使用任何绑定都会破坏 View 的 DC 的继承。
至于从 HeaderedContentControl 继承的想法——是的,在这种情况下它会起作用,但是如果我想要三个可替换的部分呢?我如何制作自己的“HeaderedAndFooteredContentControl”(或者,如果我没有 HeaderedContentControl,我将如何实现)?
EDIT2:好的,所以我的两个解决方案不起作用 - 这就是原因: ContentPresenter 从 DataContext 获取它的内容,但我需要对包含的元素进行绑定以链接到原始窗口(逻辑树中 UserControl 的父级)DataContext - 因为这样,当我嵌入绑定到 ViewModel 属性的文本框时,它没有被绑定,因为控件内部的继承链被破坏了!
似乎我需要保存父级的 DataContext,并将其恢复到所有控件容器的子级,但我没有收到任何事件表明逻辑树中的 DataContext 已更改。
EDIT3:我有一个解决方案!,删除了我之前的回答。 请参阅我的回复。
【问题讨论】:
【参考方案1】:好的,我的解决方案完全没有必要,这里是创建任何用户控件所需的唯一教程:
http://www.interact-sw.co.uk/iangblog/2007/02/14/wpfdefaulttemplate http://www.codeproject.com/Articles/37326/Lookless-Controls-Themes.aspx http://www.codeproject.com/Articles/35444/Defining-the-Default-Style-for-a-Lookless-Control.aspx http://blog.pixelingene.com/?p=58简而言之:
子类化一些合适的类(或 UIElement,如果不适合你) - 文件只是简单的 *.cs,因为我们只定义行为,而不是控件的外观。
public class EnhancedItemsControl : ItemsControl
为您的“插槽”添加依赖属性(普通属性不够好,因为它对绑定的支持有限)。很酷的技巧:在 VS 中,写 propdp
并按 Tab 展开 sn-p :):
public object AlternativeContent
get return (object)GetValue(AlternativeContentProperty);
set SetValue(AlternativeContentProperty, value);
// Using a DependencyProperty as the backing store for AlternativeContent. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AlternativeContentProperty =
DependencyProperty.Register("AlternativeContent" /*name of property*/, typeof(object) /*type of property*/, typeof(EnhancedItemsControl) /*type of 'owner' - our control's class*/, new UIPropertyMetadata(null) /*default value for property*/);
为设计器添加一个属性(因为您正在创建所谓的无外观控件),这样我们就说我们需要在我们的模板中有一个名为 PART_AlternativeContentPresenter 的 ContentPresenter
[TemplatePart(Name = "PART_AlternativeContentPresenter", Type = typeof(ContentPresenter))]
public class EnhancedItemsControl : ItemsControl
提供一个静态构造函数,它将告诉 WPF 样式系统我们的类(没有它,将不会应用针对我们新类型的样式/模板):
static EnhancedItemsControl()
DefaultStyleKeyProperty.OverrideMetadata(
typeof(EnhancedItemsControl),
new FrameworkPropertyMetadata(typeof(EnhancedItemsControl)));
如果你想对模板中的 ContentPresenter 做一些事情,你可以重写 OnApplyTemplate 方法:
//remember that this may be called multiple times if user switches themes/templates!
public override void OnApplyTemplate()
base.OnApplyTemplate(); //always do this
//Obtain the content presenter:
contentPresenter = base.GetTemplateChild("PART_AlternativeContentPresenter") as ContentPresenter;
if (contentPresenter != null)
// now we know that we are lucky - designer didn't forget to put a ContentPresenter called PART_AlternativeContentPresenter into the template
// do stuff here...
提供默认模板:始终在 ProjectFolder/Themes/Generic.xaml 中(我的独立项目包含所有自定义的通用 wpf 控件,然后从其他解决方案中引用)。这只是系统为您的控件查找模板的地方,因此将项目中所有控件的默认模板放在此处:
在这个 sn-p 中,我定义了一个新的 ContentPresenter,它显示了我们的 AlternativeContent
附加属性的值。注意语法 - 我可以使用
Content="Binding AlternativeContent, RelativeSource=RelativeSource FindAncestor, AncestorType=x:Type WPFControls:EnhancedItemsControl"
或 Content="TemplateBinding AlternativeContent"
,但如果您在模板中定义模板(例如 ItemPresenters 的样式所必需的),则前者将起作用。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WPFControls="clr-namespace:MyApp.WPFControls"
>
<!--EnhancedItemsControl-->
<Style TargetType="x:Type WPFControls:EnhancedItemsControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="x:Type WPFControls:EnhancedItemsControl">
<ContentPresenter
Name="PART_AlternativeContentPresenter"
Content="Binding AlternativeContent, RelativeSource=RelativeSource FindAncestor, AncestorType=x:Type WPFControls:EnhancedItemsControl"
DataContext="Binding DataContext, RelativeSource=RelativeSource FindAncestor, AncestorType=x:Type WPFControls:EnhancedItemsControl"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
瞧,您刚刚制作了您的第一个无外观 UserControl(为更多“内容槽”添加更多内容呈现器和依赖属性)。
【讨论】:
感谢上帝,有人以简单易懂的方式解释了这一点!很难找到有关此的资源,或者这只是 wpf 初学者的错误:p【参考方案2】:Hasta la victoria siempre!
我提供了可行的解决方案(首先在互联网上,在我看来 :))
棘手的 DialogControl.xaml.cs - 请参阅 cmets:
public partial class DialogControl : UserControl
public DialogControl()
InitializeComponent();
//The Logical tree detour:
// - we want grandchildren to inherit DC from this (grandchildren.DC = this.DC),
// but the children should have different DC (children.DC = this),
// so that children can bind on this.Properties, but grandchildren bind on this.DataContext
this.InnerWrapper.DataContext = this;
this.DataContextChanged += DialogControl_DataContextChanged;
// need to reinitialize, because otherwise we will get static collection with all buttons from all calls
this.Buttons = new ObservableCollection<FrameworkElement>();
void DialogControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
/* //Heading is ours, we want it to inherit this, so no detour
if ((this.GetValue(HeadingProperty)) != null)
this.HeadingContainer.DataContext = e.NewValue;
*/
//pass it on to children of containers: detours
if ((this.GetValue(ControlProperty)) != null)
((FrameworkElement)this.GetValue(ControlProperty)).DataContext = e.NewValue;
if ((this.GetValue(ButtonProperty)) != null)
foreach (var control in ((ObservableCollection<FrameworkElement>) this.GetValue(ButtonProperty)))
control.DataContext = e.NewValue;
public FrameworkElement Control
get return (FrameworkElement)this.GetValue(ControlProperty);
set this.SetValue(ControlProperty, value);
public ObservableCollection<FrameworkElement> Buttons
get return (ObservableCollection<FrameworkElement>)this.GetValue(ButtonProperty);
set this.SetValue(ButtonProperty, value);
public string Heading
get return (string)this.GetValue(HeadingProperty);
set this.SetValue(HeadingProperty, value);
public static readonly DependencyProperty ControlProperty =
DependencyProperty.Register("Control", typeof(FrameworkElement), typeof(DialogControl));
public static readonly DependencyProperty ButtonProperty =
DependencyProperty.Register(
"Buttons",
typeof(ObservableCollection<FrameworkElement>),
typeof(DialogControl),
//we need to initialize this for the designer to work correctly!
new PropertyMetadata(new ObservableCollection<FrameworkElement>()));
public static readonly DependencyProperty HeadingProperty =
DependencyProperty.Register("Heading", typeof(string), typeof(DialogControl));
还有 DialogControl.xaml(没有变化):
<UserControl x:Class="TkMVVMContainersSample.Views.Common.DialogControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="DynamicResource x:Static SystemColors.ControlBrushKey"
>
<DockPanel x:Name="InnerWrapper">
<DockPanel
LastChildFill="False"
HorizontalAlignment="Stretch"
DockPanel.Dock="Bottom">
<ItemsControl
x:Name="ButtonsContainer"
ItemsSource="Binding Buttons"
DockPanel.Dock="Right"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Padding="8">
<ContentPresenter Content="TemplateBinding Content" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Margin="8">
</StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DockPanel>
<Border
Background="DynamicResource x:Static SystemColors.WindowBrushKey"
Padding="8,0,8,8"
>
<StackPanel>
<Label
x:Name="HeadingContainer"
Content="Binding Heading"
FontSize="20"
Margin="0,0,0,8" />
<ContentPresenter
x:Name="ControlContainer"
Content="Binding Control"
/>
</StackPanel>
</Border>
</DockPanel>
</UserControl>
示例用法:
<Window x:Class="TkMVVMContainersSample.Services.TaskEditDialog.ItemEditView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Common="clr-namespace:TkMVVMContainersSample.Views.Common"
Title="ItemEditView"
>
<Common:DialogControl>
<Common:DialogControl.Heading>
Edit item
</Common:DialogControl.Heading>
<Common:DialogControl.Control>
<!-- Concrete dialog's content goes here -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0">Name</Label>
<TextBox Grid.Row="0" Grid.Column="1" MinWidth="160" TabIndex="1" Text="Binding Name"></TextBox>
<Label Grid.Row="1" Grid.Column="0">Phone</Label>
<TextBox Grid.Row="1" Grid.Column="1" MinWidth="160" TabIndex="2" Text="Binding Phone"></TextBox>
</Grid>
</Common:DialogControl.Control>
<Common:DialogControl.Buttons>
<!-- Concrete dialog's buttons go here -->
<Button Width="80" TabIndex="100" IsDefault="True" Command="Binding OKCommand">OK</Button>
<Button Width="80" TabIndex="101" IsCancel="True" Command="Binding CancelCommand">Cancel</Button>
</Common:DialogControl.Buttons>
</Common:DialogControl>
</Window>
【讨论】:
请不要这样做,改用我的第二个答案***.com/questions/1029955/…!【参考方案3】:如果您使用的是 UserControl
我猜你真的想要:
<ContentPresenter Content="Binding Buttons"/>
这假定传递给您的控件的 DataContext 具有 Buttons 属性。
还有一个 ControlTemplate
另一个选项是 ControlTemplate,然后您可以使用:
<ContentPresenter ContentSource="Header"/>
您需要对一个实际上具有“标题”的控件进行模板化(通常是 HeaderedContentControl)。
【讨论】:
谢谢 - 我不想使用绑定,因为我想将 DataContext (ViewModel) 分配给整个窗口(等于 View),然后从插入控件的按钮绑定到它的命令'slots' - 因此在层次结构中使用任何绑定都会破坏 View 的 DC 的继承。至于另一个选项 - 是的,在这种情况下,从 HeaderedContentControl 派生可以工作,但如果我想要三个部分怎么办?如何制作自己的“HeaderedAndFooteredContentControl”(或者,如果我没有 HeaderedContentControl,我将如何实现)?以上是关于WPF:带有 2 个(或更多!)ContentPresenters 的模板或 UserControl 以在“插槽”中呈现内容的主要内容,如果未能解决你的问题,请参考以下文章
WPF 应用程序可以在带有 .Net Core 3 的 Linux 或 Mac 中运行吗?