Creating an Umbraco 4/6 Form using pure Razor Blog post

Note: This is an old post and only really applicable to older versions of Umbraco 4 and 6 where you are using WebForms and DynamicNode. It's definitely not the best way to create a form using MVC in Umbraco.

Traditionally forms have always been created in Umbraco 4 via a Web Forms user-control. Usually these will use standard ASP.NET server controls and validators. However, this type of form can exhibit a number of problems:

  • It will require you to create your content within a single server side form tag with runat="server". This can be restrictive when you have multiple forms you want to handle.
  • ASP.NET validator controls aren't very configurable and don't provide the rich feedback people now expect from forms.
  • The framework inject all sorts of JavaScript and ViewState into your nice, clean semantic mark-up.
  • Your form won't be easily testable.

So in this post I'm going to show you how to create a simple form using pure Razor and C# - with no web-forms, ViewState or ASP.NET controls in site! The aim of this is to demonstrate some useful techniques that help fulfill the following criteria:

  • Our form must use both server-side and client-side validation.
  • It should utilise strongly-typed models to help prevent typo errors and provide full intellisense.
  • It mustn't place any restrictions on mark-up or layout.
  • It should not require any dependency on ASP.NET web-forms (so no runat="server" required).
  • It should use a simple, unintrusive "honey pot" method to deter spam bots.
  • It should prevent accidental re-submission of the form if the page is reloaded.
  • Our "business logic" should be separated form our presentation layer (as far as is possible) and be testable.

Also, given all the fury over Umbraco 5 being dropped (and hence no MVC) I thought I'd show how you can at least use some of the general concepts from MVC in Umbraco 4 (and, yes, I realise this is no substitute - but it's still a great way of avoiding using web-forms).

If you are impatient then you can download this concept as a package from the Umbraco Package Repository.

Creating Our Form

The actual form we are going to create is ultimately unimportant - it's the techniques and ideas behind it that I want to concentrate on. But for our example we'll create a simple contact form (since it's one of the most common forms you'll use). Here's what it will look like:

Diplo Razor Form

Whilst this post isn't about mark-up, I'll give an example of this particular form just so you can see how simple it is:

        <form id="contactForm" action="" method="post">
        <fieldset>
            <legend>Your Details</legend>
            <dl>
                <dt>
                    <label for="firstname">First Name</label>
                </dt>
                <dd>
                    <input type="text" id="firstname" class="required" name="firstname" value="@form.FirstName" maxlength="20" />
                </dd>
            </dl>
....
    

 

As you can see, very simple semantic mark-up that contains no server controls at all. However, you are complety free to use whatever mark-up and CSS you like. 

The Model

No, this has nothing to do with Kraftwerk! Instead, we borrow a concept from MVC and use a simple class to represent (or 'model') the form and it's fields. The idea is that this class will be responsible for parsing and storing the values of the form in a manner that allows us to easily deal with it in code. It will be strongly-typed and can be easily passed to other methods (for instance, we could have a mail class that takes an instance of our model and uses it to generate an email or perhaps a data class that persists the values to our database).

To keep things simple we will give responsibility to our model for parsing the request values posted from our form and also for server-side validation (though in a more complex system you may wish to delegate this responsibility to a service layer). So, let's have a look at our "model" class (she's quite the looker!):

 

        public class ContactFormModel
{
    public string Title {get; private set;}
    public string LastName {get; private set;}
    public string FirstName {get; private set;}
    public string Email {get; private set;}
    public string AgeRange {get; private set;}
    public string Comment {get; private set;}
    public string HoneyPot {get; private set;}
    public const string SessionKey = "Diplo.Form.Submitted";

    /// <summary>
    /// Constructs a new contact form model with default values
    /// </summary>
    public ContactFormModel()
    {
        this.Title = "Mr"; // We set default selected value
    }

    /// <summary>
    /// Constructs a new contact form model from the query string or form variables
    /// </summary>
    /// <param name="request">The name value collection</param>
    public ContactFormModel(NameValueCollection request)
    {
        this.Title = request["title"];
        this.LastName = request["lastname"];
        this.FirstName = request["firstname"];
        this.Email = request["email"];
        this.Comment = request["comment"];
        this.AgeRange = request["agerange"];
        this.HoneyPot = request["honeybear"];
    }

    /// <summary>
    /// Validates the model
    /// </summary>
    /// <returns>An empty list if valid otherwise a list of error messages</returns>
    public List<string>Validate()
    {
        List<string> errors = new List<string>();

        if (String.IsNullOrEmpty(this.Title))
            errors.Add("Please select a title");

        if (String.IsNullOrEmpty(this.FirstName))
            errors.Add("Please enter your first name");

        if (String.IsNullOrEmpty(this.LastName))
            errors.Add("Please enter your last name");

        if (String.IsNullOrEmpty(this.Email))
            errors.Add("Please enter your email address");
        else
        {
            try
            {
                MailAddress address = newMailAddress(this.Email);
            }
            catch(FormatException)
            {
                errors.Add("Your email address was not in the correct format");
            }
        }

        if (String.IsNullOrEmpty(this.AgeRange))
            errors.Add("The age range you selected is not valid");

        if (!String.IsNullOrEmpty(this.HoneyPot))
            errors.Add("Please do NOT complete the field marked IGNORE. This is to verify you are human and not a spam bot.");

        return errors;
    }
}
    

 

As you can see the class has a number of properties that represent each field in the form. It also has two constructors - an empty constructor that creates an "empty" instance of the form with some default values and another constructor that takes a NameValueCollection as it's only parameter. This may seem like an odd type to chose, but when you remember that both the Request.QueryString and Request.Form values that represent HTTP posted data use this type, it becomes clear why it's a good choice. You can pass in either to the constructor of our ContactFormModel to initialise it. But why not just pass in the actual HttpRequest? Well, the answer is simple - our class isn't dependant on it so it is easier to test (we can easily create a unit test that mocks a NameValueCollection for testing, for instance).

Our ContactFormModel also has just one simple method:

Validate() - this checks all fields have been completed correctly and, if not, returns a list of error messages (which we can display in the UI). This allows up to easily perform server-side validation of all our fields (essential even when validating client-side). The logic you use to validate fields is completely up to you!

You'll also notice a const (static) value called SessionKey. This just defines a unique "key" that can be used to store a token in Session. I'll come back to this later.

So you can see that our model can parse the posted form values and also validate whether they are correct. This means none of that "business logic" need be present in our Razor script, keeping things clean and readable. Ideally we want our Razor scripts to contain just presentation logic and nothing more. Which leads us nicely to the Razor script itself...

The Script

So now we've seen the model let's take a look at the actual Razor script (that you can use in a macro within Umbraco). As I mentioned, the idea is to keep it simple with the "heavy lifting" done in the model (or, in a larger application, a service layer). You'll see that most of the following script is just HTML markup:

 

        @inherits umbraco.MacroEngines.DynamicNodeContext
@using umbraco.MacroEngines;
@usingSystem.Web.Mvc;
@usingDiplo;

<h2>ExampleContactForm</h2>

@{
    bool isPostBack = !String.IsNullOrEmpty(Request.Form["submit-button"]);
    var formModel = new ContactFormModel();

    if (!isPostBack)
    {
        Session.Remove(ContactFormModel.SessionKey);
        @RenderForm(formModel)
    }
    else
    {
        if (Session[ContactFormModel.SessionKey] != null)
        {
            @DisplayResubmissionError()
            return;
        }

        formModel = new ContactFormModel(Request.Form);
        var errors = formModel.Validate();
        
        if (errors.Count > 0)
        {   
            @DisplayErrors(errors)
            @RenderForm(formModel)
        }
        else
        {
            @DisplayCompletionMessage(formModel)
        }
    }
}

@helper RenderForm(ContactFormModel form)
{
    Repository repository = new Repository();
    var titles = repository.GetTitles();
    var ages = repository.GetAgeRanges();
    const string selected = "selected=\"selected\"";
    
    <form id="contactForm" action="" method="post">
        <fieldset>
            <legend>Your Details</legend>
            <dl>
                <dt>
                    <label for="title">Title</label>
                </dt>
                <dd>
                    <select id="title" name="title"class="required">
                        <option value="">-PleaseSelect-</option>
                    @foreach(var title in titles)
                     {
                        <option value="@title.Key"@Library.If(form.Title== title.Key, selected)>@title.Value</option>
                     }
                    </select>
                </dd>
            </dl>
            <dl>
                <dt>
                    <label for="firstname">FirstName</label>
                </dt>
                <dd>
                    <input type="text" id="firstname"class="required" name="firstname" value="@form.FirstName" maxlength="20"/>
                </dd>
            </dl>
            <dl>
                <dt>
                    <label for="lastname">LastName</label>
                </dt>
                <dd>
                    <input type="text" id="lastname"class="required" name="lastname" value="@form.LastName" maxlength="20"/>
                </dd>
            </dl>
            <dl>
                <dt>
                    <label for="email">Email</label>
                </dt>
                <dd>
                    <input type="text" id="email"class="required  email" name="email" value="@form.Email" maxlength="255"/>
                </dd>
            </dl>
            <dl>
                <dt>
                    <label for="agerange">AgeRange</label>
                </dt>
                <dd>
                    <select size="1" name="agerange" id="agerange"class="required">
                      <option value="">-PleaseSelect-</option>
                      @foreach(var age in ages)
                      {
                        <option value="@age.Key"@Library.If(form.AgeRange== age.Key, selected)>@age.Value</option>
                      }
                    </select>
                </dd>
            </dl>
            <dl>
                <dt>
                    <label for="comment">Comment</label>
                </dt>
                <dd>
                    <textarea id="comment" name="comment" rows="5" cols="50">@form.Comment</textarea>
                </dd>
            </dl>
            <dl class="honey">
                <dt>
                    <label for="comment">IGNORE</label>
                </dt>
                <dd>
                    <input type="text" id="honeybear" name="honeybear" value="" maxlength="50" />
                </dd>
            </dl>
        </fieldset>
        <div>
            <input type="submit" name="submit-button" value="Submit" class="button" />
        </div>
    </form>
}

@helperDisplayErrors(IEnumerable<string> errors)
{
    <h3>Oops!</h3>
    
    <p>Please fix the following problems andtry again:</p>
    
    <ul>
        @foreach(var error in errors)
        {
            <li>@error</li>
        }
    </ul>
}

@helperDisplayCompletionMessage(ContactFormModel formModel)
{
    <h3>ThankYou</h3>
    
    <p>
        Thank you, @formModel.FirstName, your message has been successfully submitted!
    </p>
    
    <p>
        The data you submitted is:
    </p>
    
    <table>
        <tr>
            <th>Name</th><th>Value</th>
        </tr>
        <tr>
            <td>Title</td><td>@formModel.Title</td>
        </tr>
        <tr>
            <td>First Name</td><td>@formModel.FirstName</td>
        </tr>
        <tr>
            <td>LastName</td><td>@formModel.LastName</td>
        </tr>
        <tr>
            <td>Email</td><td>@formModel.Email</td>
        </tr>
        <tr>
            <td>AgeRange</td><td>@formModel.AgeRange</td>
        </tr>
        <tr>
            <td>Comment</td><td>@formModel.Comment</td>
        </tr>
    </table>
    
    <p>Note: If you try and reload the page the form won't be resubmitted.</p>
    
    Session.Add(ContactFormModel.SessionKey,true);
    
    // You can now easily email this or store it in a database etc.
}

@helperDisplayResubmissionError()
{
    <h3>Oops!</h3>
    <p>You are trying to submit the form again!</p>
}
    

How it Works

bool isPostBack =!String.IsNullOrEmpty(Request.Form["submit-button"]);

This essentialy does what an ASP.NET webform does and sets a boolean value to determine whether the form has been submitted (or "posted-back").

if (!isPostBack) { @RenderForm(formModel) }

If the form hasn't been submitted (ie. this is the first time the page has been loaded) then we call a helper method to render the form fields, passing in an empty form model (as the form has yet to be completed).

        var formModel = newContactFormModel(Request.Form);

if(Session[ContactFormModel.SessionKey]!=null)
{
	@DisplayResubmissionError()
	return;
}

if(errors.Count>0)
{   
	@DisplayErrors(errors)
	@RenderForm(formModel)
}
else
{
	@DisplayCompletionMessage(formModel)
}
    

 

The second part of the logic is executed when the form is submitted:

The code first checks whether a Session variable exists which denotes that the form has already been submitted (as we set it upon a successful submission). This should prevent accidental (or, indeed, purposeful) re-submissions of the from by reloading the page etc.

Assuming the form isn't a resubmission we then instantiate our ContactFormModel using the overload that takes in NameValueCollection which allows us to pass in the Request.Form collection. The constructor than parses out the form values and assigns them to the matching properties. This gives us a nice, strongly typed object to work with.

After that we can call the Validate() method on our model which, if you remember, validates all the fields have been filled in correctly. This returns a collection of error messages. If the there are no errors (i.e. the collection has zero items in it) then we know the form is OK. If it does have errors, we display these (using a simple helper) and then re-render the form using our model. This ensures all the fields that were filled in when the form was submitted still remain filled in (it's like ViewState without the headaches!).

Lastly, we display a completion message where you can put in whatever message you like. This could be be a content-managed rich-text field pulled in from Umbraco or it could be just a static value. You'll also notice we pass our ContactFormModel to this method, too. At this stage this will be fully populated and hold all the values our user submitted. We can then do something with this data - such as store it in a database or email it.

Client Side Validation with jQuery Validate

Whilst server-side validation is essential it is nice to provide client-side validation, too. This is more responsive to the user and also spares the server some load. You'll realise, of course, that we can't just use standard ASP.NET validator controls because this is a Razor script. Luckily, we can come up with something better (and easier) to implement by using the jQuery Validator plugin. This makes validating our form as simple as this:

<script type="text/javascript">
    $
(document).ready(function()
   
{
        $
("#contactForm").validate();
   
});
</script>

The "magic" works by marking the fields we wish to be validated with special CSS classes that inform the validator plugin what validation to perform. For instance, to mark a field as both required and to validate it as an email address you would add:

class="required email"

That's it! Of course, it's highly extensible so you can perform much more complex validation, too.

Preventing Spam

One thing you'll be aware of is that putting a form on a page encourages spam, often from automated "spam bots" that fill in all the form fields (often with adverts) and the submit it. One way of getting around this is to use a CAPTCHA control, but these can be obtrusive and off-putting to users. They also can add overhead to the logic of the form. So, as an alternative, I'm going to use another more simple method which is a variation of the "honey pot" technique.

How this works is to have one of your forms fields hidden via CSS (display:none). Humans shouldn't see this field and thus they won't complete it. However, automated spam-bots will fill in everything they see, getting "trapped" in the process. So as part of the Validate() method in our model we simply need to check whether this field has been completed and warn the user not to fill it in - a human will be able to understand this but a bot will not. Of course, bots are getting sophisticated and some will "see through" this, but it's a simple measure that will weed out a lot of spam.

In Summary

  • Razor scripting using C# (or VB) is a "first class" component of Umbraco 4 (from version 4.6 upward) - no need for XSLT or User Controls, if you don't want.
  • Just like in MVC we want our Razor scipts to be primarily concerned with presentation and not business logic.
  • You can break Razor scipts down by using @helper methods to make them more readable.
  • You don't need ViewState to maintain the state of form variables - they are sent as part of the HTTP header.
  • Even in Web Forms you can still have as many forms on a page as you like - so long as  they don't have runat="server" on them.
  • jQuery validate is a great way of performing client-side validation.

Download Package

I've created a small package that you can download from the Umbraco Package Repository that illustrates the code in this blog post. The repository also contains a link to the source-code if you don't want the package.

Download: Diplo Razor Form

2 Comments


Allan Cutts Avatar

Thank god you put this here.. had an old site where I used this form as a basis. For some reason I had compiled 'The Model' into a .dll - I needed to add an extra field to it!!!


Good work Diplo as usual :-)


Smithf809 Avatar

I appreciate, cause I discovered just what I used to be looking for. You have ended my four day long hunt! God Bless you man. Have a nice day. Bye

Just fill in the form and click Submit. But note all comments are moderated, so spare the viagra spam!

Tip: You can use Markdown syntax within comments.