Create a Movie Database Application with FubuMVC

I decided to have a look at FubuMVC, mainly to see what it offers over ASP.NET MVC. To do so I tasked myself with creating a simple Movies app, ala Stephen Walter’s Movie Database ASP.NET MVC tutorial.

So the requirements of the application are:

  1. List a set of movie database records
  2. Create a new movie database record (with validation!)
  3. Edit an existing movie database record

Quick, show me the code

You can download the source code from bitbucket.

The finished application is running here on AppHarbor. Click on the Title of a Movie to edit it – NB. the first 3 sample movies are read-only.

Let’s get started

In Visual Studio, File -> New Project -> Web -> ASP.NET Empty Web Application. Name it MovieApp.

Next, add FubuMVC via nuget (right-click MovieApp project, Manage NuGet packages, and search for FubuMVC).

Press F5 to check you get the FubuMVC welcome page.

Create the database

As per Stephen’s tutorial, I’m going to use SQL Server and Entity Framework for the data layer.

See his tutorial for instructions on how to set that up using Visual Studio. If you have SQL Server Management Studio then create a new database called MoviesDB, and run the following script to create a table.

CREATE TABLE [dbo].[Movies](
 [Id] [int] IDENTITY(1,1) NOT NULL,
 [Title] [nvarchar](255) NOT NULL,
 [Director] [nvarchar](255) NOT NULL,
 [DateReleased] [datetime] NOT NULL,
 [Synopsis] [ntext] NOT NULL,
 [ImageUri] [nvarchar](255) NOT NULL,
 CONSTRAINT [PK_Movies] PRIMARY KEY CLUSTERED
(
 [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[Movies] ON
INSERT [dbo].[Movies] ([Id], [Title], [Director], [DateReleased], [Synopsis], [ImageUri]) VALUES (1, N'Star Wars', N'George Lucas', CAST(0x00006DDC00000000 AS DateTime), N'Part IV in George Lucas'' epic, Star Wars: A New Hope opens with a Rebel ship being boarded by the tyrannical Darth Vader. The plot then follows the life of a simple farm boy, Luke Skywalker, as he and his newly met allies (Han Solo, Chewbacca, Obi-Wan Kenobi, C-3PO, R2-D2) attempt to rescue a Rebel leader, Princess Leia, from the clutches of the Empire. The conclusion is culminated as the Rebels, including Skywalker and flying ace Wedge Antilles make an attack on the Empire''s most powerful and ominous weapon, the Death Star.', N'http://ia.media-imdb.com/images/M/MV5BMTU4NTczODkwM15BMl5BanBnXkFtZTcwMzEyMTIyMw@@._V1._SY317_.jpg')
INSERT [dbo].[Movies] ([Id], [Title], [Director], [DateReleased], [Synopsis], [ImageUri]) VALUES (2, N'Pulp Fiction', N'Quentin Tarantino', CAST(0x0000861D00000000 AS DateTime), N'Jules Winnfield and Vincent Vega are two hitmen who are out to retrieve a suitcase stolen from their employer, mob boss Marsellus Wallace. Wallace has also asked Vincent to take his wife Mia out a few days later when Wallace himself will be out of town. Butch Coolidge is an aging boxer who is paid by Wallace to lose his next fight. The lives of these seemingly unrelated people are woven together comprising of a series of funny, bizarre and uncalled-for incidents', N'http://ia.media-imdb.com/images/M/MV5BMjE0ODk2NjczOV5BMl5BanBnXkFtZTYwNDQ0NDg4._V1._SY317_CR4,0,214,317_.jpg')
INSERT [dbo].[Movies] ([Id], [Title], [Director], [DateReleased], [Synopsis], [ImageUri]) VALUES (3, N'Memento', N'Christopher Nolan', CAST(0x00008FD100000000 AS DateTime), N'Memento chronicles two separate stories of Leonard, an ex-insurance investigator who can no longer build new memories, as he attempts to find the murderer of his wife, which is the last thing he remembers. One story line moves forward in time while the other tells the story backwards revealing more each time.', N'http://ia.media-imdb.com/images/M/MV5BMjA3MTkzMzI3N15BMl5BanBnXkFtZTcwNzYwMzQ4MQ@@._V1._SY317_.jpg')
SET IDENTITY_INSERT [dbo].[Movies] OFF

I’ve added a couple of extra columns – ImageUri and Synopsis.

Create the Model

Back in Visual Studio, right-click on MovieApp and choose Add -> New Folder, and name it ‘Model’.

Right-click the Model folder, and choose Add -> New Item…, then Data -> ADO.NET Entity Data Model. Name it Movies.edmx. Select Generate from database, click Next. Choose the MoviesDB you just created, and click Next. Tick the Tables checkbox, and click Finish. Now click on the entity ‘Movy’. Open the Properties pane, and change its name to ‘Movie’ and the Entity Set Name to ‘Movies’.

With me so far? We should have something like this:

Create the FubuMVC Controller

FubuMVC isn’t as strict about what makes a controller as ASP.NET MVC. In fact, that is one of fubu’s big features – you can define your own conventions about what make a Controller (or a Handler in fubu-parlance). But in the interests of keeping this tutorial simple, I’m going to stick to the ‘ClassesSuffixedWithController’ convention.

Right-click MovieApp, Add -> New Folder, named ‘Movies’.
Right-click Movies folder, Add -> Class, named IndexController.cs:

using System.Collections.Generic;
using System.Linq;
using MovieApp.Model;

namespace MovieApp.Movies
{
    public class IndexController
    {
        private MoviesDBEntities _db = new MoviesDBEntities();

        public ViewModel Index(InputModel model)
        {
            return new ViewModel { Movies = _db.Movies.ToList() };
        }

        public class InputModel
        {
        }

        public class ViewModel
        {
            public IEnumerable<Movie> Movies { get; set; }
        }
    }
}

Because of the namespace (MoviesApp.Movies) and the name of the Action (public ViewModel Index(InputModel model)), the Index method is going to get hit when we navigate to /Movies/Index.

One of the best practices of FubuMVC is the OMIOMO rule – One Model In, One Model Out. This means every controller Action should have one unique model passed to it, and should return a different model.

My preference is to keep these Model classes nested in the Controller class to reduce namespace clutter, but you don’t have to.

So you can see that the Index method takes an instance of IndexController.Index, and returns an instance of IndexController.ViewModel.

Add a ViewEngine

FubuMVC supports the Spark view engine, the Web Forms view engine, and as of writing the Razor view engine is a work in progress.
I didn’t feel like learning Spark, and since Razor isn’t finished yet, I’ll use the Web Forms view engine (ala ASP.NET MVC1), which means .aspx files with a code-behind .aspx.cs – sorry about that.

Add the FubuMVC.WebForms NuGet package.

Now open /ConfigureFubuMVC.cs, and add the following lines:

public ConfigureFubuMVC()
{
    ....
    // View Engine
    Import<WebFormsEngine>();
}

Add the ‘Index’ View

Now right-click the Movies folder, Add -> New Item. Choose Web Form, and name it Index.aspx.
Delete the Index.aspx.designer.cs – we don’t need it.
Open the Index.aspx.cs, delete the existing code and replace it with this:

using FubuMVC.WebForms;
namespace MovieApp.Movies
{
    public partial class Index : FubuPage<IndexController.ViewModel>
    {
    }
}

This tells Index.aspx that its Model is an IndexController.ViewModel.
Now open Index.aspx, and replace it with this:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MovieApp.Movies.Index" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title>All Movies</title>
</head>
<body>
    <% foreach (var movie in Model.Movies)
       { %>
            <h3><%: movie.Title %></h3>
            <b>Director:</b> <%: movie.Director %><br />
            <b>Date released:</b> <%: movie.DateReleased.ToString("MMM yyyy") %><br />
            <%: movie.Synopsis %><br />
    <% } %>
</body>
</html>

Notice that in the view we can use the ‘Model’ keyword just like in ASP.NET MVC.
Now press Ctrl-F5, and navigate to /movies/index, and you should see:

Remember, our Action is /Movies/Index because of the Controller’s namespace (MovieApp.Movies) + the name of the Action method (public ViewModel Index(InputModel model)). The Webforms view engine will look for a corresponding View in the same path – ~/Movies/, which has the same underlying Model type as the one returned by the Index action method – “IndexController.ViewModel”. Our Index.aspx.cs sets this (public partial class Index : FubuPage<IndexController.ViewModel>).

Add an Edit Movie page

Another FubuMVC recommendation is to have a separate Controller/Handler per action.
So let’s Right click the Movies folder, Add -> Class, and name it EditController.cs:

using System.Linq;
using FubuMVC.Core.Continuations;
using MovieApp.Model;

namespace MovieApp.Movies
{
    public class EditController
    {
        private MoviesDBEntities _db = new MoviesDBEntities();

        public Movie Get_Movies_Edit_MovieId(InputModel input)
        {
            return _db.Movies.FirstOrDefault(x => x.Id == input.MovieId);
        }

        public FubuContinuation Post_Movies_Edit(Movie input)
        {
            var movie = _db.Movies.First(m => m.Id == input.Id);
            movie.Title = input.Title;
            movie.Director = input.Director;
            movie.DateReleased = input.DateReleased;
            movie.Synopsis = input.Synopsis;

            _db.SaveChanges();
            return FubuContinuation.RedirectTo(new IndexController.InputModel());
        }

        public class InputModel
        {
            public int MovieId { get; set; }
        }
    }
}

This time, the name of the Action (Get_Movies_Edit_MovieId) follows another one of Fubu’s built-in conventions – underscores are replaced by /. So this Action will get hit when you navigate to /Movies/Edit/1. The Get at the front means we are restricting to HTTP GETs only.

Now let’s add the view.
Add a new Web Form as before, call it Edit.aspx. Edit.aspx.cs:

using FubuMVC.WebForms;
using MovieApp.Model;

namespace MovieApp.Movies
{
    public partial class Edit : FubuPage<Movie>
    {
    }
}

Edit.aspx:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Edit.aspx.cs" Inherits="MovieApp.Movies.Edit" %>
<%@ Import Namespace="MovieApp.Movies" %>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Edit Movie</title>
</head>
<body>
<% if (Model != null)
   { %>
    <%= this.FormFor(Model) %>
        <input type="hidden" name="Id" value="<%= Model.Id %>" />
        <fieldset>
            <legend>Edit Movie</legend>
            <p>
                <%= this.LabelFor(m => m.Title) %><%= this.InputFor(m => m.Title) %>
            </p>
            <p>
                <%= this.LabelFor(m => m.Director) %><%= this.InputFor(m => m.Director) %>
            </p>
            <p>
                <%= this.LabelFor(m => m.DateReleased) %><input type="text" name="DateReleased" value="<%: Model.DateReleased.ToShortDateString() %>" />
            </p>
            <p>
                <%= this.LabelFor(m => m.ImageUri) %><%= this.InputFor(m => m.ImageUri) %>
            </p>
            <p>
                <%= this.LabelFor(m => m.Synopsis) %><textarea name="Synopsis" rows="15" cols="70"><%: Model.Synopsis %></textarea>
            </p>
        </fieldset>
        <input type="submit" value="Save" />
        <%= this.LinkTo(new IndexController.InputModel()).Text("Cancel") %>
    <%= this.EndForm() %>
<% } %>
</body>
</html>

Notice that this time we’re using some of FubuMVC’s built-in HtmlTag helpers – this.FormFor(), this.LabelFor(), this.InputFor() etc.
One thing I like is the

<%= this.FormFor(Model) %>

This renders to:

   <form id="mainForm" method="post" action="/movies/edit">

Fubu is smart enough to put action=”/movies/edit” on the form – because FormFor(Model) is saying give me a form for the Model, and it knows the Model of type Movie, so it looks for any Action that takes a Movie as input. The Action it wants is defined in the EditController (public FubuContinuation Post_Movies_Edit(Movie input)) but it’s important to note that this Action could be anywhere and could be named anything. For example, if I rename the Post_Movies_Edit() method to “DumDeeDoo()”, then FormFor renders the form with

<form id="mainForm" method="post" action="/movies/dumdeedoo">

Note that the name of the Post method is following the same convention as the Get – Post_Movies_Edit exists at /Movies/Edit and will accept HTTP POSTs only. It returns a FubuContinuation which redirects us back to the Home page (/Movies/Index).

Now let’s look at the Cancel link at the bottom.

<%= this.LinkTo(new IndexController.InputModel()).Text("Cancel") %>

Renders to:

<a href="/movies/index">Cancel</a>

As with the FormFor, with “LinkTo” I ask for a link to an InputModel. This differs from the ASP.NET MVC approach of asking for an Html.ActionLink to a specific Action method. Fubu’s way is pretty cool – cos if I rename my actions or restructure the site, the links will still render the correct URLs.

Add Validation

Unfortunately validation isn’t included in the box, but we can add it. But it’s a pain in the ass.
First add the FubuMVC.Validation NuGet package.
Next change your /App_Start/FubuMVC.cs

using Bottles;
using FubuMVC.Core;
using FubuMVC.StructureMap;
using FubuValidation.StructureMap;
using StructureMap.Configuration.DSL;

// You can remove the reference to WebActivator by calling the Start() method from your Global.asax Application_Start
[assembly: WebActivator.PreApplicationStartMethod(typeof(MovieApp.App_Start.AppStartFubuMVC), "Start", callAfterGlobalAppStart: true)]

namespace MovieApp.App_Start
{
    public static class AppStartFubuMVC
    {
        public static void Start()
        {
            // FubuApplication "guides" the bootstrapping of the FubuMVC
            // application
            FubuApplication.For<ConfigureFubuMVC>() // ConfigureFubuMVC is the main FubuRegistry
                                                    // for this application.  FubuRegistry classes
                                                    // are used to register conventions, policies,
                                                    // and various other parts of a FubuMVC application

                // FubuMVC requires an IoC container for its own internals.
                .StructureMapObjectFactory(configure => configure.AddRegistry<MovieAppRegistry>())
                .Bootstrap();

			// Ensure that no errors occurred during bootstrapping
			PackageRegistry.AssertNoFailures();
        }

        public class MovieAppRegistry : Registry
        {
            public MovieAppRegistry()
            {
                Scan(x =>
                {
                    x.TheCallingAssembly();
                    x.WithDefaultConventions();
                });

                this.FubuValidation();
            }
        }
    }
}

Now add these lines to /ConfigureFubuMVC.cs

public ConfigureFubuMVC()
{
    ...
    Views.TryToAttachWithDefaultConventions()
                .RegisterActionLessViews(t => t.ViewModelType == typeof(FubuValidation.Notification));

    this.Validation(validation => {
        // Include all action calls that: 1) have input and 2) whose input models contain the string "ViewModel"
        validation
            .Actions
            .Include(call => call.HasInput && call.InputType().Name.Contains("ViewModel"));

        // This DSL reads as follows...
        // When handling failures:
        //  If the input type of the action call is not null and the name of the model contains the string "ViewModel",
        //  Then Transfer to a behavior chain that is resolved by my custom HandlerModelDescriptor class
        validation
            .Failures
            .If(f => f.InputType() != null && f.InputType().Name.Contains("ViewModel"))
            .TransferBy<HandlerModelDescriptor>();
    });
}

The above is saying that we’re adding validation to any method that takes an input, and has “ViewModel” in its name.
The method it will redirect to if validation fails is determined by the HandlerModelDescriptor class, which we will now add.

/HandlerModelDescriptor.cs

using System;
using System.Linq;
using FubuMVC.Core.Registration;
using FubuMVC.Validation;

namespace MovieApp
{
    public class HandlerModelDescriptor : IFubuContinuationModelDescriptor
    {
        private readonly BehaviorGraph _graph;

        public HandlerModelDescriptor(BehaviorGraph graph)
        {
            _graph = graph;
        }

        public Type DescribeModelFor(ValidationFailure context)
        {
            // Remember, behavior chains can be identified by the input model type
            // The IFubuContinuationModelDescriptor interface is used to describe the input model type of the chain
            // that we want to transfer to

            // we're going to query the BehaviorGraph to find the corresponding GET for the POST
            // obviously, we'd need to make this smarter but this is just a simple example
            var targetName = context.Target.HandlerType.Name;
            var getCall = _graph
                .Behaviors
                .Where(chain => chain.FirstCall() != null && chain.FirstCall().HandlerType.Name == targetName
                    && chain.Route.AllowedHttpMethods.Contains("GET"))
                .Select(chain => chain.FirstCall())
                .FirstOrDefault();

            if(getCall == null)
            {
                return null;
            }

            return getCall.InputType();
        }
    }
}

This is called when validation fails. It returns the Input Type of the method to pass control to.
To find it, it looks for an HTTP GET method inside the same Controller as the method doing the POSTing.

Now we need to add a View which is called when validation is needed.
Add a new folder, /Shared.
/Shared/ValidationSummary.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ValidationSummary.aspx.cs" Inherits="MovieApp.Shared.ValidationSummary" %>

<% if (Model.AllMessages.Any()) { %>
    <ul style="color:Red">
        <% foreach (var msg in Model.AllMessages)
           { %>
                <li><%= msg %></li>
        <% } %>
    </ul>
<% } %>

/Shared/ValidationSummary.aspx.cs

using FubuMVC.WebForms;
namespace MovieApp.Shared
{
    public partial class ValidationSummary : FubuPage <FubuValidation.Notification>
    {
    }
}

Add a Create Movie page (with validation)

/Movies/CreateController.cs

using System;
using FubuMVC.Core.Continuations;
using FubuValidation;
using MovieApp.Model;

namespace MovieApp.Movies
{
    public class CreateController
    {
        private MoviesDBEntities _db = new MoviesDBEntities();

        public ViewModel Get_Movies_Create(InputModel input)
        {
            return new ViewModel { DateReleased = DateTime.Parse("01/01/2001") };
        }

        public FubuContinuation Post_Movies_Create(ViewModel input)
        {
            var movie = new Movie
            {
                Title = input.Title,
                Director = input.Director,
                DateReleased = input.DateReleased,
                ImageUri = input.ImageUri,
                Synopsis = input.Synopsis
            };

            _db.Movies.AddObject(movie);
            _db.SaveChanges();

            return FubuContinuation.RedirectTo(new IndexController.InputModel());
        }

        public class InputModel
        {
        }

        public class ViewModel
        {
            [Required]
            public string Title { get; set; }
            [Required]
            [MaximumStringLength(50)]
            public string Director { get; set; }
            [Required]
            public DateTime DateReleased { get; set; }
            [Required]
            public string ImageUri { get; set; }
            [Required]
            public string Synopsis { get; set; }
        }
    }
}

I’ve taken a best practice approach with the Create – by binding to a view model (ViewModel), instead of binding directly to the Movie domain object. This also allows me to add FubuValidation attributes like [Required].

/Movies/Create.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Create.aspx.cs" Inherits="MovieApp.Movies.Create" %>
<%@ Import Namespace="MovieApp.Movies" %>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Create Movie</title>
</head>
<body>
    <%= this.FormFor(Model) %>
        <fieldset>
            <legend>Create Movie</legend>
            <%= this.Partial<FubuValidation.Notification>() %>
            <p>
                <%= this.LabelFor(m => m.Title) %><%= this.InputFor(m => m.Title) %>
            </p>
            <p>
                <%= this.LabelFor(m => m.Director) %><%= this.InputFor(m => m.Director) %>
            </p>
            <p>
                <%= this.LabelFor(m => m.DateReleased) %><input type="text" name="DateReleased" value="<%: Model.DateReleased.ToShortDateString() %>" />
            </p>
            <p>
                <%= this.LabelFor(m => m.ImageUri) %><%= this.InputFor(m => m.ImageUri) %>
            </p>
            <p>
                <%= this.LabelFor(m => m.Synopsis) %><textarea name="Synopsis" rows="15" cols="70"><%: Model.Synopsis %></textarea>
            </p>
        </fieldset>
        <input type="submit" value="Save" /> <%= this.LinkTo(new IndexController.InputModel()).Text("Cancel") %>
    <%= this.EndForm() %>
</body>
</html>

/Movies/Create.aspx.cs

using FubuMVC.WebForms;
namespace MovieApp.Movies
{
    public partial class Create : FubuPage<CreateController.ViewModel>
    {
    }
}

If all works correctly, you should be able to navigate to /Movies/Create, and see the following:

and then if we don’t enter a Required field, we get the Validation summary at the top:

Unfortunately that’s all you get for validation – no message alongside the invalid field. And the values which the user previously entered aren’t persisted, which is annoying.

Finishing touches

I’ll add a quick skin using one of the great templates at freecsstemplates.org. I’ll also add a link to the Create, and Edit pages, and also a Delete Movie button.

You can see the sample application running here on AppHarbor. Click on the Title of a Movie to edit it – NB. the first 3 sample movies are read-only.

You can download the source code from bitbucket.