MVC + WebAPI + KnockoutJs ToDo list (Part 1)

In this two part post i'll detail how to build a simple Todo list based on the following technologies. starting with backend on part one:

  • MVC5
  • WebAPI2
  • EF

And on part two, the Frontend:

  • jQuery/jQuery-UI
  • KnockoutJs
  • bootstrap

Database first!

the simple todo application has only one table "Tasks" simple enough, the table stores the tasks. lets describe the columns.
"Order" the order of the task in the todo list.
"Done" flag the todo task completed.
"Text" descripton of the todo task.
"Date" date chosen.

The Model

after creating the databse & table, create a new web application solution. from the template list choose MVC. check the Web Api from the core references. Also to keep this simple change authentication type to No authentication.

once the solution is ready, right click the Models folders -> Add Ado.Net Entity Data Model. specify the name "TasksModel"
select code first from database.
configure your database

after it finished you'll see the "Task" model created.

    public partial class Task
    {
        public int Id { get; set; }

        public int Order { get; set; }

        public bool Done { get; set; }

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

        public DateTime Date { get; set; }
    }

The Controller

We now add a controller, right click controllers folder -> Add Controller. Select an empty controller.

controller name: Tasks
visual studio will create the Index action automatically, we will add the following code to retreive the list of tasks and send them to the view.

        public ActionResult Index()
        {
            TasksModel m = new TasksModel();
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            ViewBag.Title = "Index";
            ViewBag.initialData = JsonConvert.SerializeObject(m.Tasks, new JsonSerializerSettings() { DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc, DateFormatString = "yyyy-MM-dd" });
            return View();
        }

due to the difference of DateTime format between .Net & Javascript, we explicitly format the date in "yyyy-MM-dd" format.

WebApi

once we setup our controller to load the intial data, we need another REST based controller to interact with our data. right click the controllers folder -> add controller. select a web api 2 controller with tasks & actions. here we name it TasksApi.

the API methods are fairly straight forward and easily understandable.

    public class TasksApiController : ApiController
    {
        private TasksModel db = new TasksModel();

        // GET: api/Tasks
        public IQueryable<Task> GetTasks()
        {
            return db.Tasks;
        }

        // GET: api/Tasks/5
        [ResponseType(typeof(Task))]
        public IHttpActionResult GetTask(int id)
        {
            Task task = db.Tasks.Find(id);
            if (task == null)
            {
                return NotFound();
            }

            return Ok(task);
        }


        [ResponseType(typeof(void))]
        public IHttpActionResult PutTask(int[] ids)
        {
            try
            {
                int i = 0;
                List<Task> tasks = db.Tasks.ToList();
                foreach(int id in ids)
                {
                    tasks.FirstOrDefault(x => x.Id == id).Order = i++;
                }
                db.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                return NotFound();
            }

            return StatusCode(HttpStatusCode.NoContent);

        }

        // PUT: api/Tasks/5
        [ResponseType(typeof(void))]
        public IHttpActionResult PutTask(int id, Task task)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != task.Id)
            {
                return BadRequest();
            }

            db.Entry(task).State = EntityState.Modified;

            try
            {
                db.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!TaskExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return StatusCode(HttpStatusCode.NoContent);
        }

        // POST: api/Tasks
        [ResponseType(typeof(Task))]
        public IHttpActionResult PostTask(Task task)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            db.Tasks.Add(task);
            db.SaveChanges();

            return CreatedAtRoute("DefaultApi", new { id = task.Id }, task);
        }

        // DELETE: api/Tasks/5
        [ResponseType(typeof(Task))]
        public IHttpActionResult DeleteTask(int id)
        {
            Task task = db.Tasks.Find(id);
            if (task == null)
            {
                return NotFound();
            }

            db.Tasks.Remove(task);
            db.SaveChanges();

            return Ok(task);
        }

        [ResponseType(typeof(int))]
        public IHttpActionResult DeleteTasks(List<int> ids)
        {
            List<Task> tasks = db.Tasks.Where(x => ids.Contains(x.Id)).ToList();

            if (tasks.Count() == 0)
            {
                return NotFound();
            }

            db.Tasks.RemoveRange(tasks);
            db.SaveChanges();

            return Ok();

        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }

        private bool TaskExists(int id)
        {
            return db.Tasks.Count(e => e.Id == id) > 0;
        }
    }

Perhaps the only method I need explaining is the PutTask(int[] ids). The task view model supports explicit ordering, after the order is done the new list of task IDs are sent back to this method. I admit its a lazy & lame solution.

Part 2 Click HERE