昨日、DataGriViewのパフォーマンスに関する記事がMSDNの日本語版に無いと言ってしまいましたが、有りました(〃゚д゚;A アセアセ・・・
というか、日本語版のMSDNと英語版のMSDNでは階層構造が違うのね。初めて知りました…これからは気を付けます(´・ω・`)
さて、DataGridView コントロールでの Just-In-Time データ読み込みによる仮想モードの実装を参考にしてサンプルを作ったのですが、基本的な考え方はWebシステム等で使うカスタムページングとデータのキャッシングをくっつけた様なものです。
登場する主なクラス/インターフェース(今回作るのも含めて)は
- DataGridView クラス
- IDataPageRetriever インターフェース (DataRetriever クラス)
- Cache クラス
です。
DataGridViewは仮想モードにしCellValueNeeded イベントをハンドリングします。IDataPageRetrieverは簡単に言うとカスタムページングを行うインターフェースです。Cache はIDataPageRetrieverから取得したページ(DataTable)をキャッシングします。ちなみに、データ自体はCacheオブジェクトが管理していることになるので、セルの情報はCache オブジェクトにアクセスして取得することになります。つまり
- CellValueNeeded イベントが発生
- Cacheオブジェクトからセルの情報を取得
- Cacheオブジェクトにセルの情報が無ければ、IDataPageRetrieverからページを取得
- 取得したページをキャッシングし、セルの情報を返却
という流れになります。
コード
やっていることは、DataGridViewに表示するであろう列を設定して、CellValueNeededイベントハンドらを記述しています。 MSDNとの違いは、テーブルのスキーマ情報を型付データテーブルから取得しているとこぐらいです。MSDNのサンプルみたいにDataAdapterを使用し、汎用的な実装をするとコーディング量が増えてしまうので、ちょっとズルしました。(列の型なども指定しません)
protected override void OnLoad(EventArgs e)
{
this.dataGridView.CellValueNeeded += new DataGridViewCellValueEventHandler(dataGridView_CellValueNeeded);
TestTableDataSet.TestTableDataTable table = new TestTableDataSet.TestTableDataTable();
for (int i = 0; i < table.Columns.Count; i++)
{
this.dataGridView.Columns.Add(table.Columns[i].ColumnName, table.Columns[i].ColumnName);
}
DataRetriever retriever = new DataRetriever();
cache = new Cache(retriever, 100);
dataGridView.RowCount = retriever.RowCount;
base.OnLoad(e);
}
void dataGridView_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
e.Value = cache.RetrieveElement(e.RowIndex, e.ColumnIndex);
}
- IDataPageRetriever インターフェース (DataRetriever クラス)
IDataPageRetriever インターフェースはカスタムページングとほとんど変わらない、要するにテーブルデータのサブセットを返却します。Cacheオブジェクトは必要となるセルの情報が無かった場合、IDataPageRetriver インターフェースを通じてDataTableを取得し、キャッシングします。
public class DataRetriever : IDataPageRetriever
{
private int rowCount = -1;
public int RowCount
{
get
{
if (rowCount == -1)
InitializeRowCount();
return rowCount;
}
}
private void InitializeRowCount()
{
using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["TestDBConnectionString"].ConnectionString))
{
SqlCommand command = con.CreateCommand();
command.CommandText = "SELECT Count(*) FROM TestTable"
con.Open();
rowCount = (int)command.ExecuteScalar();
}
}
private string columnToSortBy;
public DataTable SupplyPageOfData(int lowerPageBoundary, int rowsPerPage)
{
TestTableDataSetTableAdapters.TestTableTableAdapter adapter = new TestTableDataSetTableAdapters.TestTableTableAdapter();
TestTableDataSet.TestTableDataTable table = new ClassLibrary1.TestTableDataSet.TestTableDataTable();
if (columnToSortBy == null)
columnToSortBy = table.Columns[0].ColumnName;
adapter.FillSortedByColumn(table, columnToSortBy, rowsPerPage, lowerPageBoundary);
return table;
}
}
IDataRetrieverから取得したDataTableをキャッシングします。内部にDataPageという構造体を持っており、DataTableへの参照とDataTable内に保存している行の最小値と最大値を保持しています。ちなみに、サンプルではキャッシングするDataTableの数を二つとしています。
public class Cache
{
private static int RowPerPage;
public struct DataPage
{
public DataTable dataTable;
private int lowestIndex;
private int highestIndex;
public DataPage(DataTable table, int rowIndex)
{
dataTable = table;
lowestIndex = Map2LowerBoundary(rowIndex);
highestIndex = Map2HigherBoudnary(rowIndex);
}
public int LowestIndex
{
get { return lowestIndex; }
}
public int HighestIndex
{
get { return highestIndex; }
}
public static int Map2LowerBoundary(int rowIndex)
{
return (rowIndex / RowPerPage) * RowPerPage;
}
public static int Map2HigherBoudnary(int rowIndex)
{
return Map2LowerBoundary(rowIndex) + RowPerPage - 1;
}
}
public Cache(IDataPageRetriever dataSupplier, int rowPerPage)
{
Cache.RowPerPage = rowPerPage;
retriever = dataSupplier;
LoadFirstTwoPages();
}
private DataPage[] cachePages;
private IDataPageRetriever retriever;
private void LoadFirstTwoPages()
{
cachePages = new DataPage[] {
new DataPage(retriever.SupplyPageOfData(DataPage.Map2LowerBoundary(0),RowPerPage),0),
new DataPage(retriever.SupplyPageOfData(DataPage.Map2LowerBoundary(RowPerPage),RowPerPage),RowPerPage)};
}
public object RetrieveElement(int rowIndex, int columnIndex)
{
object element = null;
if (IfPageCached_ThenSetElement(rowIndex, columnIndex, ref element))
{
return element;
}
else
{
return RetrieveData_CacheIt_ThenReturnElement(rowIndex, columnIndex);
}
}
private bool IfPageCached_ThenSetElement(int rowIndex, int columnIndex, ref object element)
{
if (IsRowInCachedPage(0, rowIndex))
{
element = cachePages[0].dataTable.Rows[rowIndex % RowPerPage][columnIndex];
return true;
}
else if (IsRowInCachedPage(1, rowIndex))
{
element = cachePages[1].dataTable.Rows[rowIndex % RowPerPage][columnIndex];
return true;
}
return false;
}
private bool IsRowInCachedPage(int pageNumber, int rowIndex)
{
return cachePages[pageNumber].HighestIndex >= rowIndex && cachePages[pageNumber].LowestIndex <= rowIndex;
}
private object RetrieveData_CacheIt_ThenReturnElement(int rowIndex, int columnIndex)
{
DataTable table = retriever.SupplyPageOfData(DataPage.Map2LowerBoundary(rowIndex), RowPerPage);
cachePages[GetIndexToUnusedPage(rowIndex)] = new DataPage(table, rowIndex);
return RetrieveElement(rowIndex, columnIndex);
}
private int GetIndexToUnusedPage(int rowIndex)
{
// rowIndexから遠いほうのキャッシュデータを除くようにする。省略。
}
}
サンプルを実際に使ってみた感じ、やっぱスクロールバーを縦にグリグリ動かすと負荷が高くなります。あんまりやりすぎるとDBサーバの方が泣きそうになります(;´Д`A ```
仕事で使わざるを得なくなった場合は、スクロールバーの操作を少し抑制したりしないとダメかな?数万行程度なら裏で完全なデータを引っ張ってきて、ある時期にキャッシュを丸ごと完全なデータに差し替えるとか…
とにかく、あまり考えずに作ると使っている人間の気分次第でDBサーバの負荷が監視に引っかかりそう。そしてたまたま機嫌の悪かったユーザにボロクソに文句を言われる…((((;´・ω・`))))カクカクフルフル
くわばらくわばら