WPF: шаблон или UserControl с 2 (или более!) ContentPresenters для представления контента в «слотах» - PullRequest
25 голосов
/ 23 июня 2009

Я занимаюсь разработкой 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), а затем привязать его команды от кнопок, вставленных в «слоты» управления - так любое использование привязки в иерархии нарушило бы наследование DC представления.

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

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

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

РЕДАКТИРОВАТЬ3: У меня есть решение! , удалил мои предыдущие aswers. Смотрите мой ответ.

Ответы [ 3 ]

31 голосов
/ 02 ноября 2009

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

Короче говоря:

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

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

4 голосов
/ 23 июня 2009

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>
2 голосов
/ 23 июня 2009

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

Полагаю, вы действительно хотите:

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

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

И с шаблоном управления

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

<ContentPresenter ContentSource="Header"/>

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

...