程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> OutlookGrid:以Outlook樣式分組和排列數據項

OutlookGrid:以Outlook樣式分組和排列數據項

編輯:關於C#

摘要: 一個允許分組和排列數據項的網格,很像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控件遠遠沒有完成,但它對於主要用途來說已完成:就是分組,它用起來極為好用!好了,小節一下,我希望您喜歡閱讀這篇文章,而且它帶給您一些精神食糧和更新的靈感!如果關於這個控件特定主題我沒有講清楚,或者如果您需要更多關於如何處理特定實現的具體信息,或者如果您有一些關於如何實現數據綁定的好點子,請在評論部分寫下您的高見:-)

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved