Add new to-do items

The user will add new to-do items with a simple form below the list:

Final form

Adding this feature requires a few steps:

  • Adding JavaScript that will send the data to the server
  • Creating a new action on the controller to handle this request
  • Adding code to the service layer to update the database

Add JavaScript code

The Todo/Index.cshtml view already includes an HTML form that has a textbox and a button for adding a new item. You’ll use jQuery to send a POST request to the server when the Add button is clicked.

Open the wwwroot/js/site.js file and add this code:

  1. $(document).ready(function() {
  2. // Wire up the Add button to send the new item to the server
  3. $('#add-item-button').on('click', addItem);
  4. });

Then, write the addItem function at the bottom of the file:

  1. function addItem() {
  2. $('#add-item-error').hide();
  3. var newTitle = $('#add-item-title').val();
  4. $.post('/Todo/AddItem', { title: newTitle }, function() {
  5. window.location = '/Todo';
  6. })
  7. .fail(function(data) {
  8. if (data && data.responseJSON) {
  9. var firstError = data.responseJSON[Object.keys(data.responseJSON)[0]];
  10. $('#add-item-error').text(firstError);
  11. $('#add-item-error').show();
  12. }
  13. });
  14. }

This function will send a POST request to http://localhost:5000/Todo/AddItem with the name the user typed. The third parameter passed to the $.post method (the function) is a success handler that will run if the server responds with 200 OK. The success handler function uses window.location to refresh the page (by setting the location to /Todo, the same page the browser is currently on). If the server responds with 400 Bad Request, the fail handler attached to the $.post method will try to pull out an error message and display it in a the <div> with id add-item-error.

Add an action

The above JavaScript code won’t work yet, because there isn’t any action that can handle the /Todo/AddItem route. If you try it now, ASP.NET Core will return a 404 Not Found error.

You’ll need to create a new action called AddItem on the TodoController:

  1. public async Task<IActionResult> AddItem(NewTodoItem newItem)
  2. {
  3. if (!ModelState.IsValid)
  4. {
  5. return BadRequest(ModelState);
  6. }
  7. var successful = await _todoItemService.AddItemAsync(newItem);
  8. if (!successful)
  9. {
  10. return BadRequest(new { error = "Could not add item" });
  11. }
  12. return Ok();
  13. }

The method signature defines a NewTodoItem parameter, which is a new model that doesn’t exist yet. You’ll need to create it:

Models/NewTodoItem.cs

  1. using System;
  2. using System.ComponentModel.DataAnnotations;
  3. namespace AspNetCoreTodo.Models
  4. {
  5. public class NewTodoItem
  6. {
  7. [Required]
  8. public string Title { get; set; }
  9. }
  10. }

This model definition (one property called Title) matches the data you’re sending to the action with jQuery:

  1. $.post('/Todo/AddItem', { title: newTitle } // ...
  2. // A JSON object with one property:
  3. // {
  4. // title: (whatever the user typed)
  5. // }

ASP.NET Core uses a process called model binding to match up the parameters submitted in the POST request to the model definition you created. If the parameter names match (ignoring things like case), the request data will be placed into the model.

After binding the request data to the model, ASP.NET Core also performs model validation. The [Required] attribute on the Title property informs the validator that the Title property should not be missing (blank). The validator won’t throw an error if the model fails validation, but the validation status will be saved so you can check it in the controller.

Sidebar: It would have been possible to reuse the TodoItem model instead of creating the NewTodoItem model, but TodoItem contains properties that will never be submitted by the user (ID and done). It’s cleaner to declare a new model that represents the exact set of properties that are relevant when adding a new item.

Back to the AddItem action method on the TodoController: the first block checks whether the model passed the model validation process. It’s customary to do this right at the beginning of the method:

  1. if (!ModelState.IsValid)
  2. {
  3. return BadRequest(ModelState);
  4. }

If the ModelState is invalid (because the required property is empty), the action will return 400 Bad Request along with the model state, which is automatically converted into an error message that tells the user what is wrong.

Next, the controller calls into the service layer to do the actual database operation:

  1. var successful = await _todoItemService.AddItemAsync(newItem);
  2. if (!successful)
  3. {
  4. return BadRequest(new { error = "Could not add item." });
  5. }

The AddItemAsync method will return true or false depending on whether the item was successfully added to the database. If it fails for some reason, the action will return 400 Bad Request along with an object that contains an error property.

Finally, if everything completed without errors, the action returns 200 OK.

Add a service method

If you’re using a code editor that understands C#, you’ll see red squiggely lines under AddItemAsync because the method doesn’t exist yet.

As a last step, you need to add a method to the service layer. First, add it to the interface definition in ITodoItemService:

  1. public interface ITodoItemService
  2. {
  3. Task<IEnumerable<TodoItem>> GetIncompleteItemsAsync();
  4. Task<bool> AddItemAsync(NewTodoItem newItem);
  5. }

Then, the actual implementation in TodoItemService:

  1. public async Task<bool> AddItemAsync(NewTodoItem newItem)
  2. {
  3. var entity = new TodoItem
  4. {
  5. Id = Guid.NewGuid(),
  6. IsDone = false,
  7. Title = newItem.Title,
  8. DueAt = DateTimeOffset.Now.AddDays(3)
  9. };
  10. _context.Items.Add(entity);
  11. var saveResult = await _context.SaveChangesAsync();
  12. return saveResult == 1;
  13. }

This method creates a new TodoItem (the model that represents the database entity) and copies the Title from the NewTodoItem model. Then, it adds it to the context and uses SaveChangesAsync to persist the entity in the database.

Sidebar: The above is just one way to build this functionality. If you want to display a separate page for adding a new item (for a complicated entity that contains a lot of properties, for example), you could create a new view that’s bound to the model you need the user to provide values for. ASP.NET Core can render a form automatically for the properties of the model using a feature called tag helpers. You can find examples in the ASP.NET Core documentation at https://docs.asp.net.

Try it out

Run the application and add some items to your to-do list with the form. Since the items are being stored in the database, they’ll still be there even after you stop and start the application again.

As a further challenge, try adding a date picker using HTML and JavaScript, and let the user choose an (optional) date for the DueAt property. Then, use that date instead of always making new tasks that are due in 3 days.