英語で読む

次の方法で共有


Windows Forms DataGridView コントロールでの Just-In-Time データ読み込みを使用した仮想モードの実装

DataGridView コントロールに仮想モードを実装する理由の 1 つは、必要なデータのみを取得することです。 これは "Just-In-Time データ読み込み" と呼ばれます。

たとえば、リモート データベースで非常に大きなテーブルを操作している場合は、表示に必要なデータのみを取得し、ユーザーが新しい行をスクロールして表示する場合にのみ追加のデータを取得することで、スタートアップの遅延を回避できます。 アプリケーションを実行しているクライアント コンピューターに、データの格納に使用できるメモリの量が限られている場合は、データベースから新しい値を取得するときに未使用のデータを破棄することもできます。

次のセクションでは、Just-In-Time キャッシュで DataGridView コントロールを使用する方法について説明します。

このトピックのコードを 1 つの一覧としてコピーするには、「方法: Windows フォーム DataGridView コントロールで Just-In-Time データ読み込みを使用して仮想モードを実装する」を参照してください。

フォーム

次のコード例では、CellValueNeeded イベント ハンドラーを介して Cache オブジェクトと対話する読み取り専用の DataGridView コントロールを含むフォームを定義します。 Cache オブジェクトは、ローカルに格納された値を管理し、DataRetriever オブジェクトを使用して、サンプル Northwind データベースの Orders テーブルから値を取得します。 Cache クラスに必要な IDataPageRetriever インターフェイスを実装する DataRetriever オブジェクトは、DataGridView コントロールの行と列の初期化にも使用されます。

IDataPageRetrieverDataRetriever、および Cache の種類については、このトピックで後述します。

注意

接続文字列内にパスワードなどの機密情報を格納すると、アプリケーションのセキュリティに影響する可能性があります。 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 クラスによって実装される IDataPageRetriever インターフェイスを定義します。 このインターフェイスで宣言されている唯一のメソッドは、最初の行インデックスとデータの 1 ページの行数を必要とする SupplyPageOfData メソッドです。 これらの値は、データ ソースからデータのサブセットを取得するために実装者によって使用されます。

Cache オブジェクトは、構築時にこのインターフェイスの実装を使用して、2 つの初期ページのデータを読み込みます。 キャッシュされていない値が必要な場合、キャッシュはこれらのページの 1 つを破棄し、IDataPageRetrieverから値を含む新しいページを要求します。

public interface IDataPageRetriever
{
    DataTable SupplyPageOfData(int lowerPageBoundary, int rowsPerPage);
}

DataRetriever クラス

次のコード例では、サーバーからデータのページを取得する IDataPageRetriever インターフェイスを実装する DataRetriever クラスを定義します。 DataRetriever クラスには Columns プロパティと RowCount プロパティも用意されています。このプロパティは、DataGridView コントロールが必要な列を作成し、Rows コレクションに適切な数の空の行を追加するために使用します。 空の行を追加して、テーブル内のすべてのデータが含まれているかのようにコントロールが動作するようにする必要があります。 つまり、スクロール バーのスクロール ボックスには適切なサイズが設定され、ユーザーはテーブル内の任意の行にアクセスできます。 スクロールして行が表示される場合にのみ、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.
        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;
    }
}

Cache クラス

次のコード例では、Cache クラスを定義します。このクラスは、IDataPageRetriever 実装を通じて設定された 2 ページのデータを管理します。 Cache クラスは、1 つのキャッシュ ページに値を格納する DataTable を含み、ページの上下の境界を表す行インデックスを計算する内部 DataPage 構造体を定義します。

Cache クラスは、構築時に 2 ページのデータを読み込みます。 CellValueNeeded イベントが値を要求するたびに、Cache オブジェクトは値が 2 つのページのいずれかで使用可能かどうかを判断し、その場合は値を返します。 値がローカルで使用できない場合、Cache オブジェクトは、現在表示されている行から最も離れている 2 つのページを決定し、ページを要求された値を含む新しいページに置き換えて返します。

データ ページ内の行数が一度に画面に表示できる行数と同じであると仮定すると、このモデルでは、ユーザーがテーブルをページングして、最近表示されたページに効率的に戻ることができます。

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;
    }
}

その他の考慮事項

前のコード例は、Just-In-Time データ読み込みのデモンストレーションとして提供されています。 最大限の効率を実現するには、独自のニーズに合わせてコードを変更する必要があります。 少なくとも、キャッシュ内のデータのページあたりの行数に適した値を選択する必要があります。 この値は、Cache コンストラクターに渡されます。 1 ページあたりの行数は、DataGridView コントロールに同時に表示できる行の数を下回らてはなりません。

最良の結果を得るには、パフォーマンス テストとユーザビリティ テストを実施して、システムとユーザーの要件を判断する必要があります。 考慮する必要がある要素には、アプリケーションを実行しているクライアント コンピューターのメモリ量、使用されるネットワーク接続の使用可能な帯域幅、使用されるサーバーの待機時間などがあります。 帯域幅と待機時間は、ピーク時に決定する必要があります。

アプリケーションのスクロール パフォーマンスを向上させるために、ローカルに格納されるデータの量を増やすことができます。 ただし、起動時間を短縮するには、最初に読み込むデータが多くなりすぎないようにする必要があります。 Cache クラスを変更して、格納できるデータ ページの数を増やすことができます。 より多くのデータ ページを使用すると、スクロール効率が向上しますが、使用可能な帯域幅とサーバーの待機時間に応じて、データ ページ内の理想的な行数を決定する必要があります。 ページが小さい場合、サーバーへのアクセス頻度は高くなりますが、要求されたデータを返す時間は短くなります。 レイテンシが帯域幅よりも問題である場合は、より大きなデータページの使用を検討してください。

関連項目


その他のリソース