در این قسمت میخواهیم انواع رویدادهای چرخهی حیات یک کامپوننت را بررسی کنیم. به همین جهت ابتدا دو کامپوننت جدید Lifecycle.razor و LifecycleChild.razor را به
مثالی که تا این قسمت تکمیل کردهایم، اضافه کرده و آنهارا به صورت زیر جهت نمایش رویدادهای چرخهی حیات، تغییر میدهیم:
کدهای کامل کامپوننت Pages\LearnBlazor\Lifecycle.razor @page "/lifecycle"
@using System.Threading
<div class="border">
<h3>Lifecycles Parent Component</h3>
<div class="border">
<LifecycleChild CountValue="CurrentCount"></LifecycleChild>
</div>
<p>Current count: @CurrentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<br /><br />
<button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount
</div>
@code
{
int CurrentCount = 0;
int MaxCount = 5;
private void IncrementCount()
{
CurrentCount++;
Console.WriteLine("Parnet - IncrementCount is called");
}
protected override void OnInitialized()
{
Console.WriteLine("Parnet - OnInitialized is called");
}
protected override async Task OnInitializedAsync()
{
await Task.Delay(100);
Console.WriteLine("Parnet - OnInitializedAsync is called");
}
protected override void OnParametersSet()
{
Console.WriteLine("Parnet - OnParameterSet is called");
}
protected override async Task OnParametersSetAsync()
{
await Task.Delay(100);
Console.WriteLine("Parnet - OnParametersSetAsync is called");
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
CurrentCount = 111;
}
else
{
CurrentCount = 999;
Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await Task.Delay(100);
Console.WriteLine("Parnet - OnAfterRenderAsync is called");
}
protected override bool ShouldRender()
{
Console.WriteLine("Parnet - ShouldRender is called");
return true;
}
void StartCountdown()
{
Console.WriteLine("Parnet - StartCountdown()");
var timer = new Timer(TimeCallBack, null, 1000, 1000);
}
void TimeCallBack(object state)
{
if (MaxCount > 0)
{
MaxCount--;
Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
InvokeAsync(StateHasChanged);
}
}
}
و کدهای کامل کامپوننت Pages\LearnBlazor\LearnBlazorComponents\LifecycleChild.razor <h3 class="ml-3 mr-3">Lifecycles Child Componenet</h3>
@code
{
[Parameter]
public int CountValue { get; set; }
protected override void OnInitialized()
{
Console.WriteLine(" Child - OnInitialized is called");
}
protected override async Task OnInitializedAsync()
{
await Task.Delay(100);
Console.WriteLine(" Child - OnInitializedAsync is called");
}
protected override void OnParametersSet()
{
Console.WriteLine(" Child - OnParameterSet is called");
}
protected override async Task OnParametersSetAsync()
{
await Task.Delay(100);
Console.WriteLine(" Child - OnParametersSetAsync is called");
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Console.WriteLine(" Child - OnAfterRender(firstRender == true) is called");
}
else
{
Console.WriteLine(" Child - OnAfterRender(firstRender == false) is called");
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await Task.Delay(100);
Console.WriteLine(" Child - OnAfterRenderAsync is called");
}
protected override bool ShouldRender()
{
Console.WriteLine(" Child - ShouldRender is called");
return true;
}
}
و همچنین برای دسترسی به آنها، مدخل منوی زیر را به کامپوننت Shared\NavMenu.razor اضافه میکنیم:
<li class="nav-item px-3">
<NavLink class="nav-link" href="lifecycle">
<span class="oi oi-list-rich" aria-hidden="true"></span> Lifecycles
</NavLink>
</li>
با توجه به اینکه برنامهی جاری از نوع Blazor Server است، Console.WriteLineهای آن، در صفحهی کنسول اجرای برنامه ظاهر میشوند و نه در developer tools مرورگر:
رویدادهای OnInitialized و OnInitializedAsync @code
{
protected override void OnInitialized()
{
Console.WriteLine("Parnet - OnInitialized is called");
}
protected override async Task OnInitializedAsync()
{
await Task.Delay(100);
Console.WriteLine("Parnet - OnInitializedAsync is called");
}
همانطور که در تصویر فوق نیز ملاحظه میکنید، اولین رویدادی که فراخوانی میشود، OnInitialized نام دارد و پس از آن نمونهی async آن به نام OnInitializedAsync. این رویدادها زمانیکه یک کامپوننت و اجزای UI آن کاملا بارگذاری شدهاند، فراخوانی میشوند. مهمترین کاربرد آنها، دریافت اطلاعات از سرویسهای برنامهاست.
در کامپوننت Lifecycle.razor، یک کامپوننت دیگر نیز به نام LifecycleChild.razor فراخوانی شدهاست. در این حالت ابتدا OnInitialized کامپوننت والد فراخوانی شدهاست و پس از آن بلافاصله فراخوانی OnInitialized کامپوننت فرزند را مشاهده میکنیم.
رویدادهای OnParametersSet و OnParametersSetAsync
این رویدادها یکبار در زمان بارگذاری اولیهی کامپوننت و بار دیگر هر زمانیکه کامپوننت فرزند، پارامتر جدیدی را از طریق کامپوننت والد دریافت میکند، فراخوانی میشوند. برای نمونه کامپوننت LifecycleChild، پارامتر CurrentCount را از والد خود دریافت میکند:
<LifecycleChild CountValue="CurrentCount"></LifecycleChild>
هرچند این پارامتر در UI کامپوننت فرزند مثال تهیه شده استفاده نمیشود، اما مقدار دهی آن از طرف والد، سبب بروز رویدادهای OnParametersSet و OnParametersSetAsync خواهد شد. برای آزمایش آن اگر بر روی دکمهی click me در کامپوننت والد کلیک کنیم، این رویدادهای جدید را مشاهده خواهیم کرد:
Parnet - IncrementCount is called
Parnet - ShouldRender is called
Child - OnParameterSet is called
Child - ShouldRender is called
Parnet - OnAfterRender(firstRender == false) is called
Child - OnAfterRender(firstRender == false) is called
Child - OnParametersSetAsync is called
Child - ShouldRender is called
Child - OnAfterRender(firstRender == false) is called
Child - OnAfterRenderAsync is called
Parnet - OnAfterRenderAsync is called
Child - OnAfterRenderAsync is called
ابتدا متد IncrementCount کامپوننت والد فراخوانی شدهاست که سبب تغییر مقدار پارامتر CurrentCount ارسالی به کامپوننت LifecycleChild میشود و پس از آن، رویداد OnParameterSet کامپوننت فرزند را مشاهده میکنید که عکس العملی است به این تغییر مقدار. یکی از کاربردهای آن، دریافت مقدار جدید پارامترهای کامپوننت و سپس به روز رسانی قسمت خاصی از UI بر اساس آنها است.
رویدادهای OnAfterRender و OnAfterRenderAsync
پس از هر بار رندر کامپوننت، این متدها فراخوانی میشوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آنها به پایان رسیدهاست. یکی از کاربردهای آن، آغاز کامپوننتهای جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.
یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمیشوند؛ چون در مرحلهی آخر رندر UI قرار دارند.
@code
{
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
CurrentCount = 111;
}
else
{
CurrentCount = 999;
Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await Task.Delay(100);
Console.WriteLine("Parnet - OnAfterRenderAsync is called");
}
}
در مثالهای فوق، پارامتر firstRender را نیز مشاهده میکنید. یک کامپوننت چندین بار میتواند رندر شود. برای مثال هربار که توسط رویدادگردانی مقدار فیلدی را که در UI استفاده میشود، تغییر دهیم، کامپوننت مجددا رندر میشود. برای نمونه با کلیک بر روی دکمهی click me، سبب تغییر مقدار فیلد CurrentCount میشویم. این تغییر و فراخوانی ضمنی StateHasChanged در پایان کار متد و در پشت صحنه، سبب رندر مجدد UI شده و در نتیجهی آن، مقدار جدیدی را در صفحه مشاهده میکنیم. در اینجا اگر خواستیم بدانیم که رندر انجام شده برای بار اول است که صورت میگیرد یا خیر، میتوان از پارامتر firstRender استفاده کرد.
سؤال: با توجه به مقدار دهیهای 111 و 999 صورت گرفتهی در متد OnAfterRender، در اولین بار نمایش کامپوننت، چه عددی به عنوان CurrentCount نمایش داده میشود؟
در اولین بار نمایش صفحه، لحظهای عدد 111 و سپس عدد 999 نمایش داده میشود. عدد 111 را در بار اول رندر و عدد 999 را در بار دوم رندر که پس از مقدار دهی پارامتر کامپوننت فرزند است، میتوان مشاهده کرد.
اما ... اگر پس از نمایش اولیهی صفحه، چندین بار بر روی دکمهی click me کلیک کنیم، همواره عدد 1000 مشاهده میشود. علت اینجا است که تغییرات مقادیر فیلدها در متد OnAfterRender، به UI اعمال نمیشوند؛ چون در این مرحله، رندر UI به پایان رسیدهاست. در اینجا فقط مقدار فیلد CurrentCount به 999 تغییر میکند و به همین صورت باقی میماند. دفعهی بعدی که بر روی دکمهی click me کلیک میکنیم، یک واحد به آن اضافه شده و اکنون است که کار رندر UI، مجددا شروع خواهد شد (در واکشن به یک رخداد و فراخوانی ضمنی StateHasChanged در پشت صحنه) و اینبار حاصل 999+1 را در UI مشاهده میکنیم و باز هم در پایان کار رندر، مجددا مقدار CurrentCount به 999 تغییر میکند که ... دیگر به UI منعکس نمیشود تا زمان کلیک بعدی و همینطور الی آخر.
رویدادهای StateHasChanged و ShouldRender
- اگر خروجی رویداد ShouldRender مساوی true باشد، اجازهی اعمال تغییرات به UI داده خواهد شد و برعکس. بنابراین اگر حالت UI تغییر کند و خروجی این متد false باشد، این تغییرات نمایش داده نخواهند شد.
- اگر رویداد StateHasChanged فراخوانی شود، به معنای درخواست رندر مجدد UI است. کاربرد آن در مکانهایی است که نیاز به اطلاع رسانی دستی تغییرات UI وجود دارد؛ درست پس از زمانیکه رندر UI به پایان رسیدهاست. برای آزمایش این مورد و فراخوانی دستی StateHasChanged، کدهای تایمر زیر تهیه شدهاند:
@page "/lifecycle"
@using System.Threading
button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount
@code
{
int MaxCount = 5;
void StartCountdown()
{
Console.WriteLine("Parnet - StartCountdown()");
var timer = new Timer(TimeCallBack, null, 1000, 1000);
}
void TimeCallBack(object state)
{
if (MaxCount > 0)
{
MaxCount--;
Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
InvokeAsync(StateHasChanged);
}
}
}
تایمر تعریف شده، یک thread timer است. یعنی callback آن بر روی یک ترد جدید و مجزای از ترد UI اجرا میشود. در این حالت اگر StateHasChanged را جهت اطلاع رسانی تغییر حالت UI فراخوانی نکنیم، در حین کار تایمر، هیچ تغییری را در UI مشاهده نخواهیم کرد.
یک نکته: متدهای رویدادگردان در Blazor، میتوانند sync و یا async باشند؛ مانند متدهای OnClick و OnClickAsync زیر که هر دو پس از پایان متدها، سبب فراخوانی ضمنی StateHasChanged نیز میشوند (به این دلیل است که با کلیک بر روی دکمهای، UI هم به روز رسانی میشود). البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged میشوند؛ یکبار زمانیکه قسمت sync متد به پایان میرسد و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید:
<button @onclick="OnClick">Synchronous</button>
<button @onclick="OnClickAsync">Asynchronous</button>
@code{
void OnClick()
{
} // StateHasChanged is called after the method
async Task OnClickAsync()
{
text = "click1";
// StateHasChanged is called here as the synchronous part of the method ends
await Task.Delay(1000);
await Task.Delay(2000);
text = "click2";
} // StateHasChanged is called after the method
}
بنابراین یکی دیگر از دلایل نیاز به فراخوانی صریح InvokeAsync(StateHasChanged) در callback تایمر تعریف شده، عدم فراخوانی خودکار آن، در پایان کار رویداد callback تایمر است.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-06.zip