ASP.NET programming with XCST
XCST is new page/view language available for ASP.NET. Just like Razor, it compiles to C#, but it offers much greater modularity, composability and extensibility. This post however won’t go into much details, instead we’ll jump right into building a contact form page. This will give you a quick idea about the look and feel of the language and learn some of its features.
Model
The first thing we need to build a contact form page is a model that defines the fields for the form:
<c:type name='Contact'>
<c:member name='Name' as='string' required='yes' min-length='2' max-length='50'/>
<c:member name='Email' as='string' required='yes' max-length='255' data-type='EmailAddress' display-name='E-mail'/>
<c:member name='Telephone' as='string' required='yes' min-length='8' max-length='20' data-type='PhoneNumber'/>
<c:member name='Message' as='string' required='yes' max-length='2000' data-type='MultilineText'/>
</c:type>
The c:type
declaration is used to define the type for our model. Although we could have done this in C# instead, c:type
can be declared in the page right next to where it’s used. Also note the validation and presentation attributes, these compile to System.ComponentModel.DataAnnotations
attributes.
Mail template
<c:function name='MailBody' as='string'>
<c:param name='contact' as='Contact'/>
<c:return>
<c:serialize method='html'>
<a:model value='contact'>
<dl>
<a:display>
<a:member-template>
<dt>
<a:display-name/>
<c:text>:</c:text>
</dt>
<dd>
<a:display/>
</dd>
</a:member-template>
</a:display>
</dl>
</a:model>
</c:serialize>
</c:return>
</c:function>
Normally you’d put markup in a c:template
declaration, but since the MailMessage
class expects the mail body as a string
we use c:function
because it can be called directly from C# code.
The c:serialize
instruction creates a string
of anything defined inside of it.
The a:model
instruction defines the model for descendant instructions. Because in MVC you get the model from the controller, there’s only one model: this.Model
(aka global model). Since this is not a view but a page, we use a:model
to define a model, which can be used more that once in different parts of a page.
The a:display
instruction (when it has no name
or for
attributes) works like Html.DisplayForModel()
in Razor. This instruction is relative to the a:model
we’ve just reviewed. Just like Html.DisplayForModel()
, a:display
executes a template for each of the members that are displayable. Additionally, a:display
has a new feature: you can customize the template using the a:member-template
element.
The a:member-template
element defines a new model for its descendant instructions, which is a member (property) of the model used by a:display
. That means descendant instructions like a:display-name
and a:display
work against the member and not the whole model.
Send function
Now we can define the function that sends out the mail:
<c:function name='SendMail' as='bool'>
<c:param name='contact' as='Contact'/>
<c:script>
<![CDATA[
var message = new MailMessage {
From = new MailAddress("noreply@example.com", this.Request.Url.Host),
To = { contactTo },
Subject = contactSubject,
ReplyToList = { new MailAddress(contact.Email, contact.Name) },
Body = MailBody(contact),
IsBodyHtml = true
};
try {
using (var smtp = new SmtpClient()) {
smtp.Send(message);
}
return true;
} catch (SmtpException) {
this.ModelState.AddModelError("", "An unexpected error ocurred.");
return false;
}
]]>
</c:script>
</c:function>
This is basically just C# inside a c:script
instruction, which is used to put code wherever you need it. Note we are calling the MailBody
function reviewed before.
Contact form
<c:template name='content' expand-text='yes'>
<c:param name='contact' as='Contact' required='yes' tunnel='yes'/>
<c:param name='sent' as='bool' required='yes' tunnel='yes'/>
<h1>{this.title}</h1>
<a:model value='contact'>
<c:if test='sent'>
<div class='alert alert-success alert-dismissable'>
<button type='button' class='close' data-dismiss='alert' aria-hidden='true'>×</button>
<c:text>Thanks for contacting us. We'll get back to you shortly.</c:text>
</div>
</c:if>
<form method='post' class='form-horizontal'>
<a:antiforgery/>
<a:validation-summary/>
<a:editor with-params='new { labelColumnClass = "col-md-3", fieldColumnClass = "col-md-9" }'/>
<div class='form-group'>
<div class='col-md-offset-3 col-md-9'>
<button type='submit' class='btn btn-primary'>Send</button>
</div>
</div>
</form>
</a:model>
</c:template>
So far we’ve defined two functions (one that calls the other) and one type used by both functions. The contact form however is defined in a c:template
declaration. Templates are like helpers in Razor, and also serve as sections, but unlike sections it can have parameters.
This template has two parameters, one for the model and also a bool
that indicates if the mail was sent. Both are marked as required and tunnel. I’ll talk about tunnel parameters later. While function parameters are compiled to actual method parameters, template parameters are not. Template parameters are bound by name, not position, and are optional by default, unless you use required='yes'
.
Note the heading <h1>{this.title}</h1>
. Somewhere in the page there’s a global variable named title
, and we use string interpolation syntax to inject values in text nodes. This needs to be enabled using the [c:]expand-text
standard attribute, to avoid conflicts with content that uses curly braces, like CSS and JavaScript.
The rest of the template should be familiar by now.
The glue
<c:template name='c:initial-template'>
<c:script>
<![CDATA[
var contact = new Contact();
bool sent = false;
if (IsPost) {
AntiForgery.Validate();
if (TryBind(contact)
&& SendMail(contact)) {
sent = true;
// clear form
this.ModelState.Clear();
contact = new Contact();
}
}
]]>
</c:script>
<c:next-template>
<c:with-param name='contact' value='contact' tunnel='yes'/>
<c:with-param name='sent' value='sent' tunnel='yes'/>
</c:next-template>
</c:template>
c:initial-template
is a special template that is the starting point of the program, like the Main
method in a console app.
The code in the script is fairly simple.
The c:next-template
instruction calls a template with the same name as the current template in an imported module. That means somewhere in the page there’s a c:import
declaration, which in this case imports the layout module. There’s a lot more to say about modularity in XCST, but that’s a topic for another post. The last thing to note is that the parameters passed to the next template have tunnel='yes'
. This means basically two things: 1. The template we are calling doesn’t require to have those parameters defined; 2. If the template we are calling calls other templates then those parameters are passed on, and so on. This is how we get those values to the content
template we’ve defined.
Wrapping up
The complete page along with a runnable project can be found here. Questions, comments? below.