简体   繁体   English

在WPF中的ContentControl中托管ViewModel

[英]Hosting ViewModels in ContentControl in WPF

I have a traditional form layout with a menu bar at the top and status bar at the bottom. 我有一个传统的表单布局,顶部是菜单栏,底部是状态栏。 When the user selects a menu item, the space in-between (the form's entire remaining client area) gets replaced with a user control - think of an SDI app that can host multiple types of documents. 当用户选择菜单项时,中间的空间(表单的整个剩余客户区)被用户控件替换 - 想想可以托管多种类型文档的SDI应用程序。

If you know of a better way to go about this, please chime in. For now, I'm trying to get it to work in a very simplified version with a ContentControl, but I cannot get it to update the screen when its DataContext is set. 如果你知道一个更好的方法来解决这个问题,请加入。现在,我试图让它在一个带有ContentControl的非常简化的版本中运行,但是当它的DataContext是它时,我无法让它更新屏幕组。

Here's the very simple code for ViewModelA. 这是ViewModelA非常简单的代码。 ViewModelB is identical, except for the Bs. 除了Bs之外,ViewModelB是相同的。

namespace Dynamic_ContentControl
{
    public class ViewModelA: ViewModelBase
    {
        public ViewModelA()
        {
            DisplayName = "This is A";
        }
    }
}

The main window is very simple. 主窗口非常简单。 It basically declares a property to hold the view model of the hosted control and exposes two commands to assign view models A or B. 它基本上声明了一个属性来保存托管控件的视图模型,并公开两个命令来分配视图模型A或B.

namespace Dynamic_ContentControl
{
    public class MainViewModel: ViewModelBase
    {
        private ViewModelBase clientContent = null;

        public ICommand ShowA { get; private set; }
        public ICommand ShowB { get; private set; }
        public ViewModelBase ClientContent {
            get
            {
                return clientContent;
            }
            private set
            {
                clientContent = value;
                OnPropertyChanged("ClientContent");
            }
        }
        public MainViewModel()
        {
            ShowA = new RelayCommand((obj) =>
            {
                ClientContent = new ViewModelA();
            });
            ShowB = new RelayCommand((obj) =>
            {
                ClientContent = new ViewModelB();
            });
        }
    }
}

Finally, the XAML declares a ContentControl and sets its ContentTemplate to a DataTemplate called ClientAreaTemplate, whose ContentPresenter points to another DataTemplate, named TextBlockLayout: 最后,XAML声明一个ContentControl并将其ContentTemplate设置为名为ClientAreaTemplate的DataTemplate,其ContentPresenter指向另一个名为TextBlockLayout的DataTemplate:

<Window x:Class="Dynamic_ContentControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:Dynamic_ContentControl"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>
    <Window.Resources>
        <DataTemplate x:Key="TextBlockLayout">
            <TextBlock Text="{Binding Path=DisplayName}" />
        </DataTemplate>
        <DataTemplate x:Key="ButtonLayout">
            <Button Content="{Binding Path=DisplayName}" />
        </DataTemplate>
        <DataTemplate x:Key="CheckBoxLayout">
            <CheckBox Content="{Binding Path=DisplayName}" />
        </DataTemplate>
        <DataTemplate x:Key="ClientAreaTemplate">
            <ContentPresenter x:Name="ContentArea" ContentTemplate="{StaticResource ResourceKey=TextBlockLayout}"/>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=DataContext}"
                             Value="{x:Type vm:ViewModelB}">
                    <Setter TargetName="ContentArea"
                            Property="ContentTemplate"
                            Value="{StaticResource ResourceKey=ButtonLayout}" />
                </DataTrigger>
                <DataTrigger Binding="{Binding Path=DataContext}"
                             Value="{x:Type vm:ViewModelB}">
                    <Setter TargetName="ContentArea"
                            Property="ContentTemplate"
                            Value="{StaticResource ResourceKey=CheckBoxLayout}" />
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <Button Content="Show A"
                Command="{Binding Path=ShowA}"
                HorizontalAlignment="Left"
                Margin="10,10,0,0"
                VerticalAlignment="Top"
                Width="75" />
        <Button Content="Show B"
                Command="{Binding ShowB}"
                HorizontalAlignment="Left"
                Margin="90,10,0,0"
                VerticalAlignment="Top"
                Width="75" />
        <Label Content="{Binding Path=ClientContent.DisplayName}"
               HorizontalAlignment="Left"
               Margin="170,8,0,0"
               VerticalAlignment="Top" />
        <ContentControl DataContext="{Binding Path=ClientContent}"
                        Content="{Binding}"
                        ContentTemplate="{StaticResource ResourceKey=ClientAreaTemplate}"
                        HorizontalAlignment="Left"
                        Margin="10,37,0,0"
                        VerticalAlignment="Top"
                        Height="198"
                        Width="211" />

    </Grid>
</Window>

Expected behaviour

When the screen opens, I want TextBoxLayout to display. 当屏幕打开时,我想要显示TextBoxLayout。 If the user then clicks on one of the two buttons, it should load either a ButtonLayout or CheckBoxLayout, depending on the actual runtime type of the view model that is assigned. 如果用户然后单击两个按钮之一,则应加载ButtonLayout或CheckBoxLayout,具体取决于分配的视图模型的实际运行时类型。

Actual behaviour

The screen opens with the TextBoxLayout loaded, but it never changes to another type as I click the buttons. 屏幕打开时加载了TextBoxLayout,但在单击按钮时它永远不会更改为其他类型。

I think the problem is the way the DataTrigger tries to compare with the type, but there are no binding messages at all the Output window. 我认为问题是DataTrigger尝试与类型进行比较的方式,但是在所有Output窗口中都没有绑定消息。

In this situation, you need to use DataTemplateSelector : 在这种情况下,您需要使用DataTemplateSelector

Provides a way to choose a DataTemplate based on the data object and the data-bound element. 提供基于数据对象和数据绑定元素选择DataTemplate方法。

Here is a version of dynamic DataTemplateSelector which returns a desired DataTemplate depending on the type: Here是动态DataTemplateSelector一个版本,它根据类型返回所需的DataTemplate

/// <summary>
/// Provides a means to specify DataTemplates to be selected from within WPF code
/// </summary>
public class DynamicTemplateSelector : DataTemplateSelector
{
    /// <summary>
    /// Generic attached property specifying <see cref="Template"/>s
    /// used by the <see cref="DynamicTemplateSelector"/>
    /// </summary>
    /// <remarks>
    /// This attached property will allow you to set the templates you wish to be available whenever
    /// a control's TemplateSelector is set to an instance of <see cref="DynamicTemplateSelector"/>
    /// </remarks>
    public static readonly DependencyProperty TemplatesProperty =
        DependencyProperty.RegisterAttached("Templates", typeof(TemplateCollection), typeof(DataTemplateSelector),
        new FrameworkPropertyMetadata(new TemplateCollection(), FrameworkPropertyMetadataOptions.Inherits));


    /// <summary>
    /// Gets the value of the <paramref name="element"/>'s attached <see cref="TemplatesProperty"/>
    /// </summary>
    /// <param name="element">The <see cref="UIElement"/> who's attached template's property you wish to retrieve</param>
    /// <returns>The templates used by the givem <paramref name="element"/>
    /// when using the <see cref="DynamicTemplateSelector"/></returns>
    public static TemplateCollection GetTemplates(UIElement element)
    {
        return (TemplateCollection)element.GetValue(TemplatesProperty);
    }

    /// <summary>
    /// Sets the value of the <paramref name="element"/>'s attached <see cref="TemplatesProperty"/>
    /// </summary>
    /// <param name="element">The element to set the property on</param>
    /// <param name="collection">The collection of <see cref="Template"/>s to apply to this element</param>
    public static void SetTemplates(UIElement element, TemplateCollection collection)
    {
        element.SetValue(TemplatesProperty, collection);
    }

    /// <summary>
    /// Overriden base method to allow the selection of the correct DataTemplate
    /// </summary>
    /// <param name="item">The item for which the template should be retrieved</param>
    /// <param name="container">The object containing the current item</param>
    /// <returns>The <see cref="DataTemplate"/> to use when rendering the <paramref name="item"/></returns>
    public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
    {
        //This should ensure that the item we are getting is in fact capable of holding our property
        //before we attempt to retrieve it.
        if (!(container is UIElement))
            return base.SelectTemplate(item, container);

        //First, we gather all the templates associated with the current control through our dependency property
        TemplateCollection templates = GetTemplates(container as UIElement);
        if (templates == null || templates.Count == 0)
            base.SelectTemplate(item, container);

        //Then we go through them checking if any of them match our criteria
        foreach (var template in templates)
            //In this case, we are checking whether the type of the item
            //is the same as the type supported by our DataTemplate
            if (template.Value.IsInstanceOfType(item))
                //And if it is, then we return that DataTemplate
                return template.DataTemplate;

        //If all else fails, then we go back to using the default DataTemplate
        return base.SelectTemplate(item, container);
    }
}

/// <summary>
/// Holds a collection of <see cref="Template"/> items
/// for application as a control's DataTemplate.
/// </summary>
public class TemplateCollection : List<Template>
{

}

/// <summary>
/// Provides a link between a value and a <see cref="DataTemplate"/>
/// for the <see cref="DynamicTemplateSelector"/>
/// </summary>
/// <remarks>
/// In this case, our value is a <see cref="System.Type"/> which we are attempting to match
/// to a <see cref="DataTemplate"/>
/// </remarks>
public class Template : DependencyObject
{
    /// <summary>
    /// Provides the value used to match this <see cref="DataTemplate"/> to an item
    /// </summary>
    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(Type), typeof(Template));

    /// <summary>
    /// Provides the <see cref="DataTemplate"/> used to render items matching the <see cref="Value"/>
    /// </summary>
    public static readonly DependencyProperty DataTemplateProperty =
       DependencyProperty.Register("DataTemplate", typeof(DataTemplate), typeof(Template));

    /// <summary>
    /// Gets or Sets the value used to match this <see cref="DataTemplate"/> to an item
    /// </summary>
    public Type Value
    { get { return (Type)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } }

    /// <summary>
    /// Gets or Sets the <see cref="DataTemplate"/> used to render items matching the <see cref="Value"/>
    /// </summary>
    public DataTemplate DataTemplate
    { get { return (DataTemplate)GetValue(DataTemplateProperty); } set { SetValue(DataTemplateProperty, value); } }
}

Example of using

<local:DynamicTemplateSelector x:Key="MyTemplateSelector" />

<DataTemplate x:Key="StringTemplate">
    <TextBlock>
        <Run Text="String: " />
        <Run Text="{Binding}" />
    </TextBlock>
</DataTemplate>

<DataTemplate x:Key="Int32Template">
    <TextBlock>
        <Run Text="Int32: " />
        <Run Text="{Binding}" />
    </TextBlock>
</DataTemplate>

<Style x:Key="MyListStyle" TargetType="ListView">
    <Setter Property="ItemTemplateSelector" Value="{StaticResource MyTemplateSelector}"/>
    <Setter Property="local:DynamicTemplateSelector.Templates">
        <Setter.Value>
            <local:Templates>
                <local:Template Value={x:Type String} DataTemplate={StaticResource StringTemplate}/>
                <local:Template Value={x:Type Int32} DataTemplate={StaticResource Int32Template}/>
            </local:Templates>
        </Setter.Value>
    </Setter>
</Style>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM