I’m glad to see accordion getting the excitement that it’s getting. I’ve gotten great feedback, keep it coming!
Part 1 was concerned with Accordion itself, we will now focus on the individual parts of Accordion.
AccordionItem
AccordionItem is to Accordion as is ListboxItem to Listbox. Accordion will only work with AccordionItems, and that is why, if you feed Accordion an item that is not of type AccordionItem, it will wrap it inside of one.
AccordionItem has several important jobs:
- It needs to display a header and content.
- It needs to be able to ‘open’ and ‘close’
- It needs to be able to work in several ExpandDirections (Left, Right, Top, Bottom)
AccordionItem mimicks Expander in many ways (although I changed the template considerably) but inherits from HeaderedContentControl. Therefore, it has two important properties it gains from HeaderedContentControl: HeaderTemplate and ContentTemplate.
These templates are used to allow you to customize the header and content.
I will use KeyValue pairs to easily create a few AccordionItems:
acc.ItemsSource = new[]
{
new KeyValuePair<string, string>("A header", "And the content of the accordion item"),
new KeyValuePair<string, string>("Hello", "World") };
Let’s take a look at an Accordion and it’s Xaml:
ExpandDirection
An AccordionItem has the same ExpandDirection property as Accordion. When contained within an Accordion, AccordionItem will get the correct ExpandDirection from Accordion and will not allow you to change it individually.
ExpandDirection may only be set by Accordion, to prevent from weird mixes of ExpandDirections. Unfortunately, that does limit a few scenario’s (as in horizontal Accordion layout with vertical AccordionItems). If this turns out to be a common featurerequest, I’ll look into opening this up.
Accordion.ItemContainerGenerator
Accordion exposes an ItemContainerGenerator which has very helpful methods, such as ‘ContainerFromItem’ and even ContainerFromIndex’. So it is really easy to go from AccordionItem, to Item and vice versa.
This code might make you happy:
for (int i = 0; i < acc.Items.Count; i++)
{
AccordionItem item = acc.ItemContainerGenerator.ContainerFromIndex(i) as AccordionItem;
}
Locking mechanism
In certain SelectionModes, Accordion must make sure that at least one item is selected. It does so by locking an item if it is the last one open. If you somehow force it to close (through code), Accordion will just open the first AccordionItem in the list. However, that is not that simple, since an AccordionItem may not be unselected, while it is locked.
Since AccordionItem will actually throw an exception when it is unselected while locked, I expose a boolean ‘IsLocked’ that you can use to make sure you don’t accidently do this.
I expose a VisualState that allows you to visualize the lock if you’d like.
The best way to unselect the AccordionItem while in such a mode, is to select a different item.
ExpandableContentControl and AccordionButton
These are two template parts on the AccordionItem. The first takes care of opening and closing in a nice fashion and the latter makes it easier to template the header. It is not necessary, but adds a nice touch. I will talk more about templating them in a follow up post.
In the meantime, it is noteworthy that there is a property AccordionButtonStyle that you can use to style the button more easily.
Selected and Unselected events
Subscribe to these events to know when the user has selected an AccordionItem. Alternatively, you can use the SelectionChanged event on Accordion.
Layout
Under the covers, there is a lot of layout action going on! The item needs to know how to open itself, and also needs to be told _when_ to do so. The actual opening and closing does not correspond to the IsSelected state. In other words: IsSelected will be set whenever an item is selected, which could mean the AccordionItem is still closed. Accordion will instruct AccordionItem to actually open to visualize the new IsSelected state.
Templating
The most important part of the template is:
1 <Border x:Name="Background"
2 Padding="{TemplateBinding Padding}"
3 BorderBrush="{TemplateBinding BorderBrush}"
4 BorderThickness="{TemplateBinding BorderThickness}"
5 CornerRadius="1,1,1,1">
6 <Grid>
7 <Grid.RowDefinitions>
8 <RowDefinition Height="Auto" x:Name="rd0"/>
9 <RowDefinition Height="Auto" x:Name="rd1"/>
10 </Grid.RowDefinitions>
11 <Grid.ColumnDefinitions>
12 <ColumnDefinition Width="Auto" x:Name="cd0"/>
13 <ColumnDefinition Width="Auto" x:Name="cd1"/>
14 </Grid.ColumnDefinitions>
15
16 <layoutPrimitivesToolkit:AccordionButton
17 x:Name="ExpanderButton"
18 Style="{TemplateBinding AccordionButtonStyle}"
19 Content="{TemplateBinding Header}"
20 ContentTemplate="{TemplateBinding HeaderTemplate}"
21 IsChecked="{TemplateBinding IsSelected}"
22 IsTabStop="True"
23 Grid.Row="0"
24 Padding="0,0,0,0"
25 Margin="0,0,0,0"
26 FontFamily="{TemplateBinding FontFamily}"
27 FontSize="{TemplateBinding FontSize}"
28 FontStretch="{TemplateBinding FontStretch}"
29 FontStyle="{TemplateBinding FontStyle}"
30 FontWeight="{TemplateBinding FontWeight}"
31 Foreground="{TemplateBinding Foreground}"
32 VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
33 HorizontalAlignment="Stretch"
34 VerticalAlignment="Stretch"
35 HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
36 Background="{TemplateBinding Background}" />
37
38 <layoutPrimitivesToolkit:ExpandableContentControl
39 x:Name="ExpandSite"
40 Grid.Row="1"
41 IsTabStop="False"
42 Percentage="0"
43 RevealMode="{TemplateBinding ExpandDirection}"
44 Content="{TemplateBinding Content}"
45 ContentTemplate="{TemplateBinding ContentTemplate}"
46 Margin="0,0,0,0"
47 FontFamily="{TemplateBinding FontFamily}"
48 FontSize="{TemplateBinding FontSize}"
49 FontStretch="{TemplateBinding FontStretch}"
50 FontStyle="{TemplateBinding FontStyle}"
51 FontWeight="{TemplateBinding FontWeight}"
52 Foreground="{TemplateBinding Foreground}"
53 HorizontalContentAlignment="Left"
54 VerticalContentAlignment="Top"
55 HorizontalAlignment="Stretch"
56 VerticalAlignment="Stretch"/>
57 </Grid>
58 </Border>
Lines 7 through 14 create a grid with 2 columns and 2 rows.
Line 16 shows the AccordionButton, which is the little arrow + header.The arrow always points to the content and is in different locations, depending on the ExpandDirection.
Line 38 is the ExpandableContentControl, which takes care of the content.
An AccordionItem has, amongst others, 4 visual states for ExpandDirection. I will show the ExpandLeft state:
1 <vsm:VisualState x:Name="ExpandLeft">
2 <Storyboard>
3 <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ExpanderButton" Storyboard.TargetProperty="(Grid.ColumnSpan)">
4 <DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
5 </ObjectAnimationUsingKeyFrames>
6 <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ExpandSite" Storyboard.TargetProperty="(Grid.ColumnSpan)">
7 <DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
8 </ObjectAnimationUsingKeyFrames>
9 <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ExpanderButton" Storyboard.TargetProperty="(Grid.RowSpan)">
10 <DiscreteObjectKeyFrame KeyTime="0" Value="2"/>
11 </ObjectAnimationUsingKeyFrames>
12 <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ExpandSite" Storyboard.TargetProperty="(Grid.RowSpan)">
13 <DiscreteObjectKeyFrame KeyTime="0" Value="2"/>
14 </ObjectAnimationUsingKeyFrames>
15
16 <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ExpanderButton" Storyboard.TargetProperty="(Grid.Column)">
17 <DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
18 </ObjectAnimationUsingKeyFrames>
19 <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ExpandSite" Storyboard.TargetProperty="(Grid.Row)">
20 <DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
21 </ObjectAnimationUsingKeyFrames>
22 <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="rd0" Storyboard.TargetProperty="Height">
23 <DiscreteObjectKeyFrame KeyTime="0" Value="*"/>
24 </ObjectAnimationUsingKeyFrames>
25 <ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="cd0" Storyboard.TargetProperty="Width">
26 <DiscreteObjectKeyFrame KeyTime="0" Value="*"/>
27 </ObjectAnimationUsingKeyFrames>
28 </Storyboard>
29 </vsm:VisualState>
Everything in there is actually repositioning the AccordionButton (header) and the ExpandableContentControl (content) inside different grid cells.
In this case, you can imagine the header being in column 1 and the content taking up column 2.
You should not need to have to style an accordionItem itself that much. Most of the template is just about positioning the Header versus the Content. The only reason I see for restyling accordionItem is if you are unhappy with the defaults (easily changed) or with the position of the header and content. All the other styling would be done in either AccordionButton or ExpandableContentControl. Let me know if this is not the case for your scenario.