Keep Your Template Logic in the Template
Recently I stumbled upon this blog post. Unfortunately, Jonathan Holland has things completely wrong. First and foremost, the article is only speaking of presentation in all senses. And, in modern complex applications, presentation can easily include a little bit of logic to give it that "stickyness" the marketing types like. Your core, application logic should most definitely not live on ASP.NET web pages. Or preferably not even in the web project at all. But your template logic can very easily live in the template, and, if at all possible, should live more in the ASPX template than in the CodeBehind.
As some of Mr. Holland’s commenters point out, his method is fundamentally flawed to begin with because it still relies upon the rather loosey-goosey FindControl() to work it’s magic. Which is kind of self-defeating as reliance upon that method destroys some of the advantages he touts. But there are some other, deeper, more fatal flaws than reliance on that method:
If you rely upon CodeBehind to do everything, you cannot change anything in the repeater without recompiling and redeploying the entire application, or at least just the main DLL. This means that making a minor text change will result in a complete recycle, dumping any existing sessions and forcing users to wait through ASP.NET’s spin-up cycle. This also means emergency fixes require a developer with Visual Studio and access to the whole project’s source. Whereas I have been known to push in an emergency template-level fix using notepad on the production server in a pinch.
Even at design time, making a small change to output requires a recompilation of the application, completely slowing the most fungible part of the development process down considerably.
All the server controls generate some nasty overhead from several angles. First, they have ViewState. Second, they are server controls so they will be parsed and loaded into the control tree. [EDIT: As Mr. Holland points out below, this is a bit unclear and partially wrong. HtmlControls also do appear in the control tree, but they are alot “lighter” than true WebControls–no event model, no ViewState.]
In my opinion, it is much easier to make sense of the HTML when it lives in an HTML-style template rather than completely disconnected on the server side.
If you are working with a separate design team, they will likely give you HTML for such things anyhow. Why go through the trouble of translating it into compiled server-side code? Moreover, most good designers these days can at least somewhat handle ASPX template-style code—they know not to go into the <%# %> land and generally can "read around" that stuff if need be. The smarter ones can even pick up enough DataBinding syntax to be almost useful.
Now, you might ask—what do I do when I need some conditional logic in the template. I surely must use Codebehind to power this? And, at least for the light sorts of logic one should need in the template, the answer is no, it all can be done inline. Even for heavier sorts of logic, one can make calls directly from the template to deeper application services if the requirements call for it. Let’s use a slightly more complex version of Mr. Holland’s rptrComment tied into a comments class with the following fields:
- Guid Id
- string AuthorName
- Uri AuthorWeb
- string AuthorEmail
- string AuthorIpAddress
- string CommentText
- DateTime CommentTimeStamp
- bool IsSuppressed
Now, insofar as the templating logic goes, your requirements state the following:
- All users shall be able to see the AuthorName, AuthorWeb and CommentText and CommentTimeStamp fields for non-suppressed comments.
- Article owners and administrators shall see the above fields, as well as AuthorEmail and AuthorIpAddress. Furthermore, they shall see all comments, even the suppressed ones.
- Administrators shall have the ability to suppress comments.
Now, one could do this with a lot of server controls and codebehind. Or one could just push all the display-layer stuff into the template and call the back-end classes, like your SecurityHelper, from there:
<asp:Repeater runat="server" ID="CommentsRepeater" DataSource='<%# GetComments %>'> <HeaderTemplate> <ul class="commentcontainer"> </HeaderTemplate> <FooterTemplate> </ul> </FooterTemplate> <ItemTemplate> <li runat="server" visible='<%# ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).IsSuppressed && !SitePoint.Core.Security.SecurityHelper.CanSeeCommentInfo(User) ? false : true %>'> <p><%# string.IsNullOrEmpty(((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).CommentText) ? "No comment . . ." : ((SitePoint.Core.Entities.Content)Container.DataItem).CommentText %></p> <p> By : <a href='<%# ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).AuthorWeb.ToString() %>'><%# ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).AuthorName %></a> on <%# ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).CommentTimeStamp.ToShortDateString() %> at <%# ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).CommentTimeStamp.ToShortTimeString() %></p> <p runat="server" visible='<%# SitePoint.Core.Security.SecurityHelper.CanSeeCommentInfo(User) %>'> <a href='mailto:<%#((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).AuthorEmail %>'><%# ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).AuthorEmail %></a> • IP: <%# ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).AuthorIpAddress %> <asp:LinkButton runat="server" ID="SuppressCommentButton" CommandArgument='<%# ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).Id %>' Text='[Suppress Comment]' Visible='<%# !((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem).IsSuppressed && SitePoint.Core.Security.CanSuppressComment(User) %>' OnClick="DoSuppressComment" /> </p> </li> </ItemTemplate> </asp:Repeater>
Now, at first glance, that looks hideously complex. But it really is not—90% of the DataBinding code is casting your Container’s [the RepeaterItem] DataItem to it’s natural class: ((SitePoint.Core.Entities.Content.ArticleComment)Container.DataItem). Once one looks past this (or just adds a <% @ Reference %> which I am loath to do), it becomes much more digestible—in all reality a simple ASPX template, nothing more nothing less. Furthermore, it is far more malleable than pushing all this template logic in your codebehind as you can tweak, reload, tweak and reload without rebuilding things.
One question you might be asking is "Why do all that casting instead of calling DataBinder.Eval? Well, because, when one knows what class your DataItem exposes, there are more than a few advantages:
- Intellisense generally works with this tactic. I should note here that using this much DataBinding can, in some cases, drive the designer batty and it need not always be listened to.
- DataBinder.Eval is much slower as it uses reflection (see the note on this page).
- We can call methods on our now stongly-typed reference. DataBinder.Eval() only lets us output strings.
A second question you might ask is "why are you using runat=’server’ on some of those HTML tags?" The reason here is that these have become your containers for parts of the post you wish to suppress. First, on the <li> element, we hide the entire thing if the ArticleComment.IsSuppressed and if the user is does not have the right privileges. Later on we added a <p> to contain the administrative functions on a given post.
In the interest of full disclosure, I should point out the one significant issue with this approach: refactoring. Modern tools, such as Resharper and to a lesser extent Visual Studio 2005+, feature some pretty good tools for automating naming and locational changes in code. Because the code in your ASPX templates is not compiled until runtime, these tools often miss this angle. So, if you have a big refactoring job, you will have to touch each of the template files to ensure they are compatible with the back-end still. Of course, if you have a big refactoring job, you will probably be touching much of the front-end code anyhow, so this is likely not that horrible a downside.
Remember kids, template logic in the template is a good thing. And don’t let the evil man tell you otherwise.