Wednesday, August 26, 2009

ASP.NET Web forms - Receiving events in a Repeater with ViewState disabled

Welcome to my blog, and my first blog post, ever. I will use this blog to post information, tips, tricks, patterns, etc. mainly for the .NET platform.

My first blog post is about overcoming the problem that controls that can perform postback stops working if you place them inside a repeater, and the repeater's viewstate is disabled. The page does post back, but the event is lost. It never arrives at its destination. A while ago, I created a workaround for this limitation. It cannot handle all situations, but it can handle those that I find to be the most common ones.

The Problem

Why is the ViewState important for the repeater?

When the browser requests a page, a new instance of the specific page class is created; and every control that this page contains is also instantiated, and placed in the page's control tree. When all initialization logic, event handlers etc have been processed, the control tree is rendered to HTML, and the instance that was created is forgotten. When the same user makes a postback, a new fresh instance is created for every control in the control tree.

The view state is basically a place where the control tree is serialized to, and deserialized from at post back, to allow recreating the state of the page after the post back without having to initialize the page for every post back, avoiding expensive database operations.

So if for example we have a page with a button and a repeater. When you click the button, the page does a post back. By deserializing its state from the view state, the repeater is capable of recreating its control tree, therefore being able to render the same HTML again, without having to rebind to the original data. But the viewstate can take up quite a lot of space in your page. And say for example that you don't have any buttons on the page, but you have a button inside the repeater, and when you click the button you modify the underlying data, thus you have to rebind the repeater anyway. In this case, you don't need to have the viewstate to render the same HTML again.

But when you click the button, the ASP.NET framework looks at the ID of the control that should receive the event, and calls a function on this control, namely IPostBackEventHandler.RaisePostbackEvent. But the target for the function is not the repeater itself, but the button that is located in the control tree inside the repeater. An because the control tree is not regenerated, there is no one to receive this event.

The Workaround

At one project I worked on, we had a page with four repeaters, each rendering a table with quite a lot of fields. This gave such a huge viewstate that it was giving performance problems. So I found this workaround to the problem.

There are two drawbacks to this solutions. You have to place the repeater inside its own user control. I personally don't really find this to be a drawback because I always wrap my repeaters inside their own user controls to better organize the code.

The second drawback is a bit more serious. You cannot just place any control that has post back events inside the repeater. In fact, you need to control the event generation yourself. But if your repeater only does post backs from Button or LinkButton controls, this should be fairly simple.

The reason why you have to wrap the repeater inside a user control is because the controls inside the repeater is not capable of receiving the event; we simply cannot change this fact. Therefore we must direct the event to a control outside the repeater. That control happens to be the user control that we are wrapping the repeater in, thus why we have to wrap it in a user control. This leads to the second consequence; that you cannot place any control inside a repeater. This is because controls that fire post back events have a habit of routing the event to itself. We therefore need to control the post back javascript code from the user control implementation itself.

That actually means that we cannot even use Button or LinkButton controls inside the repeater. But their behavior is very easy to reproduce, so they are easy to implement in your workaround. But say that you placed a DatePicker control inside your repeater. Then you would not be able to use this workaround. Let's move on to the example:

The example

This example is created for .NET 4.0 beta 1. I originally created this pattern for a .NET 2.0 project, so I know that the pattern works for earlier versions of the framework, but there might of course be differences in the implementation. As I have described, I have the repeater wrapped in a user control. When you create a new user control, there are basically two ways to implement the user control:

  1. The user control has the responsibility of getting data from the data source, and updating data based on event.
  2. The page (or a presenter in MVP) has the responsibility of getting data from the data source, and sends the data to the user control through a public method on this. Button clicks in the repeater will cause it to raise an event that the page handles, and updates the underlying data, and tells the repeater to update itself.

Option 2 has a more decoupled design, but also more code. As this post is not about decoupling application logic, but events in a repeater, my example code will use option 1; the repeater loads data in Page_Load, and updates data directly, when the event is received.

In my simple example, I load a bunch of objects from a data source and place them in a repeater-generated table. Each row in the table has a “delete”-link. Clicking the link will delete the object and rebind the repeater.

Let's first take a look at my “data source”:

namespace RepeaterEvent
{
    public class DataClass
    {
        public int ID { get; set; }
        public string A { get; set; }
        public string B { get; set; }
    }

    public static class DataClassContainer
    {
        public static List Objects;

        public static void Init()
        {
            Objects = new List();
            for (int i = 0; i < 20; i++)
            {
                Objects.Add(new DataClass()
                {
                    ID = i,
                    A = "A" + i,
                    B = "B" + i
                });
            }
        }
    }
}

The Init() function simply reinitializes the static array, and I call this function in the Page_Load function in my user control, if it is not a postback:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        DataClassContainer.Init();
        DataRepeater.DataSource = DataClassContainer.Objects;
        DataRepeater.DataBind();
    }
}

During postbacks, I then modify the Objects collection, and rebind the repeater to the modified collection.

In my test project, my user control is named RepeaterControl. Here is the class declaration:

namespace RepeaterEvent
{
    public partial class RepeaterControl : UserControl, IPostBackEventHandler
    {
        public void RaisePostBackEvent(string eventArgument)
        {
                ...
        }
    }
}

The user control implements IPostBackEventHandler so that it may receive postback events.

As I need to manually generate the event firing code, the “LinkButton” is replaced by a simple HTML hyperlink. I use functionality in the .NET framework generate the actual javascript for me. Let's have a look at the .ascx file.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="RepeaterControl.ascx.cs" Inherits="RepeaterEvent.RepeaterControl" %>
<asp:Repeater runat="server" ID="DataRepeater" EnableViewState="false">
  <HeaderTemplate>
    <table> <tbody>
  </HeaderTemplate>
  <FooterTemplate>
    </tbody> </table> 
  </FooterTemplate>
  <ItemTemplate>
    <tr>
      <td><%# GetA(Container.DataItem) %></td>
      <td><%# GetB(Container.DataItem) %></td>
      <td><a href="<%# GetDeleteScript(Container.DataItem) %>">Delete</a></td>
    </tr>
  </ItemTemplate>
</asp:Repeater>

The GetXYZ functions are functions that I have defined in my code behind file. I always do this instead of using the Eval function, as it will provide compile time checking of the properties that I access. If someone for example removed or renamed a property, they would receive a compile time error instead of a runtime error.

Here are the three functions.

    protected string GetA(object objDataClass)
    {
        var obj = (DataClass)objDataClass;
        return obj.A;
    }
    protected string GetB(object objDataClass)
    {
        var obj = (DataClass)objDataClass;
        return obj.B;
    }
    protected string GetDeleteScript(object objDataClass)
    {
        var obj = (DataClass)objDataClass;
        string eventArgs = "delete:" + obj.ID;
        return Page
            .ClientScript
            .GetPostBackClientHyperlink(
            this, eventArgs);
    }

As you can see, I use the Page.ClientScript.GetPostbackClientHyperlink function to generate a javascript hyperlink, “javascript:__doPostBack(...)” that I can use as the URL for my hyperlink element. The first parameter is the target for the event. The target for the event is the user control itself; so we pass “this” as the first parameter. The second parameter is a string that will be sent to the RaisePostBackEvent when the link is clicked. I use this format “delete:{id}” so that I can make something that resembles the CommandName/CommandArgument behaviour of normal repeater event.

Let's just have a look at the RaisePostbackEvent(...) function:

    public void RaisePostBackEvent(string eventArgument)
    {
        string[] args = eventArgument.Split(':');
        string command = args [0];
        string argument = args[1];
        if (command == "delete")
        {
            int id = int.Parse(argument);
            DataClassContainer.Objects.Remove(
                DataClassContainer.Objects.Find(x => x.ID == id));
            DataRepeater.DataSource = DataClassContainer.Objects;
            DataRepeater.DataBind();
        }
    }

It simply decodes the argument, and determines what function should be performed (delete), and which object is the target for this operation. It then removes it from the “database” and then rebinds the repeater to the updated data source.

So basically what we have here is something that reproduces the behaviour of having a LinkButton with a CommandName and a CommandArgument inside the repeater.

Conclusion

I have shown you that you can have events fired from inside a Repeater control, and handle them. But as it's not natively supported by the framework, there are some limitations. But that said, this pattern has proved valuable to me in the past, so if you can live with the limitations, you can chunk off a great part of your view state. And if you have as much view state as I have had, you can seriously improve user experience.

As this is my first blog post, I would appreciate comments on how it is written. Is it too long, too short. Is it clearly understandable. Were there points I should have focused more on. Should I have provided more example code?

I have a few ideas for new blog posts, one or two about using StructureMap in ASP.NET projects, and then I have planned for a long post on unit testing ASP.NET Web Forms. And I'm talking about real unit testing, stuff that you can run from NUnit console without being dependent on a web server. I'm currently writing on that one, but there is going to be a lot of text. So that will be at least 3 seperate blog posts, maybe more.

I hope you find this useful.

Pete.

7 comments:

  1. A very clear and easy to read post. Your code samples in particular are very simple, well laid out and to the point.

    You've also helped me greatly. I was struggling with an issue with dynamically created linkbuttons within a datagrid. Due to the buttons being dynamic I had to rebind the whole datagrid on every page_load in order to capture the events. This caused other problems due to out of date data. Your solution helped me capture the linkbutton commands without having to rebind the datagrid first. Thanks!

    ReplyDelete
  2. Thank you so much for taking the time to post this! I was dying to know of a solution to this problem and

    found (thankfully) a link to your post from here http://stackoverflow.com/questions/1321944/asp-net-repeater-

    item-command-not-getting-fired. I haven't had to deal with this difficult problem in years. I have avoided

    using ViewState and avoided this problem entirely by choosing UI designs that avoid posting back from

    repeaters/grids/lists in favor of plain old hyperlinks to a different aspx page... If I've ever had to use a

    LinkButton or Button in a repeater/gridview/listview, i've had the luxury of having enough data on what should

    be bound very early in the life cycle (i.e., a query string parameter for example with an id in it) and

    binding all my repeaters/gridview/listview controls on Page_Init (which effectively rebinds the control with

    ViewState off very nicely). However, I know find myself working on a project where ASP.Net 3.5 WebForms are used with a VERY large ViewState and a) LinkButtons and Buttons are used extensively inside listview/gridview/repeaters and b) postbacks from those LinkButtons and Buttons lead to yet further listview/gridview/repeater controls with LinkButtons and Buttons in them as well! I am dying to turn off ViewState for this project but found myself completely stuck: ItemCommand events won't fire unless ViewState is enabled OR I rebind on all requests (gets and posts) on Page_Init... But, I don't know what to bind that early (i.e, OnInit I don't have the right ID yet)! Enter your suggested solutions here! It's the middle of the night and I haven't tried them yet, but you've definitely given me food for thought and some hope that other folks have been down this path before! Thanks for helping me get this off of my mind a bit so I can rest this weekend a little easier! Adam :)

    ReplyDelete
  3. You've also helped me greatly. I was struggling with an issue with dynamically created linkbuttons within a datagrid. Due to the buttons being dynamic I had to rebind the whole datagrid on every page_load in order to capture the events. This caused other problems due to out of date data. Your solution helped me capture the linkbutton commands without having to rebind the datagrid first
    i am .net developer in usa Miami .

    ReplyDelete
  4. Pete, what an excellent article. I implemented this on the very first attempt and was more than thrilled. Thanks.

    ReplyDelete
  5. Thanks a lot!

    Lost an entire morning because of calling a link (didn't receive the event on postback) inside a repeater. That is until I found your post on stack overflow and this nice blog.

    thank you again.

    ReplyDelete
  6. Hey I've read your this article and found this really very interesting. I am going to make a change in LikeButton inside the repeater. I think this is going to help me for sure. Thanks!

    ReplyDelete
  7. .NET development, ASP development, SharePoint development,.NET development Microsoft development , software development, Singapore – Total eBiz Solutions Home

    ReplyDelete