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 以在“插槽”中呈现内容的主要内容,如果未能解决你的问题,请参考以下文章

ionic 2,刷新带有 2 个或更多徽标的模态页面

WPF 多点触摸开发[2]:WPF触摸的几个手势的执行顺序

WPF 应用程序可以在带有 .Net Core 3 的 Linux 或 Mac 中运行吗?

在 PHP 中将 2 个或更多 png 图像合并到基本图像上

带有 2 个参数的 symfony 重定向

WPF Multibinding - 需要使用 Relaycommand