1 points - 5 years, 6 months ago by Martin Rue(50) under ASP.NET ()
1
In the final part of the Tinyweb series, we'll look at how to create a small to-do application by combining some of the features we've covered earlier in the series.

In the previous post in this series we saw how to render Spark views from handlers, looking at the use of view models and master page layouts. Finally, we took a more in-depth look at model binding support in Tinyweb.

In the final post, we’ll work through a to-do list demo which will pull together a number of the features already covered in the series into a complete application.

What We’re Building

We’re going to build a simple web application with a JSON/XML API that allows the creation, deletion and completion of simple to-do items. We’ll have a view that displays the current list, a method for removing and completing individual items and a form for creating new ones.

The Resources

Let’s consider which resources we’ll need to create in order to support the requirements.

/login

The LoginHandler will allow a user to authenticate by providing a login view.

/todo/list

The TodoListHandler will display the current list of to-do items.

/todo/add

The TodoAddHandler will create a new item and redirect back to the list.

/todo/complete

The TodoCompleteHandler will complete an item and redirect back to the list.

/todo/uncomplete

The TodoUncompleteHandler will undo the completed status and redirect back to the list.

/todo/remove

The TodoRemoveHandler will delete an item and redirect back to the list.

The API

As it’s a demo, we’ll keep the API simple. Our imaginary requirement here is that the current to-do list should be available outside of the application for things like external widgets or mobile phone applications to consume.

We’ll achieve this with a single handler at /api/todo/list that returns the current to-do list in either JSON or XML format.

Authentication

For the sake of keeping the demo easy to understand, our security requirements are simply going to be that a user must log in using a hard-coded username and password before they can use the application. We’ll use an authentication filter to implement this.

Logging

Finally, to catch any errors with our to-do list, we’ll also implement error logging so that we can periodically check any exceptions that have been thrown by the handlers.

Here’s One I Prepared Earlier

To use the demo, the first thing we need to do is authenticate at /login using the hard-coded username and password admin and password respectively:

alt text

We’re then presented with the to-do list at /todo/list:

alt text

Now that we have a feel for what this thing looks like, let’s go through the implementation details and discuss how it’s built with Tinyweb. If you’d like to follow along beyond the snippets used in the rest of this post, you can do so by looking at the demo code on github.

Project Layout

Although Tinyweb does not enforce a particular convention in terms of where you put files within the project, I tend to follow my own to be consistent between projects. Handlers go in a Handlers folder, filters in Filters and so on. Here’s the project layout for the to-do demo app.

alt text

It should be fairly easy to figure out what’s going on. Don’t worry too much about the files inside the Database directory since data access isn’t especially relevant to the demo app. Similarly, the Content directory isn’t too interesting either, it just contains CSS and JavaScript.

The core of the app is contained within the directories you see expanded. We have the aforementioned handlers right there in Handlers. Note how I’ve separated the API and the web app handlers to make it more explicit (again, just my own convention here).

There aren’t many views, so I’ve just bunched them together under Views. We also have a partial view named Add.spark which just creates a <form> that enables new to-do items to be posted to /todo/add.

From The Beginning

As we’ve covered in the previous posts, the first step in creating a Tinyweb app is to initialise the framework in Global.asax:

protected void Application_Start(object sender, EventArgs e)
{
    Tinyweb.AllowFormatExtensions = true;

    Tinyweb.Init(new DatabaseRegistry());

    Tinyweb.OnError = (exception, request, handler) =>
    {
        var error = String.Format("Error at {0} from {1}:\r\n{2}",
                                  handler.Uri,
                                  handler.Type.Name,
                                  exception.ToString());

        var log = request.HttpContext.Server.MapPath("/errors.txt");
        File.AppendAllLines(log, new[] { error });
    };
}

The first thing we do is turn on format extensions, allowing the Accept header to be specified on the URL (i.e. /api/todo/list.xml and /api/todo/list.json).

Next we initialise the framework and pass in a StructureMap registry. The DatabaseRegistry configures how the IDatabase dependency (used by handlers to persist the to-do list to a text file database) can be resolved at runtime.

Finally, we set up error logging using the Tinyweb.OnError delegate. In this instance, we just want to append to a log file every time an error occurs, storing the handler, the URL and the exception.

OK – Initialisation – check, let’s look at the root request handler.

RootHandler

I hope you didn’t get too excited, as the RootHandler is pretty dull. The only reason we have a RootHandler is so people new to Tinyweb running the demo actually reach the login page without having to find and enter the URL themselves. As such, RootHandler simply redirects to the LoginHandler:

public class RootHandler
{
    public IResult Get()
    {
        return Result.Redirect<LoginHandler>();
    }
}

LoginHandler

Moving right along, the next leg of the request takes us to the LoginHandler:

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

    public IResult Post(RequestContext request, string username,
                                                string password)
    {
        if (username == "admin" && password == "password")
        {
            issueCookie(username);
            return Result.Redirect<TodoListHandler>();
        }

        return Result.Redirect<LoginHandler>();
    }

    private void issueCookie(string username)
    {
        var ticket = new FormsAuthenticationTicket(username, true, 60);

        HttpContext.Current.Response.Cookies.Add(
          new HttpCookie(FormsAuthentication.FormsCookieName,
                         FormsAuthentication.Encrypt(ticket))
        );
    }
}

When a GET request is made to the LoginHandler, the login view that we saw in the screenshot is rendered. When the login form is posted back, the username and password are checked against our hard-coded values. If they don’t match, we redirect to the LoginHandler so the user can try again.

If the username and password are correct, we use ASP.NET Forms Authentication to issue a ticket and log the user in. We can then redirect the user to the to-do list, safe in the knowledge that the authentication filter will let them through.

AuthenticationFilter

Our AuthenticationFilter runs before every request and makes sure that only authenticated users can access the to-do list.

public class AuthenticationFilter
{
    IList<Type> allowedHandlers = new List<Type>
    {
        typeof(LoginHandler), typeof(ApiTodoListHandler)
    };

    public IResult Before(RequestContext context,
                          HandlerData handler)
    {
        if (!context.HttpContext.User.Identity.IsAuthenticated)
        {
            if (!allowedHandlers.Contains(handler.Type))
            {
                return Result.Redirect<LoginHandler>();
            }
        }

        return Result.None();
    }
}

This is achieved by maintaining a list of allowedHandlers and checking each request against the list. If the request handler is not in the list, a check if made to see if the user has authenticated to determine if they’re allowed access to the resource. If the user isn’t authenticated, a redirect result is returned which bypasses the rest of the execution pipeline and asks the user to log in.

TodoListHandler

When authenticated, we hit the TodoListHandler:

public class TodoListHandler
{
    IDatabase _database;

    public TodoListHandler(IDatabase database)
    {
        _database = database;
    }

    public IResult Get()
    {
        var model = _database.GetAll();

        return View.Spark(model, "Views/List.spark", "Master.spark");
    }
}

The TodoListHandler is a fairly standard handler which uses its IDatabase dependency to retrieve the current to-do list.

The DatabaseRegistry that was registered during initialisation will allow Tinyweb to supply a concrete Database at runtime. The IDatabase dependency is a file-based database for storing and retrieving the to-do items. In a real application, we might depend on some service class instead of directly depending on a data access class like this, but it’s a demo – it’s allowed to suck.

The TodoListHandler retrieves the current to-do list and renders the Views/List.spark view, passing the to-do list as the model. Let’s take a look at the view:

<viewdata model="TodoList" />

<content name="header">

    Todo List

</content>

<content name="body">

    <div if="!Model.Items.Any()" class="notification">
        Woohoo - nothing to do!
    </div>

    <div each="var todo in Model.Items" class="${todo.Complete ? 'complete' : 'todo'}">

        <div class="text">
            ${todo.Text}
        </div>

        <div class="date">
            ${todo.Date.ToString("dddd dd MMM yy")}
        </div>

        <div class="menu">
            <a href="${Url.For<TodoRemoveHandler>(new { todo.ID })}">Remove</a> |
            <a href="${Url.For<TodoUncompleteHandler>(new { todo.ID })}" if="todo.Complete">Undo</a>
            <a href="${Url.For<TodoCompleteHandler>(new { todo.ID })}" if="!todo.Complete">Complete</a>
        </div>

        <div class="clear"></div>

    </div>

    <render partial="partials/add" />

</content>

The first thing to note is line 1 where the expected view model type is declared. The view is then broken into two sections – the header and the body.

The first thing the view does is conditionally show the Woohoo – nothing to do message depending on whether the model contains any to-do items. Notice the very clean if=”” Spark syntax for performing this check.

Next, the view repeats a <div> for each of the to-do items that exist within the model. For each item, the class of the <div> is set depending on the state of the to-do item (complete or not) and the content of the <div> is built up to reflect the data about the particular item, such as the text and date.

Lastly, we render the Partials/Add.spark view which provides the <form> for creating new to-do items:

<form id="add" method="post" action="${Url.For<TodoAddHandler>()}">

    <input type="text" name="todo" />

</form>

<script type="text/coffeescript">

    $ ->
      $('#add input').focus()
      $('#add').submit ->
        false if $('#add input').val() is ""

</script>

The partial exists mainly as a demonstration of how this is done using Spark, but also to show how the application might be broken up at the view level to reuse separate elements of the system (we may want to add a to-do item in some other area of the application).

Also notice the use of the Url.For helper. The use of Url.For is encouraged as it keeps the details of the actual URL in one place and makes refactoring safer and easier.

TodoAddHandler

When a new to-do item is posted, a request will be made to /todo/add with a single value named todo in the request. The TodoAddHandler will then come to the rescue:

public class TodoAddHandler
{
    IDatabase _database;

    public TodoAddHandler(IDatabase database)
    {
        _database = database;
    }

    public IResult Post(string todo)
    {
        _database.Add(new TodoItem {Text = todo, Date = DateTime.Now});
        _database.Save();

        return Result.Redirect<TodoListHandler>();
    }
}

Much like the TodoListHandler, the TodoAddHandler takes a dependency on IDatabase and simply delegates the work of adding the new to-do item to it. After adding the item, the user is redirected back to the TodoListHandler.

The TodoRemoveHandler, TodoCompleteHandler and TodoUncompleteHandler are all alike, delegating to an IDatabase dependency to perform their single task and redirecting back to the main to-do list afterwards.

The API

The API in this example is just a single end point that gives us back the current to-do list in either JSON or XML format. As you may have noticed before, the ApiTodoListHandler is exempt (as it’s contained within the allowedHandlers list) from authentication, allowing unauthenticated users to request data from the API.

public class ApiTodoListHandler
{
    IDatabase _database;

    public ApiTodoListHandler(IDatabase database)
    {
        _database = database;
    }

    public IResult Get()
    {
        return Result.JsonOrXml(_database.GetAll());
    }
}

Nothing should be too surprising here – we have a simple handler that supports GET requests at /api/todo/list and gives us back the representation we requested via the Accept header or via an extension override.

Recap

And that’s a wrap! We’ve now built the 7 web app handlers that collaborate to form the to-do list and we’ve also created a single API handler which allows the to-do list data to be consumed outside of the core application.

We’ve implemented error logging inside Tinyweb.OnError by appending log messages to a text file, allowing us to easily diagnose any errors resulting from handlers at runtime.

Finally, we saw how a global before filter could be used in conjunction with Forms Authentication to ensure that only authenticated users could access particular resources of the system.

The full to-do demo app can be found at https://github.com/martinrue/Todo-Demo if you’d like to play around with it.

That’s All Folks

So, this was the final post in the Tinyweb series and I hope by now it has touched on enough aspects of the framework to give you a good idea of where it sits and what it’s good at. Hopefully, at least one takeaway from this series of posts will be how simple Tinyweb makes building close-to-the-metal web applications – after all, that is Tinyweb’s primary goal.

I’d love to hear from anyone using the framework and please let me know if you come across any issues or have any suggestions. And remember, Tinyweb is open source and can be forked and contributed to from the github repository.

All of the content in this series is also detailed in the official documentation, so please check there for any clarifications or give me a nudge on Twitter.