tags:

views:

112

answers:

3

I have two HeaderedContentControls like those below that each have their content property bound to one of two view model properties of the same base type (one control is on the left side of the window and one on the right, thus the view model property names).

However, either view model property can be one of four different derived types. So the left could be an Airplane and the right can be a Car. Then later, the left could be a Boat and right could be an Airplane. I would like the style property of the header controls to be dynamic based on the derived type. What's the best way to do this declaratively?

<Window...>
    <StackPanel 
        Grid.Row="2"
        Orientation="Horizontal" VerticalAlignment="Top">
        <Border 
            Height="380" 
            Width="330"
            Margin="0,0,4,0"
            Style="{StaticResource MainBorderStyle}">
            <HeaderedContentControl
                Content="{Binding Path=LeftChild}"
                Header="{Binding LeftChild.DisplayName}"
                Style="{StaticResource StandardHeaderStyle}"
            />
        </Border>

        <Border 
            Height="380" 
            Width="330"
            Style="{StaticResource MainBorderStyle}">
            <HeaderedContentControl
                Content="{Binding Path=RightChild}"
                Header="{Binding RightChild.DisplayName}"
                Style="{StaticResource StandardHeaderStyle}"
            />  
        </Border>
    </StackPanel>
</Window>


<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vm="clr-namespace:myViewModelNamespace;assembly=myViewModelAssembly"
    xmlns:vw="clr-namespace:myViewNamespace" >

    <!--***** Item Data Templates ****-->
    <DataTemplate DataType="{x:Type vm:CarViewModel}">
        <vw:CarView />
    </DataTemplate>

    <DataTemplate DataType="{x:Type vm:BoatViewModel}">
        <vw:BoatView />
    </DataTemplate>

    <DataTemplate DataType="{x:Type vm:AirplaneViewModel}">
        <vw:AirplaneView />
    </DataTemplate>

    <!--***** 
        Other stuff including the StandardHeaderStyle and the MainBorderStyle
    ****-->

</ResourceDictionary>
A: 

try using the Style Selector class:

http://msdn.microsoft.com/en-us/library/system.windows.controls.styleselector.aspx

I haven't used it myself specifically, so i don't have any sample code for you to check out, but the MSDN link has some.

Alastair Pitts
It's no use in the context mentioned. You can apply this selector only to an `ItemsControl` and vary styles of item containers basing on items' dynamic type or whatever you like on. However you can't use it with one-item controls simply 'cause they don't have any property inside which you would be able to put your selector.
archimed7592
Ah, I wasn't sure. It seemed like the logical thing. Thanks for the clarification.
Alastair Pitts
+1  A: 

Are you sure you need to vary HeaderedContentControl's Style, not the ContentTemplate basing on Content's dynamic type? In other words: do you need to vary the control's style or you just need to vary the item's data-template?

Because there is very handy property ContentTemplateSelector and if you'll write very simple class you'll be able to select the DataTemplate basing on content's dynamic type.

If that's not the case and you are sure you need to vary the Style, then could you please elaborate a little which parts of the style you'd like to vary - maybe there a workaround through the same ContentTemplateSelector is available.

In case you insist on varying the styles, think a little about using data trigger inside your style - using a very simple converter you'll be able to vary certain properties (or all of them if you prefer) of your style.

I'll be glad to provide you further assistance as soon as you'll elaborate the specifics of your problem.

UPD: OK, author insists that he need to vary the Style. Here are two possible ways of how you can do that.

First and simple solution, but severely limited one: since your Header content can be specified through Content content you can do this:

<DataTemplate x:Key="DefaultTemplate">
    <HeaderedContentControl Content="{Binding}"
                            Header="{Binding DisplayName}"
                            Style="{StaticResource DefaultStyle}" />
</DataTemplate>
<DataTemplate x:Key="CarTemplate"
              DataType="dm:Car">
    <HeaderedContentControl Content="{Binding}"
                            Header="{Binding DisplayName}"
                            Style="{StaticResource CarStyle}" />
</DataTemplate>
<DataTemplate x:Key="BoatTemplate"
              DataType="dm:Boat">
    <HeaderedContentControl Content="{Binding}"
                            Header="{Binding DisplayName}"
                            Style="{StaticResource BoatStyle}" />
</DataTemplate>

<u:TypeBasedDataTemplateSelector x:Key="MySelector"
                                 DefaultTemplate="{StaticResource DefaultTemplate}"
                                 NullTemplate="{StaticResource DefaultTemplate}">
    <u:TypeMapping Type="dm:Car" Template="{StaticResource CarTemplate}" />
    <u:TypeMapping Type="dm:Boat" Template="{StaticResource BoatTemplate}" />
</u:TypeBasedDataTemplateSelector>

<ContentPresenter Content="{Binding LeftChild}"
                  ContentTemplateSelector="{StaticResource MySelector}" />

The only code you'll need to back this purely declarative solution is a very simple template selector implementation. Here it goes:

public class TypeMapping
{
    public Type Type { get; set; }
    public DataTemplate Template { get; set; }
}

public class TypeBasedDataTemplateSelector : DataTemplateSelector, IAddChild
{
    public DataTemplate DefaultTemplate { get; set; }
    public DataTemplate NullTemplate { get; set; }
    private readonly Dictionary<Type, DataTemplate> Mapping = new Dictionary<Type, DataTemplate>();

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item == null)
            return NullTemplate;

        DataTemplate template;

        if (!Mapping.TryGetValue(item.GetType(), out template))
            template = DefaultTemplate;

        return template;
    }


    #region IAddChild Members

    public void AddChild(object value)
    {
        if (!(value is TypeMapping))
            throw new Exception("...");

        var tm = (TypeMapping)value;

        Mapping.Add(tm.Type, tm.Template);
    }

    public void AddText(string text)
    {
        throw new NotImplementedException();
    }

    #endregion
}

The second solution is more generic and can be applied to the cases where Header content has nothing to do with Content content. It bases on the Binding's converter capabilities.

<Style x:Key="StandardHeaderedStyle">
    <!--...-->
</Style>

<Style x:Key="CarHeaderedStyle"
       BasedOn="{StaticResource StandardHeaderedStyle}">
    <!--...-->
</Style>

<Style x:Key="BoatHeaderedStyle"
       BasedOn="{StaticResource StandardHeaderedStyle}">
    <!--...-->
</Style>

<Style x:Key="UnknownHeaderedStyle"
       BasedOn="{StaticResource StandardHeaderedStyle}">
    <!--...-->
</Style>

<u:StylesMap x:Key="MyStylesMap" 
             FallbackStyle="{StaticResource UnknownHeaderedStyle}">
    <u:StyleMapping Type="Car" Style="{StaticResource CarHeaderedStyle}" />
    <u:StyleMapping Type="Boat" Style="{StaticResource BoatHeaderedStyle}" />
</u:StylesMap>

<u:StyleSelectorConverter x:Key="StyleSelectorConverter" />

<HeaderedContentControl Content="{Binding LeftChild}"
                        Header="{Binding LeftChild.DisplayName}">
    <HeaderedContentControl.Style>
        <Binding Path="LeftChild"
                 Converter="{StaticResource StyleSelectorConverter}"
                 ConverterParameter="{StaticResource MyStylesMap}" />
    </HeaderedContentControl.Style>
</HeaderedContentControl>

It also requires some of backing code:

public class StyleMapping
{
    public Type Type { get; set; }
    public Style Style { get; set; }
}

public class StylesMap : Dictionary<Type, Style>, IAddChild
{
    public Style FallbackStyle { get; set; }

    #region IAddChild Members

    public void AddChild(object value)
    {
        if (!(value is StyleMapping))
            throw new InvalidOperationException("...");

        var m = (StyleMapping)value;

        this.Add(m.Type, m.Style);
    }

    public void AddText(string text)
    {
        throw new NotImplementedException();
    }

    #endregion
}

public class StyleSelectorConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var m = (StylesMap)parameter;

        if (value == null)
            return m.FallbackStyle;

        Style style;
        if (!m.TryGetValue(value.GetType(), out style))
            style = m.FallbackStyle;

        return style;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion
}

HTH

archimed7592
Archimed, thank you for the response. I originally simplified my code for the example, but have expanded it further your and Dabblernl's answers. I am new to WPF, so I may have this structured entirely wrong. However, it works perfectly other than the fact that instead of the one StandardHeaderStyle I need to create three header styles with the appropriate one being applied based on whether the derived type is a Car, Boat or Airplane.
MarkB
Mark, I've updated my post with two possible solutions. However, before taking any of approaches into account, make sure you've looked at triggers, control templates and a bunch of other techniques which can help you to create the same variance of styles in much more simple way...
archimed7592
A: 

My answer is an elaboration on Archimed's. Don't hesitate to ask further!

<Window x:Class="Datatemplate_selector.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
xmlns:local="clr-namespace:Datatemplate_selector">
<Window.Resources>
    <DataTemplate DataType="{x:Type local:CarDetail}">
     <Border BorderBrush="Yellow" BorderThickness="2">
        <HeaderedContentControl Margin="4" Foreground="Red">
            <HeaderedContentControl.Header>
                <Border BorderBrush="Aquamarine" BorderThickness="3">
                    <TextBlock Text="{Binding Name}"/>
                </Border>
             </HeaderedContentControl.Header>
            <HeaderedContentControl.Content>
                <Border BorderBrush="CadetBlue" BorderThickness="1">
                    <TextBlock TextWrapping="Wrap" Text="{Binding Description}"/>
                </Border>
            </HeaderedContentControl.Content>
        </HeaderedContentControl>
       </Border>
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:HouseDetail}">
        <HeaderedContentControl Margin="4" Foreground="Yellow" FontSize="20"
                    Header="{Binding Name}">
            <HeaderedContentControl.Content>
                <TextBlock Foreground="BurlyWood"  TextWrapping="Wrap"
                           Text="{Binding Description}"/>
            </HeaderedContentControl.Content>
        </HeaderedContentControl>
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:ItemDetail}">
        <HeaderedContentControl Margin="4" Foreground="Green" FontStyle="Italic" 
                    Content="{Binding Description}"
                    Header="{Binding Name}">
        </HeaderedContentControl>
    </DataTemplate>
</Window.Resources>
<StackPanel>
    <ItemsControl ItemsSource="{Binding ItemDetails}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <UniformGrid Columns="2"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</StackPanel>

using System.Collections.ObjectModel;
using System.Windows;

namespace Datatemplate_selector
{
   public partial class Window1 : Window
    {
        public ObservableCollection<ItemDetail> ItemDetails { get; set; }

        public Window1()
        {
            ItemDetails = new ObservableCollection<ItemDetail>
                              {
                                  new CarDetail{Name="Trabant"},
                                  new HouseDetail{Name="Taj Mahal"}
                              };
            DataContext = this;
            InitializeComponent();
        }
    }

    public class ItemDetail:DependencyObject
    {
        public string Name
        {
            get { return (string)GetValue(NameProperty); }
            set { SetValue(NameProperty, value); }
        }

        public static readonly DependencyProperty NameProperty =
            DependencyProperty.Register("Name",
            typeof(string),
            typeof(ItemDetail),
            new UIPropertyMetadata(string.Empty));

        public virtual string Description
        {
             get { return Name + " has a lot of details"; }
        }
    }

    public class CarDetail:ItemDetail
    {
        public override string Description
        {
            get { return string.Format("The car {0} has two doors and a max speed of 90 kms/hr", Name); }
        }
    }

    public class HouseDetail:ItemDetail
    {
        public override string Description
        {
            get { return string.Format("The house {0} has two doors and a backyard", Name); }
        }
    }
}

PS: I thought that this use of inheritance in a generic collection was not supported in .Net 3. I am pleasurably surprised that this code works!

Dabblernl
Dabblernl, thanks for the response. I have expanded my code above - also see the comment on Archimed's answer.
MarkB
Thanks for the clarification. I still think that the approach through the DataTemplate is best. I edited my code to show you that it gives the result you want, albeit in a different way.
Dabblernl
Thanks again. I understand what you're doing but I'm not sure how to get from my code to yours. If I cut my HeaderedContentControls out of my window xaml and paste them in to my the DataTemplate blocks of my ResourceDictionary xaml, I believe I will be doing basically the same thing as you - that is, having the HeaderedContentControl in the DataTemplate.But then, if I do that, what goes in the empty space where the HeaderedContentControls used to be in the Window xaml? That is, if I move them into the DataTemplates, how do I insert them back into the Borders in the Window xaml?
MarkB
But this looks like the right approach. The DataTemplates are where the derived types are differentiated - so that is where the different styles need to be defined also.
MarkB
"But then, if I do that, what goes in the empty space where the HeaderedContentControls used to be in the Window xaml?"Look at my XAML: the HeaderedContentControls will appear inside the ItemControl.You can set their appearance with the ItemsPanelTemplate
Dabblernl
Also you can add the border around the HeaderedContentControle in the DataTemplate
Dabblernl
Yea, I guess I am trying to avoid going to the ItemControl because I already have my two separate Border controls layed and my view model has two separate properties of the base type (as opposed to a collection) - one for each Border area on the window. I could re-structure all of that - but it doesn't seem like how that is structured is related to the original problem.
MarkB
Yes, I could move the Border into the DataTemplate also, surrounding the HeaderedItemControls.
MarkB
And then that would be where both approaches intersect: with an empty StackPanel. In your approach there is an ItemsControl in the StackPanel. In my approach I guess I am looking for two specific containers I can stick into the StackPanel to refer to my DataTemplates. But if that's not do-able, I can probably go with the ItemsControl. I was just concerned that it might introduce other issues that I might have problems with and, as I said, whether it is two separate things or a collection doesn't initially seem to be relevant to the original problem.
MarkB
One reason why the ItemsControl would not be ideal is because in my view model now, I have a left property and a right property that correspond to the two separate UI elements. If I go with a collection in the view model, then the index of the collection items would be the determining factor for which UI element was which. I guess index 0 would be left and 1 would be right. Again, not the end of the world, but it just seems awkward.
MarkB
You like to prefer to keep two seperate HeaderedContentControls. You can acomplish that by using two ItemControls and Setting their ItemsSource to the LeftChild and the RighChild respectively. You must change the type of LeftChild and RightChild to Collections with just one member for this to work.
Dabblernl
Thanks Dabblernl!
MarkB
**But then, if I do that, what goes in the empty space where the HeaderedContentControls used to be in the Window xaml? That is, if I move them into the DataTemplates, how do I insert them back into the Borders in the Window xaml?**Look, dude, if the problem is "what should I place instead of the blank space", then you could have just asked it :). Take a look at `ContentPresenter` framework element. There is also an example of using it at the end of "the first and the simple" solution, I've provided in my answer.
archimed7592