Bellevue is a prototype of a new Open source template engine for ASP.NET MVC framework. Bellevue uses pure HTML in its core - without any additional tags or syntax. It then uses CSS-like syntax to inject the active logic into the HTML - or in other words: to bind the data from the model to the view.
In the first part of this article, I introduced the idea in brief and some basic features. You can also find the full downloads package there.
The second part contains the following:
Templates are mostly used when you need to apply some HTML to each element of a collection. In Web Forms pages, this replaces the need for a repeat loop ("/Views/Demo/Demo2_Intro_WebForms.aspx"):
<div class="productList">
<%
foreach (string name in (string[])ViewData["AllNames"])
{
%>
<div><a href="http://www.codeproject.com/demo/Demo2_Intro_Bellevue/
<%= name %>"><%= name %></a></div>
<%
}
%>
</div>
Note that you can apply a template also to a single item, but this is currently less useful. In future versions, conditional and inline templates would make it a more justified scenario.
You start with templates by defining a template with @template
directive ("Demo2_Intro_Bellevue.bvue
"):
@template productListTemplate "#leftArea .productList div"
{
a
{
text: item();
attr: href concat("/demo/Demo2_Intro_Bellevue/", item());
}
}
The first parameter is the ID of the template (nothing to do with the HTML ID) and the second parameter is the selector that finds the template in the HTML. Matched element(s) will be taken away from the original HTML and used only when apply-template is called.
Within the template, you may have rule sets that are specific to the template. In this case, text and href-attributes within the template are modified. You could also call apply-template within the template, so nested templates are possible. The "item()
" function returns the item to which the template is bound to when the collection is looped through.
In this case the selector matches this HTML ("Demo2_Intro_Bellevue.html"):
<div class="productList">
<div><a href="http://www.codeproject.com/linkHere">Product 1</a></div>
<div><a href="http://www.codeproject.com/linkHere">Product 2</a></div>
<div><a href="http://www.codeproject.com/linkHere">Product 3</a></div>
</div>
All the inner div
s are taken as templates and applied one after another. In this case, they are all the same, but they could also have different HTMLs, e.g. if you want even and odd rows to be with different colors.
To apply the HTML, you use the apply-template declaration:
#leftArea .productList { apply-template: data(AllNames) productListTemplate }
In the next preview version, the template syntax should clearly have much more options. I already have much of the plumbing ready, but I need to check exactly how to implement the syntax. The idea is to add the following features:
- Inline templates: Possibility to add a template HTML directly in the Bellevue script.
- Conditional templates: Use this template, if "
item(x)
" is true
, otherwise use that template.
- Making the inner HTML of the match(es) to be the template instead of the outer HTML:
- Would allow templates like
text <br />
. Now you would need to use span
around this or hack it using a non-HTML tag.
- Templates from include files
Also, the most important feature missing in the current implementation is the possibility of defining new attributes as you call templates: So that you have reusable template that always need parameters "item(link)
" and "item(label)
" and when calling the template you could specify e.g. "link=item(url), label=item(text)
". I am having a bit of trouble in figuring out a good syntax that would be compatible with CSS syntax, but it will be there - I promise.
So with templates, there are many features missing in the current version. This is the biggest undercon area. But I hope you get the idea of the potential power: It is very easy to make repeating table rows and other sections with just a few lines of code. It is much easier than with Web Forms.
Master pages are Web Forms specific: there is no ASP.NET MVC standard way of plugging into them. So each view engine typically creates its own master pages system. In Bellevue, you define a master view by using the @master
directive ("/Views/Shared/Site_Master.html"):
@master
{
#mainContentPlaceholder
{
placeholder-id: main;
}
title
{
placeholder-id: title;
}
#pageStyles
{
placeholder-id: styles;
}
}
Selector can be any valid selector that matches HTML - in fact even several elements are possible. IDs must be unique. In addition, you can have normal Bellevue rule sets to add active content to master page HTML.
You use the master view in an individual view using @master
-is directive ("Demo3_UseMaster.html"):
@master-is Site_Master
{
body { placeholder: main; }
title { placeholder: title; }
}
Body and title in the above example could be any selectors within the page. So the page can be a full HTML page, which can be rendered stand-alone, but when applied to a master page certain parts of it are extracted (examples are like that).
You can switch the master page in controller, like you would do with Web Forms (see Demo controller action "Demo3_UseMaster
" for example).
I am pretty happy with the master view concept as it is. Only things I am considering adding is automatic support for title, styles and scripts both in the head and as last elements in the body. Another thing is that at the moment, if no content is added to a placeholder, the HTML inside the placeholder is rendered, but no active stuff is applied - this will be fixed using templates.
Above, I already listed features that I am planning to implement in templates and master views. There is also an @if
directive explained at the end of this page, but it is currently still missing the else
and else
-if
-functionalities so it does not make sense to talk more about it yet.
I am not making any commitments, but these are the things I am thinking:
- Currently, only one declaration is applied to each element or attribute (except with
@if
and even there are some issues): possibility to apply more.
- Extensibility: Custom declaration handlers and renderers, possibly directives and formatters.
- Implement ASP.NET MVC
HtmlHelper
methods: Not all, but ones that make sense.
- Additional declarations like:
display
/hide
, attr-hide
, change-element
@debugger
directive for more control.
- Resolving to
ViewData
and Model
property paths work in most cases, but there is some room for improvement in edge cases / consistency.
- Caching
StringTemplate as Inspiration
The main inspiration for me in the design is Terence Parr's StringTemplate. The main point there that I am trying to follow is the principle of strict model-view separation. From StringTemplate
web site:
Its distinguishing characteristic is that it strictly enforces model-view separation unlike other engines. Strict separation makes websites and code generators more flexible and maintainable; it also provides an excellent defence against malicious template authors.
I do believe in this concept: I think one of the main problems with ASP.NET (MVC) Web Forms is that when developers have pressure in projects, they start hacking model and controller logic into the aspx-views instead of making proper changes into the controller and model code. I have seen this happen many times and I must admit I am guilty of it myself.
Check the link to the PDF above to understand the whole model-view separation idea. The demo project contains a comparison to StringTemplate
features.
Other Credits
I am using the following excellent projects as bases for the implementation:
- To parse HTML, I use the HTML Agility Pack. This project seems to be quite mature: I have found one bug and that was minor.
- I use the Simple CSS Parser to parse the Bellevue script. There, I have had more problems and I might be forced to either change the code in the parser or implement a custom parser. This is not to say that the CSS Parser project is bad quality: it is just that Bellevue script is not really CSS and I am using a lot of edge cases which produce problems.
- For positioning custom logic in HTML based on the CSS selectors, I use of course Fizzler. I am using it as it is designed to be used - I have not tried many edge cases, but it seems to be working well. Note though that it does not yet implement all more exotic CSS level 3 selectors.
Principal Design: Parsing vs. Rendering and Extensibility
If you have looked at the code, you might be asking why is the parsing and rendering code so complex. You could do this in a much simpler way. The first reason is performance consideration:
HtmlAgilityPack
is quite fast - for normal sites and pages, render times are really OK, usually like dozens of milliseconds after the first JIT loads. But for big-load sites, and ones with large pages with a lot of replaces, this might be too much. So the idea is to separate the Bellevue rendering into two parts:
Parsing would be the part where HtmlAgilityPack
, CSS Parser and Fizzler do the heavy parsing with HTML and CSS DOMs and selectors. It positions the rule sets and directives all around the HTML, separates the templates etc. This logic should be independent from the ViewData
which may be different on each call so that this work could be cached to be common for several page calls as parse result.
Rendering would then be done for each call and without having access to full HTML DOM. It takes the parse result and directly outputs HTML as is, and where there is active content, puts in the data. Currently the parse result is based on HTML DOM, and it is done every time, so there is no performance gain. But later versions could implement a caching mechanism and e.g. put static
HTML parts together as long string
s the same way as aspx compilation does. In extreme cases, it should even be possible to create ASPX from Bellevue views at parse time, but I am not sure whether it makes sense or not.
The second thing that adds a lot of complexity is the extensibility story. There is no extensibility API yet, but I am preparing for it. The idea is that you could add your own declaration handlers, renderers - even directives and formatters. This way, it should be possible to add your own view functionality like you can add HTML helpers for Web Forms.
So, this is what I am currently working on. I would very much like the feedback from the great people of The Code Project:
- Do you think this kind of approach / syntax would make sense?
- Do you see clearly features missing on my list?
- Do you see a valid MVC view that you could not implement with these features? ... keeping in mind the principle of Model - View separation.
I will update these articles, when I have the release candidate and then the 1.0 version ready. In my own home page http://www.ope.ag/bellevue I have more material and I will be making updates more frequently. But there is no discussion /comments, so this CodeProject page is the best place to give feedback at the moment.
- 19th March, 2010: Initial post