一、TreeView要求父子节点控件类型需一致
最近在工作项目中,需要将ListBox (如图1)

图1 ListBox
修改为带展开按钮的树视图(如图2)

图2 带展开按钮的树视图
在修改时,本WPF新手首先想到的是TreeView,结合网上资料边学边折腾。最后发现,在绑定数据源的的前提下,TreeView在父子节点只能应用相同的控件种类。如下图,HierarchicalDataTemplate中的控件被同时应用到了父节点和子节点。
<TreeView Height="NaN" Width="NaN" x:Name="treeView" ItemsSource="{Binding ListMeasureStation}" pu:TreeViewHelper.SelectedBackground="Red" pu:TreeViewHelper.ItemHeight="100" pu:TreeViewHelper.SelectMode="Any" pu:TreeViewHelper.TreeViewStyle="Standard">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding list_sleeper_detail}">
<StackPanel>
<RadioButton Content="{Binding station_number}"/>
<TextBlock Text="{Binding station_number}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>

图3 RadioButton和TextBlock控件被同时应用到了父节点和子节点
故在本次修改中,放弃TreeView改用Expander。
总结:TreeView只适用于多节点,且父子节点的控件类型相同的应用场景。
二、改用Expander
改用Expander编码时,先是参考了站内的这篇WPF 简易手风琴 (ListBox+Expander)文章,编译运行时发现Expander的内容高度不能改变,查询相关资料Template,ItemsPanel,ItemContainerStyle ,ItemTemplate四个属性辨析后,高度可变。编码大致思路:ListBox套Expander再套ListBox。(由于还未将Style写进Resources中,故代码比较冗余)
<ListBox x:Name="ItemBox" MaxHeight="1500" Width="350" ItemsSource="{Binding ListMeasureStation}" Template="{DynamicResource ListBoxControlTemplate_101}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Expander Background="#f2f2f2" pu:ExpanderHelper.ExpanderStyle="Classic">
<Expander.Header>
<Border Height="60" Width="330" >
<TextBlock Text="{Binding station_number,StringFormat={}第{0}站}" FontSize="32" FontWeight="Bold" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
</Expander.Header>
<Expander.Content>
<ListBox MaxHeight="1500" SelectedItem="{Binding Source={StaticResource Locator},Path=MeasureDetail.SleeperDetailSingle}" SelectedIndex="{Binding Source={StaticResource Locator},Path=MeasureDetail.SelectedIndex}" ItemsSource="{Binding list_sleeper_detail}" Template="{DynamicResource ListBoxControlTemplate_101}">
<behaviors:Interaction.Behaviors>
<func:AutoScrollBehavior />
</behaviors:Interaction.Behaviors>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="border_Row" Background="#f2f2f2">
<Grid x:Name="grid_Row" Height="90" Margin="0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="4*" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="txt_Num" Text="1" Loaded="TextBlock_Loaded" Margin="10,20,10,10" TextAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="#32D3CD" FontSize="32" FontWeight="Bold"/>
<Grid Grid.Column="1" Margin="0,10,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<TextBlock x:Name="txt_SleeperNumber" Text="{Binding sleeper_number,StringFormat={}{0}(编号)}" Margin="0" TextAlignment="Center" HorizontalAlignment="Left" Foreground="#000000" FontSize="26" FontFamily="苹方-简, 苹方-简-Normal" />
<TextBlock x:Name="txt_Footage" Text="{Binding footage,StringFormat={}DK{0:F5}}" Margin="0" TextAlignment="Center" HorizontalAlignment="Left" Foreground="#000000" FontSize="26"/>
</Grid>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="false">
<Setter Property="Foreground" TargetName="txt_SleeperNumber" Value="black" />
<Setter Property="Foreground" TargetName="txt_Footage" Value="black" />
<Setter Property="Background" TargetName="grid_Row" Value="white" />
</Trigger>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Foreground" TargetName="txt_Num" Value="white" />
<Setter Property="Foreground" TargetName="txt_SleeperNumber" Value="white" />
<Setter Property="Foreground" TargetName="txt_Footage" Value="white" />
<Setter Property="Background" TargetName="grid_Row" Value="#6f99fe" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Expander.Content>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
实现的效果如下:

图4 ListBox套Expander再套ListBox 使用ItemContainerStyle自定义ListBoxItem子项的控件样式
自我批判:
1、有没有一种可能,找一个完善的UI控件库就不必这么大费周折了。。。
2、或许有更好的解决方案?
三、Expander和ListBox互相嵌套时的“bug”
主体建好后,就到了修bug时间了。之所以bug打引号,是因为有些点不算bug,但影响软件实际使用时的人机交互。
“bug”列表:
实际只需要同时展开某一个Expander的内容,不需要同时展开多个Expander。
最内层的多个ListBox的选中项需是唯一的,不能出现如图4中同时选中两个最内层元素的情况。
鼠标指针停靠在Expander内容区时,默认只会触发内层ListBox的滚动效果,无法实现外层ListBox的滚动。
经过一番折腾,找到了对应的解决方案。
解决方案:
将Expander的IsExpanded属性绑定到父容器ListBoxItem的IsSelected属性。
<Expander MaxHeight="900" Background="Transparent" pu:ExpanderHelper.ExpanderStyle="Standard" BorderThickness="0" IsExpanded="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=ListBoxItem}, Path=IsSelected}">
放弃SelectedItem,改用 SelectedValue。当在ListBox中找不到对应的SelectedItem时,绑定SelectedItem属性不会取消选择原选项。而绑定SelectedValue属性会取消选择原选项。ps:SelectedValue属性取消选择多个原选项时,会多次触发SelectionChanged事件,可能导致SelectedIndex属性跳变多次,丢失正确index值。
<ListBox SelectedValue="{Binding MySelectedItem}" />
暂缓处理。(在修复前两个bug后,此bug的问题得到缓冲,后续处理时再补充此文)

图5 最终效果
最终版代码:
<ListBox x:Name="lbox_MeasureStation" MaxHeight="1500" Width="350" ItemsSource="{Binding ListMeasureStation}" Template="{DynamicResource ListBoxControlTemplate_101}" BorderThickness="0" Background="Transparent">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Expander MaxHeight="900" Background="Transparent" pu:ExpanderHelper.ExpanderStyle="Standard" BorderThickness="0" IsExpanded="{Binding RelativeSource ={ RelativeSource Mode=FindAncestor, AncestorType=ListBoxItem}, Path=IsSelected}">
<Expander.Header>
<Border Height="60" Width="330">
<TextBlock Text="{Binding station_number,StringFormat={}第{0}站}" FontSize="32" FontWeight="Bold" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
</Expander.Header>
<Expander.Content>
<ListBox x:Name="lbox_MeasureDetail" MaxHeight="900" SelectionChanged="lbox_MeasureDetail_SelectionChanged" SelectedValue="{Binding Source={StaticResource Locator},Path=MeasureDetail.SleeperDetailSingle}" ItemsSource="{Binding list_sleeper_detail}" Template="{DynamicResource ListBoxControlTemplate_101}" BorderThickness="0" Background="Transparent">
<behaviors:Interaction.Behaviors>
<func:AutoScrollBehavior />
</behaviors:Interaction.Behaviors>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<mvvm:EventToCommand Command="{Binding Source={StaticResource Locator},Path=MeasureDetail.DataSwitchCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="border_Row" Background="#f2f2f2">
<Grid x:Name="grid_Row" Height="90" Margin="0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="4*" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="txt_Num" Text="1" Loaded="TextBlock_Loaded" Margin="10,20,10,10" TextAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="#32D3CD" FontSize="32" FontWeight="Bold" />
<Grid Grid.Column="1" Margin="0,10,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<TextBlock x:Name="txt_SleeperNumber" Text="{Binding sleeper_number,StringFormat={}{0}}" Margin="0" TextAlignment="Center" HorizontalAlignment="Left" Foreground="#000000" FontSize="26" />
<TextBlock x:Name="txt_Footage" Text="{Binding footage,StringFormat={}DK{0:F5}}" Margin="0" TextAlignment="Center" HorizontalAlignment="Left" Foreground="#000000" FontSize="26" Grid.Row="1" />
</Grid>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="false">
<Setter Property="Foreground" TargetName="txt_SleeperNumber" Value="black" />
<Setter Property="Foreground" TargetName="txt_Footage" Value="black" />
<Setter Property="Background" TargetName="grid_Row" Value="white" />
</Trigger>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Foreground" TargetName="txt_Num" Value="white" />
<Setter Property="Foreground" TargetName="txt_SleeperNumber" Value="white" />
<Setter Property="Foreground" TargetName="txt_Footage" Value="white" />
<Setter Property="Background" TargetName="grid_Row" Value="#6f99fe" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Expander.Content>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
ListBox滚动条样式:
<ControlTemplate x:Key="ListBoxControlTemplate_101" TargetType="{x:Type ListBox}">
<Border x:Name="Bd" Grid.ColumnSpan="2" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="1" SnapsToDevicePixels="True">
<ScrollViewer Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" Padding="{TemplateBinding Padding}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" TargetName="Bd" Value="#FFD9D9D9" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<!--<Condition Property="IsGrouping" Value="True" />-->
<Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="ScrollViewer.CanContentScroll" Value="False" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>