ノンコーディングでの挿入をサポートする GridView

こんな感じで、ノンコーディングにできるように、GridView クラスを拡張してみた。TemplateField を使う場合、挿入行には EditItemTemplate が適用される。InsertItemTemplate を用意してやればそっちが適用される (デザイナでは表示されないけど)。

<my:InsertableGridView ID="InsertableGridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="PrimaryKey" DataSourceID="SqlDataSource1">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" ShowInsertButton="True" />
        <asp:BoundField DataField="PrimaryKey" HeaderText="PrimaryKey" InsertVisible="False" ReadOnly="True" SortExpression="PrimaryKey" />
        <asp:BoundField DataField="Column1" HeaderText="Column1" SortExpression="Column1" />
    </Columns>
</my:InsertableGridView>

<asp:SqlDataSource ID="SqlDataSource1" runat="server" 
  ConflictDetection="CompareAllValues" 
  OldValuesParameterFormatString="original_{0}" 
  ConnectionString="<%$ ConnectionStrings:SampleDBConnectionString %>" 
  DeleteCommand="DELETE FROM [Table1] WHERE [PrimaryKey] = @original_PrimaryKey AND (([Column1] = @original_Column1) OR ([Column1] IS NULL AND @original_Column1 IS NULL))" 
  InsertCommand="INSERT INTO [Table1] ([Column1]) VALUES (@Column1)" 
  SelectCommand="SELECT * FROM [Table1]" 
  UpdateCommand="UPDATE [Table1] SET [Column1] = @Column1 WHERE [PrimaryKey] = @original_PrimaryKey AND (([Column1] = @original_Column1) OR ([Column1] IS NULL AND @original_Column1 IS NULL))">
    <DeleteParameters>
        <asp:Parameter Name="original_PrimaryKey" Type="Int64" />
        <asp:Parameter Name="original_Column1" Type="String" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="Column1" Type="String" />
        <asp:Parameter Name="original_PrimaryKey" Type="Int64" />
        <asp:Parameter Name="original_Column1" Type="String" />
    </UpdateParameters>
    <InsertParameters>
        <asp:Parameter Name="Column1" Type="String" />
    </InsertParameters>
</asp:SqlDataSource>


拡張したクラスがこれ。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Reflection;
using System.Resources;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace MoroMoro.Web.UI.WebControls
{
    [ToolboxData("<{0}:InsertableGridView runat=\"server\"><Columns><asp:CommandField ShowInsertButton=\"true\" ShowDeleteButton=\"True\" ShowEditButton=\"True\"/></columns></{0}:InsertableGridView>")]
    public class InsertableGridView : GridView
    {
        #region Static Members

        private static readonly object EventRowInserting = new object();

        private static readonly object EventRowInserted = new object();

        private static string GetResourceStringBySystemDotWeb(string name)
        {
            ResourceManager resourceManager = new ResourceManager("System.Web", typeof(GridView).Assembly);
            return resourceManager.GetString(name);
        }

        #endregion

        #region Fields

        private IOrderedDictionary _insertValues;

        private GridViewRow _insertRow;

        private string _modelValidationGroup;

        #endregion

        #region Properties

        [DefaultValue(true)]
        public bool ShowInsertRow
        {
            get
            {
                object showInsertRow = this.ViewState["ShowInsertRow"];
                if (showInsertRow != null)
                {
                    return (bool)showInsertRow;
                }
                return true;
            }
            set
            {
                bool showInsertRow = this.ShowInsertRow;
                if (value != showInsertRow)
                {
                    this.ViewState["ShowInsertRow"] = value;
                    if (base.Initialized)
                    {
                        base.RequiresDataBinding = true;
                    }
                }
            }
        }

        private bool IsInsertMode
        {
            get
            {
                return ((bool?)this.ViewState["IsInsertMode"]).GetValueOrDefault(false);
            }
            set
            {
                this.ViewState["IsInsertMode"] = value;
            }
        }

        #endregion

        #region Events

        public event GridViewInsertEventHandler RowInserting
        {
            add
            {
                base.Events.AddHandler(EventRowInserting, value);
            }
            remove
            {
                base.Events.RemoveHandler(EventRowInserting, value);
            }
        }

        public event GridViewInsertedEventHandler RowInserted
        {
            add
            {
                base.Events.AddHandler(EventRowInserted, value);
            }
            remove
            {
                base.Events.RemoveHandler(EventRowInserted, value);
            }
        }

        #endregion

        #region Constructors

        public InsertableGridView()
            : base()
        {
            this._insertValues = null;
            this._insertRow = null;
            this._modelValidationGroup = null;
        }

        #endregion

        #region Methods

        protected override void OnRowCommand(GridViewCommandEventArgs e)
        {
            base.OnRowCommand(e);

            if (string.Equals(e.CommandName, "new", StringComparison.OrdinalIgnoreCase))
            {
                this.IsInsertMode = true;
                this.EditIndex = -1;
                this.RequiresDataBinding = true;
            }
            else if (string.Equals(e.CommandName, "insert", StringComparison.OrdinalIgnoreCase))
            {
                bool causesValidation = false;
                string validationGroup = string.Empty;
                IButtonControl commandSource = e.CommandSource as IButtonControl;
                if (commandSource != null)
                {
                    causesValidation = commandSource.CausesValidation;
                    validationGroup = commandSource.ValidationGroup;
                }

                this._modelValidationGroup = null;
                if (causesValidation)
                {
                    this.Page.Validate(validationGroup);
                    if (this.EnableModelValidation)
                    {
                        this._modelValidationGroup = validationGroup;
                    }
                }

                this.HandleInsert(causesValidation);
            }
            else
            {
                this.IsInsertMode = false;
            }
        }

        private void HandleInsert(bool causesValidation)
        {
            if ((!causesValidation || (this.Page == null)) || this.Page.IsValid)
            {
                DataSourceView data = null;
                if (this.IsBoundUsingDataSourceID)
                {
                    data = this.GetData();
                    if (data == null)
                    {
                        string dataSourceReturnedNullViewMessageFormat = 
                            InsertableGridView.GetResourceStringBySystemDotWeb("GridView_DataSourceReturnedNullView");
                        throw new HttpException(string.Format(dataSourceReturnedNullViewMessageFormat, this.ID));
                    }
                }
                GridViewInsertEventArgs e = new GridViewInsertEventArgs();
                if (this.IsBoundUsingDataSourceID)
                {
                    base.ExtractRowValues(e.Values, this._insertRow, true, true);
                }
                this.OnRowInserting(e);
                if (!e.Cancel && this.IsBoundUsingDataSourceID)
                {
                    this._insertValues = e.Values;
                    data.Insert(this._insertValues, new DataSourceViewOperationCallback(this.HandleInsertCallback));
                }
            }
        }

        private bool HandleInsertCallback(int affectedRows, Exception ex)
        {
            GridViewInsertedEventArgs e = new GridViewInsertedEventArgs(affectedRows, ex);
            e.SetValues(this._insertValues);
            this.OnRowInserted(e);
            this._insertValues = null;
            if ((ex != null) && !e.ExceptionHandled)
            {
                if (this.PageIsValidAfterModelException())
                {
                    return false;
                }
                e.KeepInInsertMode = true;
            }
            if (!e.KeepInInsertMode)
            {
                this.IsInsertMode = false;
                base.RequiresDataBinding = true;
            }
            return true;
        }

        protected virtual void OnRowInserting(GridViewInsertEventArgs e)
        {
            GridViewInsertEventHandler handler = (GridViewInsertEventHandler)base.Events[EventRowInserting];
            if (handler != null)
            {
                handler(this, e);
            }
            else if (!base.IsBoundUsingDataSourceID && !e.Cancel)
            {
                string unhandledEventMessageFormat = InsertableGridView.GetResourceStringBySystemDotWeb("GridView_UnhandledEvent");
                throw new HttpException(string.Format(unhandledEventMessageFormat, this.ID, "RowInserting"));
            }

        }

        protected virtual void OnRowInserted(GridViewInsertedEventArgs e)
        {
            GridViewInsertedEventHandler handler = (GridViewInsertedEventHandler)base.Events[EventRowInserted];
            if (handler != null)
            {
                handler(this, e);
            }
        }

        private bool PageIsValidAfterModelException()
        {
            if (this._modelValidationGroup == null)
            {
                return true;
            }
            this.Page.Validate(this._modelValidationGroup);
            return this.Page.IsValid;
        }


        protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
        {
            if (!this.ShowInsertRow)
            {
                return base.CreateChildControls(dataSource, dataBinding);
            }

            this.ValidateSettings();

            CommandFieldsManager commandFieldsManager = new CommandFieldsManager(CommandFieldsManager.ExtractCommandFields(this.Columns));
            commandFieldsManager.DisableInsertButton();
            int count = base.CreateChildControls(dataSource, dataBinding);

            if (count == 0)
            {
                this.BuildEmptyTable(dataBinding);
            }

            commandFieldsManager.EnableInsertButton();
            this.BuildInsertRow(dataBinding);
            commandFieldsManager.Reset();

            return count;
        }

        private void BuildEmptyTable(bool dataBinding)
        {
            DataControlField[] fields = new DataControlField[this.Columns.Count];
            this.Columns.CopyTo(fields, 0);

            Table tableControl = new Table();
            if (this.ShowHeader)
            {
                GridViewRow headerRow = this.CreateRow(-1, -1, DataControlRowType.Header, DataControlRowState.Normal);
                this.InitializeRow(headerRow, fields);
                GridViewRowEventArgs headerRowArgs = new GridViewRowEventArgs(headerRow);
                this.OnRowCreated(headerRowArgs);
                tableControl.Rows.Add(headerRow);
                if (dataBinding)
                {
                    this.OnRowDataBound(headerRowArgs);
                }
            }
            if (this.ShowFooter)
            {
                GridViewRow footerRow = this.CreateRow(-1, -1, DataControlRowType.Footer, DataControlRowState.Normal);
                this.InitializeRow(footerRow, fields);
                GridViewRowEventArgs footerRowArgs = new GridViewRowEventArgs(footerRow);
                this.OnRowCreated(footerRowArgs);
                tableControl.Rows.Add(footerRow);
                if (dataBinding)
                {
                    this.OnRowDataBound(footerRowArgs);
                }
            }
            this.Controls.Clear();
            this.Controls.Add(tableControl);
        }

        private void BuildInsertRow(bool dataBinding)
        {
            DataControlField[] fields = new DataControlField[this.Columns.Count];
            this.Columns.CopyTo(fields, 0);

            Table childTable = (Table)this.Controls[0];
            this._insertRow = this.CreateRow(-1, -1, DataControlRowType.DataRow, (this.IsInsertMode ? DataControlRowState.Insert : DataControlRowState.Normal));
            this.InitializeRow(this._insertRow, fields);

            GridViewRowEventArgs insertRowArgs = new GridViewRowEventArgs(this._insertRow);
            this.OnRowCreated(insertRowArgs);

            int insertRowIndex = this.GetInsertIndexOfInsertRow(childTable);
            childTable.Rows.AddAt(insertRowIndex, this._insertRow);

            if (dataBinding)
            {
                this.OnRowDataBound(insertRowArgs);
            }
        }

        protected override void InitializeRow(GridViewRow row, DataControlField[] fields)
        {
            base.InitializeRow(row, fields);

            if (row.RowState != DataControlRowState.Insert)
            {
                return;
            }

            for (int i = 0; i < fields.Length; i++)
            {
                DataControlField field = fields[i];

                if (!field.InsertVisible)
                {
                    row.Cells[i].Controls.Clear();
                }
            }
        }

        private int GetInsertIndexOfInsertRow(Table childTable)
        {
            bool visibleTopPager = (this.PagerSettings.Position == PagerPosition.Top) || (this.PagerSettings.Position == PagerPosition.TopAndBottom);
            bool scanedTopPager = false;

            int i = 0;
            while (i < childTable.Rows.Count)
            {
                GridViewRow row = (GridViewRow)childTable.Rows[i];
                bool isHeader = (row.RowType == DataControlRowType.Header);
                bool isDataRow = (row.RowType == DataControlRowType.DataRow);
                bool isSeparator = (row.RowType == DataControlRowType.Separator);
                bool isTopPager = (row.RowType == DataControlRowType.Pager) && visibleTopPager && !scanedTopPager;
                scanedTopPager = isTopPager ? true : scanedTopPager;

                if (!isHeader && !isDataRow && !isSeparator && !isTopPager)
                {
                    break;
                }

                i++;
            }
            return i;
        }

        private void ValidateSettings()
        {
            if (!this.ShowInsertRow)
            {
                return;
            }
            
            const string messageFormat = "ShowInsertRow プロパティが true の場合、{0} プロパティを true に設定することはできません。";
            if (this.AutoGenerateSelectButton)
            {
                throw new NotSupportedException(string.Format(messageFormat, "AutoGenerateSelectButton"));
            }
            if (this.AutoGenerateEditButton)
            {
                throw new NotSupportedException(string.Format(messageFormat, "AutoGenerateEditButton"));
            }
            if (this.AutoGenerateDeleteButton)
            {
                throw new NotSupportedException(string.Format(messageFormat, "AutoGenerateDeleteButton"));
            }
        }

        #endregion

        #region Inner Types

        private sealed class CommandFieldsManager
        {
            #region Static Methods

            public static List<CommandField> ExtractCommandFields(DataControlFieldCollection columns)
            {
                List<CommandField> commandFields = new List<CommandField>();
                foreach (DataControlField column in columns)
                {
                    CommandField commandField = column as CommandField;
                    if (commandField != null)
                    {
                        commandFields.Add(commandField);
                    }
                }
                return commandFields;
            }

            #endregion

            #region Fields

            private readonly IList<CommandFieldManager> _targets;

            #endregion

            #region Constructors

            public CommandFieldsManager(IEnumerable<CommandField> fields)
            {
                this._targets = new List<CommandFieldManager>();
                foreach (CommandField field in fields)
                {
                    this._targets.Add(new CommandFieldManager(field));
                }
            }

            #endregion

            #region Methods

            public void DisableInsertButton()
            {
                foreach (CommandFieldManager target in this._targets)
                {
                    target.DisableInsertButton();
                }
            }

            public void EnableInsertButton()
            {
                foreach (CommandFieldManager target in this._targets)
                {
                    target.EnableInsertButton();
                }
            }

            public void Reset()
            {
                foreach (CommandFieldManager target in this._targets)
                {
                    target.Reset();
                }
            }

            #endregion

        }

        private sealed class CommandFieldManager
        {
            #region Fields

            private readonly CommandField _target;
            private readonly bool _originalShowInsertButton;
            private readonly bool _originalShowCancelButton;
            private readonly bool _originalShowSelectButton;
            private readonly bool _originalShowEditButton;
            private readonly bool _originalShowDeleteButton;

            #endregion

            #region Constructors

            public CommandFieldManager(CommandField target)
            {
                this._target = target;
                this._originalShowInsertButton = target.ShowInsertButton;
                this._originalShowCancelButton = target.ShowCancelButton;
                this._originalShowSelectButton = target.ShowSelectButton;
                this._originalShowEditButton = target.ShowEditButton;
                this._originalShowDeleteButton = target.ShowDeleteButton;
            }

            #endregion

            #region Methods

            public void DisableInsertButton()
            {
                StateBag viewState = GetViewState();
                viewState["ShowInsertButton"] = false;
                viewState["ShowCancelButton"] = false;
                viewState["ShowSelectButton"] = this._originalShowSelectButton;
                viewState["ShowEditButton"] = this._originalShowEditButton;
                viewState["ShowDeleteButton"] = this._originalShowDeleteButton;
            }

            public void EnableInsertButton()
            {
                StateBag viewState = GetViewState();
                viewState["ShowInsertButton"] = this._originalShowInsertButton;
                viewState["ShowCancelButton"] = this._originalShowCancelButton;
                viewState["ShowSelectButton"] = false;
                viewState["ShowEditButton"] = false;
                viewState["ShowDeleteButton"] = false;
            }

            public void Reset()
            {
                StateBag viewState = GetViewState();
                viewState["ShowInsertButton"] = this._originalShowInsertButton;
                viewState["ShowCancelButton"] = this._originalShowCancelButton;
                viewState["ShowSelectButton"] = this._originalShowSelectButton;
                viewState["ShowEditButton"] = this._originalShowEditButton;
                viewState["ShowDeleteButton"] = this._originalShowDeleteButton;
            }

            private StateBag GetViewState()
            {
                PropertyInfo viewStateProperty = typeof(CommandField).GetProperty("ViewState", BindingFlags.NonPublic | BindingFlags.Instance);
                StateBag viewState = (StateBag)viewStateProperty.GetValue(this._target, null);
                return viewState;
            }

            #endregion
        }

        #endregion
    }

    public class GridViewInsertEventArgs : CancelEventArgs
    {
        private readonly OrderedDictionary _values;

        public IOrderedDictionary Values
        {
            get
            {
                return this._values;
            }
        }

        public GridViewInsertEventArgs()
        {
            this._values = new OrderedDictionary();
        }
    }

    public class GridViewInsertedEventArgs : EventArgs
    {
        private int _affectedRows;
        private Exception _exception;
        private bool _exceptionHandled;
        private IOrderedDictionary _values;
        private bool _keepInInsertMode;

        public int AffectedRows
        {
            get
            {
                return this._affectedRows;
            }
        }

        public Exception Exception
        {
            get
            {
                return this._exception;
            }
        }

        public bool ExceptionHandled
        {
            get
            {
                return this._exceptionHandled;
            }
            set
            {
                this._exceptionHandled = value;
            }
        }

        public IOrderedDictionary Values
        {
            get
            {
                return this._values;
            }
        }

        public bool KeepInInsertMode
        {
            get
            {
                return this._keepInInsertMode;
            }
            set
            {
                this._keepInInsertMode = value;
            }
        }

        public GridViewInsertedEventArgs(int affectedRows, Exception e)
        {
            this._affectedRows = affectedRows;
            this._exception = e;
            this._exceptionHandled = false;
            this._values = new OrderedDictionary();
            this._keepInInsertMode = false;
        }

        internal void SetValues(IOrderedDictionary values)
        {
            this._values = values;
        }
    }

    public delegate void GridViewInsertEventHandler(object sender, GridViewInsertEventArgs e);

    public delegate void GridViewInsertedEventHandler(object sender, GridViewInsertedEventArgs e);
}


それにしても、なんで GridView は挿入をサポートしてないんだろ。