مدیریت وضعیت (State Management) در Blazor
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۲۷ ۱۰:۱۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
Singleton رفتار میکنند، چرا که کل برنامه محدود به یک تب مرورگر است).localStorage یا sessionStorage).Scoped به ازای هر اتصال (Circuit) کاربر زنده میمانند.PersistentComponentState و ویژگی جدید ویژگیهای دکلراتیو برای هیدراته کردن (Hydration) خودکار دادهها استفاده میشود.@code)[Parameter] به همراه EventCallbackCascadingValue / CascadingParameter)[PersistentState] یا سرویس PersistentComponentStateProtectedLocalStorage یا ProtectedSessionStorage@page "/products/{Category}"
@inject NavigationManager NavManager
<h3>دسته بندی: @Category</h3>
<p>عبارت جستجو: @SearchTerm</p>
<input @bind="tempSearch" placeholder="جستجو..." />
<button @onclick="ApplySearch">اعمال فیلتر</button>
@code {
[Parameter] public string? Category { get; set; }
// اتصال خودکار به Query String با قابلیت تعامل کلاینت و سرور
[SupplyParameterFromQuery(Name = "searchTerm")]
public string? SearchTerm { get; set; }
private string? tempSearch;
protected override void OnParametersSet()
{
tempSearch = SearchTerm;
}
private void ApplySearch()
{
// بازسازی آدرس برای ثبت در تاریخچه مرورگر بدون از دست رفتن وضعیت
var targetUri = NavManager.GetUriWithQueryParameter("searchTerm", tempSearch);
NavManager.NavigateTo(targetUri);
}
}مزیت: هماهنگی کامل با دکمههای Back/Forward مرورگر و سئو (SEO) عالی در حالت SSR.
محدودیت: صرفاً برای دادههای ساده (رشته، عدد و تاریخ) مناسب است و برای اشیاء پیچیده کاربرد ندارد.
@code یک وضعیت محلی است که کپسولهسازی کامل را فراهم میکند.<button class="btn btn-primary" @onclick="ToggleDetails">
@(isExpanded ? "پنهانسازی جزئیات" : "نمایش جزئیات")
</button>
@if (isExpanded)
{
<div class="alert alert-info mt-2">این یک وضعیت کاملاً محلی است.</div>
}
@code {
private bool isExpanded; // State محلی
private void ToggleDetails() => isExpanded = !isExpanded;
}[Parameter] پایین میفرستد و فرزند با EventCallback رویدادها را بالا میفرستد.<ChildComponent Title="وضعیت فعلی شمارنده"
CurrentValue="parentCounter"
OnIncrement="HandleIncrement" />
@code {
private int parentCounter = 10;
private void HandleIncrement(int step) => parentCounter += step;
}<div class="card p-3">
<h5>@Title</h5>
<p>مقدار: @CurrentValue</p>
<button @onclick="() => OnIncrement.InvokeAsync(1)">افزایش در والد</button>
</div>
@code {
[Parameter] public string Title { get; set; } = string.Empty;
[Parameter] public int CurrentValue { get; set; }
[Parameter] public EventCallback<int> OnIncrement { get; set; }
}نکته معماری: اگر تعداد لایههای میانی درختواره زیاد شود، این الگو منجر به پدیدهی کثیف Prop Drilling (پاس دادن اجباری پارامترها از لایههایی که خودشان به آن نیاز ندارند) میشود. در چنین شرایطی باید به سراغ الگوی بعدی رفت.
CascadingValueSource استفاده کنید.<CascadingValue Value="currentTheme">
<div class="@currentTheme">
@Body
</div>
</CascadingValue>
@code {
private string currentTheme = "dark-theme";
}@code {
[CascadingParameter] public string? ActiveTheme { get; set; }
}AddScoped: در Blazor Server به ازای هر اتصال (هر کاربر/تب) یک نمونه مجزا میسازد (مناسب برای سبد خرید). در Blazor WebAssembly معادل کلاینت رفتار کرده و به ازای هر تب مرورگر تفکیک میشود.AddSingleton: در Blazor Server خطرناک است؛ چون یک نمونه واحد برای تمام کاربران سیستم به اشتراک گذاشته میشود (نشت شدید داده!). اما در Blazor WebAssembly امن است چون کل رانتایم ایزوله در مرورگر یک فرد است.// CounterStateContainer.cs
public class CounterStateContainer
{
private int _count;
public int Count
{
get => _count;
set
{
if (_count != value)
{
_count = value;
NotifyStateChanged();
}
}
}
// رویدادی که تغییر وضعیت را به گوش شنوندگان میرساند
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}@page "/reactive-demo"
@implements IDisposable
@inject CounterStateContainer StateContainer
<h3>مدیریت وضعیت واکنشگرا</h3>
<p>مقدار فعلی: <strong>@StateContainer.Count</strong></p>
<button class="btn btn-success" @onclick="Increment">افزایش وضعیت</button>
<div class="mt-3">
<NestedChildComponent />
</div>
@code {
protected override void OnInitialized()
{
// subscribe به رویداد تغییر برای بهروزرسانی UI کامپوننت
StateContainer.OnChange += StateHasChanged;
}
private void Increment() => StateContainer.Count++;
public void Dispose()
{
// جلوگیری از نشت حافظه (Memory Leak) با لغو اشتراک
StateContainer.OnChange -= StateHasChanged;
}
}OnInitializedAsync دو بار اجرا شود که نتیجه آن فلیکر (Flicker) یا چشمک زدن ناخوشایند UI و اجرای دوباره کوئریهای سنگین دیتابیس است.[PersistentState] این مشکل را به زیباترین شکل ممکن حل کرده است.@page "/weather"
@rendermode InteractiveWebAssembly
<h3>پیشبینی آب و هوا</h3>
@if (forecasts == null)
{
<p>در حال بارگذاری...</p>
}
else
{
}
@code {
// رانتایم دیتای این پراپرتی را در اولین رندر (سرور) سریالایز کرده
// و در رندر دوم (کلاینت) بدون اجرای مجدد منطق متد، بازگردانی میکند.
[PersistentState]
public WeatherForecast[]? forecasts { get; set; }
protected override async Task OnInitializedAsync()
{
if (forecasts == null)
{
// این بلاک فقط یکبار روی سرور اجرا میشود
forecasts = await WeatherService.GetForecastsAsync();
}
}
}RestoreBehaviorRestoreBehavior.SkipInitialValue: در رندر اولیه از حالت پیشرندر شده استفاده نمیکند.RestoreBehavior.SkipLastSnapshot: وضعیت ذخیره شده قبلی را در زمان Reconnectهای ناگهانی نادیده گرفته و به روز رسانی جدید انجام میدهد.ProtectedLocalStorage استفاده میشود. این سرویس دادهها را با استفاده از فرآیند ASP.NET Core Data Protection سمت سرور رمزنگاری کرده و سپس در مرورگر مینویسد.@inject ProtectedLocalStorage ProtectedStorage
<button @onclick="SavePreferences">ذخیره تم کاربری</button>
@code {
private async Task SavePreferences()
{
// دادهها قبل از ذخیره در لایه کلاینت به طور کامل و امن رمزنگاری میشوند
await ProtectedStorage.SetAsync("UserTheme", "Dark-Fluid");
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// دسترسی به جاوااسکریپت و حافظه مرورگر فقط پس از رندر کلاینت ممکن است
var result = await ProtectedStorage.GetAsync<string>("UserTheme");
if (result.Success)
{
var theme = result.Value;
// اعمال تم...
StateHasChanged();
}
}
}
}قانون طلایی امنیت: هرگز توکنهای JWT بسیار حساس یا اطلاعات هویتی کلیدی (PII) را بدون لایههای امنیتی تکمیلی درون localStorage کلاینت (به خصوص در WASM) ذخیره نکنید. حملات XSS میتوانند دسترسی به حافظه عادی مرورگر را به مخاطره اندازند.| ابزار مدیریت وضعیت | بهترین سناریوی کاربرد | طول عمر و قلمرو (Scope) | پایداری در برابر F5 (Refresh) |
| URL / Query String | فیلترها، سرچ، صفحهبندی صفحات | وابسته به آدرس مرورگر | بله (صد در صد پایدار و قابل اشتراک) |
| Local Component State | رفتارهای گرافیکی درون یک کامپوننت | طول عمر نمونه کامپوننت | خیر |
| Parameters & EventCallback | ارتباطات مستقیم بالا به پایین | محدود به لایه کامپوننت والد-فرزند | خیر |
| Cascading Values | تنظیمات و ویژگیهای ساختاری سراسری درختواره | زیردرخت مشخص شده | خیر |
| DI Services (Scoped) | دادههای مشترک صفحات (مانند سبد خرید) | طول عمر زنده بودن Session / Circuit | خیر (در رفرش سرور پاک میشود) |
[PersistentState] | انتقال دیتا از پیشرندر به حالت کلاینت | بازه زمانی Hydration اولیه | خیر (مختص جابجایی حالت رندر) |
| Protected Browser Storage | ترجیحات کاربر، تنظیمات امن هیدرتیشن | حافظه ماندگار مرورگر (رمزگذاری شده) | بله (کاملاً ماندگار) |
StateHasChanged() به خصوص در الگوهای واکنشگرا (State Containers)، میتواند کل زیردرخت کامپوننتها را وادار به رندر مجدد کند. این امر در برنامههای بزرگ باعث افزایش شدید مصرف پردازنده (CPU Spike) میشود. برای جلوگیری از این مشکل، تغییر وضعیتها را بهینهسازی کرده و متدها را به صورت هوشمند و تنها در صورت تغییر واقعی مقادیر صدا بزنید.InvokeAsync کپسوله کنید:await InvokeAsync(StateHasChanged);
ProtectedBrowserStorage از کلیدهای رمزنگاری سرور استفاده میکنند. در صورت ریاستارت شدن سرور یا توزیع برنامه روی کلاسترهای چندگانه (Web Farm)، اگر کلیدها را روی یک دیسک یا سیستم مشترک (مانند Redis یا Azure Key Vault) ماندگار نکرده باشید، کلیدها منقضی شده و دادههای ذخیره شده در مرورگر کاربران دیگر قابل رمزگشایی نخواهند بود.[PersistentState] بهره ببرید. AddRazorComponents در فایل Program.cs، مکانیزم TempData به صورت خودکار ثبت شده و به عنوان یک Cascading Value در دسترس کامپوننتها قرار میگیرد.Get پس از خواندن مقدار، آن را از حافظه پاک میکند (استفاده یکباره). واسط ITempData متدهای Peek و Keep را برای کنترل دقیقتر طول عمر داده ارائه میدهد.@page "/my-form"
@inject NavigationManager NavigationManager
<p class="alert alert-info">@_message</p>
<form @onsubmit="HandleSubmit" method="post">
<button type="submit" class="btn btn-primary">ثبت فرم</button>
</form>
@code {
[CascadingParameter]
public ITempData? TempData { get; set; }
private string? _message;
protected override void OnInitialized()
{
// خواندن مقدار و حذف خودکار آن از TempData برای درخواستهای بعدی
_message = TempData?.Get("Message") as string ?? "پیامی وجود ندارد";
}
private void HandleSubmit()
{
// ذخیره پیام موقت قبل از ریدایرکت
TempData!["Message"] = "فرم با موفقیت ثبت شد!";
NavigationManager.NavigateTo("/my-form", forceLoad: true);
}
}[SupplyParameterFromTempData][SupplyParameterFromTempData] را معرفی کرده است. این ویژگی دقیقاً از همان الگوی آشنای [SupplyParameterFromQuery] پیروی میکند.@page "/account/manage"
@if (!string.IsNullOrEmpty(StatusMessage))
{
<div class="alert alert-success">@StatusMessage</div>
}
@code {
// اتصال خودکار دوطرفه به TempData
[SupplyParameterFromTempData]
public string? StatusMessage { get; set; }
}Name مشخص کنید: [SupplyParameterFromTempData(Name = "CustomKey")][SupplyParameterFromSession][SupplyParameterFromSession] معرفی شده است تا ارتباط مستقیمی بین پراپرتیهای کامپوننت و HTTP Session برقرار کند.System.Text.Json سریالایز شده و درست قبل از ارسال پاسخ HTTP به کلاینت، درون Session مربوط به کاربر بازنویسی میشوند.builder.Services.AddDistributedMemoryCache(); // نیاز به حافظه برای ذخیره سشن builder.Services.AddSession(); builder.Services.AddRazorComponents(); var app = builder.Build(); app.UseSession(); // فعالسازی میانافزار سشن
@page "/checkout"
<div class="card p-4">
<p>مرحله فعلی خرید: <strong>@CurrentStep</strong></p>
<EditForm Model="Input" FormName="checkout" OnSubmit="NextStep">
<button type="submit" class="btn btn-success">مرحله بعدی</button>
</EditForm>
</div>
@code {
// خواندن و نوشتن مستقیم و خودکار در HTTP Session کاربر
[SupplyParameterFromSession(Name = "checkout-step")]
public int CurrentStep { get; set; }
private object Input { get; } = new();
private void NextStep()
{
// با افزایش این مقدار، تغییرات به طور خودکار در سشن ذخیره میشوند
CurrentStep++;
}
}| ابزار جدید | منبع ذخیرهسازی اصلی | طول عمر وضعیت (Lifetime) | بهترین سناریوی مصرف |
ITempData (مستقیم) | Cookie (رمزنگاری شده با Data Protection) | فقط تا درخواست HTTP بعدی (حذف پس از خواندن) | پیامهای فلش سفارشی و مدیریت منطقهای پیچیده حذف/حفظ داده |
[SupplyParameterFromTempData] | Cookie / TempData Provider | فقط تا درخواست HTTP بعدی (با مدیریت خودکار) | پیامهای وضعیت سیستم، هشدارها پس از عملیات ریدایرکت (POST-Redirect-GET) |
[SupplyParameterFromSession] | Server Session (Memory/Redis) | تا زمانی که Session کاربر زنده است (ماندگاری طولانیتر) | سبد خرید در حالت SSR، فرمهای چندمرحلهای پیچیده، حفظ فیلترهای پیشرفته کاربر |
DistributedMemoryCache استفاده میکند که دیتای سشن را در حافظه همان سرور نگه میدارد. اگر برنامهی Blazor SSR خود را روی چند سرور یا داکر پاد (Pod) مختلف مستقر میکنید، حتماً از SessionStorageTempDataProvider یا یک کش توزیعشده مانند Redis استفاده کنید تا کاربران با پدیده از دست رفتن سشن در اثر جابجایی بین سرورها مواجه نشوند.