实现虚拟模式的即时数据加载Windows窗体DataGridView控件
如果你正在与一个非常大的表在一个远程数据库,例如,您可能希望避免启动延迟,只检索所需的数据显示和检索额外的数据只有当用户新行滚动到视图。 如果客户端电脑运行您的应用程序有一个有限的可用内存来存储数据,您可能还想丢弃未使用的数据从数据库中检索新值。
下面的部分描述了如何使用 DataGridView 控制与实时缓存。
复制这个主题作为一个清单中的代码,看看 如何:实现虚拟模式的即时数据加载Windows窗体DataGridView控制 。
以下代码示例定义了一个包含一个只读形式 DataGridView 与交互的控制 缓存 对象通过一个 CellValueNeeded 事件处理程序。 的 缓存 对象管理和使用本地存储的值 DataRetriever 从订单表对象检索值的样本Northwind数据库。 的 DataRetriever 对象,它实现了 IDataPageRetriever 所需的接口 缓存 类,还用于初始化 DataGridView 控制行和列。
的 IDataPageRetriever , DataRetriever , 缓存 类型被描述在这个话题。
请注意 |
---|
存储敏感信息,如密码,连接字符串内会影响应用程序的安全。 使用Windows身份验证(也称为集成安全)是一种更安全的方法来控制访问数据库。 有关更多信息,请参见 保护连接信息 。 |
using System; using System.Data; using System.Data.SqlClient; using System.Drawing; using System.Windows.Forms; public class VirtualJustInTimeDemo : System.Windows.Forms.Form { private DataGridView dataGridView1 = new DataGridView(); private Cache memoryCache; // Specify a connection string. Replace the given value with a // valid connection string for a Northwind SQL Server sample // database accessible to your system. private string connectionString = "Initial Catalog=NorthWind;Data Source=localhost;" + "Integrated Security=SSPI;Persist Security Info=False"; private string table = "Orders"; protected override void OnLoad(EventArgs e) { // Initialize the form. this.AutoSize = true; this.Controls.Add(this.dataGridView1); this.Text = "DataGridView virtual-mode just-in-time demo"; // Complete the initialization of the DataGridView. this.dataGridView1.Size = new Size(800, 250); this.dataGridView1.Dock = DockStyle.Fill; this.dataGridView1.VirtualMode = true; this.dataGridView1.ReadOnly = true; this.dataGridView1.AllowUserToAddRows = false; this.dataGridView1.AllowUserToOrderColumns = false; this.dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect; this.dataGridView1.CellValueNeeded += new DataGridViewCellValueEventHandler(dataGridView1_CellValueNeeded); // Create a DataRetriever and use it to create a Cache object // and to initialize the DataGridView columns and rows. try { DataRetriever retriever = new DataRetriever(connectionString, table); memoryCache = new Cache(retriever, 16); foreach (DataColumn column in retriever.Columns) { dataGridView1.Columns.Add( column.ColumnName, column.ColumnName); } this.dataGridView1.RowCount = retriever.RowCount; } catch (SqlException) { MessageBox.Show("Connection could not be established. " + "Verify that the connection string is valid."); Application.Exit(); } // Adjust the column widths based on the displayed values. this.dataGridView1.AutoResizeColumns( DataGridViewAutoSizeColumnsMode.DisplayedCells); base.OnLoad(e); } private void dataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) { e.Value = memoryCache.RetrieveElement(e.RowIndex, e.ColumnIndex); } [STAThreadAttribute()] public static void Main() { Application.Run(new VirtualJustInTimeDemo()); } }
下面的代码示例定义了 IDataPageRetriever 接口,它的实现 DataRetriever 类。 这个接口中声明是唯一的方法 SupplyPageOfData 方法,它需要一个初始的行数的行索引和计数在一个页面的数据。 这些值使用的实现者从数据源检索数据的一个子集。
一个 缓存 对象使用该接口的一个实现施工加载两个初始页面的数据。 每当一个未值是必要的,缓存丢弃其中一个页面,请求一个新的页面包含的值 IDataPageRetriever 。
public interface IDataPageRetriever { DataTable SupplyPageOfData(int lowerPageBoundary, int rowsPerPage); }
下面的代码示例定义了 DataRetriever 类,它实现了 IDataPageRetriever 从服务器的数据接口来检索页面。 的 DataRetriever 类还提供了 列 和 rowcount 属性, DataGridView 控制用于创建必要的列并添加适当的空行数 行 收集。 添加空行是必要的,以便控制行为好像包含所有表中的数据。 这意味着在滚动条滚动框会有适当的大小,和用户将能够访问任何表中的行。 行是满的 CellValueNeeded 事件处理程序只有当他们滚动到视图。
public class DataRetriever : IDataPageRetriever { private string tableName; private SqlCommand command; public DataRetriever(string connectionString, string tableName) { SqlConnection connection = new SqlConnection(connectionString); connection.Open(); command = connection.CreateCommand(); this.tableName = tableName; } private int rowCountValue = -1; public int RowCount { get { // Return the existing value if it has already been determined. if (rowCountValue != -1) { return rowCountValue; } // Retrieve the row count from the database. command.CommandText = "SELECT COUNT(*) FROM " + tableName; rowCountValue = (int)command.ExecuteScalar(); return rowCountValue; } } private DataColumnCollection columnsValue; public DataColumnCollection Columns { get { // Return the existing value if it has already been determined. if (columnsValue != null) { return columnsValue; } // Retrieve the column information from the database. command.CommandText = "SELECT * FROM " + tableName; SqlDataAdapter adapter = new SqlDataAdapter(); adapter.SelectCommand = command; DataTable table = new DataTable(); table.Locale = System.Globalization.CultureInfo.InvariantCulture; adapter.FillSchema(table, SchemaType.Source); columnsValue = table.Columns; return columnsValue; } } private string commaSeparatedListOfColumnNamesValue = null; private string CommaSeparatedListOfColumnNames { get { // Return the existing value if it has already been determined. if (commaSeparatedListOfColumnNamesValue != null) { return commaSeparatedListOfColumnNamesValue; } // Store a list of column names for use in the // SupplyPageOfData method. System.Text.StringBuilder commaSeparatedColumnNames = new System.Text.StringBuilder(); bool firstColumn = true; foreach (DataColumn column in Columns) { if (!firstColumn) { commaSeparatedColumnNames.Append(", "); } commaSeparatedColumnNames.Append(column.ColumnName); firstColumn = false; } commaSeparatedListOfColumnNamesValue = commaSeparatedColumnNames.ToString(); return commaSeparatedListOfColumnNamesValue; } } // Declare variables to be reused by the SupplyPageOfData method. private string columnToSortBy; private SqlDataAdapter adapter = new SqlDataAdapter(); public DataTable SupplyPageOfData(int lowerPageBoundary, int rowsPerPage) { // Store the name of the ID column. This column must contain unique // values so the SQL below will work properly. if (columnToSortBy == null) { columnToSortBy = this.Columns[0].ColumnName; } if (!this.Columns[columnToSortBy].Unique) { throw new InvalidOperationException(String.Format( "Column {0} must contain unique values.", columnToSortBy)); } // Retrieve the specified number of rows from the database, starting // with the row specified by the lowerPageBoundary parameter. command.CommandText = "Select Top " + rowsPerPage + " " + CommaSeparatedListOfColumnNames + " From " + tableName + " WHERE " + columnToSortBy + " NOT IN (SELECT TOP " + lowerPageBoundary + " " + columnToSortBy + " From " + tableName + " Order By " + columnToSortBy + ") Order By " + columnToSortBy; adapter.SelectCommand = command; DataTable table = new DataTable(); table.Locale = System.Globalization.CultureInfo.InvariantCulture; adapter.Fill(table); return table; } }
下面的代码示例定义了 缓存 类,它管理通过一个两页的数据填充 IDataPageRetriever 实现。 的 缓存 类定义了一个内部 DataPage 结构,它包含一个 datatable 将值存储在一个缓存页面和计算的行索引代表页面的上下边界。
的 缓存 在施工时类加载两个页面的数据。 每当 CellValueNeeded 事件请求值, 缓存 对象确定是否可用的值是在一个两页的,如果是,返回它。 如果该值不是本地可用的 缓存 对象决定了它的两页是最远的来自当前显示的行和替换包含请求的页面,一个新的值,然后返回。
假设在数据页的行数的行数相同,可以显示在屏幕上,该模型允许用户通过表分页高效地返回到最近查看的页面。
public class Cache { private static int RowsPerPage; // Represents one page of data. public struct DataPage { public DataTable table; private int lowestIndexValue; private int highestIndexValue; public DataPage(DataTable table, int rowIndex) { this.table = table; lowestIndexValue = MapToLowerBoundary(rowIndex); highestIndexValue = MapToUpperBoundary(rowIndex); System.Diagnostics.Debug.Assert(lowestIndexValue >= 0); System.Diagnostics.Debug.Assert(highestIndexValue >= 0); } public int LowestIndex { get { return lowestIndexValue; } } public int HighestIndex { get { return highestIndexValue; } } public static int MapToLowerBoundary(int rowIndex) { // Return the lowest index of a page containing the given index. return (rowIndex / RowsPerPage) * RowsPerPage; } private static int MapToUpperBoundary(int rowIndex) { // Return the highest index of a page containing the given index. return MapToLowerBoundary(rowIndex) + RowsPerPage - 1; } } private DataPage[] cachePages; private IDataPageRetriever dataSupply; public Cache(IDataPageRetriever dataSupplier, int rowsPerPage) { dataSupply = dataSupplier; Cache.RowsPerPage = rowsPerPage; LoadFirstTwoPages(); } // Sets the value of the element parameter if the value is in the cache. private bool IfPageCached_ThenSetElement(int rowIndex, int columnIndex, ref string element) { if (IsRowCachedInPage(0, rowIndex)) { element = cachePages[0].table .Rows[rowIndex % RowsPerPage][columnIndex].ToString(); return true; } else if (IsRowCachedInPage(1, rowIndex)) { element = cachePages[1].table .Rows[rowIndex % RowsPerPage][columnIndex].ToString(); return true; } return false; } public string RetrieveElement(int rowIndex, int columnIndex) { string element = null; if (IfPageCached_ThenSetElement(rowIndex, columnIndex, ref element)) { return element; } else { return RetrieveData_CacheIt_ThenReturnElement( rowIndex, columnIndex); } } private void LoadFirstTwoPages() { cachePages = new DataPage[]{ new DataPage(dataSupply.SupplyPageOfData( DataPage.MapToLowerBoundary(0), RowsPerPage), 0), new DataPage(dataSupply.SupplyPageOfData( DataPage.MapToLowerBoundary(RowsPerPage), RowsPerPage), RowsPerPage)}; } private string RetrieveData_CacheIt_ThenReturnElement( int rowIndex, int columnIndex) { // Retrieve a page worth of data containing the requested value. DataTable table = dataSupply.SupplyPageOfData( DataPage.MapToLowerBoundary(rowIndex), RowsPerPage); // Replace the cached page furthest from the requested cell // with a new page containing the newly retrieved data. cachePages[GetIndexToUnusedPage(rowIndex)] = new DataPage(table, rowIndex); return RetrieveElement(rowIndex, columnIndex); } // Returns the index of the cached page most distant from the given index // and therefore least likely to be reused. private int GetIndexToUnusedPage(int rowIndex) { if (rowIndex > cachePages[0].HighestIndex && rowIndex > cachePages[1].HighestIndex) { int offsetFromPage0 = rowIndex - cachePages[0].HighestIndex; int offsetFromPage1 = rowIndex - cachePages[1].HighestIndex; if (offsetFromPage0 < offsetFromPage1) { return 1; } return 0; } else { int offsetFromPage0 = cachePages[0].LowestIndex - rowIndex; int offsetFromPage1 = cachePages[1].LowestIndex - rowIndex; if (offsetFromPage0 < offsetFromPage1) { return 1; } return 0; } } // Returns a value indicating whether the given row index is contained // in the given DataPage. private bool IsRowCachedInPage(int pageNumber, int rowIndex) { return rowIndex <= cachePages[pageNumber].HighestIndex && rowIndex >= cachePages[pageNumber].LowestIndex; } }
前面的代码示例提供示范的即时数据加载。 你需要为自己的需要修改代码实现最大效率。 至少,你需要选择一个适当的值为每页的行数的数据在缓存中。 这个值被传递到 缓存 构造函数。 每页的行数应不少于同时可以显示在你的行数 DataGridView 控制。
为达到最佳效果,你需要进行性能测试和可用性测试,以确定您的系统和用户的要求。 几个因素需要考虑包括在客户端机器的内存数量运行您的应用程序,使用的网络连接的可用带宽,服务器的延迟。 带宽和延迟高峰期应该决定使用。
提高滚动您的应用程序的性能,您可以增加本地存储的数据量。 改进启动时间,但是,你必须避免最初加载太多数据。 您可能想要修改 缓存 类增加数据页的数量可以存储。 使用更多的数据页可以提高滚动效率,但您将需要确定理想的数据页的行数,根据可用带宽和服务器延迟。 较小的页面,服务器将被更频繁地访问,但会花更少的时间返回所请求的数据。 如果延迟比带宽的问题,您可能需要使用更大的数据页。