Creating a Functionally Rich Repeater Control

By Hendrik Swanepoel

Introduction:
  Download C# Source Code

We're all familiar with the bundled databound controls provided with asp.net.  The most frequently used databinding controls are the datagrid and repeater.  The datagrid is used where tabular data needs to be displayed. The repeater is used for any unstructured databinded repeating - even in a horizontal manner.  Both of these controls are very useful, but are sometimes lacking in functionality.

Sometimes a web project can take a while longer due to some complex coding done on datagrids.  I've caught myself on several occasions writing boring, redundant code on interface level, in order to display a datagrid with certain functionality.
When copying certain functionality on different instances of a datagrid in the same project, a problem that arises is maintenance.  When the client decides that he wants a change in the look or functionality you have to go and change several instances in the project - which can be a real handful in large projects.

Certainly you can use inheritance to derive from the datagrid and use that control everywhere?  When you need to make a functionality change or a change in the layout of the control, you can then change it in the derived control only once?  This is a real effective way of developing web applications - using inheritance on your controls.  Try doing this wherever you can, it will save you a huge amount of time in the long run.  But there are cases where this is just not enough.  At least at the moment - ASP.Net 2.0 will address a lot of the problems with databinding, have a look at this post to find out about databinding in asp.net 2.0.







The bundled repeater control in the current version doesn't provide paging and you don't want to use a datagrid every time you want to display repeatable data.  The datagrid is really best for displaying tabular data, but I prefer use a repeater where the data isn't tabular.  Furthermore, the datagrid doesn't specify a built in searching control - herein lies the problem.  You can't derive from the datagrid control and add "sibling" controls to it, meaning on the same level as the datagrid, and not as a child control.  A good custom repeater control isn't limited to having only search functionality, but needs to have an alphabet filter, sorting on the columns and paging capabilities.  Furthermore, the developer needs to have full control over every part in the control, making it adaptable to every situation which possibly arises in the development of the project.  For example, the developer needs to have control on whether the search control and the paging control displays in each instance.

We are going to address all of these issues and provide a custom control which can be adapted easily for different situations.

Solution

Because we need to be able to display any data in our repeater control - even horizontal data, I decided to use a repeater, instead of a datagrid. OK, so we all know how a repeater works.  If you don't, please have a look at this.

Due to the fact that we need to add lots of other modules of functionality to the control, we use a composite control architecture.  This means that we write our own class which derives from the System.Web.UI.Control class, which enables us to maintain it's controls collection.  Think of it as working with XML (and the DOM if you're familiar with it).  When adding a child node to an element, you are actually defining the parent node's definition.  This means that the parent node is composed of the child nodes.  It's the same with our control, we add sub controls to it's controls collection, which means that our control is composed of it's child controls.

The controls in our composite controls will consist of the following:

Functionality Control type
Databinded template control System.Web.UI.WebControls.Repeater We don't want to rewrite repeater logic! We will use this control to bind our data to and provide repeating functionality.
Search filter System.Web.UI.UserControl We have to provide the user with a control which allows filtering on the repeater based on a supplied textual value. The user will also choose the column which the search will be done on
Alphabet filter System.Web.UI.UserControl The user will be able to click on any letter of the alphabet and the list will be filtered on the first column. Any item starting with the clicked letter of the alphabet will be displayed.
Paging control System.Web.UI.UserControl The paging control allows for next/previous style paging and also paging on page numbers.

Code

Seeing that our control needs to be able to be defined like a repeater by means of a template - we need to be able to make it capable of handling template definitions.
We want our control to be familiar to people working with repeaters and datagrids, so what we'll be doing is passing the values set on our control to the underlying repeater control.  Meaning if the person defines the ItemTemplate in our control, we just set the inner repeater's ItemTemplate property with ou property's value.

The same counts for the HeaderTemplate, AlternatingItemTemplate and FooterTemplate. In order for us to make a property template defined, we'll have to mark the property with attributes, signifying to the ASP.Net engine that the inner markup should set the property's value.

The following code is the code that we use to define the ItemTemplate property.

						
//private variable
private ITemplate itemTemplate;    

//public property                        
[ PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(RepeaterItem)) ] 
   public ITemplate ItemTemplate 
{     
    get { return itemTemplate; }     
    set { itemTemplate = value; }  
}

The developer must also be able to bind event handlers to the repeater, meaning we have to do the same here as with the templates and pass the event handlers on to the inner repeater.
Here's an example of the proxy that we create for the OnItemCommand event.

//the private variable the event handler gets stored in
private RepeaterCommandEventHandler onItemCommand;//the public property which the programmer sets
public event RepeaterCommandEventHandler ItemCommand
{ 
    add 
    { 
        onItemCommand += value; 
    } 
    remove
     { 
         onItemCommand -= value; 
     } 
 }     

We have to add our child controls to our control.  I recommend overriding the OnInit event handler, and inserting the controls here.  I found this presents the least problems with state management.
Here's the code of the OnInit Event handler.

protected override void OnInit(EventArgs e)
        {
            base.OnInit (e);

            if(this.allowSearching)
            {
                //first add the alphabet logic
                ra = (RepeaterAlphabet)Page.LoadControl("RepeaterAlphabet.ascx");
                this.Controls.Add(ra);


                //then add the search logic
                rs= (RepeaterSearch)Page.LoadControl("RepeaterSearch.ascx");
                this.Controls.Add(rs);
                rs.Visible=false;            
            }

            // add the repeating stuff
            innerRepeater = new System.Web.UI.WebControls.Repeater();
            innerRepeater.ID = this.ID + "_Repeater";

            // the proxy event handlers' values are set on the repeater
            innerRepeater.ItemCommand += this.onItemCommand;
            innerRepeater.ItemDataBound += this.onItemDataBound;

            // the proxy templates' values are set here
            if(HeaderTemplate != null)
            {
                innerRepeater.HeaderTemplate = HeaderTemplate;
            }
            if(ItemTemplate != null)
            {
                innerRepeater.ItemTemplate = ItemTemplate;
            }
            if(AlternatingItemTemplate != null)
            {
                innerRepeater.AlternatingItemTemplate = AlternatingItemTemplate;
            }
            if(FooterTemplate != null)
            {
                innerRepeater.FooterTemplate = FooterTemplate;
            }
            //add the repeater to our control
            Controls.Add(innerRepeater);


            //add the message that no records were found in the datasource
            lc = new LiteralControl("<font class='repNormaltext'>No records were found</font>");
            lc.Visible=false;
            Controls.Add(lc);
            
            //check if paging is allowed before adding the control
            if(allowPaging)
            {
                //then add the bottom paging logic!!!    
                rb= (RepeaterBottom)Page.LoadControl("RepeaterBottom.ascx");
                this.Controls.Add(rb);

                //check to see whether the item that was clicked is from the paging control
                // if it is change the page to the correct page number
                if(current.Request.Form["__EVENTTARGET"] != null)
                {
                    // use the __EVENTTARGET variable here, because it's a shire fire way of catching 
                    // the correct event
                    // we do it this way, because the paging elements are created dynamically
                    // so with the loading of the controls, the event handlers are overwritten
                    string evTarget = current.Request.Form["__EVENTTARGET"];
                    if(evTarget.IndexOf("GoToPage_") != -1)
                    {
                        int indexLast = evTarget.LastIndexOf("_") + 1;
                        int lengthOfPage =  evTarget.Length  - indexLast;
                        string pageNr = evTarget.Substring(indexLast, lengthOfPage);
                        //call the method to change the page number to the requested one
                        DoPageChange(pageNr);
                    }
                }
            }
        }
    }

OK, so we've loaded our inner controls and set the proxy properties' values on the inner repeater.
Let's have a look at the specifics of our user controls which we load in the OnInit event handler.
First we loaded the RepeaterAlphabet control, and assigned it to the public variable ra.
We define our whole alphabet statically, this works great because you don't have the problems associated with dynamic loading when the controls are created dynamically.

 
<asp:linkbutton id=filterA CausesValidation="False" runat="server" CssClass="repItemLink">A</asp:linkbutton>
 

We then attach the same event handler to all of these controls:

this.filterA.Click += new System.EventHandler(this.setFilter);    

Here are the methods and event handlers that we defined in our code behind class of the alphabet filter control:

//the event handler that sets the filter on the repeater
        //it calls the correct method in our container class where the repeater resides
        private void setFilter(object sender, System.EventArgs e)
        {
            LinkButton lb =    (System.Web.UI.WebControls.LinkButton)sender;
            CustomRepeater ar = (CustomRepeater)this.Parent;
            
            if(ar.rs != null)
            {
                ar.rs.Visible = false;
            }

            ar.DoSearch(ar.rs.searchCols.Items[0].Value, lb.Text, true);

        }


//the user wants to use the search functionality - make it visible
        private void filterList_Click(object sender, System.EventArgs e)
        {
            CustomRepeater cr = (CustomRepeater)this.Parent;
            cr.rs.Visible = true;
        }
        
        //the user wants to remove the filtering, remove any filtering by calling the correct 
        //method in the container class
        private void removeFilter_Click(object sender, System.EventArgs e)
        {
            CustomRepeater ar = (CustomRepeater)this.Parent;
            ar.RemoveFilter();    
        }
Let's have a look at the functionality of the search filter control. Following is the code that we defined in this control's code behind class:
 

//populate the search's dropdown with the columns define in the repeater
        private void Page_Load(object sender, System.EventArgs e)
        {
            if (!IsPostBack)
            {
                populateSearchList();
            }
        }
        
        //adds the listitem to our dropdown for a column in the repeater
        public void AddToSearchList(string val, string text)
        {
            searchCols.Items.Add(new ListItem(text, val));
        }
        //calls the populateSearchList function on the correct control
        private void populateSearchList()
        {
            Repeater r = (Repeater)this.Parent.FindControl(this.Parent.ID + "_Repeater");
            populateSearchList(r);
        }

        //loops through the repeater and finds all the controls which are defined as headers
        // Headers are classes which we defined in order to provide attributes to them which allows for
        // this functionality
        private void populateSearchList(Control p)
        {    
            foreach(Control c in p.Controls)
            {
                string cType = c.GetType().Name;
                if(cType == "RepeaterColumnHeader")
                {
                    string val =  ((RepeaterColumnHeader)c).DataCol;
                    string text = ((RepeaterColumnHeader)c).LinkText;

                    ListItem l = new ListItem(text, val);
                    this.searchCols.Items.Add(l);
                }else if(c.HasControls())
                {
                    populateSearchList(c);
                }
            }
                
        }


//when the user clicks on the search button, we find the container control and 
        // call the correct method on it
        private void doSearch_Click(object sender, System.EventArgs e)
        {
            CustomRepeater cr = (CustomRepeater)this.Parent;
            cr.DoSearch(searchCols.SelectedValue , txtSearchVal.Text, false);
        }

In the paging control we look at how we create the pages dynamically with the  method. We also see how we handle the previous and next button events.


public void SetPageNums()
        {
            //check to see if paging is enabled - if it's not, skip the logic
            if (this.pagePanel.Visible)
            {
                //get a reference to the container control
                CustomRepeater cr = (CustomRepeater)this.Parent;
                int curPage = cr.CurrentPage;

                //only display back/next buttons when it's possible
                if(curPage == 1 )
                {
                    backBtn.Visible=false;
                }
                else
                {
                    backBtn.Visible=true;
                }
                if(curPage == cr.RepSource.PageCount)
                {
                    nextBtn.Visible=false;
                }
                else
                {
                    nextBtn.Visible=true;
                }
    
                //clear the page links
                this.numbersHolder.Controls.Clear();
                //get the number for pages in the container control's data source 
                // create the linkbuttons for each pgae
                for (int i = 1; i <= cr.RepSource.PageCount; i++)
                {
                    LinkButton lb = new LinkButton();
                    lb.CssClass="repItemLink";
                    lb.ID = "GoToPage_" + i;
                    lb.Text = i.ToString();
                    lb.Click += new EventHandler(cr.PageChangeHandler);
                    if (i == cr.CurrentPage)
                    {
                        lb.Style.Add("font-weight", "bold");
                    }
                    this.numbersHolder.Controls.Add(lb);
                    if (i != cr.RepSource.PageCount)
                    {
                        HtmlGenericControl spacer = new HtmlGenericControl();
                        spacer.InnerHtml = "&nbsp;";
                        numbersHolder.Controls.Add(spacer);
                    }
                }
            }
        }


        //call the Navutton function with a negative increment
        private void backBtn_Click(object sender, System.EventArgs e)
        {            
            NavButton(-1);
        }
        //call the Navutton function with a positive increment
        private void nextBtn_Click(object sender, System.EventArgs e)
        {
            NavButton(1);
        }

Back in the container control, let's look at the method used to search the datasource based on a text field and a particular column.


//We do the search based on a textual value on a certain column
        public void DoSearch(string colName, string val, bool alphabet)
        {
            //check to see if the filter has been done via the alphabet control, or via the search control
            if(alphabet)
            {
                this.rs.Visible=false;
            }
            //set the filter value to keep track of it
            this.filterValue = val;
            //set the Field the sort was performed on to keep track of it
            this.SortField = colName;
            
            if(alphabet)
            {
                //an alphabet search was done
                this.DataSource.RowFilter = SortField + " like '" + filterValue.Replace("'", "") + "*'";
            }
            else
            {
                //a  column search was done
                this.DataSource.RowFilter = SortField + " like '*" + filterValue.Replace("'", "") + "*'";
            }
            //rebind the control
            DataBind();
        }

We also have a function which removes any searching and filtering on the control.

     

        public void RemoveFilter()
        {
            this.rs.Visible =false;
            this.DataSource.RowFilter ="";
            DataBind();    
        }

We also need to have a DataBind method on our control, which looks like this:


public override void DataBind() 
        {
            EnsureChildControls();
            //Try default sorting to the first field
            base.DataBind();
            repSource.DataSource = DataSource;

            if(allowPaging)
            {
                //set the values on the datasource
                repSource.PageSize = this.pageSize;
                repSource.AllowPaging=true;
                int newPage = CurrentPage -1;
                
                //check for max
                if(newPage >= repSource.PageCount)
                {
                    newPage = repSource.PageCount -1;
                    CurrentPage = newPage +1;
                }

                //check for min
                if(newPage < 0)
                {
                    newPage =0;
                    CurrentPage = newPage +1;
                }

                repSource.CurrentPageIndex = newPage;

                //add to the viewstate in order to check the previous page
                ViewState.Add(this.ID + "_ViewPage", CurrentPage);
            }

            innerRepeater.DataSource = repSource;
            innerRepeater.DataBind();

            //check to see if the message shoudl be displayed conveying that no recors were found
            if(repSource.Count == 0)
            {
                lc.Visible=true;
            }
            else
            {
                lc.Visible = false;
            }

            //check to see if paging is allowed
            if(rb != null)
            {
                //create the pages in the bottom control
                rb.SetPageNums();
            }
        }

There are a lot of other intricacies involved., but anybody used to working with any sort of databinding will be familiar with them.  I tried to focus only on the exceptional areas of writing a container control with proxy methods and properties to the contained controls.
All the code is present in the download file, and the control can be used as is.

Hendrik lives in South-Africa, has been developing for 4+ years and specializes in the .Net framework. You can subscribe to or view his weblog at http://dotnet.org.za/hendrik