在ListView的GroupItem头中显示每列的Summary


问题描述

     WPF自带的ListView和DataGrid控,都提供了数据分组的支持,并可以对分组的Header进行自定义。但是,如果想在每个分组的Header中,显示出本分组的"小计"就不是一件容易的事情了。

     假设要用一个ListView用于显示全校学生成绩。按班级分组,并在分组头中显示班级平均分。

     最终效果大致如下:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

    图1. 在分组的Header中显示本分组的Aggregation

 

    怎么样?有什么思路?实现的难点有:

  1. Group Header中的第一例显示为分组的名称。
  2. Group Header中的其它列与数据一致。
  3. Group Header中各列的宽度与ListView中列对应始终一致。
  4. Group Header 中各列的对齐方式与ListView中各列一致。

数据

    Model层只有一个类,ScoreInfo,代码如下:

 

public  class ScoreInfo
{
     public ScoreInfo( string studentNo,  string className,  int math,  int english)
    {
        StudentNo = studentNo;
        ClassName = className;
        MathScore = math;
        EnglishScore = english;
    }

     public  string StudentNo {  getset; }

     public  string ClassName {  getset; }

     public  int MathScore {  getset; }

     public  int EnglishScore {  getset; }

    [ReadOnly( true)]
     public  int TotalScore
    {
         get {  return MathScore + EnglishScore; }
    }
}

 

    一次考试中,学生的这些数据都不会变。所以不需要实现INotifyPropertyChanged接口。

    DAL层直接返回假数据,代码如下:

 

public  class SchoolScoreProvider
{
     public List<ScoreInfo> ReadAllScoreInfo()
    {
         var random =  new Random();
         return ( from i  in Enumerable.Range( 04)
                let className = String.Format( " ({0}) 班 ", i +  1)

                 from s  in Enumerable.Range(i *  6 +  16)
                let mScore = random.Next( 101)
                let eScore = random.Next( 101)

                 select  new ScoreInfo(s.ToString(), className, mScore, eScore))
                .ToList();
    }
}

 

在XAML中声明(实体化)数据源,代码如下:

 

<!--  Prepare Data Source  -->
<!--  Just for demo, DON'T initialize your Data Source this way in real project.  -->
< ObjectDataProvider  x:Key ="Data"
                    ObjectType
=" {x:Type l:SchoolScoreProvider} "
                    MethodName
="ReadAllScoreInfo"   />
< CollectionViewSource  x:Key ="DataView"
                      Source
=" {StaticResource Data} " >
     <!--  Group By ClassName  -->
     < CollectionViewSource.GroupDescriptions >
         < PropertyGroupDescription  PropertyName ="ClassName"   />
     </ CollectionViewSource.GroupDescriptions >
     <!--  Sort By TotalScore  -->
     < CollectionViewSource.SortDescriptions >
         < c:SortDescription  PropertyName ="TotalScore"  Direction ="Descending"   />
     </ CollectionViewSource.SortDescriptions >
</ CollectionViewSource >

 

UI层代码

    然后是ListView的定义:

 

< ListView  DataContext =" {StaticResource DataView} "
          ItemsSource
=" {Binding} " >
     <!--  Specify the current ListView should Group data source items.  -->
     < ListView.GroupStyle >
         < GroupStyle  />
     </ ListView.GroupStyle >
     < ListView.View >
         < GridView >
             <!--  CellTemplate is the only NORMAL way to make the column text right align.  -->
             <!--  The following code should works with the default ListViewItem style above in Resources.
                 BUT it doesn't.
                 <GridViewColumn Header="学号" Width="75"
                                 DisplayMemberBinding="{Binding StudentNo}"
                                 TextBlock.TextAlignment="Right" />
                 Think, if you need to change the Alignment while running. Their differences will be obvious.
            
-->
             < GridViewColumn  Header ="学号"  Width ="75" >
                 < GridViewColumn.CellTemplate >
                     < DataTemplate >
                         < TextBlock  Text =" {Binding StudentNo} "
                                   TextAlignment
="Right"   />
                     </ DataTemplate >
                 </ GridViewColumn.CellTemplate >
             </ GridViewColumn >
             < GridViewColumn  Header ="数学成绩"  Width ="75" >
                 < GridViewColumn.CellTemplate >
                     < DataTemplate >
                         < TextBlock  Text =" {Binding MathScore} "
                                   TextAlignment
="Right"   />
                     </ DataTemplate >
                 </ GridViewColumn.CellTemplate >
             </ GridViewColumn >
             < GridViewColumn  Header ="英语成绩"  Width ="75" >
                 < GridViewColumn.CellTemplate >
                     < DataTemplate >
                         < TextBlock  Text =" {Binding EnglishScore} "
                                   TextAlignment
="Right"   />
                     </ DataTemplate >
                 </ GridViewColumn.CellTemplate >
             </ GridViewColumn >
         </ GridView >
     </ ListView.View >
</ ListView >

 

     为了让Group支持分列,直接能想到的方法就是在其中放置一个ListViewItem。经过尝试这个方案是可行的。需要自定义GroupItem的ControlTemplate。代码如下:

 

<!--  Custom GroupItem to support group collaps/expand  -->
< Style  TargetType =" {x:Type GroupItem} " >
     < Setter  Property ="Template" >
         < Setter.Value >
             < ControlTemplate  TargetType =" {x:Type GroupItem} " >
                 < Grid >
                     < Grid.RowDefinitions >
                         < RowDefinition  Height ="Auto"   />
                         < RowDefinition  Height ="*"   />
                     </ Grid.RowDefinitions >
                     < ListViewItem   Style =" {StaticResource GroupHeaderListViewItemStyle} "
                                   Content
=" {Binding Converter={StaticResource GroupDataAggregator}} "   />
                     < ToggleButton  Name ="expander"
                                  Style
=" {StaticResource ToggleExpanderStyle} "   />
                     < ItemsPresenter  Grid.Row ="1"
                                    Visibility
=" {Binding IsChecked, ElementName=expander, Converter={StaticResource BooleanToVisibilityConverter}} "   />
                 </ Grid >
             </ ControlTemplate >
         </ Setter.Value >
     </ Setter >
</ Style >

 

    其中的关键点是,在里面放了一个ListViewItem。这里直接放GridViewRowPresenter也是可以正确显示出数据的,但是由于没有ListViewItem作Host,每列中的文字会始终向左对齐。

    使用一个Converter,对当前组的数据进行Aggregation,生成一个表示班级平均分的ScoreInfo。Convert的代码如下:

 

public  class GroupDataAggregator : IValueConverter
{
     public  object Convert( object value, Type type,  object arg, CultureInfo culture)
    {
         var groupData = value  as CollectionViewGroup;
         if (groupData !=  null)
        {
             var scores = groupData.Items.Cast<ScoreInfo>();
             var avgMath = ( int)scores.Average(x => x.MathScore);
             var avgEng = ( int)scores.Average(x => x.EnglishScore);

             return  new ScoreInfo(groupData.Name.ToString(),  null, avgMath, avgEng);
        }

         return  new InvalidOperationException();
    }

     public  object ConvertBack( object value, Type type,  object arg, CultureInfo culture)
    {
         throw  new NotImplementedException();
    }
}

 

     当然,这个Convert并不重点。另一个重点是,要为GroupItem中的ListViewItem写一个特殊的Style,否则,什么东西都看不到。

 

<!--  Style apply to all ListViewItem, to make its cell stretch.  -->
< Style  TargetType =" {x:Type ListViewItem} " >
     < Setter  Property ="HorizontalContentAlignment"  Value ="Stretch"   />
</ Style >

<!--  Style apply to the ListViewItem in GroupItem header  -->
< Style  x:Key ="GroupHeaderListViewItemStyle"
       TargetType
=" {x:Type ListViewItem} "
       BasedOn
=" {StaticResource {x:Type ListViewItem}} " >
     < Setter  Property ="Template" >
         < Setter.Value >
             < ControlTemplate  TargetType =" {x:Type ListViewItem} " >
                 <!--  KEY STEP: Binding the Columns to ListView's  -->
                 < GridViewRowPresenter  Columns =" {Binding View.Columns, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}} "   />
             </ ControlTemplate >
         </ Setter.Value >
     </ Setter >
</ Style >

 

    关键的一步就是给GridViewRowPresenter指定Columns,ListView可不会自动为这个外人设置这个属性的。

    再回去讲GroupItem,用于展开GroupItem的是其Template中的ToggleButton,即图中三角图标。其代码如下:

 

<!--  The toggle button in GroupItem Header, used to expand/collaps a group  -->
< Style  x:Key ="ToggleExpanderStyle"
       TargetType
=" {x:Type ToggleButton} " >
     < Setter  Property ="Width"  Value ="16"   />
     < Setter  Property ="Height"  Value ="16"   />
     < Setter  Property ="HorizontalAlignment"  Value ="Left"   />
     < Setter  Property ="VerticalAlignment"  Value ="Center"   />
     < Setter  Property ="HorizontalContentAlignment"  Value ="Center"   />
     < Setter  Property ="VerticalContentAlignment"  Value ="Center"   />
     < Setter  Property ="IsChecked"  Value ="True"   />
     < Setter  Property ="BorderThickness"  Value ="1"   />
     < Setter  Property ="BorderBrush"  Value ="Black"   />
     < Setter  Property ="Background"  Value ="Black"   />
     < Setter  Property ="Padding"  Value ="0"   />
     < Setter  Property ="SnapsToDevicePixels"  Value ="True"   />
     < Setter  Property ="FocusVisualStyle"  Value =" {x:Null} "   />
     < Setter  Property ="IsTabStop"  Value ="False"   />
     < Setter  Property ="Content" >
         < Setter.Value >
             < StreamGeometry >M6,0 L6,6 L0,6Z </ StreamGeometry >
         </ Setter.Value >
     </ Setter >
     < Setter  Property ="Template" >
         < Setter.Value >
             < ControlTemplate  TargetType =" {x:Type ToggleButton} " >
                 <!--  The border is used to make the control a rectangle which is easier to click.  -->
                 < Border  Background ="Transparent"
                        BorderThickness
="0" >
                     < Path  Stretch ="None"
                          Data
=" {TemplateBinding Content} "
                          Margin
=" {TemplateBinding Padding} "
                          Fill
=" {TemplateBinding Background} "
                          Stroke
=" {TemplateBinding BorderBrush} "
                          StrokeThickness
=" {TemplateBinding BorderThickness} "
                          VerticalAlignment
=" {TemplateBinding VerticalContentAlignment} "
                          HorizontalAlignment
=" {TemplateBinding HorizontalContentAlignment} "   />
                 </ Border >
             </ ControlTemplate >
         </ Setter.Value >
     </ Setter >
     < Style.Triggers >
         < Trigger  Property ="IsMouseOver"  Value ="True" >
             < Setter  Property ="Background"  Value ="#007ACC"   />
             < Setter  Property ="BorderBrush"  Value ="#007ACC"   />
         </ Trigger >
         < Trigger  Property ="IsChecked"  Value ="False" >
             < Setter  Property ="Background"  Value ="White"   />
             < Setter  Property ="Content" >
                 < Setter.Value >
                     < StreamGeometry >M0,0 L4,4 L0,8Z </ StreamGeometry >
                 </ Setter.Value >
             </ Setter >
         </ Trigger >
     </ Style.Triggers >
</ Style >

 

题外话

    另给勤奋些、不喜欢吃冷饭的同学几个思考的方向,用于确认对上面代码的理解程度。

    针对上面这个ToggleButton的Style:

  1. 每个Setter都有什么作用?删除每一个都有什么不同后果?哪些是可以删除的?(用时5分钟)
  2. 使用TemplateBinding与直接在ControlTemplate中写值有什么不同?(30秒)
  3. 为什么把BorderThickness绑定给了Path,而不是Border?(30秒)
  4. Style中的Trigger能否放在ControlTemplate.Triggers中?为什么使用Style Trigger而不是ControlTemplate Trigger?(5分钟)
  5. 如果你不会StreamGeometry中的语法,那么尝试解读StreamGeometry中的语法(其中Z表示曲线向原点闭合),并写出一个+ - 号风格的ToggleButton Style。(15分钟)

     如果只是要方案。这里是完整的代码



免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM