Расширенный GridView с функцией вставки

ОГЛАВЛЕНИЕ

Данная статья описывает расширенный управляющий элемент ASP.NET GridView, добавляющий функцию вставки, применяемую аналогично существующим функциям редактирования и удаления. Также рассматривается внутренняя работа управляющего элемента GridView и указываются некоторые полезные методы для расширения.

•    Скачать исходники - 30.07 KB

Введение

DataGrid был плохим поскольку гордо заявлял, что делает все, что вы когда-либо хотели делать при отображении табличных данных. Но он вообще не делал хорошо то, что должен. Несмотря на то что он имел свойства AllowPaging и AllowSorting, приходилось вручную подключать листание и сортировку.

Затем появился ASP.NET 2 со своим новым передовым управляющим элементом GridView. В силу давнего отказа от DataGrid и ручного программирования быстрых Repeaters и DataLists были сомнения насчет испытания новейшего раздутого программного обеспечения. Но GridView оказался легким и полезным. Объединение GridView с ObjectDataSource позволяет производить листание, сортировку, поиск, редактирование и удаление без написания кода. И это не рекламное "отсутствие кода", на деле означающее "всего один-два метода", а настоящее серьезное " отсутствие кода ".

Единственным недостатком является отсутствие встроенной поддержки вставки новых записей через GridView. Это постоянно приходится делать на страницах ведения списков. Раньше способом было добавить пустую временную строку вверх GridView и разрешить пользователям вставлять через эту строку. Ниже показано, как это должно выглядеть.

Общий способ добиться этого заключается в следующем:

•    Получить данные
•    Изменить их путем вставки пустой записи в самое начало (т.е.: индекс 0)
•    Связать измененные данные с сеткой
•    Изменить размер страницы, так как первая страница должны показывать вставленную строку
•    Изменять командные кнопки по мере связывания строк

В силу вышесказанного страницы ведения списков требуют частого повторения лишней работы. Поэтому GridView был расширен до поддержки такой вставки. Был скопирован его принцип редактирования строки, поэтому его просто использовать и прямо поддерживать его отношение с источниками данных.

Задача была трудной, в основном из-за выяснения того, как GridView в действительности работает внутри, что было сделано благодаря замечательному инструменту дизассемблирования .NET Reflector.


Косметические улучшения

Сводка результатов

GridView часто применяется для отображения результатов поиска, а значит, в текстовые управляющие элементы всегда вставляются фразы типа "Результаты 1-10 из 50", которые надо всегда не забывать показывать и прятать, что скучно. Вот почему была добавлена "строка сводки", отображающая эту информацию автоматически. Сводка вставляет прямо над заголовком, но ее можно переместить в любое нужное вам место.

/// <span class="code-SummaryComment"><summary></span>
/// Отображает содержимое управляющего элемента.
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="writer"> <see cref="HtmlTextWriter"/> Для записи куда-либо.</param></span>
protected override void RenderContents(HtmlTextWriter writer)
{
    if (this.ShowResultSummary && this.PageCount != 0)
    {
        // Создать управляющие элементы сводки
        int firstResultIndex = this.PageIndex * this.PageSize;
        HtmlGenericControl topSummaryControl = new HtmlGenericControl("div");
        topSummaryControl.Style.Add("float", "left");
        topSummaryControl.InnerHtml = string.Format("<b>Records:</b> {0} to {1} of {2}",
            firstResultIndex + 1, firstResultIndex +
            this.Rows.Count, this.DataSourceCount);
        HtmlGenericControl bottomSummaryControl = new HtmlGenericControl("div");
        bottomSummaryControl.Style.Add("float", "left");
        bottomSummaryControl.InnerHtml = topSummaryControl.InnerHtml;

        if (this.PageCount == 1)
        {
            // Добавить сводку к таблице сверху
            this.Controls[0].Controls.AddAt(0, this.CreateSummaryRow(topSummaryControl));
            // Добавить сводку к таблице снизу
            this.Controls[0].Controls.Add(this.CreateSummaryRow(bottomSummaryControl));
        }
        else
        {
            // Добавить управляющий элемент сводки к верхнему блоку листания
            if (this.TopPagerRow != null)
                this.TopPagerRow.Cells[0].Controls.Add(topSummaryControl);
            // Добавить управляющий элемент сводки к нижнему блоку листания
            if (this.BottomPagerRow!= null)
                this.BottomPagerRow.Cells[0].Controls.Add(bottomSummaryControl);
        }
    }

    base.RenderContents(writer);
}

private TableRow CreateSummaryRow(Control summaryControl)
{
    TableRow summaryRow = new TableRow();
    TableCell summaryCell = new TableCell();
    summaryCell.ColumnSpan = this.HeaderRow.Cells.Count;
    summaryRow.Cells.Add(summaryCell);
    summaryCell.Controls.Add(summaryControl);
    return summaryRow;
}

private int _dataSourceCount;

/// <span class="code-SummaryComment"><summary></span>
/// Должна ли показываться сводка результатов.
/// <span class="code-SummaryComment"></summary></span>
[DefaultValue(false)]
[Category("Appearance")]
[Description("Whether the results summary should be shown.")]
public bool ShowResultSummary
{
    get
    {
        if (this.ViewState["ShowResultSummary"] == null)
            return false;
        else
            return (bool)this.ViewState["ShowResultSummary"];
    }
    set { this.ViewState["ShowResultSummary"] = value; }
}

/// <span class="code-SummaryComment"><summary></span>
/// Общее число строк в источнике данных.
/// <span class="code-SummaryComment"></summary></span>
public int DataSourceCount
{
    get
    {
        if (this.Rows.Count == 0)
            return 0;
        else if (this.AllowPaging)
            return this._dataSourceCount;
        else
            return this.Rows.Count;
    }
}

Пара свойств позволяет включать и выключать строку сводки по желанию и дает возможность добыть общее число записей в источнике данных, связанном с GridView, отсутствие которого всегда злило в прошлом. Значение берется из метода InitializePager (опущенного здесь, но имеющегося в демонстрационном проекте), являющегося крайне полезным методом, достойным отдельной статьи.

Индикаторы сортировки

Раньше отсутствие индикаторов сортировки столбцов мешало. Теперь два новых свойства позволяют установить изображения возрастания и убывания. Можно вставить заданные изображения в качестве веб-ресурсов и использовать их как стандартные. Изображения вставляются в соответствующий столбец в строке заголовка, когда она инициализируется методом InitializeRow.

/// <span class="code-SummaryComment"><summary></span>
/// Инициализирует строку в сетке.
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="row">Строка для инициализации.</param></span>
/// <span class="code-SummaryComment"><param name="fields">Поля, с которыми инициализируется строка.</param></span>
protected override void InitializeRow(GridViewRow row, DataControlField[] fields)
{
    base.InitializeRow(row, fields);

    if (row.RowType == DataControlRowType.Header && this.AscendingImageUrl != null)
    {
        for (int i = 0; i < fields.Length; i++)
        {
            if (this.SortExpression.Length > 0 && fields[i].SortExpression ==
                    this.SortExpression)
            {
                // Добавить индикатор сортировки
                Image sortIndicator = new Image();
                sortIndicator.ImageUrl =
                    this.SortDirection == SortDirection.Ascending ?
                    this.AscendingImageUrl : this.DescendingImageUrl;
                sortIndicator.Style.Add(HtmlTextWriterStyle.VerticalAlign, "TextTop");
                row.Cells[i].Controls.Add(sortIndicator);
                break;
            }
        }
    }
}

/// <span class="code-SummaryComment"><summary></span>
/// Изображение, отображаемое, если <span class="code-SummaryComment"><see cref="SortDirection"/> является возрастающий.</span>
/// <span class="code-SummaryComment"></summary></span>
[Editor(typeof(ImageUrlEditor), typeof(UITypeEditor))]
[Description("Image that is displayed when SortDirection is ascending.")]
[Category("Appearance")]
public string AscendingImageUrl
{
    get { return this.ViewState["AscendingImageUrl"] as string; }
    set { this.ViewState["AscendingImageUrl"] = value; }
}

/// <span class="code-SummaryComment"><summary></span>
/// Изображение, отображаемое, если <span class="code-SummaryComment"><see cref="SortDirection"/> является убывающий.</span>
/// <span class="code-SummaryComment"></summary></span>
[Editor(typeof(ImageUrlEditor), typeof(UITypeEditor))]
[Description("Image that is displayed when SortDirection is descending.")]
[Category("Appearance")]
public string DescendingImageUrl
{
    get { return this.ViewState["DescendingImageUrl"] as string; }
    set { this.ViewState["DescendingImageUrl"] = value; }
}

InitializeRow – еще один интересный метод, позволяющий выполнять дополнительные задачи при инициализации каждой строки. Можно представить его как внутренний OnRowCreated, но с большим доступом к способу создания ряда.


Функция вставки

При реализации этой функции хотелось поддерживать как можно больше существующих функций в каркасе, особенно при работе с источниками данных и привязке данных к сетке. Также хотелось, насколько это возможно, воспроизвести существующий интерфейс для сохранения единообразия, поэтому в первую очередь было введено два новых события, RowInserting и RowInserted, срабатывающие непосредственно перед и сразу после осуществления фактической вставки, как и с событиями RowUpdating и RowUpdated. Также были созданы два пользовательских класса EventArg, GridViewInsertEventArgs и GridViewInsertedEventArgs, чтобы сопровождать эти события, тоже следующие схеме обновления строки.

/// <span class="code-SummaryComment"><summary></span>
/// Срабатывает перед вставкой строки.
/// <span class="code-SummaryComment"></summary></span>
[Category("Action")]
[Description("Fires before a row is inserted.")]
public event EventHandler<GridViewInsertEventArgs> RowInserting;

/// <span class="code-SummaryComment"><summary></span>
/// Срабатывает после вставки строки.
/// <span class="code-SummaryComment"></summary></span>
[Category("Action")]
[Description("Fires after a row has been inserted.")]
public event EventHandler<GridViewInsertedEventArgs> RowInserted;

Было добавлено еще несколько свойств для придания максимальной гибкости сетке. AllowInserting позволяет пользователям включить или отключить функцию вставки полностью на периоды, когда сетка используется в режиме только чтения или только обновления. InsertRowActive контролирует состояние по умолчанию строки вставки и в случае true(истина) требует, чтобы пользователь нажал кнопку "новый" для переключения строки вставки в состояние редактирования.

При наличии этих свойств надо побеспокоиться о фактическом создании строки вставки. Ранее пустая строка добавлялась на первую страницу результатов, что разрушало коллекцию Rows и портило листание, поэтому был создан метод CreateChildControls, который ASP.NET вызывает при создании управляющего элемента на сервере и создает все дочерние управляющие элементы внутри сетки с учетом источника данных, настроек разбиения на страницы и тому подобного. Пришлось использовать пару вспомогательных методов, CreateRow и CreateColumns, чтобы создать строку вставки и ячейки внутри нее. При наличии строки надо было добавить ее к таблице сетки –  и готово.

Возникла одна сложность: при отсутствии строк по умолчанию сетка ничего не отображает, поэтому пришлось создавать пустую таблицу, если сетка пустая. Также было добавлено несколько дополнительных проверок в уже переопределенный метод InitializeRow, чтобы убедиться, что кнопка вставки появлялась только на строке вставки, и что в строке вставки не было ничего глупого, к примеру, кнопки удаления. Этот код опущен в статье для краткости.

/// <span class="code-SummaryComment"><summary></span>
/// Создает дочерние управляющие элементы управляющего элемента.
/// <span class="code-SummaryComment"></summary></span>
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
    int controlsCreated = base.CreateChildControls(dataSource, dataBinding);
    if (this.DisplayInsertRow)
    {
        ICollection cols = this.CreateColumns(null, false);
        DataControlField[] fields = new DataControlField[cols.Count];
        cols.CopyTo(fields, 0);
        if (this.Controls.Count == 0)
        {
            // Создать пустую таблицу для вставки первой записи
            Table tableControl = new Table();
            if (this.ShowHeader)
            {
                // Создать заголовок
                this._myHeaderRow = this.CreateRow(-1, -1, DataControlRowType.Header,
                    DataControlRowState.Normal);
                this.InitializeRow(this._myHeaderRow, fields);
                // Запустить события
                GridViewRowEventArgs headerRowArgs =
                    new GridViewRowEventArgs(this._myHeaderRow);
                this.OnRowCreated(headerRowArgs);
                tableControl.Rows.Add(this._myHeaderRow);
                if (dataBinding)
                    this.OnRowDataBound(headerRowArgs);
            }
            // Добавить строку вставки
            this.Controls.Add(tableControl);
        }
        else
            // Использовать сгенерированную строку заголовка
            this._myHeaderRow = null;

        // Создать ряд вставки
        this._insertRow = this.CreateRow(-1, -1, DataControlRowType.DataRow,
            this.InsertRowActive ? DataControlRowState.Insert :
                DataControlRowState.Normal);
        this._insertRow.ControlStyle.MergeWith(this.AlternatingRowStyle);
        this.InitializeRow(this._insertRow, fields);

        // Запустить события
        GridViewRowEventArgs insertRowArgs =
            new GridViewRowEventArgs(this._insertRow);
        this.OnRowCreated(insertRowArgs);

        // Добавить строку в верху таблицы, чуть ниже заголовка
        this.Controls[0].Controls.AddAt
            (this.Controls[0].Controls.IndexOf(this.HeaderRow) + 1, this._insertRow);
        if (dataBinding)
            this.OnRowDataBound(insertRowArgs);
    }
    return controlsCreated;
}

Еще не конец. Последняя часть задачи – код для фактического выполнения вставки. Это делается путем переопределения метода OnRowCommand действия в соответствии с событиями. Когда пользователь нажимает кнопку "Новый", надо отменить все редактирования, а при запуске редактирований показывается кнопка "Новый" – эти два играют роль переключателя, так что пользователь вставляет строку либо редактирует строку, но никогда одновременно  и то и другое. При нажатии пользователем кнопки "Вставить" значения извлекаются из строки вставки, и возбуждается событие RowInserting. Если сетка связана с источником данных – вызывается его метод вставки, чтобы без труда выполнился полный набор операций создания, чтения, обновления, удаления.

/// <span class="code-SummaryComment"><summary></span>
/// Возбуждает <span class="code-SummaryComment"><see cref="GridView.RowCommand"/> событие.</span>
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="e">Данные о событии.</param></span>
protected override void OnRowCommand(GridViewCommandEventArgs e)
{
    base.OnRowCommand(e);
    if (e.CommandName == "New")
    {
        this.InsertRowActive = true;
        this.EditIndex = -1;
        this.RequiresDataBinding = true;
    }
    else if (e.CommandName == "Edit")
        this.InsertRowActive = false;
    else if (e.CommandName == "Insert")
    {
        // Выполнить проверку правильности при необходимости
        bool doInsert = true;
        IButtonControl button = e.CommandSource as IButtonControl;
        if (button != null)
        {
            if (button.CausesValidation)
            {
                this.Page.Validate(button.ValidationGroup);
                doInsert = this.Page.IsValid;
            }
        }

        if (doInsert)
        {
            // Получить значения
            this._insertValues = new OrderedDictionary();
            this.ExtractRowValues(this._insertValues, this._insertRow, true, false);
            GridViewInsertEventArgs insertArgs =
                new GridViewInsertEventArgs(this._insertRow, this._insertValues);
            this.OnRowInserting(insertArgs);
            if (!insertArgs.Cancel && this.IsBoundUsingDataSourceID)
            {
                // Получить источник данных
                DataSourceView data = this.GetData();
                data.Insert(this._insertValues, this.HandleInsertCallback);
            }
        }
    }
}

private IOrderedDictionary _insertValues;

private bool HandleInsertCallback(int affectedRows, Exception ex)
{
    GridViewInsertedEventArgs e = new GridViewInsertedEventArgs(this._insertValues, ex);
    this.OnRowInserted(e);
    if (ex != null && !e.ExceptionHandled)
        return false;

    this.RequiresDataBinding = true;
    return true;
}

Весьма ловкий DataSourceView производит асинхронную вставку, так что если база данных медленная, остальная часть страницы получает возможность отобразиться во время ее выполнения. Как у большинства асинхронных операций, обратный вызов вызывает метод RowInserted и предоставляет такой же механизм обработки исключений, как и операции обновления и удаления.

Это завершает класс ExtendedGridView, который можно поместить на любую страницу и использовать так же, как GridView, и дает легкий способ использования сетки для хранения табличных данных. Если вы раньше применяли GridView для осуществления обновлений и удалений, то запросто используете для ExtendedGridView для выполнения вставок. К GridView применим такой же компромисс: если вы довольны базовыми функциями и везде используете BoundColumns, то можете делать все без написания кода, но если начинаете применять TemplateColumns для изменения поведения, то приходится чуть больше дорабатывать самостоятельно. Тем не менее, рассмотренный компонент экономит время и избавляет от проблем.

Интересные особенности

В ходе создания рассмотренного управляющего элемента было узнано много о внутренней работе управляющего элемента GridView. Классам, расширяющим GridView, предоставляется ряд интересных методов, в том числе InitializePager,InitializeRow, CreateRow и CreateColumns. Это прекрасный пример того, как расширение управляющего элемента экономит время при реализации одинаковых функций в нескольких местах.