問題描述
WPF自帶的ListView和DataGrid控,都提供了數據分組的支持,並可以對分組的Header進行自定義。但是,如果想在每個分組的Header中,顯示出本分組的"小計"就不是一件容易的事情了。
假設要用一個ListView用於顯示全校學生成績。按班級分組,並在分組頭中顯示班級平均分。
最終效果大致如下:
圖1. 在分組的Header中顯示本分組的Aggregation
怎么樣?有什么思路?實現的難點有:
- Group Header中的第一例顯示為分組的名稱。
- Group Header中的其它列與數據一致。
- Group Header中各列的寬度與ListView中列對應始終一致。
- Group Header 中各列的對齊方式與ListView中各列一致。
數據
Model層只有一個類,ScoreInfo,代碼如下:
{
public ScoreInfo( string studentNo, string className, int math, int english)
{
StudentNo = studentNo;
ClassName = className;
MathScore = math;
EnglishScore = english;
}
public string StudentNo { get; set; }
public string ClassName { get; set; }
public int MathScore { get; set; }
public int EnglishScore { get; set; }
[ReadOnly( true)]
public int TotalScore
{
get { return MathScore + EnglishScore; }
}
}
一次考試中,學生的這些數據都不會變。所以不需要實現INotifyPropertyChanged接口。
DAL層直接返回假數據,代碼如下:
{
public List<ScoreInfo> ReadAllScoreInfo()
{
var random = new Random();
return ( from i in Enumerable.Range( 0, 4)
let className = String.Format( " ({0}) 班 ", i + 1)
from s in Enumerable.Range(i * 6 + 1, 6)
let mScore = random.Next( 101)
let eScore = random.Next( 101)
select new ScoreInfo(s.ToString(), className, mScore, eScore))
.ToList();
}
}
在XAML中聲明(實體化)數據源,代碼如下:
<!-- 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的定義:
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。代碼如下:
< 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 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 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,即圖中三角圖標。其代碼如下:
< 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:
- 每個Setter都有什么作用?刪除每一個都有什么不同后果?哪些是可以刪除的?(用時5分鍾)
- 使用TemplateBinding與直接在ControlTemplate中寫值有什么不同?(30秒)
- 為什么把BorderThickness綁定給了Path,而不是Border?(30秒)
- Style中的Trigger能否放在ControlTemplate.Triggers中?為什么使用Style Trigger而不是ControlTemplate Trigger?(5分鍾)
- 如果你不會StreamGeometry中的語法,那么嘗試解讀StreamGeometry中的語法(其中Z表示曲線向原點閉合),並寫出一個+ - 號風格的ToggleButton Style。(15分鍾)
如果只是要方案。這里是完整的代碼。