Mixing logic and html can easily end up with messy, hard-to-maintain code. In MVC it’s easy to separate the parts with the controller / view-separation. But how to do it nicely in WebPages or in an Umbraco macroscript?
Ultimately I want my Razor to be free from
- variable assignments other than for loop iterators
- function calls other than formatting and html helpers
- usage of data outside of the “ViewModel”
I have some WebPages and Umbraco projects with some quite advanced razor code and I’ve been having many doubts about how I mix logic in my razor code. I try to separate it, most often by placing logic at the top, but still using Razor (with helpers or RenderPage by all means). After re-thinking some about the @functions ability in Razor and found out about the overridable InitializePage function I feel I now have a better more solid structure to use.
Update: I recommend do not use this too extensively
The “functions” approach is nice, but also consider the simpler way just to have an initialization section in the top of the script – still separating C# from actual view. See this post for an example.And if you have extensive pure C# move it to a base class which you inherit your script from.
The idea is simply this : remove all logic (but necessary iterations and some conditions) from the razor, and place it in the @functions part (in the InitializePage function) together with ViewModel properties. The ViewModel properties should contain all data that the view part needs to be able to render the page / the macro. And the view part should not access anything else than the ViewModel properties:
@* --- The logic-less view part: --- *@ <p>@SomeProperty</p> @* --- The controller / viewmodel constructor part: --- *@ @functions{ // the properties of the view model: public string SomeProperty {get; set;} // taking care of post data and constructing the view model: protected override void InitializePage() { // make up the "ViewModel" properties SomeProperty = "some data"; } }
Advantages with this approach
- A clear separation of view and logic (not as clear as having them in separate files tho)
- The logic is pure C#-code, no risk of doing mistakes due to misplaced @’s (and missing code blocks)
- The code is a big step towards MVC, and the full step will be quite easy later on if necessary
- Minimal added whitespace in page source
Helpers are perfectly fine to add to this – but just as with the view code, I think any logic but view logic should be left out of them.
A remake of my “old skool” contact form sample
I wrote a contact form razor sample quite a while ago, guilty of mixing logic into the view. However I’m not the only one ;), a sample at asp.net.
Here’s a better (I think) remake, using the initializepage-structure:
<h2>Contact form</h2> @if(ShowMessage) { <div><strong>@Message</strong></div> } @if(ShowForm) { <form action="#" method="post"> <div> <label for="name">Name:</label> <input id="name" name="name" value="@PostedName"/> </div> <div> <label for="message">Message:</label> <textarea id="message" name="message">@PostedMessage</textarea> </div> <input type="submit" value="Post message"/> </form> } @functions{ public bool ShowForm {get; set;} public bool ShowMessage {get; set;} public string Message {get; set;} public string PostedName {get; set;} public string PostedMessage {get; set;} protected override void InitializePage() { base.InitializePage(); if (!IsPost) { ShowForm = true; ShowMessage = false; } else { PostedName = Request["name"]; PostedMessage = Request["message"]; var IsValid = (!string.IsNullOrEmpty(PostedName) && !string.IsNullOrEmpty(PostedMessage)); if (IsValid) { var bodyText = "Message from " + PostedName + Environment.NewLine + PostedMessage; umbraco.library.SendMail("from@mysite.com","admin@mysite.com","New message", bodyText,false); Message = "Thank you " + PostedName + " for posting a message"; ShowForm = false; ShowMessage = true; } else { Message = "You need to enter both name and message"; ShowForm = true; ShowMessage = true; } } } }
Public properties or private fields?
The public properties could be private fields without any problem (in these samples), the reason I choose public properties is that I like to resemble the MVC structure as far as possible.
Responding to an ajax-post
This structure also makes it really easy to handle and respond to ajax posts to the same form (this can only be done in a pure WebPages razor, not Umbraco macro as that does handle the Response output the same way) :
... in the validated post part ... if (IsAjax) { Json.Write(new { postValid = true, message = Message }, Response.Output); }
In Umbraco the options for ajax are to either to run razor script outside of the Umbraco context (use WebPages + add reservedpath in web.config) or to make a separate “ajaxresult-template” with a razor call:
Bonus: posting the form with ajax in Umbraco
Using this method you can return json directly from your razor script in Umbraco. The tricky part is we dont want to return the surrounding template parts, and for simplicity we want to use code in the same razor script as as we already are in.
Add a template – call it AjaxRazor – which will be responsible for rendering the razor without anything but the script result (we add an if IsAjax condition to have some kind of security against unintended run script files, please add more security checks for posts on a live site):
<%@ Master Language="C#" MasterPageFile="~/umbraco/masterpages/default.master" AutoEventWireup="true" %> <asp:Content ContentPlaceHolderID="ContentPlaceHolderDefault" runat="server"> <umbraco:macro language="cshtml" runat="server"> @if (IsAjax){ var path = Request["path"]; if (!path.StartsWith("~")) { path = umbraco.IO.SystemDirectories.MacroScripts + "/" + path;} @RenderPage(path) } </umbraco:macro> </asp:Content>
Now it’s possible to run any razor script using the url /AjaxRazor?path={path-to-script}
Next make the form post it’s contents to the template, with the razor script path as a querystring parameter, we get that path with the help of the page property VirtualPath:
@if(showOnlyJson) { Json.Write(json, Response.Output); } else { if(showForm) { <script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.min.js"></script> <script type="text/javascript"> $(document).ready(function() { $('form').submit(function() { $.post('@ajaxPostPath', $(this).serialize(), function(data){ alert(data); }); return false; }); }); </script> <form method="post" action="#"> <input type="text" name="name"/> <input type="submit"/> </form> } if (showMessage) { <p>@message</p> } } @functions{ private bool showOnlyJson; private bool showMessage; private bool showForm; private string message; private object json; private string ajaxPostPath; protected override void InitializePage() { if(IsPost) { var postedName = Request["name"]; message = "Thanks for posting"; if (IsAjax) { showOnlyJson = true; json = new {message=message, name = postedName}; } else { showMessage = true; showForm = false; } } else { showForm = true; ajaxPostPath = "/AjaxRazor?path=" + VirtualPath; } } }
Umbraco v5
I believe this approach still might be useful in Umbraco v5. It should likely not be recommended as an option for advanced solutions, but for simple forms (and advanced navigations) I think its suitable. And even if it’s not suitable it’s a good step towards controllers and viewmodels.
