WPF DataGrid ContentPresenter Binding Errors - wpf

I'm using a WPF Datagrid with DataGridTemplateColumns with comboboxes in each cell. At startup, the output window repeats the following message and delays startup about 10 seconds. Something related to ContentPresenter and DataContext not being set (DataItem=null). Please help if you can. Here is the error message:
System.Windows.Data Information: 10 : Cannot retrieve value using the binding and no valid fallback value exists; using default instead. BindingExpression:(no path); DataItem=null; target element is 'ContentPresenter' (Name=''); target property is 'Content' (type 'Object')
It's not technically an error, but it delays the startup nonetheless. Here is a subset of the xaml:
<DataGrid x:Name="grid"
AutoGenerateColumns="False"
CanUserAddRows="True"
IsEnabled="True" Grid.Row="1" Grid.Column="0"
EnableRowVirtualization="False"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ScrollViewer.CanContentScroll="True"
GridLinesVisibility="Vertical"
AreRowDetailsFrozen="True"
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible"
SelectionMode="Extended"
HeadersVisibility="All"
Height="750"
VirtualizingStackPanel.VirtualizationMode="Standard"
VirtualizingStackPanel.IsVirtualizing="True"
DataContext="{StaticResource vm}"
ItemsSource="{Binding Source={StaticResource vm}, Path=CorpActionAutoPostConfigs, Mode=TwoWay, IsAsync=False}">
<DataGrid.Columns>
<!-- selecteditembinding: source:enum, dest:JournalType -->
<DataGridTemplateColumn Header="JournalType" x:Name="colJournalType">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox x:Name="cbJournalTypes"
ItemsSource="{Binding Source={StaticResource vm}, Path=JournalTypes, IsAsync=False}"
ItemTemplate="{StaticResource GenericDataTemplate}"
SelectedItem="{Binding Path=JournalTypeCode, Mode=TwoWay, Converter={StaticResource JournalTypeConverter}, IsAsync=False}">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
...more similar columns follow. I feel like I need to set a Style or ControlTemplate or something but not exactly sure how to proceed.
If I use a ListView/GridView structure, these "errors" do not occur and startup is much faster. But I would prefer to use the DataGrid.
One clue is it seems I get that error for each visible cell that is generated. So I tried to define a style for DataGridCell, that sets the control template for each cell and includes a ContentPresenter binding with a fallback value. Did not resolve the errors.
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridCell" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}"
SnapsToDevicePixels="True">
<ContentPresenter x:Name="DataGridCellContentPresenter"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content, FallbackValue=null}"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
EDIT:
Looking at the Visual Tree, it seems the ContentPresenter I have defined in the ControlTemplate contains yet another ContentPresenter. That ContentPresenter is nameless and I suspect is the source of the binding errors. The parent of that ContentPresenter is the Border. Does anyone know how to define this ContentPresenter in a ControlTemplate so I can add a fallback value?
I can't yet add a screencap of the visual tree, but here is what it looks like:
DataGridCell
ContentPresenter (name=DataGridCellPresenter)
ContentPresenter (unnamed, Border is parent)

A little late to the party, but I managed a work around for this issue - if derive your own custom class from DataGridTemplateColumn you can explicitly set the FallbackValue for the Binding.
Obviously you then have to go through the XAML and replace the required elements.
public sealed class DataGridTemplateColumnEx : DataGridTemplateColumn
{
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
return LoadTemplateContent(false, dataItem, cell);
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
return LoadTemplateContent(true, dataItem, cell);
}
private void ChooseCellTemplateAndSelector(bool isEditing, out DataTemplate template, out DataTemplateSelector templateSelector)
{
template = null;
templateSelector = null;
if (isEditing)
{
template = CellEditingTemplate;
templateSelector = CellEditingTemplateSelector;
}
if (template == null && templateSelector == null)
{
template = CellTemplate;
templateSelector = CellTemplateSelector;
}
}
private FrameworkElement LoadTemplateContent(bool isEditing, object dataItem, DataGridCell cell)
{
ChooseCellTemplateAndSelector(isEditing, out var template, out var templateSelector);
if (template != null || templateSelector != null)
{
var contentPresenter = new ContentPresenter();
var binding = new Binding
{
// Explicitly setting this to NULL, stops the binding FallbackValue messages, and therefore improves perf...
FallbackValue = null
};
BindingOperations.SetBinding(contentPresenter, ContentPresenter.ContentProperty, binding);
contentPresenter.ContentTemplate = template;
contentPresenter.ContentTemplateSelector = templateSelector;
return contentPresenter;
}
return null;
}
}

Related

How to Merge user controls that Binds to SAME obj smoothly?

I'm trying to merge some user controls that are binded to same target. At the start, it looks simple but I have no idea with this how can I deliver binding target to daughter control (controls inside merge control)?
I want to make this:
<Canvas>
<local:Teeth x:Name="sideR" Points="{Binding Points[0]}" IsClosedCurve="{Binding IsClosedCurve}"/>
<local:WrapTeeth Points="{Binding Points[0]}"/>
<ListBox ItemsSource="{Binding Points[0]}" ItemContainerStyle="{StaticResource PointListBoxItemStyle}">
<ListBox.Template>
<ControlTemplate>
<Canvas IsItemsHost="True"/>
</ControlTemplate>
</ListBox.Template>
</ListBox>
</Canvas>
into
<local:MergeControl Points="{Binding Points[0]}"/>
Your UserControl should have a Points dependency property like shown below. It is not clear from your question whether you need a more specialized collection type than IEnumerable. Possibly replace it with PointCollection or something more suitable.
public partial class MergeControl : UserControl
{
public static readonly DependencyProperty PointsProperty = DependencyProperty.Register(
"Points", typeof(IEnumerable), typeof(MergeControl));
public IEnumerable Points
{
get { return (IEnumerable)GetValue(PointsProperty); }
set { SetValue(PointsProperty, value); }
}
public MergeControl()
{
InitializeComponent();
}
}
The elements in the UserControl's XAML would bind to this property by RelativeSource Bindings. You may need to define another property for the IsClosedCurve Binding of the Teeth element.
<UserControl ...>
<UserControl.Resources>
<Style x:Key="PointListBoxItemStyle" TargetType="ListBoxItem">
...
</Style>
</UserControl.Resources>
<Canvas>
<local:Teeth x:Name="sideR"
Points="{Binding Points, RelativeSource={RelativeSource AncestorType=UserControl}}"
IsClosedCurve="{Binding IsClosedCurve, ...}"/>
<local:WrapTeeth
Points="{Binding Points, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<ListBox
ItemsSource="{Binding Points, RelativeSource={RelativeSource AncestorType=UserControl}}"
ItemContainerStyle="{StaticResource PointListBoxItemStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Canvas>
</UserControl>
Note also that ItemsControls have an ItemsPanel property to set the Panel element that is used to contain their items.

wpf Usercontrol template

In my MVVM app I have a treeview representing records in a database. My views and viewmodels are linked in a resource dictionary like this
<DataTemplate DataType="{x:Type vm:TrialSiteViewModel}">
<vw:TrialSiteView />
</DataTemplate>
I want to display a preview of the view when a user hovers over an icon using the tooltip. My HierarchicalDataTemplate in the treeview is this
<HierarchicalDataTemplate DataType="{x:Type vm:TrialSiteViewModel}"
ItemsSource="{Binding Path=Children}">
...
<Button Style="{StaticResource previewButtonStyle}">
<Button.ToolTip>
<ToolTip Style="{x:Null}">
<ToolTip.ContentTemplate>
<DataTemplate>
<localtools:ObjectPreview
PreviewObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=DataContext}"
/>
</DataTemplate>
</ToolTip.ContentTemplate>
</ToolTip>
</Button.ToolTip>
</Button>
</StackPanel>
</HierarchicalDataTemplate>
This correctly picks up the TrialSiteViewModel that is the DataContext for the Treeviewitem.
ObjectPreview uses a viewbox and contentcontrol to display the view of the record
<Viewbox Grid.Row="1" Name="treeviewViewBox"
Stretch="Uniform"
IsEnabled="False">
<ContentControl Name="treeViewItemViewModel"
Content="{Binding PreviewObject}">
</ContentControl>
</Viewbox>
and the code behind contains the dependency property
public partial class ObjectPreview : UserControl
{
public ObjectPreview()
{
InitializeComponent();
}
public static readonly DependencyProperty _previewObjectProperty =
DependencyProperty.Register("PreviewObject", typeof(TreeViewItemViewModel), typeof(ObjectPreview));
public TreeViewItemViewModel PreviewObject
{
get { return (TreeViewItemViewModel)GetValue(_previewObjectProperty); }
set { SetValue(_previewObjectProperty, value); }
}
}
The problem I'm having is that the Template used to display the object is the same as that used in the treeview. This simply shows an icon and an object summary (ie. Primary Key and one or two key fields) rather than the entire template as defined in the view TrialSiteView. If I amend the code to use a button Command on the TrialSiteViewModel and inject it into ObjectPreview I can set the contentcontrol in the code behind and the TrialSiteView is used.
I'm guessing that somehow the Template is inferred from the TreeViewItem. Can anyone tell me how I can ensure the tooltip uses the TrialSiteView?
UPDATE
Ok, so I've fixed this but had to resort to code behind and removed the usercontrol and put the view directly in the tooltip. The key bit is getting the datatemplate from the resources. I'd tried to do this previously by assigning a key to the datatemplate, but either my code was flawed or it did not work. Anyhow, this works but is not the preferred Xaml solution.
private void PreviewObject_MouseEnter(object sender, MouseEventArgs e)
{
Image image = (Image)sender;
var key = new System.Windows.DataTemplateKey(image.DataContext.GetType());
var datatemplate = (DataTemplate)this.FindResource(key);
ToolTip tooltip = new ToolTip();
tooltip.Style = VisualUtils.GetResource<Style>("ControlTemplates.xaml", "toolTipWithContentStyle");
tooltip.MaxWidth = 460;
ContentControl contentcontrol = new ContentControl();
contentcontrol.ContentTemplate = datatemplate;
contentcontrol.Content = image.DataContext as TreeViewItemViewModel;
Viewbox viewbox = new Viewbox();
viewbox.Stretch = Stretch.Uniform;
viewbox.Child = contentcontrol;
tooltip.Content = viewbox;
image.ToolTip = tooltip;
}
What you need to do is to specify explicitly what data template to use. In order to do that just add a template property along with the PreviewObject property in the preview control:
public static readonly DependencyProperty _previewObjectTemplateProperty =
DependencyProperty.Register("PreviewObjectTemplate", typeof(DataTemplate), typeof(ObjectPreview));
public DataTemplate PreviewObjectTemplate
{
get { return (DataTemplate)GetValue(_previewObjectTemplateProperty); }
set { SetValue(_previewObjectTemplateProperty, value); }
}
Then, in the ObjectPreview.xaml add the ContentTemplate property that is bound to the PreviewObjectTemplate property:
<Viewbox Grid.Row="1" Name="treeviewViewBox"
Stretch="Uniform"
IsEnabled="False">
<ContentControl Name="treeViewItemViewModel"
Content="{Binding PreviewObject}"
ContentTemplate="{Binding PreviewObjectTemplate}" >
</ContentControl>
</Viewbox>
And finally, give a key to your data template and specify a reference to it explicitly when you declare ObjectPreview:
<DataTemplate x:Key="FullViewTemplate" DataType="{x:Type vm:TrialSiteViewModel}">
<vw:TrialSiteView />
</DataTemplate>
...
<ToolTip Style="{x:Null}">
<ToolTip.ContentTemplate>
<DataTemplate>
<localtools:ObjectPreview
PreviewObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=DataContext}"
PreviewObjectTemplate="{StaticResource FullViewTemplate}"
/>
</DataTemplate>
</ToolTip.ContentTemplate>

WPF Scroll & focus changing problem

I am having a problem with scrolling for my WPF application.
Here is the deal. My UI is the following:
The role of my application is to act as a central hub for many applications and launch them. An admin can launch a dump recorded by another user.
Therefore, I have a ListView, showing the application list, which is scrollable if needed.
I defined a GroupStyle in order to show expanders and emulate a Windows Explorer view.
Everything works fine, I just have a problem: when scrolling with the mouse wheel, the component in clear blue ("Launch mode") seems to be catching the focus and stop scrolling.
This means especially that if my mouse is anywhere out of this control, the scrolling is okay. But whenever the mouse enters this control, I can't scroll anymore.
I tried to modify the property Focusable and set it to False everywhere I could but nothing changed. I'd guess that it is finally not a focus problem.
Anybody has an idea on how to avoid the scrolling to be caught by the element?
Here is some (simplified, removed some useless properties so as to make it as clear as possible) XAML for the expander's content:
<StackPanel Orientation="Vertical" VerticalAlignment="Top" >
<ToggleButton>
<!-- ToggleButton Content... -->
</ToggleButton>
<!-- This is the custom component in which you can see "Launch mode" -->
<my:UcReleaseChooser >
<!-- Properties there. I tried to set Focusable to False, no impact... -->
</my:UcReleaseChooser>
</StackPanel>
And the code for UcReleaseChooser:
<StackPanel HorizontalAlignment="Stretch"
Focusable="False" ScrollViewer.CanContentScroll="False">
<ListBox ItemsSource="{Binding ListChosenReleases}" BorderBrush="LightGray" Background="AliceBlue"
HorizontalAlignment="Stretch" Focusable="False" ScrollViewer.CanContentScroll="False">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Focusable="False" ScrollViewer.CanContentScroll="False"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel LastChildFill="True" HorizontalAlignment="Stretch"
Focusable="False" ScrollViewer.CanContentScroll="False">
<TextBlock DockPanel.Dock="Top"
HorizontalAlignment="Left" Text="{Binding Key}"
FontStyle="Italic"/>
<ListBox DockPanel.Dock="Bottom"
HorizontalAlignment="Right" ItemsSource="{Binding Value}"
BorderBrush="{x:Null}" Background="AliceBlue"
Focusable="False" ScrollViewer.CanContentScroll="False">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Focusable="False"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<-- Blah blah about style -->
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Key}" Margin="3"
IsChecked="{Binding Path=IsSelected, Mode=TwoWay,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListBoxItem}}}"
Focusable="False" ScrollViewer.CanContentScroll="False"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
As you can see, the UcReleaseChooser contains a list of RadioButton lists. I tried to set Focusable & CanContentScroll to False everywhere it seemed appropriate, but the control keeps preventing the main UI to scroll...
I guess I should change another property... Any idea?
Thanks!
The problem is the ListBox, or more specifically, the ScrollViewer within the ListBox's template. This is getting your scroll events and consuming them before the outer ScrollViewer in the ListView even sees them.
I would advise replacing the ListBox with an ItemsControl if possible. However, that implies there will be no SelectedItem property. If you need that, I would suggest setting ScrollViewer.HorizontalScrollBarVisibility (or VerticalScrollBarVisibility) to Disabled. Failing that, I can only suggest re-templating ListBox to not contain a ScrollViewer at all.
I had an issue with a listbox stealing focus inside a scrollviewer (I have multiple listboxes inside the scrollviewer). So I created an attached property which denies the listbox the ability to scroll. So therefore the scrollviewer which is housing the listbox can scroll
Your control is a listbox, so this should work as is, but there is no reason the Extension should be limited to a Listbox; it just is to match my exact purpose.
public static class ListboxExtensions
{
public static DependencyProperty IgnoreScrollProperty = DependencyProperty.RegisterAttached("IgnoreScroll", typeof(bool), typeof(ListboxExtensions), new UIPropertyMetadata(false, IgnoreScrollChanged));
public static bool GetIgnoreScroll(DependencyObject dependencyObject)
{
return (bool)dependencyObject.GetValue(IgnoreScrollProperty);
}
public static void SetIgnoreScroll(DependencyObject dependencyObject, bool value)
{
dependencyObject.SetValue(IgnoreScrollProperty, value);
}
private static void IgnoreScrollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var newValue = (bool)e.NewValue;
var oldValue = (bool)e.OldValue;
var frameworkElement = d as FrameworkElement;
if (frameworkElement == null) return;
if (!newValue || oldValue || frameworkElement.IsFocused) return;
var lb = frameworkElement as ListBox;
if (lb == null) return;
lb.PreviewMouseWheel += LbOnPreviewMouseWheel;
}
private static void LbOnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (!(sender is ListBox) || e.Handled) return;
e.Handled = true;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = sender
};
var parent = ((Control)sender).Parent as UIElement;
if (parent != null) parent.RaiseEvent(eventArg);
}
}
And then in your XAML, you just put this on the listbox:
<ListBox extensions:ListboxExtensions.IgnoreScroll="True">
Of course, remembering to include the namespace to your extensions in the top of the XAML:
xmlns:extensions="clr-namespace:UI.Extensions"

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>

Performance problem with multiple ItemsControl inside a ScrollViewer

I need a single scrollable surface that contains two bound lists. At first, I used a ScrollViewer with two ListBox inside, each having their scrolling disabled, so I could still have item selection. Seeing the poor loading time performance, I changed my ListBoxes to ItemsControl, but the performance is still terrible. In total, my two lists have only 110 items.
<ScrollViewer Grid.Row="1">
<StackPanel>
<Button Style="{StaticResource EmptyNonSelectButtonStyle}" BorderThickness="0" HorizontalContentAlignment="Left" Click="AnyCityButton_Click">
<TextBlock Text="{Binding Resources.CurrentLocationItem, Source={StaticResource LocalizedResources}}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeLarge}" />
</Button>
<TextBlock Text="{Binding Resources.TopTenCitiesHeader, Source={StaticResource LocalizedResources}}" Style="{StaticResource PhoneTextSubtleStyle}" Margin="12,12,12,8" />
<ItemsControl ItemsSource="{Binding TopTenCities}" ItemTemplate="{StaticResource CityDataTemplate}" HorizontalContentAlignment="Stretch" />
<TextBlock Text="{Binding Resources.TopHundredCitiesHeader, Source={StaticResource LocalizedResources}}" Style="{StaticResource PhoneTextSubtleStyle}" Margin="12,12,12,8" />
<ItemsControl ItemsSource="{Binding TopHundredCities}" ItemTemplate="{StaticResource CityDataTemplate}" HorizontalContentAlignment="Stretch" />
</StackPanel>
</ScrollViewer>
What can I do to improve performance? I've tried setting the ItemsSource after the page loading, but it still ugly (empty lists for a few seconds), doesn't make more sense.
Thank you.
This answer has turned into a monster but slog through it and I think you'll find an answer.
We need in some way to use the VirtualizingStackPanel as ListBox. We need to collect all the items to display (the button, the two textblocks and two sets of city data) into a single enumerable of some type. The the real trick and would be to determine one of three templates to use to render the items.
Bottom line is we need to create a new type of ItemsControl. Now we can gain a little advantage by simply accepting we want to create a very specific ItemsControl that supports only this task. First here is a "starter for 10" (a UK media reference).
A really dumb example of creating a specific items control:-
public class SwitchingItemsControl : ItemsControl
{
public DataTemplate AlternativeItemTemplate { get; set; }
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
ContentPresenter cp = (ContentPresenter)element;
if (AlternativeItemTemplate != null && (((int)item) & 1) == 1)
cp.ContentTemplate = AlternativeItemTemplate;
else
cp.ContentTemplate = ItemTemplate;
cp.Content = item;
}
}
This control assumes its items are a set of integers. It has an AlternativeItemTemplate which if supplied it toggles between on an odd/even basis (note that is a facet of the item).
Now lets put that use with a VirtualizingStackPanel:-
<UserControl x:Class="CustomVirtualizingPanelInSL.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SilverlightApplication1"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<local:SwitchingItemsControl x:Name="itemsControl" >
<local:SwitchingItemsControl.Template>
<ControlTemplate TargetType="local:SwitchingItemsControl">
<ScrollViewer VerticalScrollBarVisibility="Visible">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</local:SwitchingItemsControl.Template>
<local:SwitchingItemsControl.ItemTemplate>
<DataTemplate>
<Border CornerRadius="2" BorderBrush="Blue" BorderThickness="1" Margin="2">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding}" />
</Border>
</DataTemplate>
</local:SwitchingItemsControl.ItemTemplate>
<local:SwitchingItemsControl.AlternativeItemTemplate>
<DataTemplate>
<Border CornerRadius="2" BorderBrush="Red" BorderThickness="1" Margin="2">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding}" />
</Border>
</DataTemplate>
</local:SwitchingItemsControl.AlternativeItemTemplate>
<local:SwitchingItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</local:SwitchingItemsControl.ItemsPanel>
</local:SwitchingItemsControl>
</Grid>
</UserControl>
Note the ItemsPanel is using the VirtualizingStackPanel and that gets presented in a ScrollViewer.
Now we can give it lot of content:-
namespace SilverlightApplication1
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
itemsControl.ItemsSource = Enumerable.Range(0, 10000);
}
}
}
If you switch to a standard StackPanel this takes ages to load, whereas it appears instant with virtualizing.
Armed with this info you should be able to create a special ItemsControl which has the properties:-
ButtonTemplate (DataTemplate)
HeaderTemplate (DataTemplate)
TopTenHeaderText (String)
TopHundredHeaderText (String)
TopTenSource (IEnumerable<City>)
TipHunderedSource (IEnumerable<City>)
Now you can create a single enumerable with some Linq extension methods:-
itemsControl.ItemsSource = Enumerable.Repeat((object)null, 1)
.Concat(Enumerable.Repeat((object)TopTenHeadeText))
.Concat(TopTenSource.Cast<object>())
.Concat(Enumerable.Repeat((object)TopHundredText))
.Concat(TopHundredSource.Cast<object>())
Now you just need to override PrepareContainerForItemOverride and choose between ButtonTemplate (for the first null item), the HeaderTemplate for item of type string or the ItemTemplate for an item of type City.
Thank you #AnthonyWJones, your answer was (almost) exactly what I was looking for. I've decided to provide my own answer here so that other readers know how I've adapted his answer to my needs.
First, as suggested, I'm deriving from ItemsControl, and providing a second "Template" property, called HeaderTemplate:
#region HeaderTemplate PROPERTY
public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register(
"HeaderTemplate",
typeof( DataTemplate ),
typeof( ItemsControlWithHeaders ),
new PropertyMetadata( null, new PropertyChangedCallback( OnHeaderTemplateChanged ) ) );
public DataTemplate HeaderTemplate
{
get { return ( DataTemplate )this.GetValue( HeaderTemplateProperty ); }
set { this.SetValue( HeaderTemplateProperty, value ); }
}
private static void OnHeaderTemplateChanged( DependencyObject obj, DependencyPropertyChangedEventArgs args )
{
ItemsControlWithHeaders control = obj as ItemsControlWithHeaders;
control.InvalidateArrange();
}
#endregion
Second, I'm overriding PrepareContainerForItemOverride to provide my own template selection logic. What I'm doing is simply redirecting any "string" item to the HeaderTemplate, and other items to the usual ItemTemplate:
protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
{
base.PrepareContainerForItemOverride( element, item );
ContentPresenter presenter = element as ContentPresenter;
if( presenter != null )
{
if( item is string )
{
presenter.ContentTemplate = this.HeaderTemplate;
}
else
{
presenter.ContentTemplate = this.ItemTemplate;
}
}
}
This control can now be used like this:
<local:ItemsControlWithHeaders Grid.Row="1" ItemsSource="{Binding GroupedCities}" ScrollViewer.VerticalScrollBarVisibility="Auto">
<local:ItemsControlWithHeaders.Template>
<ControlTemplate TargetType="local:ItemsControlWithHeaders">
<ScrollViewer>
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</local:ItemsControlWithHeaders.Template>
<local:ItemsControlWithHeaders.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</local:ItemsControlWithHeaders.ItemsPanel>
<local:ItemsControlWithHeaders.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Style="{StaticResource PhoneTextSubtleStyle}" Foreground="{StaticResource PhoneAccentBrush}" Margin="12,12,12,8" />
</DataTemplate>
</local:ItemsControlWithHeaders.HeaderTemplate>
<local:ItemsControlWithHeaders.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource EmptyNonSelectButtonStyle}" BorderThickness="0" HorizontalContentAlignment="Left" Click="AnyCityButton_Click">
<TextBlock Text="{Binding Name, Mode=OneWay}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeLarge}" />
</Button>
</DataTemplate>
</local:ItemsControlWithHeaders.ItemTemplate>
</local:ItemsControlWithHeaders>
To build the data source you must pass to this special hybrid control, LINQ is fine, but I've chosen a much more explicit solution, implemented in my view-model:
public IEnumerable<object> GroupedCities
{
get
{
yield return new CurrentLocationCityViewModel();
yield return Localized.TopTenCitiesHeader; // string resource
foreach( CityViewModel city in this.TopTenCities )
{
yield return city;
}
yield return Localized.TopHundredCitiesHeader; // string resource
foreach( CityViewModel city in this.TopHundredCities )
{
yield return city;
}
}
}
I now have a generic ItemsControlWithHeaders I can reuse in more than just this scenario. Performance is great. The only problem remaining for a purist like me is that the base ItemsControl complains in DEBUG, since an "object" type does not have a "Name" property. It generates a System.Windows.Data Error: BindingExpression path error: 'Name' property not found message in the debug output, which can be ignored.
Could you use one list/itemscontrol, but different datatemplates to get the same effect?
Or you could use a pivot control instead, putting the top 10 sitties in one pivot, top 100 in another pivot..

Resources