Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

GridView and CommandArguments


:P
On this page:

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.

Posted in ASP.NET  

The Voices of Reason


 

Steve from Pleasant Hill
June 08, 2007

# re: GridView and CommandArguments

Thanks for writing this stuff down.

Last month I was lamenting (to myself) that why are grids still so much work, when so much effort goes into improving other areas of a developer's life? A lot of improvement is needed here!

Donnie Hale
June 08, 2007

# re: GridView and CommandArguments

Rick,

Maybe I'm missing something, but I've got code that looks like this:

protected void HandleCommand(object sender, GridViewCommandEventArgs e)
{
// Get the index of the row being changed
int rowndx = Convert.ToInt32(e.CommandArgument);

// Get the data key for that row
int pk = (int) grid.DataKeys[rowndx].Value;

// Do more stuff w/ the PK
// ...

// Change stuff on the row
GridViewRow row = grid.Rows[rowndx];
// use row.FindControl("controlID") ...
}

I agree that it would be much easier if the event args included the PK for the row directly, but that seems simpler than what you posted.

Is there a subtlety which makes this not always work?

Donnie

Josh Stodola
June 08, 2007

# re: GridView and CommandArguments

The fact that they left out the PageMode of NumericNextPrev really makes my jaw hit the floor. I can agree that the GridView still has a lot of potential for improvement.

Rick Strahl
June 08, 2007

# re: GridView and CommandArguments

Donnie I think RowIndex is not going to work in paged scenarios. The row index only refers the actual rows displayed on the page. The DataItem index gets you the index into the data source if I understand this correctly.

Josh, yeah the whole paging thing seems lame. I don't see any reason that you HAVE to implement Page_Changed/Changing to set the page number even when ViewState is off. This should just be automatic given that the _doPostback can hold that state information...

sean
June 10, 2007

# re: GridView and CommandArguments


Rob Kent
June 25, 2007

# re: GridView and CommandArguments

Datakeys depends on Viewstate, which I normally turn off. If you use an ObjectDataSource you can't use sorting because it stops the cache working; if you do use cache, the key to invalidate it is at application scope so no good for personalized lists unless you prepend a session id.

If you have Viewstate turned off on the Gridview, you won't get server-side events unless you rebind your grid manually during Init, which means that you have to have the original list contents in the same sort order (ie, not refreshed from the database, which may have changed). So this implies some kind of persistent store for the list datasource (I tend to use Session).

None of this is very nice or simple, which is not the impression you'd get from reading the MS documentation about databound list controls.

Nirmala
June 29, 2007

# re: GridView and CommandArguments

I am displaying the data in a grid view and added two footers to display summary. First Displaying the group summary and other footer showing the total summary. I am not able to access the second footer cell text by using

For each (GridViewRow orows in Gridview1.Rows)

and Gridview1.FooterRow- this returns only the first footer cells text details. How Do I access the extra footer details????


Please Help!!

Michael Teper
July 23, 2007

# re: GridView and CommandArguments

Check the Row.RowType enum first -- if not DataRow, bail. That way won't have any ambiguity about whether or not to expect the Pk in CommandArgument.

Slay
September 07, 2007

# re: GridView and CommandArguments

I have a gridView, which is contained in the ItemTemplate of a Repeater. I am doing exactly the thing you describe with the template field, set the commandName in makup, and on RowDataBound, I set the command argument. The problem is that the RowCommand event never appears to fire, I have set a breakpoint in the handler which is never reached, no matter what button I click. I have been fighting with this for half a day. Any ideas?

raheema
May 03, 2008

# re: GridView and CommandArguments

hai...

will u plz clear my doubt...i already wasted lot of hours to try to get solution for my below doubt...

how to access the cell values of a gridview in a row command event which fires when i click the button control in teplate field of the control..

i already use command argument to get the index..but it shows input string are not in correct format...so will anybody plz clear my doubt...

thanks in advance...

raheema

James
September 12, 2008

# re: GridView and CommandArguments

Sooo simple.. thanks a bunch! There seems to be a couple ways to handle nested gridviews, I like using this method to hide the data.

Cheers!

Cathal McHale
January 15, 2009

# re: GridView and CommandArguments

I just experienced a weird error, the outcome of which others may find useful.
I'm using similar code to Rick for a template column / LinkButton with CommandName and Arg.
I googled quite a few articles that were experience an empty string being returned for the command arg with markup similar to CommandArgument='<%# Eval("PK") %>'.

It turns out this problem was because I was debugging and simply refreshing (as I was only changing the ASP). Turns out that wasn't good enough. A recompile was required. I found this strange, and was baffled more by the fact that when I changed the markup to CommandArgument="9", then 9 was passed to the row-command event without a recompile!

ghkishore
March 06, 2009

# re: GridView and CommandArguments

hi, i want to pass two commandArguments from linkbutton of a gridview and should assign sessions seperately and redirect to another page on click of the link button,, will be grateful if any help.. Thanks in advance

Matt
August 13, 2009

# re: GridView and CommandArguments

Thanks Rick, this was helpful. I also find datakeys very helpful, instead of using commandarguments or Eval.

Matt

praveen
February 16, 2010

# GridView and CommandArguments

OnRowUpdated="Grid_UpdateCommand"


what is the argument for this command?

Mike
March 02, 2010

# re: GridView and CommandArguments

In case anybody reading this is trying to retrieve the row index, and finding that when using an TemplateField, it might be useful for them to know you can specify the CommandArgument in the Markup / aspx page to the following in order to retrieve it at the RowCommand event:-

CommandArgument='<%# Container.DataItemIndex %>'


For example:-

<ItemTemplate>
  <asp:ImageButton runat="server" ID="btnExpandRow"
    CausesValidation="false" 
    CommandName="Select"
    CommandArgument='<%# Container.DataItemIndex %>'
    ImageUrl="~/images/bullet_arrow_right.gif"
    Text="Click here to view details"
    PostBackUrl='<%# GetAppRelativePageUrl() %>' />
</ItemTemplate>

Wayne John
April 15, 2010

# re: GridView and CommandArguments

Thanks for writing this up, saved me some time. I can't believe it's so complicated to add a pk to command arg...

Akshat
June 30, 2010

# re: GridView and CommandArguments

In CommandArgument property assign comma separated arguments.
Like this
CommandArgument='&lt;%#Eval("carid") + ","+Eval("year") %&gt;'

protected void rptrCart_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
{
ImageButton objibtnEdit = (ImageButton)e.Item.FindControl("ibtnEdit");
string ShipmentIndex = ((CartItem)e.Item.DataItem).SrNo;
string Mode = ((((CartItem)e.Item.DataItem).Mode) == "Export" ? Convert.ToInt32(ShipmentMode.Export).ToString() : Convert.ToInt32(ShipmentMode.Import).ToString());
objibtnEdit.CommandArgument = Mode + "|" + ShipmentIndex;
}
}
</PRE>
And access as

protected void rptrCart_ItemCommand(object source, RepeaterCommandEventArgs e)
{
if (e.CommandName == "Edit")
{
string Mode = e.CommandArgument.ToString().Split('|')[0];
string ShipmentIndex = e.CommandArgument.ToString().Split('|')[1] ;
Session[SessionTypes.SelectedBookingCartItemId] = ShipmentIndex;
Response.Redirect("ShipmentSummary.aspx?Mode=" + Mode);
}
}</PRE>
<P><BR><BR>&nbsp;</P>

vinod
November 16, 2011

# re: GridView and CommandArguments

hi,
I have created Template Field in Grid View , contains a Label for Registration id,ImageButton, and three link Button labeled 1, 2, 3 respectively.
The datbase table columns are Regid, MainPhoto,optionalphoto1,optionalphoto2.
Label populated with Eval(regi...)
ImageButton's Imageurl is loaded with "MainPhoto" url from database(using eval function).
after loading, when i click linkbutton 2(ie Text="2"), the image Button should change the imageurl to optionalphoto1, this will happen for link button 3.
i tried a lot but i failed
Email :vinodtvc@yahoo.co.in

<asp:ImageButton ID="ImageButton6" runat="server" Height="145px"
ImageAlign="Middle" ImageUrl='<%#Bind("MainImagePath")%>' Width="120px" />
</div>
<div class="serleft" style="vertical-align:top">
Photo
<asp:LinkButton OnClick ="LinkButton2_Click" ID="LinkButton2" runat="server" Text="1" PostBackUrl='<%#"~/UserPages/frmQuickSearch.aspx?RegID="+Eval("RegID")%>'></asp:LinkButton>
<asp:LinkButton OnClick ="LinkButton3_Click" ID="LinkButton3" runat="server" Text="2" PostBackUrl='<%#"~/UserPages/frmQuickSearch.aspx?RegID="+Eval("RegID")%>'></asp:LinkButton>
<asp:LinkButton OnClick ="LinkButton4_Click" ID="LinkButton4" runat="server" Text="3" PostBackUrl='<%#"~/UserPages/frmQuickSearch.aspx?RegID="+Eval("RegID")%>'></asp:LinkButton></div>

DonPec
November 16, 2011

# re: GridView and CommandArguments

This save me 2 days stucked!!! Thanks a lot!

I combine your original post with the suggestion of Donnie cause I use paged gridViews and my only need was to know the DataKey Value to extract all data from my SqlDataSource...this is my code:
protected void GridViewMonitor_RowCommand(object sender, GridViewCommandEventArgs e)
        {
            GridView gv = (GridView)sender;
            WebControl wc = e.CommandSource as WebControl;
            if (wc.ID == "LinkButtonFinalizaEvnt")
            {                
                LoginView lv = wc.NamingContainer as LoginView;
                GridViewRow row = lv.NamingContainer as GridViewRow;                
                int reg = (int)gv.DataKeys[row.RowIndex].Value;
          

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024