Fully editable grid Using The Repeater Control


Introduction:

 

The ASPX DataGridView control is normally what we'd used when we have tabular data that we want to display and edit. However, sometimes it isn't flexible enough to meet our requirements specifically. 

 

For example:

 

What if we want all rows in our table to be editable at the same time?  Since the DataGridView control only allows one row to be editable at a time, then maybe using the ASPX Repeater control would be a better choice. The Repeater is a more flexible control since it loops through the data one record at a time and leaves the formatting to the developer. You might argue that if we use the Repeater then we'll give up paging and sorting which the DataGridView control provides. This is true but these features are easy to add using custom code, as I'll show in my example.

 

The solution will cover the following:

  • Using a Web User control to encapsulate the display and edit/delete/insert of each data row.
  • Session caching the data in a DataSet to make it available to both the control and the main page.
  • Using the DataTable's "Select" method to sort the data.
  • Dynamically creating the Link control to produce the paging numbers above the Grid.
  • Highlighting each row that the mouse is over using a CSS style sheet.
  • Providing data validation for data entered using the ASPX "CompareValidator" control.

You can see this sample application run here:  http://www.mcsoftware.biz/EditableGrid/

Code For Article:


The file "FullyEditableGrid.zip" contains the web project for this article. It was created in Visual Web Developer 2005 and can be opened and run locally.

 

Code Review:

 

Main Page:

 

The main page's HTML is shown in listing 1. I used a Web User Control called "TableRow" for encapsulating the displaying, editing and inserting rows. One instance is located inside the Repeater and has its FormMode attribute set to "Edit". The other instance is located outside the Repeater and has its FormMode attribute set to "Insert".

 

 

 

Figure 1:

 

The public properties of the "TableRow" control are databound to the Repeater. The page links will display dynamically above the table as children controls of the "PageLinks" DIV. Notice the table's header row contains the header text for each of the table's column. These header text are contained in link buttons to allow each of the columns to be sorted. On the first click it will sort the data ascending; the second click it will sort decending. 

 

The "hover" CSS style property applied to the row header causes the row under the mouse to be highlighted.

 

Listing 1:

 

HTML code for main page:

 

<%@ Page Language="C#" Trace="true" AutoEventWireup="true" EnableViewState="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>

<%@ Register Src="TableRow.ascx" TagName="TableRow" TagPrefix="uc1" %>

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

 

<html xmlns="http://www.w3.org/1999/xhtml" >

<head runat="server">

    <title>Untitled Page</title>

    <style>

        table tr{background-color:#FEF9E7;}

        table thead tr{background-color:#64899A;}

        table tr:hover{background-color:#D1CCB7}

        table thead tr:hover{background-color:#64899A;}

       

        table thead td{text-align:center;color:White; font-weight:bold; font-size:16px}

        table thead A{text-align:center;color:White; font-weight:bold; font-size:16px}  

    </style>

</head>

<body style="">

    <form id="form1" runat="server" >

   

    <asp:Label ID="Label2" runat="server" Text="Rows Per Page:" Font-Bold="True" Width="109px">

    </asp:Label>&nbsp;

    <asp:DropDownList ID="DropDownListRowsPerPage" runat="server" Width = "63px" AutoPostBack="True" OnSelectedIndexChanged= "DropDownListRowsPerPage_SelectedIndexChanged">

    <asp:ListItem>3</asp:ListItem>

    <asp:ListItem>5</asp:ListItem>

    <asp:ListItem>10</asp:ListItem>

    <asp:ListItem>20</asp:ListItem>

    </asp:DropDownList>

    <br /><br />

   

    <div runat=server id="PageLinks">

        <asp:Label ID="Label1" runat="server" Text="Pages: " Width="90px">

        </asp:Label>

    </div>

 

     <table border="1" cellpadding="5" cellspacing="0" style=" height: 50px">

      <thead><tr>

        <td style="width: 100px; height: 48px;">&nbsp;</td>

        <td style="width: 100px; height: 48px;">

            <asp:LinkButton ID="LinkButtonName" runat="server" OnClick="LinkButtonSort_Click">Name

            </asp:LinkButton>

        </td>

 

        <td style="width: 100px; height: 48px;">

            <asp:LinkButton ID="LinkButtonPrice" runat="server" OnClick="LinkButtonSort_Click">Price

            </asp:LinkButton>

        </td>

 

        <td style="width: 100px; height: 48px;">

            <asp:LinkButton ID="LinkButtonDescription" runat="server"

OnClick= "LinkButtonSort_Click">Description

            </asp:LinkButton>

        </td>

 

        <td style="width: 100px; height: 48px;">In Stock

        </td>

     </tr>

   </thead>

   <asp:Repeater ID="DataList1" runat="server" EnableViewState=true   >

         

     <ItemTemplate>

       <uc1:TableRow id="DataRow1" runat="server" Name='<%# DataBinder.Eval(Container.DataItem, "Name") %>'

Description='<%# DataBinder.Eval(Container.DataItem, "Description") %>'

Price='<%# DataBinder.Eval(Container.DataItem, "Price") %>'

IsInstock='<%# DataBinder.Eval(Container.DataItem, "InStock") %>'

ProductID='<%# DataBinder.Eval(Container.DataItem, "ProductID") %>'

FormMode='Edit'>

      </uc1:TableRow></div>

    </ItemTemplate>

           

    </asp:Repeater>

    <uc1:TableRow id="DataRowInsert1" runat="server"  FormMode='Insert'>

   </uc1:TableRow>

  </table>   

 </form>

</body>

</html>

 

When we are deciding where to put our code in the code behind we need to always keep in mind the order the events will be called when the page loads. The "Page_Load" event will be called first for an initial page load or a post back. Then any event's posted by the client; for our example the "Click" event for the sorting. Next, the "PreRender" event for page and finally any "PreRender" events for any Web User Controls.

 

In the code behind's "Page_Load" event for the page we catch some of the values which are being persisted between page post, e.g. the "Page" the user is viewing and the "PageSize" (number of rows to display at a time). We also get the data that we will use for the application. For the example, it reads in a dataset saved to the disk. In a real application, we would fetch the data from the database. This example uses typed DataSets. A generic Dataset could of also been used. The dataset is put into a session object so that it does not need to be reloaded for each page post and so that it is readily availiable in our Web User Controls.

 

In the "PreRender" event we gather and sort the data that we will populate our data grid with and bind it to the Repeater control. In this event we also have a call to the private "createPages" method to dynamically create the page links.

 

Listing 2:

 

Code Behind for the main page:

 

public partial class _Default : System.Web.UI.Page

{

    int _intPage = 1;

    int _intPageSize=3;

    string _sSortString=string.Empty;

 

    DataSetProducts lDataSetProducts;

    protected void Page_Load(object sender, EventArgs e)

    {

      

        if(ViewState["Page"]!=null)

        {

            _intPage=(int)ViewState["Page"];

        }

 

        // allow the page size to be passed in the query string.

        if (Request["PageSize"] != null)

        {

            _intPageSize = int.Parse(Request["PageSize"]);

        }

 

        // to simplify the code we will used a cache dataset instead of a connection to a database.

        if (!IsPostBack || Session["DataSetProducts"]==null)

        {

           

            lDataSetProducts = new DataSetProducts();

            lDataSetProducts.ReadXml(@"C:\Documents and Settings\Mike\My Documents\Visual Studio 2005\WebSites\FullyEditableGrid\DataSet.xml");

            Session["DataSetProducts"] = lDataSetProducts;

        }

        else

        {

            lDataSetProducts = (DataSetProducts)Session["DataSetProducts"];

        }

 

        //Catch the page clicked

        if ((Request.Form["__EVENTTARGET"] != null))

        {

            string lsID = Request.Form["__EVENTTARGET"];

            if (lsID.Contains("LinkButtonPage"))

            {

                lsID=lsID.Replace("LinkButtonPage", string.Empty);

                _intPage=int.Parse(lsID);

            }

        }

       

        PreRender += new EventHandler(_Default_PreRender);

    }

 

    /// <summary>

    /// Sort the records in the table and gather one page of data to bind to the Repeater

    /// </summary>

    /// <param name="sender"></param>

    /// <param name="e"></param>

    void _Default_PreRender(object sender, EventArgs e)

    {

        createPages();

        DropDownListRowsPerPage.SelectedValue = _intPageSize.ToString();

       

        DataSetProducts lDataSetProducts = ((DataSetProducts)Session["DataSetProducts"]);

        DataSetProducts.ProductsDataTable lProductsDataTable = new DataSetProducts.ProductsDataTable();

        DataSetProducts.ProductsRow[] lProductsRows=  (DataSetProducts.ProductsRow[])  lDataSetProducts.Products.Select(string.Empty,_sSortString);

        List<DataSetProducts.ProductsRow> lListProductsRow = new List<DataSetProducts.ProductsRow>();

 

        //Gather one page of data

        int lintStart = _intPageSize * (_intPage-1) +1;

        int lintEnd=lintStart+_intPageSize-1;

        for (int lintPos = lintStart - 1; lintPos < lintEnd && lintPos < lProductsRows.Length; lintPos++)

        {

            lListProductsRow.Add(lProductsRows[lintPos]);           

        }

 

        // Bind the data

        DataList1.DataSource = lListProductsRow;

        DataList1.DataBind();

        ViewState["Page"] = _intPage;       

    }

 

    /// <summary>

    /// Dynamically create the page links at the top of the grid

    /// </summary>

    private void createPages()

    {

        int lintCount= lDataSetProducts.Products.Rows.Count;

        int lintPages = (int)Math.Ceiling((double)lintCount / _intPageSize);

        if (_intPage > lintPages)

        {

            _intPage = lintPages;

        }

 

        for (int lintCnt = 1; lintCnt < lintPages+1; lintCnt++)

        {

            LinkButton lLinkButton = new LinkButton();

            lLinkButton.ID = string.Format("LinkButtonPage{0}", lintCnt);

            lLinkButton.Text = lintCnt.ToString();

          

            Literal lLiteral = new Literal();

            lLiteral.Text = "&nbsp;";

            

            lLinkButton.EnableViewState = false;

         

            //disable the page link that we are currently viewing.

            if ( _intPage == lintCnt)

            {

                lLinkButton.Enabled = false;

            }          

            PageLinks.Controls.Add(lLinkButton);

            PageLinks.Controls.Add(lLiteral);               

        }

    }

 

   /// <summary>

   /// Sorting

   /// </summary>

   /// <param name="sender"></param>

   /// <param name="e"></param>

    protected void LinkButtonSort_Click(object sender, EventArgs e)

    {

        string lsLinkButtonText=((LinkButton)sender).Text;

        //set sort direction

        if (ViewState[lsLinkButtonText + "DESC"]==null)

        {

            ViewState[lsLinkButtonText + "DESC"] = false;

        }

        ViewState[lsLinkButtonText + "DESC"] = !(bool)ViewState[lsLinkButtonText + "DESC"];

        string lsSortDirection = string.Empty;

        if (!(bool)ViewState[lsLinkButtonText + "DESC"])

        {

            lsSortDirection = "DESC";

        }

        _sSortString = string.Format("{0} {1}", lsLinkButtonText, lsSortDirection);

    }

 

    protected void DropDownListRowsPerPage_SelectedIndexChanged(object sender, EventArgs e)

    {

        _intPageSize = int.Parse(DropDownListRowsPerPage.SelectedValue);

        ViewState["PageSize"] = _intPageSize;

             

        Response.Redirect(string.Format("{0}?PageSize={1}",Request.Url.LocalPath, _intPageSize));       

    }

}

 

TableRow Web User Control:

 

The TableRow control is used to encapulate the display and edit/delete/insert logic for the table. 

 

Listing 3:

 

The HTML code is fairly simple:

 

<%@ Control Language="C#" EnableViewState="true" AutoEventWireup="true" CodeFile="TableRow.ascx.cs" Inherits="TableRow" %>

  <tr>

    <td><asp:Button ID="ButtonInsert" runat="server" Text="Insert" Visible=false OnClick="ButtonInsert_Click" /><asp:Button ID="ButtonUpdate" runat="server" Text="Update" OnClick="ButtonUpdate_Click" />&nbsp; <asp:Button ID="ButtonDelete" runat="server" Text="Delete" OnClick="ButtonDelete_Click" />        

    </td>

 

    <td style="width: 100px; height: 48px;">

      <asp:TextBox ID="TextBoxName" runat="server"></asp:TextBox>

    </td>

 

    <td style="width: 100px; height: 48px;">

      <asp:TextBox ID="TextBoxPrice" runat="server">

      </asp:TextBox>

      <asp:CompareValidator ID="CompareValidator1" runat="server" ControlToValidate="TextBoxPrice" operator="DataTypeCheck" type="Currency" ErrorMessage="Price must be a valid currency." Display="Dynamic">

      </asp:CompareValidator>

    </td>

 

    <td style="width: 100px; height: 48px;">

      <asp:TextBox ID="TextBoxDescription" runat="server">

      </asp:TextBox>

    </td>

 

    <td style="width: 100px; height: 48px;">

      <asp:CheckBox ID="CheckBoxInStock" runat="server" />

    </td>

 

</tr>

 

We display the data using the ASPX TextBox and CheckBox controls. An ASPX "CompareValidator" control is used to validate that the price entered is numeric. Finally, these data are formatted using HTML <td> and <tr> tags. 

 

In the code behind for the control, we use public properites for the values we are passing as HTML attributes from the main page.

 

Listing 4: Public properties for the control

 

public partial class TableRow : System.Web.UI.UserControl

{

    private string _sProductID;

 

    public string ProductID

    {

        get

        {

            return _sProductID;

        }

        set

        {

            _sProductID = value;

            ViewState["ProductID"] = _sProductID;

        }

    }

    private string _sName;

    public string Name

    {

        get

        {

            return _sName;

        }

        set

        {

            _sName = value;

        }

    }

}

 

We load the Textboxes and CheckBox controls in the PreRender method of the control, see Listing 5.

 

Listing 5: PreRender Method

 

void DataRow_PreRender(object sender, EventArgs e)

{

    if (_FormMode == EFormMode.Insert)

    {

        ButtonDelete.Visible = false;

        ButtonUpdate.Visible = false;

        ButtonInsert.Visible = true;

 

        TextBoxName.Text = string.Empty;

        TextBoxPrice.Text = string.Empty;

        TextBoxDescription.Text = string.Empty;

        CheckBoxInStock.Checked =false;

    }

    else

    {

        TextBoxName.Text = Name;

        TextBoxPrice.Text = Price.ToString("###.00");

        TextBoxDescription.Text = Description;

        CheckBoxInStock.Checked = IsInStock;

    }

}

 

When the user changes the data in a row and clicks the "Update" method the "ButtonUpdate_Click" method called, listing 6. In the example, we update in the fields in the DataTable and save it DataSet to disk. In a real application we update these fields and then call the Update method on a dataset adapter to persist these changes in the database. The "ButtonDelete_Click" and "ButtonInsert_Click" methods  are similar and showing in listing 7 and listing 8 respectively.

 

Listing 6: ButtonUpdate_Click Method

 

protected void ButtonUpdate_Click(object sender, EventArgs e)

{

    DataSetProducts lDataSetProducts = ((DataSetProducts)Session["DataSetProducts"]);

    DataSetProducts.ProductsRow lProductsRow = lDataSetProducts.Products.FindByProductID(new Guid(ViewState["ProductID"].ToString()));

    lProductsRow.Name = TextBoxName.Text;

    lProductsRow.Description = TextBoxDescription.Text;

    lProductsRow.Price = Decimal.Parse(TextBoxPrice.Text);

    lProductsRow.InStock = CheckBoxInStock.Checked;

 

    Session["DataSetProducts"] = lDataSetProducts;

    //save the data to a file

    lDataSetProducts.WriteXml(@"C:\Documents and Settings\Mike\My Documents\Visual Studio 2005\WebSites\FullyEditableGrid\DataSet.xml");

 

}

 

Listing 7: ButtonDelete_Click Method

 

protected void ButtonDelete_Click(object sender, EventArgs e)

{

    DataSetProducts lDataSetProducts = ((DataSetProducts)Session["DataSetProducts"]);

    DataSetProducts.ProductsRow lProductsRow = lDataSetProducts.Products.FindByProductID(new Guid(ViewState["ProductID"].ToString()));

    lDataSetProducts.Products.RemoveProductsRow(lProductsRow);

    Session["DataSetProducts"] = lDataSetProducts;

    //save the data to a file

    lDataSetProducts.WriteXml(@"C:\Documents and Settings\Mike\My Documents\Visual Studio 2005\WebSites\FullyEditableGrid\DataSet.xml");

}

 

Listing 8: ButtonInsert_Click Method

 

protected void ButtonInsert_Click(object sender, EventArgs e)

{

    DataSetProducts lDataSetProducts = ((DataSetProducts)Session["DataSetProducts"]);

    DataSetProducts.ProductsRow lProductsRow = (DataSetProducts.ProductsRow)lDataSetProducts.Products.NewRow();

 

    lProductsRow.ProductID = Guid.NewGuid();

    lProductsRow.Name = TextBoxName.Text;

    lProductsRow.Description = TextBoxDescription.Text;

    lProductsRow.Price = Decimal.Parse(TextBoxPrice.Text);

    lProductsRow.SalePrice = Decimal.Parse(TextBoxPrice.Text);

    lProductsRow.InStock = CheckBoxInStock.Checked;

 

    lDataSetProducts.Products.AddProductsRow(lProductsRow);

 

    Session["DataSetProducts"] = lDataSetProducts;

    //save the data to a file

    lDataSetProducts.WriteXml(@"C:\Documents and Settings\Mike\My Documents\Visual Studio 2005\WebSites\FullyEditableGrid\DataSet.xml");

 

}

Conclusion:

 

Using a ASPX Repeater control to display and edit tabular data provides more flexibilty than ASPX DataGridView control. This example shows that features like sorting and paging are easily added. Use of User Web Controls help to encapusulate features and complexity making the overall code easier to maintain.