Hoàn thiện (1): nhập mới, cập nhật, partial class

    4

    Ở phần trước chúng ta đã xây dựng hoàn chỉnh tất cả các lớp hỗ trợ của chương trình. Trong bài này chúng ta sẽ áp dụng để hoàn thiện các chức năng hiện có: hiển thị (single, list), nhập, cập nhật.

    Thực hành 1: hoàn thiện chức năng nhập dữ liệu

    Trong các bài trước chúng ta đang làm dở chức năng nhập dữ liệu vì chưa thể truyền được thông tin người dùng nhập về controller. MVC quy định rằng các view không trực tiếp khởi tạo object. Khởi tạo object là nhiệm vụ của controller.

    Do vậy, lớp BookCreateView sau khi nhận thông tin từ người dùng phải chuyển thông tin đó về cho BookController để xử lý.

    Bước 1. Thay đổi nội dung của lớp BookCreateView

    using System;
    namespace BookMan.ConsoleApp.Views
    {
        using Framework;
        internal class BookCreateView : ViewBase
        {
            public BookCreateView() { }
            public override void Render()
            {
                ViewHelp.WriteLine("CREATE A NEW BOOK", ConsoleColor.Green);
                var title = ViewHelp.InputString("Title");
                var authors = ViewHelp.InputString("Authors");
                var publisher = ViewHelp.InputString("Publisher");
                var year = ViewHelp.InputInt("Year");
                var edition = ViewHelp.InputInt("Edition");
                var tags = ViewHelp.InputString("Tags");
                var description = ViewHelp.InputString("Description");
                var rate = ViewHelp.InputInt("Rate");
                var reading = ViewHelp.InputBool("Reading");
                var file = ViewHelp.InputString("File");
                var request =
                    "do create ? " +
                    $"title = {title}" +
                    $" & authors = {authors}" +
                    $" & publisher = {publisher}" +
                    $" & year = {year}" +
                    $" & edition = {edition}" +
                    $" & tags = {tags}" +
                    $" & description = {description}" +
                    $" & rate = {rate}" +
                    $" & reading = {reading}" +
                    $" & file = {file}";
                Router.Forward(request);
            }
        }
    }

    Ở bước này chúng ta tạo ra một truy vấn với route là “do create” và danh sách tham số chứa tất cả giá trị mà người dùng nhập. Truy vấn này sẽ được router gửi ngược về controller.

    Bước 2. Thay đổi phương thức Create của BookController

    public void Create(Book book = null)
    {
        if (book == null)
        {
            Render(new BookCreateView());
            return;
        }
        Repository.Insert(book);
        Success("Book created!");
    }
    

    Ở bước này chúng ta thay đổi phương thức Create để có thể thực hiện hai nhiệm vụ:

    1. nếu tham số book nhận giá trị null thì sẽ kích hoạt BookCreateView để người dùng nhập thông tin;
    2. nếu book khác null thì yêu cầu Repository thêm dữ liệu và hiển thị thông báo thành công.

    Lưu ý bổ sung using Models; ở ngay sau khai báo namespace.

    namespace BookMan.ConsoleApp.Controllers
    {
        using Models;
        using DataServices;
        using Framework;
        using Views;
    ...

    Bước 3. Điều chỉnh phương thức Main

    private static void Main(string[] args)
    {
        Console.OutputEncoding = System.Text.Encoding.UTF8;
        
        SimpleDataAccess context = new SimpleDataAccess();
        BookController controller = new BookController(context);    
        Router r = Router.Instance;
        r.Register("about", About);
        r.Register("help", Help);
        r.Register(route: "create",
            action: p => controller.Create(),
            help: "[create]\r\nnhập sách mới");
        r.Register(route: "do create",
            action: p => controller.Create(toBook(p)),
            help: "this route should be used only in code");
        r.Register(route: "update",
            action: p => controller.Update(p["id"].ToInt()),
            help: "[update ? id = <value>]\r\ntìm và cập nhật sách");
        r.Register(route: "list",
            action: p => controller.List(),
            help: "[list]\r\nhiển thị tất cả sách");
        r.Register(route: "single",
            action: p => controller.Single(p["id"].ToInt()),
            help: "[single ? id = < value >]\r\nhiển thị một cuốn sách theo id");
        r.Register(route: "list file",
            action: p => controller.List(p["path"]),
            help: "[list file ? path = <value>]\r\nhiển thị tất cả sách");
        r.Register(route: "single file",
            action: p => controller.Single(p["id"].ToInt(), p["path"]),
            help: "[single file ? id = <value> & path = <value>]");
        //hàm cục bộ để chuyển object của Parameter sang object của Book
        Models.Book toBook(Parameter p)
        {
            var b = new Models.Book();
            if (p.ContainsKey("id")) b.Id = p["id"].ToInt();
            if (p.ContainsKey("authors")) b.Authors = p["authors"];
            if (p.ContainsKey("title")) b.Title = p["title"];
            if (p.ContainsKey("publisher")) b.Publisher = p["publisher"];
            if (p.ContainsKey("year")) b.Year = p["year"].ToInt();
            if (p.ContainsKey("edition")) b.Edition = p["edition"].ToInt();
            if (p.ContainsKey("isbn")) b.Isbn = p["isbn"];
            if (p.ContainsKey("tags")) b.Tags = p["tags"];
            if (p.ContainsKey("description")) b.Description = p["description"];
            if (p.ContainsKey("file")) b.File = p["file"];
            if (p.ContainsKey("rate")) b.Rating = p["rate"].ToInt();
            if (p.ContainsKey("reading")) b.Reading = p["reading"].ToBool();
            return b;
        }
        while (true)
        {
            ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
            string request = Console.ReadLine();
            Router.Instance.Forward(request);
            
            Console.WriteLine();
        }
    }
    

    Ở bước này chúng ta thêm một route nữa để gọi phương thức Create của BookController.

    r.Register(route: "do create",
          action: p => controller.Create(toBook(p)),
          help: "this route should be used only in code");
    

    Để đơn giản hóa việc chuyển object của Parameter sang object của Book, chúng ta viết thêm một hàm cục bộ toBook ở ngay trong thân của Main.

    Bước 4. Dịch và chạy thử chương trình với chức năng thêm dữ liệu

    Kết quả chạy chương trình với lệnh create
    Kết quả chạy chương trình với lệnh create

    Thực hành 2: cải tiến lớp Program với partial class

    Trong phần thực hành 1 chúng ta nhận thấy rằng phương thức Main đang mở rộng ra rất nhanh khi chúng ta bổ sung thêm các route mới để hoàn thiện các chức năng xử lý dữ liệu.

    Trong các phần thực hành tiếp theo, số lượng code ở phương thức này còn tăng lên nữa. Code được bổ sung đều là code để đăng ký route cho các chức năng mới. Chúng ta sẽ tìm cách tách rời phần đăng ký route này sang một file riêng để dễ dàng thay đổi về sau.

    Ở đây chúng ta áp dụng một giải pháp đơn giản: sử dụng partial class.

    Partial class là một tính năng của C# cho phép định nghĩa một class trên nhiều file vật lý khác nhau.
    Từ đầu đến giờ, mỗi class đều được xây dựng trong một file đặt trùng tên với class. Nếu trong class có nhiều logic khác nhau, chúng ta hoàn toàn có thể xem xét tách các logic đó sang các file riêng rẽ sử dụng partial class.
    Nếu trong một class có những thành phần code thường biến động theo quá trình phát triển và có những thành phần ổn định thì cũng có thể xem xét tách chúng ra các file khác nhau để dễ quản lý. Trong đó, thành phần biến động ra một file riêng, thành phần ổn định để lại một file riêng.

    Bước 1. Thêm mới file Program.Config.cs

    1. Click phải vào project BookMan.ConsoleApp, chọn Add => New Item;
    2. Trong cửa sổ Add New Item, chọn nhánh Code bên trong Visual C# Items (bên tay trái);
    3. Chọn mục Code File trong danh sách bên tay phải
    Tạo code file mới
    Tạo code file mới

    Đặt tên file là Program.Config.cs và ấn nút Add.

    Bước 2. Viết code cho file Program.Config.cs

    namespace BookMan.ConsoleApp
    {
        using Models;
        using Controllers;
        using DataServices;
        using Framework;
        internal partial class Program
        {
            private static void ConfigRouter()
            {
                SimpleDataAccess context = new SimpleDataAccess();
                BookController controller = new BookController(context);
                
                Router r = Router.Instance;
                r.Register("about", About);
                r.Register("help", Help);
                r.Register(route: "create",
                    action: p => controller.Create(),
                    help: "[create]rnnhập sách mới");
                r.Register(route: "do create",
                    action: p => controller.Create(toBook(p)),
                    help: "this route should be used only in code");
                
                r.Register(route: "list",
                    action: p => controller.List(),
                    help: "[list]rnhiển thị tất cả sách");
                r.Register(route: "list file",
                    action: p => controller.List(p["path"]),
                    help: "[list file ? path = <value>]rnhiển thị tất cả sách");
                
                r.Register(route: "single",
                    action: p => controller.Single(p["id"].ToInt()),
                    help: "[single ? id = < value >]rnhiển thị một cuốn sách theo id");
                r.Register(route: "single file",
                    action: p => controller.Single(p["id"].ToInt(), p["path"]),
                    help: "[single file ? id = <value> & path = <value>]");
                
                //r.Register(route: "",
                //    action: null,
                //    help: "");            
                #region helper
                //local function to convert parameter to book object
                Book toBook(Parameter p)
                {
                    var b = new Book();
                    if (p.ContainsKey("id")) b.Id = p["id"].ToInt();
                    if (p.ContainsKey("authors")) b.Authors = p["authors"];
                    if (p.ContainsKey("title")) b.Title = p["title"];
                    if (p.ContainsKey("publisher")) b.Publisher = p["publisher"];
                    if (p.ContainsKey("year")) b.Year = p["year"].ToInt();
                    if (p.ContainsKey("edition")) b.Edition = p["edition"].ToInt();
                    if (p.ContainsKey("isbn")) b.Isbn = p["isbn"];
                    if (p.ContainsKey("tags")) b.Tags = p["tags"];
                    if (p.ContainsKey("description")) b.Description = p["description"];
                    if (p.ContainsKey("file")) b.File = p["file"];
                    if (p.ContainsKey("rate")) b.Rating = p["rate"].ToInt();
                    if (p.ContainsKey("reading")) b.Reading = p["reading"].ToBool();
                    return b;
                }
                #endregion
            }
        }
    }
    

    Ở bước này chúng ta khai báo thêm một class Program nữa và nằm trong cùng không gian tên với class Program đã có sẵn trong file Program.cs.

    Chúng ta thêm từ khóa partial vào trước định nghĩa của lớp Program. Nếu không có từ khóa partial ở đây thì C# sẽ báo lỗi vì có hai class trùng tên nhau trong cùng một không gian tên.

    Trong phần này của lớp Program này định nghĩa thêm phương thức static ConfigRouter và di chuyển một phần của phương thức Main sang phương thức mới ConfigRouter.

    Bước 3. Điều chỉnh file Program.cs

    using System;
    namespace BookMan.ConsoleApp
    {
        using Framework;
        internal partial class Program
        {
            private static void Main(string[] args)
            {
                Console.OutputEncoding = System.Text.Encoding.UTF8;
                ConfigRouter();            
                while (true)
                {
                    ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
                    string request = Console.ReadLine();
                    Router.Instance.Forward(request);
                    
                    Console.WriteLine();
                }
            }
            private static void Help(Parameter parameter)
            {
                if (parameter == null)
                {
                    ViewHelp.WriteLine("SUPPORTED COMMANDS:", ConsoleColor.Green);
                    ViewHelp.WriteLine(Router.Instance.GetRoutes(), ConsoleColor.Yellow);
                    ViewHelp.WriteLine("type: help ? cmd= <command> to get command details", ConsoleColor.Cyan);
                    return;
                }
                Console.BackgroundColor = ConsoleColor.DarkBlue;
                var command = parameter["cmd"].ToLower();
                ViewHelp.WriteLine(Router.Instance.GetHelp(command));
            }
            private static void About(Parameter parameter)
            {
                ViewHelp.WriteLine("BOOK MANAGER version 1.0", ConsoleColor.Green);
                ViewHelp.WriteLine("by Mype Nguyen @ ictu.edu.vn", ConsoleColor.Magenta);
            }
        }
    }
    

    Ở bước này chúng ta thêm từ khóa partial vào trước định nghĩa của class Program, đồng thời gọi phương thức ConfigRouter đã định nghĩa ở phần khác của lớp Program trong file Program.Config.cs.

    Đây có thể xem như thành phẫn “tĩnh” hơn của Program. Toàn bộ những code khác của Main đã di chuyển sang phương thức ConfigRouter (thành phần “động” của Program).

    Partial class đặc biệt hữu ích khi sử dụng chung với các công cụ sinh code tự động. Phần class sinh ra tự động thường được định nghĩa sẵn với từ khóa partial.
    Người dùng có thể tự định nghĩa phần của riêng mình mà không ảnh hưởng đến phần sinh tự động. Khi phần sinh tự động thay đổi, phần do người dùng định nghĩa không bị ảnh hưởng.
    Khi học đến công nghệ windows forms chúng ta sẽ thường xuyên gặp partial class.

    Như vậy, qua phần thực hành vừa rồi chúng ta đã điều chỉnh lớp Program thành dạng partial class nằm trên hai file: Program.cs và Program.Config.cs. Mặc dù nằm ở hai file khác nhau nhưng lại là cùng một class. File Program.cs chứa thành phần ít biến đổi (thành phần tĩnh hơn); file Program.Config.cs chứa thành phần thường biến động (các route được liên tục bổ sung khi phát triển các chức năng của ứng dụng).

    Thực hành 3: hoàn thiện chức năng cập nhật dữ liệu

    Tương tự như chức năng thêm dữ liệu, chức năng cập nhật dữ liệu trước đây cũng không truyền được thông tin về controller. Trong phần thực hành này chúng ta sẽ xây dựng hoàn thiện.

    Bước 1. Điều chỉnh lớp BookUpdateView

    using System;
    namespace BookMan.ConsoleApp.Views
    {
        using Framework;
        using Models;
        internal class BookUpdateView : ViewBase<Book>
        {
            public BookUpdateView(Book model) : base(model)
            {
            }
            public override void Render()
            {
                ViewHelp.WriteLine("UPDATE BOOK INFORMATION", ConsoleColor.Green);
                var authors = ViewHelp.InputString("Authors", Model.Authors);
                var title = ViewHelp.InputString("Title", Model.Title);
                var publisher = ViewHelp.InputString("Publisher", Model.Publisher);
                var isbn = ViewHelp.InputString("Isbn", Model.Isbn);
                var tags = ViewHelp.InputString("Tags", Model.Tags);
                var description = ViewHelp.InputString("Description", Model.Description);
                var file = ViewHelp.InputString("File", Model.File);
                var year = ViewHelp.InputInt("Year", Model.Year);
                var edition = ViewHelp.InputInt("Edition", Model.Edition);
                var rating = ViewHelp.InputInt("Rate", Model.Rating);
                var reading = ViewHelp.InputBool("Reading", Model.Reading);
                var request =
                    "do update ? " +
                    $"id = {Model.Id}" +
                    $"& title = {title}" +
                    $"& authors = {authors}" +
                    $"& publisher = {publisher}" +
                    $"& year = {year} &" +
                    $"& edition = {edition}" +
                    $"& tags = {tags}" +
                    $"& description = {description}" +
                    $"& rate = {rating}" +
                    $"& reading = {reading}" +
                    $"& file = {file}";
                Router.Forward(request);
            }
        }
    }
    

    Bước điều chỉnh này tương tự như đối với lớp BookCreateView: tạo và gửi truy vấn về controller.

    Bước 2. Điều chỉnh phương thức Update của BookController

    public void Update(int id, Book book = null)
    {
        if (book == null)
        {
            var model = Repository.Select(id);
            var view = new BookUpdateView(model);
            Render(view);
            return;
        }
        Repository.Update(id, book);
        Success("Book updated!");
    }
    

    Tương tự với việc điều chỉnh phương thức Create, bước điều chỉnh này giúp phương thức Update làm việc với hai giai đoạn của việc cập nhật:

    1. nếu chỉ cung cấp id mà không cung cấp một object của lớp Book thì chỉ kích hoạt giao diện BookUpdateView;
    2. nếu cung cấp cả một object của lớp Book thì sẽ thực sự cập nhật thông tin sách và thông báo kết quả lại cho người dùng.

    Bước 3. Bổ sung route vào ConfigRouter

    r.Register(route: "update",
        action: p => controller.Update(p["id"].ToInt()),
        help: "[update ? id = <value>]rntìm và cập nhật sách");
    r.Register(route: "do update",
        action: p => controller.Update(p["id"].ToInt(), toBook(p)),
        help: "this route should be used only in code");
    

    Bước 4. Dịch và chạy thử chương trình với lệnh update

    Kết quả thực hiện lệnh update
    Kết quả thực hiện lệnh update

    Kết luận

    Trong bài học này chúng ta đã vận dụng các lớp hỗ trợ xây dựng trong bài trước để hoàn thành hai chức năng chính của ứng dụng: Thêm mới và cập nhật thông tin sách. Chúng ta cũng vận dụng partial class để tách nội dung lớp Program ra hai file vật lý khác nhau.

    Trong bài tiếp theo chúng ta sẽ tiếp tục hoàn thiện và bổ sung một số chức năng theo phân tích.

    + Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
    + Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
    + Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
    Cảm ơn bạn!

    Subscribe
    Notify of
    guest
    4 Thảo luận
    Oldest
    Newest
    Inline Feedbacks
    View all comments
    Võ Văn Thương

    Có phải vì mục tiêu bài này admin hướng đến thực hiện trình bày các kỹ thuật Route nên phần create và update mới thực hiện gửi qua Route phải không? Nếu mình gửi dữ liệu từ view về controller thông qua Model thì vẫn hoàn toàn được phải không nhỉ? Ví dụ Ở create có thể: – Tại view: Model = new_book. – Tại controler: Repository.Insert(vew.Model) Ở Update thì vì object là biến tham chiếu nên truyền objetc Book vào cho Model của BookUpdateView và trong BookUpdateView gán trực tiếp giá trị mới cho các thông tin luôn thì… Read more »

    Trọng Nhân

    Không phải đâu bạn. Route dữ liệu từ view về controller là để tuân thủ theo mô hình MVC. Trong mô hình này thì trong view không thực hiện bất kỳ logic nào liên quan đến xử lý dữ liệu (như tạo dữ liệu mới). Việc xử lý này phải thực hiện ở controller.
    Còn về cách viết code để chương trình chạy được thì có rất nhiều kiểu.
    Sau này khi bạn học tiếp Asp.net mvc hoặc asp.net core mvc thì sẽ thấy tư tưởng giống hệt như thế này.

    Tuấn

    Cho mình hỏi là ở bước 3 mình viết hàm toBook thì bị báo lỗi như sau là bị sao vậy?
    Models.Book toBook(Parameter p)
    {}
    Lỗi ở Parameter p. Cụ thể
    Expected ; or = (cannot specify constructor arguments in declaration)
    ‘Parameter’ is a type, which is not valid in the given context

    Tuấn

    Mình đã tìm ra lỗi, VS mình sử dụng là 2015 chưa hỗ trợ hàm cục bộ. Từ C# 7.0 mới hỗ trợ.