Thursday, June 24, 2010

Paging with the ASP.NET repeater control

Since ASP.NET does not provide paging for repeaters, and I could not find any material out there that would work in a user control, I decided to create my own custom paging. Most examples I saw used dynamically created controls for paging. As dynamically created controls go, they tend to cause more problems than the problem they solve, especially when you're planning to modify them. Don't blame the coders, wiring up dynamically created controls correctly requires an intricate knowledge of the ASP.NET page life cycle. I however felt that in most cases a simpler version would do, so I opted for statically created paging controls.

Let's say that I have a simple repeater in my user control:
<asp:Repeater ID="Repeater1" runat="server">
<HeaderTemplate>
<ul id="navi">
</HeaderTemplate>
<ItemTemplate>
<li>
<a href='<%#DataBinder.Eval(Container.DataItem, "ArticleURL")%>'>
<%#DataBinder.Eval(Container.DataItem, "Name")%>
</a>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
view raw gistfile1.html hosted with ❤ by GitHub


I'm going to add a placeholder for my paging controls.
<table>
<tr><td>
<asp:PlaceHolder ID="plcPaging" runat="server" >
<asp:LinkButton ID="prevLnk" Text = "Prev" runat="server" OnClick="prev_Click"/>
<asp:Label ID="prevspacer" Text = " " runat="server" />
<asp:LinkButton ID="lnkPage1" Text = "1" runat="server" OnClick="lnk_Click"/>
<asp:Label ID="spacer1" Text = " | " runat="server" />
<asp:LinkButton ID="lnkPage2" Text = "2" runat="server" OnClick="lnk_Click"/>
<asp:Label ID="spacer2" Text = " | " runat="server" />
<asp:LinkButton ID="lnkPage3" Text = "3" runat="server" OnClick="lnk_Click"/>
<asp:Label ID="spacer3" Text = " | " runat="server" />
<asp:LinkButton ID="lnkPage4" Text = "4" runat="server" OnClick="lnk_Click"/>
<asp:Label ID="spacer4" Text = " | " runat="server" />
<asp:LinkButton ID="nextLnk" Text = "Next" runat="server" OnClick="next_Click"/>
</asp:PlaceHolder>
</td></tr>
</table>
view raw gistfile1.html hosted with ❤ by GitHub


Notice how i'm using only 4 link buttons, which might not be adequate for most situations. So the idea is to dynamically change the properties of these link buttons depending on the current page.
Once we have a way to keep track of the current page we are on as well as the total number of data items(or pages), we are well on our way to finishing our control.
To deal with keeping track of the current page and total number of data items, i'm going to use control state here. I could have used Viewstate instead for simplicity but there's a possibility of viewstates being turned off by another developer using my control, and in a critical feature such as paging, it's better to store state in a control state.
Here's the code to make that work:


I'm using a structure to store my state specific properties here. The SaveControlState and LoadControlState methods are used by ASP.NET to serialize and deserialize my structure into control state.
The things remaining to do are to load data and also to format our control dynamically based on the page we are at. Let's look at loading data first:
I initialize page load to fire our data loading method the first time the page is called.

protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
LoadData(itemsPerPage, 0);
}
view raw gistfile1.txt hosted with ❤ by GitHub


The actual loading is done like this:

private void LoadData(int take, int pageSize)
{
//somehow return all the objects you need. could be a datatable instead of a list of objects
List articles = GetAllItems();
//set total item count
RowCount = articles.Count;
var topDocs = (from c in articles select c).Skip(pageSize).Take(take);
PagedDataSource pagedSrc = new PagedDataSource();
pagedSrc.DataSource = topDocs;
pagedSrc.PageSize = itemsPerPage;
pagedSrc.AllowCustomPaging = true;
pagedSrc.AllowPaging = true;
this.Repeater1.DataSource = topDocs;
this.Repeater1.DataBind();
}
view raw gistfile1.cs hosted with ❤ by GitHub


The important thing to note here is that we use a PagedDataSource as our datasource for the repeater, not the data items directly. This is because the repeater control does not support paging out of the box. To enable paging, we need to use the PagedDataSource as an adapter, whose properties such as the size of the page can now be set.

For formatting the output of the page links, I use the following method:
Here n has been set to 4, since we are using 4 linkbuttons in our ascx file. If we wish to add more buttons and spacers (which are to add seperation between the buttons), it would be as simple as adding those controls following the same naming convention as the other buttons, and changing the value of n.

For eg. let's say that we have 7 pages, and are currently in the second page.

The paging would look like " Prev 2 3 4 5 Next"
2 would be disabled. If you look at the ascx file again, you'll notice that the linkbuttons were hardcoded with values 1 to 4. The values have now been dynamically changed.

/// <summary>
/// sets the paging control's properties
/// </summary>
protected void FormatPagingControl()
{
//number of page links
int n = 4;
int lastpage = ((RowCount -1) / itemsPerPage) + 1;
int startPage = CurrentPage;
int lastPageToDisplay = Math.Min(lastpage, startPage + (n-1));
//if we have only page, do not show paging controls
if (RowCount <= itemsPerPage)
{
plcPaging.Visible = false;
return;
}
else plcPaging.Visible = true;
//in the case that current page is not in the first n pages and belongs to last (n-1) pages
//current page will not be the first page link.
int numberOfPagesDisplayed = lastPageToDisplay - startPage + 1;
//make sure we display maximum number of page links as possible.
if(numberOfPagesDisplayed < n)
{
//push start page back
startPage -= (n - numberOfPagesDisplayed);
}
if (startPage < 1)
startPage = 1;
/*
* Now, using startpage and lastPageToDisplay,perform render
* */
//previous and next buttons
prevLnk.Visible = (startPage > 1);
nextLnk.Visible = (lastPageToDisplay < lastpage);
//page links and spacers
for (int i = 1; i <= n; ++i)
{
LinkButton lnk = ((plcPaging.FindControl("lnkPage" + i)) as LinkButton);
Label spacer = ((plcPaging.FindControl("spacer" + i)) as Label);
int displayNum = startPage + i - 1;
//visible if page's number is less than or equal to last page to display
spacer.Visible = lnk.Visible = displayNum <= lastPageToDisplay;
//make final spacer invisible
if (i != 1 && lnk.Visible == false)
((plcPaging.FindControl("spacer" + (i - 1).ToString())) as Label).Visible = false;
//set text
lnk.Text = displayNum.ToString();
//grey out if page represents current page
lnk.Enabled = (CurrentPage != displayNum);
}
}
view raw gistfile1.cs hosted with ❤ by GitHub


The following code performs event handling for the page buttons. If you look at the LoadItemsBasedOnCurrentPage() method, you will find that the method the call to LoadData fetches only items we will require on the current page. This results in a smaller payload of information being sent at each access to a page.
protected void LoadItemsBasedOnCurrentPage()
{
int take = CurrentPage * itemsPerPage;
int skip = CurrentPage == 1 ? 0 : take - itemsPerPage;
LoadData(take, skip);
}
protected void prev_Click(object sender, EventArgs e)
{
LinkButton lnk = sender as LinkButton;
CurrentPage--;
LoadItemsBasedOnCurrentPage();
}
protected void lnk_Click(object sender, EventArgs e)
{
LinkButton lnk = sender as LinkButton;
CurrentPage = int.Parse(lnk.Text);
LoadItemsBasedOnCurrentPage();
}
protected void next_Click(object sender, EventArgs e)
{
LinkButton lnk = sender as LinkButton;
CurrentPage++;
LoadItemsBasedOnCurrentPage();
}
view raw gistfile1.cs hosted with ❤ by GitHub


That's about it. All you need to do is to add you own method to get all the data items, change items per page (which i have deliberately set to the ridiculous value of 2) and bind them however you see fit.

This example, can with a bit of care, be easily extended to support "jump to first, last" or having .. paging feature, where you can jump to the middle of the page list. However, I have not included that for the sake of simplicity.

Happy plagiarizing. Just kidding, you have my blessings.

Wednesday, June 23, 2010

Strip paragraph tags in umbraco properties using C#

Today I was pulling some properties from an umbraco document for use in a user control, and I found to my horror that some of the content still had starting and ending paragraph tags around it. This was of course thanks to the rich text editor,TinyMCE.
Normally, while using xslt one would write the following to remove them quickly.

<umbraco:item field='property' stripParagraph='true'
runat='server'>
Not finding an equivalent function in the umbraco API, i wrote this quick-and-dirty method that works quite well for the task(at least for me). Hope it saves some time for you.


private static string StripParagraphTag(string input)
{
int startPIndex = input.IndexOf("<p>");
//if found a <p> tag
if (startPIndex != -1)
{
//if <p> tag lies in between content, do not strip it.
string preP = input.Substring(0, startPIndex);
for (int i = 0; i < preP.Length; ++i)
{
if (preP[i] != '\r' || preP[i] != '\n')
return input;
}
//if ending of p tag at end, then strip contents before
if (input.LastIndexOf("</p>") == input.Length - 4)
return input.Substring(startPIndex + 3, input.Length - (startPIndex + 3) - 4);
}
return input;
}
view raw gistfile1.cs hosted with ❤ by GitHub