Подтвердить что ты не робот

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 DC - DialogControl just passes it through -->
        </gui:DialogControl.Controls>
        <gui:DialogControl.Buttons>
            <!-- My dialog 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 для окна как здесь, но опять же: окно имеет только один слот для контента, поэтому его шаблон сможет иметь только один ведущий, но мне нужно два (и если в этом случае возможно, возможно, с ним можно пойти, есть и другие варианты использования, в которых будет доступно несколько слотов контента, просто подумайте о шаблоне для пользователя, контролирующего статьи, (структурированный) контент, имя автора, изображение...).

Спасибо!

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

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

EDIT: я не хочу использовать привязку, так как я хочу назначить DataContext (ViewModel) на целое окно (которое равно View), а затем привязать к нему команды из кнопок, вставленных в "слоты" управления, - так любое использование привязки в иерархии нарушит наследование View DC.

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

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

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

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

4b9b3361

Ответ 1

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

Короче:

Подкласс представляет собой подходящий класс (или UIElement, если вам не подходит) - файл просто *.cs, поскольку мы определяем поведение, а не внешний вид элемента управления.

public class EnhancedItemsControl : ItemsControl

Добавить свойство зависимостей для ваших "слотов" (нормальное свойство недостаточно хорошее, так как оно имеет ограниченную поддержку привязки). Прохладный трюк: в VS, напишите propdp и нажмите вкладку, чтобы развернуть фрагмент:):

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

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

Ответ 2

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

Ответ 3

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

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

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

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

И с помощью ControlTemplate

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

<ContentPresenter ContentSource="Header"/>

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