Skip to content
May 12 / Martin

Tinyweb Series: 4 Views & Model Binding

In the previous post in this series, we looked at Tinyweb’s support for dependency injection and saw how to use both handler and global filters to add processing steps to the execution pipeline.

In this post we’re going to see how to render views using the Spark view engine and take a closer look at how model binding works.

Views

We’ve already looked at the various result types that you can return from a handler, but we skipped over arguably the most important one, a view. A view is just HTML, but usually rendered through the use of a templating engine that makes it easier to combine HTML and data.

You may have played around with different view engines in MSMVC and come across the Spark view engine. If not, don’t worry – it’s fairly easy. Tinyweb ships with support for Spark, so that’s what we’ll use for the examples here.

Let’s see a simple example:

public class RootHandler
{
    public IResult Get()
    {
        return View.Spark("Views/Index.spark");
    }
}

We have a pretty typical handler which accepts GET requests at / (the root request) and outputs the Spark view at Views/Index.spark.

The notable difference from the other result types is that views come from the static class View, whereas other results come from Result i.e. Result.String. The only reason for this is that simple types and views are considered different and therefore kept separate.

You’ll notice that the view path starts with a directory named Views. Unlike MSMVC, this is not a convention – it’s just a pattern I tend to follow. If the view file was placed at the root of the application, we could simply return View.Spark(“Index.spark”).

There’s little point regurgitating the Spark documentation in this post, but in case you have never come across Spark before, here’s what a typical view might look like:

<viewdata model="QuestionAnswerJokes" />

<ul>
  <li each="var joke in Model.Jokes">
    Q: ${joke.Question}
    A: ${joke.Answer}
  </li>
</ul>

Our contrived view tells Spark that it’s expecting an instance of QuestionAnswerJokes to be passed to it. It then produces an <li> element for each joke contained within the QuestionAnswerJokes.Jokes collection, outputting the question and answer part of each joke into the <li>.

So, how does it get the model object?

View Models

In the handler above, the astute will have noticed that there’s no model being passed to the view. Fear not, I wasn’t trying to hide hideous complexity:

public class RootHandler
{
    public IResult Get()
    {
        var jokes = JokeService.GetQAJokes();
        return View.Spark(jokes, "Views/JokeList.spark");
    }
}

The view model is simply passed as the first argument to View.Spark, and as long as the model type matches the <viewdata model=”…”/> declaration, we’re good to go.

Master Pages

If we want a view to be part of a master layout, we must specify the master’s name (relative to the location of the view) in the call to View.Spark. This may become cleaner in the future, but for now, it looks like this:

public class RootHandler
{
    public IResult Get()
    {
        return View.Spark("Views/Index.spark", "Master.spark");
    }
}

Rendering views from Tinyweb doesn’t get much more difficult than that, so let’s continue on and take a look at model binding in Tinyweb.

Model Binding

Model binding is the process of automatically taking data from the request (such as the query string or form post data) and mapping that data to the parameters (or properties where a class is used as a parameter) of the handler method. Model binding is useful because it means you write less code and let the framework deal with boring jobs like digging into the request data and populating objects.

There are three locations that Tinyweb will search for data. It’ll look in the query string, followed by the form post data and finally the route data (which only applies if you are using custom routes with route parameters, see Routing).

Three types of model binding are currently supported:

  • Simple parameters such as int and string
  • Custom class parameters such as LoginModel and UserSignupModel
  • Array parameters such as int[] and bool[]

Let’s go through each one and see an example of where it might be helpful.

Simple Parameters

public class CalculatorAddHandler
{
    public IResult Get(int first, int second)
    {
        return Result.String((first + second).ToString());
    }
}

If we now make a GET request to /calculator/add?first=4&second=2, we should be seeing 6 in the response. In the event no value is sent for first or second, we’ll get an error telling us that required parameters are missing. If you need optional parameters, see the Optional URL Parameters section of the docs (you could also use a custom class, where non-matched properties are just ignored).

Custom Class Parameters

We may also want to use our own classes as parameters to handlers for composing lots of separate fields. For example, If we were implementing a search handler we may want to use a SearchData class to hold the various search options:

public class SearchData
{
    public string Query { get; set; }
    public bool NewestFirst { get; set; }
}

Our handler can now accept an instance of this class, which the model binder will create and populate from the data in the request:

public class SearchHandler
{
    public IResult Get(SearchData searchData)
    {
        var results = SearchService.Search(searchData);
        return View.Spark(results, "Views/Results.spark");
    }
}

A request to /search?query=help&newestFirst=true will result in the creation of the SearchData object with the relevant properties populated. If the model binder can’t find data for a property, it will simply ignore it without throwing an exception.

Array Parameters

Binding to array parameters can be useful for situations where a collection of values is posted to a handler (via AJAX for example). Let’s assume we have a list of ToDo items in a view and when an item is clicked we toggle an attribute that marks it as complete. Clicking the Save button might run something like the following:

var completed = new Array();

$('#todos').each(function() {
    if ($(this).attr('complete') == 'true') {
        completed.push($(this).attr('todoId'));
    }
});

$.post('/todo/delete', { Completed: completed }, function() {
    alert('Saved');
});

The script builds up an array of numbers and posts them to /todo/delete. A handler can receive the collection like this:

public class TodoDeleteHandler
{
    public IResult Post(int[] completed)
    {
        var deleted = TodoService.DeleteMany(completed);
        return Result.JsonOrXml(deleted);
    }
}

Special Binding Cases

Tinyweb’s model binder has two special cases where the handler or filter can get access to either the RequestContext or the HandlerData for the current request.

The RequestContext gives you access to the underlying ASP.NET infrastructure (the HttpContext – please, don’t run away, come back) and means you can do anything that you’d do from within a raw HttpHandler, such as access the Request, Response, Cache and Session collections.

HandlerData is a class from tinyweb.framework that holds the handler’s System.Type and its URL, in addition to any default route values it has defined. Accessing HandlerData is useful more so in filters where the handler for the current request needs to be identified for things like logging.

The model binder will supply RequestContext and HandlerData to any handler or filter that declares a parameter of either type.

Model Binding Limitations

As of version 2.1.0 of Tinyweb, the model binder provides no way to exclude individual properties from model binding. The recommended practice is to use a type specifically crafted for receiving only the data you care about from model binding. The next release will add support for binding exclusions, addressing those situations where you wish to bind to existing objects but restrict the binding scope.

EDIT As of version 2.1.2, binding exclusions are supported via use of the [Ignore] attribute on class members that should be excluded from model binding. See model binding for more information.

In This Post

We looked at rendering Spark views and touched on how to use view models and master layouts. We then focused on model binding in a little more depth, looking at each of the three (simple, complex & array) types of binding supported and – as is tradition at this point – built a couple of contrived examples to demonstrate the point. Finally, we saw the two special binding cases for getting access to RequestContext and HandlerData.

Keep in mind that everything we’ve covered so far is also covered in the documentation, which is the best place to go if you require clarification. Failing that, I’ve been known to tweet once or twice.

In The Final Post

We’ll put together a few of the things talked about so far and build a ToDo application, dealing with things like API access, authentication and logging. We may even get time to do some benchmarking.

Leave a Comment