在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