Get height of header of a ListView with GridView - wpf

How can I get the height of the header of a ListView with a GridView? Is it even possible?
I need to set the height of one control based on the height of the header of a ListView so I started writing a Converter. The problem is that I can't access the actual height.
The debugger of Visual Studio shows me that GridView has a property called HeaderRowPresenter, which in turn has a property ActualHeight. But I can't access it, HeaderRowPresenter seems to be protected or private.
All other ColumnHeader* properties (ColumnHeaderContainerStyle, ColumnHeaderTemplate, etc.) are null on this object, same for all Header* properties on the Columns (except for the String that is the content of the header).
Btw: I'm trying to solve a different problem and my current approach let me to this, so maybe I'm taking this on the wrong way.

Okay, I've found a solution but I'm not very proud of it, because it uses reflection:
I wrote a Converter that takes a View and a ListView as value and parameter and returns the height difference of the headers:
public class HeightSyncConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
GridView gvLocal = value as GridView;
ListView lvOther = parameter as ListView;
if( gvLocal == null || lvOther == null)
{
return 0;
}
GridView gvOther = (GridView)lvOther.View;
try
{
// get the non-public HeaderRowPresenter property
GridViewHeaderRowPresenter hrpLocal = (GridViewHeaderRowPresenter)gvLocal.GetType().GetProperty("HeaderRowPresenter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(gvLocal);
GridViewHeaderRowPresenter hrpOther = (GridViewHeaderRowPresenter)gvOther.GetType().GetProperty("HeaderRowPresenter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(gvOther);
if( hrpLocal == null || hrpOther == null )
{
return 0;
}
// Only works if the other ListView's header is higher than the local one's
if( hrpLocal.ActualHeight > hrpOther.ActualHeight )
{
return 0;
}
return hrpOther.ActualHeight - hrpLocal.ActualHeight;
}
catch(TargetInvocationException) { }
return 0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
I then use it to define the height of a Grid.Row:
<UserControl x:Class="GUI.ListViewLayout"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:GUI"
xmlns:conv="clr-namespace:GUI.Converter"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"
Loaded="Window_Loaded">
<UserControl.Resources>
<Style x:Key="verticalGridViewColumnHeader" TargetType="GridViewColumnHeader">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" FontWeight="Bold"
VerticalAlignment="Center" TextAlignment="Center" HorizontalAlignment="Center">
<TextBlock.LayoutTransform>
<RotateTransform Angle="270" />
</TextBlock.LayoutTransform>
</TextBlock>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<conv:HeightSyncConverter x:Key="myConverter" />
</UserControl.Resources>
<Grid x:Name="grid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="150" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition x:Name="fillerRow" Height="{Binding View, ElementName=listView1, Converter={StaticResource myConverter}, ConverterParameter={x:Reference listView2}}" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button x:Name="someControl" Grid.RowSpan="2">Placeholder</Button>
<ListView x:Name="listView1" Grid.Row="2">
<ListView.View>
<GridView>
<GridViewColumn Header="Header 1" Width="60"/>
<GridViewColumn Header="Header 2" Width="60" />
</GridView>
</ListView.View>
</ListView>
<ListView x:Name="listView2" Grid.Row="1" Grid.Column="1" Grid.RowSpan="2">
<ListView.View>
<GridView>
<GridViewColumn Header="Long Header 1"
HeaderContainerStyle="{StaticResource verticalGridViewColumnHeader}" Width="Auto" />
<GridViewColumn Header="Long Header 2"
HeaderContainerStyle="{StaticResource verticalGridViewColumnHeader}" Width="Auto" />
</GridView>
</ListView.View>
</ListView>
</Grid>
</UserControl>
The problem is that the converter is called before the ListViews and their headers are initialized, so I reevaluate the Binding in the code-behind:
public partial class ListViewLayout : UserControl
{
public ListViewLayout()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
BindingExpression binding = fillerRow.GetBindingExpression(RowDefinition.HeightProperty);
binding.UpdateTarget();
}
}
All in all, I don't think this is worth the effort and the best way is to define the height of the headers myself instead of retrieving an automatically calculated ActualHeight.

Related

ListView ItemsPanelTemplate with horizontal orientation, how to Identify ListViewItems on first row?

First of all I am working with MVVM / WPF / .Net Framework 4.6.1
I have a ListView configured with ItemsPanelTemplate in horizontal orientation that displays items from a DataTemplate. This setup allows me to fit as many items inside the Width of the ListView (the witdth size is the same from the Window), and behaves responsively when I resize the window.
So far everything is fine, now I just want to Identify what items are positioned on the first row, including when the window get resized and items inside the first row increase or decrease.
I merely want to accomplish this behavior because I would like to apply a different template style for those items (let's say a I bigger image or different text color).
Here below the XAML definition for the ListView:
<ListView x:Name="lv"
ItemsSource="{Binding Path = ItemsSource}"
SelectedItem="{Binding Path = SelectedItem}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
BTW: I already did a work around where I am getting the Index from each ListViewItem and calculating against the Width of the Grid inside the DataTemplate that is a fixed value of 180, but unfortunately it did not work as I expected since I had to use a DependencyProperty to bind the ActualWidth of the of the ListView to my ViewModel and did not responded very well when I resized the window.
I know I am looking for a very particular behavior, but if anyone has any suggestions about how to deal with this I would really appreciate. Any thoughts are welcome even if you think I should be using a different control, please detail.
Thanks in advance!
You shouldn't handle the layout in any view model. If you didn't extend ListView consider to use an attached behavior (raw example):
ListBox.cs
public class ListBox : DependencyObject
{
#region IsAlternateFirstRowTemplateEnabled attached property
public static readonly DependencyProperty IsAlternateFirstRowTemplateEnabledProperty = DependencyProperty.RegisterAttached(
"IsAlternateFirstRowTemplateEnabled",
typeof(bool), typeof(ListView),
new PropertyMetadata(default(bool), ListBox.OnIsEnabledChanged));
public static void SetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement, bool value) => attachingElement.SetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty, value);
public static bool GetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement) => (bool)attachingElement.GetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty);
#endregion
private static void OnIsEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (!(attachingElement is System.Windows.Controls.ListBox listBox))
{
return;
}
if ((bool)e.NewValue)
{
listBox.Loaded += ListBox.Initialize;
}
else
{
listBox.SizeChanged -= ListBox.OnListBoxSizeChanged;
}
}
private static void Initialize(object sender, RoutedEventArgs e)
{
var listBox = sender as System.Windows.Controls.ListBox;
listBox.Loaded -= ListBox.Initialize;
// Check if items panel is WrapPanel
if (!listBox.TryFindVisualChildElement(out WrapPanel panel))
{
return;
}
listBox.SizeChanged += ListBox.OnListBoxSizeChanged;
ListBox.ApplyFirstRowDataTemplate(listBox);
}
private static void OnListBoxSizeChanged(object sender, SizeChangedEventArgs e)
{
if (!e.WidthChanged)
{
return;
}
var listBox = sender as System.Windows.Controls.ListBox;
ListBox.ApplyFirstRowDataTemplate(listBox);
}
private static void ApplyFirstRowDataTemplate(System.Windows.Controls.ListBox listBox)
{
double calculatedFirstRowWidth = 0;
var firstRowDataTemplate = listBox.Resources["FirstRowDataTemplate"] as DataTemplate;
foreach (FrameworkElement itemContainer in listBox.ItemContainerGenerator.Items
.Select(listBox.ItemContainerGenerator.ContainerFromItem).Cast<FrameworkElement>())
{
calculatedFirstRowWidth += itemContainer.ActualWidth;
if (itemContainer.TryFindVisualChildElement(out ContentPresenter contentPresenter))
{
if (calculatedFirstRowWidth > listBox.ActualWidth - listBox.Padding.Right - listBox.Padding.Left)
{
if (contentPresenter.ContentTemplate == firstRowDataTemplate)
{
// Restore the default template of previous first row items
contentPresenter.ContentTemplate = listBox.ItemTemplate;
continue;
}
break;
}
contentPresenter.ContentTemplate = firstRowDataTemplate;
}
}
}
}
Helper Extension Method
/// <summary>
/// Traverses the visual tree towards the leafs until an element with a matching element type is found.
/// </summary>
/// <typeparam name="TChild">The type the visual child must match.</typeparam>
/// <param name="parent"></param>
/// <param name="resultElement"></param>
/// <returns></returns>
public static bool TryFindVisualChildElement<TChild>(this DependencyObject parent, out TChild resultElement)
where TChild : DependencyObject
{
resultElement = null;
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild child)
{
resultElement = child;
return true;
}
if (childElement.TryFindVisualChildElement(out resultElement))
{
return true;
}
}
return false;
}
Usage
<ListView x:Name="lv"
ListBox.IsAlternateFirstRowTemplateEnabled="True"
ItemsSource="{Binding Path = ItemsSource}"
SelectedItem="{Binding Path = SelectedItem}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Resources>
<DataTemplate x:Key="FirstRowDataTemplate">
<!-- Draw a red border around first row items -->
<Border BorderThickness="2" BorderBrush="Red">
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</Border>
</DataTemplate>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Remarks
If the visual tree itself will not change for the first row, consider to add a second attached property to the ListBox class (e.g., IsFirstRowItem) which you would set on the ListBoxItems. You can then use a DataTrigger to modify the control properties to change the appearance. This will very likely increase the performance too.

Setting margin to controls outside Grid with reference to control inside Grid

Working on wpf solution
i have created a Grid with different columns, a control is added in column 2.
Now i have a control outside grid and the Left margin should be same as the control inside the grid.
Can this be done?
We can of course take the very simple way of changing Margin using code.
For pure MVVM approach, you can do that using AttachedProperty.
Binding with a Convertor won't work here as Thickness type is not a DependencyObject.
If we simply bind the Margin of outer Button to inner Button, Entire Margin of outer Button will change, which we dont want. So, we need to preserve whole Margin except Left Margin. Left Margin can be changed using Binding. But how ? Our outer Button needs to have two Margin values, one original, and another coming from inner Button, so that original one can be changed. For this another margin, we can take help of Attached Property, as they allow us to extend a control.
AttachedProperty
public static BindingExpression GetLefMargin(DependencyObject obj)
{
return (BindingExpression)obj.GetValue(LefMarginProperty);
}
public static void SetLefMargin(DependencyObject obj, BindingExpression value)
{
obj.SetValue(LefMarginProperty, value);
}
// Using a DependencyProperty as the backing store for LefMargin. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LefMarginProperty =
DependencyProperty.RegisterAttached("LefMargin", typeof(BindingExpression), typeof(Window1), new PropertyMetadata(null, new PropertyChangedCallback(MarginCallback)));
private static void MarginCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FrameworkElement elem = d as FrameworkElement;
BindingExpression exp = e.NewValue as BindingExpression;
// Create a new Binding to set ConverterParameter //
Binding b = new Binding();
b.Converter = exp.ParentBinding.Converter;
b.ConverterParameter = elem.Margin;
b.Path = exp.ParentBinding.Path;
b.ElementName = exp.ParentBinding.ElementName;
b.Mode = exp.ParentBinding.Mode;
elem.SetBinding(FrameworkElement.MarginProperty, b);
}
Converter
public class LeftMarginCnv : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double gridCtrlLeftMargin = ((Thickness)value).Left;
Thickness tgtCtrlMargin = (Thickness)parameter;
return new Thickness(gridCtrlLeftMargin, tgtCtrlMargin.Top, tgtCtrlMargin.Right, tgtCtrlMargin.Bottom);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Usage
<Grid>
<Grid Background="Red" Margin="29,55,52,125" ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="90*"/>
<ColumnDefinition Width="121*"/>
</Grid.ColumnDefinitions>
<Button x:Name="Btn" Content="Press" Margin="18,22,35,28" Grid.Column="1" Click="Btn_Click"/>
</Grid>
<Button local:Window1.LefMargin="{Binding Margin, ElementName=Btn, Converter={StaticResource LeftMarginCnvKey}}" Content="Button" HorizontalAlignment="Left" Margin="55,199,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
Outer Button will change its Left Margin if you change the Left margin of inner Button.
You can set the common style for both. Something like this.
<StackPanel>
<StackPanel.Resources>
<Style x:Key="commonstyle" TargetType="{x:Type FrameworkElement}">
<Setter Property="Margin" Value="10,0,0,0" />
</Style>
</StackPanel.Resources>
<TextBox x:Name="outside" Width="100" Height="70" Style="{StaticResource commonstyle}"/>
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Width="100" Height="70" x:Name="inside" Grid.Column="2" HorizontalAlignment="Left" Style="{StaticResource commonstyle}"/>
</Grid>
</StackPanel>
(or)
Make it simple
<StackPanel>
<TextBox x:Name="outside" Width="100" Height="70" Margin="{Binding ElementName=inside, Path=Margin}"/>
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Width="100" Height="70" x:Name="inside" Grid.Column="2" HorizontalAlignment="Left" Margin="20"/>
</Grid>
</StackPanel>
Hope that helps.

Custom templated combobox with special non templated item

I have a RadTreeView, in each item there is a RadCombobox with some elements. Now I need to add some "special" item into each combobox. User can click on this item to add new element in combobox:
My current code:
<DataTemplate x:Key="Monitor">
<Grid Height="Auto" Width="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Height="16" Width="16" Source="icons\monitor.png" />
<TextBlock Text="{Binding Name}" Margin="5 0 0 0" Grid.Column="1" Width="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
<!-- PROBLEM IS HERE -->
<telerik:RadComboBox Name="RadComboSchedule"
Grid.Column="2"
Margin="10 0 0 0"
Width="155"
ItemsSource="{Binding Source={StaticResource DataSource}, Path=ScheduleDataSource}"
ItemTemplate="{StaticResource ComboBoxTemplate}"
>
</telerik:RadComboBox>
<Button Name="BtnRemoveMonitor" Grid.Column="3" Style="{StaticResource ButtonListBoxItemStyle}" Template="{StaticResource RemoveButtonTemplate}" />
</Grid>
</DataTemplate>
<HierarchicalDataTemplate x:Key="Group"
ItemTemplate="{StaticResource Monitor}"
ItemsSource="{Binding Monitors}">
<TextBlock Text="{Binding Name}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</HierarchicalDataTemplate>
<telerik:RadTreeView
Name="RadTreeViewGroups"
Height="auto"
Width="auto"
ItemsSource="{Binding Source={StaticResource DataSource}, Path=GroupsDataSource}"
ItemTemplate="{StaticResource Group}"
>
</telerik:RadTreeView>
So, I have all like at a screenshot without element "Add new item".
Any ideas?
PS It's not a problem to use standard WPF Combobox and TreeView controls.
You can create a new item in the DataSource of the ComboBox which name is "ADD NEW ITEM" and handle when the user select it.
private void SelectItem(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems[0].ToString() == "new")
{
string newItem = "completely new item";
dataSource.Add(newItem);
((ComboBox)sender).SelectedItem = newItem;
}
}
In this question you can see a better example that each item is an instance of a class, so it's easier to handle the "add item" request:
Databound WPF ComboBox with 'New...' item
Edit (about the 'add item' button template):
Based on the example above
Having this class
public class DisplayClass
{
public string Name { get; set; }
public bool IsDummy { get; set; }
}
You bind ComboBox.ItemsSource to an ObservableCollection like this one:
public ObservableCollection<DisplayClass> DataSource { get; set; }
Add that "dummy" item to the collection
DataSource.Add(new DisplayClass { Name = "ADD ITEM", IsDummy = true });
Then you handle the item selection with something like this:
private void SelectItem(object sender, SelectionChangedEventArgs e)
{
var comboBox = (ComboBox)sender;
var selectedItem = comboBox.SelectedItem as DisplayClass;
if (selectedItem != null && selectedItem.IsDummy)
{
//Creating the new item
var newItem = new DisplayClass { Name = comboBox.Items.Count.ToString(), IsDummy = false };
//Adding to the datasource
DataSource.Add(newItem);
//Removing and adding the dummy item from the collection, thus it is always the last on the 'list'
DataSource.Remove(selectedItem);
DataSource.Add(selectedItem);
//Select the new item
comboBox.SelectedItem = newItem;
}
}
To display the items properly, you'll need to change the ComboBox.ItemTemplate, making the image invisible when the item is dummy
<ComboBox ItemsSource="{Binding DataSource}" SelectionChanged="SelectItem">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" Width="180" />
<Image HorizontalAlignment="Right" Source="..." MouseLeftButtonUp="DeleteItem">
<Image.Style>
<Style TargetType="Image">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDummy}" Value="True">
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

Horizontal stretch list item in WP7 with grouping

I want to implement ListBox Grouping on WP7. I found this article is very useful. Actually I made the grouping works. But I got a problem with ListItem horizontal stretch. I guess I need to set ItemContainerStyle and change HorizontalContentAlignment as Stretch. But it doesn't work for this case (if set the ItemTemplate directly, it works). Any suggestions? Thanks a lot!
Here is the code, the ListItem is supposed to be stretched, but it's centered instead.
C#:
public class GroupingItemsControlConverter : IValueConverter
{
public object Convert(object value, Type tagetType, object parameter, CultureInfo culture)
{
var valueAsIEnumerable = value as IEnumerable;
if (null == valueAsIEnumerable)
{
throw new ArgumentException("GroupingItemsControlConverter works for only IEnumerable inputs.", "value");
}
var parameterAsGroupingItemsControlConverterParameter = parameter as GroupingItemsControlConverterParameters;
if (null == parameterAsGroupingItemsControlConverterParameter)
{
throw new ArgumentException("Missing required GroupingItemsControlConverterParameter.", "parameter");
}
var groupSelectorAsIGroupingItemsControlConverterSelector = parameterAsGroupingItemsControlConverterParameter.GroupSelector as IGroupingItemsControlConverterSelector;
if (null == groupSelectorAsIGroupingItemsControlConverterSelector)
{
throw new ArgumentException("GroupingItemsControlConverterParameter.GroupSelector must be non-null and implement IGroupingItemsControlConverterSelector.", "parameter");
}
// Return the grouped results
return ConvertAndGroupSequence(valueAsIEnumerable.Cast<object>(), parameterAsGroupingItemsControlConverterParameter);
}
private IEnumerable<object> ConvertAndGroupSequence(IEnumerable<object> sequence, GroupingItemsControlConverterParameters parameters)
{
// Validate parameters
var groupKeySelector = ((IGroupingItemsControlConverterSelector)(parameters.GroupSelector)).GetGroupKeySelector();
var orderKeySelector = ((IGroupingItemsControlConverterSelector)(parameters.GroupSelector)).GetOrderKeySelector();
if (null == groupKeySelector)
{
throw new NotSupportedException("IGroupingItemsControlConverterSelector.GetGroupSelector must return a non-null value.");
}
// Do the grouping and ordering
var groupedOrderedSequence = sequence.GroupBy(groupKeySelector).OrderBy(orderKeySelector);
// Return the wrapped results
foreach (var group in groupedOrderedSequence)
{
yield return new ContentControl { Content = group.Key, ContentTemplate = parameters.GroupStyle };
foreach (var item in group)
{
yield return new ContentControl { Content = item, ContentTemplate = parameters.ItemStyle };
}
}
}
public object ConvertBack(object value, Type tagetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException("GroupingItemsControlConverter does not support ConvertBack.");
}
}
public class GroupingItemsControlConverterParameters
{
public DataTemplate GroupStyle { get; set; }
public DataTemplate ItemStyle { get; set; }
public IGroupingItemsControlConverterSelector GroupSelector { get; set; }
};
public abstract class IGroupingItemsControlConverterSelector
{
public abstract Func<object, IComparable> GetGroupKeySelector();
public virtual Func<IGrouping<IComparable, object>, IComparable> GetOrderKeySelector() { return g => g.Key; }
}
public class GroupingItemsControlConverterSelector : IGroupingItemsControlConverterSelector
{
public override Func<object, IComparable> GetGroupKeySelector()
{
return (o) => (o as ItemViewModel).Group;
}
}
XAML:
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.Resources>
<DataTemplate x:Key="GroupHeaderTemplate">
<Border BorderBrush="Yellow" BorderThickness="1" Margin="12,3,12,12" Padding="6" VerticalAlignment="Center">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Number}" HorizontalAlignment="Left" Margin="6,0,0,0" FontSize="22" Foreground="White"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" HorizontalAlignment="Right" Margin="0,0,6,0" FontSize="22" Foreground="White"/>
</Grid>
</Border>
</DataTemplate>
<DataTemplate x:Key="CustomItemTemplate">
<Grid Margin="12,3,12,12" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding Number}" HorizontalAlignment="Left" Margin="6,0,0,0" FontSize="22" Foreground="White"/>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding Name}" HorizontalAlignment="Right" Margin="0,0,6,0" FontSize="22" Foreground="White"/>
</Grid>
</DataTemplate>
<local:GroupingItemsControlConverter x:Key="GroupingItemsConverter" />
<local:GroupingItemsControlConverterSelector x:Key="GroupingItemsSelector" />
<local:GroupingItemsControlConverterParameters x:Key="GroupingItemParameters"
GroupStyle="{StaticResource GroupHeaderTemplate}"
ItemStyle="{StaticResource CustomItemTemplate}"
GroupSelector="{StaticResource GroupingItemsSelector}"
/>
<Style TargetType="ListBoxItem" x:Key="CustomItemContainerStyle">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</Grid.Resources>
<ListBox x:Name="TheListBox"
ItemsSource="{Binding Items, Converter={StaticResource GroupingItemsConverter}, ConverterParameter={StaticResource GroupingItemParameters}}"
ItemContainerStyle="{StaticResource CustomItemContainerStyle}" />
</Grid>
ListBox grouping? You should consider using the LongListSelector from the Silverlight Toolkit. And to simplify the binding for that, you can use the LongListCollection collection type (Check the entire example, for details).
Then you can simply create apps that groups values, for example like this:

Binding a Silverlight TabControl to a complex object using a converter

I am trying to bind my data to a tab control. I have got the headers displaying fine but I'm not sure how I get the content of the tabs to bind correctly based on my item template shown below.
I think I'm missing something when I'm creating the tab item but I'm not sure how to bind my MyCustomObject to each of the TabItem's.
XAML:
<sdk:TabControl ItemsSource="{Binding Singles,Converter={StaticResource TabConverter}}">
<sdk:TabControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</sdk:TabControl.ItemsPanel>
<sdk:TabControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Converter={StaticResource RoundNumberConverter}}" Margin="2" />
<ListBox x:Name="Matches" ItemsSource="{Binding}" Margin="2">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Converter={StaticResource SeedingConverter}, ConverterParameter=true}" Margin="2" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{Binding Converter={StaticResource SeedingConverter}, ConverterParameter=false}" Margin="2" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Player1Name}" Margin="2" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Player2Name}" Margin="2" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</sdk:TabControl.ItemTemplate>
</sdk:TabControl>
Converter:
public class TabConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
IEnumerable<IGrouping<string, MyCustomObject>> source = value as IEnumerable<IGrouping<string, MyCustomObject>>;
if (source != null)
{
var controlTemplate = (ControlTemplate)parameter;
List<TabItem> result = new List<TabItem>();
foreach (IGrouping<string, MyCustomObject> tab in source)
{
result.Add(new TabItem()
{
Header = tab.Key,
DataContext = tab //not sure this is right?
});
}
return result;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
You're most of the way there, the following works for me:
Strip out your DataTemplate and drop its contents straight into a new UserControl, for the example here, lets call it MatchesView.
Then in your TabConverter, amend the contents of the foreach loop to something like the following:
TabItem tabitem = new TabItem();
tabitem.Header = tab.Key;
MatchesView tabview = new MatchesView();
tabview.DataContext = parameter;
tabitem.Content = tabview;
result.Add(tabitem);
Note: this requires that you pass your ViewModel to the TabConverter as a parameter, eg:
<sdk:TabControl SelectedItem="{Binding YourSelectedObject}" ItemsSource="{Binding YourCollectionObject, Converter={StaticResource TabConverter}, ConverterParameter={StaticResource YourViewModel}, Mode=TwoWay}" />
Then, as you have your ViewModel in each instance of the new control, adjust your binding accordingly!
Note that the trick is that you have a separate binding for the single instance of the Selected object
checking your problem I think there is no support for that. The WPF's TabItem contains ItemTemplate & ContentTemplate. The first is the template for the Header, the second is the template for the "body". In silverlight we still have no ContentTemplate. Haven't seen an official statement yet, but this guy says it won't be supported till SL5.

Resources