最近一個項目中,需要從數據庫查詢大量數據(上萬條)並在Winform里面用Datagridview展示,故而查找了相關資料並進行研究。
在.NET中有兩種思路實現這種分頁式datagridview: 一種是通過純客戶端進行數據分頁篩取展示來實現; 另一種是通過結合數據庫分頁查詢來實現.
1. 客戶端分頁篩選式DataGridView
在Web程序中,Asp.NET提供了Web的分頁式DataGridView控件,顯然web數據傳輸限制了通信數據量,html和js限制了如winform中的許多高級特性,這恐怕也是其不得不提供分頁的原因。然而在Form程序中並沒有這種實現,相反卻提供了許多讓人愛不釋手的特性,可唯獨缺乏了這種分頁機制。
為什么要分頁。可能有人會覺得數據量增大時,通信的代價遠大於較短的窗體構建過程,然而事實於此相反,當每行記錄字段較為復雜時(比如包括圖片,bool,字符串)時,datagridview構建的窗體cell將包含圖片、復選框、單選框、文本框,筆者實測上千條記錄就能導致3-5s以上的窗體構建過程,這段時間窗體主線程將die在這了。退一步講,就算記錄字段較為簡單,都是文本集合,然而上萬條記錄仍將導致數秒中的響應時間。可見大數量數據構建分頁的必要性。
[1]中展示了一個簡潔易行的datagridview分頁方案。其效果如下圖所示,其思路大概如下:通過對DataTable的行Rows以設定的每頁大小進行篩選來構成新的某頁數據的DataTable,並把其綁定到Bindingsource上來控制BindingNavigate上面的導航條和DataGridView的展現。其實現了上頁,下頁,首頁,尾頁的功能,當然也可以實現跳轉到某頁的功能(這並不復雜)。最后為了代碼的移植性和易用性,可以把兩個控件合一起做成一個控件。具體結果及代碼張貼如下:
1 // 1、定義幾個所需的公有成員: int pageSize = 0; //每頁顯示行數 int nMax = 0; //總記錄數 int pageCount = 0; //頁數=總記錄數/每頁顯示行數 int pageCurrent = 0; //當前頁號 int nCurrent = 0; //當前記錄行 DataSet ds = new DataSet(); DataTable dtInfo = new DataTable(); //2、在窗體載入事件中,從數據源讀取記錄到DataTable中: string strConn = "SERVER=127.0.0.1;DATABASE=NORTHWIND;UID=SA;PWD=ULTRATEL"; //數據庫連接字符串 SqlConnection conn = new SqlConnection(strConn); conn.Open(); string strSql = "SELECT * FROM CUSTOMERS"; SqlDataAdapter sda = new SqlDataAdapter(strSql,conn); sda.Fill(ds,"ds"); conn.Close(); dtInfo = ds.Tables[0]; InitDataSet(); //3、用當前頁面數據填充DataGridView private void InitDataSet() { pageSize = 20; //設置頁面行數 nMax = dtInfo.Rows.Count; pageCount=(nMax/pageSize); //計算出總頁數 if ((nMax % pageSize) > 0) pageCount++; pageCurrent = 1; //當前頁數從1開始 nCurrent = 0; //當前記錄數從0開始 LoadData(); } private void LoadData() { int nStartPos = 0; //當前頁面開始記錄行 int nEndPos = 0; //當前頁面結束記錄行 DataTable dtTemp = dtInfo.Clone(); //克隆DataTable結構框架 if (pageCurrent == pageCount) { nEndPos = nMax; } else { nEndPos = pageSize * pageCurrent; } nStartPos = nCurrent; lblPageCount.Text = pageCount.ToString(); txtCurrentPage.Text = Convert.ToString(pageCurrent); //從元數據源復制記錄行 for (int i = nStartPos; i < nEndPos; i++) { dtTemp.ImportRow(dtInfo.Rows[i]); nCurrent++; } bdsInfo.DataSource = dtTemp; bdnInfo.BindingSource = bdsInfo; dgvInfo.DataSource = bdsInfo; } // 4、菜單響應事件: private void bdnInfo_ItemClicked(object sender, ToolStripItemClickedEventArgs e) { if (e.ClickedItem.Text == "關閉") { this.Close(); } if (e.ClickedItem.Text == "上一頁") { pageCurrent--; if (pageCurrent <= 0) { MessageBox.Show("已經是第一頁,請點擊“下一頁”查看!"); return; } else { nCurrent = pageSize * (pageCurrent - 1); } LoadData(); } if (e.ClickedItem.Text == "下一頁") { pageCurrent++; if (pageCurrent > pageCount) { MessageBox.Show("已經是最后一頁,請點擊“上一頁”查看!"); return; } else { nCurrent=pageSize*(pageCurrent-1); } LoadData(); } }
2. 數據庫分頁查詢式Datagridview
解決了大量數據客戶端Datagridview顯示后,大大縮小了反應的時間。然而隨着數據量的增大,到上百萬條,通信數據量將大大提高,造成的數據讀取延時較為明顯。當然這還是要通過分頁解決,畢竟用戶根本不需要那么多數據同時展現,他也看不過來,那么僅有限的數據量是有必要的,太大的數據量相信大多數人會通過專門的查找選項(畢竟有用的數據有限)。這時,顯然仍通過客戶端篩取的方法是不可行的,數據庫分頁查詢勢在必行,需要將上下頁及跳轉的導航結合數據庫的查詢。
2.1 首先來看數據庫的分頁查詢
(1) [1]文中介紹了幾種適用的SQL Server分頁查詢的方法,思路主要有兩種。一種是通過降序和升序結合來查詢某段的數據;另一種是通過ROW_NUMER()函數來生成排序號,通過該排序號進行篩選數據。這兩種思想分別體現在文中第4種和第5種方案。正如作者推薦,第5種方案更可取,當數據字段沒有明顯排序規律,或者想進一步操作時,合適和排序號row_number機制更佳。針對該函數,可參考[4]中SQL2005四個排名函數(row_number、rank、dense_rank和ntile)的比較。需要注意的是,正如[4]中所言,row_number依賴於over子句的排序,當存在排序字段相同的記錄時,記錄的順序不定,所以多次分頁查詢,相同排序字段的記錄順序不定。
(2) [2]文中介紹了Winform中datagridview大數量查詢分頁顯示,微軟的解決辦法。代碼張貼如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Data.SqlClient; //Winform datagridview 大數量查詢分頁顯示 微軟的解決辦法 namespace WindowsApplication1 { public partial class Form1 : Form { // WinForm上的控件 Button prevBtn = new Button(); Button nextBtn = new Button(); Button firstBtn = new Button(); Button lastBtn = new Button(); static DataGrid myGrid = new DataGrid(); static Label pageLbl = new Label(); // 分頁的變量 static int pageSize = 4; // 每頁顯示多少 static int leftpageSiz; // 分頁余數 static int totalPages = 0; // 總共頁數 static int currentPage = 0; // 當前頁數. static string firstVisibleCustomer = ""; // First customer on page to determine location for move previous. static string lastVisibleCustomer = ""; // Last customer on page to determine location for move next. // DataSet to bind to DataGrid. static DataTable custTable; // Initialize connection to database and DataAdapter. static SqlConnection nwindConn = new SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind"); static SqlDataAdapter custDA = new SqlDataAdapter("", nwindConn); static SqlCommand selCmd = custDA.SelectCommand; public Form1() { InitializeComponent(); // Initialize controls and add to form. this.ClientSize = new Size(360, 274); this.Text = "NorthWind Data"; myGrid.Size = new System.Drawing.Size(729, 240); myGrid.Dock = System.Windows.Forms.DockStyle.Top; myGrid.AllowSorting = true; myGrid.CaptionText = "NorthWind Customers"; myGrid.ReadOnly = true; myGrid.AllowNavigation = false; myGrid.PreferredColumnWidth = 150; firstBtn.Text = "First"; firstBtn.Size = new Size(48, 24); firstBtn.Location = new Point(22, 240); firstBtn.Click += new EventHandler(First_OnClick); prevBtn.Text = "Prev"; prevBtn.Size = new Size(48, 24); prevBtn.Location = new Point(92, 240); prevBtn.Click += new EventHandler(Prev_OnClick); nextBtn.Text = "Next"; nextBtn.Size = new Size(48, 24); nextBtn.Location = new Point(160, 240); nextBtn.Click += new EventHandler(Next_OnClick); lastBtn.Text = "Last"; lastBtn.Size = new Size(48, 24); lastBtn.Location = new Point(230, 240); lastBtn.Click += new EventHandler(Last_OnClick); pageLbl.Text = "沒有記錄"; pageLbl.Size = new Size(130, 16); pageLbl.Location = new Point(300, 244); this.Controls.Add(myGrid); this.Controls.Add(prevBtn); this.Controls.Add(firstBtn); this.Controls.Add(nextBtn); this.Controls.Add(lastBtn); this.Controls.Add(pageLbl); // 獲取第一頁數據 GetData("Default"); DataView custDV = new DataView(custTable, "", "ID", DataViewRowState.CurrentRows); myGrid.SetDataBinding(custDV, ""); } public static void First_OnClick(object sender, EventArgs args) { GetData("First"); } public static void Prev_OnClick(object sender, EventArgs args) { GetData("Previous"); } public static void Next_OnClick(object sender, EventArgs args) { GetData("Next"); } public static void Last_OnClick(object sender, EventArgs args) { GetData("Last"); } private void Form1_Load(object sender, EventArgs e) { } public static void GetData(string direction) { // Create SQL statement to return a page of records. selCmd.Parameters.Clear(); switch (direction) { case "First": selCmd.CommandText = "SELECT TOP " + pageSize + " * FROM Customers "; break; case "Next": selCmd.CommandText = "SELECT TOP " + pageSize + " * FROM Customers " + "WHERE ID > @ID ORDER BY ID"; selCmd.Parameters.Add("@ID", SqlDbType.VarChar, 5).Value = lastVisibleCustomer; break; case "Previous": selCmd.CommandText = "SELECT TOP " + pageSize + " * FROM Customers " + "WHERE ID < @ID ORDER BY ID DESC"; selCmd.Parameters.Add("@ID", SqlDbType.VarChar, 5).Value = firstVisibleCustomer; break; case "Last": selCmd.CommandText = "SELECT TOP " + leftpageSiz + " * FROM Customers ORDER BY ID DESC"; break; default: selCmd.CommandText = "SELECT TOP " + pageSize + " * FROM Customers ORDER BY ID"; // Determine total pages. SqlCommand totCMD = new SqlCommand("SELECT Count(*) FROM Customers", nwindConn); nwindConn.Open(); int totalRecords = (int)totCMD.ExecuteScalar(); nwindConn.Close(); totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); if ((totalRecords % pageSize) == 0) { leftpageSiz = pageSize; } else { leftpageSiz = totalRecords % pageSize; } break; } // Fill a temporary table with query results. DataTable tmpTable = new DataTable("Customers"); int recordsAffected = custDA.Fill(tmpTable); // If table does not exist, create it. if (custTable == null) custTable = tmpTable.Clone(); // Refresh table if at least one record returned. if (recordsAffected > 0) { switch (direction) { case "First": currentPage = 1; break; case "Next": currentPage++; break; case "Previous": currentPage--; break; case "Last": currentPage = totalPages; break; default: currentPage = 1; break; } pageLbl.Text = "Page " + currentPage + " of " + totalPages; // Clear rows and add new results. custTable.Rows.Clear(); foreach (DataRow myRow in tmpTable.Rows) custTable.ImportRow(myRow); // Preserve first and last primary key values. DataRow[] ordRows = custTable.Select("", "ID ASC"); firstVisibleCustomer = ordRows[0][0].ToString(); lastVisibleCustomer = ordRows[custTable.Rows.Count - 1][0].ToString(); } } } }
顯而易見,這種方法效率極高,因為只對一個字段進行排序篩選。但同時,這也存在極大的缺點:
- 只能依據一個排序字段,如果有多個字段需要排序則無能為力
- 無法提供跳轉的功能,因為僅靠通過上次起始ID來進行上頁和下頁的查詢,但無法指定查詢哪一頁。
(3) 我喜歡的兩種: 正如(1)所言, [2]中的4方案和5方案是極佳選擇
2.2 DataGridView的實現
由前文可知,2.1中(2)的方法提供了一種簡單高效的分頁查詢方法, 但其缺點也顯而易見,不適宜更高級的查詢需求。同時將顯示指定SQL查詢字符串,無法構造通用的DataGridView控件,使得控件的可移植性受限。下文將介紹一種可行的方案(未完待續)。
參考文獻:
[1] DataGridView分頁功能的實現, http://www.cnblogs.com/kevin-top/archive/2010/01/05/1639448.html
[2] 高效的SQLSERVER分頁查詢, http://www.jb51.net/article/35213.htm
[3] Winform datagridview 大數量查詢分頁顯示 微軟的解決辦法,http://bbs.csdn.net/topics/320194542
[4]SQL2005四個排名函數(row_number、rank、dense_rank和ntile)的比較, ,http://www.cnblogs.com/xhyang110/archive/2009/10/27/1590448.html


