|
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 = " ";
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 |