Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

In-place Editing with ASP.NET Repeater

0.00/5 (No votes)
29 Aug 2011 1  
A demonstration of how the ASP.NET Repeater control can be used for in-place editing and adding items

Introduction

Recently, someone had asked in the forums how to use an ASP.NET Repeater control for adding new items to the datasource it is bound to. Since words sometimes don't convey enough understanding, I put together this sample to show how the Repeater control can be used in this situation along with in-place editing.

This sample contains two versions; a server-side oriented approach, and a client-side oriented approach using AJAX.

Repeater vs. Gridview

When providing in-place editing and adding rows, the first thing that comes to mind is usually to make use of an ASP.NET GridView control. This article is not about the differences between the two controls nor when/how to choose one over the other, but a quick look at the differences shows this:

GridView Repeater
Table layout by default Uses templates
Has select/edit/delete commands Must be added manually
Built-in pager support Must be added manually
Column sorting Must be added manually

Although not an exhaustive list, it seems as though the GridView holds a distinct advantage by providing more built-in functionality. However, all of this functionality comes at a price. The GridView is a very "heavy" control and relies on extensive use of ViewState to work correctly and in some cases adds too much overhead to a page. That is when an ASP.NET Repeater control can have advantages.

Server-side Approach

In this first approach, I'll rely on the traditional server-side coding using PostBacks and databinding events to provide the functionality. I'll wrap the Repeater control in and ASP.NET UpdatePanel to reduce the page refreshing, but otherwise there are only a few lines of client-side code involved.

<asp:UpdatePanel runat="server">
    <ContentTemplate>
        <asp:Repeater runat="server" ID="Repeater1" 
        OnItemCommand="OnItemCommand" OnItemDataBound="OnItemDataBound">
            <HeaderTemplate>
                <table border="0" cellpadding="0" cellspacing="0">
                    <tr>
                        <th></th>
                        <th>First Name</th>
                        <th>Last Name</th>
                    </tr>
            </HeaderTemplate>
            <ItemTemplate>
                <tr>
                    <td>
                        <asp:ImageButton ID="Edit" 
                        ImageUrl="~/Images/EditDocument.png" 
                        runat="server" CommandName="edit" />
                        <asp:ImageButton ID="Delete" 
                        ImageUrl="~/Images/Delete_black_32x32.png" runat="server"
                            CommandName="delete" />
                    </td>
                    <td>
                        <asp:Label runat="server" 
                        ID="firstName"><%# Eval
                        ("FirstName") %></asp:Label>
                        <asp:PlaceHolder runat="server" 
                        ID="firstNameEditPlaceholder" />
                        <input type="hidden" runat="
                        server" id="firstNameHidden" />
                    </td>
                    <td>
                        <asp:Label runat="server" ID="
                        lastName"><%# Eval("LastName") %></asp:Label>
                        <asp:PlaceHolder runat="server" 
                        ID="lastNameEditPlaceholder" />
                        <input type="hidden" runat="
                        server" id="lastNameHidden" />
                    </td>
                </tr>
            </ItemTemplate>
            <FooterTemplate>
                <tr>
                    <td>
                        <asp:ImageButton ID="Delete" 
                        ImageUrl="~/Images/112_Plus_Blue_32x32_72.png" runat="server"
                            OnClick="OnAddRecord" />
                    </td>
                    <td><asp:TextBox runat="server" 
                    ID="NewFirstName" /></td>
                    <td><asp:TextBox runat="server" 
                    ID="NewLastName" /></td>
                </tr>
                </table>
            </FooterTemplate>
        </asp:Repeater>
    </ContentTemplate>
</asp:UpdatePanel>    

As can be seen, this is a very minimal example, just enough to demonstrate the techniques, actual usage will, of course, vary. A table layout is used for simplicity but any type of layout can be produced using the templates and CSS.

The two key events here are OnItemCommand and OnItemDataBound which will be covered in a moment.

Data Source

Since the control needs a datasource in order to bind to, I've created a very simple entity...

 public class Contact
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

...with a very simple Data Access Layer for this sample:

public class Data
{
    public Data()
    {
    }

    public int NextId
    {
        get
        {
            int id = 0;
            if(Contacts.Count != 0)
            {
                id = Contacts.Max(c => c.ID) + 1;
            }
            return id;
        }
    }

    public List<contact /> Contacts
    {
        get
        {
            if(HttpContext.Current.Session["contacts"] == null)
            {
                HttpContext.Current.Session["contacts"] = new List<contact />();
            }
            return HttpContext.Current.Session["contacts"] as List<contact />;
        }
    }
}

Of course, in a production system, this would be more complex and most likely use a Database as the datastore. However, for demonstration purposes, this will be sufficient.

Server-side Events

If no data is present for the control to bind to, there needs to be a way to add items to the datasource. The markup shown above contains a FooterTemplate with a table row containing controls to allow the user to input data and a LinkButton to trigger the OnAddRecored event handler.

protected void OnAddRecord(object sender, EventArgs e)
{
    // Get the textboxes using the button as the starting point
    TextBox firstName = ((Control)sender).Parent.FindControl("NewFirstName") as TextBox;
    TextBox lastName = ((Control)sender).Parent.FindControl("NewLastName") as TextBox;

    //  No point in adding anything if empty
    if(!string.IsNullOrWhiteSpace(firstName.Text) || 
			!string.IsNullOrWhiteSpace(lastName.Text))
    {
        // Add a new Contact and rebind the repeater
        Data.Contacts.Add(new Contact() 
	{ ID = Data.NextId, FirstName = firstName.Text, LastName = lastName.Text });

        Repeater1.DataSource = Data.Contacts;
        Repeater1.DataBind();
    }
}

Nothing too complex here. The event is being triggered from the LinkButton, the sender object in this event handler. From that, you get the RepeaterItem which is the Parent object of the LinkButton and find the TextBox controls. Then it is simply a matter of adding to the datasource and rebinding the Repeater control. It gets slightly more complex when handling in-place editing.

The OnItemCommand event handles the command actions form the LinkButtons:

protected void OnItemCommand(object source, RepeaterCommandEventArgs e)
{
    if(e.CommandName == "delete")
    {
        Data.Contacts.RemoveAt(e.Item.ItemIndex);
    }
    else if(e.CommandName == "edit")
    {
        EditIndex = e.Item.ItemIndex;
    }
    else if(e.CommandName == "save")
    {
        HtmlInputHidden t = e.Item.FindControl("firstNameHidden") as HtmlInputHidden;
        Data.Contacts[e.Item.ItemIndex].FirstName = t.Value;
        t = e.Item.FindControl("lastNameHidden") as HtmlInputHidden;
        Data.Contacts[e.Item.ItemIndex].LastName = t.Value;
        EditIndex = -1;
    }

    Repeater1.DataSource = Data.Contacts;
    Repeater1.DataBind();
}

The Delete action is very simple. Just remove the item from the datasource based on the index. For editing, I set a ViewState backed property, EditIndex, to the index of the item wishing to be edited which will be used when the control is databound.

Although the TextBox controls could have been added in each row of the ItemTemplate and the visibility property used creates too much overhead. Editing is only done on one item at a time so to have many TextBoxes rendered, even thought they are invisible and may never be used is too heavy an approach. Instead, the controls will be added dynamically to the row being edited using a PlaceHolder control.

protected void OnItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if(e.Item.ItemType == ListItemType.Item || 
		e.Item.ItemType == ListItemType.AlternatingItem)
    {
        if(e.Item.ItemIndex == EditIndex)
        {
            // Find the placeholder
            PlaceHolder p = e.Item.FindControl("firstNameEditPlaceholder") as PlaceHolder;

            // Create textBox and assign the current value of the data item
            TextBox t = new TextBox();
            t.ID = "firstNameEdit";
            t.Text = ((Contact)e.Item.DataItem).FirstName;

            // Add the textbox to the placeholder
            p.Controls.Add(t);

             // Get the existing label and hide it
            Label l = e.Item.FindControl("firstName") as Label;
            l.Visible = false;

            p = e.Item.FindControl("lastNameEditPlaceholder") as PlaceHolder;            
            t = new TextBox();
            t.ID = "lastNameEdit";
            t.Text = ((Contact)e.Item.DataItem).LastName;
            p.Controls.Add(t);

            l = e.Item.FindControl("lastName") as Label;
            l.Visible = false;

            // Make hidden fields visible
            HtmlInputHidden h = e.Item.FindControl("firstNameHidden") as HtmlInputHidden;
            h.Visible = true;
            h = e.Item.FindControl("lastNameHidden") as HtmlInputHidden;
            h.Visible = true;

            // Remove the edit button from display
            ImageButton b = e.Item.FindControl("Edit") as ImageButton;
            b.Visible = false;

            // Re use the delete button
            b = e.Item.FindControl("Delete") as ImageButton;
            b.CommandName = "save";
            b.OnClientClick = "OnSave(this)";
            b.ImageUrl = "~/Images/base_floppydisk_32.png";                    
        }
    }
} 

The PlaceHolder control is necessary to dynamically add the TextBox controls because, although you can retrieve the Label control and find the Parent control, table cell in this case, you can't add the Textbox because ASP.NET generates the element as a LiteralControl which doesn't support adding child Controls. After adding the Textbox and assigning the current value from the datasource, the Label control is hidden by setting visibility=false then the HtmlHiddenInput fields are set with visiblity=true so they will be rendered and usable. The final step is to remove the edit button and reuse the delete button for saving the item once editing is complete. The reason for the HtmlHiddenInput is explained below.

Saving the Edited Item

Saving the in-place edit is where it gets a bit more complicated using server-side processing. When the Save button is clicked, the first event to be fired is the ItemCommand. However, since the edit controls were added dynamically in the ItemDataBound event, and this event has not been fired yet, they are not available. Likewise, since the control has not been bound to any data yet, the Label controls are being reconstituted from ViewState so they can't be used for temporary storage. In place of this, the HtmlHiddenInput controls are used with JQuery based JavaScript to transfer the values from the edit controls to the hidden fields. This is the only client-side code in this example.

function OnSave(obj)nSave(obj)
{
    // Find the row this button is in
    var tr = $(obj).closest("tr");
    // Get the value from the edit control
    var firstNameEdit = tr.find("[id*='firstNameEdit']").val();
    // assign value to hidden input
    tr.find("[id*='firstNameHidden']").val(firstNameEdit);

    var lastNameEdit = tr.find("[id*='lastNameEdit']").val();
    tr.find("[id*='lastNameHidden']").val(lastNameEdit);
}

Client-side with AJAX

In the previous example, the ASP.NET engine was handling a lot of the rendering and behind the scenes processing, with a client-side approach, however, it must be implemented by hand.

The first thing to start with is hooking up the button events when the document is loaded. The live JQuery method is used for the edit and delete buttons so any new rows that are added will automatically have the events implements.

 $(document).ready(function ()
{tion ()
{
    // Use live so when adding new records the events will
    // automatically be bounde
    $("[id*='edit']").live('click',OnEdit);
    $("[id*='delete']").live('click', OnDelete);
    $("[id*='add']").click(OnAdd);
});   

The next step is to implement the functionality to add new contacts.

function OnAdd()
{
    // Get the row this button is within
    var tr = $(this).closest("tr");
    // Get the first and last name controls in this row
    var firstName = tr.find("#newFirstName");
    var lastName = tr.find("#newLastName");

    // Create a new row and update the firstname and lastname elements
    // appropriately
    newRow = NewRow(tr);
    newRow.find("span[id='firstName']").text(firstName.val());
    newRow.find("span[id='lastName']").text(lastName.val());

    AddContact(firstName.val(), lastName.val())

    // Clear everything out to start again
    firstName.val("");
    lastName.val("");
}

Here the this variable, which represents the Add button that was clicked, is used to find the table row it belongs to. From this, the input elements are found, the text that was entered by the user is extracted. The trick now comes in inserting a new row into the table. The NewRow will attempt to clone an existing row if any exists, otherwise the HTML must be created. An alternative would be to create the row in the markup but hide it from display, then use it for cloning.

function NewRow(tr)NewRow(tr)
{
    // If only one sibling then create a new row
    // otherwise just clone an existing one
    if(tr.siblings().length != 1)
    {
        var clone = tr.prev().clone();
        tr.before(clone);
    }
    else
    {
        var newRow = "<tr id=''>" +
            "<td>" +
                "<image id='edit' src='Images/EditDocument.png' class='imgButton' />" +
                "<image id='delete' 
			src='Images/Delete_black_32x32.png' class='imgButton'/>" +
            "</td>" +
            "<td>" +
                "<span ID='firstName'></span>" +
            "</td>" +
            "<td>" +
                "<span ID='lastName'></span>" +
            "</td>" +
        "</tr>";
        tr.before(newRow);
    }

    return tr.prev();
}

After the new row has been inserted and the text entered by the user has been updated, the next step is to add the new contact to the datastore on the server. This is accomplished with an AJAX call to a WebMethod on the page.

function AddContact(firstName, lastName)
{
    var data = '{'
            + "\"firstName\":\"" + firstName + "\","
            + "\"lastName\":\"" + lastName + "\""
            + '}';
    $.ajax({
        type: "POST",
        url: "AjaxEdit.aspx/AddContact",
        data: data,
        contentType: "application/json",
        dataType: "json",
        error: OnAjaxError,
        success: OnAddContactSuccess
    });
}

function OnAddContactSuccess(data)
{
    var result = eval('(' + data.d + ')');
    // Assign id from newly added contact
    newRow.attr("id", result);
    newRow = null;
}

There is nothing very complex here, just package the data and send it via AJAX to the method. If the method completes successfully, the id of the newly added contact is then assigned to the id attribute of the newly added row so it will be available during and edit or delete operation.

Edit and Save

Edit functionality is handled similarly to the server-side approach; insert input controls inline and replace the edit and delete buttons.

function OnEdit()
{
    // Get the row this button is within
    var tr = $(this).closest("tr");
    // Get the first and last name controls in this row
    var firstName = tr.find("span[id='firstName']");
    var lastName = tr.find("span[id='lastName']");

    // Insert an input element before the labels
    // and set the value to the label text
    // Then hide the label
    firstName.before("<input id='firstNameEdit' 
	type='text' value='" + firstName.text() + "'/>").hide();
    lastName.before("<input id='lastNameEdit' 
	type='text' value='" + lastName.text() + "'/>").hide();

    // Hide the existing buttons and add a save button in there place
    tr.find("[id*='delete']").hide();
    tr.find("[id*='edit']").before("<img id='save' src='images/base_floppydisk_32.png' />")
        .hide();

    tr.find("[id*='save']").one('click', OnSave);
}

Saving is once again similar; extract the values, update the labels, restore the buttons and update the datasource on the server.

function OnSave()
{
    // Get the row this button is within
    var tr = $(this).closest("tr");

    var firstName = tr.find("[id='firstNameEdit']");
    var lastName = tr.find("[id='lastNameEdit']");

    // Set the text of the labels from the input elements and show them
    tr.find("span[id='firstName']").text(firstName.val()).show();
    tr.find("span[id='lastName']").text(lastName.val()).show();

    // Remove the input elements
    firstName.remove();
    lastName.remove();

    // Show the buttons again and remove the save
    tr.find("[id*='delete']").show();
    tr.find("[id*='edit']").show();
    tr.find("[id*='save']").remove();

    // update the contact on the server
    UpdateContact( tr.attr("id"), firstName.val(), lastName.val())
}

Conclusion

The example was not meant to be an extensive exploration of the capabilities of the ASP.NET Repeater control, but hopefully it has successfully demonstrated how it could be used for CRUD operations against a datasource.

History

  • Initial edit: 8/29/11

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here