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 để nghe thay đổi.

🧬 6. Nâng cấp: Generic StateContainer

Khi app lớn, bạn sẽ có UserState, CartState, GameState… Tránh lặp code bằng base class:

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;
        OnChange?.Invoke();
    }

    protected void UpdateState(Func<TState, TState> update)
    {
        _state = update(_state);
        OnChange?.Invoke();
    }
}
⚠️ Lưu ý constraint where TState : new() — bắt buộc phải có, không thì compiler sẽ báo lỗi ngay vì new() trong generic cần guarantee TState có parameterless constructor.

Ví dụ:

public record UserStateData
{
    public string? Username { get; init; }
    public bool IsAuthenticated { get; init; }
}

public class UserState : StateContainerBase<UserStateData>
{
    public void Login(string username)
    {
        SetState(new UserStateData
        {
            Username = username,
            IsAuthenticated = true
        });
    }
}

💾 7. Persistence – Giữ state sau khi refresh

❗ Vấn đề cũ: Refresh trang là… reset hết, giỏ hàng bay màu, counter về 0, user phải login lại. Đau vl 🍜.

✅ Từ .NET 10, Blazor có attribute [PersistentState] — cực kỳ gọn:

@code {
    [PersistentState]
    public int Count { get; set; } = 0;

    void Increment() => Count++;
}

Chỉ vậy thôi. Refresh xong vẫn giữ nguyên số đếm. Blazor tự lo phần serialize/deserialize, không cần đụng vào localStorage hay sessionStorage bằng tay.

📌 [PersistentState] là feature mới từ .NET 10. Nếu bạn đang dùng .NET 9 trở xuống thì phải làm thủ công hơn qua PersistentComponentState hoặc thư viện Blazored.LocalStorage.

🧩 8. Khi app to 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.