WPF:具有 2 个(或更多!)ContentPresenters 的模板或 UserControl 以在“插槽”中呈现内容
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1029955/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
WPF: template or UserControl with 2 (or more!) ContentPresenters to present content in 'slots'
提问by Tomá? Kafka
I am developing LOB application, where I will need multiple dialog windows (and displaying everything in one window is not an option/makes no sense).
我正在开发 LOB 应用程序,我将需要多个对话框窗口(并且在一个窗口中显示所有内容不是一种选择/没有意义)。
I would like to have a user control for my window that would define some styling, etc., and would have severalslots where content could be inserted - for example, a modal dialog window's template would have a slot for content, and for buttons (so that user can then provide a content and set of buttons with bound ICommands).
我希望我的窗口有一个用户控件,它可以定义一些样式等,并且有几个可以插入内容的插槽 - 例如,模态对话框窗口的模板将有一个用于内容和按钮的插槽(这样用户就可以提供一个内容和一组带有绑定 ICommand 的按钮)。
I would like to have something like this (but this doesn't work):
我想要这样的东西(但这不起作用):
UserControl xaml:
用户控件 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>
Is something like this possible? How should I tell VS that my control exposes two content placeholders so that I can use it like this?
这样的事情可能吗?我应该如何告诉 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>
Or maybe I could use a ControlTemplate for a window like here, but then again: Window has only one content slot, therefore its template will be able to have only one presenter, but I need two (and if in this case it would po maybe possible to go with one, there are other use cases where several content slots would come hand, just think about a template for article - control's user would supply a title, (structured) content, author name, image...).
或者,也许我可以将 ControlTemplate 用于像这里这样的窗口,但话又说回来:窗口只有一个内容槽,因此它的模板将只能有一个演示者,但我需要两个(如果在这种情况下它可能会可以使用一个,还有其他用例会出现多个内容槽,只需考虑文章模板 - 控件的用户将提供标题、(结构化)内容、作者姓名、图像...)。
Thank you!
谢谢!
PS: If I wanted to just have buttons side by side, how can I put multiple controls (buttons) to StackPanel? ListBox has ItemsSource, but StackPanel has not, and it's Children property is read-only - so this doesn't work (inside the usercontrol):
PS:如果我只想并排放置按钮,如何将多个控件(按钮)放到 StackPanel 上?ListBox 有 ItemsSource,但 StackPanel 没有,它的 Children 属性是只读的 - 所以这不起作用(在用户控件内):
<StackPanel
Orientation="Horizontal"
Children="{Binding Buttons}"/>
EDIT: I don't want to use binding, as I want to assign a DataContext (ViewModel) to a whole window (which equals View), and then bind to it's commands from buttons inserted into control 'slots' - so any use of binding in the hierarchy would break inheritance of View's DC.
编辑:我不想使用绑定,因为我想将 DataContext (ViewModel) 分配给整个窗口(等于 View),然后从插入控件“插槽”的按钮绑定到它的命令 - 所以任何使用层次结构中的绑定会破坏视图 DC 的继承。
As for the idea of inheriting from HeaderedContentControl - yes, in this case it would work, but what if I want three replacable parts? How do I make my own "HeaderedAndFooteredContentControl" (or, how would I implement HeaderedContentControl if I didn't have one)?
至于从 HeaderedContentControl 继承的想法 - 是的,在这种情况下它会起作用,但是如果我想要三个可替换的部分怎么办?我如何制作我自己的“HeaderedAndFooteredContentControl” (或者,如果我没有,我将如何实现 HeaderedContentControl)?
EDIT2: OK, so my two solutions doen't work - this is why:The ContentPresenter gets it's content from the DataContext, but I need the bindings on contained elements to link to original windows' (UserControl's parent in logical tree) DataContext - because this way, when I embed textbox bound to ViewModel's property, it is not bound, as the inheritance chain has been broken inside the control!
EDIT2:好的,所以我的两个解决方案不起作用 - 这就是为什么:ContentPresenter 从 DataContext 获取它的内容,但我需要包含元素的绑定以链接到原始窗口(逻辑树中的 UserControl 的父级)DataContext - 因为这样,当我嵌入绑定到 ViewModel 属性的文本框时,它不会被绑定,因为控件内部的继承链已被破坏!
It seems that I would need to save parent's DataContext, and restore it to the children of all control's containers, but I don't get any event that DataContext up in the logical tree has changed.
似乎我需要保存父级的 DataContext,并将其恢复到所有控件容器的子级,但我没有收到逻辑树中的 DataContext 已更改的任何事件。
EDIT3: I have a solution!, deleted my previous aswers. See my response.
EDIT3:我有一个解决方案!,删了我之前的回答。看我的回复。
回答by Tomá? Kafka
OK, my solution was totally unnecessary, here are the only tutorials you will ever need for creating any user control:
好的,我的解决方案完全没有必要,这里是创建任何用户控件所需的唯一教程:
- 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
- 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
In short:
简而言之:
Subclass some suitable class (or UIElement if none suits you) - the file is just plain *.cs, as we are only defining the behaviour, not the looks of the control.
子类化一些合适的类(或 UIElement,如果没有适合你) - 该文件只是普通的 *.cs,因为我们只定义行为,而不是控件的外观。
public class EnhancedItemsControl : ItemsControl
Add dependency property for your 'slots' (normal property is not good enough as it has only limited support for binding). Cool trick: in VS, write propdp
and press tab to expand the snippet :):
为您的“槽”添加依赖属性(普通属性不够好,因为它对绑定的支持有限)。很酷的技巧:在 VS 中,编写propdp
并按 Tab 键以展开代码段 :):
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*/);
Add an attribute for a designer (because you are creating so-called lookless control), this way we say that we need to have a ContentPresenter called PART_AlternativeContentPresenter in our template
为设计器添加一个属性(因为您正在创建所谓的无外观控件),这样我们说我们需要在我们的模板中有一个名为 PART_AlternativeContentPresenter 的 ContentPresenter
[TemplatePart(Name = "PART_AlternativeContentPresenter", Type = typeof(ContentPresenter))]
public class EnhancedItemsControl : ItemsControl
Provide a static constructor that will tell to WPF styling system about our class (without it, the styles/templates that target our new type would not be applied):
提供一个静态构造函数,它将告诉 WPF 样式系统关于我们的类(没有它,将不会应用针对我们的新类型的样式/模板):
static EnhancedItemsControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(EnhancedItemsControl),
new FrameworkPropertyMetadata(typeof(EnhancedItemsControl)));
}
If you want to do something with the ContentPresenter from the template, you do it by overriding the OnApplyTemplate method:
如果你想对模板中的 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...
}
}
Provide a default template: always in ProjectFolder/Themes/Generic.xaml (I have my standalone project with all custom universally usable wpf controls, which is then referenced from other solutions). This is only place where system will look for templates for your controls, so put default templates for all controls in a project here:
In this snippet I defined a new ContentPresenter that displays a value of our AlternativeContent
attached property. Note the syntax - I could use either
Content="{Binding AlternativeContent, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type WPFControls:EnhancedItemsControl}}}"
or Content="{TemplateBinding AlternativeContent}"
, but the former will work if you define a template inside your template (necessary for styling for example ItemPresenters).
提供默认模板:始终在 ProjectFolder/Themes/Generic.xaml 中(我的独立项目包含所有自定义通用 wpf 控件,然后从其他解决方案中引用)。这是系统为您的控件寻找模板的唯一地方,因此将项目中所有控件的默认模板放在这里:在这个片段中,我定义了一个新的 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>
Voila, you just made your first lookless UserControl (add more contentpresenters and dependency properties for more 'content slots').
瞧,您刚刚制作了第一个无外观的 UserControl(为更多“内容槽”添加更多内容展示器和依赖属性)。
回答by Tomá? Kafka
Hasta la victoria siempre!
Hasta la victoria siempre!
I have come with working solution (first on the internet, it seems to me :))
我带来了有效的解决方案(首先在互联网上,在我看来 :))
The tricky DialogControl.xaml.cs - see comments:
棘手的 DialogControl.xaml.cs - 见评论:
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));
}
And the DialogControl.xaml (no changes):
和 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>
Sample usage:
示例用法:
<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>
回答by Alun Harford
If you're using a UserControl
如果您使用的是用户控件
I'm guessing you actually want:
我猜你真的想要:
<ContentPresenter Content="{Binding Buttons}"/>
This assumes that the DataContext passed to your control has a Buttons property.
这假定传递给控件的 DataContext 具有 Buttons 属性。
And with a ControlTemplate
并使用 ControlTemplate
The other option would be a ControlTemplate and then you might use:
另一个选项是 ControlTemplate,然后您可以使用:
<ContentPresenter ContentSource="Header"/>
You would need to be templating a control that actually has a 'Header' to do this (normally a HeaderedContentControl).
您需要模板化一个实际上具有“标题”的控件来执行此操作(通常为 HeaderedContentControl)。