GridView でセルを結合する方法

概要

この記事では、 GridView (ASP.NET) のセルを結合して表示する方法について説明します。*1

似たような記事として DataGrid コントロールの同一列内のセルを結合するには?が@IT にありますが、こちらはポストバック時に問題が発生する場合があります。そこで、@IT とは違った方法でセルを結合します。

解説

基本的な方針は、結合したいセルを見えなくするだけです。そのため、結合するかどうかの条件にマッチしたセルに対して CSS を適用します。

詳細はソースコードを参照してください。コメントを多めに記述しているため、再利用しやすいと思います。

単純な使用例は、メインソースの example 要素をご覧ください。

ソースコード

結合時に適用する CSS
/* 結合されたセル用 */
.GridJoined
{
	display: none;
}
メインソース
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

/// <summary>
/// GridView に以下の設定を行う
///   ・ 同じ項目が連続する部分を非表示
/// </summary>
/// <remarks>
/// 非表示になったデータは、画面のレイアウトからは見えないが 
/// HTML のソースとしては出力されている。
/// 
/// サンプルは以下の通り。
/// GridView の結合は、 DataBind() が終了した後に行う。
/// 
/// ページングやソートの後にも結合処理が必要になるため、
/// 以下の部分は、メソッド (たとえば JoinGridView という名称) として
/// 用意すると使いやすい。
/// 
/// <example>
/// GridViewJoin joiner = new GridViewJoin(gridRowHeader, (int)Column.番号);
/// joiner.AddRelationToKey((int)Column.関連項目1);
/// joiner.AddRelationToKey((int)Column.関連項目2);
/// joiner.Apply();
/// </example>
/// </remarks>
public sealed class GridViewJoin
{
    #region 変数

    // 対象となる GridView
    private GridView gridView;

    // Key となる Column
    private int keyColumn;

    // Relation となる Column
    private List<int> relationColumns = new List<int>();

    #endregion  // 変数

    #region Public

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="gridView">対象となる GridView</param>
    /// <param name="keyColumn">Key となる Column</param>
    public GridViewJoin(GridView gridView, int keyColumn)
    {
        this.gridView = gridView;
        this.keyColumn = keyColumn;
    }

    /// <summary>
    /// Key に連動して非表示にする Column を設定する
    /// </summary>
    /// <param name="relationColumn">Key に連動して非表示にする Column</param>
    public void AddRelationToKey(int relationColumn)
    {
        // 事前条件のチェック
        Debug.Assert(keyColumn != relationColumn,
            "事前条件: Key となる Column と同じ Column を Add することはできない");
        Debug.Assert(!relationColumns.Contains(relationColumn),
            "事前条件: すでに Add している Column と同じ Column を Add することはできない");

        relationColumns.Add(relationColumn);
    }

    /// <summary>
    /// 全ての設定を適用
    /// </summary>
    public void Apply()
    {
        ApplyJoin();
    }

    #endregion  // Public

    #region Private

    /// <summary>
    /// GridView で同じ項目が連続する部分を非表示にする
    /// </summary>
    private void ApplyJoin()
    {
        int baseRow = 0;
        // 比較元の行番号

        while (baseRow < gridView.Rows.Count - 1)
        {
            TableCellCollection baseCollection = gridView.Rows[baseRow].Cells;
            int targetRow = baseRow + 1;  // 比較先の行番号

            while (targetRow < gridView.Rows.Count)
            {
                TableCellCollection targetCollection = gridView.Rows[targetRow].Cells;

                // キーとなるテキストが等しいか判定し、値が等しければ以降のセルを非表示にする
                if (GridViewUtil.GetText(baseCollection[keyColumn])
                    == GridViewUtil.GetText(targetCollection[keyColumn]))
                {
                    // キーとなる列を非表示にする
                    AddRowSpan(baseCollection[keyColumn]);
                    targetCollection[keyColumn].CssClass = GridViewUtil.JOINED_CSS_CLASS;

                    // キーに付随する列を非表示にする
                    foreach (int column in relationColumns)
                    {
                        AddRowSpan(baseCollection[column]);
                        targetCollection[column].CssClass = GridViewUtil.JOINED_CSS_CLASS;
                    }

                    targetRow += 1;
                }
                else
                {
                    // 値が同じではなかったため Break
                    break;
                }
            }

            // baseRow を次の行 (targetRow) にする
            baseRow = targetRow;
        }
    }

    /// <summary>
    /// Cell を結合する
    /// </summary>
    /// <param name="cell">結合対象の Cell</param>
    private void AddRowSpan(TableCell cell)
    {
        if (cell.RowSpan == 0)
        {
            cell.RowSpan = 2;    // 未結合の場合は初期値 2
        }
        else
        {
            cell.RowSpan += 1;   // 結合済みの場合は結合の値を増やす
        }
    }

    #endregion  // Private
}
ユーティリティクラス
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

/// <summary>
/// GridViewUtil の概要の説明です
/// </summary>
public static class GridViewUtil
{
    public static string JOINED_CSS_CLASS = "GridJoined";

    /// <summary>
    /// Grid 用の行比較ヘルパクラス
    /// </summary>
    public sealed class Compare
    {
        private GridViewRow prevGridRow;

        /// <summary>
        /// 前の行と今回指定した行が異なるかを比較する
        /// </summary>
        /// <param name="row">対象行</param>
        /// <param name="from">比較範囲の開始</param>
        /// <param name="to">比較範囲の終了</param>
        /// <returns>前の行と今回指定した行が異なれば true, そうでなければ false</returns>
        public bool IsNextRow(GridViewRow row, int from, int to)
        {
            bool isNext = false;

            if (prevGridRow == null)
            {
                isNext = true;
            }
            else
            {
                for (int columnNum = from; columnNum <= to; ++columnNum)
                {
                    if (prevGridRow.Cells[columnNum].Text != row.Cells[columnNum].Text)
                    {
                        isNext = true;
                        break;
                    }
                }
            }

            prevGridRow = row;

            return isNext;
        }
    }

    /// <summary>
    /// 行を取得する
    /// </summary>
    /// <param name="gridView">対象の GridView</param>
    /// <param name="rowNumber">行番号</param>
    /// <returns>セル</returns>
    public static TableRow GetRow(GridView gridView, int rowNumber)
    {
        return gridView.Rows[rowNumber];
    }
    
    /// <summary>
    /// セルを取得する
    /// </summary>
    /// <param name="gridView">対象の GridView</param>
    /// <param name="rowNumber">行番号</param>
    /// <param name="columnNumber">列番号</param>
    /// <returns>セル</returns>
    /// <remarks>非表示に設定されているセルを考慮する</remarks>
    public static TableCell GetCell(GridView gridView, int rowNumber, int columnNumber)
    {
        // CssClass が "joined" なら 1 行上のセルを参照する
        while (GetRow(gridView, rowNumber).Cells[columnNumber].CssClass == JOINED_CSS_CLASS)
        {
            rowNumber -= 1;
        }

        return GetRow(gridView, rowNumber).Cells[columnNumber];
    }

    /// <summary>
    /// セルに表示されているテキストを取り出す
    /// </summary>
    /// <param name="gridView">対象の GridView</param>
    /// <param name="rowNumber">行番号</param>
    /// <param name="columnNumber">列番号</param>
    /// <returns>セルに表示されているテキスト</returns>
    public static string GetText(GridView gridView, int rowNumber, int columnNumber)
    {
        // セルに表示されているテキストを取り出す
        return GetText(GetCell(gridView, rowNumber, columnNumber));
    }

    /// <summary>
    /// セルに表示されているテキストを取り出す
    /// </summary>
    /// <param name="cell">セル</param>
    /// <returns>セルに表示されているテキスト</returns>
    public static string GetText(TableCell cell)
    {
        foreach (Control control in cell.Controls)
        {
            // Control のテキストを返す
            if (control is TextBox)
            {
                return ((TextBox)control).Text;
            }
            else if (control is DropDownList)
            {
                return ((DropDownList)control).Text;
            }
            else if (control is CheckBox)
            {
                return ((CheckBox)control).Checked.ToString();
            }
            else if (!(control is LiteralControl || control is DataBoundLiteralControl))
            {
                throw new NotImplementedException();
                // TODO
                //return ((object)control).Text;
            }
        }

        // Control が見つからなかったため、 BoundColumn の テキストを返す
        return cell.Text;
    }

    /// <summary>
    /// GridView に列を追加する
    /// </summary>
    /// <param name="gridView">対象の GridView</param>
    /// <param name="dataField">DataField</param>
    /// <param name="headerText">HeaderText</param>
    public static void AddBoundColumn(GridView gridView,
        string dataField, string headerText)
    {
        BoundField field = new BoundField();

        field.DataField = dataField;
        field.HeaderText = headerText;

        gridView.Columns.Add(field);
    }

    /// <summary>
    /// GridView に列を追加する
    /// </summary>
    /// <param name="gridView">対象の GridView</param>
    /// <param name="itemTemplate">ITemplate</param>
    /// <param name="headerText">HeaderText</param>
    public static void AddTemplateColumn(GridView gridView,
        ITemplate itemTemplate, string headerText)
    {
        TemplateField field = new TemplateField();

        field.ItemTemplate = itemTemplate;
        field.HeaderText = headerText;

        gridView.Columns.Add(field);
    }

    /// <summary>
    /// TableCell に CSS を追加適用する
    /// </summary>
    /// <param name="cell">対象の TableCell</param>
    /// <param name="cssClassName">適用する CSS のクラス名</param>
    public static void AddCssClass(TableCell cell, string cssClassName)
    {
        string currentCssClassName = cell.CssClass;

        // 複数の CSS クラスを適用するため、
        // すでに適用されている CSS クラスがあった場合は、スペースで区切って追加する
        if (String.IsNullOrEmpty(currentCssClassName))
        {
            cell.CssClass = cssClassName;
        }
        else
        {
            cell.CssClass = currentCssClassName + " " + cssClassName;
        }
    }

    /// <summary>
    /// GridViewRow の指定した位置に CSS を追加適用する
    /// </summary>
    /// <param name="row">対象の GridViewRow</param>
    /// <param name="position">指定位置</param>
    /// <param name="cssClassName">適用する CSS のクラス名</param>
    public static void AddCssClass(GridViewRow row, int position, string cssClassName)
    {
        if (0 <= position && position < row.Cells.Count)
        {
            AddCssClass(row.Cells[position], cssClassName);
        }
    }

    /// <summary>
    /// GridViewRow の指定した位置に縦線を表示する
    /// </summary>
    /// <param name="row">対象の GridViewRow</param>
    /// <param name="position">指定位置</param>
    public static void AddBorder(GridViewRow row, int position)
    {
        // TODO CSS 名を変更する
        if (0 <= position && position < row.Cells.Count)
        {
            AddCssClass(row.Cells[position], "GridTestBorderLeft");
        }
        else if (position == row.Cells.Count)
        {
            AddCssClass(row.Cells[position - 1], "GridTestBorderRight");
        }
    }

    /// <summary>
    /// デバッグ用簡易出力ヘルパメソッド
    /// </summary>
    /// <param name="grid">出力対象の GridView</param>
    public static void OutputForDebug(GridView grid)
    {
        System.Diagnostics.Debug.WriteLine("--[begin GridView]-----");
        foreach (GridViewRow row in grid.Rows)
        {
            foreach (TableCell cell in row.Cells)
            {
                System.Diagnostics.Debug.Write(cell.Text + " | ");
            }
            System.Diagnostics.Debug.WriteLine("");
        }
        System.Diagnostics.Debug.WriteLine("--[end GridView]------");
    }
}

*1:DataGrid も同様の方法で可能です。