惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
V
Vulnerabilities – Threatpost
C
Cisco Blogs
A
Arctic Wolf
L
LINUX DO - 热门话题
P
Proofpoint News Feed
Security Latest
Security Latest
AWS News Blog
AWS News Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
L
Lohrmann on Cybersecurity
W
WeLiveSecurity
爱范儿
爱范儿
Last Week in AI
Last Week in AI
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Security Affairs
PCI Perspectives
PCI Perspectives
C
Cybersecurity and Infrastructure Security Agency CISA
Spread Privacy
Spread Privacy
IT之家
IT之家
月光博客
月光博客
云风的 BLOG
云风的 BLOG
宝玉的分享
宝玉的分享
J
Java Code Geeks
美团技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
I
Intezer
博客园_首页
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Palo Alto Networks Blog
NISL@THU
NISL@THU
Recent Commits to openclaw:main
Recent Commits to openclaw:main
有赞技术团队
有赞技术团队
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
量子位
The Last Watchdog
The Last Watchdog
Google Online Security Blog
Google Online Security Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 聂微东
N
News and Events Feed by Topic
Webroot Blog
Webroot Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Security @ Cisco Blogs
罗磊的独立博客
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
V
V2EX
Jina AI
Jina AI

博客园 - 南柯之石

用户中心 - 博客园 自引用泛型模式分析 一定间隔时间下重复执行一个函数的几个方法 为什么.NET Framework就没有个专门的P/Invoke Library? Which would you perfer? "easy and fast and works well" or "hard and time consuming and error prone" 小例子背后的大道理——用户需求+设计原则+正确应用 =设计方案 GET和POST有什么区别?及为什么网上的多数答案都是错的。 小例子背后的大道理——Adapter模式详解 小例子背后的大道理——从DIP中“倒置”的含义说接口的正确使用 XmlSerializer, DataContractSerializer 和 BinaryFormatter区别与用法分析 使用WPF开发的扫雷游戏,双系统主题复刻版 技术误用带来的技术偏见及所谓的“经验” 从一个UI交互设计师的讲座说开去 NoSQL和MemeryCache的出现意味着传统数据库使用方式的变革吗? 不使用反射进行C#属性的运行时动态访问 一次模块划分的争论及其结局 电子书籍质量保证事件分析 在SQL Server中调用.NET程序集 T-SQL 操作XML示例
在ListView的GroupItem头中显示每列的Summary
南柯之石 · 2012-09-04 · via 博客园 - 南柯之石

问题描述

     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分钟)

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