Todo App ASP.NET MVC x Entity Framework

1996

Bài viết được sự cho phép của tác giả Khiêm Lê

Giới thiệu

Hôm nay mình sẽ chia sẻ đến các bạn về cách thực hiện Todo Web App đơn giản với ASP.NET MVC và Entity Framework. Mình không chuyên .NET hay web, nhưng mình có học qua rồi nên viết cho vui để sau này biết đâu cần lại quên.

Todo của mình sẽ có các thông tin như Name, Content, DateAdded, IsDone và UserId. UserId để xác định todo đó của user nào. App sẽ có các thao tác cơ bản như xem toàn bộ todo, xem chi tiết 1 todo, thêm, sửa, xóa todo.

Tuyển dụng ASP.NET lương cao không yêu cầu kinh nghiệm.

  Giới thiệu Web service – SOAP, WSDL và ASP.NET Web Service cơ bản

Tạo project

Mình sẽ tạo một project ASP.NET MVC, .NET Framework hiện tại là 4.7.2, chọn Authentication là Individual User Accounts.

Sau khi tạo project xong, các model và controller quản lý việc đăng nhập đã được tạo sẵn, mình sẽ không mất công tạo lại nữa, mình cũng chỉ cần đăng nhập thôi nên không cần thêm sửa gì.

Các trang như About, Privacy mình không dùng đến nên xóa nó đi (nhớ xóa cả action của nó trong HomeController.cs), xóa luôn các link trên thanh nabbar trong ~/Views/Shared/_Layout.cshtml.

Enable migraitons

Mình sẽ sử dụng Code-First workflow để tạo database, đỡ phải viết query dài dòng. Do lúc tạo project, mình đã chọn Authentication rồi nên nó sẽ tự tạo cho mình các cái table cần thiết để quản lý user. Vậy nên, mình sẽ enable migrations rồi add migration authentication trước.

Enable-Migrations
Add-Migration InitialAuthenticationModel
Update-Database

Lúc này bạn có thể Show All Files và mở file .mdf trong folder App_Data, bạn sẽ thấy các table đã được tạo.

Authentication

Người dùng sẽ phải đăng nhâp để thao tác với todo vì mỗi người dùng có todo khác nhau. Mình sẽ quay lại HomeController và thêm annotation Authorize:

[Authorize]
public class HomeController : Controller
{
    // Code...
}

Lúc này khi người dùng vào trang mà chưa đăng nhập thì sẽ bị chuyển hướng đến trang đăng nhập.

Add Todo Model

Tiếp theo mình sẽ tạo Todo model với các thông tin mình đã nêu. Do Id của user là string(128) nên mình sẽ để thuộc tính UserId của Todo là string luôn.

namespace TodoWebApp.Models
{
    [Table("TodoTable")]
    public class Todo
    {
        public int Id { get; set; }

        [Required]
        [MaxLength(256)]
        public string Name { get; set; }

        [Required]
        [MaxLength(1024)]
        public string Content { get; set; }

        [Required]
        [Display(Name = "Have you done it?")]
        public bool IsDone { get; set; } = false;

        [Required]
        public DateTime DateAdded { get; set; } = DateTime.Now;

        [Required]
        [MaxLength(128)]
        public string UserId { get; set; }
    }
}

Sau khi đã có model rồi thì mình tiến hành thêm nó vào ApplicationDBContext. Mở file IdentityModels.cs và thêm vào thuộc tính todo:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public DbSet<Todo> Todo { get; set; } // Add this line
    // Code goes here...
}

Sau đó tiếp tục add migration:

Add-Migration AddTodoModel

Sau khi add migraiton, mình sẽ chỉnh sửa lại một chút. Trong khi create table, mình sẽ để mặc định cho IsDone là false và DateAdded là lấy ngày tháng hiện tại, mình sẽ sửa lại như sau:

IsDone = c.Boolean(nullable: false, defaultValue: false),
DateAdded = c.DateTime(nullable: false, defaultValueSql: "GETDATE()"),

Sau đó updata database:

Update-Database

Show Todo

Mình sẽ bắt đầu với controller trước, để lấy được data từ database, mình sẽ cần có DbContext. Mình sẽ thêm thuộc tính private vào HomeController:

private readonly ApplicationDbContext _context = new ApplicationDbContext();

Tiếp theo, trong action Index, mình cần lấy ra toàn bộ todo của user đã login, mình sẽ lấy UserId sau đó tìm todo có UserId đó trong database và return về View kèm theo ViewModel là một todo list:

public ActionResult Index()
{
    var uid = User.Identity.GetUserId();
    var todo = _context.Todo.Where(t => t.UserId == uid);
    if (todo == null)
    throw new HttpException(404, "Not found");
    return View(todo);
}

Sau đó, trong Index.cshtml, mình cho Model của nó là một IQueryable<Todo> (tương tự như list vậy) sau đó foreach và đổ data ra table, sử dụng bootstrap.

@model IQueryable<TodoWebApp.Models.Todo>

@{
    ViewBag.Title = "Home Page";
}

<h1 class="page-header">All Your Todo</h1>

<div>
    <table class="table table-bordered table-hover">
        <thead>
        <tr>
            <th>Name</th>
            <th>Done</th>
            <th>Edit</th>
            <th>Delete</th>
        </tr>
        </thead>
        <tbody>
        @foreach (var todo in Model)
        {
            <tr>
                <td>@todo.Name</td>
                <td>
                    @if (todo.IsDone)
                    {
                        <p>Done</p>
                    }
                </td>
                <td>Edit</td>   // các action này sẽ được thay thế sau
                <td>Delete</td> // các action này sẽ được thay thế sau
            </tr>
        }
        </tbody>
    </table>
</div>

Do mình chưa có data nên nó sẽ không có gì cả. Các bạn có thể test bằng cách bỏ Where() sau _context.Todo đi sau đó thêm data thủ công vào database và refresh lại trang.

View Detail

Tiếp tục, tạo action Detail và view Detail để xem thông tin chi tiết của todo. Trong action Detail sẽ nhận vào một id của todo, trong action này sẽ lấy thêm UserId để lấy được chính xác todo của user cần xem và trả về view cho người dùng.

public ActionResult Detail(int id)
{
    var uid = User.Identity.GetUserId();
    var todo = _context.Todo.SingleOrDefault(t => t.Id == id && t.UserId == uid);
    if (todo == null)
         throw new HttpException(404, "Not found");
    return View(todo);
}

Phần view thì đơn giản mình sẽ cho hiển thị thêm thông tin:

@model TodoWebApp.Models.Todo
@{
    ViewBag.Title = "Detail";
}

<div class="jumbotron">
    <h1>@Model.Name</h1>
    <p>@Model.Content</p>
    <p>Added on @Model.DateAdded.ToLongDateString()</p>
    @if (Model.IsDone)
    {
        <p>You have done this task!</p>
    }
</div>

Sau đó để xem thông tin, mình sẽ cho một đường dẫn đến action Detail kèm theo id của todo. Mình sẽ sửa lại Index.cshtml:

// Thay
<td>@todo.Name</td>

// Thành
<td>@Html.ActionLink(@todo.Name, "Detail", new { id = todo.Id })</td>

Add Todo

Để add todo, mình cần 2 action, 1 là để điều hướng người dùng đến form, 2 là để thực hiện add todo vào trong database:

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

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Add(Todo todo)
{
    if (!ModelState.IsValid) return View(todo);
    todo.UserId = User.Identity.GetUserId();
    _context.Todo.Add(todo);
    _context.SaveChanges();
    return RedirectToAction("Index");
}

Action Add khi thêm todo thì sẽ gọi action Add có phương thức POST và truyền model vào. Mình sẽ build form Add như sau:

@model TodoWebApp.Models.Todo
@{
    ViewBag.Title = "Add todo";
}

<h2 class="page-header">Add Todo</h2>

@using (Html.BeginForm("Add", "Home", null, FormMethod.Post, new { @class = "form-group" }))
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()

    @Html.LabelFor(t => t.Name)
    @Html.TextBoxFor(t => t.Name, new { @class = "form-control" })
    @Html.ValidationMessageFor(t => t.Name)
    <br />

    @Html.LabelFor(t => t.Content)
    @Html.TextBoxFor(t => t.Content, new { @class = "form-control" })
    @Html.ValidationMessageFor(t => t.Content)
    <br />

    <div class="checkbox">
        <label>
            @Html.CheckBoxFor(t => t.IsDone)
            @Html.LabelFor(t => t.IsDone)
        </label>
    </div>

    @Html.HiddenFor(t => t.UserId, new { @Value = "default_user_id" })

    <input type="submit" class="btn btn-primary" value="Submit" />
}

Mình có để HiddenFor là để tránh việc validate bị lỗi do trường UserId bị null do mình có để Data Annotation là Required trong model Todo.

Edit Todo

Sẵn làm form add todo thì làm luôn form edit todo vì chúng khá giống nhau:

public ActionResult Edit(int id)
{
    var uid = User.Identity.GetUserId();
    var todo = _context.Todo.SingleOrDefault(t => t.Id == id && t.UserId == uid);
    if (todo == null)
        throw new HttpException(404, "Not found");
    return View(todo);
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Todo todo)
{
    if (!ModelState.IsValid) return View(todo);
    var uid = User.Identity.GetUserId();
    var todoInDb = _context.Todo.SingleOrDefault(t => t.Id == todo.Id && t.UserId == uid);
    todoInDb.Name = todo.Name;
    todoInDb.Content = todo.Content;
    todoInDb.IsDone = todo.IsDone;
    _context.SaveChanges();
    return RedirectToAction("Index");
}

Và form edit thì cũng tương tự, mình chỉ sửa action của BeginForm() thành “Edit” thôi. Tiếp theo mình cần dẫn người dùng từ index đến chỉnh sửa todo và truyền kèm id của todo.

// Thay
<td>Edit</td>

// Thành
<td>@Html.ActionLink("Edit", "Edit", new { @id = todo.Id })</td>

Delete Todo

Phần Delete cũng khá đơn giản, mình chỉ cần truyền Id của todo được nhấn vào action và tìm xem có không, có thì delete đi.

public ActionResult Delete(int id)
{
    var uid = User.Identity.GetUserId();
    var todo = _context.Todo.SingleOrDefault(t => t.Id == id && t.UserId == uid);
    if (todo == null)
        throw new HttpException(404, "Not found");
        _context.Todo.Remove(todo);
        _context.SaveChanges();
        return RedirectToAction("Index");
}

Và sửa Index như sau:

// Thay
<td>Delete</td>

// Thành
<td>@Html.ActionLink("Delete", "Delete", new { @id = todo.Id })</td>

Tổng kết

Như vậy là mình đã có thể tạo được một App Todo đơn giản rồi. Mình chỉ làm project nhỏ này cho vui thôi, chưa tối ưu, chưa sử dụng các kỹ thuật dùng ajax để gọi API, client-side render… Các bạn có thể tự tạo API rồi dùng ajax gọi nó để giảm tải cho server và nó cũng có thể dễ dàng upscale.

Hi vọng là các bạn thích bài viết của mình. Nếu các bạn thấy bài viết có ích, đừng quên chia sẻ cho bạn bè cùng biết nha. Cảm ơn các bạn.

Theo dõi tin tuyển dụng lập trình viên .NET mới nhất tại đây

Source code trên github

Bài viết gốc được đăng tải tại khiemle.dev