Use different template for last item in a WPF itemscontrol - wpf

I'm using a custom template in my itemscontrol to display the following result:
item 1, item 2, item3,
I want to change the template of the last item so the result becomes:
item 1, item2, item3
The ItemsControl:
<ItemsControl ItemsSource="{Binding Path=MyCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}"/>
<TextBlock Text=", "/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Is there anyone who can give a solution for my problem? Thank you!

I've found the solution for my problem using only XAML. If there is anybody who needs to do the same, use this:
<ItemsControl ItemsSource="{Binding Path=MyCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="comma" Text=", "/>
<TextBlock Text="{Binding}"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}">
<Setter TargetName="comma" Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

You can use DataTemplateSelector, in SelectTemplate() method you can check whether item is the last and then return an other template.
In XAML:
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentPresenter
ContentTemplateSelector = "{StaticResource MyTemplateSelector}">
In Code behind:
private sealed class MyTemplateSelector: DataTemplateSelector
{
public override DataTemplate SelectTemplate(
object item,
DependencyObject container)
{
// ...
}
}

This solution affects the last row and updates with changes to the underlying collection:
CodeBehind
The converter requires 3 parameters to function properly - the current item, the itemscontrol, the itemscount, and returns true if current item is also last item:
class LastItemConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
int count = (int)values[2];
if (values != null && values.Length == 3 && count>0)
{
System.Windows.Controls.ItemsControl itemsControl = values[0] as System.Windows.Controls.ItemsControl;
var itemContext = (values[1] as System.Windows.Controls.ContentPresenter).DataContext;
var lastItem = itemsControl.Items[count-1];
return Equals(lastItem, itemContext);
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
The Data-Trigger for a DataTemplate, that includes a textbox named 'PART_TextBox':
<DataTemplate.Triggers>
<DataTrigger Value="True" >
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource LastItemConverter}">
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" />
<Binding RelativeSource="{RelativeSource Self}"/>
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}" Path="Items.Count"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Foreground" TargetName="PART_TextBox" Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
The converter as a static resource in the Xaml
<Window.Resources>
<local:LastItemConverter x:Key="LastItemConverter" />
</Window.Resources>
SnapShot
And a snapshot of it in action
The code has been added to the itemscontrol from this 'codeproject'
https://www.codeproject.com/Articles/242628/A-Simple-Cross-Button-for-WPF
Note the last item's text in red

One question... I see you're using an ItemsControl as opposed to say a ListBox and that it appears to be bound to a collection of strings, and that you're only trying to display the resulting text without formatting the individual parts, which makes me wonder if your desired output is actually the string itself as mentioned in the question, and not an actual ItemsControl per se.
If I'm correct about that, have you considered just using a simple TextBlock bound to the items collection, but fed through a converter? Then Inside the converter, you would cast value to an array of strings, then in the Convert method, simply Join them using a comma as the separator which will automatically, only add them between elements, like so...
var strings = (IEnumerable<String>)value;
return String.Join(", ", strings);

Related

Combine design time menu elements into databound menu item binding

Hopefully I got the vernacular right on my question so I don't throw people completely off.
I have a menu that is data bound and using a HierarchicalDataTemplate that handles the various nested types in my binding object. So far everything is working fantastically; but now I would like to add a couple additional menu items to menu items of a certain type, but of course that breaks the binding as I cannot bind to a collection that already contains elements. CompositeCollection seems to be what I am looking for but I keep running into syntax errors when trying to apply that to my HierarchicalDataTemplate.
<Menu.Resources>
<HierarchicalDataTemplate DataType="{x:Type ODIF:PluginContainer}" ItemsSource="{Binding Instance.Devices}">
<HierarchicalDataTemplate.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Instance.Devices}"/>
<MenuItem>One more item!</MenuItem>
<MenuItem>Two more items!</MenuItem>
</CompositeCollection>
</HierarchicalDataTemplate.ItemsSource>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Image Source="{Binding PluginIcon}" Width="16" Height="16">
<Image.Style>
<Style TargetType="{x:Type Image}">
<Style.Triggers>
<DataTrigger Binding="{Binding PluginIcon}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<TextBlock Text="{Binding PluginName}"/>
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type ODIF:Device}" ItemsSource="{Binding InputChannels}">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<Image Source="{Binding StatusIcon}" Width="16" Height="16">
<Image.Style>
<Style TargetType="{x:Type Image}">
<Style.Triggers>
<DataTrigger Binding="{Binding StatusIcon}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<TextBlock Text="{Binding DeviceName}"/>
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type ODIF:DeviceChannel}">
<local:ChannelBox Channel="{Binding}" Width="200" Click="ChannelClicked"/>
</HierarchicalDataTemplate>
</Menu.Resources>
This throws:
The specified value cannot be assigned. The following type was
expected: "BindingBase".
and
Property 'ItemsSource' does not support values of type 'CompositeCollection'.
I guess you have to use a converter for solving your issue.
Let's suppose that MenuModel is a class which represents a menu item. It is really simple:
public class MenuModel
{
private List<MenuModel> children = new List<MenuModel>();
public string Description { get; set; }
public IList Children
{
get
{
return children;
}
}
}
Now we have our XAML:
<Window.Resources>
<collections:ArrayList x:Key="someOtherMenus">
<local:MenuModel Description="Menu A">
<local:MenuModel.Children>
<local:MenuModel Description="SubMenu i" />
<local:MenuModel Description="SubMenu ii" />
</local:MenuModel.Children>
</local:MenuModel>
<local:MenuModel Description="Menu B" />
</collections:ArrayList>
</Window.Resources>
<DockPanel>
<Menu DockPanel.Dock="Top" Margin="3" ItemsSource="{Binding MenuModels}">
<Menu.ItemTemplate>
<HierarchicalDataTemplate>
<HierarchicalDataTemplate.ItemsSource>
<Binding ConverterParameter="someOtherMenus">
<Binding.Converter>
<local:CompositeCollectionConverter />
</Binding.Converter>
</Binding>
</HierarchicalDataTemplate.ItemsSource>
<TextBlock Text="{Binding Description}" Margin="3" />
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
<TextBlock Text="text" Margin="10" />
</DockPanel>
So now we can consider the converter implementation:
public class CompositeCollectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
MenuModel menuModel = value as MenuModel;
if (parameter != null)
{
CollectionContainer collectionContainer = new CollectionContainer();
collectionContainer.Collection = menuModel.Children;
CompositeCollection compositeCollection = new CompositeCollection();
compositeCollection.Add(collectionContainer);
collectionContainer = new CollectionContainer();
collectionContainer.Collection = (IEnumerable)App.Current.MainWindow.FindResource(parameter);
compositeCollection.Add(collectionContainer);
return compositeCollection;
}
else
{
return menuModel.Children;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
As you can see it uses its parameter to get a specific resource (in our case I am using the resource called "someOtherMenus", which is an IEnumerable of MenuModels).
Of course the HierarchicalDataTemplate is recursive so the "someOtherMenus" MenuModels will be added to each level (but the first one) of your Menu.
I hope my sample can help you.

WPF Radcalender customization day template format

I want to cretae wpf radcalender in my wpf mvvm application. I can't able to customize the calender day template. I want to this format.
I am using this code. But the text has been overwrite the date.
xmal:
<telerik:RadCalendar Margin="30" SelectableDateStart="2015-01-01" SelectableDateEnd="2015-01-31" >
<telerik:RadCalendar.DayTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="Test"/>
</StackPanel>
</DataTemplate>
</telerik:RadCalendar.DayTemplate>
</telerik:RadCalendar>
I've worked with the RadCalendar within a Winrt app and here how i fixed that (with some help from the official Docs)
First add a new converter that will return the Date Label like that:
public class CellModelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var cellModel = value as CalendarCellModel;
return cellModel.Label;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
then in you Xaml add a textBlock to display the Cell Date using the previous converter :
<telerik:RadCalendar Margin="30" SelectableDateStart="2015-01-01" SelectableDateEnd="2015-01-31" >
<telerik:RadCalendar.DayTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="Test"/>
<TextBlock Text="{Binding Converter={StaticResource CellModelConverter}}" FontSize="13.333" VerticalAlignment="Bottom" Margin="6,0,0,4" />
</StackPanel>
</DataTemplate>
</telerik:RadCalendar.DayTemplate>
</telerik:RadCalendar>
and don't forget to add the Converter to the Static Resources:
<converters:CellModelConverter x:Key="CellModelConverter" />
--EDIT 1
the code above work fine in a Winrt application, for a WPF project you don't need a converter at all :
<telerik:RadCalendar Margin="30" SelectableDateStart="2015-01-01" SelectableDateEnd="2015-01-31" >
<telerik:RadCalendar.DayTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="Test"/>
<TextBlock Text="{Binding}" FontSize="13.333" VerticalAlignment="Bottom" Margin="6,0,0,4" />
</StackPanel>
</DataTemplate>
</telerik:RadCalendar.DayTemplate>
</telerik:RadCalendar>
--EDIT 2
the above code shows the "test" text in all the cells including WeekName, DayName ..., to fix that you can use either a Converter or a DataTrigger, here how to do it using a DataTrigger :
<telerik:RadCalendar Margin="30" SelectableDateStart="2015-01-01" SelectableDateEnd="2015-01-31" >
<telerik:RadCalendar.DayTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="Test">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="Visibility" Value="Collapsed"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding DataContext.ButtonType,ElementName=tb}"
Value="Date">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<TextBlock x:Name="tb" Text="{Binding Converter={StaticResource CellModelConverter}}" FontSize="13.333" VerticalAlignment="Bottom" Margin="6,0,0,4" />
</StackPanel>
</DataTemplate>
</telerik:RadCalendar.DayTemplate>
</telerik:RadCalendar>
Output:

Text separator between ListView in WPF

I'm trying to get the following from a ListView:
Text | Text | Text
I've already achieved the vertical orientation by the following
<ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemsPanelTemplate>
Each part(Text) is a TextBlock bound to a string in MVVM.
Preferably the lines between should just be regular vertical bars.
Any tips for achieving these vertical bars as specified??
Duplicate of How can a separator be added between items in an ItemsControl, try this:
<ItemsControl Name="theListBox">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="seperator" Text=" | "/>
<TextBlock Text="{Binding}"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}">
<Setter Property="Visibility" TargetName="seperator" Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In case you have achieved to show separator in between two items, you can have a converter say LastItemInContainerToVisibilityConverter which will be binded to the visibility of separator making separator collapsed for last item and visible for all other items.
Assuming you have used Rectangle to show the seperation between items -
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"/>
<Rectangle Visibility="{Binding RelativeSource={RelativeSource
Mode=FindAncestor, AncestorType=ListViewItem},
Converter={StaticResource
LastItemInContainerToVisibilityConverter}}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
Here goes your converter which will return if it's last item in collection -
public class LastItemInContainerToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
DependencyObject item = (DependencyObject)value;
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
return (ic.ItemContainerGenerator.IndexFromContainer(item)
== ic.Items.Count - 1) ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotImplementedException();
}
}

How to use ItemsControl with WrapPanel to list items separated by a comma?

I have an ItemsControl which lists items by separating them with a comma. The code is the following:
<ItemsControl ItemsSource="{Binding MyItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text=", "
Name="commaTextBlock"/>
<TextBlock Text="{Binding}"/>
</StackPanel>
<!-- Hide the first comma -->
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource PreviousData}}"
Value="{x:Null}">
<Setter Property="Visibility"
TargetName="commaTextBlock"
Value="Collapsed"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The result is something like this: Item1, Item2, Item3
Now, I'd like to do the same using a WrapPanel instead of a StackPanel as the ItemsPanelTemplate. I tested it and it works fine, except for a small detail, it does something like this:
Item1, Item2
, Item3
Of course this is because the comma is before each element and I hide the first one. I would like to put the comma after each element and hide the last one, so the result would be this:
Item1, Item2,
Item3
It would be really simple if there existed something like NextData (so I would bind to this instead of to PreviousData), but unfortunately no such thing exists (or I haven't found one). Does anyone have an idea how to solve this problem?
Thanks
I have done a visibility converter for a similar problem:
<TextBlock Text=", " Name="commaTextBlock">
<TextBlock.Visibility>
<MultiBinding Converter="{StaticResource commaVisibilityConverter}">
<Binding ElementName="myItemsControl" Path="ItemsSource"/>
<Binding/>
</MultiBinding>
</TextBlock.Visibility>
</TextBlock>
And the converter logic:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var collection = values[0] as IEnumerable<MyItem>;
var item = values[1] as MyItem;
if ((collection != null) && (item != null) && (collection.Count() > 0))
{
return collection.Last() == item ? Visibility.Hidden : Visibility.Visible;
}
return Visibility.Hidden;
}
Maybe you can try to use multibinding and a converter. Something like this:
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource myConverter}">
<Binding Mode="OneWay"/>
<Binding ElementName="root" Path="ItemsSource" Mode="OneWay"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
Where root is the name of your ItemsControl.
And write a converter which checks for position:
public class MyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var text = values[0] as string;
var list = values[1] as ObservableCollection<string>;
//check for null etc...
if (list.IndexOf(text) == list.Count - 1)
return text;
return string.Format("{0}, ", text);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
//ignore this, not gonna happen with OneWay Binding
return null;
}
}
Works for me! Hope it helps you with your problem!
EDIT:
Almost the same as the other answer, the difference here is that you only need 1 TextBlock in your template, and the converter decides if there is a comma or not. But basically the same principle. MultiBinding rocks! :-)

Can I use a different Template for the selected item in a WPF ComboBox than for the items in the dropdown part?

I have a WPF Combobox which is filled with, say, Customer objects. I have a DataTemplate:
<DataTemplate DataType="{x:Type MyAssembly:Customer}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</DataTemplate>
This way, when I open my ComboBox, I can see the different Customers with their Name and, below that, the Address.
But when I select a Customer, I only want to display the Name in the ComboBox. Something like:
<DataTemplate DataType="{x:Type MyAssembly:Customer}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
Can I select another Template for the selected item in a ComboBox?
Solution
With help from the answers, I solved it like this:
<UserControl.Resources>
<ControlTemplate x:Key="SimpleTemplate">
<StackPanel>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</ControlTemplate>
<ControlTemplate x:Key="ExtendedTemplate">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</ControlTemplate>
<DataTemplate x:Key="CustomerTemplate">
<Control x:Name="theControl" Focusable="False" Template="{StaticResource ExtendedTemplate}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</UserControl.Resources>
Then, my ComboBox:
<ComboBox ItemsSource="{Binding Customers}"
SelectedItem="{Binding SelectedCustomer}"
ItemTemplate="{StaticResource CustomerTemplate}" />
The important part to get it to work was Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}" (the part where value should be x:Null, not True).
The issue with using the DataTrigger/Binding solution mentioned above are two-fold. The first is you actually end up with a binding warning that you can't find the relative source for the selected item. The bigger issue however is that you've cluttered up your data templates and made them specific to a ComboBox.
The solution I present better follows WPF designs in that it uses a DataTemplateSelector on which you can specify separate templates using its SelectedItemTemplate and DropDownItemsTemplate properties as well as ‘selector’ variants for both.
Note: Updated for C#9 with nullability enabled and using pattern matching during the search
public class ComboBoxTemplateSelector : DataTemplateSelector {
public DataTemplate? SelectedItemTemplate { get; set; }
public DataTemplateSelector? SelectedItemTemplateSelector { get; set; }
public DataTemplate? DropdownItemsTemplate { get; set; }
public DataTemplateSelector? DropdownItemsTemplateSelector { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
var itemToCheck = container;
// Search up the visual tree, stopping at either a ComboBox or
// a ComboBoxItem (or null). This will determine which template to use
while(itemToCheck is not null
and not ComboBox
and not ComboBoxItem)
itemToCheck = VisualTreeHelper.GetParent(itemToCheck);
// If you stopped at a ComboBoxItem, you're in the dropdown
var inDropDown = itemToCheck is ComboBoxItem;
return inDropDown
? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
: SelectedItemTemplate ?? SelectedItemTemplateSelector?.SelectTemplate(item, container);
}
}
To make it easier to use in XAML, I've also included a markup extension that simply creates and returns the above class in its ProvideValue function.
public class ComboBoxTemplateSelectorExtension : MarkupExtension {
public DataTemplate? SelectedItemTemplate { get; set; }
public DataTemplateSelector? SelectedItemTemplateSelector { get; set; }
public DataTemplate? DropdownItemsTemplate { get; set; }
public DataTemplateSelector? DropdownItemsTemplateSelector { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
=> new ComboBoxTemplateSelector(){
SelectedItemTemplate = SelectedItemTemplate,
SelectedItemTemplateSelector = SelectedItemTemplateSelector,
DropdownItemsTemplate = DropdownItemsTemplate,
DropdownItemsTemplateSelector = DropdownItemsTemplateSelector
};
}
And here's how you use it. Nice, clean and clear and your templates stay 'pure'
Note: 'is:' here is my xmlns mapping for where I put the class in code. Make sure to import your own namespace and change 'is:' as appropriate.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MySelectedItemTemplate},
DropdownItemsTemplate={StaticResource MyDropDownItemTemplate}}" />
You can also use DataTemplateSelectors if you prefer...
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplateSelector={StaticResource MySelectedItemTemplateSelector},
DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />
Or mix and match! Here I'm using a template for the selected item, but a template selector for the DropDown items.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MySelectedItemTemplate},
DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />
Additionally, if you don't specify a Template or a TemplateSelector for the selected or dropdown items, it simply falls back to the regular resolving of data templates based on data types, again, as you would expect. So, for instance, in the below case, the selected item has its template explicitly set, but the dropdown will inherit whichever data template applies for the DataType of the object in the data context.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MyTemplate} />
Enjoy!
Simple solution:
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Address}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
(Note that the element that is selected and displayed in the box and not the list is not inside a ComboBoxItem hence the trigger on Null)
If you want to switch out the whole template you can do that as well by using the trigger to e.g. apply a different ContentTemplate to a ContentControl. This also allows you to retain a default DataType-based template selection if you just change the template for this selective case, e.g.:
<ComboBox.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}"
Value="{x:Null}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<!-- ... -->
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ComboBox.ItemTemplate>
Note that this method will cause binding errors as the relative source is not found for the selected item. For an alternate approach see MarqueIV's answer.
In addition to what said by H.B. answer, the Binding Error can be avoided with a Converter. The following example is based from the Solution edited by the OP himself.
The idea is very simple: bind to something that alway exists (Control) and do the relevant check inside the converter.
The relevant part of the modified XAML is the following. Please note that Path=IsSelected was never really needed and ComboBoxItem is replaced with Control to avoid the binding errors.
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Control}},
Converter={StaticResource ComboBoxItemIsSelectedConverter}}"
Value="{x:Null}">
<Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>
The C# Converter code is the following:
public class ComboBoxItemIsSelectedConverter : IValueConverter
{
private static object _notNull = new object();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// value is ComboBox when the item is the one in the closed combo
if (value is ComboBox) return null;
// all the other items inside the dropdown will go here
return _notNull;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I was going to suggest using the combination of an ItemTemplate for the combo items, with the Text parameter as the title selection, but I see that ComboBox doesn't respect the Text parameter.
I dealt with something similar by overriding the ComboBox ControlTemplate. Here's the MSDN website with a sample for .NET 4.0.
In my solution, I change the ContentPresenter in the ComboBox template to have it bind to Text, with its ContentTemplate bound to a simple DataTemplate that contains a TextBlock like so:
<DataTemplate x:Uid="DataTemplate_1" x:Key="ComboSelectionBoxTemplate">
<TextBlock x:Uid="TextBlock_1" Text="{Binding}" />
</DataTemplate>
with this in the ControlTemplate:
<ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding Text}" ContentTemplate="{StaticResource ComboSelectionBoxTemplate}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left"/>
With this binding link, I am able to control the Combo selection display directly via the Text parameter on the control (which I bind to an appropriate value on my ViewModel).
I used next approach
<UserControl.Resources>
<DataTemplate x:Key="SelectedItemTemplate" DataType="{x:Type statusBar:OffsetItem}">
<TextBlock Text="{Binding Path=ShortName}" />
</DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
<ComboBox DisplayMemberPath="FullName"
ItemsSource="{Binding Path=Offsets}"
behaviors:SelectedItemTemplateBehavior.SelectedItemDataTemplate="{StaticResource SelectedItemTemplate}"
SelectedItem="{Binding Path=Selected}" />
<TextBlock Text="User Time" />
<TextBlock Text="" />
</StackPanel>
And the behavior
public static class SelectedItemTemplateBehavior
{
public static readonly DependencyProperty SelectedItemDataTemplateProperty =
DependencyProperty.RegisterAttached("SelectedItemDataTemplate", typeof(DataTemplate), typeof(SelectedItemTemplateBehavior), new PropertyMetadata(default(DataTemplate), PropertyChangedCallback));
public static void SetSelectedItemDataTemplate(this UIElement element, DataTemplate value)
{
element.SetValue(SelectedItemDataTemplateProperty, value);
}
public static DataTemplate GetSelectedItemDataTemplate(this ComboBox element)
{
return (DataTemplate)element.GetValue(SelectedItemDataTemplateProperty);
}
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uiElement = d as ComboBox;
if (e.Property == SelectedItemDataTemplateProperty && uiElement != null)
{
uiElement.Loaded -= UiElementLoaded;
UpdateSelectionTemplate(uiElement);
uiElement.Loaded += UiElementLoaded;
}
}
static void UiElementLoaded(object sender, RoutedEventArgs e)
{
UpdateSelectionTemplate((ComboBox)sender);
}
private static void UpdateSelectionTemplate(ComboBox uiElement)
{
var contentPresenter = GetChildOfType<ContentPresenter>(uiElement);
if (contentPresenter == null)
return;
var template = uiElement.GetSelectedItemDataTemplate();
contentPresenter.ContentTemplate = template;
}
public static T GetChildOfType<T>(DependencyObject depObj)
where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
}
worked like a charm. Don't like pretty much Loaded event here but you can fix it if you want
Yes. You use a Template Selector to determine which template to bind at run-time. Thus if IsSelected = False then Use this template, if IsSelected = True, use this other template.
Of Note:
Once you implement your template selector, you will need to give the templates keynames.
I propose this solution without DataTemplateSelector, Trigger, binding nor behavior.
The first step is to put the ItemTemplate (of the selected element) in the ComboBox resources and the ItemTemplate (of the item in the drop down menu) in the ComboBox.ItemsPanel resources and give both resources the same key.
The second step is to postpone the ItemTemplate resolution at run time by using both a ContentPresenter and a DynamicResource in the actual ComboBox.ItemTemplate implementation.
<ComboBox ItemsSource="{Binding Items, Mode=OneWay}">
<ComboBox.Resources>
<!-- Define ItemTemplate resource -->
<DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
<TextBlock Text="{Binding FieldOne, Mode=OneWay}" />
</DataTemplate>
</ComboBox.Resources>
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Grid.IsSharedSizeScope="True"
IsItemsHost="True">
<StackPanel.Resources>
<!-- Redefine ItemTemplate resource -->
<DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="GroupOne" />
<ColumnDefinition Width="10" SharedSizeGroup="GroupSpace" />
<ColumnDefinition Width="Auto" SharedSizeGroup="GroupTwo" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding FieldOne, Mode=OneWay}" />
<TextBlock Grid.Column="2" Text="{Binding FieldTwo, Mode=OneWay}" />
</Grid>
</DataTemplate>
</StackPanel.Resources>
</StackPanel>
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate>
<ContentPresenter ContentTemplate="{DynamicResource ItemTemplate}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

Resources