WPF: шаблон или UserControl с 2 (или более!) ContentPresenters для представления контента в "слотах"

Я занимаюсь разработкой LOB-приложения, в котором мне понадобится несколько диалоговых окон (и отображение всего в одном окне не вариант / не имеет смысла).

Я хотел бы иметь пользовательский элемент управления для моего окна, который бы определял некоторые стили и т. Д., И имел бы несколько слотов, в которые можно было бы вставлять контент - например, шаблон модального диалогового окна имел бы слот для контента и для кнопок (так что пользователь может затем предоставить контент и набор кнопок со связанными ICommands).

Я хотел бы иметь что-то вроде этого (но это не работает):

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 имеет только один слот контента, поэтому его шаблон сможет иметь только одного докладчика, но мне нужно два (и если в этом случае это может быть Можно пойти с одним, есть и другие случаи использования, когда под рукой будет несколько слотов контента, просто подумайте о шаблоне для статьи - пользователь элемента управления предоставит заголовок, (структурированный) контент, имя автора, изображение...).

Спасибо!

PS: Если бы я хотел, чтобы кнопки располагались рядом, как я могу поместить несколько элементов управления (кнопок) в StackPanel? ListBox имеет ItemsSource, а StackPanel - нет, а его свойство Children доступно только для чтения, поэтому это не работает (внутри usercontrol):

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

РЕДАКТИРОВАТЬ: я не хочу использовать привязку, так как я хочу назначить DataContext (ViewModel) для всего окна (что равно View), а затем привязать к его командам от кнопок, вставленных в "слоты" управления - так что любое использование привязка в иерархии нарушит наследование DC представления.

Что касается идеи наследования от HeaderedContentControl - да, в этом случае это будет работать, но что если я захочу три сменные части? Как мне сделать свой собственный "HeaderedAndFooteredContentControl" (или, как бы я реализовал HeaderedContentControl, если бы у меня его не было)?

EDIT2: ОК, поэтому мои два решения не работают - вот почему: ContentPresenter получает свой контент из DataContext, но мне нужны привязки к содержащимся элементам для ссылки на оригинальные окна (родительский элемент UserControl в логическом дереве) DataContext - потому что таким образом, когда я встраиваю текстовое поле, привязанное к свойству ViewModel, оно не привязывается, поскольку цепочка наследования была разорвана внутри элемента управления!

Кажется, мне нужно сохранить DataContext родительского объекта и восстановить его для потомков всех контейнеров элемента управления, но я не получаю никакого события, когда DataContext в логическом дереве изменился.

EDIT3: у меня есть решение!, удалил мои предыдущие ответы. Смотрите мой ответ.

3 ответа

Решение

Хорошо, мое решение было совершенно ненужным, вот единственные учебники, которые вам понадобятся для создания любого пользовательского элемента управления:

Короче:

Подкласс некоторого подходящего класса (или 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*/);

Добавьте атрибут для дизайнера (потому что вы создаете так называемый элемент управления без внешнего вида), таким образом, мы говорим, что нам нужно иметь ContentPresenter с именем PART_AlternativeContentPresenter в нашем шаблоне

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

Вуаля, вы только что создали свой первый простой пользовательский элемент управления (добавьте больше контент-презентаторов и свойств зависимостей для большего количества "слотов контента").

Hasta la victoria siempre!

Я пришел с рабочим решением (сначала в интернете, мне кажется:))

Сложный 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>

Если вы используете UserControl

Я предполагаю, что вы действительно хотите:

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

Это предполагает, что DataContext, передаваемый вашему элементу управления, имеет свойство Buttons.

И с ControlTemplate

Другой вариант будет ControlTemplate, и тогда вы можете использовать:

<ContentPresenter ContentSource="Header"/>

Для этого вам нужно было бы создать шаблон элемента управления, который на самом деле имеет "заголовок" (обычно HeaderedContentControl).

Другие вопросы по тегам