What is this? From this page you can use the Social Web links to save FubuToDo – Part 2: Forms, Controllers, View, and jQuery to a social bookmarking site, or the E-mail form to send a link via e-mail.

Social Web

E-mail

E-mail It
January 17, 2010

FubuToDo – Part 2: Forms, Controllers, View, and jQuery

Posted in: Examples, Software Development

This is the second post in a series of posts I am writing on creating a simple “To-Do” application using FubuMVC. I’ve broken down this series into the following:

  1. Part 1: Conventions, Opinions and Bootstrapping
  2. Part 2: Forms, Controllers, Views, and jQuery
  3. Part 3: Persistence

Overview

I wanted to create a simple “To Do” application using FubuMVC. In my previous post, I showed the basics for getting your controllers created, tested, and wired up. I also explained how to setup your conventions and how to bootstrap your application. This time, I’m going to talk about creating forms, actions on controllers to respond to those forms, and how to work in some jQuery.

In order to have a functioning “To-Do” application, we’re missing the following:

  1. Add/Edit/Remove Functionality
  2. Validation
  3. Persistence

We’re going to talk about #1 in this part.

Controllers

As always, let’s start with some tests. I like to group entity-related actions into more semantic urls (e.g., “Items/Add”, “Items/Edit”). I’m just going to group those actions in a single controller so I came up with the following tests for my ItemsController:

  1. When adding an item, the controller calls Insert on the IUnitOfWork
  2. When adding an item, the appropriate ViewModel is returned
  3. When editing an item, the controller calls Update on the IUnitOfWork
  4. When editing an item, the appropriate ViewModel is returned
  5. When removing an item, the controller calls Delete on the IUnitOfWork
  6. When removing an item, the appropriate ViewModel is returned

Now, I know that I want to implement the add/edit/remove functionality via a modal dialogue. I’ve found I can write reusable javascript handlers for responses like this when the result is all the same for CRUD-based operations. To make it easier, I’ve decided to make the Add/Edit/Remove methods return an instance of the JsonUnitOfWorkResult class:

JsonUnitOfWorkResult

/// <summary>
/// Represents the result of a unit of work.
/// </summary>
public class JsonUnitOfWorkResult
{
    /// <summary>
    /// Gets or sets a message describing the result.
    /// </summary>
    public string Message { get; set; }
    /// <summary>
    /// Gets or sets a flag indicating whether the unit of work was successful.
    /// </summary>
    public bool Success { get; set; }
}

And now a sample of our tests:

Test 1

/// <summary>
/// When adding an item, the controller calls Insert on the IUnitOfWork.
/// </summary>
[Test]
public void Add_Item_Calls_Insert()
{
    IUnitOfWork unitOfWork = _mockRepository.StrictMock<IUnitOfWork>();
    ItemsController controller = new ItemsController(unitOfWork,
        _mockRepository.DynamicMock<IToDoItemRepository>());

    using(_mockRepository.Ordered())
    {
        unitOfWork.Expect(uow => uow.Insert(null)).IgnoreArguments();
        unitOfWork.Expect(uow => uow.Commit());
    }

    _mockRepository.ReplayAll();

    AddItemInputModel inputModel = new AddItemInputModel
    {
        Description = "Hello, World!"
    };

    controller.Add(inputModel);

    _mockRepository.VerifyAll();
}

Test 2

/// <summary>
/// When removing an item, the appropriate ViewModel is returned.
/// </summary>
[Test]
public void Remove_Item_Sets_Result()
{
    IUnitOfWork unitOfWork = _mockRepository.StrictMock<IUnitOfWork>();
    ItemsController controller = new ItemsController(unitOfWork,
        _mockRepository.DynamicMock<IToDoItemRepository>());

    RemoveItemInputModel inputModel = new RemoveItemInputModel
    {
        ItemId = 1
    };

    JsonUnitOfWorkResult result = controller.Remove(inputModel);
    Assert.IsNotNull(result);
}

As I’ve mentioned before, Fubu has the notion of “one model in, one model out” – following this gives you a lot of control over your behaviors which I’ll explain later in this post. To make things a little easier, I created an abstract class for all of my CRUD action input models:

ItemInputModel

/// <summary>
/// Provides a base class for all <see cref="ToDoItem"/> input models.
/// </summary>
public abstract class ItemInputModel
{
    /// <summary>
    /// Gets or sets the unique identifier of the item.
    /// </summary>
    public int ItemId { get; set; }
    /// <summary>
    /// Gets or sets the description of the item.
    /// </summary>
    public string Description { get; set; }
}

The actual controller implementation is incredibly easy (admittedly I could’ve done it better). Here’s the Add method:

/// <summary>
/// Adds a new item.
/// </summary>
/// <param name="itemInputModel">The item to add.</param>
/// <returns></returns>
public JsonUnitOfWorkResult Add(AddItemInputModel itemInputModel)
{
    try
    {
        _unitOfWork.Insert(new ToDoItem
                               {
                                   Description = itemInputModel.Description
                               });

        _unitOfWork.Commit();

        return new JsonUnitOfWorkResult
                   {
                       Success = true,
                       Message = "Item added successfully"
                   };
    }
    catch (Exception exc)
    {
        return new JsonUnitOfWorkResult
                   {
                       Success = false,
                       Message = exc.Message
                   };
    }
}

Forms

Ok, we’ve got a working controller. Now let’s setup our forms. I’m going to use jQuery and jQuery UI to handle my dialogs. Let’s take a look at what I wanted the application to do:

  1. Display a list of to-do items
  2. Allow me to add a new item
  3. Allow me to specify that an item is completed or not
  4. Allow me to edit an existing item
  5. Allow me to remove an existing item

We can already do the first requirement. However, since I’m planning on doing the rest through modals, we know we’re going to have to respond to some commands and refresh that list. Let’s revisit the Home/Index.aspx view and refactor our the displaying of those items:

Step 1: Move to a partial view

ItemListViewModel

/// <summary>
/// Provides a model containing a list of <see cref="ToDoItem"/> entities.
/// </summary>
public class ItemListViewModel
{
    /// <summary>
    /// Gets or sets the associated collection of items.
    /// </summary>
    public IEnumerable<ToDoItem> Items { get; set; }
}

List.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="List.aspx.cs" Inherits="FubuToDo.Web.Controllers.Item.List" %>
<%@ Import Namespace="System.Linq"%>
<% if(Model.Items.Any()) { %>
<ul class="item-list">
<% foreach (var item in Model.Items) { %>
    <li id="<%= item.ItemId %>" class="item">
        <div class="row no-label">
            <input type="checkbox" name="Item-<%= item.ItemId %>" <%= item.IsComplete ? "checked=\"checked\"" : string.Empty %> />
            <label class="checkbox"><a href="javascript:return false;"><%= item.Description %></a></label>
        </div>
        <br class="cboth" />
    </li>
<% } %>
</ul>
<% } else { %>
<span>No items.</span>
<% } %>

Step 2: Defer list rendering to an ajax call

In order to do this, I removed the Items property off of the IndexViewModel and removed the list rendering from the Home/Index.aspx view. Let’s take a look at the following snippets:

Index.aspx

<div id="blog-entry-container">
    <div class="success" style="display:none;"></div>
    <div class="error" style="display:none;"></div>
    <h2>To-Do List</h2>
    <div id="Item-List">
    </div>
    <button class="button-prim-med" type="button" value="Add" id="Add-New">
        <span>Add New</span>
    </button>
</div>
<div id="ItemDialog" style="display: none;">
    <form id="frmItem" method="post">
        <input type="hidden" id="hdnItemId" name="ItemId" />
        <fieldset>
            <div class="row">
                <label>
                    Description:</label>
                <input type="text" id="Item-Description" name="Description" class="text required" />
            </div>
        </fieldset>
    </form>
</div>

Now the Home/Index.aspx also references the Scripts/home/index.js file which will handle loading this information. Let’s take a look:

home.js

function reloadItems() {
    var $list = $('#Item-List');
    $list.html('<span>Loading...</span>');

    $.ajax({
        url: '/Items/List',
        type: 'GET',
        contentType: "application/json; charset=utf-8",
        data: { random: new Date().getUTCMilliseconds() },
        success: function(data) {
            $list.html(data);
            bindEditLinks();            bindItemCheckboxes();
        },
        timeout: 5000,
        error: function(xmlObj, textStatus, errorThrown) {
            $list.html('<span>Error</span>');
        }
    });
};

Our Items/List.aspx view returns a simple html snippet. Since we haven’t told Fubu any differently, it treats it as HTML and the appropriate content type is set. Our simple GET request will receive this content and set the contents of that empty div we created in our Home/Index.aspx view. If you take a look at the file a little deeper, you’ll see I call reloadItems on document ready.

You might have noticed the additional “random” parameter that we’re sending in. This this is a GET request, most browsers will cache the result. I threw this in there to avoid bang-head-into-desk syndrome.

Ok, now when I run it I get my items rendering through my ajax call. Now to wire up my dialogs and get it all working.

Adding/Editing an Item

I’ve setup the form in a way that could be used to add or edit an item. Since our input models are virtually identical, all we have to do is change the action of the form. We already have our dialog worked out (see Home/Index.aspx) so let’s wire up our buttons and links:

Wiring up Add Item

function addItem() {
    var itemForm = $('#frmItem');
    var itemDialog = $('#ItemDialog');
    itemForm.clearForm();

    // set up buttons
    itemDialog.dialog('option', 'buttons', {
        'Create': function() {
            itemForm.attr('action', '/Items/Add');
            itemForm.submit();
        },
        Cancel: function() {
            itemForm.clearForm();
            $(this).dialog('close');
        }
    });
    itemDialog.dialog('option', 'title', 'Add Item');

    // show the dialog
    itemDialog.dialog('open');
};

Wiring up Edit Item

function bindEditLinks() {
    $('.item-list > .item > div.row > label > a').click(function() {
        var itemForm = $('#frmItem');
        var itemDialog = $('#ItemDialog');

        var listItemId = $(this).parent().parent().parent().attr('id');
        itemForm.find('#hdnItemId').val(listItemId);
        itemForm.find('#Item-Description').val($(this).html());

        // set up buttons
        itemDialog.dialog('option', 'buttons', {
            'Save': function() {
                itemForm.attr('action', '/Items/Edit');
                itemForm.submit();
            },
            'Remove': function() {
                itemForm.attr('action', '/Items/Remove');
                itemForm.submit();
            },
            Cancel: function() {
                itemForm.clearForm();
                $(this).dialog('close');
            }
        });
        itemDialog.dialog('option', 'title', 'Edit Item');

        // show the dialog
        itemDialog.dialog('open');
    });
};

Wiring up Item Complete:

function bindItemCheckboxes() {
    $(".item-list > .item > div.row > input[type='checkbox']").click(function() {
        var listItemId = $(this).parent().parent().attr('id');
        var description = $(this).parent().find('label > a').html();

        $.ajax({
            url: '/Items/Edit',
            type: 'POST',
            dataType: 'json',
            data: {
                IsComplete: document.getElementById($(this).attr('id')).checked,
                ItemId: listItemId,
                Description: description
            },
            success: function(response) {
                jsonTransactionHandler(response);
            },
            error: function(responseObj, textError, errorCode) {
                $('.error').html(textError).fadeIn('slow', function() {
                    setTimeout(function() { $('.error').fadeOut(); }, 4000);
                });
            }
        });
    });
};

Three things here to note here:

  1. bindEditLinks is called in the reloadItems function.
  2. bindItemCheckboxes is also called in the reloadItems function. This simply submits a POST message to the Edit action with the appropriate fields.
  3. The ‘Remove’ button sets the action of the form to ‘Items/Remove’ and submits it which will successfully remove the item.

Let’s take a look at how we have our form being submitted and handled:

Form Initialization

// initialize the form
$('#frmItem').ajaxSubmit({
    dataType: 'json',
    success: function(response) {
        jsonTransactionHandler(response);
        $('#ItemDialog').dialog('close');
        reloadItems();
    },
    error: function(responseObj, textError, errorCode) {
        $('.error').html(textError).fadeIn('slow', function() {
            setTimeout(function() { $('.error').fadeOut(); }, 4000);
        });
    }
});

Result Handler

function jsonTransactionHandler(response) {
    if (response.Success) {
        $('.success').html(response.Message);
        $('.success').fadeIn('slow', function() {
            setTimeout(function() { $('.success').fadeOut(); }, 4000);
        });
    } else {
        $('.error').html(response.Message);
        $('.error').fadeIn('slow', function() {
            setTimeout(function() { $('.error').fadeOut(); }, 4000);
        });
    }
};

Any successful HTTP response from the controller will trigger the jsonTransactionHandler (ff you remember, we set up our JsonUnitOfWorkResult to let us handle all responses the same way). The dialog then gets closed and the items get reloaded to reflect the changes.

Getting it to run

There are only two changes we’ll need to make to get these changes up and running w/ Fubu:

FubuStructureMapBoostrapper (line 32):

ObjectFactory.Initialize(x =>
                             {
                                 x.For<IToDoItemRepository>().Use<FakeToDoItemRepository>();
                                 x.For<IUnitOfWork>().Use<FakeUnitOfWork>();
                             });

We’re registering a fake unit of work for our ItemsController.

FubuToDoRegistry (last line of the constructor):

JsonOutputIf.WhenTheOutputModelIs<JsonUnitOfWorkResult>();

This tells Fubu that we want a JSON response whenever it encounters our JsonUnitOfWorkResult class.

That’s it! We’re all done. In my next part I’ll talk implementing validation and switching over our repository and unit of works classes to enable some real persistence.

Source code

You can download the source for this part at: http://svn.joshua-arnold.com/fubutodo/tags/part2

Technorati Tags: , ,


Return to: FubuToDo – Part 2: Forms, Controllers, View, and jQuery