Monday, July 2, 2012

Gridview paging, sorting, rowcommand and other stuffs

Hack the Gibson
A Gridview is an organized control that allows you to display bound or unbound data in a table like fashion.  Each column is a field and each row is a record.  Think of a GridView as a DataGrid 2.0.  It offers you the ability to customize items such as header, footer, paging.. etc.. and uses a different model for events.
Example -
This is a typical GridView in that it is displayed in a fashion that resembles a table.  Below is also a GridView, but it has been templated to look somewhat different
Example -
The fields in the above example are templated like so:
Where the subject is actually a hyperlink that will (through JQuery) show a pop-up animation and display the relevant data (thus, the body).
These are great examples of how you can take the same data and display them in two totally different ways using the same control (GridView).  Lets start dissecting the code that we used to achieve the first GridView.

<asp:GridView ID="gdvAppLinks" runat="server" AutoGenerateColumns="false"BorderWidth="0" ShowHeader="true" EmptyDataText="No Links Available"Width="100%" DataKeyNames="launchpad_link_cd" >
<HeaderStyle BackColor="LightGray" ForeColor="Black"  />
<asp:ButtonField Text="Edit" ButtonType="Button" CommandName="LinkEdit" />
<asp:ButtonField Text="Delete" ButtonType="Button" CommandName="LinkDelete" />
<asp:BoundField HeaderText="Description" DataField="launchpad_link_desc" />
<asp:BoundField HeaderText="URL" DataField="launchpad_link" />
<asp:BoundField HeaderText="Category" DataField="launchpad_link_category" />
<asp:BoundField HeaderText="Create Date" DataField="created_dt" DataFormatString="{0:MM/dd/yyyy}" />

There are some important properties we need to identify here.  Frist and foremost is the ID.  Obviously “Best Practices” naming convention (which was invented by MS and no longer followed by them) is that we identify the type of control along with a meaningful name.  AutoGenerateColumns: This property defaulted to true, if not explicitly turned off will cause the GridView to read the column headers from the dataset and create column names (regardless of what you specify in the header tags).

ShowHeader: Default is true, however if set to false there is absolutely nothing you can do to make column headers to show up (short of changing it back to true).

EmptyDataText:  Lets take a scenario where we are using a Grid to display a file listing of the files uploaded in the last 24 hrs.  If there have been no files uploaded this text will be displayed.

This is the text that is displayed when your DataSource returns no data.  The DataSource for the gridview must be completely void of any data.

DataKeyNames: This is the property that identifies your unique record.  For instance it is your key field or a group of fields that identify a record.  For instance it could be ID or perhaps subject, date, from fields (when listing multiple fields, they are comma seperated values).

Now that we have looked through the properties, the only other code that might serve some interest is the HeaderStyle tag.  This identifies the CSS properties of the header (or footer/pager.. etc.. depending on the tag) BEYOND the identified CSS for the Grid.  What this means is that if you identify values here, it will overwrite the general values for the Grid and the uniquely identified values in the that tags will be used.  Just remember if you uniquely write it out, MOST GENERALLY in the .Net Framework it will be the one used (not always however).

One last interesting piece is the DataFormatString.  This will take a value OTHER THAN a string and convert it to the context of the property.  In this case, we are formatting a date value into the American Standard. 


The 0 represents a place marker (for a value).  The MM represents Month, dd Day, and yyyy is a 4 year format (yy is a 2 year format).

This is essential the same as writing:

String.Format("{0:MM/dd/yyyy}", myDateValue)

Now, you started out with a date value, but like in our String.Format example, you end up with a value of type string (Thus, there is no need to convert it with a CType or .ToString() method).  This fits neatly into a Grid.  Very rarely will you simply display a value other than text without format/convert it in some method.

Now that we have the fields defined, we need to bind some data so that we can populate the Grid.  Here’s the issue, anytime you perform an action on the Grid – It performs a postback.  Considering this, we do not simply want to perform our bind in the Page_Load.  This will cause the Grid to repopulate with the original data anytime we perform an event.  Additionally, during times of paging and sorting, we need to REbind the data to the Grid, so we need a method we can call on to accomplish this task.  So in the code behind, let’s create a method to accomplish this task:

Protected Sub bindDataMessagesSource()
     Dim Messages As RidgeWorth.Launchpad.Business.MessageBoardDetails = _
 New RidgeWorth.Launchpad.Business.MessageBoardDetails
        gdvMessageBoard.DataSource = Messages.getAllMessages()
    End Sub

Here you can see that I have created a method called bindDataMessagesSource.  All this does, is goes to a strongly typed namespace and gets the dataset from a stored procedure (removed for brevity).  Now that we have this method, we can call the databind on a “needed” basis.  Since when you first load the page, you will need an initial bind, you need to add it to the Page_Load.  Due to only wanting it in the INITIAL load, we will use a conditional statement to make sure it is not a postback:

Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
        If Not (Page.IsPostBack) Then
        End If
    End Sub

Here you can see that we perform our initial load, but will not do so during any subsequent postbacks.  Remember this, as we will need to account for post backs when performing Grid Events.

If you run the code in debug mode, it will populate the grid.

Now lets look at Row Commands.  This is defined in the HTML as CommandNames in the ButtonFields.  We need to pay special attention to the DataKeyNames.  This is how we get our Primary Key values as you will see:

Protected Sub gdvMessageBoard_RowCommand(ByVal sender As Object,_
 ByVal e As System.Web.UI.WebControls.GridViewCommandEventArgs) _
 Handles gdvMessageBoard.RowCommand
        Dim index As Integer = Convert.ToInt32(e.CommandArgument)
        Dim key As String = gdvMessageBoard.DataKeys(index).Value.ToString()
        If e.CommandName = "LinkEdit" Then
        Dim fetcher As RidgeWorth.Launchpad.Business.MessageBoardDetails = _
 New RidgeWorth.Launchpad.Business.MessageBoardDetails
            fetcher.MessageID = key.ToString()
            txtCreationDate.Text = fetcher.CreateDate
            txtMessageID.Text = fetcher.MessageID
            txtStartDate.Text = fetcher.StartDate
            txtSubject.Text = fetcher.MessageSubject
            'ftbMessage.Text = fetcher.MessageText
            Editor1.Text = fetcher.MessageText
            pnlEditMessage.Visible = True
            pnlMessageGridView.Visible = False

        ElseIf e.CommandName = "LinkDelete" Then
        Dim deleter As RidgeWorth.Launchpad.Business.MessageBoardDetails = _
 New RidgeWorth.Launchpad.Business.MessageBoardDetails
            deleter.MessageID = key.ToString()
        End If
    End Sub

As you can see, you handle CommandName events in the Grid’s Row Command event.  e.CommandArgument is a Grid Row representation of the Row number that we clicked on.  So we convert that into an Int32.  We use this as an index.  Looking at this it was extremely simple to get the row number that was clicked on.  From there, we are able to get our Primary Key from the gdvMessageBoard.DataKeys(index).Value.ToString().  DataKeys(index) = our DataKeyNames for the index that was clicked.  This is the simplistic approach.  If we have more than one DataKeyName defined, we would need to create an array, and then it becomes ordinal in the array for the values.  Once we have our primary key, we are checking to see which command was actually clicked (edit v. delete).  If you notice, I am then using a strongly typed namespace to retrieve the data again from the database.  You could just as easily retrieve the relevant data from the Grid (provided it exists in the Grid) by using the following:

For instance, to fetch the create date, we could read the value directly from the Grid -

txtCreationDate.Text = gdvMessageBoard.Rows(index).Cells(5).ToString()

*** UPDATE *** This should read-
txtCreationDate.Text = gdvMessageBoard.Rows(index).Cells(5).Text().  If you leave it .ToString() it will give you the type as a string.

Using our HTML example above, and counting the columns (starting with zero) it is the 5th column in the Grid (ButtonFields count as columns also).  We start with zero because the Grid sees the columns as arrays (as we all know they all start with the zero position).  We use Rows(Index) to get the specific row that was clicked.  Otherwise we could say Rows(Index + 1) to get the row following the one that was clicked (why we would do this, I’m not sure, but it can be done).

Notice after the LinkDelete command, that I again, call our bind method.  We want to give the user immediate response that the record really was deleted.  So, I call my strongly typed namespace that runs a SP that deletes the record, then I update the Grid.  This creates a partial page post back that would not be covered by our Page_Load, so we must explicitly call the method.

It takes two methods for us to implement paging.  The first method is used to actually increment the pageIndex in the Grid (a property of the Grid) and the second to call our bind method to bind the new dataset to the Grid.  We are actually creating a subset of data when we implement paging.  Luckily, this is all handled by the Grid.

Protected Sub gdvMessageBoard_PageIndexChanging(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.GridViewPageEventArgs) _
 Handles gdvMessageBoard.PageIndexChanging
        gdvMessageBoard.PageIndex = e.NewPageIndex
    End Sub

WOW!! One line of code.  The PageIndexChanging method event is fired when changing pages.  We set our PageIndex to the new Index and rebind the data.  Without the second event, the Grid will not change.  The second event that is fired is PageIndexChanged.  Without it, you won’t even notice that the Page was changed.  Why? Due to the fact that this is where we rebind the data, so -

Protected Sub gdvMessageBoard_PageIndexChanged(ByVal sender As Object, _
 ByVal e As System.EventArgs) Handles gdvMessageBoard.PageIndexChanged
    End Sub

That’s it.  With two lines of code, you have paging.  The Grid handles the rest.  Everything else is handled by the wizard behind the curtain.  Suppose that you want to increment the page by two instead of one, well -

gdvMessageBoard.PageIndex = e.NewPageIndex

would be

gdvMessageBoard.PageIndex = e.NewPageIndex + 1

In the example above, you will notice that I have a Page x of x in the top right corner.  To accomplish this, we need to create a separate method that is called AFTER the bind. 

Private Sub ApplyPageCounter()
        Dim currentPage As Integer = gdvMessages.PageIndex + 1
        Dim totalPages As Integer = gdvMessages.PageCount
        lblPagerCount.Text = String.Format("Page {0} of {1}", _
 currentPage, totalPages)
    End Sub

The Grid gives us everything we need to accomplish this.  We add 1 to the PageIndex because once again this is viewed as an array by the Grid.  Obviously, this populates a Label Control that I have on the form.

One more piece of eye candy is suppose we want to “highlight” the row that the mouse is on.  Even if we do not click the Grid, we can still manipulate the Grid through CSS.  By manipulating the CSS Properties for the row when they are created we can create a “Hover Effect” that does what we are expecting:

Protected Sub gdvMessageBoard_RowCreated(ByVal sender As Object, _
 ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) _
 Handles gdvMessageBoard.RowCreated
        If (e.Row.RowType = DataControlRowType.DataRow) Then
            e.Row.Attributes.Add("OnMouseOver", _
            e.Row.Attributes.Add("OnMouseout", _
        End If
    End Sub

This creates some great effects that is sure to catch anyone’s attention.  You can even go as far as to conditionally apply the code above to say a row that has a creation date prior to 1985.  Additionally, you can color only cells that are a negative value to red, to attract special attention.  It’s actually pretty relenting and limitless.

Our last concern is Sorting.  In order to achieve this, we need to turn AllowSorting to True in the Grid (HTML) property.  Then we need to identify the fields that we want to allow sorting from.  To do this we need to go to the BoundField and set the SortExpession property to the field we want.  So using the example above, we’ll sort on the category:

<asp:BoundField HeaderText="Category" DataField="launchpad_link_category" SortExpression="launchpad_link_category" />

Unlike pagaing, It actually takes one event and two methods to accomplish the task.  You will see that it is not very complicated however:

Protected Sub gdvExceptions_Sorting(ByVal sender As Object, _
 ByVal e As System.Web.UI.WebControls.GridViewSortEventArgs) _
 Handles gdvExceptions.Sorting
        SortField() = e.SortExpression
        If SortDir() = "asc" Then
            SortDir() = "desc"
            SortDir() = "asc"
        End If

    End Sub

Here we are calling the SortField method (which we create) to handle the event.  It is being passed the SortExpression value that we defined in the HTML property of the field clicked.

Public Property SortField As String
            If ViewState("_sortField") = Nothing Then
                ViewState("_sortField") = "launchpad_link_cd"
                Return ViewState("_sortField").ToString()
                Return ViewState("_sortField").ToString()
            End If
        End Get
        Set(ByVal value As String)
            ViewState("_sortField") = value
        End Set
    End Property
    Public Property SortDir As String
            If ViewState("_sortDir") = Nothing Then
                ViewState("_sortDir") = "asc"
                Return ViewState("_sortDir").ToString()
            End If
            Return ViewState("_sortDir").ToString()
        End Get
        Set(ByVal value As String)
            ViewState("_sortDir") = value
        End Set
    End Property

Obviously, our ViewState(“_sortfield” is set to the Primary Key which we named as our DataKeyNames.  Additionally, we will want to set the EnableViewState property of the Grid to True.

Final thing to look at is hiding columns.  Suppose we want to hide a delete ButtonField if they do not have the delete role.  Simple.  We perform our check and if it turns up false then -

gdvExceptions.Columns(0).Visible = True

If the ButtonField happens to be the first column.  Columns(x) – x is the index value of the column. If it’s the second column it would be Columns(1).

Happy .Netting

Hack the Gibson

No comments:

Post a Comment