I'm busy working on a project with a client and today as I uploaded a new test front end page a couple of people asked me how I implemented this list display:
The exact question was: What control is that? <s>
Come again? No control unfortunately and it's actually a fairly simple UI layout, although having done this sort of thing a few times before certainly makes it a lot easier (what doesn't?). So it's not actually a control, but since a couple of people asked I thought I describe how it's put together. What you see above is merely a databound repeater with some 'wrapping' around it. But the interesting thing - and what was asked about - is the scrolling and grouping implemented in the display. The idea of this particular Repeater is that it displays in a fixed size display, so no matter how many items are displayed the content stays contained in a fixed area.
This is a common scenario especially in AJAX enabled applications where you form layout needs to stay visible and should not resize itself so the application doesn't 'bounce' around constantly. The effect is more like somehting that you would maybe achieve with a ListView in a Windows Form application, which I am pretty fond of in Win apps.
So what you see above is nothing fancy - it's simply a Repeater wrapped in several <div> tags. What's there is essentially an outer frame that provides the blue bordered bounds, the header div, the 'toolbar' div and a scrolling div that is the container for the actual repeater and finally the 'status bar' which is just another <div>.
<div id="linkList" class="blackborder"
style="Width:700px;text-align:left">
<div Class="gridheader">Available Requests</div>
<div class="toolbarcontainer">
<a href="javascript:EditLink(null);" class="hoverbutton" accesskey="a">
<asp:Image ID="Image1" runat="server" ImageUrl="~/images/edit.gif" BorderWidth="0px" /> Add New Link</a>
</div>
<div id="ScrollList"
style="height: 400px; overflow-y: scroll;border-top: solid 1px silver;padding-left: 5px;text-align:left;">
<asp:Repeater runat="server" ID="repWebRequestList"
onitemcommand="repWebRequestList_ItemCommand" >
<ItemTemplate>
<%# this.RenderGroup(Eval("Group") as string) %>
<div class="linkitem">
<div style="float:right"
<ww:wwWebImageLink runat="server" class="hoverbutton"
ImageUrl="~/images/pdf.gif"
NavigateUrl='<%# Eval("Url") %>'
onclick="AddClientKey(this.parentNode,true);return false;"
ToolTip="Show Pdf Report"
Visible='<%# (bool) Eval("AllowPdf") && (bool) Eval("Active") %>' />
<ww:wwWebImageLink runat="server" ImageUrl="~/images/edit.gif"
class="hoverbutton"
onclick='<%# Eval("id","EditLink(\"{0}\");return false;") %>' />
<asp:ImageButton ID="ImageButton1" runat="server"
ImageUrl="~/images/remove.gif"
CommandName="DeleteRequest"
CommandArgument='<%# Eval("Id") %>'
class="hoverbutton"/>
<small><%# Eval("Updated","{0:MM-dd-yy}") %></small>
</div>
<asp:HyperLink ID="HyperLink1" runat="server"
NavigateUrl='<%# ((bool) Eval("Active")) ? Eval("Url") : "" %>'
Text='<%# Eval("Description") %>'
onclick="AddClientKey(this);return false;"
ToolTip='<%# Eval("DetailDescription") %>'>
</asp:HyperLink>
</div>
</ItemTemplate>
</asp:Repeater>
</div>
<div id="divStatus" class="toolbarcontainer">Ready</div>
</div>
With an associated set of styles:
<style type="text/css">
.hoverbutton { text-decoration:none;padding-top:33px; padding: 2px; border: solid 1px whitesmoke; font-size: 8pt;}
.hoverbutton:hover { background: white url(images/menuhighlight.png); border: solid 1px silver; }
.groupheader { background: steelblue; color: White; padding: 4px; margin-top: 10px; margin-bottom: 5px; font-weight: bold;}
.toolbarcontainer { background:#eeeeee;border: solid 1px silver;padding: 5px; }
.linkitem, linkitemdisabled { border-bottom: dotted 1px teal;padding:10px;padding; }
.linkitemdisabled { color: #eeeeee; text-decoration: none; }
small { font-size: 8pt; }
</style>
As you can see this layout is fairly simple to accomplish. It's primarily one div tag after another that give the 'control' feel to this layout. The key component is the scrollable area which is accomplished by applying overflow-y: scroll to the div tag that houses the actual repeater along with a fixed size for this container. The fixed size with overflow-y: scroll provides the scrolling, control like interface.
The actual item layout is also pretty simple using only div tags with float:right to force the icons and text in another div tag to float to the right side while the link simply renders as text. In lieu of tables float: right is your friend if you want multiple div tags to align themselves properly. One key thing to remember with float as it is with align and images is that you want to do any right floating before any other content you display - otherwise the floated text will end up below the text or other block content.
Ok, so nothing really new here but neat in that it's actually easy to accomplish I think <g>.
Grouping
A few weeks back I took a look at the Orcas ListView control and I noticed that it included a feature for grouping. Unfortunately I actually couldn't figure out how to use the grouping feature on it, but it got me to thinking that if you're using a Repeater (or said ListView which basically an über-Repeater) it's pretty easy to do grouping assuming you have data that is properly sorted for grouping to start with. Grouping also makes a lot of sense in displays/controls like the above that don't necessarily have any sort of paging behavior built in. Grouping helps break up the content.
So in the sample above databinding actually occurs against a list of objects loaded from an XML file. The list is sorted by group, sort order and alphabetically, so the items are already in the correct order when the Repeater is databound.
Now repeaters don't support grouping, so you have to do this on your own, but it's pretty easy to do. In the repeater above the following data expression is responsible for the grouping:
<%# this.RenderGroup(Eval("Group") as string) %>
The expression calls a method on the form to render an extra div tag. The code for this simple method looks like this:
string LastGroup = "@#@~";
protected string RenderGroup(string Group)
{
if (Group == this.LastGroup)
return "";
// *** Group has changed
this.LastGroup = Group;
return "<div class='groupheader'>" + Group + "</div>";
}
This code gets called for every item and it simply checks to see if the group has changed. If it hasn't nothing is returned otherwise a simple div tag is returned as a string to embed into the markup. I chose to send back the HTML but I suppose you could also return true or false and conditionally render a control in the Repeater which would make the CSS Nazis happier <s>.
ScrollPosition
So this particular page uses both AJAX code to pop up editing information but it also uses post backs to write data and add new links. Displaying editing information is done with AJAX the actual submit is done with a postback because the list which makes up 90% of the page markup needs to be redrawn in order to allow for sort order and new entries anyway (yes it can be done with AJAX but it's actually easier to do without).
So when an item is saved or a new item added the list is redrawn, but for the user experience it certainly would be nice to keep the scroll position of the container intact. There are a number of ways to do this (including ASP.NET's own massive page scroll position code which you want to avoid if you can <s>). To save the scroll position I use a hidden field which is assigned in client script when an item is loaded for editing:
<asp:HiddenField runat="server" ID="ListScrollPos" />
and
function EditLink_Callback(result)
{
EditWindow.show();
EditWindow.centerInClient();
...
$('LinkId').value = result.Id;
$('ListScrollPos').value = $('ScrollList').scrollTop;
// *** Hold on to the link
request = result;
}
The script reads the scrollTop property of the scrollable container and this value is then posted back to the page if and when it's submitted.
When the page posts back and saves the scroll position is posted back like this in Page_Load (or anywhere really):
if (Scroll > 0)
{
this.ClientScript.RegisterStartupScript(this.GetType(), "ListScrollPos",
"$('ScrollList').scrollTop = " + Scroll.ToString() + ";", true);
}
Now when the page posts back the scroll position will be maintained.
FWIW, I noticed today that FireFox must have recently done something to its rendering engine - in FireFox these postbacks with scroll position intact don't even blink the UI. Even though it's not an AJAX request it actually looks like it due to whatever adaptive rendering they are doing to change only what's changed in the document (I assume). That's pretty cool - I hadn't noticed this until today. In IE the same operation is acceptable but you definitely notice the blinking.
Repeat after me
There you have it. The concept is pretty simple and it works well - it's easy to get your head around, but it does look a heck of a lot nicer than popping a long, never ending list into a page. A scrollable view combined with group headers makes for a nice user interface addition especially if you're dealing with AJAX interfaces.
It'd be nice to wrap some of this up into a control at some point, but I haven't gotten around to it. If I do a few more of these I might <s>. But maybe that's not really worth the effort - after all the layout is pretty simple - with a few key CSS tags in a theme it gets pretty easy to create this layout quickly. If you know what to do the 'wrapping' around the edges can be accomplished pretty quickly.
Other Posts you might also like