Creating a re-usable DataTemplate resource for an ItemsControl - c#

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

Related

How can I make controls in WPF stretch to fill available width when HorizontalAlignment and HorizontalContentAlignment don't work?

I'm trying to make a simple WPF app that has sections that fill the available width. Despite trying various ways of stretching the width of elements, containers, and children, nothing is working and I can't figure out why.
Another question said to use uniformgrid which worked well EXCEPT that it set the height of all the elements uniformly which was definitely not what I wanted. I want all of the sections to look like the one in the picture - filled width, height auto based on the content. Here's the basic setup:
<Window x:Class="A_Customizer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:A_Customizer"
mc:Ignorable="d"
Title="MainWindow"
Background="#FF2B2B2B"
Width="800"
>
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type CheckBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Name="mainApp" >
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<WrapPanel Grid.Row="0" >
<Button ToolTip="Click to apply the below settings to this Jumpbox" Click="ApplyCustomizations">Customize</Button>
</WrapPanel>
<ScrollViewer Grid.Row="1">
<WrapPanel HorizontalAlignment="Stretch" >
<GroupBox
Background="#FFE2E2E2"
BorderBrush="#FF7F7F7F"
Margin="10,10,10,10"
Name="pathsBox"
HorizontalContentAlignment="Stretch"
HorizontalAlignment="Stretch"
>
<GroupBox.Header>
<Border Background="#FFAFAFAF" CornerRadius="3">
<Label FontWeight="Bold">Key Paths</Label>
</Border>
</GroupBox.Header>
<StackPanel HorizontalAlignment="Stretch">
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBox Name="homeFolder" Grid.Column="0" HorizontalAlignment="Stretch"></TextBox>
<Button Grid.Column="1" Click="NewQuickPath" ToolTip="Change home folder">
<Image Source="images\add_folder.png" Height="25" Cursor="Hand"></Image>
</Button>
</Grid>
<TextBox Name="progFolder" Grid.Column="0" HorizontalAlignment="Stretch"></TextBox>
</StackPanel>
</GroupBox>
<GroupBox
Background="#FFE2E2E2"
BorderBrush="#FF7F7F7F"
Margin="10,10,10,10"
Name="quickBox"
Height="auto"
HorizontalContentAlignment="Stretch"
>
<GroupBox.Header>
<Border Background="#FFAFAFAF" CornerRadius="3">
<Label FontWeight="Bold">Quick Access Folders</Label>
</Border>
</GroupBox.Header>
<StackPanel HorizontalAlignment="Stretch">
<TextBlock TextWrapping="Wrap" Margin="15">
There are going to be folders you'll need to access frequently and keeping them pinned on top of the left menu in Explorer is helpful.
Select here to add them to the list of folders restored with the "Customize" button. Click any folder to remove it.
</TextBlock>
<Border CornerRadius="3" Background="#FFF3C7C7" Margin="6" Visibility="Collapsed" Name="quickErr" Tag="err_box">
<TextBlock Tag="errMsg" Foreground="#FFFD3434" TextWrapping="Wrap" Margin="6" ></TextBlock>
</Border>
<UniformGrid Name="quickPathsArea" Columns="1">
</UniformGrid>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" HorizontalAlignment="Stretch"></TextBox>
<Button Grid.Column="1" Click="NewQuickPath" ToolTip="Add a new folder">
<Image Source="images\add_folder.png" Height="25" Cursor="Hand"></Image>
</Button>
</Grid>
</StackPanel>
</GroupBox>
</wrappanel>
</scrollviewer>
</grid>
StackPanel with Orientation="Vertical" (default value) instead of WrapPanel should work: it will allow each child element use full width and as much height as necessary

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>

Styled Expander Control Template and not able to view Expanded Content on Click

I am just styling the Expander Control in WPF. I have defined only the styles in Expander Control Template, but I am not able to view the content of expander when I click on it.
I guess I have to define the expander trigers also ? but I don't know which triger and how to define it.
Also why I have to define triggers when I am just styling the expander.
<Window x:Class="ExpanderControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="Expander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="0" Name="contentRow"></RowDefinition>
</Grid.RowDefinitions>
<!--Expander Header-->
<Border Background="AliceBlue"
Grid.Row="0"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="20"></ColumnDefinition>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0" ContentSource="Header"
RecognizesAccessKey="True"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Margin="5"
></ContentPresenter>
<ToggleButton Grid.Column="1">
<TextBlock>x</TextBlock>
</ToggleButton>
</Grid>
</Border>
<!--Expander Content-->
<Border Background="Aqua" Grid.Row="1">
<ContentPresenter Grid.Row="1"></ContentPresenter>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Expander Header="Expander" HorizontalAlignment="Left" Margin="205,95,0,0" VerticalAlignment="Top" Width="200">
<Grid Background="#FFE5E5E5">
<Menu>
<MenuItem Header="hi"></MenuItem>
</Menu>
</Grid>
</Expander>
</Grid>
</Window>
At the moment there is no action linked to ToggleButton. You need to utilize Expander.IsExpanded property by binding it
To ToggleButton.IsChecked
To Border.Visibility via BooleanToVisibilityConverter (custom or built in)
Set content row height to Auto
This way changing ToggleButton.IsChecked will change Expander.IsExpanded which in turn will affect visibility of content Border. It will also work when you change IsExpanded property from outside.
This is working XAML
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<Style TargetType="{x:Type Expander}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="Auto" Name="contentRow"/>
</Grid.RowDefinitions>
<!--Expander Header-->
<Border Background="AliceBlue" Grid.Row="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0" ContentSource="Header" RecognizesAccessKey="True" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="5" />
<ToggleButton Grid.Column="1" Content="x" IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsExpanded}"/>
</Grid>
</Border>
<!--Expander Content-->
<Border Background="Aqua" Grid.Row="1" Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
<ContentPresenter/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
EDIT
If you want whole header to be able to expand/collapse your Expander you need to bring ContentPresenter for Header into Content of ToggleButton. In your case basically bring header Grid into ToggleButton.Content
<!--Expander Header-->
<ToggleButton IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsExpanded}" HorizontalContentAlignment="Stretch" Grid.Row="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0" ContentSource="Header" RecognizesAccessKey="True" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="5" />
<TextBlock Grid.Column="1" Text="x" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ToggleButton>

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>

Template initialisation causes stack overflow

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.

Categories

Resources