WPF:使用具有2个(或更多!)ContentPresenter的模板或UserControl在“插槽”中呈现内容

29
我正在开发一个LOb应用程序,其中我需要多个对话框窗口(在一个窗口中显示所有内容不是选项/没有意义)。
我想为我的窗口创建一个用户控件,用于定义某些样式等,并且该控件将具有几个插槽,用于插入内容 - 例如,模态对话框窗口的模板将具有内容和按钮插槽(因此用户可以提供内容和一组绑定ICommand的按钮)。
我希望像这样拥有一个控件(但这不起作用):
UserControl 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>

或者我可以像这里一样,使用一个窗口的控制模板(ControlTemplate),但是再次强调:Window只有一个内容插槽,因此它的模板只能有一个presenter,但我需要两个(即使在这种情况下,可能只需要一个,但还有其他用例需要多个内容插槽,比如文章模板-控件用户会提供标题,结构化内容,作者姓名,图像等)。

谢谢!

PS:如果我想将多个控件(按钮)并排放置,如何将它们放入 StackPanel 中?ListBox 有 ItemsSource,但 StackPanel 没有,并且其 Children 属性是只读的-因此这种方法无法实现(在 usercontrol 内部):

<StackPanel 
    Orientation="Horizontal"
    Children="{Binding Buttons}"/> 

编辑:我不想使用绑定,因为我想将一个DataContext(ViewModel)分配给整个窗口(相当于View),然后从插入到控件“插槽”中的按钮绑定到它的命令——因此,在层次结构中使用任何绑定都会破坏View的DC的继承。

至于从HeaderedContentControl继承的想法-是的,在这种情况下它会起作用,但是如果我想要三个可替换部分怎么办?如何制作自己的“HeaderedAndFooteredContentControl”(或者如果我没有一个HeaderedContentControl该怎么实现)?

编辑2:好的,我的两个解决方案都不起作用-原因如下: ContentPresenter从DataContext获取其内容,但我需要所包含元素的绑定链接到原始窗口(UserControl在逻辑树中的父级上)的DataContext-因为这样,当我嵌入绑定到ViewModel属性的文本框时,它不会被绑定,由于继承链已经在控件内部断开了!

似乎我需要保存父级的DataContext,并将其恢复到所有控件容器的子级,但我没有得到任何有关逻辑树中DataContext已更改的事件。

编辑3:我有了一个解决方案!删除了我的先前回答。 请参阅我的响应。

3个回答

35
好的,我的解决方案完全是不必要的,以下是您需要创建任何用户控件的唯一教程: 简而言之:
子类化某些适当的类(如果没有适合您的类,则使用UIElement)-该文件只是普通的*.cs文件,因为我们仅定义控件的行为,而不是外观。
public class EnhancedItemsControl : ItemsControl

为您的“插槽”添加依赖属性(普通属性对绑定的支持有限,不够好)。酷炫技巧:在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*/);

为设计师添加一个属性(因为你正在创建所谓的无外观控件),这样我们就需要在模板中有一个名为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控件,然后从其他解决方案中引用)。这是系统寻找控件模板的唯一位置,因此请在项目中为所有控件放置默认模板: 在此片段中,我定义了一个新的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(添加更多的ContentPresenter和依赖属性以获得更多的“内容槽”)。


5
谢谢上帝有人用简单易懂的方式解释了这个问题!很难找到相关的资源,或者只是wpf初学者的过错:p - Shion

5

永远的胜利!

我提供了一个可行的解决方案(似乎是互联网上第一个:))

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));
}

而 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>

4
请不要这样做,使用我在Stack Overflow上的第二个答案 https://dev59.com/n3NA5IYBdhLWcg3wQ7i4#1658916! - Tomáš Kafka

2

如果您正在使用UserControl

我猜您实际上想要:

<ContentPresenter Content="{Binding Buttons}"/>

这里假设传递给你的控件的DataContext有一个Buttons属性。
另一种选择是使用ControlTemplate,然后你可以使用以下代码:
<ContentPresenter ContentSource="Header"/>

您需要对实际具有“Header”控件进行模板化才能实现此操作(通常是HeaderedContentControl)。

谢谢 - 我不想使用绑定,因为我想将DataContext(ViewModel)分配给整个窗口(等同于View),然后从插入到控件“插槽”的按钮中绑定它的命令 - 因此在层次结构中使用任何绑定都会破坏View的 DC的继承。至于另一个选项 - 是的,在这种情况下,从HeaderedContentControl派生将起作用,但是如果我需要三个部分怎么办?我如何制作自己的“具有标题和页脚的内容控件”(或者如果我没有HeaderedContentControl,我该如何实现它)? - Tomáš Kafka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接