Hoàn thiện dự án: exception, try-catch, Settings

    1

    Trong loạt bài từ đầu đến giờ, chúng ta đã lần lượt hoàn thiện tất cả chức năng của ứng dụng theo phân tích. Tuy nhiên, trước khi đưa ứng dụng đến được người dùng cuối, chúng ta cần bổ sung thêm một số tính năng, vốn không liên quan trực tiếp đến việc phân tích nghiệp vụ.

    Các chức năng mới này mang tính kỹ thuật hơn là nghiệp vụ, bao gồm bắt và xử lý ngoại lệ (Exception, giúp ứng dụng ổn định hơn), sử dụng file cấu hình (Settings, giúp người dùng thay đổi cấu hình của ứng dụng).

    Xử lý ngoại lệ (Exception Handling)

    Chúng ta đã nhắc đến khái niệm ngoại lệ (exception) và xem xét cách thức đơn giản nhất để phát thông báo ngoại lệ bằng lệnh throw và lớp Exception. Exception là một cơ chế rất mạnh trong .NET giúp phát hiện lỗi logic trong chương trình ở giai đoạn Runtime.

    Ngoại lệ và chế độ Debug

    Khi chạy chương trình ở chế độ debug, nếu phát sinh ngoại lệ, Visual Studio sẽ mở file mã nguồn ở đúng vị trí lỗi cùng với thông báo cụ thể. Qua đó, chúng ta có thể xác định nguồn gốc của lỗi và đưa ra cách giải quyết.

    Hình dưới đây minh họa tình huống lỗi khi người dùng nhập vào một lệnh chưa tồn tại.

    Ngoại lệ ở chế độ chạy debug
    Ngoại lệ ở chế độ chạy debug

    Đây là cơ chế bắt và xử lý lỗi ở chế độ Debug. Chương trình chúng ta viết từ đầu dự án đến giờ đều dịch và chạy ở chế độ Debug.

    Xử lý ngoại lệ ở chế độ Release

    Một chương trình trước khi đem triển khai cho người dùng cuối phải được dịch ở chế độ Release. Chương trình được dịch ở chế độ này sẽ không chạy được ở chế độ Debug nữa.

    Nếu chương trình chạy ở chế độ Release mà gặp lỗi, thông báo lỗi sẽ được hiển thị như dưới đây.

    Đây là cơ chế bắt và xử lý lỗi mặc định của .NET framework đối với ứng dụng console.

    Như chúng ta thấy, cơ chế bắt và xử lý lỗi mặc định của .NET framework tương đối không thân thiện với người dùng.

    .NET cũng cung cấp cho các chương trình tính năng bắt và xử lý ngoại lệ (Exception Handling) để tự mình xác định hoạt động của chương trình khi xảy ra lỗi (ngoại lệ), tránh phải sử dụng cơ chế bắt và xử lý lỗi mặc định.

    Thực hành 1: bổ sung chức năng bắt và xử lý lỗi

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

    private static void Main(string[] args)
    {
        Console.OutputEncoding = System.Text.Encoding.UTF8;
    
        ConfigRouter();
    
        while (true)
        {
            ViewHelp.Write("# Request >>> ", ConsoleColor.Green);
            string request = Console.ReadLine();
    
            try
            {
                Router.Instance.Forward(request);
            }
            catch (Exception e)
            {
                ViewHelp.WriteLine(e.Message, ConsoleColor.Red);
            }
            finally
            {
                Console.WriteLine();
            }
        }
    }

    Khối try được đặt để kiểm soát “cửa ngõ” của chương trình: lời gọi phương thức Forward. Lời gọi phương thức này là khởi đầu của bất kỳ hoạt động nào của ứng dụng khi người dùng nhập truy vấn.

    Khối catch được viết để bắt tất cả các loại lỗi (vì bắt lỗi thuộc loại Exception) và đưa vào biến e. Thông báo lỗi cụ thể được truy xuất qua thuộc tính Message của object Exception và viết ra mới màu đỏ.

    Trong bất kỳ tình huống nào (dù thực hiện lệnh Forward có lỗi hay không) sẽ luôn in thêm một dòng trống sau khi thực hiện (khối finally).

    Bước 2. Dịch và chạy chương trình với các lệnh lỗi

    Chạy chương trình với các lệnh lỗi
    Chạy chương trình với các lệnh lỗi

    Chúng ta có thể thấy cơ chế bắt lỗi mặc định của .NET đã không còn kích hoạt nữa. Thay vào đó, cơ chế bắt lỗi riêng của chương trình đã hoạt động. Các thông báo cũng đơn giản nhẹ nhàng hơn. Có những thông báo xuất phát từ lỗi do chúng ta tự phát ra, cũng có một số lỗi được phát ra từ các phương thức chuẩn của .NET.

    Chúng ta cũng để ý rằng, dù chạy ở chế độ nào (Debug hay Release), cơ chế bắt lỗi riêng cũng đều hoạt động. Điều này cũng có nghĩa là chúng ta đã vứt bỏ ưu thế của chế độ Debug: không xác định được vị trí gây lỗi, cũng không theo dõi được stack khi bị lỗi. Việc bắt lỗi như vậy không thích hợp ở giai đoạn phát triển ứng dụng. Cũng vì lý do này mà nội dung bắt và xử lý lỗi của chúng ta xem xét ở bài cuối cùng của dự án.

    Vấn đề cấu hình của ứng dụng

    Khi một chương trình đã được dịch, đóng gói và triển khai cho người dùng cuối, chúng ta không thể dễ dàng thay đổi nó được nữa vì liên quan đến nhiều khâu. Ví dụ, nếu chúng ta quyết định sử dụng file nhị phân để lưu trữ dữ liệu của chương trình thì sau khi triển khai, nếu muốn chuyển sang dùng file json, chúng ta lại phải thực hiện trọn vẹn quy trình sửa mã nguồn => dịch => đóng gói => triển khai phiên bản mới.

    Trong nhiều tình huống chúng ta đã dự trù sẵn những thay đổi cho chương trình mà người dùng đầu cuối có thể thực hiện. Ví dụ, chúng ta muốn cho phép người dùng cuối thay đổi màu sắc và văn bản của con trỏ nhắc lệnh. Chúng ta cũng muốn cho người dùng cuối lựa chọn loại file dữ liệu để lưu trữ (file nhị phân, xml hay json).

    Một tình huống khác đó là thiết lập của ứng dụng ở giai đoạn phát triển không thể sử dụng ở giai đoạn triển khai. Ví dụ kết nối cơ sở dữ liệu ở giai đoạn phát triển ứng dụng hoàn toàn khác với kết nối ở giai đoạn triển khai. Khi đó, thông tin về chuỗi kết nối không thể code thẳng trong ứng dụng mà phải đặt ở file cấu hình để khi cài đặt chương trình người dùng sẽ trực tiếp thay đổi.

    Rất nhiều tình huống tương tự dẫn tới nhu cầu lưu trữ và truy xuất thông tin cấu hình cho chương trình.

    Trong phần thực hành này chúng ta sẽ vận dụng cơ chế lưu trữ và truy xuất thông tin cấu hình của .NET để xây dựng tính năng cấu hình. Tính năng này cho phép người dùng cuối thực hiện các thao tác sau:

    • Thay đổi màu sắc và văn bản của dấu nhắc lệnh: Hiện nay chúng ta đang thiết lập cứng văn bản của dấu nhắc lệnh là “# Request >>>” với màu Green. Chúng ta muốn người dùng có thể tùy ý chọn văn bản và màu sắc của dấu nhắc lệnh.
    • Thay đổi cơ chế và nơi lưu trữ dữ liệu: Hiện nay chúng ta đã xây dựng ba cơ chế lưu trữ dữ liệu khác nhau: binary, json, xml. Các cơ chế này lưu dữ liệu vào vào các file tương ứng là data.bin, data.json, data.xml. Chúng ta muốn người dùng có thể tùy chọn cơ chế lưu trữ và file dữ liệu.

    Thực hành 2: bổ sung chức năng thiết lập cấu hình

    Bước 1. Tạo file settings

    Click đúp vào nút Properties của BookMan.ConsoleApp. Trong cửa sổ chọn mục Settings.

    Vì project này chưa có file settings nào, phần nội dung bên tay phải đang trống. Click vào đường link sẽ tạo ra file settings đầu tiên của project. Mặc định file này có tên gọi Setttings.settings và nằm trong nút Properties.

    Visual Studio tạo ra một giao diện đồ họa để nhập các thiết lập (setting). Cũng có thể mở giao diện này bằng cách click đúp vào nút Settings.settings.

    Bước 2. Nhập các giá trị vào bảng thông tin cấu hình

    Nhập giá trị cho bảng settings
    Nhập giá trị cho bảng settings

    Đây là bảng thông tin đặc biệt, trong đó dữ liệu từ bảng sẽ được Visual Studio sử dụng để tự động sinh ra một class hỗ trợ truy xuất thông tin cầu hình. Class này có tên là Settings nằm trong không gian tên con Properties. Như trong project này, tên đầy đủ của lớp Settings là BookMan.ConsoleApp.Properties.Settings. Ở tất cả các file mã nguồn của project chúng ta đều có thể sử dụng lớp Settings này.

    Mỗi setting chứa 4 thông tin:

    Name: tên của setting. Thông tin Name sẽ được sử dụng để tạo ra một property tương ứng của class Settings. Settings là một class được Visual Studio sinh ra tự động dựa trên dữ liệu của bảng này, trong đó mỗi setting sẽ là một property của class. Vì vậy, Name phải tuân thủ theo quy tắc đặt tên biến và quy ước đặt tên property mà chúng ta đã học.

    Type: kiểu dữ liệu của setting. Như trên đã nói, mỗi setting sẽ trở thành một property của class Settings, giá trị của Type sẽ là kiểu dữ liệu của property.

    Lưu ý với setting PromptColor, kiểu dữ liệu System.ConsoleColor bình thường sẽ không xuất hiện trong danh sách lựa chọn. Chúng ta cần tự mình chỉ định vị trí chứa kiểu này: Trong combo box của cột Type chọn Browse; trong hộp thoại Select a Type mở nhánh mscorlib => System => ConsoleColor. Khi đó, trong ô Value sẽ có thể mở combobox để lựa chọn một trong 16 màu của ConsoleColor.

    Value: giá trị mặc định của setting. Giá trị này cũng tương đương với giá trị gán ban đầu cho mỗi property. Mặc dù trong ô nhập dữ liệu chúng ta chỉ nhập được chuỗi ký tự, giá trị này thực sự được chuyển đổi về kiểu tương ứng của thuộc tính. Vì lý do này, chỉ những kiểu có khả năng serialize (tuần tự hóa) về xml mới có thể được sử dụng.

    Scope: quyết định phạm vi sử dụng của setting. Scope có thể nhận một trong hai giá trị: User hoặc Application.

    Application scope quyết định rằng đây là một thuộc tính chỉ đọc (read-only). Chúng ta không thể thay đổi giá trị của setting trong khi chương trình hoạt động. Scope này sử dụng đối với các setting cấu hình một lần khi triển khai hệ thống và sau đó không (hoặc hiếm khi) thay đổi nữa.

    User cope sử dụng cho những setting cần thay đổi, ngay cả khi chương trình hoạt động (và có hiệu lực ngay lập tức). Trong project này chúng ta đặt cả 4 setting trong user scope.

    Khi lưu bảng cấu hình lại, Visual Studio sẽ tự động lưu thông tin vào file App.config như sau:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <configSections>
        
        <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >      
          <section name="BookMan.ConsoleApp.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />      
        </sectionGroup>
      </configSections>
      <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
      </startup>
      
      <userSettings>    
        <BookMan.ConsoleApp.Properties.Settings>
          <setting name="DataAccess" serializeAs="String">
            <value>binary</value>
          </setting>
          <setting name="PromptText" serializeAs="String">
            <value># Command >>></value>
          </setting>
          <setting name="PromptColor" serializeAs="String">
            <value>Green</value>
          </setting>
          <setting name="DataFile" serializeAs="String">
            <value>data.bin</value>
          </setting>
        </BookMan.ConsoleApp.Properties.Settings>    
      </userSettings>
    </configuration>

    Khi dịch ra ứng dụng, nội dung của file App.config sẽ được copy vào một file được đặt tên theo tên ứng dụng với phần mở rộng .config. Đối với ứng dụng này, file cấu hình khi triển khai là BookMan.ConsoleApp.exe.config.

    Song song với lưu dữ liệu vào file App.config, Visual Studio cũng tự sinh ra code cho class Settings:

    Không nên thay đổi nội dung của file này vì nếu như dữ liệu thay đổi, Visual Studio sẽ tự động sinh lại code cho class này. Khi đó những thay đổi của người dùng sẽ mất. Vì đây là một partial class, nếu thực sự muốn bổ sung code có thể tạo thêm file mã nguồn nữa ghép nối với class này.

    Bước 3. Tạo class Config trong file mã nguồn Config.cs trực thuộc project

    using System;
    namespace BookMan.ConsoleApp
    {
        using DataServices;
    
        internal class Config
        {
            private static Config _instance;
            public static Config Instance = _instance ?? (_instance = new Config());
            private Config() { }
    
            private Properties.Settings _s = Properties.Settings.Default;
    
            public void Reload() => _s.Reload();
    
            public IDataAccess IDataAccess
            {
                get {
                    var da = _s.DataAccess;
                    switch (da.ToLower())
                    {
                        case "binary": return new BinaryDataAccess();
                        case "json": return new JsonDataAccess();
                        case "xml": return new XmlDataAccess();
                        default: return new BinaryDataAccess();
                    }
                }
            }
    
            public string DataAccess
            {
                get => _s.DataAccess;
                set {
                    _s.DataAccess = value;
                    _s.Save();
                }
            }
    
            public string PromptText
            {
                get => _s.PromptText;
                set {
                    _s.PromptText = value;
                    _s.Save();
                }
            }
    
            public ConsoleColor PromptColor
            {
                get => _s.PromptColor;
                set {
                    _s.PromptColor = value;
                    _s.Save();
                }
            }
    
            public string DataFile
            {
                get => _s.DataFile;
                set {
                    _s.DataFile = value;
                    _s.Save();
                }
            }
        }
    }

    Bước 4. Thay đổi code của các class data access

    // lớp BinaryDataAccess
    public class BinaryDataAccess : IDataAccess
    {
        public List<Book> Books { get; set; } = new List<Book>();
        private readonly string _file = Config.Instance.DataFile; // "data.dat";
    ...
    
    // lớp JsonDataAccess
    public class JsonDataAccess : IDataAccess
    {
        public List<Book> Books { get; set; } = new List<Book>();
        private readonly string _file = Config.Instance.DataFile; // "data.json";
    ...
    
    // lớp XmlDataAccess
    public class XmlDataAccess : IDataAccess
    {
        public List<Book> Books { get; set; } = new List<Book>();
        private readonly string _file = Config.Instance.DataFile; // "data.xml";
    ...

    Bước 5. Thay đổi code của phương thức Main

    private static void Main(string[] args)
    {
        Console.OutputEncoding = System.Text.Encoding.UTF8;
        var text = Config.Instance.PromptText;
        var color = Config.Instance.PromptColor;
        ConfigRouter();
    
        while (true)
        {
            ViewHelp.Write(text, color);
            string request = Console.ReadLine();
    
            try
            {
                Router.Instance.Forward(request);
            }
            catch (Exception e)
            {
                ViewHelp.WriteLine(e.Message, ConsoleColor.Red);
            }
            finally
            {
                Console.WriteLine();
            }
        }
    }

    Bước 6. Thay đổi code của phương thức ConfigRouter

    private static void ConfigRouter()
    {
        IDataAccess context = Config.Instance.IDataAccess; //new BinaryDataAccess();
        BookController controller = new BookController(context);
        ShellController shell = new ShellController(context);
    ...

    Bước 7. Xây dựng thêm lớp ConfigController trong file ConfigController.cs trong thư mục Controllers

    using System;
    
    namespace BookMan.ConsoleApp.Controllers
    {
        using Framework;
    
        internal class ConfigController : ControllerBase
        {
            private Config _c = Config.Instance;
            public void ConfigPromptText(string text)
            {
                _c.PromptText = text;
                Success("The command prompt will change next time");
            }
    
            public void ConfigPromptColor(string text)
            {
                if (Enum.TryParse(text, true, out ConsoleColor color))
                {
                    _c.PromptColor = color;
                    Success("The command prompt color will change nex time");
                }
            }
    
            public void CurrentDataAccess()
            {
                var da = _c.DataAccess;
                var file = _c.DataFile;
                Inform($"Current data access engine: {da}rnCurrent data file: {file}");
            }
    
            public void ConfigDataAccess(string da, string file)
            {
                _c.DataAccess = da;
                _c.DataFile = file;
                Success("The changes will be available next time");
            }
        }
    }

    Bước 8. Thay đổi phương thức ConfigRouter

    Bổ sung khai báo object của kiểu ConfigController:

    private static void ConfigRouter()
    {
        IDataAccess context = Config.Instance.IDataAccess;
    
        BookController controller = new BookController(context);
        ShellController shell = new ShellController(context);
        ConfigController config = new ConfigController();
    
        Router r = Router.Instance;
    

    Bổ sung thêm các route sau:

    r.Register(route: "config prompt text",
        action: p => config.ConfigPromptText(p["text"]),
        help: "[config prompt text ? text = <value>]");
    r.Register(route: "config prompt color",
        action: p => config.ConfigPromptColor(p["color"]),
        help: "[config prompt color ? color = <value>]");
    r.Register(route: "current data access",
        action: p => config.CurrentDataAccess(),
        help: "[current data access]");
    r.Register(route: "config data access",
        action: p => config.ConfigDataAccess(p["da"], p["file"]),
        help: "[config data access ? da = <value:json, binary, xml> & file = <value>]");
    

    Bước 9. Dịch và chạy thử chương trình với các chức năng mới

    Chạy chức năng cấu hình
    Chạy chức năng cấu hình

    Sau khi thực hiện các lệnh trên, đóng và chạy lại chương trình để thấy các thiết lập mới đã có hiệu lực.

    Như vậy, chúng ta đã cung cấp cho người dùng cuối khả năng thay đổi các cấu hình cơ bản: màu sắc và văn bản của dấu nhắc lệnh; cơ chế lưu trữ dữ liệu và file dữ liệu.

    Kết luận

    Trong bài học này chúng ta đã xem xét hai vấn đề: Exception và Settings. Đây là những kỹ thuật bổ sung giúp ứng dụng hoạt động ổn định và uyển chuyển hơn. Chúng ta cũng đã vận dụng hai kỹ thuật này vào ứng dụng.

    Trong bài tiếp theo và là bài học cuối cùng của chuỗi bài giảng này, chúng ta sẽ học cách triển khai ứng dụng.

    + 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
    1 Thảo luận
    Oldest
    Newest
    Inline Feedbacks
    View all comments
    Sylvian

    Mình dùng net 5 thì ko có mục properties, cũng ko tạo được file setting.