Chuyển đến nội dung chính

Blazor: Blazor State Management - Part 3

State trong Blazor là một trong những khái niệm quan trọng nhất. Nếu hiểu đúng, bạn sẽ thấy Blazor rất "mượt"; nếu hiểu sai, code sẽ nhanh chóng thành… mì gói 🍜.

Bài này đi từ ví dụ đơn giản nhất (click button) đến các pattern nâng cao như StateContainer, PersistentState, Fluxor và Undo/Redo.

1. State là gì?

State = dữ liệu hiện tại của ứng dụng tại một thời điểm.

Ví dụ:

  • Counter = 5
  • User đã đăng nhập
  • Giỏ hàng có 3 sản phẩm

UI luôn được render dựa trên state.

2. Ví dụ cơ bản nhất: Click Counter

Counter.razor

<h3>Counter</h3>

<p>Current count: @count</p>

<button @onclick="Increment">Click me</button>

@code {
    int count = 0;

    void Increment()
    {
        count++;
    }
}

Điều gì đang xảy ra? Flow rất đơn giản:

User click
   ↓
Increment()
   ↓
count thay đổi
   ↓
Blazor re-render UI

Đây chính là nền tảng: UI = f(state)

3. Khi state nằm trong component

Cách trên hoạt động tốt khi:

  • State chỉ dùng trong 1 component
  • Không cần chia sẻ

Nhưng vấn đề xuất hiện khi:

Component A cần state
Component B cũng cần
Component C cũng cần

4. Cách sai (truyền Parameter lòng vòng)

A → B → C → D

Code sẽ:

  • khó maintain
  • coupling cao
  • dễ bug

🧠 5. Giải pháp: StateContainer

📌 Ý tưởng: Tạo một service giữ state và thông báo khi state thay đổi.

AppState.cs

public class AppState
{
    private int _count;

    public int Count
    {
        get => _count;
        set
        {
            _count = value;
            NotifyStateChanged();
        }
    }

    public event Action? OnChange;

    private void NotifyStateChanged() => OnChange?.Invoke();
}

Program.cs

builder.Services.AddSingle<AppState>();
Tất cả mọi người vào web của bạn đều dùng chung một biến. Cách này khá nguy hiểm.

Component sử dụng

@inject AppState AppState
@implements IDisposable

<p>Count: @AppState.Count</p>

<button @onclick="Increase">Click</button>

@code {
    protected override void OnInitialized()
    {
        AppState.OnChange += HandleChange;
    }

    void Increase()
    {
        AppState.Count++;
    }

    void HandleChange()
    {
        InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        AppState.OnChange -= HandleChange;
    }
}

🎯 Ý nghĩa: Service giữ state, Components subscribe để lắng nghe thay đổi.

Circuit là gì?

Circuit trong Blazor Interactive Server giống như một cuộc gọi điện thoại liên tục (kết nối SignalR) giữa trình duyệt của bạn và server.

Mỗi tab trình duyệt sẽ có một Circuit riêng. Circuit chỉ bị hủy khi đóng tab hoặc refresh trang.

flowchart TD subgraph "Trình duyệt của Người dùng" Tab1["Tab 1\n(Window 1)"] Tab2["Tab 2\n(Window 2)"] end subgraph "Server (Blazor Interactive Server)" Circuit1["Circuit 1\n(Kết nối SignalR cho Tab 1)"] Circuit2["Circuit 2\n(Kết nối SignalR cho Tab 2)"] AppState1["AppState Instance 1\n(count = 5)"] AppState2["AppState Instance 2\n(count = 0)"] end Tab1 -->|Kết nối SignalR| Circuit1 Tab2 -->|Kết nối SignalR| Circuit2 Circuit1 --> AppState1 Circuit2 --> AppState2 style Tab1 fill:#e3f2fd style Tab2 fill:#e3f2fd style Circuit1 fill:#f0f4c3 style Circuit2 fill:#f0f4c3
Lưu ý các trang razor cần khai báo InteractiveServer mode
@page "/counter"
@rendermode InteractiveServer

6. Nâng cấp: Generic StateContainerBase

Khi app bắt đầu lớn lên, bạn sẽ không muốn tạo từng class AppState lặp đi lặp lại cho từng loại state (UserState, CartState, ThemeState…). Lúc này, StateContainerBase sẽ giúp bạn viết code sạch hơn, ít lặp lại hơn và dễ maintain hơn.

Ý tưởng chính:

  • Dùng một class base (abstract) để xử lý chung việc lưu state và phát event thông báo thay đổi.
  • Mỗi loại state sẽ kế thừa từ StateContainerBase<TState>, trong đó TState thường là một record hoặc class immutable.
  • Khuyến khích dùng immutable (record) để tránh mutate state trực tiếp – giúp code an toàn và dễ debug hơn.

Ví dụ thực tế: CounterState

Đầu tiên, ta định nghĩa class chứa dữ liệu (nên dùng record để immutable):

public record CounterState
{
    public int Count { get; init; } = 0;
}

Sau đó tạo class AppState kế thừa từ StateContainerBase:

public class AppState : StateContainerBase<CounterState>
{
    public int Count
    {
        get => _state.Count;
        set => UpdateState(state => state with { Count = value });
    }

    public void Increment()
    {
        UpdateState(state => state with { Count = state.Count + 1 });
    }

    public void Decrement()
    {
        UpdateState(state => state with { Count = state.Count - 1 });
    }

    public void Reset()
    {
        UpdateState(_ => new CounterState { Count = 0 });
    }
}

StateContainerBase là gì?

Đây là lớp abstract giúp xử lý phần chung cho tất cả các StateContainer:

public abstract class StateContainerBase<TState> where TState : new()
{
    protected TState _state = new();

    public event Action? OnChange;

    public TState State => _state;

    protected void SetState(TState newState)
    {
        _state = newState;
        NotifyStateChanged();
    }

    protected void UpdateState(Func<TState, TState> update)
    {
        _state = update(_state);
        NotifyStateChanged();
    }

    protected void NotifyStateChanged() => OnChange?.Invoke();
}

Cách sử dụng trong Component

Sử dụng hoàn toàn giống như AppState thông thường:

@inject AppState AppState
@implements IDisposable

<p>Current count: @AppState.Count</p>

<button @onclick="AppState.Increment">+1</button>
<button @onclick="AppState.Decrement">-1</button>
<button @onclick="AppState.Reset">Reset</button>

@code {
    protected override void OnInitialized()
    {
        AppState.OnChange += HandleStateChanged;
    }

    private void HandleStateChanged()
    {
        InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        AppState.OnChange -= HandleStateChanged;
    }
}

Lợi ích khi dùng StateContainerBase:

  • Không phải viết lại code notify event, SetState, UpdateState cho mỗi loại state.
  • Dễ mở rộng (thêm UserState, CartState… chỉ cần kế thừa).
  • Khuyến khích dùng immutable (record + with expression) → code an toàn hơn.
  • Dễ test hơn vì logic nằm hoàn toàn ở service, không phụ thuộc UI.

7. PersistentState: Khắc phục Flash khi Pre-rendering

Flash là gì?

Flash là hiện tượng chớp nhoáng mà bạn thấy trên trang Blazor khi component được render hai lần:

  1. Server chạy: Tạo dữ liệu, render HTML
  2. Client chạy: Chạy lại code, tạo dữ liệu mới khác
  3. Hai dữ liệu xung đột: Trang nháy một cái

Ví dụ: Trang thời tiết hiển thị 40°F → Flash ⚡ → 27°F

Cách cũ

Trước .NET 10, developer tắt pre-rendering:

@rendermode @(new InteractiveServerRenderMode(false))

Vấn đề:

  • Trang tải chậm hơn
  • HTML trống lúc đầu
  • SEO kém
  • Người dùng thấy "Loading..."

Giải pháp mới: [PersistentState]

Attribute [PersistentState] cho phép Blazor lưu giữ dữ liệu từ server render và tái sử dụng khi client khởi động.

Kết quả: Không flash, không tải lại, trang hiển thị mượt mà.

Cách sử dụng

@page "/weather"
@rendermode InteractiveServer

@if (Forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temperature (C)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in Forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    [PersistentState]
    public WeatherForecast[]? Forecasts { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);
        
        // only create new data if not existing data
        if (Forecasts == null)
        {
            var startDate = DateOnly.FromDateTime(DateTime.Now);
            var summaries = new[] 
            { 
                "Freezing", "Bracing", "Chilly", "Cool", "Mild", 
                "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 
            };
            
            Forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        }
    }

    public class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
    }
}

Điều gì xảy ra?

Lần 1 (Server - Pre-rendering):

  • Forecasts = null
  • if (null) → TRUE
  • Tạo dữ liệu: [25°C, 30°C, 35°C]
  • Render HTML
  • Lưu dữ liệu vào PersistentState

Lần 2 (Client - Hydration):

  • PersistentState khôi phục: Forecasts = [25°C, 30°C, 35°C]
  • if (null) → FALSE
  • Bỏ qua tạo dữ liệu
  • Render HTML với cùng dữ liệu
  • Không flash!

Tại sao phải check null?

Nếu không check, code sẽ luôn tạo dữ liệu mới lần 2, ghi đè dữ liệu từ server:

protected override async Task OnInitializedAsync()
{
    Forecasts = Enumerable.Range(1, 5)...  // Luôn tạo mới, ghi đè
}

Kết quả: Server: 25°C → Flash ⚡ → Client: 18°C (random mới)

Với check null:

if (Forecasts == null)
    Forecasts = Enumerable.Range(1, 5)...

Kết quả: Server: 25°C → Client: 25°C (không flash) ✅

Tính năng nâng cao

1. Cho phép cập nhật khi điều hướng

[PersistentState(AllowUpdates = true)]
public WeatherForecast[]? Forecasts { get; set; }

Giờ khi bạn chuyển từ trang này sang trang khác, dữ liệu vẫn giữ nguyên.

2. Kiểm soát hành vi khôi phục

[PersistentState(AllowUpdates = true, RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public WeatherForecast[]? Forecasts { get; set; }

Các tuỳ chọn:

  • Default: Khôi phục tất cả dữ liệu
  • SkipInitialValue: Bỏ qua dữ liệu pre-render lần đầu
  • SkipLastSnapshot: Bỏ qua snapshot cuối cùng (luôn tải mới)

Khi nào dùng PersistentState?

  • 🛒 E-commerce: Phải trông chuyên nghiệp, không flash
  • 📊 Dashboard: Dữ liệu real-time cần ổn định
  • 📱 Blazor Hybrid: Kết hợp server + client mode
  • 📰 Blog/News: Dữ liệu tĩnh, cần SEO tốt

8. Khi app lớn hơn: Nên cân nhắc Fluxor

StateContainer + Generic như trên rất ổn cho app vừa và nhỏ. Nhưng khi dự án phình to, hàng chục state khác nhau, action phức tạp, thì nhiều team chuyển sang Fluxor — thư viện Flux/Redux cho .NET.

Fluxor hoạt động theo kiểu unidirectional data flow rõ ràng: Action → Reducer → State → UI.

Cài nhanh:

dotnet add package Fluxor.Blazor.Web

Ví dụ nhanh — define một counter feature:

// State
public record CounterState(int Count);

// Action
public record IncrementAction();

// Reducer
public static class CounterReducers
{
    [ReducerMethod]
    public static CounterState OnIncrement(CounterState state, IncrementAction _)
        => state with { Count = state.Count + 1 };
}

Dùng trong component:

@inject IState<CounterState> CounterState
@inject IDispatcher Dispatcher

<p>Count: @CounterState.Value.Count</p>

<button @onclick='() => Dispatcher.Dispatch(new IncrementAction())'>Click</button>

Ưu điểm lớn:

  • Code rất dễ test (không dính UI)
  • Predictable, dễ debug và trace lỗi
  • Hỗ trợ middleware, effect, persistence tốt
  • Ít boilerplate hơn so với Redux JS ngày xưa

Nếu bạn thấy StateContainer đang "lộn xộn" thì Fluxor là bước nâng cấp tự nhiên đấy.

⏪ 9. Undo / Redo

🧠 Ý tưởng: Dùng 2 stack (UndoRedo).

SetState → push undo, clear redo
Undo     → pop undo, push redo
Redo     → pop redo, push undo

Implementation skeleton:

public class UndoableState<TState> where TState : new()
{
    private TState _current = new();
    private readonly Stack<TState> _undoStack = new();
    private readonly Stack<TState> _redoStack = new();

    public TState Current => _current;

    public event Action? OnChange;

    public void SetState(TState newState)
    {
        _undoStack.Push(_current);
        _redoStack.Clear();
        _current = newState;
        OnChange?.Invoke();
    }

    public void Undo()
    {
        if (_undoStack.Count == 0) return;
        _redoStack.Push(_current);
        _current = _undoStack.Pop();
        OnChange?.Invoke();
    }

    public void Redo()
    {
        if (_redoStack.Count == 0) return;
        _undoStack.Push(_current);
        _current = _redoStack.Pop();
        OnChange?.Invoke();
    }
}

Ứng dụng: editor, game (cờ vua cực hợp), form phức tạp.

🧪 10. Testing (điểm mạnh lớn)

StateContainer không phụ thuộc UI nên test dễ:

var state = new CartState();
state.AddItem(new CartItem { Name = "Laptop", Price = 999 });

Assert.Equal(1, state.State.Items.Count);
Assert.Equal(999, state.State.TotalPrice);

⚠️ 11. Best Practices

✅ Nên:

  • Dùng immutable (record)
  • Expose read-only hoặc qua method
  • Unsubscribe event trong Dispose
  • Dùng InvokeAsync(StateHasChanged)
  • Nhớ thêm where TState : new() khi dùng Generic StateContainer
  • Dùng [PersistentState] khi có thể (.NET 10+)
  • Cân nhắc Fluxor cho app phức tạp

❌ Tránh:

  • Dùng static
  • Sửa state trực tiếp từ bên ngoài
  • Quên Dispose
  • Gọi StateHasChanged sai context

🏁 12. Kết luận

Blazor State đi theo hướng rất rõ:

User action
   ↓
Update state
   ↓
Notify
   ↓
UI re-render

Lộ trình học:

  • Level 1: State trong component
  • Level 2: StateContainer
  • Level 3: Generic + clean architecture
  • Level 3.5: [PersistentState] (gần như không tốn công)
  • Level 4: Fluxor hoặc Undo/Redo

👉 Nếu bạn đã từng làm ASP.NET: Blazor không "giả lập state" nữa mà cho bạn làm việc với state thật (object trong memory).

👉 Nếu bạn từng dùng React: Bạn đã hiểu 70% Blazor rồi.

🚀 Gợi ý tiếp theo: Thử áp dụng vào Todo App, Shopping Cart hoặc Chess Game. Bạn sẽ thấy StateContainer (và Fluxor) phát huy sức mạnh rất rõ.

Nhận xét

Bài đăng phổ biến từ blog này

[ASP.NET MVC] Authentication và Authorize

Một trong những vấn đề bảo mật cơ bản nhất là đảm bảo những người dùng hợp lệ truy cập vào hệ thống. ASP.NET đưa ra 2 khái niệm: Authentication và Authorize Authentication xác nhận bạn là ai. Ví dụ: Bạn có thể đăng nhập vào hệ thống bằng username và password hoặc bằng ssh. Authorization xác nhận những gì bạn có thể làm. Ví dụ: Bạn được phép truy cập vào website, đăng thông tin lên diễn đàn nhưng bạn không được phép truy cập vào trang mod và admin.

ASP.NET MVC: Cơ bản về Validation

Validation (chứng thực) là một tính năng quan trọng trong ASP.NET MVC và được phát triển trong một thời gian dài. Validation vắng mặt trong phiên bản đầu tiên của asp.net mvc và thật khó để tích hợp 1 framework validation của một bên thứ 3 vì không có khả năng mở rộng. ASP.NET MVC2 đã hỗ trợ framework validation do Microsoft phát triển, tên là Data Annotations. Và trong phiên bản 3, framework validation đã hỗ trợ tốt hơn việc xác thực phía máy khách, và đây là một xu hướng của việc phát triển ứng dụng web ngày nay.

Tổng hợp một số kiến thức lập trình về Amibroker

Giới thiệu về Amibroker Amibroker theo developer Tomasz Janeczko được xây dựng dựa trên ngôn ngữ C. Vì vậy bộ code Amibroker Formula Language sử dụng có syntax khá tương đồng với C, ví dụ như câu lệnh #include để import hay cách gói các object, hàm trong các block {} và kết thúc câu lệnh bằng dấu “;”. AFL trong Amibroker là ngôn ngữ xử lý mảng (an array processing language). Nó hoạt động dựa trên các mảng (các dòng/vector) số liệu, khá giống với cách hoạt động của spreadsheet trên excel.