Bài viết

Kho tài liệu và bài viết được chia sẻ, đánh giá bởi cộng đồng

Kỹ thuật sử dụng Dependency Injection trong Winform project - Winform never die

skynet đã tạo 12:29 13-09-2021 Hoạt động 12:32 17-09-2021 555 lượt xem 4 bình luận

Nội dung bài viết

DẪN NHẬP

Trước khi đi vào code mẫu của DI trong Winform mình cùng tìm hiểu xem DI là gì và vì sao các lập trình viên lại đam mê đến thế.

Định nghĩa có thể sẽ gây khó hiểu, vì thực sự cũng chưa biết định nghĩa nào mới là chuẩn, nhưng mình sẽ miêu tả dựa trên nhu cầu:

DI là kỹ thuật được sử dụng Nếu “bạn có thứ gì đó muốn khai báo một lần, sử dụng ở nhiều nơi”.

Như vậy nhiều bạn có thể nói: Tôi tạo singleton ở static class cũng được, chỉ một lần khai báo cũng sài được ở nhiều nơi.

Điều ấy đúng, nhưng chưa tiện, vì sao tôi nói vậy?

Giả sử bạn có 1 class cần sử dụng nhiều Service:

public classA(){

          public Service B { get; set; }

          public Service C { get; set; }
          
          ...

          public Service Z { get; set; }
}

Để dùng được Service B, C..Z cần làm gì? Lại truyền từ ngoài vào, hay sẽ dùng constructor để set giá trị khởi tạo. Chưa kể các Service có thể cần (phụ thuộc vào) nhiều, nhiều tham số hoặc các Service có liên quan khác nữa (nhũn não nếu như bạn có hàng tá Service có họ hàng – quan hệ nhập nhằng với nhau - bạn sẽ phải sửa rất nhiều code nếu có sự thay đổi với 1 đối tượng service nào đó).

Lúc này, DI sẽ tỏ rõ được vị thế sức mạnh của mình khi tự động TIÊM tất cả những thứ mà chương trình cần để chạy được;

Để có thể tự động TIÊM một thứ gì đó, code cần được triển khai dạng constructor-parameters (tham số truyền vào thông qua constructor) và phải đảm bảo Quy trình 3 bước như sau đây: điều động các biến làm việc như cán bộ vậy (^_^)

  1. Khai báo thứ muốn truyền vào thông qua Constructor
  2. Có 01 field để Hứng dữ liệu đã được truyền vào
  3. Sử dụng nó thôi.

Ví dụ:

public Form1 : Form

{

          //Hứng
          private AppContext _context;
          
          
          //Tham số đầu vào constructor
          public Form1(AppDBContext context){

                   _context = context;
         } 



          //Ok rồi, code bất kể thứ gì liên quan đến appcontext ở đây

          //Ví dụ như:
          //(Method này mình viết dạng generic chỉ có giá trị tham khảo, không chạy trong Form1 này)

          private void AddSomething(TEntity entity){

          _context.Set<TEntity>().Add(entity);

          _context.SaveChanges();

          }

}

 

+ Nghe hấp dẫn rồi đấy, nhưng đoạn code trên đã hoạt động hay chưa?

- Chưa đâu, cần có HOST để thực hiện công việc tự động TIÊM mọi thứ khi cần dùng nữa.

 

Các kỹ thuật ở bài này có thể sử dụng một phần hoặc hoàn toàn ở các dự án console, wpf hoặc asp.net khác.

Đặc biệt là cách dùng Serilog, SQLite, chạy lệnh EF như: add-migration, update-database có thể tái sử dụng ở mọi dự án .NetCore khác.

Bài này giờ mới vào phần chính, cả nhà nhớ ăn uống đầy đủ, kẻo tụt huyết áp học ko vào nhé! =]]

Thứ tự các bước như sau nè:

1. Tạo mới Winform project (.NET CORE - Chọn luôn 5.0 cho mới)

- Chạy visual studio => chọn new project => tìm dự án winform (.Net Core hoặc .Net - không có chữ framework).

- Này tự làm nhé ^_^!

2. Cài các Thư viện có liên quan

- DBContext

"Microsoft.EntityFrameworkCore"                 Version="5.0.9"

- UseSQLite()
"Microsoft.EntityFrameworkCore.Sqlite"          Version="5.0.9"

- Add-Migration; Update-database
"Microsoft.EntityFrameworkCore.Tools"           Version="5.0.9"

--Để dùng IHOST (nơi khởi nguồn DI)
"Microsoft.Extensions.Hosting"                  Version="5.0.0"

--Này như trên (mình ko hiểu)
"Microsoft.Extensions.Hosting.Abstractions"     Version="5.0.0"

--UserSerilog()
"Serilog"                                       Version="2.10.0"

--UserSerilog()
"Serilog.Extensions.Hosting"                    Version="4.1.2"

--WriteTo.File()
"Serilog.Sinks.File"                            Version="5.0.0"

--WriteTo.SQLite()
"Serilog.Sinks.SQLite"                          Version="5.0.0"

3. Khai báo HOST

- Khai báo Host này ở chỗ nào cũng được, miễn là nó static readonly (khởi tạo một lần duy nhất & không thể chỉnh sửa trong suốt quá trình app chạy).

private static readonly IHost _host
         = Host.CreateDefaultBuilder()
                //Ilogger ở tất cả các constructor sẽ được tự động tiêm giá trị do Serilog là một
                //package sử dụng thay thế cho Logger của Microsoft. Serilog sẽ hỗ trợ lưu file,
                //hoặc viết ra console có màu nhấn mạnh dễ đọc.
                .UseSerilog((host, loggerConfiguration) =>
                {
                    loggerConfiguration
                        //Viết log hệ thống ra file test.txt, mỗi ngày tạo 1 file
                        .WriteTo.File("test.txt", rollingInterval: RollingInterval.Day)

                        //Đồng thời viết cả log ra SQLiteDB có tên blog.db (thích thì thay tên khác)
                        .WriteTo.SQLite(".\\blog.db")

                        //WriteTo Console và Debug không hoạt động ở Winform .NetCore 5.0
                        //(mình chữa rõ lý do, cũng ko ảnh hưởng vì log này có file text dễ dùng rồi.
                        //.WriteTo.Debug()
                        
                        //Level tối thiểu sẽ pass để chuyển lưu vào log hoặc hiển thị ra console 
                        // ở các dự án web hoặc wpf khác vì console, debug ko thấy chạy ở winform này 
                        //như đã trình bày ở trên
                        .MinimumLevel.Debug()
                        /*.MinimumLevel.Override("WinFormsApp3.Form1", Serilog.Events.LogEventLevel.Debug)*/;
                })
                .ConfigureServices(services =>
                {
                    services.AddDbContext<AppContext>(c => {
                                                            c.UseSqlite("Data Source=.\\blog.db"); 
                                                            });
                    
                    services.AddSingleton<Form1>();

                    //services.AddSingleton<ICommand, MakeSandwichCommand>();

                    //services.AddSingleton<MainViewModel>();

                    //services.AddSingleton<MainWindow>(s => new MainWindow()
                    //{
                    //    DataContext = s.GetRequiredService<MainViewModel>()
                    //});
                })
                .Build();

* Sau này tất cả mọi thứ cần tự động TIÊM sẽ được khai báo trong cái Host này

ví dụ:

- services.AddSingleton<ServiceA>(); //Chỗ nào cần ServiceA thì truyền cho nó 1 cái (Singleton thì cả chương trình chỉ có 1)

- services.AddScoped<ServiceA>(); //Mỗi constructor gọi ServiceA sẽ nhận được một cái khác nhau.

- services.AddTransient<ServiceA>(); //Cứ hễ ServiceA được sử dụng đến là sẽ được tạo mới

(đoạn này mình hiểu vậy, sai xin anh em comment giúp)


4. Sử dụng HOST trong Program

        [STAThread]
        static void Main()
        {
            //Khởi chạy host chuẩn bị sẵn sàng mọi thứ cho việc TIÊM
            _host.Start();


            //Đoạn này mặc định của winform kệ nó thôi.
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);


            //Lấy ra cái Form1 đã được khai báo trong services
            var form1 = _host.Services.GetRequiredService<Form1>();

            //Lệnh chạy gốc là: Application.Run(new Form1);
            //Đã được thay thế bằng lệnh sử dụng service khai báo trong host
            Application.Run(form1);

            //Khi form chính (form1) bị đóng <==> chương trình kết thúc ấy
            //thì dừng host
            _host.StopAsync().GetAwaiter().GetResult();

            //và giải phóng tài nguyên host đã sử dụng.
            _host.Dispose();
        }

Ok rồi, sử dụng thực tế nè:

public partial class Form1 : Form
    {
        //Hứng các tham số được tiêm vào thông qua constructor
        private readonly ILogger<Form1> _logger;
        private readonly AppContext _context;


        //Tham số được tự động truyền vào bởi host, trong đó:
        //      - logger được xử lý bởi serilog
        //      - AppContext được xử lý bởi service nằm trong host
        public Form1(ILogger<Form1> logger, AppContext context)
        {
            //gán tham số được truyền vào cho biến nội bộ để sử dụng
            _logger = logger;
            _context = context;

            //Ghi thử một log thông báo ra test.txt và blog.db rằng "app started!"
            _logger.LogWarning("app started!");

            //Khởi tạo form mặc định thôi.
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {

            //nếu chưa có dữ liệu trong table Customers thì thêm vào 3 cái
            if (_context.Customers.Count() == 0)
            {
                _context.Customers.Add(new Customer { Id = "11", Name = "11" });
                _context.Customers.Add(new Customer { Id = "12", Name = "12" });
                _context.Customers.Add(new Customer { Id = "13", Name = "13" });
                _context.SaveChanges();
            }

            //rồi sau đó show ra id của thằng đầu tiên.
            MessageBox.Show(_context.Customers.First().Id);

        }
    }
}

5. (optional) Tạo Thêm AppContext Customer để lưu thử dữ liệu nữa!

optional nghĩa là tạo hoặc không đều được, nếu bạn không có nhu cầu sử dụng db thì bỏ qua bước này và xoá tất cả lệnh có chứa AppContext hoặc context đi; và ứng dụng có thể chạy bình thường rồi.

Nếu có sử dụng db (trong bài này là SQLite) thì cần khai báo các thứ sau:

Khai báo AppContext và AppContextFactory (trùng lặp sao? không phải đâu, winform 5.0 nó phải khai báo 2 lần lận, cả trong host và ở đây, dị vậy đó)

//nếu chưa rõ về EF thì mọi người cần đọc thêm tài liệu nhé
//Bài này mình không giới thiệu sâu về EF
public class AppContext : DbContext
{
    public AppContext(DbContextOptions<AppContext> options) : base(options)
    {
        Database.EnsureCreated();
    }
    public DbSet<Customer> Customers { get; set; }
}


//Muốn DI được cần khai báo cái này mặc dù bên host đã services.DBContext rồi(winform 5.0 thôi)
public class AppContextFactory : IDesignTimeDbContextFactory<AppContext>
{
    public AppContext CreateDbContext(string[] args)
    {
         var optionsBuilder = new DbContextOptionsBuilder<AppContext>();
         optionsBuilder.UseSqlite("Data Source=.\\blog.db");

         return new AppContext(optionsBuilder.Options);
     }
}

Chạy nha?

Ok làm xong bước 6 là chạy được

6. (required if 5) Mở package console manager lên để chạy lệnh

Add-migration init       (tạo migration có tên là Init)

Update-Database       (cập nhật vào db là sqlite file)

*một số lưu ý

- Xem code mẫu  file nằm ở đây

- Do giới hạn của winform (chắc thế) lệnh Update-Database có thể báo lỗi, nhưng nó tạo file xong, là ok dùng tốt! ^_^!

BÀI CHƯA ĐẦY ĐỦ, ANH EM GÓP Ý MÌNH SẼ SỬA ĐỂ LÀM TÀI LIỆU VỀ LÂU DÀI CHO CÁC NEWBIE NHÉ!

Nội dung bài viết

Bình luận

Để bình luận, bạn cần đăng nhập bằng tài khoản Howkteam.

Đăng nhập
hoanganh9192 đã bình luận 14:23 13-09-2021

ui, tuyệt quá! cảm ơn a

K9 SuperAdmin, KquizAdmin, KquizAuthor đã bình luận 14:10 13-09-2021

hay nè. viết thêm nữa đi bạn

Câu hỏi mới nhất