Вопрос по contentpresenter, user-controls, wpf – WPF: шаблон или UserControl с 2 (или более!) ContentPresenters для представления контента в «слотах»

25

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

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

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

UserControl xaml:

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

Возможно ли что-то подобное? Как мне сообщить VS, что мой элемент управления предоставляет два заполнителя содержимого, чтобы я мог использовать его следующим образом?

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

Или, может быть, я мог бы использовать ControlTemplate для окнакак здесь, но опять же: Window имеет только один слот контента, поэтому в его шаблоне может быть только один докладчик, но мне нужно два (и если в этом случае было бы возможно пойти с одним, есть другие случаи использования, когда подойдет несколько слотов контента, просто подумайте о шаблоне для пользователя, управляющего статьями, который предоставит заголовок, (структурированный) контент, имя автора, изображение ...).

Спасибо!

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

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

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

Что касается идеи наследования от HeaderedContentControl - да, в этом случае это будет работать, но что, если я хочу три сменные части? Как мне создать свой собственный & quot; HeaderedAndFooteredContentControl & quot;(or, how would I implement HeaderedContentControl if I didn't have one)?

EDIT2: OK, so my two solutions doen't work - this is why: ContentPresenter получает его содержимое из DataContext, но мне нужны привязки для содержащихся элементов, чтобы связать их с исходными окнами. (Родитель UserControl в логическом дереве) DataContext - потому что таким образом, когда я встраиваю текстовое поле, привязанное к свойству ViewModel, оно не привязывается, так какthe inheritance chain has been broken inside the control!

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

EDIT3: I have a solution!, удалил мои предыдущие ответы. Смотрите мой ответ.

Ваш Ответ

3   ответа
2

If you're using a UserControl

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

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

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

And with a ControlTemplate

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

<ContentPresenter ContentSource="Header"/>

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

Спасибо. Я не хочу использовать привязку, поскольку я хочу назначить DataContext (ViewModel) всему окну (что равно View), а затем привязать к нему команды от кнопок, вставленных в слоты управления. - поэтому любое использование привязки в иерархии нарушило бы наследование DC представления. Что касается другого варианта - да, в этом случае производное от HeaderedContentControl будет работать, но что, если я хочу три части? Как мне создать свой собственный & quot; HeaderedAndFooteredContentControl & quot; (или как бы я реализовал HeaderedContentControl, если бы у меня его не было)? Tomáš Kafka
4

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

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

http://www.interact-sw.co.uk/iangblog/2007/02/14/wpfdefaulttemplate http://www.codeproject.com/Articles/37326/Lookless-Controls-Themes.aspx http://www.codeproject.com/Articles/35444/Defining-the-Default-Style-for-a-Lookless-Control.aspx http://blog.pixelingene.com/?p=58

Короче:

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

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

Слава богу, кто-то объяснил это в простой и понятной форме! Трудно найти ресурсы по этому поводу, или это просто ошибка начинающих wpf: p

Похожие вопросы