I don't know about you, but I cringe every time I need to create a GridView based layout that needs to create a few custom link handlers off that grid. The typical scenario might be a list where you can toggle certain options or where you can fire an operation that otherwise updates the data that's underlying the grid.
I tend to use business objects in my applications so using the standard data controls doesn't work very well, nor would it really buy much in terms of abstraction. In my code I tend to write the databinding logic as part of the page logic which usually is just a couple of lines of code.
Anyway, the problem isn't that it can't be done 'manually' - but rather that there are a myriad of ways that you can hook up custom link/command processing and they are all similar but yet quite different. None of them feels natural to me, so quite frequently - especially if I haven't written that code in a while - I spent way more time than I should rediscovering what approach I should use the hard way. So I'm taking a few minutes - for my own sake - to write this stuff down so I can look it up next time I have a brain fart <s>.
So there are two different ways that I use frequently for hooking up command links in gridViews.
- ButtonFields and CommandFields
- Template Fields with CommandNames/CommandArguments
ButtonFields and CommandFields
ButtonFields supposedly are the simpler case and you can use a ButtonField with a CommandName.
<asp:GridView ID="dgProviders" runat="server"
onrowcommand="dgProviders_RowCommand">
<columns>
...
<asp:ButtonField ButtonType="Link" CommandName="Approval" Text="Approve" />
</columns>
</asp:GridView>
Using a ButtonField you can specify a CommandName which in turn provides the OnRowCommand event some context as to what type of operation you are dealing with in CodeBehind. In CodeBehind you have to do a bit of work though if you want to retrieve the proper context from a DataItem. Here's some code that needs to retrieve a Pk of one of the bound items and then perform some work on the data (in this case the business object):
protected void dgProviders_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (e.CommandName == "Approval")
{
// *** Retreive the DataGridRow
int row = -1;
int.TryParse(e.CommandArgument as string, out row);
if (row == -1)
{
this.ErrorDisplay.ShowError("Invalid selection...");
return;
}
GridViewRow gdrow = dgProviders.Rows[row];
// *** Get the underlying data item - in this case a DataRow
DataRow dr = ((DataTable) this.dgProviders.DataSource ).Rows[gdrow.DataItemIndex];
// *** Retrieve our context
int pk = (int) dr["pk"];
if (e.CommandName == "Approve")
{
this.Provider.Entity.Approved = !this.Provider.Entity.Approved;
this.Provider.Save();
// *** Update the data source and rebind so we display the right thing
dr["Approved"] = this.Provider.Entity.Approved;
this.dgProviders.DataBind();
}
}
}
This seems like a lot of code to have to write just to retrieve data context. The code first has to retrieve the GridViewRow, which is done by retrieving the CommandArgument which in this case a row index of the rendered grid view items. Although the GridViewCommandEventArgs have a DataItem member that member is unfortunately not set with the DataRow as you would maybe expect. So rather than getting the bound item directly, I have to retrieve the gridRow's DataItem index, then use that index and retrieve the value from the original data source.
At that point I have the data I need to be on my way and do my business logic (which in this case is very simple). One advantage using this approach is that I do have access to the underlying data source item (a DataRow here) and so I can update the value and rebind easily and immediately show the updated value(s).
Doesn't it seem very redundant to have to go through all of this just to get at the underlying data item? It seems to me that GridViewEventArgs could have included a consistent mechanism to expose both the DataItemIndex as well as the DataItem itself. Oddly I have no idea why the data item would not be available given that this particular grid is bound to a DataTable which has persistent backing and is live on the form.
The problem with the button field is that it's very limited in functionality. If you need custom text for the caption you can't do that because databinding expressions are not allowed. The following does not work:
<asp:ButtonField ButtonType="Link" CommandName="Approval"
Text='<%# ((bool) Eval(Approved) ? "Approved" : "Unapproved" ) %>' />
because it's not a template container. There is a DataTextField that can be used to display a dynamic value (ie. Approved which would yield True or False only though) and a DataFormatString that can in some cases make this work but for a more dynamic scenario as above that still doesn't do the trick.
Template Columns and Commands
If something more dynamic is required you need to use template fields. Personally I prefer using Template fields most of the time anyway because usually it gives you much more control. Using Template fields with CommandArgument and CommandName also fires the fires the same RowCommand event on the DataGrid. But the handling of the GridViewCommandArgument is quite different.
First here's the implementation of a couple of LinkButtons in the template
<asp:GridView ID="dgProviders" runat="server" CellPadding="4" width="900px"
onrowcommand="dgProviders_RowCommand">
<columns>
...
<asp:TemplateField ItemStyle-VerticalAlign="Top" ItemStyle-Width="150px" ItemStyle-HorizontalAlign="Center" HeaderText="Action">
<ItemTemplate>
<asp:LinkButton runat="server" ID="btnIcon" CommandName="Icon" CommandArgument='<%# Eval("Pk") %>' Text="Remove Icon"></asp:LinkButton><br />
<asp:LinkButton runat="server" ID="LinkButton1" CommandName="Approve" CommandArgument='<%# Eval("Pk") %>' Text="Toggle Approval"></asp:LinkButton>
</ItemTemplate>
</asp:TemplateField>
</columns>
<headerstyle cssclass="gridheader" />
<alternatingrowstyle cssclass="gridalternate" />
</asp:GridView>
Here I can specify a command argument which is nice because it lets me be very concise about what data I want to pass to the RowCommand event. In the event handler the code to retrieve the PK in this case becomes considerably simpler, but as we'll see it's a little harder to get a reference to the underlying data source control if that needs to be updated. For clarity I've explicitly broken out each of the objects here:
protected void dgProviders_RowCommand(object sender, GridViewCommandEventArgs e)
{
int pk = 0;
int.TryParse(e.CommandArgument as string, out pk);
if (pk == 0)
{
this.ErrorDisplay.ShowError("Couldn't access provider entry.");
return;
}
if (!this.Provider.Load(pk))
{
this.ErrorDisplay.ShowError("Couldn't load provider entry.");
return;
}
// *** Grab the underlying DataItem
WebControl wc = e.CommandSource as WebControl;
GridViewRow row = wc.NamingContainer as GridViewRow;
int DataItemIndex = row.DataItemIndex;
DataRow dr = ( (DataTable) this.dgProviders.DataSource).Rows[DataItemIndex];
if (e.CommandName == "Icon")
{
Provider.Entity.IconUrl = "";
Provider.Save();
dr["IconUrl"] = "";
}
else if (e.CommandName == "Approve")
{
this.Provider.Entity.Approved = !this.Provider.Entity.Approved;
this.Provider.Save();
dr["Approved"] = this.Provider.Entity.Approved;
}
// *** Rebind with existing data
this.dgProviders.DataBind();
}
As you can see getting the PK is now easy - it's simply available as the CommandArgument and so it's easy to get context to the underlying data. However, if you need to get at the actual live data item the work requires first to get the control that fired the event (LinkButton), walking up to its naming container (GridViewRow) and then getting the DataItemIndex. From there I can then retrieve the DataRow based on the index.
Again it seems odd given that we are firing a ROW level RowCommand event that there's not an easier way to get row level context even when we are using a Command argument. <shrug>
Also notice that if you have both ButtonFields/CommandFields and template items that have command arguments, the CommandArgument will contain different things. In the case of a CommandField the CommandArgument is the RowIndex. In the case of the CommandArgument passed on a standard control it's the actual command argument and so you have to very carefully make sure you separate each of those operations. Presumably you should should use one or the other but not both together to avoid confusion.
When it's all said and done neither of these approaches is particularly complex. However, if you've never gone down the path of using a RowCommand event the discoverability of the above code is practically nil. It certainly would be much nicer if the event arguments passed back on these row commands provided a little more information and more consistently. But alas - the above works for now.
Other Posts you might also like