WPF: шаблон или UserControl с 2 (или более!) Объектами ContentPresenter для представления содержимого в «слотах»

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

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

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

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

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

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

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


person Tomáš Kafka    schedule 22.06.2009    source источник


Ответы (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>

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

person Tomáš Kafka    schedule 02.11.2009
comment
Слава богу, кто-то объяснил это простым и понятным языком! Об этом сложно найти ресурсы, или это просто ошибка начинающих wpf: p - person Shion; 25.07.2012

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>
person Tomáš Kafka    schedule 23.06.2009
comment
Пожалуйста, не делайте этого, используйте мой второй ответ stackoverflow.com/questions/1029955/ вместо этого! - person Tomáš Kafka; 02.11.2009

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

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

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

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

И с помощью ControlTemplate

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

<ContentPresenter ContentSource="Header"/>

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

person Alun Harford    schedule 23.06.2009
comment
Спасибо - я не хочу использовать привязку, так как я хочу назначить DataContext (ViewModel) всему окну (которое равно View), а затем привязать к нему команды с кнопок, вставленных в `` слоты '' управления - поэтому любое использование привязка в иерархии нарушит наследование контроллера домена View. Что касается другого варианта - да, в этом случае наследование от HeaderedContentControl будет работать, но что, если мне нужны три части? Как мне создать свой собственный HeaderedAndFooteredContentControl (или как мне реализовать HeaderedContentControl, если бы у меня его не было)? - person Tomáš Kafka; 23.06.2009