Complete items with a checkbox

Adding items to your to-do list is great, but eventually you’ll need to get things done, too. In the Views/Todo/Index.cshtml view, a checkbox is rendered for each to-do item:

  1. <input type="checkbox" name="@item.Id" value="true" class="done-checkbox">

The item’s ID (a guid) is saved in the name attribute of the element. You can use this ID to tell your ASP.NET Core code to update that entity in the database when the checkbox is checked.

This is what the whole flow will look like:

  • The user checks the box, which triggers a JavaScript function
  • JavaScript is used to make an API call to an action on the controller
  • The action calls into the service layer to update the item in the database
  • A response is sent back to the JavaScript function to indicate the update was successful
  • The HTML on the page is updated

Add JavaScript code

First, open site.js and add this code to the $(document).ready block:

wwwroot/js/site.js

  1. $(document).ready(function() {
  2. // ...
  3. // Wire up all of the checkboxes to run markCompleted()
  4. $('.done-checkbox').on('click', function(e) {
  5. markCompleted(e.target);
  6. });
  7. });

Then, add the markCompleted function at the bottom of the file:

  1. function markCompleted(checkbox) {
  2. checkbox.disabled = true;
  3. $.post('/Todo/MarkDone', { id: checkbox.name }, function() {
  4. var row = checkbox.parentElement.parentElement;
  5. $(row).addClass('done');
  6. });
  7. }

This code uses jQuery to send an HTTP POST request to http://localhost:5000/Todo/MarkDone. Included in the request will be one parameter, id, containing the item’s ID (pulled from the name attribute).

If you open the Network Tools in your web browser and click on a checkbox, you’ll see a request like:

  1. POST http://localhost:5000/Todo/MarkDone
  2. Content-Type: application/x-www-form-urlencoded
  3. id=<some guid>

The success handler function passed to $.post uses jQuery to add a class to the table row that the checkbox sits in. With the row marked with the done class, a CSS rule in the page stylesheet will change the way the row looks.

Add an action to the controller

As you’ve probably guessed, you need to add a MarkDone action on the TodoController:

  1. public async Task<IActionResult> MarkDone(Guid id)
  2. {
  3. if (id == Guid.Empty) return BadRequest();
  4. var successful = await _todoItemService.MarkDoneAsync(id);
  5. if (!successful) return BadRequest();
  6. return Ok();
  7. }

Let’s step through each piece of this action method. First, the method accepts a Guid parameter called id in the method signature. Unlike the AddItem action, which used a model (the NewTodoItem model) and model binding/validation, the id parameter is very simple. If the incoming request includes a parameter called id, ASP.NET Core will try to parse it as a guid.

There’s no ModelState to check for validity, but you can still check to make sure the guid was valid. If for some reason the id parameter in the request was missing couldn’t be parsed as a guid, it will have a value of Guid.Empty. If that’s the case, the action can return early:

  1. if (id == Guid.Empty) return BadRequest();

The BadRequest() method is a helper method that simply returns the HTTP status code 400 Bad Request.

Next, the controller needs to call down into the service to update the database. This will be handled by a new method called MarkDoneAsync on the ITodoItemService, which will return true or false depending on if the update succeeded:

  1. var successful = await _todoItemService.MarkDoneAsync(id);
  2. if (!successful) return BadRequest();

Finally, if everything looks good, the Ok() method is used to return status code 200 OK. More complex APIs might return JSON or other data as well, but for now returning a status code is all you need.

Add a service method

First, add MarkDoneAsync to the interface definition:

Services/ITodoItemService.cs

  1. Task<bool> MarkDoneAsync(Guid id);

Then, add the concrete implementation to the TodoItemService:

Services/TodoItemService.cs

  1. public async Task<bool> MarkDoneAsync(Guid id)
  2. {
  3. var item = await _context.Items
  4. .Where(x => x.Id == id)
  5. .SingleOrDefaultAsync();
  6. if (item == null) return false;
  7. item.IsDone = true;
  8. var saveResult = await _context.SaveChangesAsync();
  9. return saveResult == 1; // One entity should have been updated
  10. }

This method uses Entity Framework Core and Where to find an entity by ID in the database. The SingleOrDefaultAsync method will return either the item (if it exists) or null if the ID was bogus. If it didn’t exist, the code can return early.

Once you’re sure that item isn’t null, it’s a simple matter of setting the IsDone property:

  1. item.IsDone = true;

Changing the property only affects the local copy of the item until SaveChangesAsync is called to persist your changes back to the database. SaveChangesAsync returns an integer that reflects how many entities were updated during the save operation. In this case, it’ll either be 1 (the item was updated) or 0 (something went wrong).

Try it out

Run the application and try checking some items off the list. Refresh the page and they’ll disappear completely, because of the Where filter in the GetIncompleteItemsAsync method.

Right now, the application contains a single, shared to-do list. It’d be even more useful if it kept track of individual to-do lists for each user. In the next chapter, you’ll use ASP.NET Core Identity to add security and authentication features to the project.