Friday, May 26, 2006

DataGrid ItemCommand Without ViewState

I blogged awhile back about my frustrations with the ASP.NET DataGrid and its reliance on ViewState, specifically for firing the ItemCommand event. Well this situation presented itself to me again and I decided to attack the problem with a little more aggression than previous attempts.

It's important to note that with my concept of Custom Web Controls Everywhere, I was able to implement an application-wide solution pretty easily. I will outline the solution here, but without some custom controls already in place, this concept cannot be easily applied.

Let’s start by discussing why ItemCommand events don’t fire within the DataGrid when ViewState is turned off. There are a couple of causes for this. First, with ViewState turned off, when the postback fires, the DataGrid does not have any items anymore. With no items, there are no controls to look at to identify that the event source was an item within the DataGrid. Second, the CommandName and CommandArgument that were set on the control are not persisted anywhere outside of ViewState. So even if the system was able to identify that the event source was within the DataGrid, there’s no CommandName or CommandArgument to use for firing the command.

So, how do we solve these two problems?

The typical recommendation seems to be to bind the DataGrid again, so that the control hierarchy can be rebuilt and the CommandName and CommandArgument can be restored. But this has severe drawbacks in my opinion. First, you’re forced to hit the database again, and depending on the system and the function, this might be a big deal. Second, and probably more importantly, the data may change between calls, altering the control hierarchy. This could potentially line the control ID from the event up with an entirely different record. Imagine you’re firing a Delete command, and you will now go delete the selected record without confirmation. You may be deleting the incorrect record – oops! Because of these 2 issues, I’ve never thought re-binding was an acceptable solution.

Unfortunately though, I don’t have an answer to the first problem of losing the control references. The controls are gone, but maybe we don’t care… let’s look at how we solve the second problem. I’m actually using a trick that I used a few years ago for http://www.statsworld.com. There were many screens in that site that were going to show very large amounts of data, in a grid format. These screens needed to be ultra-high performance, so I didn’t even attempt to use the DataGrid. Instead, I created what we called at the time the SuperGrid. The SuperGrid did lots of great things, including having the ability to drill down into records without ViewState being turned on. But the SuperGrid was an extremely specialized solution that won’t ever get reused outside of Statsworld. But I came up with a key principle that I’m reapplying now. And that is to persist the CommandName and CommandArgument into the JavaScript __doPostBack call.

What I find ironic about this is that from what I recall, in ASP.NET 1.0 Beta 1, the CommandArgument was persisted into the JavaScript __doPostBack call for us. But with ASP.NET 1.0 Beta 2, it was no longer persisted. I’m not sure I’ve ever found what the __EVENTARGUMENT is used for anymore, but it’s going to come in handy here for us.

Now, in order to get the CommandName and CommandArgument persisted into the JavaScript, we have some hoops we need to jump through. But with a custom DataGrid in place, we only have to do this in one place. Aren’t you glad that you created a custom DataGrid even though you weren’t sure what all you would use it for? Let’s break it down...

First, let’s start with an inherited DataGrid class, nothing special here except that we’re declaring constants for __EVENTTARGET and __EVENTARGUMENT which will get used later. We also went ahead and wired up the Load and PreRender events.

using System;

namespace Controls
{
/// <summary>
/// Inherit the ASP.NET DataGrid
/// </summary>
public class DataGrid : System.Web.UI.WebControls.DataGrid
{
// Use these to get data from the forms collection
private const string _EventTarget = "__EVENTTARGET";
private const string _EventArgument = "__EVENTARGUMENT";

/// <summary>
/// Constuctor for the custom data grid
/// </summary>
public DataGrid()
{
// Wire up the Load & PreRender event handlers
this.Load += new EventHandler(DataGrid_Load);
this.PreRender += new EventHandler(DataGrid_PreRender);
}
}
}

Now we need to handle the PreRender event within the DataGrid. This is where we’ll check to see if ViewState is being used and if not, we’ll start the ball rolling.

/// <summary>
/// PreRender event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DataGrid_PreRender(object sender, EventArgs e)
{
// If viewstate is turned off, then we need to persist any
// commands inside the grid
// We need to look all the way up to the page to make
// sure that we don't have a parent with viewstate
// turned off even though ours is turned on
if (!IsUsingViewState())
PersistCommands(this);
}

/// <summary>
/// Determine if we get to use viewstate
/// </summary>
private bool IsUsingViewState()
{
System.Web.UI.Control Control = this;

// We need to look all the way up to the page to make
// sure that we don't have a parent with viewstate
// turned off even though ours is turned on
while (Control != null && Control.EnableViewState)
Control = Control.Parent;

bool UsingViewState = true;

// If we found a parent with viewstate turned off, we
// have stopped searching and we need to persist any
// commands inside ourself
if (Control != null && !Control.EnableViewState)
UsingViewState = false;

// Let's store a flag in viewstate. This allows us to know
// for sure whether we rendered with viewstate or not.
this.ViewState["DataGrid.IsUsingViewState"] = UsingViewState;

return UsingViewState;
}

/// <summary>
/// Check to see if we were using viewstate on the
/// previous render
/// </summary>
/// <returns></returns>
private bool WasUsingViewState()
{
// If we don't have the flag in viewstate, then we can assume
// that we don't have viewstate
if (this.ViewState["DataGrid.IsUsingViewState"] == null)
return false;
else
return Convert.ToBoolean(
this.ViewState["DataGrid.IsUsingViewState"]);
}

Inside PreRender, we use the IsUsingViewState function and search up the control hierarchy to see if ViewState is turned off anywhere. If so, we call a method to persist any commands inside ourself. This is where the fun begins.

/// <summary>
/// Persist any commands inside LinkButtons -- Recursive method
/// </summary>
/// <param name="Parent"></param>
private void PersistCommands(System.Web.UI.Control Parent)
{
bool LinkButtonFound = false;

// Search all child controls for LinkButtons
foreach (System.Web.UI.Control Child in Parent.Controls)
{
if (!LinkButtonFound
&& Child is System.Web.UI.WebControls.LinkButton)
{
// We found a LinkButton, let's take over the
// rendering of the LinkButton's parent. We have
// to take over rendering at the parent level
// because the RenderMethod is only called when
// the control has its own children. We can't
// assume that the LinkButton will have children,
// but we know that the LinkButton is a child of
// its parent. So we can safely take over rendering
// of the current Parent.
System.Web.UI.WebControls.LinkButton Link =
(System.Web.UI.WebControls.LinkButton)Child;

Parent.SetRenderMethodDelegate(
new System.Web.UI.RenderMethod(
LinkButtonParentRenderer));

// We don't need to check any more controls within
// this parent, because the parent is already set
// to use the custom renderer
LinkButtonFound = true;
}

// Process any grandchild controls -- recursive call
PersistCommands(Child);
}
}

The magic trick here is using the SetRenderMethodDelegate property on the LinkButton’s parent control – the parent control – not the LinkButton itself. SetRenderMethodDelegate is documented as, “Assigns an event handler delegate to render the server control and its content into its parent control.” Using .NET Reflector, we can see that System.Web.UI.Control.RenderChildren is defined as the following:

protected virtual void RenderChildren(HtmlTextWriter writer)
{
if (this._renderMethod != null)
{
this._renderMethod(writer, this);
}
else if (this._controls != null)
{
int num1 = this._controls.Count;
for (int num2 = 0; num2 < num1; num2++)
{
this._controls[num2].RenderControl(writer);
}
}
}

So, when there is a render method defined for a control, that method is called in place of the standard RenderControl. This lets us hijack the rendering of any control. Now we are doing this for any control that contains a LinkButton.

So what do we do with the LinkButtonParentRenderer? Let’s dig in!

/// <summary>
/// This is our custom renderer for link button parents
/// </summary>
/// <param name="writer"></param>
/// <param name="Parent"></param>
private void LinkButtonParentRenderer(
System.Web.UI.HtmlTextWriter writer,
System.Web.UI.Control Parent)
{
// Loop through all of the controls in the Parent.
// We know that we'll hit at least one LinkButton
foreach (System.Web.UI.Control Child in Parent.Controls)
{
// Is this our LinkButton?
if (Child is System.Web.UI.WebControls.LinkButton)
{
// We found a LinkButton. We need to override
// his rendering so that we can set up a custom
// Href attribute, overriding what the LinkButton
// would have done by default
System.Web.UI.WebControls.LinkButton Link =
(System.Web.UI.WebControls.LinkButton)Child;

// Get the custom PostBack Link to use on the tag
// and add it as the Href attribute
writer.AddAttribute(
System.Web.UI.HtmlTextWriterAttribute.Href,
GetPostBackLink(Link));

// Call the LinkButton's RenderBeginTag routine
// to open his tag
Link.RenderBeginTag(writer);

// If the LinkButton has controls, we need to render
// them. From: System.Web.UI.Control.RenderChildren
if (Link.HasControls())
{
// Make sure the controls collection exists
if (Link.Controls != null)
{
// Render each child control
foreach (System.Web.UI.Control LinkChild
in Link.Controls)
{
LinkChild.RenderControl(writer);
}
}
}
else
{
// The LinkButton didn't have any child controls,
// so just render his text
writer.Write(Link.Text);
}

// Close the tag
Link.RenderEndTag(writer);
}
else
{
// This wasn't a LinkButton, so just
// render him normally
Child.RenderControl(writer);
}
}
}

The important part is that for any LinkButtons, we call writer.AddAttribute to assign the Href attribute, and then we call RenderBeginTag. This opens the <a> tag with our custom Href attribute. From there, we render the contents of the LinkButton. Thank goodness for .NET Reflector!

Our custom routine for GetPostBackLink is pretty simple, but this is where we actually persist the CommandName and CommandArgument into the JavaScript.

/// <summary>
/// Get a modified PostBack client hyperlink with the
/// command name and the command argument embedded
/// </summary>
/// <param name="Link"></param>
/// <returns></returns>
private string GetPostBackLink(
System.Web.UI.WebControls.LinkButton Link)
{
// Get a standard postback hyperlink
string PostBack = this.Page.GetPostBackClientHyperlink(Link,
Link.CommandArgument);

// Mimic Control.UniqueIDWithDollars so that we can
// manipulate the EventTarget portion of the hyperlink.
// Control.UniqueIDWithDollars is used by
// GetPostBackClientHyperlink to get the EventTarget
string UniqueIDWithDollars = Link.UniqueID;

if (UniqueIDWithDollars != null
&& UniqueIDWithDollars.IndexOf(':') >= 0)
UniqueIDWithDollars =
UniqueIDWithDollars.Replace(':', '$');

// Add the CommandName onto the EventTarget,
// using '=' to designate it
return PostBack.Replace(
UniqueIDWithDollars,
UniqueIDWithDollars + '=' + Link.CommandName);
}

What we’ve done is inject our CommandName into the __EVENTTARGET portion of the __doPostBack hyperlink. And then we’ve put our CommandArgument into the otherwise empty __EVENTARGUMENT parameter. This will result in a hyperlink such as the following:

    javascript:__doPostBack('dgGrid$_ctl2$_ctl0=Edit','1')

This hyperlink tells us that within our DataGrid with ID of ‘dgGrid’, we’ve got a control with a CommandName of ‘Edit’ and a CommandArgument of ‘1’.

Okay, so we’ve persisted our CommandName and CommandArgument into the JavaScript in the rendered HTML. Now what? Well, now we need to capture postbacks that were raised by these special postback events. This is where we need to handle the DataGrid.Load event.

/// <summary>
/// On Load, search for any events we need to fire if
/// viewstate was disabled last render
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DataGrid_Load(object sender, EventArgs e)
{
// We only care about postbacks when we didn't have
// viewstate on the previous render
if (Page.IsPostBack && !WasUsingViewState())
{
// The page is posting back and we didn't have
// viewstate. Get the event target of the postback
string EventTarget =
this.Page.Request.Form[_EventTarget];

// If the event target starts with our ID then we
// know that we fired the postback
if (EventTarget != null
&& EventTarget.StartsWith(this.ClientID))
{
// Get the command name from the event target and
// the command argument from event argument
string CommandName = GetCommandName(EventTarget);
string CommandArgument =
this.Page.Request.Form[_EventArgument];

// Fire the bubble event, which will handle
// sort commands, edit commands, etc.
this.OnBubbleEvent(this, new
System.Web.UI.WebControls.DataGridCommandEventArgs(
null, this,new
System.Web.UI.WebControls.CommandEventArgs(
CommandName, CommandArgument)));
}
}
}

The Load event checks to see if the page was posted back from a control within the DataGrid. And we only do this processing if we’re not using ViewState. When we identify an event that needs to be raised, we raise it using the OnBubbleEvent routine. OnBubbleEvent does all of the work for determining of the SortCommand, EditCommand, SaveCommand, etc. need to be fired. So we don’t have to worry about any of that, we just fire and forget!

We did use a GetCommandName function above that extracts the CommandName from the __EVENTTARGET for us. That routine is pretty straight-forward.

/// <summary>
/// Get the command name from the event target value
/// </summary>
/// <param name="EventTarget"></param>
/// <returns></returns>
private string GetCommandName(string EventTarget)
{
// Make sure that we have our designator to identify
// our command name
if (EventTarget.IndexOf('=') > 0)
return EventTarget.Substring(EventTarget.IndexOf('=') + 1);
else
return null;
}

Just find the token that follows the designator that we set up for adding the CommandName to the end of the __EVENTTARGET.

Well, that’s it! By putting this logic into our DataGrid control, any page can turn off ViewState, and all LinkButton commands can be fired and captured. There is one disclaimer though.

Back to our original #1 problem. We don’t have the DataGrid Items anymore. This means that when the ItemCommand is fired, we had to pass in a null reference to the actual source item. So you won’t be able to use e.Item when you handle the ItemCommand. If you need to access e.Item.ItemIndex or whatnot, you’ll have to keep ViewState on. But maybe the real solution to this is to code the screen in a manner that you won’t need to rely on e.Item. You can actually manufacture a CommandArgument with a delimited list of values, with whatever you want inside of it. Then you could get whatever properties you want for the item that was clicked.

I had tried 3 times to solve this problem previously, with the first attempt resulting in the Statsworld “SuperGrid”. That effort proved that this concept could work, but I had not been able to re-apply the JavaScript persistence technique to a true DataGrid until now. I’m pretty pumped as I expect this to allow me to turn ViewState off for the majority of the DataGrids in my current project. I expect the events to fire just fine everywhere, but testing will need to be done to ensure each screen wasn’t relying on ViewState for the DataGrid for some other display function.

I have posted the full code here. Enjoy!


[5/26/2006 2:30pm]
I wanted to give a call out to Alexandre Gomes. His post discussing SetRenderMethodDelegate sparked the idea for me to use this routine to override the LinkButton rendering. Before that, my implementation for this was project-specific and spanning multiple controls. I'm super-pumped that I now have a generic solution that is baked entirely into the DataGrid. Thanks Alexandre!

Also, I am going to convert the DataGrid code to VB and I'll get it added into the zip file later today.


[5/26/2006 3:50pm]
While converting the code to VB, I found a bug... but a minor one. In some cases of programmatically altering the EnableViewState setting of the datagrid, on postback, the grid thought that viewstate was turned on, but it wasn't during the previous rendering. So I added a routine for WasUsingViewState and a mechanism for flagging whether or not viewstate was enabled during the previous render.

I have updated the code above as well as the zip file's contents. And the zip now contains the VB code too.

2 comments:

Alexandre Gomes said...

I appreciate the Credits and its nice to know someone still reads my blogs, and it was usefull :)

Thats a pretty cool article you've here and it shows the power of SetMethodRenderDelegate :)

Anonymous said...

Thx for this great article, but Source code not accessible! File not found.