Hosting ViewModels in ContentControl in WPF

2019-04-12 11:11发布

问题:

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.

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.

Here's the very simple code for ViewModelA. ViewModelB is identical, except for the Bs.

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.

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:

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

Actual behaviour

The screen opens with the TextBoxLayout loaded, but it never changes to another type as I click the buttons.

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.

回答1:

In this situation, you need to use DataTemplateSelector:

Provides a way to choose a DataTemplate based on the data object and the data-bound element.

Here is a version of dynamic DataTemplateSelector which returns a desired DataTemplate depending on the type:

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


标签: c# wpf xaml mvvm