Building View Components For MonoRail
While WebForms and MonoRail share the ability to refactor common UI elements into reusable components, the design decisions that should be considered for each of those is quite different. Build view components for MonoRail is much simpler than building controls for web forms, for one :-)
I talked in the past about the mechanics of implementing View Components, now I want to talk about several best practices that may be useful for developers building view components for MonoRail. For this discussion, I am going to give examples from the grid component for MonoRail, whose implementation can be found here and here. In what seems like the traditional manner for MonoRail code, the grid components comes in two flavors, GridComponent, that contains the basic functionality, and SmartGridComponent, that contains a lot more smarts and assumptions. There is nothing particularily interesting in this decision, it is just a way to split the responsabilities in a clear manner.
View Components are all about the UI, and I am going to talk about general components, not something specific for a single page/use-case. A lot of what I want to cover is about allow easy extensability and re-use of the component, which is not an issue if you are building it for a single use case only.
In order to understand view components, we first need to understand a bit about the view engines in MonoRail. The view engines (broadly) have the following concepts:
- Template / View - will generate the HTML for a request. Similar to an aspx page.
- Sub template / Sub view - is called from a parent template/view in order to handle some of the processing. Similar to an ascx control. However, a sub view is also a fully fledged view, so you can use it to render a page in one scenario, and embed it in another page in a second scenario (quite useful, by the way).
- View Component - responsible to handle the rendering of a piece of HTML, similar to a server control. May contains sections (which can be required or optional).
- Section - Resides inside a view components, allow to pass the view component a template that it can render at will. The best analogy from the web forms world are templates (<ItemTemplate> in a repeater, for instance).
Now that we have covered the concepts, let us move into the implementation. A grid is very common is web applications, and it is a very simple to build. You have a data source, and you simply want to generate the headers and rows tags accordingly.
A rudimentary implementation of that would give us a repeater, which can be as simple as:
public class RepeaterComponent : ViewComponent
{
public override bool SupportsSection(string name)
{
return "item".Equals(name, StringComparison.CurrentCultureIgnoreCase);
}
public override void Initialize()
{
if(Context.HasSection("item")==false)
{
throw new ArgumentException("RepeaterComponent must have an 'item' section");
}
}
public override void Render()
{
IEnumerable source = ComponentParams["source"] as IEnumerable;
if (source == null)
throw new ArgumentException(
"RepeaterComponent must have an enumerable 'source' parameter.");
foreach (object item in source)
{
PropertyBag["item"] = item;
Context.RenderSection("item");
}
}
You can use this view component like this:
<%
component RepeaterComponent, {@source: customers}:
section item:
output item.CompantName
end
end
%>
This should give you an idea about how the interaction between the view and the view component works. We pass arguments to the view component in the declaration, and we pass sections in the body. The view component can then access them via the ComponentParams variable and the Context, respectively. Note the foreach inside the Render() method, we assign the the variable "item" to the PropertyBag, which would expose it to the section, and then we render the section, which can now access the "item" variable that we just assigned.
I would never write such a component, because this is much simpler to understand, and does the same thing:
<%
for item in customers:
output item.CompantName
end
%>
We need the repeater in WebForms, because there is no way to iterate over a parameter, nor easy way to pass data between the markup and the code.
The overall structure of a grid is similar to this:
<table> <!-- table start -->
<tr><!-- header -->
<th>Header 1</th>
<th>Header 2</th>
</tr>
<tr><!-- data rows -->
<td>column 1</td>
<td>column 2</td>
</tr>
</table><!-- table end -->
<!-- pagination -->
<b>first</b> | <b>prev</b> | <b>next</b> | <b>last</b>
I am sure that you already know of this, so why am I boring you with this? Well, in order to build a reusable piece of code, you need to allow customization of this basic structure. While you could build a component that would generate everything except the data rows, that would be of very limit use. Off the top of my head, I want to be able to control cell spacing and padding, the border and padding, the table CSS class, etc...
You can pass parameters to the component to control the rendering of the control, and indeed, this is the way that the GridView is using. On last count, the GridView has 85 properties, most of which can be used to control the output of the GridView. Trying to build that is going to take a long time, and it is going to be a very complex task. That doesn't really fit the MonoRail (or Castle in general) philosophy.
The Reflector output of the GridView goes well beyond 3,500 lines of code. I have built full blown systems that had less lines of code than that. (Just to compare, the entire Rhino Mocks code base now stands at about 7,900 Lines.
Let us take a look at the Render() method of the GridComponent, shall we?
public override void Render()
{
IEnumerable source = ComponentParams["source"] as IEnumerable;
if (source == null)
{
throw new ViewComponentException(
"The grid requires an IEnumerable parameter named 'source' ");
}
ShowStartTable();
ShowHeader(source);
ShowRows(source);
ShowFooter();
ShowEndTable();
IPaginatedPage page = source as IPaginatedPage;
if (page != null)
{
ShowPagination(page);
}
}
We can see that we are following the same structure as above, but with methods name. Remember all the options I wanted to set on the <table> tag alone? How do I handle it with this component? I certainly can't give it up, and I most certainly don't want to start writing > 3,000 lines of code to cover each eventuallity.
Well, it turns out that there is another way to express what I want the <table> tag to look like. Are you ready? Sure that you are ready?
Well, it turns out that I can expression what I want the <table> tag to look like using a brand new concept called HTML. Here is how I can do this:
<%
component SmartGridComponent, {@source: customers}:
section tableStart:
%>
<table cellpadding="2" cellspacing="0" style="border: dashed 2px red">
<%
end
end
%>
This also shows inline HTML inside a section, but this isn't as important as the concept. We express what we want in the native language. We don't need some translation layer in between. This turn out to simplify quite a bit of my life.
How does the ShowStartTable() method looks like?
private void ShowStartTable()
{
if (Context.HasSection("tablestart"))
{
Context.RenderSection("tablestart");
}
else
{
RenderText("<table id='grid'>");
}
}
This approach, sensible defaults with the ease of overriding them, means that it is:
Gaining Some Smarts
Everything I said so far is possible in WebForms, it is just alien to the way Microsoft positioned WebForms. Now let us move from the realms of the obvious to realm of really cool stuff. We now move from talking about GridComponent to talking about SmartGridComponent. SmartGridComponent inherits from GridComponent and handles a lot of the details of renderring the UI. Mostly, it moves the level that we need to handle from the entire grid to single properties of the object. Think about it like turning AutoGenerateColumns to true in the GridView, except that it is not like that at all ;-)
A View Component is free to define what sections it can support, and there is not compile time limitation. Using this tidbit, we can start doing more interesting things with convention over configuration. Let us say that I want to display a list of customers, and I want to customize the header of the customer id column in some manner. I can do it like this:
<%
component SmartGridComponent, {@source: customers}:
section customerIdHeader:
output "<th>Id</th>"
end
end
%>
By specifying a section with the property name postfixed with "Header", I can override the renderring of this column with my own code. This is a simple example of replacing the text, but I could put anything there at all, including complex HTML or calling a sub view or another view compoennt.
Under the same principal, I can override the rendering of the column itself:
<%
component SmartGridComponent, {@source: customers}:
section customerId:
output "<td><b>${item}</b></td>"
end
end
%>
Again, this just shows the customer id in bold, but anything it possible here. Let us see what we need to do to make this happen, shall we?
Here is a small section from the code that is responsible for rendering the headers of the grid:
foreach (PropertyInfo property in this.properties)
{
string overrideSection = property.Name + "Header";
if (Context.HasSection(overrideSection))
{
Context.RenderSection(overrideSection);
continue;
}
RenderText("<th class='grid_header'>");
RenderText(SplitPascalCase(property.Name));
RenderText("</th>");
}
We check the existance of an overriding section, and defer to it if it exists. The same goes for the rendering the column itself:
foreach (PropertyInfo property in properties)
{
if (Context.HasSection(property.Name))
{
PropertyBag["item"] = property.GetValue(item, null);
Context.RenderSection(property.Name);
continue;
}
RenderText("<td>");
object val = property.GetValue(item, null) ?? "";
RenderText(val.ToString());
RenderText("</td>");
}
The technical details are very simple, which is very good, but the power and flexibility that they bring is quite amazing.
The combination of convention over configuration, and the ease of overriding the defaults is a key stregth to building complex UI easily. It is important to note that when we actually build the UI, we never really leave the realm of HTML, what we write is very close to what would be sent to the browser. This give a lot more control over the final output, but not at the expense of having to deal with additional complexity.
I would like to end with a quote (via Avery):
Comments
Nice post - I'm just starting to play with MonoRail and this post gives a great start for component development. One question though; is there any centralized resource where people can register components they think are useful? I'd love to contribute if my team comes up with anything they think is going to be of benefit to the MonoRail community and we'd like to see anything anyone else has put together, but I've yet to find a site that covers this.
Thanks for another interesting post!
Symon.
There is the castle contrib project, which is a place that can be used to aggregate useful components.
This is really cool. How would you implement inline editing with your grid?
Doron
I wouldn't.
Inline editing isn't that hard, but it is very scenario driven. Do you want to do it with Ajax or not, multiply rows or single one, etc.
I would rather keep it until I need it, and I really much prefer to just have an Edit link that pop ups a dialog with the details for edit, rather than do it inline.
Ha! I seriously laughed out loud (and got a very funny look from my g/f) when I read that.
It's funny how we always expect these great "new technologies" to come with their own diluted and unintuitive syntax (possibly the result of some Pavlovian conditioning from dealing with Redmond tech?), yet in the end its so simply solved by going back to good old HTML.
Great write up Ayende... now if I could only find some time to put MonoRail to use on a real project. :)
Is there any way to pass Actions to a component? I've been looking into creating a sortable, filterable grid component in MonoRail but couldn't work out how to pass a sort value to the grid.
Well, that depends on what you are trying to do. You can do this:
component SmartGrid, {@source: customers, @sortOrders: [ Order.Asc(@contactName), Order.Desc(@contactTitle)] }
Can you define what you mean by actions
Adhering to "Simple vs Easy":
This approach, sensible defaults with the ease of overriding them, means that it is:
So you get the best of both worlds: Simple+Easy! Simple usage is a must, but too many developers jo go with what's easy to implement.
I have the same question as Kevin,
I have a PhotoUpload ViewComponent. It will manage uploading a certain number of photos to the ViewComponent. Depending on parameters I pass to the view component, the photos will be handled in different ways.
The question arises from, how would I target a specific Action on the ViewComponent, such as "Upload", and then after the upload is complete (pending no errors), the Render action would be called that would display the photos on the viewcomponent again instead of showing fileupload inputs.
I guess I could do all the processing in the Render method, but that would require ugly if/else when I really want to target a specific action depending on the form submit.
Perhaps I'm trying to get the ViewComponent to do too much processing though?
I would probably split it into two.
The view component will render the file uploads, and submit to a controller, which will select a different view for that part of the screen.
I would think that this is asking too much of a single component, yes.
Comment preview