Template initialisation causes stack overflow - c#

I have a DataModel like so :
public class Node
{
public List<Node> Children { get; private set; }
public string Name { get; private set; }
public Node(string _name, params Node[] _children)
{
Name = _name;
Children = new List<Node>(_children);
}
}
I now want to define a View for this model (I'm not using a TreeView for reasons out of scope of this question), that allows people to use it in one of two ways.
Example 1 : Default layout, the content should auto Expand
<NodeView DataContext="{Binding Root}"/>
The above should expand the node tree in the same way a treeview would, i.e recursively going down the Node and its children creating new views for each one.
Example 2 : Allow people to manually set the content
<NodeView DataContext="{Binding Root}">
<StackPanel>
<TextBlock Text="{Binding Children[0].Name"/>
<TextBlock Text="{Binding Children[1].Name"/>
<TextBlock Text="{Binding Children[2].Name"/>
</StackPanel>
</NodeView>
The above now won't expand, but only show the first three child nodes.
I thought I could do this with the following Custom Control, but I get a stackoverflow exception, what am I doing wrong?
<Style TargetType="{x:Type l:NodeView}">
<Setter Property="Content">
<Setter.Value>
<GroupBox>
<ItemsControl ItemsSource="{Binding Children}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<l:NodeView />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type l:NodeView}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}"
Grid.Column="1" />
<ContentPresenter Grid.Row="1"
Grid.Column="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here is a link to the project if anyone finds that easier to use
https://www.dropbox.com/s/j32mm7gave17v7j/NodeView.zip

The problem comes basically from your Style. You set the Content. However you should specify the Template and the ItemTemplate.
The first one will descripe how NodeView is visuallised: a textblock with a list of children.
The second will describe how your children nodes are visualised: NodeView control.
<Style TargetType="{x:Type l:NodeView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type l:NodeView}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}"
Grid.Column="1" />
<ItemsControl Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding Children}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<l:NodeView Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You will notice that it looks a bit strange with no ContentPresenter. But that's a result of a non appropriate base class of the NodeView. Content control is usually used for a single content but here you have a content with children items.
By the way it is not the only way you can write it. It is possible to keep the default template for the Template and to put the entire thing in to the ContentTemplate of the NodeView.
<Style TargetType="{x:Type l:NodeView}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate TargetType="{x:Type l:NodeView}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}"
Grid.Column="1" />
<ItemsControl Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding Children}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<l:NodeView Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
In the main window you should use Content property and not the DataContext.
PS: I wrote the code without testing so not sure it works out of the box.

The problem seems to be, that you reference the control you're templating, in the style of the control itself.
So when WPF tries to create the style, it tries to initialize it, whereby it has to create the datatemplate, so it has to initialize the style... and there you have your loop.
The only way around this, that I found, was to set your default content by code, like this:
public NodeView()
{
var dt = FindResource("DefaultNodeContent") as DataTemplate;
var lb = new ItemsControl();
lb.ItemTemplate = dt;
var binding = new Binding("Children");
lb.SetBinding(ItemsControl.ItemsSourceProperty, binding);
var gb = new GroupBox();
gb.Content = lb;
this.Content = gb;
}
For this, you'd have to add a resource dictionary to you app
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfApplication5">
<DataTemplate x:Key="DefaultNodeContent">
<l:NodeView DataContext="{Binding}" />
</DataTemplate>
</ResourceDictionary>
And then your style would change to:
<Style TargetType="{x:Type l:NodeView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type l:NodeView}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="18" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}"
Grid.Column="1" />
<ContentPresenter Grid.Row="1"
Grid.Column="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Hope this helps.

Related

How to change the toggle of a TreeView

I have a treeview working, it has a toggle button that can expand it's contents. When I do, each column is too far to the right from where I want it due to the toggle expansion width. I'd like to override that behaviour but I don't know how, I understand it's something to do with defining the control template of the TreeView?
This is my code
c#
Class MyList
{
double someDouble;
double somestring;
souble anotherString;
bool thisOnesABool;
}
Class MyContainer
{
string headerText1;
string headerText2;
List<MyList> SomeList;
}
List<MyContainer> SomeContainerInCodeBehind = new List<MyContainer>();
WPF
<UserControl.Resources>
<DataTemplate x:Key="level2">
<Grid>
//the content of 'MyList'
</Grid>
</DataTemplate>
<HierarchicalDataTemplate x:Key="level1"
ItemsSource="{Binding SomeListWithinContainer}"
ItemTemplate="{StaticResource level2}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding HeaderText}" />
<TextBox Grid.Column="1" Text="{Binding MoreHeaderText}" />
</Grid>
</HierarchicalDataTemplate>
</UserControl.Resources>
<Grid>
<TreeView Name="TheTreeView"
Grid.Row="1"
ItemTemplate="{StaticResource level1}"
ItemsSource="{Binding SomeContainerInCodeBehind}">
</TreeView>
</Grid>
In order to change the position of the expander (or expanded child items), you have to override the ControlTemplate of the TreeViewItem.
The following Style is taken from Microsoft Docs: TreeView Styles and Templates and shortened to show the relevant code. Visit the link to get the full code.
<Style x:Key="{x:Type TreeViewItem}"
TargetType="{x:Type TreeViewItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="19"
Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<VisualStateManager.VisualStateGroups>
...
</VisualStateManager.VisualStateGroups>
<!-- Use Margin to modify the position of the "Expander" ToggleButton -->
<ToggleButton x:Name="Expander"
Style="{StaticResource ExpandCollapseToggleStyle}"
ClickMode="Press"
IsChecked="{Binding IsExpanded,
RelativeSource={RelativeSource TemplatedParent}}"/>
<Border x:Name="Bd"
Grid.Column="1"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<!-- Modify e.g. Margin to change the position of the Header (parent item) -->
<ContentPresenter x:Name="PART_Header"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
</Border>
<!-- Modify e.g. using Margin to reposition the expanded child items -->
<ItemsPresenter x:Name="ItemsHost"
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
Visibility="Collapsed" />
</Grid>
...
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

WPF Bind a List<List<object>> to a ItemsControl which holds ItemControls as Items

I want to show a List with items where each item contains several items.
I tried to realize this with ItemsControls so that the ItemsControl on top is bound to the List that holds several Lists.
The itemtemplate of the ItemsControl on top contains a Label with a Style which defines that the label should hold a textbox and a ItemsControl.
The textbox should be bound to an property of the list and the ItemsControl should be bound to the list itself. This ItemsControl again has an textblock as itemtemplate which Text and Visibility-property is bound to properties of the Items which are in the lower list.
The ItemsControl on top:
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Styles/AlternativeAdressBoxStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="10,0,10,10">
<Border BorderBrush="Black"
BorderThickness="1">
<ItemsControl ItemsSource="{Binding AlternativeAdressLabelList}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Label Style="{StaticResource AlternativeAdressBoxStyle}"
DataContext="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Grid>
The Style-file:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Styles/LabelRowTextBlockStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style x:Key="AlternativeAdressBoxStyle"
TargetType="Label">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.1*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Text="{Binding Path=.AcountingArea}"/>
<Border Grid.Row="1"
Margin="20"
BorderBrush="Black"
BorderThickness="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"></TextBlock>
<ItemsControl Grid.Column="0"
Grid.Row="1"
HorizontalAlignment="Left"
ItemsSource="{Binding Path=.}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Style="{StaticResource LabelRowTextBlockStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The TextBlock-Style:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="LabelRowTextBlockStyle"
TargetType="TextBlock">
<Setter Property="Margin"
Value="0,5"/>
<Setter Property="Height"
Value="20"/>
<Setter Property="Width"
Value="200"/>
<Setter Property="Text"
Value="{Binding Content}"/>
<Setter Property="Visibility"
Value="{Binding Visibility}"/>
</Style>
</ResourceDictionary>
The property to which the ItemsControl is bound:
public List<AlternativeAdressLetterLabel> AlternativeAdressLabelList
{
get { return this._alternativeAdressLabelList; }
set { this.SetProperty( ref this._alternativeAdressLabelList, value); }
}
The list-class which fundamental inherits from List<LabelRow>
<!-- language: c# -->
public class AlternativeAdressLetterLabel : DbConnectedLabelRowList
{
#region fields
private string _sNumber;
private Dictionary<int, string> _acountingArea;
#endregion
#region properties
public string SNumber
{
get { return this._sNumber; }
set
{
this._sNumber = value;
this.OnPropertyChanged();
}
}
public Dictionary<int, string> AcountingArea
{
get { return this._acountingArea; }
set
{
this._acountingArea = value;
this.OnPropertyChanged();
}
}
#endregion
and finally the properties of the class which is hold by AlternativeAdressLetterLabel
public string Content
{
get { return this._content; }
set { SetProperty(ref this._content, value); }
}
public string Visibility
{
get { return this._visibility; }
set { SetProperty(ref this._visibility, value); }
}
Thanks in advance :)
You should be able to achieve what you want without having to modify any ControlTemplates.
Within the Resources of your UserControl, you can define 2 DataTemplate resources which define how each item in your ItemsControl will be displayed. For example:
<UserControl.Resources>
<DataTemplate x:Key="LabelRowTemplate" DataType="{x:Type model:LabelRow}">
<TextBlock Margin="0,5" Height="20" Width="200" Text="{Binding Content}" />
</DataTemplate>
<DataTemplate x:Key="AlternativeAddressTemplate" DataType="{x:Type model:AlternativeAddressLetterLabel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.1*" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding AccountingArea}" />
<Border Grid.Row="1" Margin="20" BorderBrush="Black" BorderThickness="1">
<StackPanel>
<TextBlock Text="<Whatever text goes here>" />
<ItemsControl ItemsSource="{Binding}" ItemTemplate="{StaticResource LabelRowTemplate}" />
</StackPanel>
</Border>
</Grid>
</DataTemplate>
</UserControl.Resources>
Note that the second DataTemplate references the first DataTemplate. The second DataTemplate defines how you want the items in your "outer" list to be displayed--a TextBox on top of an ItemsControl. The template is referred to by its Key property as a StaticResource.
Also note that in the code above, it is assuming a declaration of a namespace called model pointing to where your classes are defined. e.g.,
xmlns:model="clr-namespace:MyAwesomeCompany.MyAwesomeApp.Model"
With those DataTemplate resources defined, you can declare your "outer" list ItemsControl as follows:
<ItemsControl ItemsSource="{Binding AlternativeAddressLabelList}" ItemTemplate={StaticResource AlternativeAddressTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
That should get you on your way to achieving the layout you are looking for.
One additional note--if these lists of lists are added to dynamically, those changes will not automatically show up in the view. If you want that, look into collections that provide CollectionChanged events such as ObservableCollection<T>.

WPF XAML Custom Listbox Does Not Position Elements Correctly

I am trying to implement a custom multi-page dialog that takes an arbitrary number of visuals (slides) and displays them. The desired behavior would be that the selected item would appear at the top-center of the display area in the foreground. The previous slide would be at the bottom-left with a lower z-index, and the next slide would be at the bottom-right with a lower z-index. "Previous" and "Next" buttons would set the selected index. In the set method for the index, I loop through the slides and set an integer value called "SelectionState" based on whether each slide is hidden, selected, just before the selected one, or just after the selected one. I am trying to position the slides based on this integer using IValueConverters.
For my Listbox.ItemsPanelTemplate, I tried using a Grid. In the ItemsTemplate, I was setting the Grid.Column and Grid.Row using IValueConverters. Stepping through code, I can see that the value converters are being called, and that they are returning appropriate values, but all items appear in row 0, column 0 anyway.
After getting frustrated, I tried changing the Grid to a Canvas and setting the Canvas.Left and Canvas.Top properties, and again, I can see that I am getting good values back from the converter, but all items position themselves in the top-left corner anyway. (This code is shown, but commented out)
Since I know the value converters are behaving as expected, does anybody else see what I am doing wrong? Thank you in advance for any suggestions!
<Grid x:Name="DialogLayer">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="420" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="1100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox x:Name="lbSlides" ItemsSource="{Binding CurrentFormSet.InterviewSlides}" Grid.Column="1" Grid.Row="1" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Hidden">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="250" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="250" />
<ColumnDefinition Width="250" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition Height="300" />
<RowDefinition Height="60" />
</Grid.RowDefinitions>
</Grid>
<!--<Canvas Grid.Row="1" Grid.Column="1" />-->
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<!--<Border BorderBrush="Black" BorderThickness="1" Width="600" Height="360" Canvas.Top="{Binding SelectionState, Converter={StaticResource SelectionStateToCanvasTopConverter}}" Canvas.Left="{Binding SelectionState, Converter={StaticResource SelectionStateToCanvasLeftConverter}}" Panel.ZIndex="{Binding SelectionState, Converter={StaticResource SelectionStateToZIndexConverter}}">
<TextBlock Text="Hello World" />
</Border>-->
<Border BorderBrush="Black" BorderThickness="1" Width="600" Height="360" Grid.Column="{Binding SelectionState, Converter={StaticResource SelectionStateToGridColumnConverter}}" Grid.Row="{Binding SelectionState, Converter={StaticResource SelectionStateToGridRowConverter}}" Panel.ZIndex="{Binding SelectionState, Converter={StaticResource SelectionStateToZIndexConverter}}">
<TextBlock Text="Hello World" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
You have to set those properties on the item container (i.e. the ListBoxItem) by setting the ItemContainerStyle property:
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Grid.Column" Value="{Binding SelectionState, ...}"/>
<Setter Property="Grid.Row" Value="{Binding SelectionState, ...}"/>
<Setter Property="Panel.ZIndex" Value="{Binding SelectionState, ...}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1" Width="600" Height="360">
<TextBlock Text="Hello World" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>

How to make a custom tool tip template in WPF XAML

I have a number of buttons on a form (devcomponents ButtonDropDown controls to be precise).
I want to show a tool tip for each that contains a header at the top, an image on the left and a description on the right.
The header needs to be tied to the ButtonDropDown.Header, the image to the ButtonDropDown.Image. I also then need to define the description somewhere.
I've only been using WPF for a few weeks so I've not managed to find any answers via searching, though I have studied a few.
Below is my attempt at creating a template that really doesn't work at all:
<Style TargetType="dcr:ButtonDropDown">
<Setter Property="OverridesDefaultStyle" Value="True"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="dcr:ButtonDropDown">
<ContentControl>
<ContentControl.ToolTip>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Content="{TemplateBinding Header}" FontWeight="Bold" />
<Viewbox Grid.Row="1" Grid.Column="0" Width="64" Height="32" Margin="3">
<ContentControl Content="{TemplateBinding Image}" />
</Viewbox>
<Label Grid.Row="1" Grid.Column="1"
Content="{TemplateBinding ToolTip.Content}" />
</Grid>
</ContentControl.ToolTip>
</ContentControl>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I then define a button as follows:
<dcr:ButtonDropDown Header="-X" Command="{Binding MoveCommand}" CommandParameter="xMinus"
ImagePosition="Top" IsEnabled="{Binding UserConfiguration.Move.Visible}"
ToolTip="move x axis down">
<dcr:ButtonDropDown.Image>
<Viewbox Width="32" Height="32" Margin="3">
<ContentControl Content="{StaticResource minusXImage}" />
</Viewbox>
</dcr:ButtonDropDown.Image>
</dcr:ButtonDropDown>
Please could someone give me an idea how to go about this?
I've gone some way to answering this question.
The following style is defined for ToolTip:
<Style TargetType="ToolTip">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border Background="GhostWhite" BorderBrush="Gainsboro" BorderThickness="1">
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Content="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ToolTip}},
Path=PlacementTarget.(dcr:ButtonDropDown.Header)}"
FontWeight="Bold" />
<Viewbox Grid.Row="1" Grid.Column="0" Width="64" Height="32" Margin="3">
<ContentControl Content="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ToolTip}},
Path=PlacementTarget.(dcr:ButtonDropDown.Image)}" />
</Viewbox>
<Label Grid.Row="1" Grid.Column="1"
Content="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ToolTip}},
Path=PlacementTarget.(dcr:ButtonDropDown.ToolTip)}" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I then define a button as above and the ToolTip text and header appear in the ToolTipas required.
The key is the binding:
Content="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ToolTip}},
Path=PlacementTarget.(dcr:ButtonDropDown.Header)}"
Which finds the Tooltip as an ancestor of Label and casts its PlacementTarget into a ButtonDropDown
What doesn't work is the image. This appears in the ToolTip but is removed from the button.
This will also break any other controls' tooltips if they are not ButtonDropDown controls.
I'm beginning to think I'll need to create some custom controls that contain the information I require for the ToolTip for each control.

Creating a re-usable DataTemplate resource for an ItemsControl

What I currently have
I currently have an ItemsControl that I use to display a list of controls. Due to the fact each "item" contains multiple controls I have it setup by specifying a DataTemplate. Something like this (I have removed some element attributes to make the code easier to follow):
<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<c:MyControl />
<c:MyButton />
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
What I am trying to do
The above gives me exactly what I want in terms of functionality, but I have it in a few places and I want to minimize the duplicate code. In regards to the above xaml, the only things that need to be different when the DataTemplate is reused are the controls for "MyButton" and "MyControl". With that in mind my ideal way to define the XAML above would be something like this:
<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyItems}">
<c:MyControl />
<c:MyButton />
</ItemsControl>
I am happy for some variation of course, but hopefully it is clear the duplication I am trying to eliminate.
What I have tried
So far I have tried creating a template in my resources file, but that isn't working so well and I am not even sure I am going down the right track. This is what I have:
<DataTemplate x:Key="MyTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" Fill="..." Data="..." />
<StackPanel Grid.Column="1" Orientation="Horizontal">
<ItemsPresenter />
</StackPanel>
</Grid>
</DataTemplate>
Which I try to apply to my XAML like so:
<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyItems}" ItemTemplate="{StaticResource MyTemplate}">
<c:MyControl />
<c:MyButton />
</ItemsControl>
It all builds fine, but during run-time I get an error: "Items collection must be empty before using ItemsSource." which is obviously a side effect of my incorrect approach.
What am I doing wrong? How can I setup my template to work the way I want it to?
You may create a derived ItemsControl class that uses ContentControl (instead of ContentPresenter) for the item container type:
public class MyItemsControl : ItemsControl
{
protected override DependencyObject GetContainerForItemOverride()
{
return new ContentControl();
}
}
Now you may separate your current DataTemplate into an "outer" reusable part that resides in the ContentControl's Template and an "inner" part defined by the remaining DataTemplate:
<!-- somewhere in Resources -->
<Style x:Key="ReusableItemContainerStyle" TargetType="ContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" ... />
<ContentPresenter Grid.Column="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<local:MyItemsControl
ItemsSource="{Binding MyItems}"
ItemContainerStyle="{StaticResource ReusableItemContainerStyle}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<c:MyControl />
<c:MyButton />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</local:MyItemsControl>
Update: You may also set the reusable ItemContainerStyle in a default style for your derived ItemsControl in Generic.xaml like this:
<Style TargetType="local:MyItemsControl"
BasedOn="{StaticResource {x:Type ItemsControl}}">
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0" ... />
<ContentPresenter Grid.Column="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
Then you would also have to set the default style key for your ItemsControl:
public class MyItemsControl : ItemsControl
{
static MyItemsControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(MyItemsControl),
new FrameworkPropertyMetadata(typeof(MyItemsControl)));
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ContentControl();
}
}
Declare your template as a new control as:
<UserControl x:Class="UI.Views.NewControl"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" Name="myNewControl">
<Grid>
<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="0"/>
<ContentControl Grid.Row="1" Content="{Binding MyCustomControl, ElementName=myNewControl}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
MyCustomControl should be a dependency property of your control.
How you would use this :
<MyNewControl>
<MyNewControl.MyCustomControl>
<StackPanel>
<MyControl/>
<MyButton/>
</StackPanel>
</MyNewControl.MyCustomControl>
</MyNewControl>
Hope this helps

Categories

Resources