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

ОГЛАВЛЕНИЕ

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

При реализации этой функции хотелось поддерживать как можно больше существующих функций в каркасе, особенно при работе с источниками данных и привязке данных к сетке. Также хотелось, насколько это возможно, воспроизвести существующий интерфейс для сохранения единообразия, поэтому в первую очередь было введено два новых события, 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. Это прекрасный пример того, как расширение управляющего элемента экономит время при реализации одинаковых функций в нескольких местах.