Separating html and logic in Razor (WebPages or Umbraco macroscript)

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) &amp;&amp; !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.

About these ads

16 thoughts on “Separating html and logic in Razor (WebPages or Umbraco macroscript)

  1. Hi Jonas,

    your article has been a real guide for me, since I started exploring Razor with Umbraco!

    One simple question I have regarding your approach is about macro parameters. For example, if I render the form in a template and post it, I will be redirected in the AjaxRazor template. Now, suppose I had a macro parameter in my first template which I’d like to be available in AjaxRazor template as well, without using an extra field in my query string.
    Can this be done?

  2. Hi,

    to be honest the hidden value I’d like to pass is a mailing list ID (from MailChimp service), so I’d like to be completely hidden from the user (even from the form’s markup)….
    I don’t know if this has anything to do with your article, but could you please provide and tips?

    Thanks for your help.

    • Hi, sorry for the late reply. I guess you solved it already. Anyways: one easy way would be to use a session variable to keep the value from one post to another without showing it to the user. Session[“id”]=nnn;

  3. Thanks! This is helpful.
    I have used @Html.Raw() in the razor script using above code but it says object not defined. If I remove above code it works fine. Is it do with protected override void InitializePage()

    Any ideas?

  4. I added base.InitializePage(); as first line of protected override void InitializePage() and Html.Raw started working fine.

  5. Tom, regular stuff like: parameterized sql’s, make emails from web sites go through spam filters, sometimes invisible simple bot-filters, captchas if needed.

  6. Hey Jonas,

    Thanks for this post. Very helpful.

    I was wondering: this way to call macro scripts does not handle multilingual, does it ?

    Regards,

    Nicolas.

    • Hi Nicolas, glad you like it. You can use umbraco.library.GetDictionaryItem (you could even use that to choose language specific macro script, naming them with language prefixes)

      • Sorry, I assume now I’ve been unclear…

        I’m already using @Dictionary.someEntry which run fine inside my macro scripts, even when called through your method.

        Current concern is that as these macro scripts are run out of the Umbraco environment, I’m guessing they can not be aware of the current culture of the ‘parent’ Umbraco page which is calling the razor script… So they are using the default culture.

        Saying differently, I’m calling a Umbraco page which includes some ajax boxes which then are set to exploit your method. Unfortunaately, all will be rendered in English even when called from a French Umbraco page.

        Does that make sense ?

        I actually don’t know yet how to retrieve the current culture as we can not use Umbraco macro (with the culture as an argument). Do you have some direction I could look to ?

        Thanks,

        Nicolas.

      • Aah, the ajax one, ok, Umbraco will set the culture from the root, and you need it the one from a node below the root, right? I guess one way would be to make a ajaxrendering node below each culture, which are using the ajax template, like mysite.com/fr/ajax – and append the culture in the call url from the calling javascript. Does that make sense?

      • … or set the culture in the first row in the ajax template (based on a querystring ?culture=fr)
        System.Threading.Thread.CurrentThread.CurrentUICulture = Request[“culture”]

  7. You made it. Your help here has been much appreciated.

    I finally get the current UI culture by requesting my meta with jQuery:
    var currentCulture = $(“meta[name=’language’]”).attr(“content”);

    I then ‘prepare’ my container (still jQuery):
    $(‘#container’).on(‘click’, function() {
    $(this).html(‘Loading…’)
    .load(‘/AjaxRazor?path=myMacroScript&culture=’+currentCulture);
    });

    The AjaxRazor template is a bit modified:
    var path = Request[“path”];
    var culture = Request[“culture”];
    if (!path.StartsWith(“~”)) { path = umbraco.IO.SystemDirectories.MacroScripts + “/” + path;}
    if (!path.EndsWith(“.cshtml”)) { path = path + “.cshtml”;}
    @RenderPage(path, culture)

    Finally, under my macro script:
    protected override void InitializePage()
    {
    try {
    System.Threading.Thread.CurrentThread.CurrentUICulture
    = new System.Globalization.CultureInfo(@PageData[0].ToString());
    } catch { }
    ……
    }

    Thanks again for your assistance.

    Best regards,
    Nicolas.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s