Tách giao diện và dữ liệu
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
MainForm đang gọi thẳng ShopDbContext. Muốn chạy thử màn hình khi chưa
bật Oracle hay đổi cách lưu dữ liệu, bạn đều phải sửa form. Bài này đặt phần
đọc ghi sau interface IProductStore, đúng như API đã làm ở khoá ASP.NET
Core.
Khái niệm
🔩 Composition root: nơi duy nhất trong app tạo các object và ghép chúng lại với nhau.
Trong app WinForms, chỗ đó là Main. Đây vẫn là DIP của khoá OOP: ở đó ta
tự new rồi truyền vào constructor. Ở khoá ASP.NET Core, container làm hộ
việc này qua AddScoped<IProductStore, DbProductStore>().
Ví dụ
Chép nguyên IProductStore và DbProductStore từ bài Truy vấn với EF Core
của khoá ASP.NET Core, không sửa dòng nào. Interface chỉ có hai method:
public interface IProductStore
{
Task<List<Product>> InStockAsync();
Task<bool> SetStockAsync(int id, int stock);
}Main ghép các phần, MainForm chỉ biết IProductStore:
using Microsoft.EntityFrameworkCore;
namespace ShopDesk;
static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
var cs = "User Id=shopapi;Password=shopapi_pw;"
+ "Data Source=localhost:1521/FREEPDB1";
var options =
new DbContextOptionsBuilder<ShopDbContext>()
.UseOracle(cs)
.UseUpperSnakeCaseNamingConvention()
.Options;
var db = new ShopDbContext(options);
IProductStore store = new DbProductStore(db);
Application.Run(new MainForm(store));
}
}
class MainForm : Form
{
private readonly IProductStore _store;
private readonly BindingSource _source =
new BindingSource();
public MainForm(IProductStore store)
{
_store = store;
Text = "Hàng còn trong kho";
Width = 500;
var grid = new DataGridView
{
Dock = DockStyle.Fill,
DataSource = _source,
ReadOnly = true,
AllowUserToAddRows = false
};
var soldOutButton = new Button
{
Text = "Hết hàng",
Dock = DockStyle.Top
};
soldOutButton.Click += SoldOutButton_Click;
Controls.Add(grid);
Controls.Add(soldOutButton);
Load += async (sender, e) => await LoadAsync();
}
private async Task LoadAsync()
{
_source.DataSource =
await _store.InStockAsync();
}
private async void SoldOutButton_Click(
object? sender, EventArgs e)
{
object? item = _source.Current;
if (item == null)
{
return;
}
var product = (Product)item;
await _store.SetStockAsync(product.Id, 0);
await LoadAsync();
}
}MainFormkhông có chữDbContextnào. Nó chỉ gọi hai method của interface._source.Currentlà dòng đang chọn trên lưới, kiểuobject, ép vềProductnhư bài ListBox.- Bấm "Hết hàng" thì tồn kho về 0, danh sách tải lại, dòng đó biến mất. Cho gọn, ví dụ không tắt nút trong lúc chờ như bài Async giữ giao diện mượt; app thật nên tắt.
- API và app kho dùng cùng một đoạn code
DbProductStore, chép nguyên. NhưngDbContextở đây sống suốt vòng đời của form, nênInStockAsync(không cóAsNoTracking()) không thấy thay đổi do nơi khác ghi vào.
Thử ngay
Viết một bản giả giữ dữ liệu trong bộ nhớ, giống ý tưởng
InMemoryProductStore ở bài Dependency injection trong ASP.NET Core:
class FakeProductStore : IProductStore
{
private readonly List<Product> _products =
new List<Product>
{
new Product
{
Id = 1, Name = "Bút bi",
Price = 5000m, Stock = 120
},
new Product
{
Id = 2, Name = "Thước",
Price = 7000m, Stock = 50
}
};
public Task<List<Product>> InStockAsync()
{
var result = _products
.Where(p => p.Stock > 0)
.ToList();
return Task.FromResult(result);
}
public Task<bool> SetStockAsync(int id, int stock)
{
foreach (var p in _products)
{
if (p.Id == id)
{
p.Stock = stock;
return Task.FromResult(true);
}
}
return Task.FromResult(false);
}
}Task.FromResult bọc sẵn một kết quả thành Task, dùng khi không có gì
phải chờ. Tắt Oracle bằng docker stop oracle, rồi trong Main đổi dòng
tạo store thành IProductStore store = new FakeProductStore();.
Đoán trước khi chạy: Oracle đã tắt. App có mở được, có bấm "Hết hàng" được không?
Xem kết quả
App mở bình thường, lưới có Bút bi và Thước.
Chọn Thước, bấm "Hết hàng": dòng Thước biến mất.MainForm không đổi một chữ nào mà vẫn chạy trên dữ liệu giả. Thử xong,
chạy docker start oracle và đổi Main về lại DbProductStore.
Lỗi hay gặp
Form tự new phần dữ liệu. Form lại gắn chặt với Oracle và không thay
được bằng bản giả.
// SAI — form tự tạo DbProductStore
class MainForm : Form
{
private readonly IProductStore _store;
public MainForm(ShopDbContext db)
{
_store = new DbProductStore(db);
}
}// ĐÚNG — form nhận interface từ Main
class MainForm : Form
{
private readonly IProductStore _store;
public MainForm(IProductStore store)
{
_store = store;
}
}Tóm tắt
- Form chỉ phụ thuộc
IProductStore, không biếtDbContext. Mainlà composition root: tạoDbContext,DbProductStore, form, rồi ghép lại.- API và app WinForms dùng cùng code
DbProductStore. - Đổi sang
FakeProductStorelà chạy được giao diện khi không có Oracle.
Tự kiểm tra
0/3 câuApp WinForms có thêm màn hình đơn hàng cần IOrderStore. Nên tạo DbOrderStore ở đâu?
Muốn chạy thử màn hình kho trên máy chưa cài Oracle, cần sửa ở đâu?
_source.Current trả về gì?