Extending the GridView — Part II

In Part I, I talked about why we needed to extend the GridView and some of the benefit in doing that. In this post, I will discuss how to extend the standard GridView control to take advantage of binding to the type safe value objects mentioned previously.

2. GridViewEx
To make the binding as generic as possible, there are pretty much two ways. One way is to bind to an object collection of typed value objects. Since all value objects can be cast into objects, we could manipulate them generically. And because value objects are typically consisted of simple types, we could encapsulate all the operations needed for the GridView within the extended GridView itself.

Another solution is to utilize generics in .Net 2.0 and above. One obvious advantage is that the type information is not lost at any point of the object manipulation and thus a lot of small mistakes can be caught at compile time. This approach does have its draw backs. As you will see, because the purpose of generic types are to eliminate casting introduced by object containers, we have to change the extended GridView to accommodate the type of objects we are binding to the list whenever a new generic type is introduced. This of course requires the changing of the extended GridView control itself whenever we add a new generic type and thus might not be as convenient as the pure object oriented approach mentioned previously. However, weighing both the advantages and disadvantages I personally think this approach is more attractive.

So here, I will show you how to extend the GridView to handle generic lists.

GridViewEx inherits from GridView. We override the DataSource property so that we get a chance to inspect it before the databinding actually taking place. This gives us an opportunity to test whether the data source to be bound is empty. We use this information to decide whether to show the pager or not as you will see in a later post.

The initialization routine (OnInit) is overridden to setup some property values and hook up some events so that we provide row editing functionalities.

The RowEditing, PageSelectedIndexChanged and some of the other overridden functions are handled in a pretty standard way. To demonstrate how easy it is to add new functionality to the GridView, I added an event handler for RowDataBound. The purpose of the code for this RowDataBound event is to check if the CheckBox on a particular row is checked and if so, the background color of that row is changed. This behavior is particularly helpful for giving a visual cue to the end user which row of data he/she is currently dealing with.

When each row is created, OnRowCreated method is called, here we determine whether the row being created is the header or not and if so, we add an arrow to indicate the current sorting option depending on how the grid is currently sorted. If the row being created is a data row, we add some nice touch so that the data row becomes highlighted when mouse moves pass it.

We also have to override the OnSorting function to handle our generic lists. This is where we need to add the supported list types (see comments in code). For additional generic list types, we simply need to add three lines for each type of the list. While this might not be as convenient as using generic objects, all the changes are kept in a central place so that adding/removing types should not be an issue.

namespace ExtendedGridView
{
    /// <summary>
    /// Extended GridView
    /// Supports paging/sorting using a customized object data source.
    /// http://www.kerrywong.com/
    /// </summary>
    public class GridViewEx : GridView
    {
        public delegate void DataSourceChangedEventHandler(object sender, bool isEmpty);
        public event DataSourceChangedEventHandler DataSourceChanged;
 
        /// <summary>
        /// A readonly unique ID for the current view.
        /// </summary>
        public string CurrentViewID
        {
            get { return string.Concat("CurrentView_", UniqueID); }
        }
 
        /// <summary>
        /// Current page id when paging is used.
        /// To remember the page index user selected, a session variable is used.
        /// </summary>
        public int CurrentPageID
        {
            get
            {
                if (HttpContext.Current != null && HttpContext.Current.Session != null)
                {
                    if (HttpContext.Current.Session[string.Concat("CurrentPageID_", UniqueID)] == null)
                        return 1;
                    else
                        return int.Parse(HttpContext.Current.Session[string.Concat("CurrentPageID_", UniqueID)].ToString());
                }
                else
                    return 1;
            }
            set
            {
                if (HttpContext.Current != null && HttpContext.Current.Session != null)
                    HttpContext.Current.Session[string.Concat("CurrentPageID_", UniqueID)] = value;
            }
        }
 
        /// <summary>
        /// Overrides the data source property, so that we get a chance to test whether the source
        /// being bound contains data or not.
        ///
        /// This is used to communicate with the pager so that for an empty list no pager is shown.
        /// </summary>
        public override object DataSource
        {
            get
            {
                if (ViewState[CurrentViewID] != null)
                    return ViewState[CurrentViewID];
                else
                    return base.DataSource;
            }
            set
            {
                base.DataSource = value;
                ViewState[CurrentViewID] = value;
 
                if (value == null)
                {
                    if (DataSourceChanged != null)
                        DataSourceChanged(this, true);
                }
                else
                {
                    if (DataSourceChanged != null)
                        DataSourceChanged(this, false);
                }
            }
        }
 
        /// <summary>
        /// Set up event handlers.
        /// </summary>
        protected override void OnInit(System.EventArgs e)
        {
            AllowPaging = true;
            AllowSorting = true;
            this.AutoGenerateColumns = false;
            PageSize = 10;
 
            RowCancelingEdit += new GridViewCancelEditEventHandler(GridViewEx_RowCancelingEdit);
            RowDataBound += new GridViewRowEventHandler(GridViewEx_RowDataBound);
            RowEditing += new GridViewEditEventHandler(GridViewEx_RowEditing);
 
            base.OnInit(e);
        }
 
        protected override void OnPageIndexChanging(GridViewPageEventArgs e)
        {
            PageIndex = e.NewPageIndex;
            CurrentPageID = PageIndex;
 
            if (ViewState[CurrentViewID] != null)
                DataSource = ViewState[CurrentViewID];
 
            DataBind();
        }
 
        /// <summary>
        /// Handles the sort event.
        /// Since the data source is a generic list, we need to cast the list
        /// back to what ever type it is before we can sort. All the list types
        /// need to be handled here (see comments below)
        /// </summary>
        protected override void OnSorting(GridViewSortEventArgs e)
        {
            ViewState["SortExpression"] = e.SortExpression;
 
            if (ViewState[e.SortExpression] == null)
                ViewState[e.SortExpression] = e.SortDirection;
            else
            {
                ViewState[e.SortExpression] = (SortDirection)(1 (int)ViewState[e.SortExpression]);
                e.SortDirection = (SortDirection)ViewState[e.SortExpression];
            }
 
            object o = null;
 
            //This is where more list types can be added.
            if (ViewState[CurrentViewID] is List<ItemVal>)
            {
                List<ItemVal> l = ViewState[CurrentViewID] as List<ItemVal>;
                l.Sort(new GenericComparer<ItemVal>(e.SortDirection, e.SortExpression));
                o = l;
            }
            else
                base.OnSorting(e);
 
            DataSource = o;
            ViewState[CurrentViewID] = o;
            DataBind();
        }
 
        protected override void OnRowCreated(GridViewRowEventArgs e)
        {
            base.OnRowCreated(e);
 
            if (e.Row != null && e.Row.RowType == DataControlRowType.Header)
            {
                foreach (TableCell cell in e.Row.Cells)
                {
                    if (cell.HasControls())
                    {
                        LinkButton button = cell.Controls[0] as LinkButton;
 
                        if (button != null)
                        {
                            Image image = new Image();
 
                            string sortExpression = string.Empty;
                            SortDirection dir = SortDirection.Ascending;
 
                            if (ViewState["SortExpression"] != null)
                            {
                                sortExpression = ViewState["SortExpression"].ToString();
                                dir = (SortDirection)ViewState[sortExpression];
                            }
 
                            if (sortExpression != null && sortExpression == button.CommandArgument)
                            {
                                if (dir == SortDirection.Ascending)
                                    image.ImageUrl = "~/images/sort_asc.gif";
                                else
                                    image.ImageUrl = "~/images/sort_desc.gif";
 
                                cell.Controls.Add(image);
                            }
                        }
                    }
                }
            }
            else if (e.Row.RowType == DataControlRowType.DataRow) //Handles datarow highlighting on mouse move
            {
                e.Row.Attributes.Add("onmouseover", "this.originalstyle=this.style.backgroundColor;this.style.backgroundColor=’lightgreen’");
                e.Row.Attributes.Add("onmouseout", "this.style.backgroundColor=this.originalstyle;");
            }
        }
 
        protected override void OnDataBinding(System.EventArgs e)
        {
            if (CurrentPageID != 1)
                PageIndex = CurrentPageID;
 
            base.OnDataBinding(e);
        }
 
        protected void GridViewEx_RowEditing(object sender, GridViewEditEventArgs e)
        {
            EditIndex = e.NewEditIndex;
            DataSource = ViewState[CurrentViewID];
            DataBind();
        }
 
        protected void GridViewEx_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            foreach (TableCell cell in e.Row.Cells)
            {
                foreach (System.Web.UI.Control c in cell.Controls)
                {
                    if (c is CheckBox)
                    {
                        CheckBox chkBox = c as CheckBox;
 
                        if (chkBox.Checked)
                        {
                            if (e.Row.RowState == DataControlRowState.Alternate)
                                e.Row.BackColor = System.Drawing.Color.LightSteelBlue;
                            else
                                e.Row.BackColor = System.Drawing.Color.AliceBlue;
                        }
                    }
                }
            }
        }
 
        protected void GridViewEx_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
        {
            EditIndex = 1;
            DataSource = ViewState[CurrentViewID];
            DataBind();
        }
 
        public void PagerSelectedIndexChanged(object sender, int pageSize)
        {
            this.PageSize = pageSize;
            DataSource = ViewState[CurrentViewID];
            DataBind();
        }
 
        /// <summary>
        /// Calculate the real index since the actual index returned from the
        /// GridView is the index in relation to the current page. We need the
        /// real index in order to index into the list.
        /// </summary>
        public int Index(int rowIndex)
        {
            return PageIndex * PageSize + rowIndex;
        }
    }
}

Because we are using customized objects as data bound objects, we also need to provider our own comparer so that the grid knows how to sort each column. This task is fairly easy and I modified the implementation from GridViewGuy. One thing you need to pay special attention is that since the comparison is done using property reflections, the property names are by default case sensitive (e.g. if you have a value property named Item, the binding in ASP.Net must be "Item" as well. "item" will give a run time error).  The code for the comparer is as follows:

 

    public class GenericComparer<T>: IComparer<T>
    {
        private SortDirection _direction = SortDirection.Ascending;
        private string _expression = string.Empty;
 
        public SortDirection Direction
        {
            get { return _direction; }
            set { _direction = value; }
        }
 
        public GenericComparer(SortDirection direction, string expression)
        {
            _direction = direction;
            _expression = expression;
        }
 
        public int Compare(T x, T y)
        {
            PropertyInfo propertyInfo = typeof(T).GetProperty(_expression);
            IComparable obj1 = (IComparable)propertyInfo.GetValue(x, null);
            IComparable obj2 = (IComparable)propertyInfo.GetValue(y, null);
 
            if (_direction == SortDirection.Ascending)
                return obj1.CompareTo(obj2);
            else
                return obj2.CompareTo(obj1);
 
        }
    }

See also

Code Download: ExtendingTheGridView.zip

Be Sociable, Share!

Leave a Reply