How to create a simple blog using ASP.NET MVC - Part II

Table of Contents

1. Introduction

In the first part of the series, we have completed the basic functionalities of the blog. We have completed the functionalities to display the latest blog posts, display posts based on categories, tags or search for interested posts. We also created sidebar widgets to display the categories, tags and latest posts.

In this part, we are going to see how we can create an admin console to manage posts, categories and tags. We need a grid control to display the posts, categories and tags. We can't use the ASP.NET grid control because they works based on ViewState. We are going to use a jquery plugin called jQGrid which provides many functionalities required by a typical grid like pagination, sorting, filtering, column resizing and much more. jQGrid is easy to use and most importantly it has a pretty good documentation.

JustBlog Admin Console

JustBlog Admin Console

We'll see how to secure the admin page through Forms Authentication. Not only that, we'll also see how to write unit tests for controller actions using NUnit and Rhino Mocks.

2. Part II - Create an admin console to manage posts, categories and tags

The following are the user stories we are going to implement in this part.

2.1 User Stories

Story #1 - Implement login page

Story #2 - Implement functionalities to add, edit and delete posts

Story #3 - Implement functionalities to add, edit and delete categories

Story #4 - Implement functionalities to add, edit and delete tags

Let's start one by one.

3. Story #1 - Implement login page

In this story, we are going to implement the login/logout functionalities for our blog. We will also see how to write unit tests for the controller actions which will give you enough idea about the things that you should consider while writing code that needs to be unit testable.

ASP.NET MVC framework is built on top of ASP.NET. The features available in ASP.NET can be used in ASP.NET MVC as well. The Forms Authentication in ASP.NET is achieved through the FormsAuthentication class which can also be used in ASP.NET MVC. We are going to assume that there is only one administrator who is going to manage the blog and therefore we are going to store the credentials in web.config.

The following are the tasks we are going to accomplish in this story.

1. Create an action to display the login view
2. Create the login view
3. Create an action that handles the login form submit
4. Create an action to logout the admin
5. Configure Forms Authentication in web.config
6. Write unit tests for actions

3.1 Create an action to display the login view

Let's create a new controller called AdminController that's going to house all the actions required for admin operations.

Create Admin Controller

Create Admin Controller

Except the login actions, the other actions are allowed to be accessed only by the admin. We can secure those actions by applying Authorize attribute over them. If there are too many actions that has be secured in a controller then we can apply the Authorize attribute over the controller. The actions that don't needs to be secured can be by-passed by applying AllowAnonymous attribute. Applying just the Authorize attribute will not protect our actions; we have to configure Forms Authentication in web.config, which we will see soon.

In our AdminController most of the actions needs to be secured so let's apply the Authorize attribute over it.

using System.Web.Mvc;

namespace JustBlog.Controllers
{
    [Authorize]
    public class AdminController : Controller
    {

    }
}

Listing 1. Decorating AdminController with Authorize attribute

Authorize attribute

Authorize attribute is a type of filter which implements IAuthorizationFilter. These filters are executed very earlier than any other filter. Internally this filter checks whether the user is authenticated and authorized, if not it throws a 401 exception.

We can pass the users and roles who are authorized to access as a comma separated string to the Authorize attribute.

For ex.

[Authorize(Users = "Vijay")]

[Authorize(Roles = "Author, Admin")]

Frequently in enterprise applications we may have to create our own custom authorization filter. In those cases it's better to extend the built-in AuthorizeAttribute class instead of implementing the IAuthorizationFilter. The reason is that the built-in AuthorizeAttribute class does some workaround to avoid caching issue.

Create a new action with the name Login that's going to return a view which contains the login form.

using System.Web.Mvc;

namespace JustBlog.Controllers
{
    [Authorize]
    public class AdminController : Controller
    {
        [AllowAnonymous]
        public ActionResult Login(string returnUrl)
        {
            if (User.Identity.IsAuthenticated)
            {
                if (!String.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
                    return Redirect(returnUrl);

                return RedirectToAction("Manage");
            }

            ViewBag.ReturnUrl = returnUrl;

            return View();
        }
    }
}

Listing 2. Login action

Here is what we are doing in the Login action. If the admin is already authenticated we are redirecting him to the url that he was trying to access else we are returning the login view to the user. We have used the Url.IsLocalUrl method to avoid the user being redirected to some malicious site (open redirection attacks). You can learn about open redirection attacks from here. If the url is not local then we are redirecting him to the Manage action (which we will create in the upcoming story).

3.1.1 Fix the routes

All the routes currently we configured in RouteConfig.cs direct the requests to blog controller. We have to define a new route to direct requests for the login action. Note that, the login route has to be placed above the default one as shown below.

...

routes.MapRoute(
	"Login",
	"Login",
	new { controller = "Admin", action = "Login" }
);

// default route
routes.MapRoute(
	"Action",
	"{action}",
	new { controller = "Blog", action = "Posts" }
);

Listing 3. Configuring routes for Login action

3.2 Create the login view

We need a view model for our login view to store the username and password. Create a class with name LoginModel under the Models folder.

namespace JustBlog.Models
{
    public class LoginModel
    {
	    public string UserName { get; set; }

	    public string Password { get; set; }
    }
}

Listing 4. LoginModel

We need to apply some basic validations to both the properties. ASP.NET MVC provides built-in support for validations through data annotations.

DataAnnotations

Data annotations are attributes exists in the System.ComponentModel.DataAnnotations assembly. They have a bunch of built-in validation attributes like Required, Range, RegularExpression and others.

Ex.

[Required]
[StringLength(50)]
public string Name { get; set; }

We can even create our own custom validation attributes.

Both the UserName and Password properties are required. We can apply the required validation by decorating those properties with the Required validation attribute as shown below.

namespace JustBlog.Models
{
    public class LoginModel
    {
	    [Required]
	    public string UserName { get; set; }

	    [Required]
	    public string Password { get; set; }
    }
}

Listing 5. LoginModel with validation attributes

The System.ComponentModel.DataAnnotations assembly not only contains attributes for validation but also they contain attributes that affects the display behaviors of a property. For example, the Display attribute is used to specify the text that should be displayed in the labels when the property is rendered using html helpers.

using System.ComponentModel.DataAnnotations;

namespace JustBlog.Models
{
    public class LoginModel
    {
	    [Required(ErrorMessage = "User name is required")]
	    [Display(Name = "User name (*)")]
	    public string UserName { get; set; }

	    [Required(ErrorMessage = "Password is required")]
	    [Display(Name = "Password (*)")]
	    public string Password { get; set; }
    }
}

Listing 6. Display attribute

Both the server-side and client-side validations are taken care by data-annotation attributes. For client-side validation we should include the necessary javascript files. Also we should set both the ClientValidationEnabled and UnobtrusiveJavaScriptEnabled keys to true in web.config.

<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />

Listing 7. Configuration to enable client-side validation

Being our view model ready, we can now easily complete the login view!

Create a login view by right-clicking inside the Login action and selecting "Add View". To keep things simple we don't need any layout for login view so leave the "Use a layout or master page" checkbox in the dialog unchecked.

Create Login View

Create Login View

Replace the content of the Login view as below. The explanation is given below the markup.

@model JustBlog.Models.LoginModel

@{
	Layout = null;
}

<!DOCTYPE html>

<html>

<head>
	<meta name="viewport" content="width=device-width" />
	<title>Login</title>

	<link href="@Url.Content("~/Content/themes/simple/admin.css")" rel="stylesheet" />

	<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js"></script>
	<script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.10.0/jquery.validate.min.js"></script>
	<script src="http://ajax.aspnetcdn.com/ajax/mvc/3.0/jquery.validate.unobtrusive.min.js"></script>
</head>

<body>
<div id="login-div">
	@Html.ValidationSummary(true)

	@using (Html.BeginForm())
	{
		<fieldset>
			<legend>Login</legend>
			<p>
				@Html.LabelFor(m => m.UserName)
				@Html.TextBoxFor(m => m.UserName)
				@Html.ValidationMessageFor(m => m.UserName)
			</p>

			<p>
				@Html.LabelFor(m => m.Password)
				@Html.PasswordFor(m => m.Password)
				@Html.ValidationMessageFor(m => m.Password)
			</p>

			@Html.Hidden("ReturnUrl")
			@Html.AntiForgeryToken()

			<p>
				<input type="submit" value="Login" />
			</p>
		</fieldset>
	}
</div>
</body>
</html>

Listing 8. Login view

Let me explain about the things we are doing in the Login view. We have created a simple stylesheet admin.css that contains some styles associated with both the login and manage views. You can copy the styles from here or from attached source-code.

We have included three javascript libraries for unobtrusive client-side validation.

1. jquery
2. jquery validation library
3. Microsoft's jquery unobtrusive validation library

You can learn more about unobtrusive client-side validation from here.

Let see about the html helpers that we have used in the view.

3.2.1 Html.ValidationSummary

This helper is used to display the validation errors. As default, the helper displays both the model and property validation errors. Passing true to an overloaded version makes it to display only the model validation errors.

The ValidationSummary helper displays the errors as an unordered list as shown below,

Validation Summary

Validation Summary

3.2.2 Html.BeginForm

This helper displays a html form. This is the only built-in html-helper that we can wrap with the using statement. There are dozen of overloaded versions available which takes controller name, action name, route values and html attributes. If we don't pass the controller and action names then the form is submitted to the same action that rendered it unless we have another action with the same name and decorated with the HttpPost attribute.

3.2.3 Html.LabelFor

This htmlhelper is used to render the <label> element for a property. The LabelFor helper uses the Name property that we specified in the Display attribute as the label text.

For example the below Razor statement,

@Html.LabelFor(m => m.UserName)

Listing 9. LabelFor htmlhelper

will render the following html.

<label for="UserName">User name (*)</label>

Listing 10. Rendered html

3.2.4 Html.TextBoxFor and Html.PasswordFor

We already saw about Html.TextBoxFor in Part I and it's used to render textbox for a property. Html.PasswordFor is similar like that which is used to render <input> element with type "password" for a property.

3.2.5 Html.ValidationMessageFor

This helper is used to display validation error messages for a property. Unlike the ValidationSummary helper this one is used to display the validation error messages for a particular property near to the field.

Validation Errors

Validation Errors

3.2.6 Html.AntiForgeryToken

Anti-Forgery tokens are used to protect a site against Cross Site Request Forgery (CSRF) attacks. The AntiForgeryToken helper renders a hidden field and a cookie. Both of them contains the same security token which will be validated by the actions that is decorated with ValidateAntiForgeryTokenAttribute. You can learn more about CSRF from here. Usually these tokens are used along with the html forms to secure the POST actions from CSRF attacks.

CSRF

CSRF is an attack which forces an end user to execute unwanted actions on a web application in which he/she is currently authenticated. With a little help of social engineering (like sending a link via email/chat), an attacker may force the users of a web application to execute actions of the attacker's choosing.

A successful CSRF exploit can compromise end user data and operation in case of normal user. If the targeted end user is the administrator account, this can compromise the entire web application. - more

We have completed the login view. The rendered html of the form looks like below.

<form method="post" action="/admin/" novalidate="novalidate">
	<fieldset>
		<legend>Login</legend>
		<p>
			<label for="UserName">User name (*)</label>
			<input type="text" value="" name="UserName" id="UserName" data-val-required="The User name (*) field is required." data-val="true" class="valid">
			<span data-valmsg-replace="true" data-valmsg-for="UserName" class="field-validation-valid"></span>
		</p>

		<p>
			<label for="Password">Password (*)</label>
			<input type="password" name="Password" id="Password" data-val-required="The Password (*) field is required." data-val="true" class="valid">
			<span data-valmsg-replace="true" data-valmsg-for="Password" class="field-validation-valid"></span>
		</p>

		<input type="hidden" value="" name="ReturnUrl" id="ReturnUrl">
		<input type="hidden" value="Hky2m1FivI6uPEmDcaBdHYspt-H5a_ixSmBHrMZelmfEMDLbkMk0z96s5xcd1h89mbeG89tyZMhgaYFghGDJX9a0BbY9zqbrDX_u6ESUA181" name="__RequestVerificationToken">

		<p>
			<input type="submit" value="Login">
		</p>
	</fieldset>
</form>

Listing 11. Rendered html of the login form

Let's work on the action that will handle the form submit.

3.3 Create an action that handles the login form submit

We need an action that handles the login form submit request, check if the credentials are valid and set the authentication cookie. As usual, first the code is shown and the explanation follows it.

using JustBlog.Models;
using System;
using System.Web.Mvc;
using System.Web.Security;

namespace JustBlog.Controllers
{
    ...

    [HttpPost, AllowAnonymous, ValidateAntiForgeryToken]
    public ActionResult Login(LoginModel model, string returnUrl)
    {
	    if(ModelState.IsValid)
	    {
		    if(FormsAuthentication.Authenticate(model.UserName, model.Password))
		    {
			    FormsAuthentication.SetAuthCookie(model.UserName, false);

			    if(!String.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
				    return Redirect(returnUrl);

			    return RedirectToAction("Manage");
		    }

		    ModelState.AddModelError("", "The user name or password provided is incorrect.");
	    }
	    return View();
    }
}

Listing 12. Login action

There are many things I've to explain about the code. Let's see them one by one.

3.3.1 Model Binding

First, if you see the action parameters, we have LoginModel, and a string (returnUrl - where the user has to be redirected on successful login). The username and password values submitted by the user are directly mapped to the LoginModel by the "Model binding" feature.

Model binding is the powerful feature of ASP.NET MVC in which the model instances are automatically created and populated by the framework based upon the information available in the request.

DefaultModelBinder

The DefaultModelBinder is the built-in component that binds the values to the model properties. The Model binders uses the help of ValueProviders to get the information from the different sources of request like Form, QueryStrings etc. to populate the action parameters or properties of Models.

3.3.2 ModelState

At the time of model binding, validation happens and the validation errors occurred are stored in the ModelState. ModelState is a special dictionary. The IsValid property of ModelState says that if there are any validation errors or not. We can add our own validation errors to the ModelState through the ModelState.AddModelError method.

The ValidationSummary helper which we we saw previously displays the validation errors retrieving them from the ModelState dictionary.

3.3.3 ValidateAntiForgeryToken attribute

If you notice the Login action, along with the HttpPost attribute we also decorated it with ValidateAntiForgeryToken attribute. The antiforgery token that we have created using Html.AntiForgeryToken helper is validated by this attribute. Note that, including just the token in the view doesn't secure the actions. We have to make sure the corresponding actions are marked with ValidateAntiForgeryToken attribute. If the token is absent or invalid an exception will be thrown by this attribute.

I would like to explain about the HttpPost attribute as well. The HttpPost attribute is an action method selector which selects the action only if the request type is POST. In our AdminController We have both actions with the same name Login. The second Login action will be selected only if the request type is POST. If you don't mark an action with HttpPost or HttpGet then it will be selected for both GET and POST requests.

Action Method Selectors vs Action Filters

Action Method Selectors influence the selection of an action method and they are executed before Action Filters. Action Filters are used to execute a piece of logic before or after an action is executed.

Action Method Selectors: HttpPost, HttpGet, etc.
Action Filters: Authorize, OutputCache, etc.

Let's summarize what we've done in the Login action. First, we are making sure there are no any validation errors by checking ModelState.IsValid. If there are no validation errors we are authenticating the user by the FormsAuthentication.Authenticate method. On successfully authenticated we are setting the cookie using the FormsAuthentication.SetAuthCookie method and redirecting him to the appropriate action or url. Else, returning the same login view which displays the errors.

IMPORTANT: To avoid credentials being sent as clear text we should use HTTPS protocol for login actions. ASP.NET MVC simplifies forcing the requests to use HTTPS by decorating the actions with RequireHttps attribute. To keep things simple, I'm not applying the attribute but if you are going for production then I advice you to buy an SSL certificate and decorate both the login actions with RequireHttps attribute. At the time of development you could still generate a self-signed certificate from IIS and apply the attribute.

3.4 Create an action to logout the admin

Implementing an action to logout the admin is quite simple! All we have to do is, call the SignOut method of the FormsAuthentication class and redirect to the login action.

public ActionResult Logout()
{
	FormsAuthentication.SignOut();

	return RedirectToAction("Login", "Admin");
}

Listing 13. Logout action

To make the Logout action work, we have to define a new route for that as well.

routes.MapRoute(
	"Logout",
	"Logout",
	new { controller = "Admin", action = "Logout" }
);

Listing 14. Configuring route for Logout action

3.5 Configure Forms Authentication in web.config

Configuring Forms Authentication for an MVC application is same as in WebForms application. To keep things simple we are going to store the credentials as clear text.

<system.web>
	<authentication mode="Forms">
		<forms loginUrl="~/Login" timeout="2880">
			<credentials passwordFormat="Clear">
				<user name="admin" password="justblog" />
			</credentials>
		</forms>
	</authentication>
</system.web>

Listing 15. Configuring Forms Authentication in web.config

We have completed the login functionality. Let see how we can write unit tests for the login actions.

3.6 Write unit tests for actions

I've included this task with this user story because I want to demonstrate how to write unit tests for controller actions. If you want your controllers more testable then you should loosely couple them with their dependencies. Actually.. this rule applies for any class that needs to be testable! We are going to use NUnit for unit testing and Rhino Mocks for mocking. You could also use Visual Studio Unit Testing Framework with some mocking framework but the principles are mostly going to be same.

Unit Testing & Mocking

Unit Testing is testing a piece of unit isolated from the rest of the code. In Object Oriented Programming a unit could be a class or a method in the class. Unit Testing is all about testing only the functionalities of a class under test and not the dependencies.

Mocking is the process of providing mock or dummy instances for dependencies to a class at the time of unit testing to make the tests pass.

If you see our admin controller, it's tightly coupled with the FormsAuthentication class. Having tight dependency with the concrete class FormsAuthentication hinders writing unit tests for the actions that contains some good logic to test. Unit testing is all about testing only the piece of code under test and not the dependencies or framework classes.

3.6.1 Refactoring AdminController

We have to refactor the admin controller to make our actions unit testable. One way to decouple the FormsAuthentication class from admin controller is to wrap all the calls to FormsAuthentication in an interface which will be referenced by the controller. By this way, we can provide mock instances for the interface at the time of unit testing and make our tests pass without hindrances.

Let's create an interface with name IAuthProvider under the new Providers folder at the root of the application. Define couple of methods to login/logout user and a property to know whether the user is already logged-in or not.

namespace JustBlog.Providers
{
    public interface IAuthProvider
    {
	    bool IsLoggedIn { get; }
	    bool Login(string username, string password);
	    void Logout();
    }
}

Listing 16. IAuthProvider

Create a new class AuthProvider under the Providers folder and implement IAuthProvider.

using System.Web;
using System.Web.Security;

namespace JustBlog.Providers
{
    public class AuthProvider: IAuthProvider
    {
	    public bool IsLoggedIn
	    {
		    get
		    {
			    return HttpContext.Current.User.Identity.IsAuthenticated;
		    }
	    }

	    public bool Login(string username, string password)
	    {
		    bool result = FormsAuthentication.Authenticate(username, password);

		    if (result)
			    FormsAuthentication.SetAuthCookie(username, false);

		    return result;
	    }

	    public void Logout()
	    {
		    FormsAuthentication.SignOut();
	    }
    }
}

Listing 17. AuthProvider

We are going to modify our AdminController such that it has only dependency with IAuthProvider and not FormsAuthentication. IAuthProvider is injected through constructor by Ninject that we have configured in our application in Part I. All we have to do is add a binding in Global.asax.cs that maps IAuthProvider to the implementation AuthProvider.

protected override IKernel CreateKernel()
{
	var kernel = new StandardKernel();

	kernel.Load(new RepositoryModule());
	kernel.Bind<IBlogRepository>().To<BlogRepository>();
	kernel.Bind<IAuthProvider>().To<AuthProvider>();

	return kernel;
}

Listing 18. CreateKernel method

[Authorize]
public class AdminController: Controller
{
	private readonly IAuthProvider _authProvider;

	public AdminController(IAuthProvider authProvider)
	{
		_authProvider = authProvider;
	}

	// our actions
}

Listing 19. AdminController

Let's replace the calls to FormsAuthentication in AdminController to IAuthProvider.

[Authorize]
public class AdminController : Controller
{
	private readonly IAuthProvider _authProvider;

	public AdminController(IAuthProvider authProvider)
	{
		_authProvider = authProvider;
	}

	[AllowAnonymous]
	public ActionResult Login(string returnUrl)
	{
		if (_authProvider.IsLoggedIn)
			return RedirectToUrl(returnUrl);

		ViewBag.ReturnUrl = returnUrl;

		return View();
	}

	[HttpPost, AllowAnonymous, ValidateAntiForgeryToken]
	public ActionResult Login(LoginModel model, string returnUrl)
	{
		if (ModelState.IsValid && _authProvider.Login(model.UserName, model.Password))
		{
			return RedirectToUrl(returnUrl);
		}

		ModelState.AddModelError("", "The user name or password provided is incorrect.");
		return View(model);
	}

	public ActionResult Manage()
	{
		return View();
	}

	public ActionResult Logout()
	{
		_authProvider.Logout();

		return RedirectToAction("Login", "Admin");
	}

	private ActionResult RedirectToUrl(string returnUrl)
	{
		if (Url.IsLocalUrl(returnUrl))
		{
			return Redirect(returnUrl);
		}
		else
		{
			return RedirectToAction("Manage");
		}
	}
}

Listing 20. AdminController

Now our admin controller is more unit testable. Let's create a test project and write some unit tests.

3.6.2 Create test project and configure NUnit and Rhino Mocks

Our unit tests should be in a separate project and not messed up with the source code. Create a new class library project with the name JustBlog.Tests.

Test Project

Test Project

We can install both the NUnit and Rhino Mocks through Nuget package manager console.

Open Tools > Library Package Manager > Package Manager Console

Make sure the "Default Project" dropdown in the Package Manager Console is set to JustBlog.Tests and run the following commands,

PM> Install-Package NUnit
PM> Install-Package RhinoMocks

If the installation is successful both the nunit.framework.dll and Rhino.Mocks are added to the references.

3.6.3 Create a test class and write tests for admin controller actions

Create a class in the test project with the name AdminControllerTests.cs.

namespace JustBlog.Tests
{
    public class AdminControllerTests
    {

    }
}

Listing 21. Test class for AdminController

To represent the AdminControllerTests as a test class to NUnit framework we have to decorate it with the TestFixture attribute (from NUnit assembly).

A test class contain both test and non-test methods. To separate the test methods from the non-test ones we have to decorate them with the Test attribute.

We are going to write unit tests only for the login actions. Following are the tests we are going to write for the respective actions.

3.6.3.1 Login(string returnUrl)

1. Write a test to verify if the admin is already logged-in then the action should redirect him to the returnUrl passed to the method
2. Write a test to verify if the admin is not logged-in then the action should return the login view

3.6.3.2 Login(LoginModel model, string returnUrl)

3. Write a test to verify if there are validation errors then the action should return login view
4. Write a test to verify if the credentials are invalid then the action should return login view the authentication error
5. Write a test to verify if the credentials are valid then the action should redirect him to the returnUrl

Let's write the tests.

3.6.3.3 Write a test to verify if the admin is already logged-in then the action should redirect him to the returnUrl passed to the method

Here is the complete code required for the first test. Explanation follows it.

using JustBlog.Controllers;
using JustBlog.Providers;
using NUnit.Framework;
using Rhino.Mocks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace JustBlog.Tests
{
    [TestFixture]
    public class AdminControllerTests
    {
	    private AdminController _adminController;
	    private IAuthProvider _authProvider;

	    [SetUp]
	    public void SetUp()
	    {
		    _authProvider = MockRepository.GenerateMock<IAuthProvider>();
		    _adminController = new AdminController(_authProvider);

            var httpContextMock = MockRepository.GenerateMock<HttpContextBase>();
            _adminController.Url = new UrlHelper(new RequestContext(httpContextMock, new RouteData()));
	    }

	    [Test]
	    public void Login_IsLoggedIn_True_Test()
	    {
		    // arrange
		    _authProvider.Stub(s => s.IsLoggedIn).Return(true);

		    // act
		    var actual = _adminController.Login("/admin/manage");

		    // assert
		    Assert.IsInstanceOf<RedirectResult>(actual);
		    Assert.AreEqual("/admin/manage", ((RedirectResult)actual).Url);
	    }
    }
}

Listing 22. First unit test

We already saw about TestFixture and Test attributes. What is the use of SetUp attribute? The method marked with that attribute runs at first before running each test. The method name don't need to be SetUp it could be anything. If we want to do some initialization work before running each test we can write that in a method and decorate it with SetUp attribute. There is one more attribute called TearDown which does the opposite work, it's used to decorate the method that performs some cleanup after running each test.

We require an instance of AdminController and a mock instance of IAuthProvider in all the tests. So we have made both of them as private and the instantiation is done in the SetUp method. The advantage of loosely coupling classes through interfaces is, at the time of testing we can pass mock instances for the interfaces to the test classes.

We have used the MockRepository class of Rhino Mocks to create mock instance for IAuthProvider. The mock instance acts like it implements all the methods but they does nothing. We can stub those methods and properties of the mock objects and tell the framework what they have to return when they get called.

I've to explain you about the last two lines of the SetUp method. At the time of running unit tests, the Url property of the AdminController is null, which throws error from the methods we are accessing it. It's our responsibility to instantiate UrlHelper and provide to the controller. To create UrlHelper we need an instance of RequestContext which badly needs HttpContextBase. Since HttpContextBase is an abstract class we've easily mocked it and created the UrlHelper instance successfully. You've to mostly mock HttpContextBase whenever you write unit tests for ASP.NET MVC actions.

In every test, we usually follow these three steps.

a. Arrange
b. Act
c. Assert

In the Arrange step, we will instantiate the class and it's dependencies that are required to pass the test.

In the Act step, we will call the test method passing the required arguments.

In the final step i.e. Assert we will verify the actual outcome with the expected.

In our Login_IsLoggedIn_True_Test method, what we are testing is, if the admin is already logged in and calling the Login method, it should not return the login form instead it should redirect him to the passed url. The important thing is we want to simulate he/she is already logged in.

In the "arrange" step, we have stubbed the IAuthProvider's IsLoggedIn property to return true. Note that, the part of the code that runs in SetUp method also belongs to the arrange step. In the "act" step we have called the Login method passing the url that has to be redirected to if the admin is already logged in. In the final step, we are checking the returned result is an RedirectResult and it contains the passed url.

Stub

Stub is a process of giving a temporary implementation to a method or property. They are very much used in unit testing.

Let's see our next test.

3.6.3.4 Write a test to verify if the admin is not logged-in then the action should return the login view

This test is opposite to the previous one. If the admin is not at all logged in then calling the Login method should not redirect him to somewhere but should return the view that contains the login form.

Here is the code.

[Test]
public void Login_IsLoggedIn_False_Test()
{
	// arrange
	_authProvider.Stub(s => s.IsLoggedIn).Return(false);

	// act
	var actual = _adminController.Login("/");

	// assert
	Assert.IsInstanceOf<ViewResult>(actual);
	Assert.AreEqual("/", ((ViewResult)actual).ViewBag.ReturnUrl);
}

Listing 23. Unit test

Our next three tests are against the Login post action.

3.6.3.5 Write a test to verify if there are validation errors then the action should return login view

In this test we are ensuring the Login post action checks the validation errors. If there are any validation errors the action should return the same view without redirecting him to the other place.

[Test]
public void Login_Post_Model_Invalid_Test()
{
	// arrange
	var model = new LoginModel();
	_adminController.ModelState.AddModelError("UserName", "UserName is required");

	// act
	var actual = _adminController.Login(model, "/");

	// assert
	Assert.IsInstanceOf<ViewResult>(actual);
}

Listing 24. Unit test

This test is very simple. All we are doing is adding our own validation error in the ModelState dictionary (Note that, this is the place where the validation errors are added) and asserting the action returns a ViewResult in that case.

3.6.3.6 Write a test to verify if the credentials are invalid then the action should return login view with authentication error

Without question this is an important test! If the credentials are not valid, we don't want the action redirecting the user to some authorized place at any cost. For this test, we have to stub the Login method of IAuthProvider to return false to mimick the user is invalid.

[Test]
public void Login_Post_User_Invalid_Test()
{
	// arrange
	var model = new LoginModel
	{
		UserName = "invaliduser",
		Password = "password"
	};
	_authProvider.Stub(s => s.Login(model.UserName, model.Password))
				 .Return(false);

	// act
	var actual = _adminController.Login(model, "/");

	// assert
	Assert.IsInstanceOf<ViewResult>(actual);
	var modelStateErrors = _adminController.ModelState[""].Errors;
	Assert.IsTrue(modelStateErrors.Count > 0);
	Assert.AreEqual("The user name or password provided is incorrect.",
		modelStateErrors[0].ErrorMessage);
}

Listing 25. Unit test

3.6.3.7 Write a test to verify if the credentials are valid then the action should redirect him to the returnUrl

This is our final test and it's opposite to the previous one.

[Test]
public void Login_Post_User_Valid_Test()
{
	// arrange
	var model = new LoginModel
	{
		UserName = "validuser",
		Password = "password"
	};
	_authProvider.Stub(s => s.Login(model.UserName, model.Password))
				 .Return(true);

	// act
	var actual = _adminController.Login(model, "/");

	// assert
	Assert.IsInstanceOf<RedirectResult>(actual);
	Assert.AreEqual("/", ((RedirectResult)actual).Url);
}

Listing 26. Unit test

3.6.4 How to run unit tests?

The unit tests written using Visual Studio Unit Testing Framework can be run directly inside VS. To run Nunit tests inside Visual Studio 2012, you have to install the NUnit Test Adapter. NUnit also comes with a simple GUI test runner tool that you can download from here.

The below screen show how all our tests has successfully passed in the GUI test runner.

NUnit GUI Runner

NUnit GUI Runner

We have complete our first story! The coming stories will be more interesting ones, where we will see how to add/edit/delete posts, categories and tags.

4. Story #2 - Implement functionalities to add, edit and delete posts

In this story, first we are going to create a "manage page" that displays all the posts, categories and tags in separate grids. Next we are going to implement functionalities to add, edit and delete posts.

At the higher level, following are the tasks we are going to accomplish in this story.

1. Implement action and view that helps to display manage page
2. Implement functionality to return posts that has to be displayed in the grid
3. Implement functionality to add new post
4. Implement functionality to edit post
5. Implement functionality to delete post

4.1 Implement action and view that helps to display manage page

The implementation of Manage action is very simple. But we have to do more work in the view.

public ActionResult Manage()
{
	return View();
}

Listing 27. Manage action

We need to configure two more routes in RouteConfig. One route is for the Manage action and the second route is to handle the add, edit, delete requests for posts, categories and tags which we will be doing in the upcoming tasks.

Let's define the two routes.

...

routes.MapRoute(
	"Manage",
	"Manage",
	new { controller = "Admin", action = "Manage" }
);

routes.MapRoute(
	"AdminAction",
	"Admin/{action}",
	new { controller = "Admin", action = "Login" }
);

...

Listing 28. Configuring routes

Create a view by just right-clicking inside the action and select "Add View". Like the Login view, we don't have to select a layout for the Manage view.

Replace the content of the Manage view with below.

@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
	<meta name="viewport" content="width=device-width" />
	<title>Manage Posts, Categories and Tags</title>

	
	<script src="http://code.jquery.com/jquery-1.8.2.js"></script>

	
	<script src="http://code.jquery.com/ui/1.9.1/jquery-ui.js"></script>
	<link href="@Url.Content("~/Content/themes/simple/jquery-ui-1.9.2.custom/css/sunny/jquery-ui-1.9.2.custom.min.css")" rel="stylesheet" />

	
	<link href="@Url.Content("~/Scripts/jquery.jqGrid-4.4.2/css/ui.jqgrid.css")" rel="stylesheet" />
	<script src="@Url.Content("~/Scripts/jquery.jqGrid-4.4.2/js/jquery.jqGrid.min.js")"></script>
	<script src="@Url.Content("~/Scripts/jquery.jqGrid-4.4.2/js/i18n/grid.locale-en.js")"></script>

	
	<script src="@Url.Content("~/Scripts/tiny_mce/tiny_mce.js")"></script>

	<link href="@Url.Content("~/Content/themes/simple/admin.css")" rel="stylesheet" />
	<script src="@Url.Content("~/Scripts/admin.js")"></script>

</head>
<body>

	
	<div style="text-align:right; margin-bottom:5px;">
		@Html.ActionLink("logout", "Logout")
	</div>

	<h2>Manage Posts, Categories and Tags</h2>

	
	<div id="tabs">
		<ul>
			<li><a href="#tab-posts">Posts</a></li>
			<li><a href="#tab-cats">Categories</a></li>
			<li><a href="#tab-tags">Tags</a></li>
		</ul>

		<div id="tab-posts" class="tab">
			<table id="tablePosts" class="grid">
			</table>
			<div id="pagerPosts" class="pager">
			</div>
		</div>

		<div id="tab-cats" class="tab">
			<table id="tableCats" class="grid">
			</table>
			<div id="pagerCats" class="pager">
			</div>
		</div>

		<div id="tab-tags" class="tab">
			<table id="tableTags" class="grid">
			</table>
			<div id="pagerTags" class="pager">
			</div>
		</div>
	</div>
</body>
</html>

Listing 29. Manage view

There are many libraries we have included in the Manage view. Let see them one by one.

4.1.1 jQuery library

<script src="http://code.jquery.com/jquery-1.8.2.js"></script>

Listing 30. Referencing jQuery library

4.1.2 jQuery UI library

<link href="@Url.Content("~/Content/themes/simple/jquery-ui-1.9.2.custom/css/sunny/jquery-ui-1.9.2.custom.min.css")" rel="stylesheet" />
<script src="http://code.jquery.com/ui/1.9.1/jquery-ui.js"></script>

Listing 31. Referencing jQuery UI library

jQuery UI library provides a bunch of widgets, user interface interactions, effects that are built on top of the powerful jQuery library. We are going to use a tab control from that library in our Manage view. You may think that for a single control why we need an entire library? You could avoid using that library by constructing the tab control yourself. I've used that library because I thought in future if we need more widgets it's easier to use from that library. Notice that, we are loading the library from CDN and that saves our bandwidth.

We have downloaded a custom theme named "sunny" for jQuery UI widgets. You can download a different theme or even you can create a custom one from the Theme Roller. You can download the "sunny" theme from here.

4.1.3 jqgrid

jQGrid

jQGrid

<link href="@Url.Content("~/Scripts/jquery.jqGrid-4.4.2/css/ui.jqgrid.css")" rel="stylesheet" />
<script src="@Url.Content("~/Scripts/jquery.jqGrid-4.4.2/js/jquery.jqGrid.min.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.jqGrid-4.4.2/js/i18n/grid.locale-en.js")"></script>

Listing 32. Referencing jqgrid library css and script files

We need a grid control to display posts, categories and tags. Also, the grid control should provide functionalities like paging, sorting and options to add, edit and delete. jqgrid is an amazing grid that provides many functionalities. Advanced functionalities can be achieved through plugins. Most importantly they have a good documentation right here.

You can download the latest jqgrid plugin from here. We have included the downloaded plugin under the Scripts folder. jqgrid also provides support for internationalization. The third script file grid.locale-en.js which is mandatory; contains validation messages and other display text messages for English language.

4.1.4 tinyMCE html editor

TinyMCE html editor

TinyMCE html editor

<script src="@Url.Content("~/Scripts/tiny_mce/tiny_mce.js")"></script>

Listing 33. Referencing tinyMCE editor

We need a html editor to edit the blog posts. Choosing the good one is very important for any content management application. Without question, tinyMce is one of the best out there. They provide lot of functionalities and it's worth to visit their website to know all the features they provide. You can download the latest tinyMCE plugin from their website or from the one I used in my blog from here.

Finally, we also included our own admin.css which contains some style definitions for both Login and Manage views. Also, we have created a new script file admin.js to place all our custom script required for admin pages.

Our Manage view looks little nasty by all those css and script references. Don't worry in the next part we will see how we can avoid all that by using the bundling and minification feature.

We are going to use the jquery UI tab control to display all the three grids (posts, categories and tags) in separate tabs. Below is the initial html that's required to create the tab control and grids.


<div id="tabs">

	
	<ul>
		<li><a href="#tab-posts">Posts</a></li>
		<li><a href="#tab-cats">Categories</a></li>
		<li><a href="#tab-tags">Tags</a></li>
	</ul>

	
	<div id="tab-posts" class="tab">
		<table id="tablePosts" class="grid">
		</table>
		<div id="pagerPosts" class="pager">
		</div>
	</div>

	
	<div id="tab-cats" class="tab">
		<table id="tableCats" class="grid">
		</table>
		<div id="pagerCats" class="pager">
		</div>
	</div>

	
	<div id="tab-tags" class="tab">
		<table id="tableTags" class="grid">
		</table>
		<div id="pagerTags" class="pager">
		</div>
	</div>
</div>

Listing 34. Html markup for jquery tab control

To create a jqgrid grid control we need a table and a pager div. If you see all the tab containers (tab-posts, tab-cats and tab-tags) we have a table and a div. Note that the class names (tab, grid, pager) that we have used in the elements(table, div) which are very important.

4.1.5 Test Drive

Our manage view is ready. If you run the application and go to the Manage action, you'll be redirected to the Login page.

Login

Login

After successfully logged-in you'll be redirected to the Manage view.

Manage view

Manage view

If you see the page, the tabs are not created properly and are just displayed as links, it's because we haven't called the script function to make it a tab control. All our custom script code has to be placed in the admin.js file which is under Scripts folder.

All we have to do in the admin.js file is call the jQuery UI library's tabs() function over the div #tabs.

$(function(){
	$("#tabs").tabs();
});

Listing 35. jQuery UI's tabs() method

Now if you refresh the manage page, you will see the below screen.

Manage view

Manage view

The tabs are ready. Now we have to create the grids. Our strategy is when each tab is shown at the first time, we have to call a script function to convert the table and pager div into a jqgrid. For that, we have to listen to the show event of the tab control.

$("#tabs").tabs({
	show: function (event, ui) {

		if (!ui.tab.isLoaded) {

			switch (ui.index) {
				case 0:
					// call function to create grid for managing posts
					// from "#tablePosts" and "#pagerPosts"
					break;
				case 1:
					// call function to create grid for managing posts
					// from "#tableCats" and "#pagerCats"
					break;
				case 2:
					// call function to create grid for managing posts
					// from "#tableTags" and "#pagerTags";
					break;
			};

			ui.tab.isLoaded = true;
		}
	}
});

Listing 36. tabs() method

The show event is fired every time the tab is shown but we want to create the grid only at the first time. So I've used a custom property called isLoaded in the ui.tab object which will be set to true once the tab is shown for the first time and the next time onwards we don't create grids again.

4.1.6 Object Oriented Javascript

Before starting to create more javascript functions, I would like to tell something about Object Oriented JavaScript. There were times I was thinking JavaScript was nothing more than a set of functions. All that changed after I started worked in couple of JavaScript tools where I realised that JavaScript is not just a functional oriented programming language but it's an object based language. We can even create objects, classes (not exactly like the ones in Java or C#) and group set of properties and behaviors.

We are going to group the related javascript functions under objects that helps us to manage them easily and avoid global variable collisions.

In JavaScript we can create an object straightaway without the need of a class. We can use javascript functions to act as class and create objects from it using the new operator but we are not going to use that approach here.

For ex.,

var employee = { name: "Vijaya Anand", age: 30 };

Listing 37. JavaScript Object

The object can even contain functions,

var employee =
{
	name: "Vijaya Anand",
	age: 30,
	details: function(){
		return this.name + " " + this.age;
	}
}

Listing 38. JavaScript Object with function

'this' in JavaScript

In JavaScript, 'this' represents the context of the current object. If there's no current object, 'this' refers to the global object. In a web browser, that's 'window' � the top-level object which represents the document, location, history and a few other useful properties and methods.

If you are calling a function, 'this' represents the global object.

Ex.

function sayHello() {
  alert('hello');
  alert(this); // [object Window]
}

If you are calling an object method, 'this' represents the object.

Ex.

function calculator(a, b){
  this.a = a,
  this.b = b,

  this.sum = function(){
    alert(this.a + this.b);
    alert(this); // [object Object]
  }
}

By grouping a set of functions under an object we can easily manage them. In our project, we are going to create a single object called JustBlog where we are going to group all the important javascript functions.

var JustBlog = {};

JustBlog.GridManager = {

	// function to create grid to manage posts
	postsGrid: function(gridName, pagerName) {
	},

	// function to create grid to manage categories
	categoriesGrid: function(gridName, pagerName){
	},

	// function to create grid to manage tags
	tagsGrid: function(gridName, pagerName){
	}
};

Listing 39. JustBlog Object

In the above code, we have created a child-object GridManager under JustBlog that will contain the functions related to create each grid. We are going to call these functions from the tab control's "show" event.

$("#tabs").tabs({
	show: function (event, ui) {

		if (!ui.tab.isLoaded) {

			var gdMgr = JustBlog.GridManager,
				fn, gridName, pagerName;

			switch (ui.index) {
				case 0:
					fn = gdMgr.postsGrid;
					gridName = "#tablePosts";
					pagerName = "#pagerPosts";
					break;
				case 1:
					fn = gdMgr.categoriesGrid;
					gridName = "#tableCats";
					pagerName = "#pagerCats";
					break;
				case 2:
					fn = gdMgr.tagsGrid;
					gridName = "#tableTags";
					pagerName = "#pagerTags";
					break;
			};

			fn(gridName, pagerName);
			ui.tab.isLoaded = true;
		}
	}
});

Listing 40. tabs() function

What we are doing in the above code is, based upon the tab index we are figuring out the grid, pager element IDs and the script function that has to be called to create the grid. Now, all the work we have to do is implement the functions of the JustBlog.GridManager to create the grids and implement the necessary actions in AdminController.

4.2 Implement functionality to return posts that has to be displayed in the grid

Let's start with the Posts grid. First we have to implement the action and the necessary repository methods to return posts that has to be populated in the grid.

public ActionResult Posts()
{
	// TODO: return posts based on the pagination parameters
	// and order them based upon the "order" parameter sent by jqgrid
}

Listing 41. Posts action

We have to fetch the posts based upon the pagination and order parameters sent by jqgrid. The following are the important parameters that are send by jqgrid.

1. rows - no. of records to fetch
2. page - the page index
3. sidx - sort column name
4. sord - sort order "asc" or "desc"

Instead of specifying this parameters directly in the action to receive them; we can create a view model with name JqInViewModel under the Models folder that wraps all of them.

namespace JustBlog.Models
{
    public class JqInViewModel
    {
	    // no. of records to fetch
	    public int rows
	    { get; set; }

	    // the page index
	    public int page
	    { get; set; }

	    // sort column name
	    public string sidx
	    { get; set; }

	    // sort order "asc" or "desc"
	    public string sord
	    { get; set; }
    }
}

Listing 42. JqInViewModel

Let's change our action to take this view model as action parameter and rest will be taken care by the model binding.

public ActionResult Posts(JqInViewModel jqParams)
{
	// TODO: call the repository method and return the posts as JSON
}

Listing 43. Posts action

4.2.1 Implement the necessary repository methods

Let's define a new repository method called Posts in the IBlogRepository interface that takes both pagination and sorting parameters.

IList<Post> Posts(int pageNo, int pageSize, string sortColumn,
							bool sortByAscending);

Listing 44. IBlogRepository

Here goes the implementation,

public IList<Post> Posts(int pageNo, int pageSize, string sortColumn,
									bool sortByAscending)
{
  IList<Post> posts;
  IList<int> postIds;

  switch (sortColumn)
  {
    case "Title":
      if (sortByAscending)
      {
        posts = _session.Query<Post>()
                        .OrderBy(p => p.Title)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderBy(p => p.Title)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      else
      {
        posts = _session.Query<Post>()
                        .OrderByDescending(p => p.Title)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderByDescending(p => p.Title)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      break;
    case "Published":
      if (sortByAscending)
      {
        posts = _session.Query<Post>()
                        .OrderBy(p => p.Published)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderBy(p => p.Published)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      else
      {
        posts = _session.Query<Post>()
                        .OrderByDescending(p => p.Published)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderByDescending(p => p.Published)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      break;
    case "PostedOn":
      if (sortByAscending)
      {
        posts = _session.Query<Post>()
                        .OrderBy(p => p.PostedOn)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderBy(p => p.PostedOn)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      else
      {
        posts = _session.Query<Post>()
                        .OrderByDescending(p => p.PostedOn)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                        .Where(p => postIds.Contains(p.Id))
                        .OrderByDescending(p => p.PostedOn)
                        .FetchMany(p => p.Tags)
                        .ToList();
      }
      break;
    case "Modified":
      if (sortByAscending)
      {
        posts = _session.Query<Post>()
                        .OrderBy(p => p.Modified)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderBy(p => p.Modified)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      else
      {
        posts = _session.Query<Post>()
                        .OrderByDescending(p => p.Modified)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderByDescending(p => p.Modified)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      break;
    case "Category":
      if (sortByAscending)
      {
        posts = _session.Query<Post>()
                        .OrderBy(p => p.Category.Name)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderBy(p => p.Category.Name)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      else
      {
        posts = _session.Query<Post>()
                        .OrderByDescending(p => p.Category.Name)
                        .Skip(pageNo * pageSize)
                        .Take(pageSize)
                        .Fetch(p => p.Category)
                        .ToList();

        postIds = posts.Select(p => p.Id).ToList();

        posts = _session.Query<Post>()
                          .Where(p => postIds.Contains(p.Id))
                          .OrderByDescending(p => p.Category.Name)
                          .FetchMany(p => p.Tags)
                          .ToList();
      }
      break;
    default:
      posts = _session.Query<Post>()
                      .OrderByDescending(p => p.PostedOn)
                      .Skip(pageNo * pageSize)
                      .Take(pageSize)
                      .Fetch(p => p.Category)
                      .ToList();

      postIds = posts.Select(p => p.Id).ToList();

      posts = _session.Query<Post>()
                        .Where(p => postIds.Contains(p.Id))
                        .OrderByDescending(p => p.PostedOn)
                        .FetchMany(p => p.Tags)
                        .ToList();
      break;
  }

  return posts;
}

Listing 45. Posts method implementation in BlogRepository

We have used the switch statement to perform sorting for different properties. We could have done that easily through NHibernate's ICriteria but they have some limitations in the case of many-to-many relationships and so we have to rely on this for the time being. If you have any easy solution please let me know :).

We need to know the total no. of posts (published and not published) exists in the database. We already have a method that's implemented in part 1 which returns the total no. of published posts. Let's modify the method little to take an input flag so that it returns the count of only published posts or both.

int TotalPosts(bool checkIsPublished = true);

Listing 46. IBlogRepository

We have added an optional parameter checkIsPublished to the TotalPosts method defined in the interface. Since checkIsPublished has a default value of true, it won't affect the methods that are calling this already.

Let's change the implementation of the TotalPosts method to consider the checkIsPublished parameter.

public int TotalPosts(bool checkIsPublished = true)
{
	return _session.Query<Post>()
			.Where(p => !checkIsPublished || p.Published == true)
			.Count();
}

Listing 47. BlogRepository

As default the method returns the total count of only published posts but passing false to the method returns the count of all the posts.

Return the data from Posts action as JSON

Our repository methods are ready! Let's complete the pending work in the Posts action.

public ActionResult Posts(JqInViewModel jqParams)
{
	var posts = _blogRepository.Posts(jqParams.page - 1, jqParams.rows,
		jqParams.sidx, jqParams.sord == "asc");

	var totalPosts = _blogRepository.TotalPosts(false);

	// TODO: return the posts, count and other information in the
	// JSON format needed by the jqGrid
}

Listing 48. Posts action

We have to return the data as JSON to bind with jqGrid (we can also use XML). To successfully bind the records to jqGrid we have to return the JSON in the following format.

{
   page: current page no,
   records: total no. of records,
   rows: records,
   total: no. of records returned now
}

Listing 49. jQGrid output JSON structure

We can use the built-in JsonResult class to return the data in JSON format as shown in the below code.

public JsonResult Posts(JqInViewModel jqParams)
{
	var posts = _blogRepository.Posts(jqParams.page - 1, jqParams.rows,
		jqParams.sidx, jqParams.sord == "asc");

	var totalPosts = _blogRepository.TotalPosts(false);

	return Json(new
	{
		page = jqParams.page,
		records = totalPosts,
		rows = posts,
		total = Math.Ceiling(Convert.ToDouble(totalPosts) / jqParams.rows)
	}, JsonRequestBehavior.AllowGet );
}

Listing 50. Posts action

Why JsonRequestBehavior needed?

By default, the ASP.NET MVC framework does not allow you to respond to HTTP GET requests with JSON payload. If you need to send JSON in response to GET requests, you'll need to explicitly allow the behavior by using JsonRequestBehavior.AllowGet as the second parameter to the Json method.

However, there is a chance a malicious user can gain access to the JSON payload through a process known as JSON Hijacking. You do not want to return sensitive information using JSON in a GET request. For more details, see Phil's post.

The JsonResult class uses the built-in JavaScriptSerializer class exists in the System.Web.Extensions assembly for serialization.

Below is the sample JSON returned from the action,

{
	page: 1,
	records: 10,
	rows:
	[
		{
			Id: 1,
			Title: "test",
			Category: ...,
			Tags: ...,
			PostedOn: "/Date(1359095545000)/",
			... other properties
		},
		{
			Id: 2,
			...
		}
	],
	total: 2
}

Listing 51. Sample output JSON

One problem with using the JavaScriptSerializer is it converts the datetimes into a format that is inconvenient to developers for debugging and other things. If you see the value of the PostedOn property in the above json it is "/Date(1359095545000)/" which is difficult to process in the client-side.

What is this ("/Date(1359095545000)/") format?

One of the sore points of JSON is the lack of a date/time literal. Many people are surprised and disappointed to learn this when they first encounter JSON. The simple explanation (consoling or not) for the absence of a date/time literal is that JavaScript never had one either: The support for date and time values in JavaScript is entirely provided through the Date object.

Most applications using JSON as a data format, therefore, generally tend to use either a string or a number to express date and time values. If a string is used, you can generally expect it to be in the ISO 8601 format. If a number is used, instead, then the value is usually taken to mean the number of milliseconds in Universal Coordinated Time (UTC) since epoch, where epoch is defined as midnight January 1, 1970 (UTC). Again, this is a mere convention and not part of the JSON standard.

If you are exchanging data with another application, you will need to check its documentation to see how it encodes date and time values within a JSON literal. For example, Microsoft's ASP.NET AJAX uses neither of the described conventions. Rather, it encodes .NET DateTime values as a JSON string, where the content of the string is /Date(ticks)/ and where ticks represents milliseconds since epoch (UTC). So November 29, 1989, 4:55:30 AM, in UTC is encoded as "\/Date(628318530718)\/" - MSDN.

To customize the way in which the dates are serialized and for other advanced functionalities it's better to use Newtonsoft.Json library.

You can install Newtonsoft.Json through the Package Manager Console,

Install-Package Newtonsoft.Json

If the package is installed successfully you will see the assembly Newtonsoft.Json added to the References.

Let's modify the Posts action to serialize data using Newtonsoft's serializer.

public ContentResult Posts(JqInViewModel jqParams)
{
	var posts = _blogRepository.Posts(jqParams.page - 1, jqParams.rows,
		jqParams.sidx, jqParams.sord == "asc");

	var totalPosts = _blogRepository.TotalPosts(false);

	return Content(JsonConvert.SerializeObject(new
	{
		page = jqParams.page,
		records = totalPosts,
		rows = posts,
		total = Math.Ceiling(Convert.ToDouble(totalPosts) / jqParams.rows)
	}), "application/json");
}

Listing 52. Posts action

We have used the SerializeObject of JsonConvert class to serialize the object into JSON. Also, note that I've changed the return type of the action to ContentResult. The returned JSON would look something like below,

{
	page: 1,
	records: 10,
	rows:
	[
		{
			Id: 1,
			Title: "test",
			Category: ...,
			Tags: ...,
			PostedOn: "2013-01-25T12:02:25",
			.. other properties
		},
		{
			Id: 2,
			...
		}
	],
	total: 2
}

Listing 53. Ouput JSON

The value of the PostedOn property is "2013-01-25T12:02:25" which is an ISO 8601 format, which is the one Newtonsoft.Json uses as default. But, that's not what we exactly wanted. We need a custom converter for datetime objects to represent datetimes in "en-US" (ex. 01/25/13 12:02:08 PM) format.

Custom datetime converter

A custom datetime converter can be easily created by inheriting the Newtonsoft.Json's DateTimeConverterBase abstract class. Create a custom datetime converter class with name CustomDateTimeConverter in the root of the project. We have to override only the WriteJson method to serialize the datetimes to the format we expect.

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;

namespace JustBlog
{
    public class CustomDateTimeConverter : DateTimeConverterBase
    {
	    public override object ReadJson(JsonReader reader, Type objectType,
		    object existingValue, JsonSerializer serializer)
	    {
		    throw new NotImplementedException();
	    }

	    public override void WriteJson(JsonWriter writer, object value,
		    JsonSerializer serializer)
	    {
		    writer.WriteValue(value.ToString());
	    }
    }
}

Listing 54. CustomDateTimeConverter

Now, all we have to do is pass an instance of the CustomDateTimeConverter to the JsonConvert.SerializeObject method.

return Content(JsonConvert.SerializeObject(new
{
	page = jqParams.page,
	records = totalPosts,
	rows = posts,
	total = Math.Ceiling(Convert.ToDouble(totalPosts) / jqParams.rows)
}, new CustomDateTimeConverter()), "application/json");

Listing 55. Returning JSON using NewtonSoft's JsonConvert.SerializeObject

Avoiding circular references when serializing objects to JSON

When we try to access access the Posts action from browser we'll get the following exception.

Exception due to circular references

Exception due to circular references

The exception occurs because we have circular references between objects, i.e. Post object contains Category object and vice-versa. When serializing the collection of Post objects, we don't want to serialize the IList<Post> inside the Category and Tag objects. We can tell Newtonsoft.Json to ignore those properties by applying the JsonIgnore attribute over them. For that, first we have to install the Newtonsoft.Json package in the JustBlog.Core project as well.

public class Category
{
	...

	[JsonIgnore]
	public virtual IList<Post> Posts
	{ get; set; }
}

public class Tag
{
	...

	[JsonIgnore]
	public virtual IList<Post> Posts
	{ get; set; }
}

Listing 56. JsonIgnore

All right, now there won't be any circular reference issue. Let's complete the client-side work to finish the story.

Create and configure posts grid

If you remeber, we have wrapped all our functions needed to create grids in the JustBlog.GridManager object. The postsGrid function which is called when we click the "Posts" tab is shown below. We are going to complete that function.

JustBlog.GridManager = {

	postsGrid: function(gridName, pagerName) {

	},

	// other functions
}

Listing 57. postsGrid function

Let's first complete the necessary work required to display the posts in the grid. In the upcoming stories we will see about including add, edit and delete options to the grid.

Here is the initial code that's required to display the posts in the grid.

postsGrid: function(gridName, pagerName) {

	// columns
	var colNames = [
		'Id',
		'Title',
		'Short Description',
		'Description',
		'Category',
		'Category',
		'Tags',
		'Meta',
		'Url Slug',
		'Published',
		'Posted On',
		'Modified'
	];

	var columns = [];

	columns.push({
		name: 'Id',
		hidden: true,
		key: true
	});

	columns.push({
		name: 'Title',
		index: 'Title',
		width: 250
	});

	columns.push({
		name: 'ShortDescription',
		width: 250,
		sortable: false,
		hidden: true
	});

	columns.push({
		name: 'Description',
		width: 250,
		sortable: false,
		hidden: true
	});

	columns.push({
		name: 'Category.Id',
		hidden: true
	});

	columns.push({
		name: 'Category.Name',
		index: 'Category',
		width: 150
	});

	columns.push({
		name: 'Tags',
		width: 150
	});

	columns.push({
		name: 'Meta',
		width: 250,
		sortable: false
	});

	columns.push({
		name: 'UrlSlug',
		width: 200,
		sortable: false
	});

	columns.push({
		name: 'Published',
		index: 'Published',
		width: 100,
		align: 'center'
	});

	columns.push({
		name: 'PostedOn',
		index: 'PostedOn',
		width: 150,
		align: 'center',
		sorttype: 'date',
		datefmt: 'm/d/Y'
	});

	columns.push({
		name: 'Modified',
		index: 'Modified',
		width: 100,
		align: 'center',
		sorttype: 'date',
		datefmt: 'm/d/Y'
	});

	// create the grid
	$(gridName).jqGrid({
		// server url and other ajax stuff
		url: '/Admin/Posts',
		datatype: 'json',
		mtype: 'GET',

		height: 'auto',

		// columns
		colNames: colNames,
		colModel: columns,

		// pagination options
		toppager: true,
		pager: pagerName,
		rowNum: 10,
		rowList: [10, 20, 30],

		// row number column
		rownumbers: true,
		rownumWidth: 40,

		// default sorting
		sortname: 'PostedOn',
		sortorder: 'desc',

		// display the no. of records message
		viewrecords: true,

		jsonReader: { repeatitems: false }
	});
}

Listing 58. postsGrid function

Most part of the code is self explained by the comments. The jqGrid() function is used to create jqGrid over the passed div. We are passing the grid options as an object to the function. i.e.,

$(gridName).jqGrid(options);

Listing 59. jqGrid function

The options object contains information about the column model, pagination and other stuff. The details of the important option properties that we have used are shown in the below table.

Property Description
url The url of the action that returns the data needed to populate the grid.
datatype Defines in what format to expect the data that fills the grid. We have used JSON, we can also use other types like XML.
mtype Defines the type of request to make ("POST" or "GET").
colNames An array in which we place the names of the columns. This is the text that appears in the head of the grid (header layer). The names are separated with commas. Note that the number of elements in this array should be equal of the number elements in the colModel array.
colModel Array which describes the parameters of the columns.This is the most important part of the grid. For a full description of all valid values see colModel API.
toppager When enabled this option places a pager element at top of the grid, below the caption (if available). If another pager is defined, both can coexist and are kept in sync.
pager Defines that we want to use a pager bar to navigate through the records. This must be a valid HTML element.
rowNum Sets how many records we want to view in the grid. This parameter is passed to the url for use by the server routine retrieving the data. Note that if you set this parameter to 10 (i.e. retrieve 10 records) and your server return 15 then only 10 records will be loaded.
rowList An array to construct a select box element in the pager in which we can change the number of the visible rows. Typically you can set this like [10,20,30].
rownumbers If this option is set to true, a new column at left of the grid is added.
sortname The column according to which the data is to be sorted when it is initially loaded from the server.
viewrecords If true, jqGrid displays the beginning and ending record number in the grid, out of the total number of records in the query.
jsonReader An array which describes the structure of the expected json data. For a full description and default setting, see here.

You can read all the grid options from here.

The information about the columns is stored as an array of objects in the colModel property. Some of the important properties of colModel options are shown in the below table.

Property Description
name This is a mandatory property which is used to map with the returned json object. This property is also used to set a unique name for the column in the grid. If you don't set any value for this property then the corresponding column will be displayed as empty.
index This property is used to set an index name for the column which is used for sorting. The value you set in this property will be passed in the sidx parameter to the server. We have set a value to this property for only the columns that requires sorting.
hidden Defines if this column is hidden at initialization.
key In case if there is no id from server, this can be set as as id for the unique row id. Only one column can have this property. If there are more than one key the grid finds the first one and the second is ignored.
sortable Defines if the column can be sorted.

We will see more about options in the upcoming tasks. To read the complete list of options please go here.

If you go to the Manage page, you could see the posts displayed in the grid as shown below.

Tag column issue

Tag column issue

You can notice a problem in the Tags column, all the rows are displayed as [object Object]. The reason is because actually we are binding the collection of tags to the grid. Actually, what we have to do is, we have to display only the tag names as comma separated string. We could do that from the server-side but I want to show you how we can easily do that in the client-side through the jqGrid events.

jqGrid provides a lot of events and one of the event is afterInsertRow which is fired every time a row is successfully inserted to the grid. You can read the complete list of jqGrid events from here.

What I'm going to do is, in the afterInsertRow event, I'll read the tags collection that is binded to the row, iterate them and form a comma separated string from the tag name and assign it to the cell.

$(gridName).jqGrid({
	url: '/Admin/Posts',
	datatype: 'json',
	...

	afterInsertRow: function (rowid, rowdata, rowelem) {
		var tags = rowdata["Tags"];
		var tagStr = "";

		$.each(tags, function(i, t){
			if(tagStr) tagStr += ", "
			tagStr += t.Name;
		});


		$(gridName).setRowData(rowid, { "Tags": tagStr });
	}
});

Listing 60. jqGrid()

That's it! our first task to display the posts is over.

Tag column issue fixed

Tag column issue fixed

In the upcoming tasks we will see how to implement add, edit and delete functionalities to the grid.

3. Implement functionality to add post

Like the previous task, first we are going to complete our work in the server side and then we will work on the client side. Let's complete the necessary repository methods and action.

Implement repository method to add post

Define a new method in IBlogRepository to add new post.

int AddPost(Post post);

Listing 61. IBlogRepository

Implementation is quite easy!

public int AddPost(Post post)
{
	using (var tran = _session.BeginTransaction())
	{
		_session.Save(post);
		tran.Commit();
		return post.Id;
	}
}

Listing 62. AddPost implementation in BlogRepository

One important thing to keep in mind when you are performing add/edit operations using the NHibernate's session object, the entire code should be wrapped inside transaction else the changes won't be committed to database.

4.3.2 Implement action to add post

Create a new action in the admin controller called AddPost. Try to understand the code and the explanation follows it.

[HttpPost]
public ContentResult AddPost(Post post)
{
	string json;

	if(ModelState.IsValid)
	{
		var id = _blogRepository.AddPost(post);

		json = JsonConvert.SerializeObject(new
		{
			id = id,
			success = true,
			message = "Post added successfully."
		});
	}
	else
	{
		json = JsonConvert.SerializeObject(new
		{
			id = 0,
			success = false,
			message = "Failed to add the post."
		});
	}

	return Content(json, "application/json");
}

Listing 63. AddPost action

If you notice, We have decorated the action with HttpPost attribute to make sure the action will handle only the POST request. We are saving the post object to database only if there are no validation errors (ModelState.IsValid = true).

If the object is added successfully or failed we have to convey the information to jqGrid. In case of success, we have to return the id of the newly created post, a boolean flag set to true and an optional message. In case of failure due to validation errors or some other reason we have to return a json object with the boolean flag set to false and the failure message. Whatever the message that we are returning will be displayed in the jqGrid's add/edit dialogs which we will see soon!

4.3.3 Applying validations to Post class

Unless we don't apply any validations there is no much use of the ModelState.IsValid property. Currently we don't have any validations applied to the Post class. Let's apply the necessary validations.

public class Post
{
	[Required(ErrorMessage = "Id: Field is required")]
	public virtual int Id
	{ get; set; }

	[Required(ErrorMessage = "Title: Field is required")]
	[StringLength(500, ErrorMessage = "Title: Length should not exceed 500 characters")]
	public virtual string Title
	{ get; set; }

	[Required(ErrorMessage = "ShortDescription: Field is required")]
	public virtual string ShortDescription
	{ get; set; }

	[Required(ErrorMessage = "Description: Field is required")]
	public virtual string Description
	{ get; set; }

	[Required(ErrorMessage = "Meta: Field is required")]
	[StringLength(1000, ErrorMessage = "Meta: Length should not exceed 1000 characters")]
	public virtual string Meta
	{ get; set; }

	[Required(ErrorMessage = "Meta: Field is required")]
	[StringLength(1000, ErrorMessage = "Meta: UrlSlug should not exceed 50 characters")]
	public virtual string UrlSlug
	{ get; set; }

	public virtual bool Published
	{ get; set; }

	[Required(ErrorMessage = "PostedOn: Field is required")]
	public virtual DateTime PostedOn
	{ get; set; }

	public virtual DateTime? Modified
	{ get; set; }

	public virtual Category Category
	{ get; set; }

	public virtual IList<Tag> Tags
	{ get; set; }
}

Listing 64. Post class with validations

I think the code is self-explanatory and there is no need for much explanation. We have pretty much completed the server-side work to add new post. Let's focus on the client-side stuff.

If you see the grid, there is no facility to add new posts. To make the grid to provide options to add, edit, delete and for others we have to configure the "navigator toolbar".

To configure the toolbar all we have to do is call the navGrid() function.

$(gridName).navGrid(pagerName,
	{ settings },
	{ add options },
	{ edit options },
	{ delete options },
	{ search options },
	{ view options }
);

Listing 65. jQGrid's navGrid function

We can pass a bunch of parameters to the navGrid function and most of them are optional. To keep things simple, we are ignoring the last two options (search and view) in our case. To know more about the navigator options visit this page.

$(gridName).navGrid(pagerName,
	{ // settings
		cloneToTop: true,
        search: false
    },
	{}, // add options
	{}, // edit options
	{} // delete options
);

Listing 66. navGrid()

Passing the cloneToTop property as true in the settings object makes the navigator to appear at both the top and bottom of the grid. Passing search as false make the search icon not appear in the navigator toolbar (we are not going to implement the search functionality, you can take it as an exercise!).

Just to see how things are coming up, we are passing empty object to add, edit and delete options. Soon we will pass real values to those parameters. When we run the application, we could see the below screen.

Navigation toolbar

Navigation toolbar

We can see the navigation toolbar displayed at both the top and bottom of the grid with four icons (add, edit, delete and refresh).

What happen when you click the add icon?

Add dialog

Add dialog

You'll see a modal dialog popped up without any input fields and this is because we haven't configured the edit options for the fields. For some fields, we need textboxes and for some other fields we need dropdowns and so on. We have to specify these options while we defining the column model. Come-on, let's do it!

The following are the edit options we need to apply to the columns.

1. The Id column should not be editable.
2. The Title column should be editable and we need to display a textbox in the popup.
3. The ShortDescription column should be editable and we need to display tinyMCE editor (rich textbox).
4. The Description column should be editable and we need to display tinyMCE editor. This property only contain the complete information of the blog post.
5. The Category.Id column should be editable and we need to display a dropdown with all the categories in the blog.
6. The Category.Name column should not be editable.
7. The Tags column should be editable and we need to display a multi-select dropdown with all the tags in the blog.
8. The Meta and UrlSlug columns should be editable and we need to display text-boxes for them. Note that, in the part 1, I told we will be creating the UrlSlug programmatically from the Title. Due to the shortage of time I'm making it a manual process which means user has to enter appropriate value.
9. The Published column should be editable and we need to display a checkbox.
10. Both PostedOn and Modified columns are not editable.

Let's apply the jqgrid edit options to the columns.


postsGrid: function (gridName, pagerName) {

	var colNames = [
		'Id',
		'Title',
		'Short Description',
		'Description',
		'Category',
		'Category',
		'Tags',
		'Meta',
		'Url Slug',
		'Published',
		'Posted On',
		'Modified'
	];

	var columns = [];

	columns.push({
		name: 'Id',
		hidden: true,
		key: true
	});

	columns.push({
		name: 'Title',
		index: 'Title',
		width: 250,
		editable: true,
		editoptions: {
			size: 43,
			maxlength: 500
		},
		editrules: {
			required: true
		}
	});

	columns.push({
		name: 'ShortDescription',
		width: 250,
		sortable: false,
		hidden: true,
		editable: true,
		edittype: 'textarea',
		editoptions: {
			rows: "10",
			cols: "100"
		},
        editrules: {
            edithidden: true
        }
	});

	columns.push({
		name: 'Description',
		width: 250,
		sortable: false,
		hidden: true,
		editable: true,
		edittype: 'textarea',
		editoptions: {
			rows: "40",
			cols: "100"
		},
        editrules: {
            edithidden: true
        }
	});

	columns.push({
		name: 'Category.Id',
		hidden: true,
		editable: true,
		edittype: 'select',
		editoptions: {
			style: 'width:250px;',
			dataUrl: '/Admin/GetCategoriesHtml'
		},
		editrules: {
			required: true,
			edithidden: true
		}
	});

	columns.push({
		name: 'Category.Name',
		index: 'Category',
		width: 150
	});

	columns.push({
		name: 'Tags',
		width: 150,
		editable: true,
		edittype: 'select',
		editoptions: {
			style: 'width:250px;',
			dataUrl: '/Admin/GetTagsHtml',
			multiple: true
		},
		editrules: {
			required: true
		}
	});

	columns.push({
		name: 'Meta',
		width: 250,
		sortable: false,
		editable: true,
		edittype: 'textarea',
			editoptions: {
			rows: "2",
			cols: "40",
			maxlength: 1000
		},
		editrules: {
			required: true
		}
	});

	columns.push({
		name: 'UrlSlug',
		width: 200,
		sortable: false,
		editable: true,
		editoptions: {
			size: 43,
			maxlength: 200
		},
		editrules: {
			required: true
		}
	});

	columns.push({
		name: 'Published',
		index: 'Published',
		width: 100,
		align: 'center',
		editable: true,
		edittype: 'checkbox',
		editoptions: {
			value: "true:false",
			defaultValue: 'false'
		}
	});

	columns.push({
		name: 'PostedOn',
		index: 'PostedOn',
		width: 150,
		align: 'center',
		sorttype: 'date',
		datefmt: 'm/d/Y'
	});

	columns.push({
		name: 'Modified',
		index: 'Modified',
		width: 100,
		align: 'center',
		sorttype: 'date',
		datefmt: 'm/d/Y'
	});

	$(gridName).jqGrid({
		url: '/Admin/Posts',
		datatype: 'json',
		mtype: 'GET',
		height: 'auto',
		toppager: true,

		colNames: colNames,
		colModel: columns,

		pager: pagerName,
		rownumbers: true,
		rownumWidth: 40,
		rowNum: 10,
		rowList: [10, 20, 30],

		sortname: 'PostedOn',
		sortorder: 'desc',
		viewrecords: true,

		jsonReader: {
			repeatitems: false
		},

		afterInsertRow: function (rowid, rowdata, rowelem) {
			var tags = rowdata["Tags"];
			var tagStr = "";

			$.each(tags, function(i, t){
				if(tagStr) tagStr += ", "
				tagStr += t.Name;
			});


			$(gridName).setRowData(rowid, { "Tags": tagStr });
		}
	});

	$(gridName).navGrid(pagerName,
						{
							cloneToTop: true,
							search: false
						}, {}, {}, {});
};

Listing 67. postsGrid()

Let see the edit options that we have applied to the columns. We have mainly used four properties: editable, edittype, editoptions and editrules.

Property Description
editable The editable option is a boolean and can have a value of true or false. The option defines whether this field is editable (or not). Default is false. To make a field editable, set this option to true.
edittype Edittype option defines the type of of the editable field. Possible values: 'text', 'textarea', 'select', 'checkbox', 'password', 'button', 'image', 'file' and 'custom'. The default value is 'text'.
editoptions The editoptions property is an array which contains information about the editing column.
editrules This option add additional properties to the editable element and should be used in colModel. Mostly it is used to validate the user input before submitting the value(s) to the server.

To have a detail understanding about the edit options please visit here.

Now, if you click the add icon, you'll see the below screen.

Add record dialog

Add record dialog

The fields are displayed properly but the width of the dialog is small! We'll see how to make those two fields appear soon, but before that let's increase the width of the dialog. We can configure the width of the dialog, the url where the form has to be posted and other things through the add options object that we pass to the navGrid function.

// configuring add options
var addOptions = {
	url: '/Admin/AddPost',
	addCaption: 'Add Post',
	processData: "Saving...",
	width: 900,
	closeAfterAdd: true,
	closeOnEscape: true
};

$(gridName).navGrid(pagerName,
					{
						cloneToTop: true,
						search: false
					},
					{}, addOptions, {});

Listing 68. Passing addOptions to navGrid()

The addOptions parameter contains many properties. Some of the important properties are shown in the below table. You can see the complete properties list here.

Property Description
url url where to post data
closeAfterAdd Close the dialog after the record is added successfully
closeOnEscape When set to true the modal window can be closed with ESC key from the user.

After these changes, our add dialog should look like below.

Add record dialog

Add record dialog

4.3.4 Integrating tinyMCE with dialog

If you remember, before configuring the edit options I told we need to display tinyMCE editor for "Short Description" and "Description" fields. jQGrid provides different types of edittype like "text", "textarea", "checkbox". It also provides an option called "custom" which you can read from here. But I felt difficult in using that option to integrate tinyMCE editor to the form and so we are going to use a different approach!

To create a tinyMCE editor we need a textarea to be rendered in the page. The textarea can be converted into editor by calling the execCommand of the tinyMCE object.

<textarea name="ShortDescription"></textarea>

Listing 69. textarea html control

tinyMCE.execCommand('mceAddControl', false, "ShortDescription");

Listing 70. Creating editor using tinyMCE.execCommand

The execCommand accepts three parameters. The first parameter is the command. The second one is the boolean (I don't have much idea about this!) and the third parameter is the name of the textarea.

To convert the editor back to textarea we have to pass the command string as "mceRemoveControl".

i.e.,

tinyMCE.execCommand('mceRemoveControl', false, "ShortDescription");

Listing 71. Removing editor using tinyMCE.execCommand

Our plan is when the dialog is shown we have to convert the textareas into editors and when the dialog is closed we have to do the reverse thing.

var afterShowForm = function (form) {
	tinyMCE.execCommand('mceAddControl', false, "ShortDescription");
	tinyMCE.execCommand('mceAddControl', false, "Description");
};

var onClose = function (form) {
	tinyMCE.execCommand('mceRemoveControl', false, "ShortDescription");
	tinyMCE.execCommand('mceRemoveControl', false, "Description");
};

 // configuring add options
var addOptions = {
	url: '/Admin/AddPost',
	addCaption: 'Add Post',
	processData: "Saving...",
	width: 900,
	closeAfterAdd: true,
	closeOnEscape: true,
	afterShowForm: afterShowForm,
	onClose: onClose,
};

$(gridName).navGrid(pagerName,
					{
						cloneToTop: true,
						search: false
					},
					{}, addOptions, {});

Listing 72. afterShowForm and onClose event handlers

What we have done is, we have subscribed to both the afterShowForm and onClose events of the add dialog. In the afterShowForm event we are converting both the textareas rendered for Description and ShortDescription into editors and in the onClose event we are doing the reverse thing. If we don't remove the editors in the onClose event then in the next time when we open the dialog we may encounter some issues.

If everything is done right, on clicking the add icon we'll see the editors as shown below,

Integrating tinyMCE editors

Integrating tinyMCE editors

4.3.5 Specifying data to populate for Category and Tags dropdowns

If you see the editoptions that we have specified before for each column, the edittype of both the Category.Id and Tags are set as "select" and so dropdowns are shown in the form. We have to specify the urls for those two fields to fetch the data that needs to populate the dropdowns.

columns.push({
	name: 'Category.Id',
	...
	editoptions: {
		style: 'width:250px;',
		dataUrl: '/Admin/GetCategoriesHtml'
	},
	...
});

columns.push({
	name: 'Tags',
	...
	editoptions: {
		style: 'width:250px;',
		dataUrl: '/Admin/GetTagsHtml',
		multiple: true
	},
	...
});

Listing 73. Specifying dataUrl for Category and Tag dropdowns

The dataUrl we have specified in the editoptions point to the actions that returns html to populate the dropdowns.

Here are the implementation of those actions.

public ContentResult GetCategoriesHtml()
{
	var categories = _blogRepository.Categories().OrderBy(s => s.Name);

	var sb = new StringBuilder();
	sb.AppendLine(@"<select>");

	foreach (var category in categories)
	{
		sb.AppendLine(string.Format(@"<option value=""{0}"">{1}</option>",
			category.Id, category.Name));
	}

	sb.AppendLine("<select>");
	return Content(sb.ToString(), "text/html");
}

public ContentResult GetTagsHtml()
{
	var tags = _blogRepository.Tags().OrderBy(s => s.Name);

	StringBuilder sb = new StringBuilder();
	sb.AppendLine(@"<select multiple=""multiple"">");

	foreach (var tag in tags)
	{
		sb.AppendLine(string.Format(@"<option value=""{0}"">{1}</option>",
			tag.Id, tag.Name));
	}

	sb.AppendLine("<select>");
	return Content(sb.ToString(), "text/html");
}

Listing 74. GetCategoriesHtml() and GetTagsHtml()

We have pretty much completed the work in the add dialog. Let's try to add a new post and remember we already have the action in the admin controller to add the post to database.

First let's see the client-side validations are working fine by trying to add a post leaving the Title field empty (Note that, we have set the client-side validations through the editrules property in each column).

Client-side validation

Client-side validation

When you click the submit button you could see the error message displayed at the top of the dialog. So, our client-side validations are working. But note that, we haven't set the validations for both ShortDescription and Description columns. Unlike the other fields, the required validation can't be done directly on the editor fields, we have to create custom validation function to make it work!

To set custom validation for the field, we have to set the custom property in editrules to true. And, the custom_func property should contain the appropriate function that does the validation.

columns.push({
	name: 'ShortDescription',
	index: 'ShortDescription',
	width: 250,
	editable: true,
	sortable: false,
	hidden: true,

	edittype: 'textarea',
		editoptions: {
		rows: "10",
		cols: "100"
	},

	editrules: {
		custom: true,

		custom_func: function(val, colname){
			val = tinyMCE.get("ShortDescription").getContent();
			if(val) return [true, ""];
				return [false, colname + ": Field is required"];
		},

		edithidden: true
	}
});

columns.push({
	name: 'Description',
	index: 'Description',
	width: 250,
	editable: true,
	sortable: false,
	hidden: true,
	edittype: 'textarea',

	editoptions: {
		rows: "40",
		cols: "100"
	},

	editrules: {
		custom: true,

		custom_func: function(val, colname){
			val = tinyMCE.get("Description").getContent();
			if(val) return [true, ""];
				return [false, colname + ": Field is requred"];
		},

		edithidden: true
	}
});

Listing 75. Specifying client-side validations for ShortDescription and Description fields

That's all about the validation stuff!

What happens when we submit the form without validation errors? We could see that, the add dialog get closed but the new post is not added to the database. Why?

If we explore the returned JSON from server using Firebug you could see the below thing,

Server JSON response

Server JSON response

So we are facing two problems! First something is going wrong in the server and the new post is not getting added. Second the add dialog is getting closed even-though the server returns an error. We could easily fix the second issue by hooking some code into the afterSubmit event of the grid.

JustBlog.GridManager = {
	postsGrid: function(gridName, pagerName) {
		...

		var addOptions = {
			url: '/Admin/AddPost',
			addCaption: 'Add Post',
			processData: "Saving...",
			width: 900,
			closeAfterAdd: true,
			closeOnEscape: true,
			afterShowForm: afterShowForm,
			onClose: onClose,
			afterSubmit: JustBlog.GridManager.afterSubmitHandler
		};

		$(gridName).navGrid(pagerName,
		{
			cloneToTop: true,
			search: false
		},
		{}, addOptions, {});

	},

	afterSubmitHandler: function(response, postdata) {

		var json = $.parseJSON(response.responseText);

		if (json) return [json.success, json.message, json.id];

		return [false, "Failed to get result from server.", null];
	}
}

Listing 76. Hooking code into afterSubmit event

I've placed the afterSubmitHandler function outside the postsGrid function because we need that for other grids as well. What we are doing in the event handler is, we are parsing the returned JSON and returning an array with success/failure information to the jqgrid. Now, the add dialog not vanishes away when the server returns an error and the error message is displayed at the top of the dialog.

Server error

Server error

Next thing we have to try is debug the server-code and find why add the post fails.

Debugging AddPost action

Debugging AddPost action

When debugging we can find the Description property is empty, but we have entered some value for that.

When we see the post data in the Firebug we could find no values are submitted for both Description and ShortDescription fields.

Post data in Firebug

Post data in Firebug

Since tinyMCE editors are special fields we have to do some work before the form is submitted to the server. jQGrid provides an event called beforeSubmit just for that.

var beforeSubmitHandler = function (postdata, form) {
	var selRowData = $(gridName).getRowData($(gridName).getGridParam('selrow'));
	if(selRowData["PostedOn"])
		postdata.PostedOn = selRowData["PostedOn"];
	postdata.ShortDescription = tinyMCE.get("ShortDescription").getContent();
	postdata.Description = tinyMCE.get("Description").getContent();

	return [true];
};

// configuring add options
var addOptions = {
	url: '/Admin/AddPost',
	...
	afterSubmit: JustBlog.GridManager.afterSubmitHandler,
	beforeSubmit: beforeSubmitHandler
};

Listing 77. Hooking code into beforeSubmit event

I've created a private function beforeSubmitHandler which is hooked into the beforeSubmit event. What we are doing in the event handler is, read the values from the editors and add them to the post data. Now the values we entered in both the editor fields are successfully submitted to the server.

When you try again, you could see again the same error message returned from the server. The reason this time it fails is due to validation errors occured at server side. One of the main validation error is we are trying to bind a string value (comma separated numbers) to the Tags collection of the Post object. To overcome the validation issues and also when we save the Post object we need the actual Category and Tag objects connected to them. So a better option is to go for a custom model binder.

Custom ModelBinder

As default, model binding is taken care by the DefaultModelBinder. Sometimes the DefaultModelBinder doesn't fits and at those cases we can easily go for a custom model binder either extending the DefaultModelBinder or directly implementing IModelBinder. The custom modelbinder can either be applied at the global level or at action level using the ModelBinder attribute.

Here is the complete code of the custom modelbinder.

using JustBlog.Core;
using JustBlog.Core.Objects;
using Ninject;
using System;
using System.Collections.Generic;
using System.Web.Mvc;

namespace JustBlog
{
    public class PostModelBinder : DefaultModelBinder
    {
	    private readonly IKernel _kernel;

	    public PostModelBinder(IKernel kernel)
	    {
		    _kernel = kernel;
	    }

	    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
	    {
		    var post = (Post)base.BindModel(controllerContext, bindingContext);

		    var _blogRepository = _kernel.Get<IBlogRepository>();

		    if (post.Category != null)
			    post.Category = _blogRepository.Category(post.Category.Id);

		    var tags = bindingContext.ValueProvider.GetValue("Tags").AttemptedValue.Split(',');

		    if (tags.Length > 0)
		    {
			    post.Tags = new List<Tag>();

			    foreach (var tag in tags)
			    {
				    post.Tags.Add(_blogRepository.Tag(int.Parse(tag.Trim())));
			    }
		    }

		    if (bindingContext.ValueProvider.GetValue("oper").AttemptedValue.Equals("edit"))
			    post.Modified = DateTime.UtcNow;
		    else
			    post.PostedOn = DateTime.UtcNow;

		    return post;
	    }
    }
}

Listing 78. PostModelBinder

We need an instance of IBlogRepository implementation so we have passed IKernel to the constructor. What we are doing in the custom model binder is, first we are getting Post instance generated by the DefaultModelBinder and then assigning the actual Category and Tag objects fetching from database.

We need to implement couple of methods to get the Category and Tag by id.

public interface IBlogRepository
{
	...
	Category Category(int id);

	Tag Tag(int id);
}
public Category Category(int id)
{
	return _session.Query<Category>().FirstOrDefault(t => t.Id == id);
}

public Tag Tag(int id)
{
	return _session.Query<Tag>().FirstOrDefault(t => t.Id == id);
}

Listing 79. Category and Tag methods

To use our custom model binder for Post we have to add the below statement in the OnApplicationStarted method of Global.asax.cs.

protected override void OnApplicationStarted()
{
	...
	ModelBinders.Binders.Add(typeof(Post), new PostModelBinder(Kernel));
}

Listing 80. Adding PostModelBinder to ModelBinders collection

We need to change the AddPost action in the AdminController little bit to make things work using the custom model binder approach.

[HttpPost, ValidateInput(false)]
public ContentResult AddPost(Post post)
{
	string json;

	ModelState.Clear();

	if (TryValidateModel(post))
	{
		var id = _blogRepository.AddPost(post);

		json = JsonConvert.SerializeObject(new
		{
			id = id,
			success = true,
			message = "Post added successfully."
		});
	}
	else
	{
		json = JsonConvert.SerializeObject(new
		{
			id = 0,
			success = false,
			message = "Failed to add the post."
		});
	}

	return Content(json, "application/json");
}

Listing 81. AddPost action

ValidateInput attribute

By default, the ASP.NET MVC framework prevents you from submitting form data that contains potentially malicious content. This feature is called request validation. ValidateInput attribute is used to disable request validation for an action.

What changes we have done in the above method is, once our custom model binder successfully delivered the Post instance to the action, we are again kicking the validation by calling the TryValidateModel method. The TryValidateModel method again validates the model and returns true if there are no validation errors.

If we try to add a post again, we could see the new post successfully added to the database.

Add new post

Add new post

Posts grid

Posts grid

4.4 Implement functionality to edit post

We have successfully implemented the add functionality and implementing the edit functionality is quite easy.

First implement the repository method.

public interface IBlogRepository
{
	...
	void EditPost(Post post);
}

Listing 82. IBlogRepository

public void EditPost(Post post)
{
	using (var tran = _session.BeginTransaction())
	{
		_session.SaveOrUpdate(post);
		tran.Commit();
	}
}

Listing 83. EditPost implementation

Implement the action which is quite same like the AddPost implementation.

[HttpPost, ValidateInput(false)]
public ContentResult EditPost(Post post)
{
	string json;

	ModelState.Clear();

	if (TryValidateModel(post))
	{
		_blogRepository.EditPost(post);
		json = JsonConvert.SerializeObject(new
		{
			id = post.Id,
			success = true,
			message = "Changes saved successfully."
		});
	}
	else
	{
		json = JsonConvert.SerializeObject(new
		{
			id = 0,
			success = false,
			message = "Failed to save the changes."
		});
	}

	return Content(json, "application/json");
}

Listing 84. EditPost action

Finally, we have to make some changes in the script to pass the editOptions to the navGrid function.

var editOptions = {
	url: '/Admin/EditPost',
	editCaption: 'Edit Post',
	processData: "Saving...",
	width: 900,
	closeAfterEdit: true,
	closeOnEscape: true,
	afterclickPgButtons: afterclickPgButtons,
	afterShowForm: afterShowForm,
	onClose: onClose,
	afterSubmit: JustBlog.GridManager.afterSubmitHandler,
	beforeSubmit: beforeSubmitHandler
};

$(gridName).navGrid(pagerName,
{
	cloneToTop: true,
	search: false
},
editOptions, addOptions, {});

Listing 85. Passing editOptions to navGrid function

We can now successfully edit the existing posts.

4.5 Implement functionality to delete post

Here is the definition and implementation of the repository method.

public interface IBlogRepository
{
	...
	void DeletePost(int id);
}

Listing 86. IBlogRepository

public void DeletePost(int id)
{
	using (var tran = _session.BeginTransaction())
	{
		var post = _session.Get<Post>(id);
		_session.Delete(post);
		tran.Commit();
	}
}

Listing 87. DeletePost implementation

The action implementation is,

[HttpPost]
public ContentResult DeletePost(int id)
{
	_blogRepository.DeletePost(id);

	var json = JsonConvert.SerializeObject(new
	{
		id = 0,
		success = true,
		message = "Post deleted successfully."
	});

	return Content(json, "application/json");
}

Listing 88. DeletePost action

Passing the deleteOptions to the navGrid function.

var deleteOptions = {
	url: '/Admin/DeletePost',
	caption: 'Delete Post',
	processData: "Saving...",
	msg: "Delete the Post?",
	closeOnEscape: true,
	afterSubmit: JustBlog.GridManager.afterSubmitHandler
};

$(gridName).navGrid(pagerName,
{
	cloneToTop: true,
	search: false
},
editOptions, addOptions, deleteOptions);

Listing 89. Passing deleteOptions to navGrid function

So we have successfully completed the add, edit and delete functionalities for Post. Implementing the same things for Category and Tag is quite easy and that's what we are going to do in the coming stories.

5. Story #3 - Implement functionalities to add, edit and delete categories

Being completed the manage functionalities for post, completing this story is not difficult. The following are the tasks we are going to work on this story.

1. Implement action to return categories that has to be displayed in the grid
2. Implement functionality to add category
3. Implement functionality to edit category
4. Implement functionality to delete category

5.1 Implement action to return categories that has to be displayed in the grid

In this task, we are going to implement the repository method and action to return all the categories that will be displayed in the jQGrid.

The repository method to return all the categories is already there (which we created in Part 1 to display the categories in sidebar). Next, we have to create an action in AdminController that return the categories as JSON.

public ContentResult Categories()
{
	var categories = _blogRepository.Categories();

	return Content(JsonConvert.SerializeObject(new
	{
		page = 1,
		records = categories.Count,
		rows = categories,
		total = 1
	}), "application/json");
}

Listing 90. Categories action

Let's do the work in the client-side to display the categories in the grid. If you remember we already have a function with name categoriesGrid in JustBlog.GridManager. Let's complete that.

categoriesGrid: function (gridName, pagerName) {
	var colNames = ['Id', 'Name', 'Url Slug', 'Description'];

	var columns = [];

	columns.push({
		name: 'Id',
		index: 'Id',
		hidden: true,
		sorttype: 'int',
		key: true,
		editable: false,
		editoptions: {
			readonly: true
		}
	});

	columns.push({
		name: 'Name',
		index: 'Name',
		width: 200,
		editable: true,
		edittype: 'text',
		editoptions: {
			size: 30,
			maxlength: 50
		},
		editrules: {
			required: true
		}
	});

	columns.push({
		name: 'UrlSlug',
		index: 'UrlSlug',
		width: 200,
		editable: true,
		edittype: 'text',
		sortable: false,
		editoptions: {
			size: 30,
			maxlength: 50
		},
		editrules: {
			required: true
		}
	});

	columns.push({
		name: 'Description',
		index: 'Description',
		width: 200,
		editable: true,
		edittype: 'textarea',
		sortable: false,
		editoptions: {
			rows: "4",
			cols: "28"
		}
	});

	$(gridName).jqGrid({
		url: '/Admin/Categories',
		datatype: 'json',
		mtype: 'GET',
		height: 'auto',
		toppager: true,
		colNames: colNames,
		colModel: columns,
		pager: pagerName,
		rownumbers: true,
		rownumWidth: 40,
		rowNum: 500,
		sortname: 'Name',
		loadonce: true,
		jsonReader: {
			repeatitems: false
		}
	});

	// configuring the navigation toolbar.
	$(gridName).jqGrid('navGrid', pagerName,
	{
		cloneToTop: true,
		search: false
	},
	{}, {}, {});
};

Listing 91. categoriesGrid function

I think explanation is not required. We have already discussed about these in posts grid. This is the output you would see on visting the "Categories" tab.

Categories grid

Categories grid

5.2 Implement functionality to add category

Let's complete the server side work first. First we have to set validations at the Category class. Then implement the repository method and action.

public class Category
{
	public virtual int Id
	{ get; set; }

	[Required(ErrorMessage = "Name: Field is required")]
	[StringLength(500, ErrorMessage = "Name: Length should not exceed 500 characters")]
	public virtual string Name
	{ get; set; }

	[Required(ErrorMessage = "UrlSlug: Field is required")]
	[StringLength(500, ErrorMessage = "UrlSlug: Length should not exceed 500 characters")]
	public virtual string UrlSlug
	{ get; set; }

	public virtual string Description
	{ get; set; }

	[JsonIgnore]
	public virtual IList<Post> Posts
	{ get; set; }
}

Listing 92. Category class

Here is our repository method definition and implementation.

int AddCategory(Category category);

Listing 93. IBlogRepository

public int AddCategory(Category category)
{
	using (var tran = _session.BeginTransaction())
	{
		_session.Save(category);
		tran.Commit();
		return category.Id;
	}
}

Listing 94. AddCategory method

We don't need any custom model binder for Category. Below is the AddCategory action implementation.

[HttpPost]
public ContentResult AddCategory([Bind(Exclude = "Id")]Category category)
{
	string json;

	if (ModelState.IsValid)
	{
		var id = _blogRepository.AddCategory(category);
		json = JsonConvert.SerializeObject(new
		{
			id = id,
			success = true,
			message = "Category added successfully."
		});
	}
	else
	{
		json = JsonConvert.SerializeObject(new
		{
			id = 0,
			success = false,
			message = "Failed to add the category."
		});
	}

	return Content(json, "application/json");
}

Listing 95. AddCategory action

One interesting thing to note down is we have decorated our action with Bind attribute. The Bind attribute is used to tell the model binder what are the parameters that has be excluded or only included in model binding. In our AddCategory action, we have excluded the Id attribute from model binding. The reason is, when adding any entity jQGrid post a string value "_empty" for Id. The DefaultModelBinder can't bind a string value to the integer property Id and sets a validation error. To avoid the validation error, we are excluding the Id property from binding and this is only at the time of adding.

In the client-side all we have to do is, create an addOptions object, set the corresponding properties and pass it to the navGrid function.

var addOptions = {
	url: '/Admin/AddCategory',
	width: 400,
	addCaption: 'Add Category',
	processData: "Saving...",
	closeAfterAdd: true,
	closeOnEscape: true,
	afterSubmit: function(response, postdata){
		var json = $.parseJSON(response.responseText);

		if (json) {
			// since the data is in the client-side, reload the grid.
			$(gridName).jqGrid('setGridParam',{datatype:'json'});
			return [json.success, json.message, json.id];
		}

		return [false, "Failed to get result from server.", null];
	}
};

// configuring the navigation toolbar.
$(gridName).jqGrid('navGrid', pagerName,
{
	cloneToTop: true,
	search: false
},

{}, addOptions, {});

Listing 96. Passing addOptions

That's it! Now we can easily add new categories to the blog.

5.3 Implement functionality to edit category

First implement the repository method to edit category.

void EditCategory(Category category);

Listing 97. IBlogRepository

public void EditCategory(Category category)
{
	using (var tran = _session.BeginTransaction())
	{
		_session.SaveOrUpdate(category);
		tran.Commit();
	}
}

Listing 98. EditCategory method

Create an action with name EditCategory.

[HttpPost]
public ContentResult EditCategory(Category category)
{
	string json;

	if (ModelState.IsValid)
	{
		_blogRepository.EditCategory(category);
		json = JsonConvert.SerializeObject(new
		{
			id = category.Id,
			success = true,
			message = "Changes saved successfully."
		});
	}
	else
	{
		json = JsonConvert.SerializeObject(new
		{
			id = 0,
			success = false,
			message = "Failed to save the changes."
		});
	}

	return Content(json, "application/json");
}

Listing 99. EditCategory action

Passing the editOptions to the navGrid() to enable the grid for editing categories.

var editOptions = {
	url: '/Admin/EditCategory',
	width: 400,
	editCaption: 'Edit Category',
	processData: "Saving...",
	closeAfterEdit: true,
	closeOnEscape: true,
	afterSubmit: function(response, postdata){
		var json = $.parseJSON(response.responseText);

		if (json) {
			$(gridName).jqGrid('setGridParam',{datatype:'json'});
			return [json.success, json.message, json.id];
		}

		return [false, "Failed to get result from server.", null];
	}
};

// configuring the navigation toolbar.
$(gridName).jqGrid('navGrid', pagerName, {
	cloneToTop: true,
	search: false
},

editOptions, addOptions, {});

Listing 100. Passing editOptions

5.4 Implement functionality to delete category

Implement the repository method to delete category.

void DeleteCategory(int id);

Listing 101. IBlogRepository

public void DeleteCategory(int id)
{
	using (var tran = _session.BeginTransaction())
	{
		var category = _session.Get<Category>(id);
		_session.Delete(category);
		tran.Commit();
	}
}

Listing 102. DeleteCategory method

Below is the DeleteCategory implementation.

[HttpPost]
public ContentResult DeleteCategory(int id)
{
	_blogRepository.DeleteCategory(id);

	var json = JsonConvert.SerializeObject(new
	{
		id = 0,
		success = true,
		message = "Category deleted successfully."
	});

	return Content(json, "application/json");
}

Listing 103. DeleteCategory action

Passing the deleteOptions to the navGrid().

var deleteOptions = {
	url: '/Admin/DeleteCategory',
	caption: 'Delete Category',
	processData: "Saving...",
	width: 500,
	msg: "Delete the category? This will delete all the posts belongs to this category as well.",
	closeOnEscape: true,
	afterSubmit: JustBlog.GridManager.afterSubmitHandler
};

// configuring the navigation toolbar.
$(gridName).jqGrid('navGrid', pagerName,
{
	cloneToTop: true,
	search: false
},

editOptions, addOptions, deleteOptions);

Listing 104. Passing deleteOptions

We have completed the add, edit and delete functionalities for Category. In the next story, we will see about implementing the same functionalities for tag.

6. Story #4 - Implement functionalities to add, edit and delete tags

The following are the tasks we are going to work on this story.

1. Implement action to return tags that has to be displayed in the grid
2. Implement functionality to add tag
3. Implement functionality to edit tag
4. Implement functionality to delete tag

6.1 Implement action to return tags that has to be displayed in the grid

In this task, we are going to implement the repository method and action to return all the tags that will be displayed in the jQGrid. The repository method to return all the tags is already there.

Create an action in AdminController that return the tags as JSON.

public ContentResult Tags()
{
	var tags = _blogRepository.Tags();

	return Content(JsonConvert.SerializeObject(new
	{
		page = 1,
		records = tags.Count,
		rows = tags,
		total = 1
	}), "application/json");
}

Listing 105. Tags action

Let's complete the tagsGrid() function in the client-side to display the tags in the grid.

tagsGrid: function (gridName, pagerName) {
	var colNames = ['Id', 'Name', 'Url Slug', 'Description'];

	var columns = [];

	columns.push({
		name: 'Id',
		index: 'Id',
		hidden: true,
		sorttype: 'int',
		key: true,
		editable: false,
		editoptions: {
			readonly: true
		}
	});

	columns.push({
		name: 'Name',
		index: 'Name',
		width: 200,
		editable: true,
		edittype: 'text',
		editoptions: {
			size: 30,
			maxlength: 50
		},
		editrules: {
			required: true
		}
	});

	columns.push({
		name: 'UrlSlug',
		index: 'UrlSlug',
		width: 200,
		editable: true,
		edittype: 'text',
		sortable: false,
		editoptions: {
			size: 30,
			maxlength: 50
		},
		editrules: {
			required: true
		}
	});

	columns.push({
		name: 'Description',
		index: 'Description',
		width: 200,
		editable: true,
		edittype: 'textarea',
		sortable: false,
		editoptions: {
			rows: "4",
			cols: "28"
		}
	});

	$(gridName).jqGrid({
		url: '/Admin/Tags',
		datatype: 'json',
		mtype: 'GET',
		height: 'auto',
		toppager: true,
		colNames: colNames,
		colModel: columns,
		pager: pagerName,
		rownumbers: true,
		rownumWidth: 40,
		rowNum: 500,
		sortname: 'Name',
		loadonce: true,
		jsonReader: {
			repeatitems: false
		}
	});

	// configuring the navigation toolbar.
	$(gridName).jqGrid('navGrid', pagerName,
	{
		cloneToTop: true,
		search: false
	},

	{}, {}, {});
};

Listing 106. tagsGrid function

On clicking the "Tags" tab, you would see the below screen.

Tags grid

Tags grid

6.2 Implement functionality to add tag

Let's complete the server side work first. First we have to set validations at the Tag class then implement the repository method and action.

public class Tag
{
	public virtual int Id
	{ get; set; }

	[Required(ErrorMessage = "Name: Field is required")]
	[StringLength(500, ErrorMessage = "Name: Length should not exceed 500 characters")]
	public virtual string Name
	{ get; set; }

	[Required(ErrorMessage = "UrlSlug: Field is required")]
	[StringLength(500, ErrorMessage = "UrlSlug: Length should not exceed 500 characters")]
	public virtual string UrlSlug
	{ get; set; }

	public virtual string Description
	{ get; set; }

	[JsonIgnore]
	public virtual IList<Post> Posts
	{ get; set; }
}

Listing 107. Tag class

Here is our repository method definition and implementation.

int AddTag(Tag tag);

Listing 108. IBlogRepository

public int AddTag(Tag tag)
{
	using (var tran = _session.BeginTransaction())
	{
		_session.Save(tag);
		tran.Commit();
		return tag.Id;
	}
}

Listing 109. AddTag method

Below is the AddTag action implementation.

[HttpPost]
public ContentResult AddTag([Bind(Exclude = "Id")]Tag tag)
{
	string json;

	if (ModelState.IsValid)
	{
		var id = _blogRepository.AddTag(tag);
		json = JsonConvert.SerializeObject(new
		{
			id = id,
			success = true,
			message = "Tag added successfully."
		});
	}
	else
	{
		json = JsonConvert.SerializeObject(new
		{
			id = 0,
			success = false,
			message = "Failed to add the tag."
		});
	}

	return Content(json, "application/json");
}

Listing 110. AddTag action

Client-side work,

var addOptions = {
	url: '/Admin/AddTag',
	width: 400,
	addCaption: 'Add Tag',
	processData: "Saving...",
	closeAfterAdd: true,
	closeOnEscape: true,
	afterSubmit: function(response, postdata){
		var json = $.parseJSON(response.responseText);

		if (json) {
			$(gridName).jqGrid('setGridParam',{datatype:'json'});
			return [json.success, json.message, json.id];
		}

		return [false, "Failed to get result from server.", null];
	}
};

// configuring the navigation toolbar.
$(gridName).jqGrid('navGrid', pagerName,
{
	cloneToTop: true,
	search: false
},

{}, addOptions, {});

Listing 111. Passing addOptions

6.3 Implement functionality to edit tag

Implement repository method to edit tag.

void EditTag(Tag tag);

Listing 112. IBlogRepository

public void EditTag(Tag tag)
{
	using (var tran = _session.BeginTransaction())
	{
		_session.SaveOrUpdate(tag);
		tran.Commit();
	}
}

Listing 113. EditTag method

Here is the EditTag action.

[HttpPost]
public ContentResult EditTag(Tag tag)
{
	string json;

	if (ModelState.IsValid)
	{
		_blogRepository.EditTag(tag);
		json = JsonConvert.SerializeObject(new
		{
			id = tag.Id,
			success = true,
			message = "Changes saved successfully."
		});
	}
	else
	{
		json = JsonConvert.SerializeObject(new
		{
			id = 0,
			success = false,
			message = "Failed to save the changes."
		});
	}

	return Content(json, "application/json");
}

Listing 114. EditTag action

Passing the editOptions to the navGrid() to enable the grid for editing tags.

var editOptions = {
	url: '/Admin/EditTag',
	width: 400,
	editCaption: 'Edit Tag',
	processData: "Saving...",
	closeAfterEdit: true,
	closeOnEscape: true,
	afterSubmit: function(response, postdata){
		var json = $.parseJSON(response.responseText);

		if (json) {
			$(gridName).jqGrid('setGridParam',{datatype:'json'});
			return [json.success, json.message, json.id];
		}

		return [false, "Failed to get result from server.", null];
	}
};

// configuring the navigation toolbar.
$(gridName).jqGrid('navGrid', pagerName,
{
	cloneToTop: true,
	search: false
},

editOptions, addOptions, {});

Listing 115. Passing editOptions

6.4 Implement functionality to delete tag

Implement the repository method to delete tag.

void DeleteTag(int id);

Listing 116. IBlogRepository

public void DeleteTag(int id)
{
	using (var tran = _session.BeginTransaction())
	{
		var tag = _session.Get<Tag>(id);
		_session.Delete(tag);
		tran.Commit();
	}
}

Listing 117. DeleteTag method

DeleteTag action implementation,

[HttpPost]
public ContentResult DeleteTag(int id)
{
	_blogRepository.DeleteTag(id);

	var json = JsonConvert.SerializeObject(new
	{
		id = 0,
		success = true,
		message = "Tag deleted successfully."
	});

	return Content(json, "application/json");
}

Listing 118. DeleteTag action

Passing the deleteOptions to the navGrid().

var deleteOptions = {
	url: '/Admin/DeleteTag',
	caption: 'Delete Tag',
	processData: "Saving...",
	width: 400,
	msg: "Delete the tag? This will delete all the posts belongs to this tag as well.",
	closeOnEscape: true,
	afterSubmit: JustBlog.GridManager.afterSubmitHandler
};

// configuring the navigation toolbar.
$(gridName).jqGrid('navGrid', pagerName,
{
	cloneToTop: true,
	search: false
},

editOptions, addOptions, deleteOptions);

Listing 119. Passing deleteOptions

By this story, we have completed all the user stories of this part.

7. Summary

In this part we have seen many things. We saw about implementing login page in ASP.NET MVC and how to write unit tests for the controller actions. We also saw about creating an admin page to manage posts, categories and tags.

In the next part, we will see about many interesting things. We will see about exception handling using ELMAH, integrating Disqus commenting system to our blog. We will also see about creating contact page, minifying scripts and much more.

Download Source  Fork in Github

blog comments powered by Disqus