My Requirements For A Proper Page Design / Rendering
Peter Bell has been blogging lately about page views and peripheral modules and it's got me thinking about what's required for a page to properly function. So far, these are the simple laws that I have come up with:
Law One: A page must NOT do any extra processing!. (This law can be bent, but make it minimal). Just as we don't return unused columns from a database, a page should never process any data or retrieve any data whose eventual display has not yet been decided. For example, if a page is not 100% sure that a "related items" module will be displayed, then those related items should not be pulled from the database.
Law Two: Any code that produces page output (HTML) should allow the ColdFusion CFFlush tag to function normally. The ONLY exceptions to this is a page that returns output not directly used (as in AJAX or for a web service) or a chunk of content that is being "buffered" for later manipulation (such as stripping out extra white space before being flushed to the browser).
That's it. That's all that is required for a page to function. Of course, there are other things that can be done make sure a page runs optimally, however, without the above two features, the page, in my opinion, is not functioning properly.
Let's examine a scenario: the classic data form that a user fills out and submits back to the server. The form processing can result in two action; result one, the form data is valid, used, and the user is CFLocated to another page. Result two, the form data is not valid and the page re-renders the form allowing the user to update their data.
In this scenario, the outcome of "which page to eventually render," hinges singularly on whether or not the form data is valid. This means that until the form has finished processing, no modules on the page are destined to be displayed. According to my first law, that means that until the form has finished processing, no other data for the page can be processed or retrieved (unless it is required, such as for security and page flow).
Now, let's take a look at Law Two, the CFFlush requirement. CFFlush is a powerful little tag that allows page performance and rendering to be optimized for the end user's perception. This has a huge impact on the way page modules are strung together. Some frameworks have a tendency to store module content in variables and the render pages from a top-down, tail-ended bulk rendering approach (rendering a page template that then includes pre-processed module output). This clearly breaks Law Two. For page to render properly, the page must be rendered in top-down order AND in a streaming fashion, not in tail-ended bulk rendering. This will allow CFFlush to be used without it affecting the rest of the page processing.
The requirement of CFFlush has a different type of impact: the late phase exception. Image you have a page that has a bunch of modules, the middle of which is some sort of item detail. Now imagine, someone requests a item via an invalid identifier. As a result, the page must perform a CFLocation to a different page - we cannot display a detail page without the associated item (perhaps we need to CFLocate to a list page or a search form).
To accommodate this, no content can be flushed to the browser before the possibility of a late phase exception. This means that most all data retrieval / validation must be done before any page rendering has taken place. BUT, this also touches on Law One, no excess data retrieval or processing. This forces all modules that might create late phase exceptions to be processed first. This means that all peripheral modules MUST be processed AFTER primary modules that have absolute data requirements (regardless of physical ordering on the rendered page).
This presents us with a very tricky situation! We cannot process a page's data in a purely top-down fashion (might violate Law One). This forces us to select a Primary Action for any given page - an action, whose data requirements dictate the way in which a page is rendered (or ultimately not rendered). Of course, this law must be bent a bit; if you have multiple pieces of data that are required, you cannot always get them at the same time and therefore a certain amount of extra data retrieval must be performed.
So how do we handle all of this? I do not have a great solution, but I have one that I use rather successfully. It requires that each page have a primary action or function. The data for that page module is processed. Right before that module is rendered, I render the top of the page (since we know the page WILL be rendered at this point). Then right after the main module is rendered, I render the bottom of the page. You probably recognize this methodology as the page header / footer methodology:
<!---
Pre page processing is done. Also all data for thie main
module is retrieved and processed. This might be done at
the top of a file or in a separate action file or in
controllers. This is based on your framework.
--->
<!--- // .... // ---->
<!--- Include page header. --->
<cfinclude template="./header.cfm" />
<p>
This is the main module for this page.
</p>
<!--- Include page footer. --->
<cfinclude template="./footer.cfm" />
This approach satisfies both laws. No extra processing is done since the main page module is processed first. CFFlush will continue to work as the content is rendered in a top-down, streaming fashion (header, then main module, then footer).
What this approach is not very satisfactory at is the decision regarding which of the other page modules will be rendered and in what way they will be rendered. Take for instance, a "Related Items" modules. In the above approach, there would have to be some sort of conditional in the footer.cfm ColdFusion template that includes the related items module and passes in the appropriate data. This can make for a hard to read and hard to maintain page template.
The trick is to be able to make the page rendering "path" more dynamic. I have fooled around with a page object to which you can then add page module file paths (that the header and footer templates will CFInclude) and this has been somewhat satisfactory. I am still not fully satisfied with it. Nothing I have tried feels wholey natural yet.
This method, however, is a bit hard coded. If you wanted to make your templates more dynamic... I have NO idea. If you have a good solution that is dynamic and satisfies both of the above laws please let me know!
Want to use code from this post? Check out the license.
Reader Comments
Hey Ben,
There is a fundamental limitation when trying to solve this problem. While it is nice for pages to be able to be flushed, it limits the flexibility of your application and on the whole I've decided not to support this.
For instance, what happens if your header should say hello #FirstName# #LastName# or Hello Guest if you're a registered user? With a flushed approach when someone is logging in you have already posted a "hello guest" before the user authentication status is known. Equally if you want to display number of items in cart in topbar, you can't flush content, so I've decided on balance to do all processing first and then to render the entire page. Any other approach as a general solution limits the amount of dynamic content that can be displayed outside of your main content area.
Ben
So, (as usual) your post got me thinking. Here is where I'm going with this. Any thoughts appreciated!
http://www.pbell.com/index.cfm/2006/12/8/Is-it-Worth-Being-Able-to-CF-Flush
FWIW, this'll all be built into LightBase when I finally finish it!
Peter,
I am just about to read your link, but I just wanted to touch upon your "for instance." You scenario, in my ideal page wouldn't be an issue because you wouldn't push any content to the browser before user authentication was done. This actually touches upon both my page laws. CFFlush, but also the extra work. If a user is logging in, it's possible their credentials are not correct. If they are, you don't take them back to a login form, you take them to some sort of "my account" page. If they are incorrect, they get the form again. Authentication / Authorization determine which page to render and hence, no content should be flushed (that might be deemed useless by a CFLocation or a Forward()) until it is fated to be displayed.
Let me stress though that I do NOT have a satisfactory solution for this. This is my ideal scenario, not one that I have been able to implement in any satisfactory way for anything complicated.
...Now I go read your next post.
Hi Ben,
yeah, more I thought about it, more I realized we were both coming to same place from different arguments. FYI, I DO have a generalized solution to the problem - see what you think of it in my post!
Ben,
Curious as to what you would think of layout components. They allow for your scenario except that you can easily switch the output (and have the layout all in one file).
http://coldfusion.sys-con.com/read/154231.htm
http://steve.coldfusionjournal.com/managing_the_ui.htm
Although I may not mention it in the article (which I should update with a blog entry soon), allowing cfflush was a major requirement in my development of this approach.
Steve,
That's funny to post those articles. I actually came across the CFDJ article about a year ago and it rocked my world a bit. I thought it was genius, genius I tell you. I then went to try and implement on a project that then went double the budget.
Now, I am not saying this has anything to do with your article. This was my first OOP style project and I really had no place doing it (as I was just learning OOP at the time ... and still trying to wrap my head around it).
But, I really liked the idea a lot. And even now, I am believe I just wasn't applying it correctly. What I neded up doing was creating a page component, BasePage.cfc. Then I had new page components for each super-section of the site (about, documents, contacts, etc.), each of which extended the BasePage.cfc.
The biggest problem that I had was that I didn't really have the interface nailed down at the start of production, so I kept having to make changes to the base page and then to all the extending pages (this of course also has to do with my OOP inexperience).
What I wanted to do was attempt the template pattern, something like a page that generically had methods like (off the top of my head):
Page::OnStart()
Page::OnContentStart()
Page::OnContentEnd()
Page::OnEnd()
Then each concrete page module would extend this one and override. The problem I had was that I build the pages from the inside out. So, the page module would have to call it's own method (start calls content start... content end calls end) and then the ocntent area would call start and end or something.... I never really got it working nicely and it's a pain in the butt to maintain.
That being said, I just didn't know what I was doing... but I know that when I saw your article i was like "Yes, that is a good idea". Now I just need to figure out how to best apply.
Hi Steve,
Thanks for reposting those links. I was trying to find them the other day to credit you in a blog posting and I gave up - for whatever reason I just couldn't get them via Google, and as it was more "deep background" than directly relevant I just let it slide! Looking forwards to reading these again!
Oops. Take that back. Looks like I DID manage to find them and credit/link to you properly. That's good!
Ben,
I will try to write a blog entry soon on how I have implemented the technique (in the meantime, CodeCop is a pretty good example).
I suspect the main problem is that you are over-complicating the approach. In my experience, layout components are best thought of as a nearly direct replacement for using cfinclude for headers and footers.
I think that the major challenge to the approach is that it opens up so much power that it is tempting to try to do things that you can do with components but that aren't helpful to do.
My basic advise would be to keep it simple and use them much as you would headers and footers. They have more power, but you can go to far with that.
Peter,
Thanks for finding the reference.
Awesome. I will download CodeCop and pick it apart ;)
Hi Steve,
I actually think there are LOTS of things that you can take advantage of with layout components such as rendering URLs so you can encapsulate variability in how you get some place and concatenate the URL state info. It can simplify composition, it allows for DI access to controller if you want to create component based MVC as opposed to page based MVC (where all of the rendered components on your page call their independent controllers which in turn gets them the data they need). You can also abstract all of the logic from stuff like creating a table with all of the paging links and records per page functionality so that you have a very clean template a designer can edit and a set of methods in the Table class for doing the smart stuff. And again with inheritence, you can subclass that if you want different logic in different projects.
There are a LOT of things the cfcs can do and I think there s a value in it doing many of them if it means you can deliver well architected projects faster and more easily.
Keep an eye out as I keep posting on and then start to release LightBase - I think there will be a few cool things that could save us all quite a bit of time!
Peter,
I absolutely agree and I will try to blog about many of my experiences with that. My point is that when switching to layout components, it is best to start simple. No reason to start using all of the power at first, especially when you don't need to do so.
I think using layout components for table logic (paging and the like) or for anything with the word "logic" in it (you mentioned subclassing for different logic in different projects) is dangerous.
I do tend to pass in my DI component (and factory) as well as the CGI structure to my layout components upon initialization which has proved helpful.
The trick, I think, is figuring out which things really add value and which add complexity for very little real benefit. My advise in most circumstances would be to start simple and add functionality as you need it.
I'm just excited to see more blogging about this :)