个人开发历程知识库

关注C++/Java/C#技术, 致力于安防监控/移动应用/WEB方面开发
------------------------------------ 业精于勤,荒于嬉;行成于思,毁于随
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

OutlookGrid:以Outlook样式分组和排列数据项

Posted on 2009-05-29 01:45  peterzb  阅读(11103)  评论(2编辑  收藏  举报

作者:Herre Kuijpers

译者:小刀人

原文链接    源代码下载

摘要: 一个允许分组和排列数据项的网格,很像Outlook。

运行环境:C#,Windows (Win2K, WinXP, Win2003), .NET (.NET 2.0)
Win32, VS (VS2005), WinForms Dev


简介

如果您要处理用大量的比如说:一百个数据项(例如,收件箱里的邮件列表)的列表(译注:本文将items译为数据项),过滤、查找、分类,以及其它分组功能将很快变得困难而单调乏味。特别是排序和分组大大提高了一个列表中数据项的结构,默认情况下,这是我想要应用到我的所有列表的功能特性。特别情况下,我还会寻找一个允许将相似的项一起安排和分组列表/网格的控件,很像在Outlook 2003中使用的网格(或列表?)。

我知道这里有一些支持这类功能的商业列表/网格(控件);然而,在试用它们的时候我也遇到过若干bug。不能访问源代码(译注:因为商业目的)使得这非常令人沮丧,因此我想倒不如我写篇CodeProject文章,看看我是否可以拿出一个自定义解决方案。

因为网格比列表更加灵活,我决定实现一个可以一起分组的网格控件,正如Outlook一样。这个控件用C#2.0在VS 2005上实现的。现在,我不能保证这个实现没有bug,但至少它是免费的,并且它有源代码。因而,您可以根据您的需要修改它们,以适合于您自己的用途。无论如何要注意:这个控件并没有完成!一些功能也许不能正确地工作或完全不能工作。该控件主要关注于排序、分组并在网格上显示数据项,这些功能特性我想已做得相当地好了。在网格中插入、更新和删除行及单元格不在我考虑的范围之内。

在这篇文章中,我将解释这个控件怎么用,和它能做什么,并且也包含了什么它不能做,主要把焦点集中在只想要按现在的样子复用该控件的开发组。然后,对于为了他们自己的用途想要扩展或改变这个控件的实现的开发者,我将对这个控件内部运作做一点更为详细的解释。

背景

OutlookGrid继承于VS2005新推出的DataGridView控件。如果您对DataGridView熟悉,将OutlookGrid整合进您的解决方案应该是相当容易。如果您之前已做过一些GDI+编程和用户控件,OutlookGrid应该不难被扩展。

我想要用尽可能少的代码尽可能简单地创建OutlookGrid。因此,该控件不能使用复杂的钩子、回调及Windows API。该控件只简单地重写了一些DataGridView的事件处理器。不幸的是,DataGridView只实现了很少的事件,这让我花费了数个小时决定重写哪个事件。同样,这也花费了我不少的时间来找到一个可工作的解决方案以使得网格容易使用。

使用该代码

假定您已创建了一个VS2005下的C# Windows应用程序项目,添加OutlookGrid.cs、OutlookGrid.Designer.cs、OutlookGridRow.cs、OutlookGridGroup.cs和DataSourceManager.cs文件到您的项目。在添加OutlookGrid控件到您的窗体之前,确保您首先编译所有文件。在此之后,控件被添加到您的工具箱中。您现在可以将其拖至您的窗体。

一旦控件就位,这里有两个方法来填充网格:

  • 手动(非绑定的)添加列、行及单元格,或
  • 使用数据绑定(绑定的)。

正如在示例代码中所示:后面的一个方法是最简单的实现。

在本文中,我将不会讨论所有选项,然而,在演示项目和资源的例子中它们已被实现。

数据绑定

通常,只有两个数据类型可以被用于数据绑定:DataSet或一个对象数组列表(该列表必须实现IList接口,比如:ArrayList)其它类型到现在为止仍然不被支持,例如像:DataTable或DataTableView。


在创建窗体时添加以下代码:

//创建一个dataset(数据集)对象
          DataSet dataset = new DataSet();

//填充dataset,举例来说,通过从一个xml文件中读取数据
          dataset.ReadXml(@"invoices.xml");

//绑定dataset到OutlookGrid(在这个例子中命名为outlookGrid1)。
//设置dataMember(数据成员)变量为“invoice”,指示dataset中的表的名称以显示该表到网格中。
          outlookGrid1.BindData(dataset, "invoice");


注意OutlookGrid使用BindData()来绑定数据,而不是设置DataSource和DataMember属性。DataSource和DataMember属性现在是只读的。为了清空绑定,用:
outlookGrid1.BindData(null, null);.


分组和排序

到目前为止一切顺利,但我们仍不能做任何排序/分组工作。为了使用分组,网格需要被排序。为此,Sort(...)方法已经被实现了。通过从行的逻辑排列顺序选择同时用相似的值将行分配到相同的组中来创建数据项(行)的分组。这里有两个处理步骤。首先,确定数据项如何被分组,然后将这些项排序。

分组可以基于不同标准,比如:数据项可以按字母排序分组,将所有以‘A’开头的项放入相同的组中。然而缺省情况下,数据项是基于它们的string值分组的,因此有着相同string值的所有项将被放入相同组。为了让OutlookGrid知道用什么来分组,我们设置了outlookGrid1.GroupTemplate属性。该属性负责获得Group对象的一个实例来用。缺省情况下,它被设置为OutlookGridDefaultGroup。在排序中被创建的所有分组的行将从GroupTemplate对象中被逐字克隆。

因此,为了在我们的示例中分组各项,我们需要以这些数据项的其中一个特征属性来排序之。在这个示例中,我们将对DataTable(记住,我们绑定了一个DataSet到这个网格)"invoice"中的行排序。DataTable中的行是DataRow类型的,因此我们需要将DataRow排序。为了排序各项,.NET使用一个比较对象。比较对象实现了IComparer接口。在我们的例子中,我定义了一个DataRowComparer类(在Form1.cs文件)用来对网格中的数据项排序(而不是DataTable!)。

因此基本上,它归结为:

 

//基于一个索引确定对哪一列排序:
          int ColumnIndex = 2;

//设置使用的分组模版,比如,按字母顺序排序
          outlookGrid1.GroupTemplate = new OutlookGridAlphabeticGroup();

//确定Group将被关联的列
          outlookGrid1.GroupTemplate.Column = outlookGrid1.Columns[ColumnIndex];

//在列表中的所有分组将被折叠,因此只有分组显示,没有数据项
          outlookGrid1.GroupTemplate.Column.Collapsed = true;

//使用DataRowComparer对象排序网格,DataRowComparer构造器有两个参数:
//将被排序的列,和排序的方向(升序和降序)
          outlookGrid1.Sort(new DataRowComparer(ColumnIndex, direction));


在执行上面的代码之后,网格将按字母排序显示所有分组的数据项。另一方面,如果您想要排序列表,但是因为一些隐私的原因不想将数据项分组,只要在调用Sort方法前设置outlookGrid1.GroupTemplate = null。

作为选择之一,OutlookGrid支持附加函数以定义数据项如何被显示:

  • CollapseAll()将折叠网格中的所有分组,使得所有数据项看不见而只显示分组。
  • ExpandAll()将展开所有分组,显示所有分组和它们的数据项。
  • ClearGroups()将移走所有分组,并只显示数据项。
  • CollapseIcon 和 ExtendIcon属性定义了 在分组中的+ 和 –号。如果它们设置为not,+ 和 –是不会绘制的。

当然,OutlookGrid也支持所有其它众所周知的DataGridView方法和属性。

绑定数据

现在我们已看到网格是如何为绑定数据工作的,我现在将简要解释网格是如何用非绑定数据构建的。网格可以像DataGridView一样地被构建,使用Columns.Add()和Rows.Add()方法。然而,在创建行时一个异常被抛出:每一个row必须是OutlookGridRow类型!用row的CreateCells()函数在每一行中填充单元格,并添加该行到网格的Rows集合(译注:此处一定需要结合程序来看):

 

 //首先清除然后先前的绑定(如果它们已被设置的话)
          outlookGrid1.BindData(null, null);

//创建列的头(译注:即网格的表头)
          outlookGrid1.Columns.Add("column1", "Id");
          outlookGrid1.Columns.Add("column2", "Name");
          etc...

// 然后创建行
// row 1:第1行:
          OutlookGridRow row = new OutlookGridRow();
          row.CreateCells(outlookGrid1, id1, name1, ...);
          outlookGrid1.Rows.Add(row);

// row 2:第2行:
          OutlookGridRow row = new OutlookGridRow();
          row.CreateCells(outlookGrid1, id2, name2, ...);
          outlookGrid1.Rows.Add(row);//etc...等等...       


因为我们没有潜在的数据源可以被用来排序该网格,排序必须基于网格自己的内容。这意味着在排序中,网格自身的数据项将需要被比较。用OutlookGridRowComparer对象来做这件事。这个比较对象只基于它们的字符串值比较列表中的数据项。然而,一个更容易的选择是使用可选的Sort()方法,只确定要排序的列和排序的方向(升序或降序):

 

// 设置被用来分组的列
          outlookGrid1.GroupTemplate.Column = outlookGrid1.Columns[e.ColumnIndex];

       

//然后选择两个排序方法之一
//选择1:容易的方法,用OutlookGridRowComparer对象
          outlookGrid1.Sort(new OutlookGridRowComparer(ColumnIndex, direction));

//选择2:更加容易的方法,确定用哪一列来排序
          outlookGrid1.Sort(outlookGrid1.Columns[ColumnIndex], direction);      


‘代码用法’的介绍到此结束。到目前为止,基本排序和分组功能工作的相当好,甚至对于更大的数据集也是这样;比如,Invoice示例包含了超过2000个记录,但仍然在我的电脑上执行得相当棒。(本文)给出的该代码是完全用C#编写,这一点也不坏!B-)

缺少的和未经测试的功能

因为DataGridView基控件包含众多方法、事件和属性,它们完全被OutlookGrid继承,我没有努力用OutlookGrid的实现来测试它们全部。这意味着:一旦您开始为其它超出本文叙述的功能特性而使用OutlookGrid,您将很有可能遇到bug或功能缺失。因为我已遇到其中的一些(问题),我将在下面列出我所知道的问题:

  • 很不幸,OutlookGrid不支持嵌套分组。这就是下一步要做的。
  • 改变网格的显示风格可能导致分组不能被100%正确地绘出。
  • 通常,Group(分组)的文本颜色被设置为黑色并且不能改变,您将需要改变OutlookGridRow类中的Paint()方法。
  • 我没有用VirtualMode(虚模式)测试网格。(译注:VirtualMode属性在 .NET Framework 2.0 版中是新增的。虚拟模式是为使用大型数据存储区而设计的。当 VirtualMode 属性为 true 时,可以创建一个包含大量行和列的 DataGridView,然后处理 CellValueNeeded 事件来填充单元格。虚拟模式要求实现基础数据缓存,以便基于用户的操作来处理 DataGridView 单元格的填充、编辑和删除。详见MSDN。)说实话,我不知道那个概念是如何工作的,因此我怀疑:一旦您开启了VirtualMode(译注:即将其设置为true)数据项还能被正确显示。
  • 绑定数据源不是直接绑定到DataGridView基控件。因此,数据绑定只为显示数据项工作。然而,一旦您编辑网格中的数据项,数据源将不会被更新。您将不得不自己动手实现更新。
  • 这也意味着网格中的新数据项将不会自动被添加到数据源。这也将不得不用手工实现。
  • 为Group的row重写了缺省行,不是所有普通行触发的事件分组的行都能触发。比如,分组行重写OnDoubleClick事件,以自动折叠或展开。不改变代码该行为是不能被触发的。


毫无疑问这里还有一些其它问题,请向我报告它们,那么我和其它开发者都将可以从中获益。也许,我会投入更多时间开发这个控件的更多功能。

设计和可扩展性

在这个部分,我要描述这个控件如何实现的更多细节,特别面向可能要在该控件上做一些编码的开发人员读者。我尝试用VS2005中某些东西弄成类似一个的UML图,但是好吧…这个图将必须做出来。

OutlookGrid

OutlookGrid是主要对象,它引用并控制了其它所有对象。除了像Columns(译注:此为DataGridView的属性成员之一)属性、方法、成员和集合它继承自DataGridView之外,例如,还有下面三个属性成员是特别有趣的:

  • RowTemplate属性,和与之相关的:Rows(行)集合。
  • DataSource属性其被用于处理我们自己的DataSources(数据源)。
  • GroupTemplate属性是新加入的,它决定应该用到哪一个分组对象。

OutlookGrid只用OutlookGridRow对象工作。因此,RowTemplate属性已被重写为新的,所以它不允许设置一个新的RowTemplate对象。这意味着Rows集合将只包含OutlookGridRow对象。这是重要的,因为OutlookGridRow决定一个行在该控件上如何被绘制。除此之外,OutlookGrid也用DataSourceManager对象管理其自身的数据源。这里有一个令人感兴趣的现象是:DataSourceManager也可以用OutlookGrid作为一个数据源!在用非绑定数据工作时这特别有用。然而,对于用户,这是透明的。

GroupTemplate,如前面例子中所示,主要确定Groups在排序操作中是如何被创建。用克隆GroupTemplate对象来动态创建新的分组。因此,在排序前改变分组模版的属性将导致所有分组克隆这些属性。

OutlookGridRow

OutlookGridRow用两个新的属性进行了扩展:

  • IsGroupRow确定这个行是作为分组还是一个一般行来绘出。
  • Group确定了该行所属的分组。


因此,这意味着行(row)不是作为一个Group-row(分组行)显示展开/折叠图标以及分组文本,就是作为一个普通行只通过调用基类来绘制自己。为了恰当地这样做,两个方法需要被重写:Paint和PaintCells方法。一个附加方法:PaintHeader可以被重写,它决定了行的头如何被绘出。(译注:PaintHeader是此方法在 .NET Framework 2.0 版中是新增的。本文实现代码中没有重写。)

因为每一行是被放在一个分组中并且将获得一个引用到分组,每一行也可以自行决定是否它应该被绘出。比如,如果这个分组被折叠,行就不应该绘出。为了让基控件认为:这个数据项被设置为不可见,而不是设置Row.Visible属性为false,我们需要重写GetState方法。不知何故,设置一行的Visible属性触发了所有种类的事件并启动了基控件重绘。并且,一旦它的分组被再次标识为扩展,被设置为不可见的行将不能被绘出!为了解决这个问题,我们重写了GetState方法更换之。GetState方法将标识该行为只读, 而不是为了显示,然而该行(Row)的Visible属性将仍是true!这将使得基控件保持设法绘出该行。这正是我们需要支持的折叠和展开功能的行为。

Group对象

IOutlookGridRow是必须由使用这个控件的所有Group类所必须实现的接口。缺省情况下,该控件将利用OutlookGridDefaultGroup。Group类的实现并不是很难理解。Clone和CompareTo函数虽然是由每个Group类正确地实现但是很重要,并与在排序网格时的Comparer对象的行为相适应。

具有代表性的,CompareTo函数将行的值与分组中的值进行比较。Text属性确定了什么文本最终会被显示在屏幕上。因此,如果您,比如,对一个DateTime特征属性进行排序和分组,这是相当容易来解释,例如,月份的名称,代替分组中DateTime的值,乃至好想象的人:像Outlook,显示分组文本如‘Date: Yesterday'', ''Date: Last week'',''Date: Last month'' 等等。

DataSourceManager

如果您设法阅读所有关于这一点的文章,您已经获得了我的尊敬:-) 我猜,如果是那样的话,您一定如饥似渴地希望知道这个小故事的结尾;-)

我们现在来到这个控件最为复杂的部分。在我想要即Bound(绑定)数据源也支持Unbound(非绑定)数据源时,我遇到了麻烦。一旦数据被绑定到DataGridView基控件类,基控件的行为就变得非常难于影响,并且它有它自己的想法的感觉。举个例子,一旦它是数据绑定的,就不能添加行到网格,这真让人灰心。

我只看到一个解决的方法,这就是重写基控件的DataSource属性而不是简单地绑定数据到基控件本身。改为:我创建我自己的DataSourceManager类。我必须承认这让我陷入我所不愿的更大的麻烦中去。突然地,我不得不自己实现数据绑定了!倒不是我特别喜欢数据绑定,因为以我粗陋的看法,它基本上完全摧毁了任何层次(体系)结构(architectural layering)概念,使用多个逻辑层并将业务(层)从表现(层)中分离。但是OK,这是另一个话题。另一方面,一旦您只用很少的几行代码就可以排序、分组、并绘出整个DataSet(数据集),我必须承认它是相当令人胆寒的。

因此,如果您想要用如DataTable或DataTableView之类的附加数据源,您将不得不做一些编码。现在,DataSourceManager已被以一种粗糙的方法实现。基本上,它是一个索引器类,它编制数据源的列和行的索引(作为简单ArrayLists实现),以允许OutlookGrid快速访问当前的数据。比如,如果您用业务对象绑定一个ArrayList到OutlookGrid,其属性将被作为列索引(假若这样,用反射),并且每个对象将被映射到DataSourceManager中的一行。这也意味着当您排序OutlookGrid,实际上,只有DataSourceManager中索引行被排序。

所以,您可以看到DataSourceManager作为OutlookGrid和实际数据之间的一个抽象层(是的,我太喜欢分层了?)。这里有一个有趣的细节:将OutlookGrid自身作为一个数据源绑定到DataSourceManager是可能的。甚至在您的OutlookGrid被分组后,DataSourceManager将只索引那些没有分组的行。这使得先前插入网格的非绑定数据的排序和分组非常容易。用这个办法,我一石二鸟解决了所有绑定和非绑定数据问题(是的,是的,我太爱抽象类、分离业务和表现等等,等等)!

几点注意

我喜欢在这篇CodeProject文章中总结它们:我想方设法解决了一些问题。比如,继承于DataGridView控件的方法和事件,以及如何处理数据绑定。总而言之,鼓捣出这个控件并运行之对我来说很是一个挑战。正如前面所说,OutlookGrid控件远远没有完成,但它对于主要用途来说已完成:就是分组,它用起来极为好用!好了,小节一下,我希望您喜欢阅读这篇文章,而且它带给您一些精神食粮和更新的灵感!如果关于这个控件特定主题我没有讲清楚,或者如果您需要更多关于如何处理特定实现的具体信息,或者如果您有一些关于如何实现数据绑定的好点子,请在评论部分写下您的高见:-)