Bài viết này khám phá các khía cạnh kỹ thuật khi làm việc với Blazor, đặc biệt trong ngữ cảnh của một khung ứng dụng như Known Framework. Chúng ta sẽ đi sâu vào cấu trúc giao diện người dùng, quản lý trạng thái, tương tác với dịch vụ, và các tính năng phát triển quan trọng khác.
Cấu trúc Trang Quản Trị và Điều Hướng
Trong các ứng dụng Blazor, trang quản trị thường được xây dựng với các thành phần điều hướng và quản lý nội dung động. Ví dụ, một thành phần layout chính có thể chứa menu bên và khu vực hiển thị nội dung theo tab. Các thành phần này được thiết kế để tương tác, cho phép người dùng điều hướng giữa các chức năng khác nhau.
Một ví dụ về cấu trúc bố cục trang quản trị:
<div class="admin-layout">
<aside class="sidebar">
<NavMenu Items="ApplicationMenus" OnMenuClick="HandleMenuSelection" />
</aside>
<main class="content-area">
<TabManager @ref="tabHost" CurrentActiveTab="ActiveTabItem" OpenTabs="VisibleTabs" />
</main>
</div>
@code {
private List<MenuItem> ApplicationMenus = new();
private TabItem ActiveTabItem;
private List<TabItem> VisibleTabs = new();
private TabManager tabHost;
protected override async Task OnInitializedAsync()
{
// Tải dữ liệu menu từ API hoặc dịch vụ
var dashboardInfo = await SomeApiService.GetDashboardDataAsync();
ApplicationMenus = BuildUserNavigation(dashboardInfo?.AvailableFunctions);
// Khởi tạo tab mặc định nếu có
ActiveTabItem = VisibleTabs.FirstOrDefault();
}
private void HandleMenuSelection(MenuItem selectedMenu)
{
// Logic để mở một tab mới hoặc chuyển đến tab đã có
var newTab = new TabItem { Title = selectedMenu.Text, Path = selectedMenu.Url };
tabHost.OpenOrActivateTab(newTab);
}
}
Khởi Tạo Dữ Liệu Thành Phần
Để tải dữ liệu khi một thành phần được khởi tạo, bạn có thể ghi đè phương thức OnInitializedAsync. Phương thức này chạy bất đồng bộ và lý tưởng để gọi các dịch vụ backend.
public class BaseAdminPageComponent : ComponentBase
{
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
// Giả sử có một dịch vụ xác thực để lấy thông tin người dùng và menu
var userProfile = await AuthenticationService.GetUserProfileAsync();
// Xử lý dữ liệu để xây dựng danh sách menu
// UserMenus = ProcessRawMenus(userProfile?.UserSpecificMenus);
}
}
Truy vấn Cơ sở Dữ liệu
Trong backend, việc truy vấn dữ liệu từ cơ sở dữ liệu là phổ biến. Ví dụ, để lấy danh sách các module hệ thống đang hoạt động:
SELECT ModuleID, ModuleName, Description FROM ApplicationModules WHERE IsActive = TRUE;
Tạo Trang Tùy Chỉnh và Đa Ngôn Ngữ
Phát Triển Trang Blazor Tùy Chỉnh
Để tạo một trang tùy chỉnh trong Blazor và tích hợp nó vào khung ứng dụng, bạn cần định nghĩa một thành phần Razor (.razor) thường kế thừa từ một thành phần cơ sở của khung (ví dụ: BaseFrameworkPage). Tên tệp Razor sẽ tương ứng với mã định danh của trang trong cấu hình.
<!-- Pages/CustomReport.razor -->
@inherits BaseFrameworkPage
<h3>Báo Cáo Hoạt Động Người Dùng</h3>
<p>Đây là nội dung của trang báo cáo tùy chỉnh.</p>
@code {
// Logic cụ thể cho trang báo cáo này
}
Khi thay đổi các tệp Razor, tính năng Hot Reload của Blazor giúp bạn xem ngay các thay đổi mà không cần biên dịch lại toàn bộ ứng dụng, tăng tốc đáng kể quá trình phát triển.
Hỗ trợ Đa Ngôn Ngữ
Để hỗ trợ đa ngôn ngữ, bạn có thể sử dụng các tệp tài nguyên (ví dụ: JSON hoặc RESX) chứa các chuỗi được bản địa hóa. Trong Blazor, bạn sẽ tham chiếu các khóa từ các tệp này để hiển thị văn bản phù hợp với ngôn ngữ hiện tại của người dùng.
Ví dụ tệp cấu hình ngôn ngữ (locales/vi-VN.json):
{
"Application.WelcomeMessage": "Chào mừng đến với hệ thống!",
"Application.Subtitle": "Hệ thống quản lý nguồn lực doanh nghiệp"
}
Sử dụng trong thành phần Razor:
<h1>@Localizer["Application.WelcomeMessage"]</h1>
<p>@Localizer["Application.Subtitle"]</p>
Tham Số Thành Phần và Ràng Buộc Dữ Liệu
Truyền Tham Số cho Thành Phần Con
Để truyền dữ liệu từ thành phần cha xuống thành phần con, bạn sử dụng thuộc tính [Parameter]. Đây là cách tiêu chuẩn trong Blazor để thiết lập các thuộc tính của thành phần con.
// Components/UserForm.razor
@code {
[Parameter]
public UserProfileData UserInformation { get; set; }
}
Để truyền một khối HTML hoặc các thành phần khác từ thành phần cha xuống thành phần con, bạn có thể sử dụng RenderFragment. Điều này cho phép thành phần con có các "slot" mà nội dung của chúng được định nghĩa bởi thành phần cha.
// Components/CustomCard.razor
@code {
[Parameter]
public RenderFragment HeaderContent { get; set; }
[Parameter]
public RenderFragment BodyContent { get; set; }
}
Định Tuyến Trang Blazor
Bạn có thể chỉ định URL cho một trang Blazor bằng chỉ thị @page ở đầu tệp Razor. Đây là cách Blazor ánh xạ một thành phần đến một đường dẫn URL cụ thể.
<!-- Pages/Dashboard.razor -->
@page "/dashboard"
<h3>Trang Tổng Quan</h3>
<p>Nội dung tổng quan của ứng dụng.</p>
Chỉ Thị Ràng Buộc Dữ Liệu @bind
Chỉ thị @bind trong Blazor cung cấp khả năng ràng buộc dữ liệu hai chiều thông minh. Nó tự động liên kết với thuộc tính phù hợp nhất của phần tử HTML (ví dụ: value cho input, checked cho checkbox). Bạn cũng có thể chỉ định định dạng cho ràng buộc dữ liệu.
<input @bind="searchTerm" placeholder="Tìm kiếm..." />
<input type="checkbox" @bind="isActivated" />
<input type="date" @bind:format="yyyy-MM-dd" @bind="selectedDate" />
@code {
private string searchTerm = "";
private bool isActivated = false;
private DateTime selectedDate = DateTime.Today;
}
Chế Độ Kết Xuất Tương Tác (@rendermode)
Để các thành phần Blazor có thể xử lý các sự kiện UI và sử dụng ràng buộc dữ liệu, chúng phải được kết xuất ở chế độ tương tác. Mặc định, các thành phần Blazor được kết xuất tĩnh từ máy chủ. Bạn có thể bật chế độ tương tác bằng chỉ thị @rendermode.
@rendermode InteractiveServer là một chế độ phổ biến, nơi các sự kiện UI được xử lý trên máy chủ thông qua kết nối WebSocket, và các cập nhật DOM được gửi lại cho trình duyệt.
<!-- Components/ClickCounter.razor -->
@rendermode InteractiveServer
<h3>Bộ Đếm Tương Tác</h3>
<p>Số lần nhấp: @clickCount</p>
<button class="btn btn-primary" @onclick="IncrementCounter">Nhấp vào đây</button>
@code {
private int clickCount = 0;
private void IncrementCounter()
{
clickCount++;
}
}
Truyền Tham Số cho Sự Kiện @onclick
Bạn có thể truyền tham số cho các bộ xử lý sự kiện @onclick bằng cách sử dụng biểu thức lambda.
<button class="btn btn-info" @onclick="() => EditItem(currentItemId)">Chỉnh sửa</button>
<button class="btn btn-danger" @onclick="() => DeleteItem(anotherItemId)">Xóa</button>
@code {
private int currentItemId = 123;
private string anotherItemId = "abc";
private void EditItem(int id)
{
Console.WriteLine($"Chỉnh sửa mục có ID: {id}");
// Logic để mở hộp thoại chỉnh sửa
}
private void DeleteItem(string id)
{
Console.WriteLine($"Xóa mục có ID: {id}");
// Logic để xác nhận và xóa
}
}
Các Thành Phần Nền Tảng và Dịch Vụ
Định nghĩa Trang Chủ Mặc Định
Các khung ứng dụng Blazor thường có một phương thức cấu hình để chỉ định trang chủ hoặc menu mặc định. Điều này đảm bảo rằng ứng dụng có một điểm vào hợp lý khi khởi động.
// ApplicationDefaults.cs
internal static FrameworkPage GetDefaultLandingPage()
{
return new FrameworkPage("Dashboard", "/home", typeof(DashboardComponent)) { IsClosable = false };
}
Thành Phần Bảng Dữ Liệu Chung
Một thành phần bảng dữ liệu mạnh mẽ là xương sống của nhiều ứng dụng kinh doanh. Nó thường bao gồm các tính năng như bộ lọc tìm kiếm, thanh công cụ với các hành động (thêm, sửa, xóa, xuất), và bảng hiển thị dữ liệu.
// Components/GenericDataTable.razor.cs
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace BlazorApp.Components;
public class GenericDataTable<TModel> : ComponentBase where TModel : class, new()
{
[Parameter] public DataTableConfiguration<TModel> Configuration { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (Configuration.FilterFields.Count > 0 || Configuration.ToolbarActions.Any())
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "data-table-controls");
// Render filter inputs
// Render toolbar buttons
builder.CloseElement(); // Close data-table-controls
}
builder.OpenElement(2, "div");
builder.AddAttribute(3, "class", "data-table-grid");
// Render the actual data table (e.g., using a third-party grid component or custom rendering)
builder.CloseElement(); // Close data-table-grid
}
}
// Models/DataTableConfiguration.cs
public class DataTableConfiguration<TModel>
{
public List<QueryField> FilterFields { get; set; } = new();
public List<ToolbarButton> ToolbarActions { get; set; } = new();
public Func<Task<IEnumerable<TModel>>> DataProvider { get; set; } // Hàm cung cấp dữ liệu
// Other properties like pagination, column definitions, etc.
}
Truy Cập Bộ Nhớ Cục Bộ (LocalStorage) qua JS Interop
Blazor cho phép bạn tương tác với JavaScript thông qua JS Interop. Điều này rất hữu ích để truy cập các API trình duyệt như localStorage.
Định nghĩa các phương thức JavaScript trong tệp wwwroot/js/clientStorage.js:
// wwwroot/js/clientStorage.js
export class ClientStorageHelper {
static getLocalItem(key) {
return localStorage.getItem(key);
}
static setLocalItem(key, value) {
if (value !== null && value !== undefined) {
localStorage.setItem(key, JSON.stringify(value));
} else {
localStorage.removeItem(key);
}
}
}
Định nghĩa dịch vụ C# để gọi các phương thức JavaScript này (ví dụ: Services/ClientStorageService.cs):
// Services/ClientStorageService.cs
using Microsoft.JSInterop;
using System.Text.Json;
public class ClientStorageService
{
private readonly IJSRuntime _jsRuntime;
public ClientStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task<T> GetItemAsync<T>(string key)
{
var jsonValue = await _jsRuntime.InvokeAsync<string>("ClientStorageHelper.getLocalItem", key);
return string.IsNullOrEmpty(jsonValue) ? default : JsonSerializer.Deserialize<T>(jsonValue);
}
public Task SetItemAsync(string key, object value)
{
return _jsRuntime.InvokeVoidAsync("ClientStorageHelper.setLocalItem", key, value);
}
}
Cách sử dụng trong một thành phần Blazor:
public class MyComponent : ComponentBase
{
[Inject] private ClientStorageService _storageService { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await _storageService.SetItemAsync("appSetting:theme", "dark");
var theme = await _storageService.GetItemAsync<string>("appSetting:theme");
Console.WriteLine($"Chủ đề hiện tại: {theme}");
}
}
}
Xuất Bảng Dữ Liệu ra Excel
Để xuất dữ liệu từ bảng ra tệp Excel, bạn thường sẽ tạo tệp Excel trên máy chủ (ví dụ: bằng thư viện EPPlus), sau đó truyền tệp này dưới dạng luồng dữ liệu về client để trình duyệt tải xuống. JS Interop có thể được sử dụng để kích hoạt quá trình tải xuống ở phía client.
// ExportService.cs
using OfficeOpenXml; // Thay vì 'EPPlus' trực tiếp, sử dụng namespace OfficeOpenXml
public class ExportService
{
private readonly IJSRuntime _jsRuntime;
public ExportService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task ExportDataToExcelAsync<T>(string fileName, IEnumerable<T> data)
{
ExcelPackage.LicenseContext = LicenseContext.NonCommercial; // Hoặc Commercial nếu có license
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Dữ liệu báo cáo");
// Ví dụ: thêm tiêu đề cột
worksheet.Cells[1, 1].Value = "Mã SP";
worksheet.Cells[1, 2].Value = "Tên SP";
worksheet.Cells[1, 3].Value = "Số lượng";
// Giả sử T có các thuộc tính Id, Name, Quantity
int row = 2;
foreach (var item in data)
{
// Sử dụng Reflection hoặc ánh xạ thủ công để điền dữ liệu
// worksheet.Cells[row, 1].Value = item.Id;
// worksheet.Cells[row, 2].Value = item.Name;
// worksheet.Cells[row, 3].Value = item.Quantity;
row++;
}
using (var memoryStream = new MemoryStream(package.GetAsByteArray()))
{
// Một hàm JS tương tự như BlazorDownloadFile có thể được định nghĩa trong clientStorage.js
await _jsRuntime.InvokeVoidAsync("ClientStorageHelper.downloadFileFromStream", fileName, Convert.ToBase64String(memoryStream.ToArray()));
}
}
}
}
Cấu Hình Kích Thước Tệp Tải Lên Tối Đa
Để thay đổi kích thước tệp tải lên tối đa mặc định (thường là 50MB), bạn cần cấu hình trong tệp Program.cs của ứng dụng Blazor và có thể trong các cấu hình riêng của khung ứng dụng.
// Program.cs
builder.Services.AddApplicationFramework(options =>
{
options.ConfigureWebHostInfo(builder.Environment.WebRootPath, builder.Environment.ContentRootPath, builder.Environment.IsDevelopment());
// Cấu hình kích thước tải lên tối đa thành 100 MB
options.MaxFileUploadSize = 1024 * 1024 * 100;
});
Đồng thời, cấu hình này cũng cần được phản ánh trong các thiết lập của khung nếu có:
// FrameworkConfig.cs (conceptual)
public class FrameworkConfiguration
{
public long MaxFileUploadSize { get; set; } = 1024 * 1024 * 100; // Mặc định 100 MB
}
Giải Quyết Xung Đột Định Tuyến
Lỗi AmbiguousMatchException xảy ra khi Blazor tìm thấy nhiều điểm cuối (endpoints) phù hợp với cùng một yêu cầu URL. Điều này thường xảy ra khi bạn có nhiều thành phần Razor định nghĩa cùng một tuyến đường (@page).
Ví dụ, nếu bạn có hai tệp Razor đều định nghĩa tuyến đường gốc:
<!-- Pages/Index.razor -->
@page "/"
<h3>Trang Chủ Chính</h3>
<!-- Pages/Home.razor -->
@page "/"
<h3>Trang Chủ Phụ</h3>
Khi người dùng truy cập /, Blazor không biết nên hiển thị Index.razor hay Home.razor, dẫn đến lỗi AmbiguousMatchException. Để khắc phục, hãy đảm bảo rằng mỗi tuyến đường chỉ được định nghĩa một lần hoặc sử dụng các ràng buộc tuyến đường phức tạp hơn để phân biệt.