GridView.AlternatingRowStyle -- lacking style

Ran into this issue today in a SharePoint WebPart that was using the SPGridView to display a list of items. However, the underlying issue is with the base System.Web.UI.WebControls.GridView Control and how it handles the various ways you can set style attributes on the rows in the grid. So this information will apply equally in ASP.NET as well as SharePoint GridView controls.

The GridView gives you easy access to the row styles. For example, a common requirement is to have an alternating row style that is slightly different from the normal row to make large lists of data easier on the eye. In the sample code I chose a very easy on the eye shade of green ;-) as a background-color for the alternating row styles. To enable this I have set the following:

Grid.AlternatingRowStyle

   1: Private Const ROW_CSSCLASS As String = "row-style"
   2: Private Const ALTROW_CSSCLASS As String = "alt-row-style"
   3: Private Const SPECIALROW_CSSCLASS As String = "special-row-style"
   4:  
   5: Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
   6:    Dim items As List(Of itemInfo) = GetList()
   7:    gvGrid.AlternatingRowStyle.CssClass = ALTROW_CSSCLASS
   8:    gvGrid.RowStyle.CssClass = ROW_CSSCLASS
   9:    gvGrid.DataSource = items
  10:    AddHandler gvGrid.RowCreated, AddressOf myGrid_RowCreated
  11:    gvGrid.DataBind()
  12: End Sub

Set the background-color in the style sheet in the page

   1: .alt-row-style 
   2: { 
   3:     background-color:Lime; 
   4: }

With this quick setup I have this as my grid:

gridrows

So, that's all both hunky and dory, but what if I need to apply another style to only certain rows? For instance what if my data item indicated which rows were "new" or "unread" and I had a style to be applied to that row, in addition to the style which I applied to the grid. Seems fairly straight-forward, I create a style in the stylesheet and handle the RowCreated event to set CssClass property on the row. For my quick test I will set every row whose id is divisible by 3 to have a bold font.

   1: .special-row-style
   2: {
   3:     font-weight:bold;
   4: }
   1: Private Sub myGrid_RowCreated(ByVal sender As Object, ByVal e As GridViewRowEventArgs)
   2:     If e.Row.RowType = DataControlRowType.DataRow Then           
   3:         Dim item As itemInfo = CType(e.Row.DataItem, itemInfo)
   4:         If item.Id Mod 3 = 0 Then
   5:             e.Row.CssClass = SPECIALROW_CSSCLASS
   6:         End If
   7:     End If
   8: End Sub

However, when I do that what I see is this

gridrowsBroken

So, what is happening is pretty obvious, the css class name we are setting in the RowCreated event is overriding the top-level class name set at the grid level. Stepping into the Microsoft source symbols on the System.Web.dll we see this code in the Style class (Style.cs) that is called to merge the style defined on the Row, which is the instance of the Style class being called, with the top-level style defined at the grid level, which is the passed in Style parameter "s". So the class name defined closest to the metal (the row) wins in all cases.

   1: /// <devdoc> 
   2:  /// Copies non-blank elements from the specified style, 
   3:  /// but will not overwrite any existing style elements. 
   4:  /// </devdoc> 
   5:  public virtual void MergeWith(Style s) { 
   6: ...
   7: if (s.IsSet(PROP_CSSCLASS) && !this.IsSet(PROP_CSSCLASS)) 
   8:              this.CssClass = s.CssClass; 

While this makes good sense in an object model it is not how I like to think about CSS, kind of puts a wet towel on the whole "cascading" thing. What I really want to do is be able to define an additional class name that will be applied to the row based on data and logic etc. So my next step is to start investigating the RowState of the row I am dealing with and then concatenating class names in the RowCreated event something like this:

   1: Private Sub myGrid_RowCreated2(ByVal sender As Object, ByVal e As GridViewRowEventArgs)
   2:     If e.Row.RowType = DataControlRowType.DataRow Then
   3:         Debug.Print("create: {0}", e.Row.CssClass)
   4:         Dim rowClass As String = ROW_CSSCLASS
   5:         If e.Row.RowState = DataControlRowState.Alternate Then
   6:             rowClass = ALTROW_CSSCLASS
   7:         End If
   8:         Dim item As itemInfo = CType(e.Row.DataItem, itemInfo)
   9:         If (item.Id / 3) = Math.Floor(item.Id / 3) Then
  10:             e.Row.CssClass = String.Format("{0} {1}", SPECIALROW_CSSCLASS, rowClass)
  11:         End If
  12:     End If
  13:  
  14: End Sub

This works, makes the grid look like I wanted it to look. Alternating rows in correct style and every third row in bold.

gridrowsFixed

What I don't like about this solution though, is that the RowCreated code needs to know all the styles that are being set "above its head" so that it can preserve them as it modifies the styles on the individual rows. This "code smell" becomes more apparent when dealing with the SPGridView in SharePoint. This derivative of the GridView sets the AlternatingRowStyle in its constructor like so.

   1: public SPGridView() 
   2: { 
   3:     this.groupMenu = new SPMenuField(); 
   4:     this.m_LowestDataItemIndex = -1; 
   5:     this.m_HighestDataItemIndex = -1; 
   6:     this.EmptyDataText = SPHttpUtility.HtmlEncode(SPResource.GetString("SMGenericEmptyMessage", new object[0])); 
   7:     base.AlternatingRowStyle.CssClass = "ms-alternating"; 
   8:     this.GridLines = GridLines.None; 
   9: }              

So, in SharePoint at least, the client code that is actually modifying the row in the RowCreated event handler doesn't have direct knowledge of the class name being applied by the grid control and has to cross some encapsulation lines to manufacture the desired results. So, once the problem was better defined, I moved the logic of concatenating CSS class names into an extension method defined on the GridViewRow object.

   1: <Extension()> _
   2: Sub AppendRowCssClass(ByVal row As GridViewRow, ByVal newCssClass As String, ByVal parentGrid As GridView)
   3:     'only deal with datarows
   4:     If row.RowType = DataControlRowType.DataRow Then
   5:         'capture top-level cssclass settings for the different row states
   6:         Dim topLevelCssClass As String = String.Empty
   7:         Select Case row.RowState
   8:             Case DataControlRowState.Normal
   9:                 topLevelCssClass = parentGrid.RowStyle.CssClass
  10:             Case DataControlRowState.Alternate
  11:                 topLevelCssClass = parentGrid.AlternatingRowStyle.CssClass
  12:             Case DataControlRowState.Selected
  13:                 topLevelCssClass = parentGrid.SelectedRowStyle.CssClass
  14:             Case DataControlRowState.Edit
  15:                 topLevelCssClass = parentGrid.EditRowStyle.CssClass
  16:         End Select
  17:         'now build a new cssClass name with both settings
  18:         row.CssClass = String.Format("{0} {1}", topLevelCssClass, newCssClass)
  19:     End If
  20: End Sub

This keeps the client code cleaner and insulated from any changes to the top-level class names. And allows me to add a new class name to a row like so:

   1: If (item.Id / 3) = Math.Floor(item.Id / 3) Then
   2:     e.Row.AppendRowCssClass(SPECIALROW_CSSCLASS, gvGrid)
   3: End If

I have uploaded the complete code sample here

 

© Copyright 2009 - Andreas Zenker

2 Comments

  • Great exmaple but when I open the example I get the following error:

    Error1 Could not load type 'Sandbox.gridStyle'.

    Am I missing something?

  • Richard,
    The code sample was not a complete project, so the .aspx page that is included in the sample will not work with a different project name (=namespace). So, in your gridStyle.aspx page update the Inherits="Sandbox.gridStyle" to match your project name/namespace. i.e. Inherits="MyProjectname.gridStyle"
    That should do it.
    Andy

Comments have been disabled for this content.