Building Reusable UI Components In MonoRail
I have talked recently about the problems with 3rd party controls and I made the statement that most of the time, I would want to do this on my own, since figuring out how to get to where I want with a 3rd party control often takes as much time as developing the functionality myself. Scott Bellware commented on this post, asking how to get this functionality in MonoRail.
Here is a simple implementation of a grid ViewComponent in MonoRail. It supports header / footer (which the GridView doesn't), empty template, alternating rows schemes, and pagination. I feel that this encompass quite a large precentage of the feature that are in the ASP.Net GridView, so let us see how we build it. We start by deriving from ViewComponent:
public class BasicGridComponent : ViewComponent
We then define the sections that we support. If you are coming from Web Forms world, you can think of View Components as Server / User Controls and sections as the equipollent of ITemplate.
static readonly string[] sections = new string[]
{
"header", "footer",
"pagination", "empty",
"item", "alternateItem",
"tablestart", "tableend"
};
public override bool SupportsSection(string name)
{
return Array.IndexOf(sections, name) != -1;
}
Now, let us build the all important Render() method:
public override void Render()
{
IPaginatedPage source = (IPaginatedPage) ComponentParams["source"];
ShowStartTable();
ShowHeader(source);
if(source != null && source.TotalItems>0)
{
ShowRows(source);
}
else
{
ShowEmpty();
}
ShowFooter();
ShowEndTable();
ShowPagination(source);
}
One interesting thing to note here is that we are using IPaginatedPage as our data source, which provides us with most of the paging support out of the box. We are mostly delegating to methods that does a single thing, mostly, they provide default overridable functionality. Here is a sample of such a method:
private void ShowEmpty()
{
if(Context.HasSection("empty"))
{
Context.RenderSection("empty");
}
else
{
RenderText("Grid has not data");
}
}
Most of the other methods are implemented in a similar fashion, we provide a default implementation and we can override it in place by specifying the appropriate section. ShowStartTable, ShowHeader, ShowEmpty and ShowFooter all share the same concepts. ShowPagination share the same idea, but is a bit more complex, because it outputs the entire pagination toolbar, which has a lot of conditionals.
ShowRows, however, is very interesting:
protected virtual void ShowRows(IPaginatedPage source)
{
bool hasAlternate = Context.HasSection("alternateItem");
bool isAlternate = false;
foreach(object item in source)
{
PropertyBag["item"] = item;
if (hasAlternate && isAlternate)
Context.RenderSection("alternateItem");
else
Context.RenderSection("item");
isAlternate = !isAlternate;
}
}
We check if we have an alternateItem section, and render the sections accordingly. The PropertyBag["item"] allows the section to access the current item in a convient fashion.
Here is the code for this View Component:
<?brail
component BasicGridComponent, {"source": contacts}:
section header:
?>
<th>EMail</th>
<th>Phone</th>
<?brail
end
section item:
?>
<tr style="background-color: #fea;">
<td>${item.Email}</td>
<td>${item.Phone}</td>
</tr>
<?brail
end
section alternateItem:
?>
<tr style="background-color: white;">
<td>${item.Email}</td>
<td>${item.Phone}</td>
</tr>
<?brail
end
end
?>
But, you know what, this is too much for me to write. Let us say that I love the AutoGenerateColumns option in the GridView. What would it take to implement it as a ViewComponent? Well, as it turns out, not much, really.
public class GridComponentWithAutoGenerateColumns : BasicGridComponent
{
private PropertyInfo[] properties;
protected override void ShowRows(IPaginatedPage source)
{
if (properties == null)//there are no rows, if this is the case
return;
bool isAlternate = false;
foreach(object item in source)
{
RenderText("<tr>");
foreach(PropertyInfo info in properties)
{
if(isAlternate)
RenderText("<td class='alternateItem'>");
else
RenderText("<td class='item'>");
RenderText(info.GetValue(item,null).ToString());
RenderText("</td>");
isAlternate = !isAlternate;
}
RenderText("</tr>");
}
}
protected override void ShowHeader(IPaginatedPage source)
{
if(source!=null && source.TotalItems>0)
{
IEnumerator enumerator = source.GetEnumerator();
enumerator.MoveNext();
object first = enumerator.Current;
properties = first.GetType().GetProperties();
foreach(PropertyInfo property in this.properties)
{
RenderText("<th>");
RenderText(property.Name);
RenderText("</th>");
}
}
else
{
RenderText("<th>empty grid</th>");
}
}
}
And now, the code for this view component turns into:
<?brail component GridComponentWithAutoGenerateColumns, {"source": contacts} ?>
So, in about 170 lines of code we created a grid components that gives you quite a bit of functionality. It is an basic grid component that we can configure quite liberaly all over the place. It supports paging seamlessly, and with a tiny bit of work, we managed to make it support AutoGenerateColumns in another 30 lines or so. The full code is here, feel free to poke around.
All in all, it took me about half an hour to write this View Component. Just to put this in perspective, I spent more time writing this entry...
Comments
Comment preview