نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
یک نکته‌ی تکمیلی: روش ارسال یک المان razor به کدهای جاوا اسکریپتی
فرض کنید قصد داریم قطعه کد جاوا اسکریپتی زیر را که یک المان را جهت هدایت فوکوس به آن، نیاز دارد، در کدهای #C فراخوانی کنیم:
window.setFocus = function (element) {
  element.focus();
};
برای مقدار دهی این element در کدهای #C باید به صورت زیر عمل کرد:
الف) در اینجا نیز از ref@ برای دسترسی به المان استفاده خواهیم کرد:
<input @ref="@ReferenceToInputControl" />
در این حالت input ای که توسط Blazor رندر می‌شود، چنین شکلی را پیدا می‌کند:
<input _bl_bc0f34fa-16bd-4687-a8eb-9e3838b5170d="">
هدف این است که بدون دستکاری Id المان، بتواند آن‌را به صورت منحصربفردی مشخص کند.

ب) سپس شیء ReferenceToInputControl را در کدهای کامپوننت به صورت زیر تعریف می‌کنیم:
private ElementReference ReferenceToInputControl;
که قابلیت ارسال به کدهای جاوا اسکریپتی را دارد:
await JSRuntime.InvokeVoidAsync("setFocus", ReferenceToInputControl);
در این قطعه کد، ReferenceToInputControl همان element ای است که تابع setFocus تعریف شده نیاز دارد.

توجه! اگر قصد دارید قطعه کد #C فوق را در روال‌های رویدادگردان چرخه‌ی حیات کامپوننت فراخوانی کنید، اینکار باید در OnAfterRender صورت گیرد؛ چون پیش از آن ref@ هنوز تشکیل نشده‌است.
مطالب
استفاده از لنگر (anchor) برای اسکرول به قسمت خاصی از صفحه در Blazor Server
فرض کنید کدی مانند زیر را در یک کامپوننت داریم و انتظار این است که با کلیک بر روی Section2، به بخش مورد نظر اسکرول شویم:
@page "/test"

<nav>
    <!-- یک روش -->
    <a href="#section2">Section2</a>

    <!-- روش دیگر -->
    <NavLink href="#section2">Section2</NavLink>    
</nav>

@* ... *@


<h2 id="section2">It's Section2.</h2>
@* ... *@
اما متاسفانه در Blazor Server تا نسخه فعلی آن (نسخه هفت)، این کار ساده به راحتی امکان‌پذیر نیست. همانطور که ملاحظه می‌کنید، به دو روش، نویگیشن انجام شده‌است؛ اما هیچ‌یک ما را به هدف نمی‌رسانند. دلیل این موضوع، رفتار Blazor Server در بارگذاری صفحات می‌باشد. در حقیقت المان‌ها موقع بارگذاری، هنوز در صفحه وجود ندارند. در واقع ابتدا نیاز است که اتصال SignalR برقرار شود و سپس داده‌ها از سرور دریافت شوند (مگر در حالت pre-rendered که مشکلات خاص خود را در پی دارد).
برای انجام این کار دو روش وجود دارد؛ یکی بر پایه‌ی جاوااسکریپت است و دیگری توسط توابع داخلی Blazor JS.


روش جاوااسکریپتی

ابتدا یک کامپوننت را به نام AnchorNavigation ایجاد می‌نماییم:
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@implements IDisposable
@code {
    protected override void OnInitialized()
    {
        NavigationManager.LocationChanged += OnLocationChanged;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await ScrollToFragment();
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }

    private async void OnLocationChanged(object sender, LocationChangedEventArgs e)
    {
        await ScrollToFragment();
    }

    private async Task ScrollToFragment()
    {
        var uri = new Uri(NavigationManager.Uri, UriKind.Absolute);
        var fragment = uri.Fragment;
        if (fragment.StartsWith('#'))
        {
            // Handle text fragment (https://example.org/#test:~:text=foo)
            // https://github.com/WICG/scroll-to-text-fragment/
            var elementId = fragment.Substring(1);
            var index = elementId.IndexOf(":~:", StringComparison.Ordinal);
            if (index > 0)
            {
                elementId = elementId.Substring(0, index);
            }

            if (!string.IsNullOrEmpty(elementId))
            {
                await JSRuntime.InvokeVoidAsync("BlazorScrollToId", elementId);
            }
        }
    }
}
سپس کد جاوا اسکریپتی زیر را در جایی قبل از فراخوانی <script src="_framework/blazor.server.js"></script> قرار می‌دهیم (برای مثال اگر می‌خواهیم در اکثر صفحات از آن بهره ببریم، آن را در layout.cshtmlـ قرار می‌دهیم).
function BlazorScrollToId(id) {
            const element = document.getElementById(id);
            if (element instanceof HTMLElement) {
                element.scrollIntoView({
                    behavior: "smooth",
                    block: "start",
                    inline: "nearest"
                });
            }
        }
حال در هر کامپوننتی که نیاز به استفاده از لنگر (anchor) داریم، به شکل زیر عمل می‌کنیم:
@page "/"

<PageTitle>Index</PageTitle>

<a href="#section2">
    <h1>Section2</h1>
</a>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<div id="section2">
    <h2>It's Section2. </h2>
</div>

<AnchorNavigation />


روش استفاده از توابع داخلی Blazor JS 

می توان از ElementReference و FocusAsync که در حقیقت مربوط به خود Blazor JS می‌باشند استفاده نمود. اینبار کدهای کامپوننت AnchorNavigation را به شکل زیر تغییر می‌دهیم:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using System.Diagnostics.CodeAnalysis;

namespace TestAnchorNavigation;

public class AnchorNavigation: ComponentBase, IDisposable
{
    private bool _setFocus;

    [Inject] private NavigationManager NavManager { get; set; } = default!;
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter] public string? BookmarkName { get; set; }
    [DisallowNull] public ElementReference? Element { get; private set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(0, "span");
        builder.AddAttribute(2, "tabindex", "-1");
        builder.AddContent(3, this.ChildContent);
        builder.AddElementReferenceCapture(4, this.SetReference);
        builder.CloseElement();
    }

    protected override void OnInitialized()
        => NavManager.LocationChanged += this.OnLocationChanged;

    protected override void OnParametersSet()
        => _setFocus = this.IsMe();

    private void SetReference(ElementReference reference)
        => this.Element = reference;

    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        if (this.IsMe())
        {
            _setFocus = true;
            this.StateHasChanged();
        }
    }

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (_setFocus)
            await this.Element!.Value.FocusAsync(false);

        _setFocus = false;
    }

    private bool IsMe()
    {
        string? elementId = null;

        var uri = new Uri(this.NavManager.Uri, UriKind.Absolute);
        if (uri.Fragment.StartsWith('#'))
        {
            elementId = uri.Fragment.Substring(1);
            return elementId == BookmarkName;
        }
        return false;
    }

    public void Dispose()
        => NavManager.LocationChanged -= this.OnLocationChanged;
}
و پیرو آن، صفحه‌ی موردنظر برای استفاده از لنگر نیز به شکل زیر تغییر خواهد کرد:
@page "/"

<PageTitle>Index</PageTitle>

<NavLink href="#section2">
    <h1>Section2</h1>
</NavLink>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<AnchorNavigation BookmarkName="section2">
      <h2>It's Section2. </h2>
</AnchorNavigation>
مطالب
Blazor 5x - قسمت هشتم - مبانی Blazor - بخش 5 - تامین محتوای نمایشی کامپوننت‌های فرزند توسط کامپوننت والد
تا اینجا با نحوه‌ی تعریف کامپوننت‌ها و انتقال اطلاعات به آن‌ها و برعکس، آشنا شدیم. در ادامه علاقمندیم اگر کامپوننتی را به صورت زیر تعریف کردیم:
<Comp1>This is a content coming from the parent</Comp1>
محتوایی را که بین تگ‌های این کامپوننت فرزند، توسط کامپوننت والد تنظیم شده‌است، در قسمت مشخصی از UI کامپوننت فرزند نمایش دهیم.


معرفی مفهوم Render Fragment

برای درج محتوای تامین شده‌ی توسط کامپوننت والد در یک کامپوننت فرزند، از ویژگی به نام Render Fragment استفاده می‌شود. مثالی جهت توضیح جزئیات آن:
در ابتدا یک کامپوننت والد جدید را در مسیر Pages\LearnBlazor\ParentComponent.razor به صورت زیر تعریف می‌کنیم:
@page "/ParentComponent"

<h1 class="text-danger">Parent Child Component</h1>

<ChildComponent Title="This title is passed as a parameter from the Parent Component">
    A `Render Fragment` from the parent!
</ChildComponent>

<ChildComponent Title="This is the second child component"></ChildComponent>

@code {

}
- در اینجا یک کامپوننت متداول با مسیریابی منتهی به ParentComponent/ تعریف شده‌است.
- سپس دوبار کامپوننت فرضی ChildComponent به همراه پارامتر Title و یک محتوای جدید قرار گرفته‌ی در بین تگ‌های آن، در صفحه تعریف شده‌اند.
- بار دومی که ChildComponent در صفحه قرار گرفته‌است، به همراه محتوای جدیدی در بین تگ‌های خود نیست.

برای دسترسی به این کامپوننت از طریق منوی برنامه، مدخل منوی آن‌را به کامپوننت Shared\NavMenu.razor اضافه می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="ParentComponent">
       <span class="oi oi-list-rich" aria-hidden="true"></span> Parent/Child Relation
    </NavLink>
</li>
اکنون می‌خواهیم کامپوننت ChildComponent را افزوده و انتظارت یاد شده (دریافت یک پارامتر و نمایش محتوای سفارشی تنظیم شده) را پیاده سازی کنیم. به همین جهت فایل جدید Pages\LearnBlazor\LearnBlazor‍Components\ChildComponent.razor را با محتوای زیر ایجاد می‌کنیم:
<div>
    <div class="alert alert-info">@Title</div>
    <div class="alert alert-success">
        @if (ChildContent == null)
        {
            <span> Hello, from Empty Render Fragment </span>
        }
        else
        {
            <span>@ChildContent</span>
        }
    </div>
</div>


@code {
    [Parameter]
    public string Title { get; set; }

    [Parameter]
    public RenderFragment ChildContent { get; set; }
}
توضیحات:

- خاصیت عمومی Title که توسط ویژگی Parameter مزین شده‌است، امکان تنظیم مقدار مشخصی را توسط کامپوننت دربرگیرنده‌ی ChildComponent میسر می‌کند.
- در اینجا پارامتر عمومی دیگری نیز تعریف شده‌است که اینبار از نوع ویژه‌ی RenderFragment است. توسط آن می‌توان به محتوایی که در کامپوننت والد ChildComponent در بین تگ‌های آن تنظیم شده‌است، دسترسی یافت. همچنین اگر این محتوا توسط کامپوننت والد تنظیم نشده باشد، مانند دومین باری که ChildComponent در صفحه قرار گرفته‌است، می‌توان با بررسی نال بودن آن، یک محتوای پیش‌فرض را نمایش داد.


با این خروجی:



روش دیگری برای فراخوانی Event Call Back ها

در قسمت قبل روش انتقال اطلاعات را از کامپوننت‌های فرزند، به والد مشاهده کردیم. فراخوانی آن‌ها در سمت Child Component نیاز به یک متد اضافی داشت و همچنین تنها یک پارامتر را هم ارسال کردیم. برای ساده سازی این عملیات از روش زیر نیز می‌توان استفاده کرد:
<button class="btn btn-danger"
        @onclick="@(() => OnClickBtnMethod.InvokeAsync((1, "A message from child!")))">
   Show a message from the child!
</button>


@code {
    // ...

    [Parameter]
    public EventCallback<(int, string)> OnClickBtnMethod { get; set; }
}
- در اینجا برای تعریف و ارسال بیش از یک پارامتر، از tuples استفاده شده‌است.
- همچنین فراخوانی OnClickBtnMethod.InvokeAsync را نیز در محل تعریف onclick@ بدون نیازی به یک متد اضافی، مشاهده می‌کنید. نکته‌ی مهم آن، قرار دادن این قطعه کد داخل ()@ است تا ابتدا و انتهای کدهای #C مشخص شود؛ وگرنه کامپایل نمی‌شود.

در سمت کامپوننت والد برای دسترسی به OnClickBtnMethod که اینبار یک tuple را ارسال می‌کند، می‌توان به صورت زیر عمل کرد:
@page "/ParentComponent"

<h1 class="text-danger">Parent Child Component</h1>

<ChildComponent
    OnClickBtnMethod="ShowMessage"
    Title="This title is passed as a parameter from the Parent Component">
    A `Render Fragment` from the parent!
</ChildComponent>

<ChildComponent Title="This is the second child component">
    <p><b>@MessageText</b></p>
</ChildComponent>

@code {
    string MessageText = "";

    private void ShowMessage((int Value, string Message) args)
    {
        MessageText = args.Message;
    }
}
در اینجا OnClickBtnMethod تعریف شده، به متد ShowMessage متصل شده‌است که یک tuple از جنس (int, string) را دریافت می‌کند. سپس با انتساب خاصیت رشته‌ای آن به فیلد جدید MessageText، سبب نمایش آن می‌شویم.


امکان تعریف چندین RenderFragment

تا اینجا یک RenderFragment را در کامپوننت فرزند تعریف کردیم. امکان تعریف چندین RenderFragment در ChildComponent.razor نیز وجود دارند:
@code {
    // ...

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public RenderFragment DangerChildContent { get; set; }
}
در یک چنین حالتی، روش استفاده‌ی از این پارامترهای RenderFragment در سمت والد (ParentComponent.razor) به صورت زیر خواهد بود:
<ChildComponent
    OnClickBtnMethod="ShowMessage"
    Title="This title is passed as a parameter from the Parent Component">
    <ChildContent>
        A `Render Fragment` from the parent!
    </ChildContent>
    <DangerChildContent>
        A danger content from the parent!
    </DangerChildContent>
</ChildComponent>
که به همراه ذکر صریح نام پارامترها به صورت تگ‌هایی جدید، داخل تگ اصلی است.

از آنجائیکه ذکر این تگ‌ها اختیاری است، نیاز است در ChildComponent.razor بر اساس null بودن آن‌ها، تصمیم به رندر محتوایی پیش‌فرض گرفت:
    @if(DangerChildContent == null)
    {
        @if (ChildContent == null)
        {
            <span> Hello, from Empty Render Fragment </span>
        }
        else
        {
            <span>@ChildContent</span>
        }
    }
    else
    {
        <span>@DangerChildContent</span>
    }




کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-08.zip
مطالب
نمایش بلادرنگ اعلامی به تمام کاربران در هنگام درج یک رکورد جدید به صورت notification
در ادامه می‌خواهیم مثالی را که در این مطلب مورد بررسی قرار گرفت، به صورتی تغییر دهیم که با ثبت یک آیتم جدید درون دیتابیس، یک notification، به تمامی کاربران متصل به هاب ارسال شود. همچنین با کلیک بر روی Notification سطر جدید نیز بلافاصله نمایش داده شود:

در این مثال برای نمایش پیام به صورت notification، از کتابخانه toastr استفاده می‌کنیم که از طریق nuget می‌توانید آن را به پروژه اضافه کنید:
PM> Install-Package toastr
کار با این کتابخانه خیلی ساده است؛ کافی است فایل‌های js و css آن را به فایل layout اضافه کرده و به این صورت از آن استفاده کنیم:
toastr.info("نمایش یک پیام - info");
toastr.success("نمایش یک پیام - success");
toastr.error("نمایش یک پیام - error");
toastr.warning("نمایش یک پیام - warning");
دستورات فوق خروجی‌های زیر را نمایش می‌دهد:

برای پیام‌های فوق نیز می‌توانید عنوانی را انتخاب کنید:
toastr.success("نمایش یک پیام - success", "عنوان");
اگر به فایل js این کتابخانه مراجعه کنید، می‌توانید مقادیر پیش‌فرض آن را برای نمایش یک پیام مشاهده کنید. برای سفارشی‌سازی آن نیز می‌توانید به این صورت عمل کنید:
toastr.options = {
            tapToDismiss: true,
            toastClass: 'toast',
            containerId: 'toast-container',
            debug: false,

            showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery
            showDuration: 300,
            showEasing: 'swing', //swing and linear are built into jQuery
            onShown: undefined,
            hideMethod: 'fadeOut',
            hideDuration: 1000,
            hideEasing: 'swing',
            onHidden: undefined,

            extendedTimeOut: 1000,
            iconClasses: {
                error: 'toast-error',
                info: 'toast-info',
                success: 'toast-success',
                warning: 'toast-warning'
            },
            iconClass: 'toast-info',
            positionClass: 'toast-top-right',
            timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky
            titleClass: 'toast-title',
            messageClass: 'toast-message',
            target: 'body',
            closeHtml: '<button>&times;</button>',
            newestOnTop: true,
            preventDuplicates: false,
            progressBar: false
};
اکنون برای نمایش این نوع پیام‌ها در زمان اتصال به هاب (در واقع در زمان ثیت یک رکورد جدید) نیاز به ارسال پارامتر خاصی به سرور (از سمت کلاینت) نمی‌باشد. تنها باید کدهای سمت سرور یعنی هاب را به گونه‌ایی تغییر دهیم تا به محض فراخوانی SendNotification، آخرین رکورد ثبت شده در دیتابیس را به تمامی کلاینت‌های متصل به هاب ارسال کند:
public class NotificationHub : Hub
{
        private readonly IProductService _productService;

        public NotificationHub(IProductService productService)
        {
            _productService = productService;
        }

        public void SendNotification()
        {
            Clients.Others.ShowNotification(_productService.GetLastProduct());
        }
}
در سمت کلاینت نیز کدها همانند مثال قبل هستند؛ با این تفاوت که در متد سمت کلاینت باید اطلاعات ارسال شده از سمت سرور را با نمایش یک notification به کاربران اطلاع دهیم:
var notify = $.connection.notificationHub;
notify.client.showNotification = function (data) {
toastr.info("رکورد جدیدی ثبت گردید جهت نمایش اینجا کلیک کنید");
};
$.connection.hub.start().done(function () {
            @{
                if (ViewBag.NotifyUsers)
                {
                    <text>notify.server.sendNotification();</text>
                }
            }
});
تا اینجا همانند مثال قبلی عمل کردیم. یعنی به جای نمایش یک alert بوت‌استرپ، از کتابخانه toastr استفاده گردید. در مثال قبلی کاربر برای دیدن تغییرات می‌بایستی یکبار صفحه را ریفرش کند، اکنون می‌خواهیم کاربر بعد از کلیک بر روی پیام، بلافاصله سطر جدید را نیز مشاهده کند:
var positionClasses = {
            topRight: 'toast-top-right',
            bottomRight: 'toast-bottom-right',
            bottomLeft: 'toast-bottom-left',
            topLeft: 'toast-top-left',
            topCenter: 'toast-top-center',
            bottomCenter: 'toast-bottom-center'
        };
        var notify = $.connection.notificationHub;
        notify.client.showNotification = function (data) {
            toastr.options = {
                showDuration: 300,
                positionClass: positionClasses.bottomRight,
                onclick: function () {
                    $('#table tr:last').after("<tr>" +
                    "<td>" + data.Title + "</td>" +
                    "<td>" + data.Description + "</td>" +
                    "<td>" + data.Price + "</td>" +
                    "<td>" + data.Category + "</td>" +
                    "<td>  </td>" +
                    "</tr>");

                }
            };
            toastr.info("رکورد جدیدی ثبت گردید جهت نمایش اینجا کلیک کنید");
            
            
        };
        $.connection.hub.start().done(function () {
            @{
                if (ViewBag.NotifyUsers)
                {
                    <text>notify.server.sendNotification();</text>
                }
            }
});
همانطور که مشاهده می‌کنید از onClick برای toastr استفاده کرده‌ایم. با این callback گفته‌ایم که اگر بر روی پیام کلیک شد، اطلاعات را به صورت یک سطر جدید به جدول اضافه کن:
onclick: function () {
                    $('#table tr:last').after("<tr>" +
                    "<td>" + data.Title + "</td>" +
                    "<td>" + data.Description + "</td>" +
                    "<td>" + data.Price + "</td>" +
                    "<td>" + data.Category + "</td>" +
                    "<td>  </td>" +
                    "</tr>");

}
مقادیر به صورت یک شیء جاوااسکریپتی برگردانده خواهند شد:
data {Id: 12, Title: "Item1", Description: "Des", Price: 100000, Category: 0}
که توسط data می‌توانیم به هر کدام از فیلدها، جهت نمایش در خروجی، دسترسی داشته باشیم.
دریافت سورس مثال جاریShowAlertSignalR 
مطالب
Blazor 5x - قسمت 29 - برنامه‌ی Blazor WASM - یک تمرین: رزرو کردن یک اتاق انتخابی


در قسمت قبل که لیست اتاق‌های دریافتی از Web API را نمایش دادیم، هرکدام از آن‌ها، به همراه یک دکمه‌ی Book هم هستند (تصویر فوق) که هدف از آن، فراهم آوردن امکان رزرو کردن آن اتاق، توسط کاربران سایت است. این قسمت را می‌توان به عنوان تمرینی جهت یادآوری مراحل مختلف تهیه‌ی یک Web API و قسمت‌های سمت کلاینت آن، تکمیل کرد.



تهیه موجودیت و مدل متناظر با صفحه‌ی ثبت رزرو یک اتاق

تا اینجا در برنامه‌ی سمت کلاینت، زمانیکه بر روی دکمه‌ی Go صفحه‌ی اول کلیک می‌کنیم، تاریخ شروع رزرو و تعداد روز مدنظر، به صفحه‌ی مشاهده‌ی لیست اتاق‌ها ارسال می‌شود. اکنون می‌خواهیم در این لیست اتاق‌های نمایش داده شده، اگر بر روی لینک Book اتاقی کلیک شد، به صفحه‌ی اختصاصی رزرو آن اتاق هدایت شویم (مانند تصویر فوق). به همین جهت نیاز است موجودیت متناظر با اطلاعاتی را که قرار است از کاربر دریافت کنیم، به صورت زیر به پروژه‌ی BlazorServer.Entities اضافه کنیم:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        public DateTime CheckInDate { get; set; }

        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        public long TotalCost { get; set; }

        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        [ForeignKey("RoomId")]
        public HotelRoom HotelRoom { get; set; }

        public string Status { get; set; }
    }
}
در اینجا مشخصات شروع و پایان رزرو یک اتاق مشخص و مشخصات کاربری که قرار است این فرم را پر کند، مشاهده می‌کنید که Status یا وضعیت آن، در پروژه‌ی مشترک BlazorServer.Common به صورت زیر تعریف می‌شود:
namespace BlazorServer.Common
{
    public static class BookingStatus
    {
        public const string Pending = "Pending";
        public const string Booked = "Booked";
        public const string CheckedIn = "CheckedIn";
        public const string CheckedOutCompleted = "CheckedOut";
        public const string NoShow = "NoShow";
        public const string Cancelled = "Cancelled";
    }
}
پس از این تعاریف، DbSet آن‌را نیز به ApplicationDbContext اضافه می‌کنیم:
namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; }

        // ...
    }
}
بنابراین مرحله‌ی بعدی، ایجاد و اجرای Migrations متناظر با این جدول جدید است. برای این منظور با استفاده از خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddRoomOrderDetails --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
این دستورات به پروژه‌ی آغازین WebApi اشاره می‌کنند که قرار است از طریق سرویسی، با بانک اطلاعاتی ارتباط برقرار کند.

پس از تعریف یک موجودیت، یک DTO متناظر با آن‌را که جهت مدلسازی UI از آن استفاده خواهیم کرد، در پروژه‌ی BlazorServer.Models ایجاد می‌کنیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class RoomOrderDetailsDTO
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        [Required]
        public DateTime CheckInDate { get; set; }

        [Required]
        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        [Required]
        public long TotalCost { get; set; }

        [Required]
        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        public HotelRoomDTO HotelRoomDTO { get; set; }

        public string Status { get; set; }
    }
}
و همچنین در پروژه‌ی BlazorServer.Models.Mappings، نگاشت دوطرفه‌ی AutoMapper آن‌را نیز برقرار می‌کنیم؛ تا در حین تبدیل اطلاعات بین این دو، نیازی به تکرار سطرهای مقدار دهی اطلاعات خواص، نباشد:
namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            // ... 
            CreateMap<RoomOrderDetail, RoomOrderDetailsDTO>().ReverseMap(); // two-way mapping
        }
    }
}


ایجاد سرویسی برای کار با جدول RoomOrderDetails

در برنامه‌ی سمت کلاینت برای کار با بانک اطلاعاتی، دیگر نمی‌توان از سرویس‌های سمت سرور به صورت مستقیم استفاده کرد. به همین جهت آن‌ها را از طریق یک Web API endpoint، در معرض دید استفاده کننده قرار می‌دهیم. اما پیش از اینکار، سرویس سمت سرور Web API باید بتواند با سرویس دسترسی به اطلاعات جدول RoomOrderDetails، کار کند. بنابراین در ادامه این سرویس را تهیه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details);

        Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync();

        Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId);

        Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate);

        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id);

        Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status);
    }
}
که به صورت زیر پیاده سازی می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public RoomOrderDetailsService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
        {
            var roomOrder = _mapper.Map<RoomOrderDetail>(details);
            roomOrder.Status = BookingStatus.Pending;
            var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
        }

        public Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync()
        {
            return _dbContext.RoomOrderDetails
                            .Include(roomOrderDetail => roomOrderDetail.HotelRoom)
                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                            .ToListAsync();
        }

        public async Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.Id == roomOrderId);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }

        public Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate)
        {
            return _dbContext.RoomOrderDetails
                .AnyAsync(
                    roomOrderDetail =>
                        roomOrderDetail.RoomId == RoomId &&
                        roomOrderDetail.IsPaymentSuccessful &&
                        (
                            (checkInDate < roomOrderDetail.CheckOutDate && checkInDate > roomOrderDetail.CheckInDate) ||
                            (checkOutDate > roomOrderDetail.CheckInDate && checkInDate < roomOrderDetail.CheckInDate)
                        )
                );
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id)
        {
            throw new NotImplementedException();
        }

        public Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status)
        {
            throw new NotImplementedException();
        }
    }
}
توضیحات:
- از متد CreateAsync برای تبدیل مدل فرم ثبت اطلاعات، به یک رکورد جدول RoomOrderDetails، استفاده می‌کنیم.
- متد GetAllRoomOrderDetailsAsync، لیست تمام سفارش‌های ثبت شده را بازگشت می‌دهد.
- متد GetRoomOrderDetailAsync بر اساس شماره اتاقی که دریافت می‌کند، لیست سفارشات آن اتاق خاص را بازگشت می‌دهد. این لیست به علت استفاده از Include‌های تعریف شده، به همراه مشخصات اتاق و همچنین تصاویر مرتبط با آن اتاق نیز هست.
- متد IsRoomBookedAsync بر اساس شماره اتاق و بازه‌ی زمانی درخواستی توسط یک کاربر مشخص می‌کند که آیا اتاق خالی شده‌است یا خیر؟

پس از تعریف این سرویس، به کلاس آغازین پروژه‌ی Web API مراجعه کرده و آن‌را به سیستم تزریق وابستگی‌ها، معرفی می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IRoomOrderDetailsService, RoomOrderDetailsService>();
            // ...
 
 
تشکیل سرویس ابتدایی کار با RoomOrderDetails در پروژه‌ی WASM

در ادامه، تعاریف خالی سرویس سمت کلاینت کار با RoomOrderDetails  را به پروژه‌ی WASM اضافه می‌کنیم. تکمیل این سرویس را به قسمت بعدی واگذار خواهیم کرد:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
    }
}
با این پیاده سازی ابتدایی:
namespace BlazorWasm.Client.Services
{
    public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
    {
        private readonly HttpClient _httpClient;

        public ClientRoomOrderDetailsService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }

        public Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }
    }
}
که این مورد نیز باید به نحو زیر به سیستم تزریق وابستگی‌های برنامه‌ی سمت کلاینت در فایل Program.cs آن اضافه شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
            // ...
        }
    }
}


تعریف مدل فرم ثبت اطلاعات سفارش

پس از تدارک مقدمات فوق، اکنون می‌توانیم کار تکمیل فرم ثبت اطلاعات سفارش را شروع کنیم. به همین جهت مدل مخصوص آن‌را در برنامه‌ی سمت کلاینت به صورت زیر تشکیل می‌دهیم:
using BlazorServer.Models;

namespace BlazorWasm.Client.Models.ViewModels
{
    public class HotelRoomBookingVM
    {
        public RoomOrderDetailsDTO OrderDetails { get; set; }
    }
}


تعریف کامپوننت جدید RoomDetails و مقدار دهی اولیه‌ی مدل آن

در ادامه فایل جدید BlazorWasm.Client\Pages\HotelRooms\RoomDetails.razor را ایجاد کرده و به صورت زیر مقدار دهی اولیه می‌کنیم:
@page "/hotel/room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService

@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{

}

@code {
    [Parameter]
    public int? Id { get; set; }

    HotelRoomBookingVM HotelBooking  = new HotelRoomBookingVM();
    int NoOfNights = 1;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
            if (Id != null)
            {
                if (await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking) != null)
                {
                    var roomInitialInfo = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking);
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, roomInitialInfo.StartDate, roomInitialInfo.EndDate);
                    NoOfNights = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.CheckInDate = roomInitialInfo.StartDate;
                    HotelBooking.OrderDetails.CheckOutDate = roomInitialInfo.EndDate;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        roomInitialInfo.NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
                else
                {
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, DateTime.Now, DateTime.Now.AddDays(1));
                    NoOfNights = 1;
                    HotelBooking.OrderDetails.CheckInDate = DateTime.Now;
                    HotelBooking.OrderDetails.CheckOutDate = DateTime.Now.AddDays(1);
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = 1;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
            }
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}
- در ابتدا مسیریابی کامپوننت جدید RoomDetails را مشخص کرد‌ه‌ایم که یک Id را می‌پذیرد که همان Id اتاق انتخاب شده‌ی توسط کاربر است. به همین جهت پارامتر عمومی متناظری با آن هم در قسمت کدهای کامپوننت تعریف شده‌است.
- سپس سرویس توکار IJSRuntime به کامپوننت تزریق شده‌است تا توسط آن و Toastr، بتوان خطاهایی را به کاربر نمایش داد.
- از سرویس ILocalStorageService برای دسترسی به اطلاعات شروع به رزرو شخص و تعداد روز مدنظر او استفاده می‌کنیم که در قسمت قبل آن‌را مقدار دهی کردیم.
- همچنین از سرویس IClientHotelRoomService که آن‌را نیز در قسمت قبل افزودیم، برای فراخوانی متد GetHotelRoomDetailsAsync آن استفاده کرده‌ایم.

در روال آغازین OnInitializedAsync، اگر Id تنظیم شده بود، یعنی کاربر به درستی وارد این صفحه شده‌است. سپس بررسی می‌کنیم که آیا اطلاعاتی از درخواست ابتدایی او در Local Storage مرورگر وجود دارد یا خیر؟ اگر این اطلاعات وجود داشته باشد، بر اساس آن، بازه‌ی تاریخی دقیقی را می‌توان تشکیل داد و اگر خیر، این بازه را از امروز، به مدت 1 روز درنظر می‌گیریم.
پس از پایان کار متد OnInitializedAsync، چون اجزای HotelBooking مقدار دهی کامل شده‌اند، نمایش loading ابتدای کامپوننت، متوقف شده و قسمت else شرط نوشته شده اجرا می‌شود؛ یعنی اصل UI فرم نمایان خواهد شد.

در قسمت قبل، متد GetHotelRoomDetailsAsync را تکمیل نکردیم؛ چون به آن نیازی نداشتیم و فقط قصد داشتیم تا لیست تمام اتاق‌ها را نمایش دهیم. اما در اینجا برای تکمیل کدهای آغازین کامپوننت RoomDetails، متد دریافت اطلاعات یک اتاق را نیز تکمیل می‌کنیم تا توسط آن بتوان در این کامپوننت نیز جزئیات اتاق انتخابی را نمایش داد:
namespace BlazorWasm.Client.Services
{
    public class ClientHotelRoomService : IClientHotelRoomService
    {
        private readonly HttpClient _httpClient;

        public ClientHotelRoomService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/hotelroom/{roomId}"))
                            .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}")
                            .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}")
                            .Uri;
            return _httpClient.GetFromJsonAsync<HotelRoomDTO>(uri);
        }

        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate)
        {
           // ...
        }
    }
}

اتصال مدل کامپوننت RoomDetails به فرم ثبت سفارش آن

تا اینجا مدل فرم را مقدار دهی اولیه کردیم. اکنون می‌توانیم قسمت else شرط نوشته شده را تکمیل کرده و در قسمتی از آن، مشخصات اتاق جاری را نمایش دهیم و در قسمتی دیگر، فرم ثبت سفارش را تکمیل کنیم.
الف) نمایش مشخصات اتاق جاری
در کامپوننت جاری با استفاده از خواص مقدار دهی اولیه شده‌ی شیء HotelBooking.OrderDetails.HotelRoomDTO، می‌توان جزئیات اتاق انتخابی را نمایش داد که نمونه‌ای از آن‌را در قسمت قبل هم مشاهده کردید:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
    <div class="mt-4 mx-4 px-0 px-md-5 mx-md-5">
        <div class="row p-2 my-3 " style="border-radius:20px; ">
            <div class="col-12 col-lg-7 p-4" style="border: 1px solid gray">
                <div class="row px-2 text-success border-bottom">
                    <div class="col-8 py-1"><p style="font-size:x-large;margin:0px;">Selected Room</p></div>
                    <div class="col-4 p-0"><a href="hotel/rooms" class="btn btn-secondary btn-block">Back to Room's</a></div>
                </div>
                <div class="row">
                    <div class="col-6">
                        <div id="" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel">
                            <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel">
                                <ol class="carousel-indicators">
                                    <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
                                    <li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
                                </ol>
                                <div class="carousel-inner">
                                    <div class="carousel-item active">
                                        <img class="d-block w-100" src="images/slide1.jpg" alt="First slide">
                                    </div>
                                </div>
                                <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
                                    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Previous</span>
                                </a>
                                <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
                                    <span class="carousel-control-next-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Next</span>
                                </a>
                            </div>
                        </div>
                    </div>
                    <div class="col-6">
                        <span class="float-right pt-4">
                            <span class="float-right">Occupancy : @HotelBooking.OrderDetails.HotelRoomDTO.Occupancy adults </span><br />
                            <span class="float-right pt-1">Size : @HotelBooking.OrderDetails.HotelRoomDTO.SqFt sqft</span><br />
                            <h4 class="text-warning font-weight-bold pt-5">
                                <span style="border-bottom:1px solid #ff6a00">
                                    @HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount.ToString("#,#.00#;(#,#.00#)")
                                </span>
                            </h4>
                            <span class="float-right">Cost for @HotelBooking.OrderDetails.HotelRoomDTO.TotalDays nights</span>
                        </span>
                    </div>
                </div>
                <div class="row p-2">
                    <div class="col-12">
                        <p class="card-title text-warning" style="font-size:xx-large">@HotelBooking.OrderDetails.HotelRoomDTO.Name</p>
                        <p class="card-text" style="font-size:large">
                            @((MarkupString)@HotelBooking.OrderDetails.HotelRoomDTO.Details)
                        </p>
                    </div>

                </div>
            </div>
}
ب) نمایش فرم متصل به مدل کامپوننت
قسمت دوم UI کامپوننت جاری، نمایش فرم زیر است که اجزای مختلف آن به فیلد HotelBooking متصل شده‌اند:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
  // ...
             
            <div class="col-12 col-lg-5 p-4 2 mt-4 mt-md-0" style="border: 1px solid gray;">
                <EditForm Model="HotelBooking" class="container" OnValidSubmit="HandleCheckout">
                    <div class="row px-2 text-success border-bottom"><div class="col-7 py-1"><p style="font-size:x-large;margin:0px;">Enter Details</p></div></div>

                    <div class="form-group pt-2">
                        <label class="text-warning">Name</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Name" type="text" class="form-control" />
                    </div>
                    <div class="form-group pt-2">
                        <label class="text-warning">Phone</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Phone" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Email</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Email" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check in Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckInDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check Out Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckOutDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">No. of nights</label>
                        <select class="form-control" value="@NoOfNights" @onchange="HandleNoOfNightsChange">
                            @for (var i = 1; i <= 10; i++)
                            {
                                if (i == NoOfNights)
                                {
                                    <option value="@i" selected="selected">@i</option>
                                }
                                else
                                {
                                    <option value="@i">@i</option>
                                }
                            }
                        </select>
                    </div>
                    <div class="form-group">
                        <button type="submit" class="btn btn-success form-control">Checkout Now</button>
                    </div>
                </EditForm>
            </div>
        </div>
    </div>
}
در این فرم دو روال رویدادگردان زیر نیز مورد استفاده هستند:
@code {
    // ...

    private async Task HandleNoOfNightsChange(ChangeEventArgs e)
    {
        NoOfNights = Convert.ToInt32(e.Value.ToString());
        HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
            Id.Value,
            HotelBooking.OrderDetails.CheckInDate,
            HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights));

        HotelBooking.OrderDetails.CheckOutDate = HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights);
        HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = NoOfNights;
        HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
    }

    private async Task HandleCheckout()
    {
        if (!await HandleValidation())
        {
            return;
        }
    }

    private async Task<bool> HandleValidation()
    {
        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Name))
        {
            await JsRuntime.ToastrError("Name cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Phone))
        {
            await JsRuntime.ToastrError("Phone cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Email))
        {
            await JsRuntime.ToastrError("Email cannot be empty");
            return false;
        }
        return true;
    }
}
- کاربر اگر تعداد شب‌های اقامت را از طریق دارپ‌داون فرم تغییر داد، در روال رویدادگردان HandleNoOfNightsChange، محاسبات جدیدی را بر این اساس انجام می‌دهیم؛ چون هزینه و سایر مشخصات جزئیات اتاق نمایش داده شده، باید تغییر کنند.
- همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کرده‌ایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده می‌کنید، کرده‌ایم. این روش دستی را نیز می‌توان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آن‌را توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمی‌کند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کرده‌ایم.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-29.zip
مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت چهارم
در قسمت قبل، اطلاعات نمایش داده شده، از یک سری آرایه ثابت جاوا اسکریپتی تامین شدند. در یک برنامه‌ی واقعی نیاز است داده‌ها را یا از HTML 5 local storage تامین کرد و یا از سرور به کمک Ajax. برای اینگونه اعمال، ember.js به همراه افزونه‌ای است به نام Ember Data که جزئیات کار با آن‌را در این قسمت بررسی خواهیم کرد.


استفاده از Ember Data با Local Storage

برای کار با HTML 5 local storage نیاز به Ember Data Local Storage Adapter نیز هست که در قسمت اول این سری، آدرس دریافت آن معرفی شد. این فایل‌ها نیز در پوشه‌ی Scripts\Libs برنامه کپی خواهند شد.
در ادامه به فایل Scripts\App\store.js که در قسمت قبل جهت تعریف دو آرایه ثابت مطالب و نظرات اضافه شد، مراجعه کرده و محتوای فعلی آن‌را با کدهای زیر جایگزین کنید:
Blogger.ApplicationSerializer = DS.LSSerializer.extend();
Blogger.ApplicationAdapter = DS.LSAdapter.extend();
این تعاریف سبب خواهند شد تا Ember Data از Local Storage Adapter استفاده کند.
در ادامه با توجه به حذف دو آرایه‌ی posts و comments که پیشتر در فایل store.js تعریف شده بودند، نیاز است مدل‌های متناظری را جهت تعریف خواص آن‌ها، به برنامه اضافه کنیم. این‌کار را با افزودن دو فایل جدید comment.js و post.js به پوشه‌ی Scripts\Models انجام خواهیم داد.
محتوای فایل Scripts\Models\post.js :
Blogger.Post = DS.Model.extend({
  title: DS.attr(),
  body: DS.attr()
});
محتوای فایل Scripts\Models\comment.js :
Blogger.Comment = DS.Model.extend({
  text: DS.attr()
});
سپس مداخل تعریف آن‌ها را به فایل index.html نیز اضافه خواهیم کرد:
 <script src="Scripts/Models/post.js" type="text/javascript"></script>
<script src="Scripts/Models/comment.js" type="text/javascript"></script>

برای تعاریف مدل‌ها در Ember data مرسوم است که نام مدل‌ها، اسامی جمع نباشند. سپس با ایجاد وهله‌ای از DS.Model.extend یک مدل ember data را تعریف خواهیم کرد. در این مدل، خواص هر شیء را مشخص کرده و مقدار آن‌ها همیشه ()DS.attr خواهد بود. این نکته را در دو مدل Post و Comment مشاهده می‌کنید. اگر دقت کنید به هر دو مدل، خاصیت id اضافه نشده‌است. این خاصیت به صورت خودکار توسط Ember data تنظیم می‌شود.

اکنون نیاز است برنامه را جهت استفاده از این مدل‌های جدید به روز کرد. برای این منظور فایل Scripts\Routes\posts.js را گشوده و مدل آن‌را به نحو ذیل ویرایش کنید:
Blogger.PostsRoute = Ember.Route.extend({
    //controllerName: 'posts', // مقدار پیش فرض است و نیازی به ذکر آن نیست
    //renderTemplare: function () {
    //    this.render('posts'); // مقدار پیش فرض است و نیازی به ذکر آن نیست
    //},
    model: function () {
        return this.store.find('post');
    }
});
در اینجا this.store معادل data store برنامه است که مطابق تنظیمات برنامه، همان ember data می‌باشد. سپس متد find را به همراه نام مدل، به صورت رشته‌ای در اینجا مشخص می‌کنیم.
به همین ترتیب فایل Scripts\Routes\recent-comments.js را نیز جهت استفاده از data store ویرایش خواهیم کرد:
Blogger.RecentCommentsRoute = Ember.Route.extend({
    model: function () {
        return this.store.find('comment');
    }
});
و فایل Scripts\Routes\post.js که در آن منطق یافتن یک مطلب بر اساس آدرس مختص به آن قرار دارد، به صورت ذیل بازنویسی می‌شود:
Blogger.PostRoute = Ember.Route.extend({
    model: function (params) {
        return this.store.find('post', params.post_id);
    }
});
اگر متد find بدون پارامتر ذکر شود، به معنای بازگشت تمامی عناصر موجود در آن مدل خواهد بود و اگر پارامتر دوم آن مانند این مثال تنظیم شود، تنها همان وهله‌ی درخواستی را بازگشت می‌دهد.


افزودن امکان ثبت یک مطلب جدید

تا اینجا اگر برنامه را اجرا کنید، برنامه بدون خطا بارگذاری خواهد شد اما فعلا رکوردی را برای نمایش ندارد. در ادامه، برنامه را جهت افزودن مطالب جدید توسعه خواهیم داد. برای اینکار ابتدا به فایل Scripts\App\router.js مراجعه کرده و سپس مسیریابی جدید new-post را تعریف خواهیم کرد:
Blogger.Router.map(function () {
    this.resource('posts', { path: '/' });
    this.resource('about');
    this.resource('contact', function () {
        this.resource('email');
        this.resource('phone');
    });
    this.resource('recent-comments');
    this.resource('post', { path: 'posts/:post_id' });
    this.resource('new-post');
});
اکنون در صفحه‌ی اول سایت، توسط قالب Scripts\Templates\posts.hbs، دکمه‌ای را جهت ایجاد یک مطلب جدید اضافه خواهیم کرد:
<h2>Ember.js blog</h2>
<ul>
    {{#each post in arrangedContent}}
    <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li>
    {{/each}}
</ul>
 
<a href="#" class="btn btn-primary" {{action 'sortByTitle' }}>Sort by title</a>
{{#link-to 'new-post' classNames="btn btn-success"}}New Post{{/link-to}}
در اینجا دکمه‌ی New Post به مسیریابی جدید new-post اشاره می‌کند.
برای تعریف عناصر نمایشی این مسیریابی، فایل جدید قالب Scripts\Templates\new-post.hbs را با محتوای زیر اضافه کنید:
<h1>New post</h1>
<form>
  <div class="form-group">
    <label for="title">Title</label>
    {{input value=title id="title" class="form-control"}}
  </div>

  <div class="form-group">
    <label for="body">Body</label>
    {{textarea value=body id="body" class="form-control" rows="5"}}
  </div>


  <button class="btn btn-primary" {{action 'save'}}>Save</button>
</form>
با نمونه‌ی این فرم در قسمت قبل در حین ویرایش یک مطلب، آشنا شدیم. دو المان دریافت اطلاعات در آن قرار دارند که هر کدام به خواص مدل برنامه bind شده‌اند. همچنین یک دکمه‌ی save، با اکشنی به همین نام در اینجا تعریف شده‌است.
پس از آن نیاز است نام فایل قالب new-post را به template loader برنامه در فایل index.html اضافه کرد:
<script type="text/javascript">
    EmberHandlebarsLoader.loadTemplates([
       'posts', 'about', 'application', 'contact', 'email', 'phone',
       'recent-comments', 'post', 'new-post'
    ]);
</script>
برای مدیریت دکمه‌ی save این قالب جدید نیاز است کنترلر جدیدی را در فایل جدید Scripts\Controllers\new-post.js تعریف کنیم؛ با این محتوا:
Blogger.NewPostController = Ember.Controller.extend({
    actions: {
        save: function () {
            var newPost = this.store.createRecord('post', {
                title: this.get('title'),
                body: this.get('body')
            });
            newPost.save();
            this.transitionToRoute('posts');
        }
    }
});
به همراه افزودن مدخلی از آن به فایل index.html برنامه:
 <script src="Scripts/Controllers/new-post.js" type="text/javascript"></script>

در اینجا کنترلر جدید NewPostController را مشاهده می‌کنید. از این جهت که برای دسترسی به خواص مدل تغییر کرده، از متد this.get استفاده شده‌است، نیازی نیست حتما از یک ObjectController مانند قسمت قبل استفاده کرد و Controller معمولی نیز برای اینکار کافی است.
آرگومان اول this.store.createRecord نام مدل است و آرگومان دوم آن، وهله‌ای که قرار است به آن اضافه شود. همچنین باید دقت داشت که برای تنظیم یک خاصیت، از متد this.set و برای دریافت مقدار یک خاصیت تغییر کرده از this.get به همراه نام خاصیت مورد نظر استفاده می‌شود و نباید مستقیما برای مثال از this.title استفاده کرد.
this.store.createRecord صرفا یک شیء جدید (ember data object) را ایجاد می‌کند. برای ذخیره سازی نهایی آن باید متد save آن‌را فراخوانی کرد (پیاده سازی الگوی active record است). به این ترتیب این شیء در local storage ذخیره خواهد شد.
پس از ذخیره‌ی مطلب جدید، از متد this.transitionToRoute استفاده شده‌است. این متد، برنامه را به صورت خودکار به صفحه‌ی متناظر با مسیریابی posts هدایت می‌کند.

اکنون برنامه را اجرا کنید. بر روی دکمه‌ی سبز رنگ new post در صفحه‌ی اول کلیک کرده و یک مطلب جدید را تعریف کنید. بلافاصله عنوان و لینک متناظر با این مطلب را در صفحه‌ی اول سایت مشاهده خواهید کرد.
همچنین اگر برنامه را مجددا بارگذاری کنید، این مطالب هنوز قابل مشاهده هستند؛ زیرا در local storage مرورگر ذخیره شده‌اند.


در اینجا اگر به لینک‌های تولید شده دقت کنید، id آن‌ها عددی نیست. این روشی است که local storage با آن کار می‌کند.


افزودن امکان حذف یک مطلب به سایت

برای حذف یک مطلب، دکمه‌ی حذف را به انتهای قالب Scripts\Templates\post.hbs اضافه خواهیم کرد:
<h2>{{title}}</h2>
{{#if isEditing}}
<form>
    <div class="form-group">
        <label for="title">Title</label>
        {{input value=title id="title" class="form-control"}}
    </div>
    <div class="form-group">
        <label for="body">Body</label>
        {{textarea value=body id="body" class="form-control" rows="5"}}
    </div>
    <button class="btn btn-primary" {{action 'save' }}>Save</button>
</form>
{{else}}
<p>{{body}}</p>
<button class="btn btn-primary" {{action 'edit' }}>Edit</button>
<button class="btn btn-danger" {{action 'delete' }}>Delete</button>
{{/if}}


سپس کنترلر Scripts\Controllers\post.js را جهت مدیریت اکشن جدید delete به نحو ذیل تکمیل می‌کنیم:
Blogger.PostController = Ember.ObjectController.extend({
    isEditing: false,
    actions: {
        edit: function () {
            this.set('isEditing', true);
        },
        save: function () {
            var post = this.get('model');
            post.save();

            this.set('isEditing', false);
        },
        delete: function () {
            if (confirm('Do you want to delete this post?')) {
                this.get('model').destroyRecord();
                this.transitionToRoute('posts');
            }
        }
    }
});
متد destroyRecord، مدل انتخابی را هم از حافظه و هم از data store حذف می‌کند. سپس کاربر را به صفحه‌ی اصلی سایت هدایت خواهیم کرد.
متد save نیز در اینجا بهبود یافته‌است. ابتدا مدل جاری دریافت شده و سپس متد save بر روی آن فراخوانی می‌شود. به این ترتیب اطلاعات از حافظه به local storage نیز منتقل خواهند شد.


ثبت و نمایش نظرات به همراه تنظیمات روابط اشیاء در Ember Data

در ادامه قصد داریم امکان افزودن نظرات را به مطالب، به همراه نمایش آن‌‌ها در ذیل هر مطلب، پیاده سازی کنیم. برای اینکار نیاز است رابطه‌ی بین یک مطلب و نظرات مرتبط با آن‌را در مدل ember data مشخص کنیم. به همین جهت فایل Scripts\Models\post.js را گشوده و تغییرات ذیل را به آن اعمال کنید:
Blogger.Post = DS.Model.extend({
  title: DS.attr(),
  body: DS.attr(),
  comments: DS.hasMany('comment', { async: true })
});
در اینجا خاصیت جدیدی به نام comments به مدل مطلب اضافه شده‌است و توسط آن می‌توان به تمامی نظرات یک مطلب دسترسی یافت؛ تعریف رابطه‌ی یک به چند، به کمک متد DS.hasMany که پارامتر اول آن نام مدل مرتبط است. تعریف async: true برای کار با local storage اجباری است و در نگارش‌های آتی ember data حالت پیش فرض خواهد بود.
همچنین نیاز است یک سر دیگر رابطه را نیز مشخص کرد. برای این منظور فایل Scripts\Models\comment.js را گشوده و به نحو ذیل تکمیل کنید:
Blogger.Comment = DS.Model.extend({
    text: DS.attr(),
    post: DS.belongsTo('post', { async: true })
});
در اینجا خاصیت جدید post به مدل نظر اضافه شده‌است و مقدار آن از طریق متد DS.belongsTo که مدل post را به یک نظر، مرتبط می‌کند، تامین خواهد شد. بنابراین در این حالت اگر به شیء comment مراجعه کنیم، خاصیت جدید post.id آن، به id مطلب متناظر اشاره می‌کند.

در ادامه نیاز است بتوان تعدادی نظر را ثبت کرد. به همین جهت با تعریف مسیریابی آن شروع می‌کنیم. این مسیریابی تعریف شده در فایل Scripts\App\router.js نیز باید تو در تو باشد؛ زیرا قسمت ثبت نظر (new-comment) دقیقا داخل همان صفحه‌ی نمایش یک مطلب ظاهر می‌شود:
Blogger.Router.map(function () {
    this.resource('posts', { path: '/' });
    this.resource('about');
    this.resource('contact', function () {
        this.resource('email');
        this.resource('phone');
    });
    this.resource('recent-comments');
    this.resource('post', { path: 'posts/:post_id' }, function () {
        this.resource('new-comment');
    });
    this.resource('new-post');
});
لینک آن‌را نیز به انتهای فایل Scripts\Templates\post.hbs اضافه می‌کنیم. از این جهت که این لینک به مدل جاری اشاره می‌کند، با استفاده از متغیر this، مدل جاری را به عنوان مدل مورد استفاده مشخص خواهیم کرد:
<h2>{{title}}</h2>
{{#if isEditing}}
<form>
    <div class="form-group">
        <label for="title">Title</label>
        {{input value=title id="title" class="form-control"}}
    </div>
    <div class="form-group">
        <label for="body">Body</label>
        {{textarea value=body id="body" class="form-control" rows="5"}}
    </div>
    <button class="btn btn-primary" {{action 'save' }}>Save</button>
</form>
{{else}}
<p>{{body}}</p>
<button class="btn btn-primary" {{action 'edit' }}>Edit</button>
<button class="btn btn-danger" {{action 'delete' }}>Delete</button>
{{/if}}
 
<h2>Comments</h2>
{{#each comment in comments}}
<p>
    {{comment.text}}
</p>
{{/each}}
 
<p>{{#link-to 'new-comment' this class="btn btn-success"}}New comment{{/link-to}}</p>
{{outlet}}
پس از تکمیل روابط مدل‌ها، قالب Scripts\Templates\post.hbs را جهت استفاده از این خواص به روز خواهیم کرد. در تغییرات جدید، قسمت <h2>Comments</h2> به انتهای صفحه اضافه شده‌است. سپس حلقه‌ای بر روی خاصیت جدید comments تشکیل شده و مقدار خاصیت text هر آیتم نمایش داده می‌شود.
در انتهای قالب نیز یک {{outlet}} اضافه شده‌است. کار آن نمایش قالب ارسال یک نظر جدید، پس از کلیک بر روی لینک New Comment می‌باشد. این قالب را با افزودن فایل Scripts\Templates\new-comment.hbs با محتوای ذیل ایجاد خواهیم کرد:
<h2>New comment</h2>

<form>
  <div class="form-group">
    <label for="text">Your thoughts:</label>
    {{textarea value=text id="text" class="form-control" rows="5"}}
  </div>

  <button class="btn btn-primary" {{action "save"}}>Add your comment</button>
</form>
سپس نام این قالب را به template loader فایل index.html نیز اضافه می‌کنیم؛ تا در ابتدای بارگذاری برنامه شناسایی شده و استفاده شود:
<script type="text/javascript">
    EmberHandlebarsLoader.loadTemplates([
       'posts', 'about', 'application', 'contact', 'email', 'phone',
       'recent-comments', 'post', 'new-post', 'new-comment'
    ]);
</script>
این قالب به خاصیت text یک comment متصل بوده و همچنین اکشن جدیدی به نام save دارد. بنابراین برای مدیریت اکشن save، نیاز به کنترلری متناظر خواهد بود. به همین جهت فایل جدید Scripts\Controllers\new-comment.js را با محتوای ذیل ایجاد کنید:
Blogger.NewCommentController = Ember.ObjectController.extend({
    needs: ['post'],
    actions: {
        save: function () {
            var comment = this.store.createRecord('comment', {
                text: this.get('text')
            });
            comment.save();
 
            var post = this.get('controllers.post.model');
            post.get('comments').pushObject(comment);
            post.save();
 
            this.transitionToRoute('post', post.id);
        }
    }
});
و مدخل تعریف آن‌را نیز به صفحه‌ی index.html اضافه می‌کنیم:
 <script src="Scripts/Controllers/new-comment.js" type="text/javascript"></script>

قسمت ذخیره سازی comment جدید با ذخیره سازی یک post جدید که پیشتر بررسی کردیم، تفاوتی ندارد. از متد this.store.createRecord جهت معرفی وهله‌ای جدید از comment استفاده و سپس متد save آن، برای ثبت نهایی فراخوانی شده‌است.
در ادامه باید این نظر جدید را به post متناظر با آن مرتبط کنیم. برای اینکار نیاز است تا به مدل کنترلر post دسترسی داشته باشیم. به همین جهت خاصیت needs را به تعاریف کنترلر جاری به همراه نام کنترلر مورد نیاز، اضافه کرده‌‌ایم. به این ترتیب می‌توان توسط متد this.get و پارامتر controllers.post.model در کنترلر NewComment به اطلاعات کنترلر post دسترسی یافت. سپس خاصیت comments شیء post جاری را یافته و مقدار آن‌را به comment جدیدی که ثبت کردیم، تنظیم می‌کنیم. در ادامه با فراخوانی متد save، کار تنظیم ارتباطات یک مطلب و نظرهای جدید آن به پایان می‌رسد.
در آخر با فراخوانی متد transitionToRoute به مطلبی که نظر جدیدی برای آن ارسال شده‌است باز می‌گردیم.


همانطور که در این تصویر نیز مشاهده می‌کنید، اطلاعات ذخیره شده در local storage را توسط افزونه‌ی Ember Inspector نیز می‌توان مشاهده کرد.


افزودن دکمه‌ی حذف به لیست نظرات ارسالی

برای افزودن دکمه‌ی حذف، به قالب Scripts\Templates\post.hbs مراجعه کرده و قسمتی را که لیست نظرات را نمایش می‌دهد، به نحو ذیل تکمیل می‌کنیم:
{{#each comment in comments}}
<p>
    {{comment.text}}
    <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button>
</p>
{{/each}}
همچنین برای مدیریت اکشن جدید delete، کنترلر جدید comment را در فایل Scripts\Controllers\comment.js اضافه خواهیم کرد.
Blogger.CommentController = Ember.ObjectController.extend({
    needs: ['post'],
    actions: {
        delete: function () {
            if (confirm('Do you want to delete this comment?')) {
                var comment = this.get('model');
                comment.deleteRecord();
                comment.save();
 
                var post = this.get('controllers.post.model');
                post.get('comments').removeObject(comment);
                post.save(); 
            }
        }
    }
});
به همراه تعریف مدخل آن در فایل index.html :
 <script src="Scripts/Controllers/comment.js" type="text/javascript"></script>

در این حالت اگر برنامه را اجرا کنید، پیام «Do you want to delete this post» را مشاهده خواهید کرد بجای پیام «Do you want to delete this comment». علت اینجا است که قالب post به صورت پیش فرض به کنترلر post متصل است و نه کنترلر comment. برای رفع این مشکل تنها کافی است از itemController به نحو ذیل استفاده کنیم:
{{#each comment in comments  itemController="comment"}}
<p>
    {{comment.text}}
    <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button>
</p>
{{/each}}
به این ترتیب اکشن delete به کنترلر comment ارسال خواهد شد و نه کنترلر پیش فرض post جاری.
در کنترلر Comment روش دیگری را برای حذف یک رکورد مشاهده می‌کنید. می‌توان ابتدا متد deleteRecord را بر روی مدل فراخوانی کرد و سپس آن‌را save نمود تا نهایی شود. همچنین در اینجا نیاز است نظر حذف شده را از سر دیگر رابطه نیز حذف کرد. روش دسترسی به post جاری در این حالت، همانند توضیحات NewCommentController است که پیشتر بحث شد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_04.zip
 
مطالب
Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر
از زمان Blazor 5x، پشتیبانی توکار از آپلود فایل‌ها، به آن اضافه شده‌است و پیش از آن می‌بایستی از کامپوننت‌های ثالث استفاده می‌شد. در این قسمت نحوه‌ی استفاده از کامپوننت آپلود فایل‌های Blazor را بررسی می‌کنیم. همچنین یک نمونه مثال، از فرم‌های master-details را نیز با هم مرور خواهیم کرد.



افزودن فیلد آپلود تصاویر، به فرم ثبت اطلاعات یک اتاق

در ادامه به کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor که تا این قسمت آن‌را تکمیل کرده‌ایم مراجعه کرده و فیلد جدید InputFile را ذیل قسمت ثبت توضیحات، اضافه می‌کنیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
</div>

@code
{
    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {

    }
}
- ذکر ویژگی multiple در اینجا سبب می‌شود تا بتوان بیش از یک فایل را هربار انتخاب و آپلود کرد.
- در این کامپوننت، رویداد OnChange، پس از تغییر مجموعه‌ی فایل‌های اضافه شده‌ی به آن، فراخوانی می‌شود و آرگومانی از نوع InputFileChangeEventArgs را دریافت می‌کند.


افزودن لیست فایل‌های انتخابی به HotelRoomDTO

تا اینجا اگر به BlazorServer.Models\HotelRoomDTO.cs مراجعه کنیم (کلاسی که مدل UI فرم ثبت اطلاعات اتاق را فراهم می‌کند)، امکان افزودن لیست تصاویر انتخابی به آن وجود ندارد. به همین جهت در این کلاس، تغییر زیر را اعمال می‌کنیم:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class HotelRoomDTO
    {
        // ... 
        public virtual ICollection<HotelRoomImageDTO> HotelRoomImages { get; set; } = new List<HotelRoomImageDTO>();
    }
}
HotelRoomImageDTO را در قسمت قبل اضافه کردیم. متناظر با ICollection فوق، چنین خاصیتی در موجودیت HotelRoom که از نوع <ICollection<HotelRoomImage است نیز تعریف شده‌است تا بتوان به ازای هر اتاق، مشخصات تعدادی تصویر را در بانک اطلاعاتی ذخیره کرد.


تکمیل متد رویدادگردان HandleImageUpload

در ادامه، لیست فایل‌ها‌ی انتخاب شده‌ی توسط کاربر را دریافت کرده و آن‌ها را آپلود می‌کنیم:
@inject IHotelRoomService HotelRoomService
@inject NavigationManager NavigationManager
@inject IJSRuntime JsRuntime
@inject IFileUploadService FileUploadService
@inject IWebHostEnvironment WebHostEnvironment

@code
{
    // ...

    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {
        var files = args.GetMultipleFiles(maximumFileCount: 5);
        if (args.FileCount == 0 || files.Count == 0)
        {
            return;
        }

        var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
        if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
        {
            await JsRuntime.ToastrError("Please select .jpg/.jpeg/.png files only.");
            return;
        }

        foreach (var file in files)
        {
            var uploadedImageUrl = await FileUploadService.UploadFileAsync(file, WebHostEnvironment.WebRootPath, "Uploads");
            HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
        }
    }
}
- در اینجا نیاز به تزریق چند سرویس جدید هست؛ مانند IFileUploadService که در قسمت قبل تکمیل کردیم و سرویس توکار IWebHostEnvironment. به همین جهت به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضاهای نام متناظر زیر را اضافه می‌کنیم:
@using Microsoft.AspNetCore.Hosting
@using System.Linq
@using System.IO
برای مثال سرویس IWebHostEnvironment که از آن برای دسترسی به WebRootPath یا محل قرارگیری پوشه‌ی wwwroot استفاده می‌کنیم، در فضای نام Microsoft.AspNetCore.Hosting قرار دارد و یا متد Path.GetExtension در فضای نام System.IO و متد الحاقی Contains با دو پارامتر استفاده شده، در فضای نام System.Linq قرار دارند.
- متد ()args.GetMultipleFiles، امکان دسترسی به فایل‌های انتخابی توسط کاربر را میسر می‌کند که خروجی آن از نوع <IReadOnlyList<IBrowserFile است. در قسمت قبل، سرویس آپلود فایل‌هایی را که تکمیل کردیم، امکان آپلود یک IBrowserFile را به سرور میسر می‌کند. اگر متد ()GetMultipleFiles را بدون پارامتری فراخوانی کنیم، حداکثر 10 فایل را قبول می‌کند و اگر تعداد بیشتری انتخاب شده باشد، یک استثناء را صادر خواهد کرد.
- سپس بر اساس پسوند فایل‌های دریافتی، آن‌ها را صرفا به فایل‌های تصویری محدود کرده‌ایم.
- در آخر، لیست فایل‌های دریافتی را یکی یکی به سرور آپلود کرده و Url دسترسی به آن‌ها را به لیست HotelRoomImages اضافه می‌کنیم. فایل‌های آپلود شده در پوشه‌ی BlazorServer.App\wwwroot\Uploads قابل مشاهده هستند.


نمایش فایل‌های انتخاب شده‌ی توسط کاربر


در ادامه می‌خواهیم پس از آپلود فایل‌ها، آن‌ها را در ذیل کامپوننت InputFile نمایش دهیم. برای اینکار در ابتدا به فایل wwwroot\css\site.css مراجعه کرده و شیوه نامه‌ی نمایش تصاویر و عناوین آن‌ها را اضافه می‌کنیم:
.room-image {
  display: block;
  width: 100%;
  height: 150px;
  background-size: cover !important;
  border: 3px solid green;
  position: relative;
}

.room-image-title {
  position: absolute;
  top: 0;
  right: 0;
  background-color: green;
  color: white;
  padding: 0px 6px;
  display: inline-block;
}
سپس بر روی لیست HotelRoomModel.HotelRoomImages که در متد HandleImageUpload آن‌را تکمیل کردیم، حلقه‌ای را ایجاد کرده و تصاویر را بر اساس RoomImageUrl آن‌ها، نمایش می‌دهیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
    <div class="row">
    @if (HotelRoomModel.HotelRoomImages.Count > 0)
    {
        var serial = 1;
        foreach (var roomImage in HotelRoomModel.HotelRoomImages)
        {
            <div class="col-md-2 mt-3">
                <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; ">
                   <span class="room-image-title">@serial</span>
                </div>
                <button type="button" class="btn btn-outline-danger btn-block mt-4">Delete</button>
            </div>
            serial++;
        }
    }
    </div>
</div>

ذخیره سازی اطلاعات تصاویر آپلودی یک اتاق در بانک اطلاعاتی

تا اینجا موفق شدیم تصاویر انتخابی کاربر را آپلود کرده و همچنین لیست آن‌ها را نیز نمایش دهیم. در ادامه نیاز است تا این اطلاعات را در بانک اطلاعاتی ثبت کنیم. به همین جهت ابتدا سرویس IHotelRoomImageService را که در قسمت قبل تکمیل کردیم، به کامپوننت جاری تزریق می‌کنیم و سپس با استفاده از متد CreateHotelRoomImageAsync، رکوردهای تصویر متناظر با اتاق ثبت شده را اضافه می‌کنیم:
// ...
@inject IHotelRoomImageService HotelRoomImageService


@code
{
    // ...

    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages)
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
}
در حین آپلود فایل‌ها، فقط خاصیت RoomImageUrl را مقدار دهی کردیم:
HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
در اینجا RoomId هر imageDto را نیز بر اساس Id واقعی اتاق ثبت شده‌ی جاری، تکمیل کرده و سپس آن‌را به CreateHotelRoomImageAsync ارسال می‌کنیم.

محل فراخوانی AddHotelRoomImageAsync فوق، در متد HandleHotelRoomUpsert است که در قسمت‌های قبل تکمیل کردیم. در اینجا پس از ثبت اطلاعات اتاق در بانک اطلاعاتی است که به Id آن دسترسی پیدا می‌کنیم:
private async Task HandleHotelRoomUpsert()
    {
       // ...

       // Create Mode
       var createdRoomDto = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
       await AddHotelRoomImageAsync(createdRoomDto);
       await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully.");

       // ... 
    }
اکنون اگر اطلاعات اتاق جدیدی را تکمیل کرده و تصاویری را نیز به آن انتساب دهیم، با کلیک بر روی دکمه‌ی ثبت، ابتدا اطلاعات این اتاق در بانک اطلاعاتی ثبت شده و Id آن به‌دست می‌آید، سپس رکوردهای تصویر آن جداگانه ذخیره خواهند شد.

یک نکته: در انتهای بحث خواهیم دید که اینکار غیرضروری است و با وجود رابطه‌ی one-to-many تعریف شده‌ی توسط EF-Core، اگر لیست HotelRoomImages موجودیت اتاق تعریف شده و در حال ثبت نیز مقدار دهی شده باشد، به صورت خودکار جزئی از این رابطه و تنها در یک رفت و برگشت، ثبت می‌شود. یعنی همان متد CreateHotelRoomAsync، قابلیت ثبت خودکار اطلاعات خاصیت HotelRoomImages موجودیت اتاق را نیز دارا است.


نمایش تصاویر یک اتاق، در حالت ویرایش رکورد آن

تا اینجا فقط حالت ثبت یک رکورد جدید را پوشش دادیم. در این حالت اگر به لیست اتاق‌های ثبت شده مراجعه کرده و بر روی دکمه‌ی edit یکی از آن‌ها کلیک کنیم، به صفحه‌ی ویرایش رکورد منتقل خواهیم شد؛ اما این صفحه، فاقد اطلاعات تصاویر منتسب به آن رکورد است.
علت اینجا است که در حین ویرایش اطلاعات، در متد OnInitializedAsync، هرچند اطلاعات یک اتاق را از بانک اطلاعاتی دریافت کرده و آن‌را تبدیل به Dto آن می‌کنیم که سبب نمایش جزئیات هر خاصیت در فیلد متصل به آن در فرم جاری می‌شود:
    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
        }
        // ...
    }
اما چون یک رابطه‌ی one-to-many بین اتاق و تصاویر آن برقرار است، نیاز است این رابطه را از طریق eager-loading و فراخوانی متد Include، واکشی کنیم تا اینبار زمانیکه GetHotelRoomAsync فراخوانی می‌شود، به همراه اطلاعات navigation property لیست تصاویر اتاق (HotelRoomImages) نیز باشد.
بنابراین به فایل BlazorServer\BlazorServer.Services\HotelRoomService.cs مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        // ...
 
        public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync()
        {
            return _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .AsAsyncEnumerable();
        }

        public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId)
        {
            return _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
        }
    }
}
در اینجا تنها تغییری که صورت گرفته، استفاده از متد Include(x => x.HotelRoomImages) است؛ تا هنگامیکه اطلاعات یک اتاق را واکشی می‌کنیم، به صورت خودکار اطلاعات تصاویر مرتبط به آن نیز واکشی گردد و سپس توسط AutoMapper، به Dto آن انتساب داده شود (یعنی انتساب HotelRoomImages موجودیت اتاق، به همین خاصیت در DTO آن). این انتساب، سبب به روز رسانی خودکار UI نیز می‌شود. یعنی برای نمایش تصاویر مرتبط با یک اتاق، همان کدهای قبلی که پیشتر داشتیم، هنوز هم کار می‌کنند.


افزودن تصاویر جدید، در حین ویرایش یک رکورد

پس از نمایش لیست تصاویر منتسب به یک اتاق در حال ویرایش، اکنون می‌خواهیم در همین حالت اگر کاربر تصویر جدیدی را انتخاب کرد، این تصویر را نیز به لیست تصاویر ثبت شده‌ی در بانک اطلاعاتی اضافه کنیم. برای اینکار نیز به متد HandleHotelRoomUpsert مراجعه کرده و از متد AddHotelRoomImageAsync در قسمت به روز رسانی آن استفاده می‌کنیم:
private async Task HandleHotelRoomUpsert()
{
   //...

   // Update Mode
   var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel);
   await AddHotelRoomImageAsync(updatedRoomDto);
   await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully.");

   //...
}
مشکل! اگر از این روش استفاده کنیم، هربار به روز رسانی اطلاعات یک جدول، به همراه ثبت رکوردهای تکراری نمایش داده شده‌ی در حالت ویرایش هم خواهند بود. برای مثال فرض کنید سه تصویر را به یک اتاق انتساب داده‌اید. در حالت ویرایش، ابتدا این سه تصویر نمایش داده می‌شوند. بنابراین در لیست HotelRoomModel.HotelRoomImages وجود خواهند داشت. اکنون کاربر دو تصویر جدید دیگر را هم به این لیست اضافه می‌کند. در زمان ثبت، در متد AddHotelRoomImageAsync، بررسی نمی‌کنیم که این تصویر اضافه شده، جدید است یا خیر  و یا همان سه تصویر ابتدای کار نمایش فرم در حالت ویرایش هستند. به همین جهت رکوردها، تکراری ثبت می‌شوند.
برای رفع این مشکل می‌توان در متد AddHotelRoomImageAsync، جدید بودن یک تصویر را بر اساس RoomId آن بررسی کرد. اگر این RoomId مساوی صفر بود، یعنی تازه به لیست اضافه شده‌است و حاصل بارگذاری اولیه‌ی فرم ویرایش اطلاعات نیست:
    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages.Where(x => x.RoomId == 0))
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
در قسمت بعد، کدهای حذف اطلاعات اتاق‌ها و تصاویر مرتبط با هر کدام را نیز تکمیل خواهیم کرد.


یک نکته: متد AddHotelRoomImageAsync اضافی است!

چون از AutoMapper استفاده می‌کنیم، در ابتدای متد ثبت یک اتاق، کار نگاشت DTO، به موجودیت متناظر با آن انجام می‌شود:
public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO)
{
   var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
یعنی در اینجا چون خاصیت مجموعه‌ای HotelRoomImages موجود در HotelRoomDTO با نمونه‌ی مشابه آن در HotelRoom هم نام است، به صورت خودکار توسط AutoMapper به آن انتساب داده می‌شود و چون رابطه‌ی one-to-many در EF-Core تنظیم شده، همینقدر که hotelRoom حاصل، به همراه HotelRoomImages از پیش مقدار مقدار دهی شده‌است، به صورت خودکار آن‌ها را جزئی از اطلاعات همین اتاق ثبت می‌کند.
مقدار دهی RoomId یک تصویر، در اینجا غیرضروری است؛ چون RoomId و Room، به عنوان کلید خارجی این رابطه تعریف شده‌اند که در اینجا Room یک تصویر، دقیقا همین اتاق در حال ثبت است و EF Core در حین ثبت نهایی، آن‌را به صورت خودکار در تمام تصاویر مرتبط نیز مقدار دهی می‌کند.
یعنی نیازی به چندین بار رفت و برگشت تعریف شده‌ی در متد AddHotelRoomImageAsync نیست و اساسا نیازی به آن نیست؛ نه برای ثبت و نه برای ویرایش اطلاعات!


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-17.zip
مطالب
Blazor 5x - قسمت ششم - مبانی Blazor - بخش 3 - چرخه‌های حیات کامپوننت‌ها
در این قسمت می‌خواهیم انواع رویدادهای چرخه‌ی حیات یک کامپوننت را بررسی کنیم. به همین جهت ابتدا دو کامپوننت جدید Lifecycle.razor و Lifecycle‍Child.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\LearnBlazor‍Components\Lifecycle‍Child.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، یک کامپوننت دیگر نیز به نام Lifecycle‍Child.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 ارسالی به کامپوننت Lifecycle‍Child می‌شود و پس از آن، رویداد 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
مطالب
ارتباط بین کامپوننت‌ها در Vue.js - قسمت دوم استفاده از Event Bus
در قسمت قبلی، نحوه ارتباط بین کامپوننت‌های Parent و Child را مورد بررسی قرار دادیم و اینکه چگونه داده‌هایشان را به اشتراک میگذارند؛ اما چند موضوع در روش قبلی مورد بحث می‌باشد:

1)  مدیریت nested componentها برای استفاده از چنین روشی مشکل است.
2) اگر تعداد داده‌های اشتراکی زیاد باشد، مدیریت آنها با استفاده از props گیج کننده می‌باشد.

3) اگر دو کامپوننت مجزا (Sibling Component) قصد به اشتراک گذاری اطلاعاتی را داشتند تکلیف چیست؟ (هر چند با استفاده از یک کامپوننت Parent مشترک مقدور می‌باشد)

4) روش ساده‌تری برای ارتباط کامپوننت‌های Parent و Child  و همچنین Sibling Component وجود دارد.

 



استفاده از Event Bus:

با استفاده از EventBus، بسیاری از معایب مطرح شده در روش قبلی را نخواهیم داشت:



 تعریف Event Bus: یک Design Pattern ^ ,^  می باشد. در Vue.js یک نمونه از vue را بصورت سراسری (global) ایجاد میکنیم و درکامپوننت‌هایی که نیاز به ارتباط دارند، آن را فراخوانی (import) و با استفاده از متدهای emit$ و on$، ارتباط را ایجاد میکنیم.


یک فایل جاوا اسکریپتی را با نامی دلخواه (eventBus.js) در فولدر src ایجاد میکنیم و یک نمونه از Vue را در آن وهله سازی میکنیم:

import Vue from 'vue'
export default new Vue()
سپس در کامپوننت‌هایی که قصد ارتباط دارند، این فایل را import میکنیم ( مثال سبد خرید را در مقاله‌ی قبلی، به روش جاری تغییر میدهیم). 

در کامپوننت Shop-Button-Add، کد زیر را در قسمت script اضافه میکنیم:

import  EventBus  from "../eventBus";

کد تابع buttonClicked را بشکل زیر تغییر میدهیم:

 buttonClicked() {
      EventBus.$emit("shop-button-clicked", this.item);
    }

در کامپوننت App.vue  هم کد زیر را در قسمت script اضافه میکنیم:

import  EventBus  from "./eventBus";

و در تابع mounted که از توابع life cycle مربوط به Vue.js میباشد، کد زیر را اضافه میکنیم:

mounted() {
    EventBus.$on("shop-button-clicked", item => {
      this.updateCart(item);
    });
  }


مقایسه‌ی روش استفاده از EventBus با روش قبلی :

مراحل انجام کار در روش قبلی:

دو کامپوننت ارتباط داشتند Shop-Button-Add و App.vue: 

1) در کامپوننت Shop-Button-Add با زدن دکمه Add To Cart متد buttonClicked اجرا میشد.

2) متد buttonClicked  یک سیگنال به کامپوننت Parent خود (Shop-Item) ارسال می‌نمود.

  this.$emit('button-clicked')

3) در کامپوننت Shop-Item مشخص شده بود در صورت ارسال سیگنال از Shop-Button-Add، متد addToCart اجرا شود.

 <Shop-Button-Add    @button-clicked="addToCart(item)"     :item="item">
            <p>Add To Cart</p>
 </Shop-Button-Add>

4) اجرای متد addToCart در کامپوننت Shop-Item یک سیگنال را به کامپوننت App.vue به همراه دیتای مورد نظر ارسال می‌نمود.

addToCart(item) {
                this.$emit('update-cart', item)
            }

5) در کامپوننت App.vue مشخص شده بود با ارسال سیگنال از کامپوننت Shop-Item، متد updateCart اجرا شود.

<shop-item v-for="item in this.items" :item="item" :key="item.id" 
@update-cart="updateCart">
</shop-item>

6) در نهایت کار بروزرسانی سبد خرید با اجرای متد updateCart انجام میشد:

updateCart(e) {
      this.cart.push(e);
      this.total = this.shoppingCartTotal;
    }


نتیجه گیری:

مزایای استفاده از Event Bus :

1) کم شدن مراحل ارتباط بین کامپوننت‌ها

2) حل مشکل ارتباطی بین Sibling Component 

3) نوشتن کد کمتر


 کد سبد خرید به روش مقاله‌ی جاری-استفاده از EventBus  


نکته: برای اجرای برنامه و دریافت پکیج‌های مورد استفاده در مثال جاری، نیاز است دستور زیر را اجرا کنید:  

npm install

همچنین نیاز هست تا پکیچ node-sass را نیز با دستور زیر برای این مثال نصب کنید.

npm install node-sass
مطالب
Blazor 5x - قسمت پنجم - مبانی Blazor - بخش 2 - کامپوننت‌ها
انتقال محتوای کامپوننت Index به یک کامپوننت جدید و تعریف مسیریابی و مدخل منوی آن

پیش از ادامه‌ی مثال قسمت قبل، قصد داریم تمام کدهای موجود در فایل Pages\Index.razor را به یک فایل اختصاصی آن‌ها منتقل کرده و مسیریابی و منوی آن‌را تکمیل کنیم. به همین جهت در پوشه‌ی Pages، یک پوشه‌ی جدید را به نام LearnBlazor ایجاد کرده و درون آن، فایل خالی BindProp.razor را ایجاد می‌کنیم. سپس تمام محتوای فایل Pages\Index.razor را cut کرده و به درون فایل جدید Pages\LearnBlazor\BindProp.razor، منتقل و Paste می‌کنیم.
پس از این تغییرات، در فایل Pages\Index.razor، مهم‌ترین سطر آن، همان اولین سطر تعریف مسیریابی آن خواهد بود و هر محتوای دلخواهی که علاقمند بودید:
@page "/"

<h1>Hello, world!</h1>
در ادامه چون می‌خواهیم گزینه‌ی منوی جدیدی را برای BindProp.razor تعریف کنیم، سطر اول آن‌را به صورت زیر تغییر می‌دهیم:
@page "/bindprop"
با اینکار، این کامپوننت صرفنظر از محل قرارگیری آن که اکنون در پوشه‌ی Pages\LearnBlazor است، در مسیر https://localhost:5001/bindprop قابل دسترسی خواهد شد. اما چگونه باید مدخل منوی جدیدی را برای آن تعریف کرد؟ برای اینکار به فایل Shared\NavMenu.razor مراجعه کرده و دقیقا شبیه به ساختار مداخل منوهای Home ، Counter و غیره، مدخل جدیدی را برای آن تعریف می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="bindprop">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Bind Properties
    </NavLink>
</li>
در اینجا برچسب مدخل جدید تعریف شده، Bind Properties است و href لینک به آن، دقیقا به مسیریابی تعریف شده‌ی در فایل BindProp.razor اشاره می‌کند.



نمایش لیست اتاق‌های تعریف شده، به همراه ویژگی‌های آن‌ها

در قسمت قبل، نمایش ردیفی لیست اتاق‌های تعریف شده را مشاهده کردید. در این قسمت می‌خواهیم هر اتاق تعریف شده را در یک card جداگانه نمایش دهیم. هدف این است که در ابتدا به یک UI متداول شلوغ برسیم و بعد شروع کنیم به Refactoring این UI پیچیده، به کامپوننت‌های کوچک‌تر تشکیل دهنده‌ی آن، جهت مدیریت ساده‌تر این UI و درک بهتر آن. بنابراین در ابتدا با یک کامپوننت کلی شلوغ، شروع خواهیم کرد.
به همین جهت فایل جدید Pages\LearnBlazor\DemoHotel.razor را برای نمایش لیست اتاق‌های موجود اضافه می‌کنیم. سپس محتوای آن‌را به صورت زیر تغییر خواهیم داد:
@page "/demoHotel"

<h3>Hotel Rooms</h3>
<div class="border p-2 mt-2" style="background-color:azure">
    <h2 class="text-info">Rooms List</h2>
    <div class="row container">
        @foreach (var room in Rooms)
        {
            <div class="bg-light border p-2 col-5 ml-2">
                <h4 class="text-secondary">Room - @room.Id</h4>

                @room.Name<br />
                @room.Price.ToString("c")<br />
                <input type="checkbox" @bind-value="room.IsActive" checked="@(room.IsActive?"checked":null)" /> &nbsp; Is Active<br />
                <span>This room is @(room.IsActive?"Active": "InActive")</span>

                @if (room.IsActive)
                {
                    @foreach (var roomProp in room.RoomProps)
                    {
                        <p>@roomProp.Name - @roomProp.Value</p>
                    }
                }

                <input type="button" class="btn btn-danger" value="Delete" />
                <input type="button" class="btn btn-success" value="Edit" />
            </div>
        }
    </div>
</div>
- قسمت کدهای آن که در اینجا ذکر نشده (code@)، با قسمت کدهای کامپوننت Pages\LearnBlazor\BindProp.razor که در قسمت قبل تهیه کردیم، یکی است و هدف از آن، ارائه‌ی List<BlazorRoom> Rooms است که در کدهای razor جاری استفاده شده‌است.
- سپس مسیریابی منتهی به این کامپوننت، به آدرس demoHotel/ تنظیم شده‌است. این مسیریابی را در کامپوننت Shared\NavMenu.razor به صورت زیر مورد استفاده قرار خواهیم داد تا مدخل منوی جدیدی برای آن تهیه شود:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="demoHotel">
      <span class="oi oi-list-rich" aria-hidden="true"></span> Demo Hotel
    </NavLink>
</li>
- در این کامپوننت، با ایجاد حلقه‌ای بر روی لیست اتاق‌ها، مشخصات هر کدام نمایش داده می‌شود. همچنین در اینجا اگر اتاق در حال نمایش فعال باشد، لیست خواص آن نیز درج خواهد شد. به علاوه دو دکمه‌ی جدید حذف و ویرایش نیز در انتهای هر برگه اضافه شده‌است:



تبدیل دکمه‌های حذف و ویرایش هر اتاق به یک کامپوننت جدید

اکنون می‌خواهیم کامپوننت شلوغ Pages\LearnBlazor\DemoHotel.razor را به چند زیر کامپوننت بشکنیم تا هر کدام وظایف خاص خود را انجام دهند و در نهایت به یک UI قابل درک‌تر برسیم. برای مثال می‌خواهیم دکمه‌های حذف و ویرایش هر اتاق را به یک کامپوننت جدید منتقل کنیم تا هم این UI خلوت‌تر شود و هم اگر در قسمت دیگری از برنامه نیاز به یک چنین دکمه‌هایی بود، بتوان از آن کامپوننت اختصاصی، استفاده‌ی مجدد کرد.
برای این منظور ابتدا پوشه‌ی جدید Pages\LearnBlazor\LearnBlazor‍Components را افزوده و سپس در داخل آن، فایل جدید کامپوننت EditDeleteButton.razor را نیز ایجاد می‌کنیم. در این فایل جدید در ابتدا کدهای دو دکمه‌ی تعریف شده را از کامپوننت DemoHotel.razor انتخاب و cut کرده و سپس در این فایل جدید paste می‌کنیم. در این کامپوننت جدید، نیازی به تعریف page@ و مسیریابی آن نیست. به این معنا که این کامپوننت، یک کامپوننت اشتراکی است و routable نیست و قرار است در داخل یک کامپوننت دیگر مورد استفاده قرار گیرد.
بنابراین تا اینجا محتوای کامپوننت EditDeleteButton.razor فقط از دو سطر زیر تشکیل می‌شود:
<input type="button" class="btn btn-danger" value="Delete" />
<input type="button" class="btn btn-success" value="Edit" />
در ادامه برای درج این کامپوننت در حلقه‌ی نمایشی آن در کامپوننت DemoHotel، باید به صورت زیر عمل کرد که به فضای نام کامل این کامپوننت اشاره می‌کند:
<BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton></BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton>
برای اینکه مجبور به تعریف یک چنین نام طولانی نباشیم، می‌توان فضای نام پوشه‌ی آن‌را در انتهای فایل Imports.razor_ قرار داد:
@using BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents
البته اگر قرار نیست از این کامپوننت در سایر کامپوننت‌ها استفاده شود و فقط یک محل استفاده را دارد، می‌توان این using را در بالای تعاریف فایل DemoHotel.razor نیز قرار داد.

اکنون می‌توان تعریف مدخل کامپوننت را به صورت زیر خلاصه کرد:
<EditDeleteButton></EditDeleteButton>


ارسال پارامترها به یک کامپوننت

فرض کنید قصد داریم دکمه‌های ویرایش و حذف را تنها به کاربران ادمین نمایش دهیم. به همین جهت نیاز است بتوان پارامتری مانند IsAdmin را به کامپوننت EditDeleteButton ارسال کرد. برای اینکار کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\EditDeleteButton.razor را به صورت زیر ویرایش می‌کنیم:
@if (IsAdmin)
{
    <input type="button" class="btn btn-danger" value="Delete" />
    <input type="button" class="btn btn-success" value="Edit" />
}

@code
{
    [Parameter]
    public bool IsAdmin { get; set; }
}
در اینجا خواص عمومی مزین شده‌ی با ویژگی Parameter، به عنوان پارامتر ورودی کامپوننت عمل می‌کنند. برای نمونه بر اساس مقدار خاصیت IsAdmin، توسط یک if@ تصمیم خواهیم گرفت که آیا قسمتی از UI نمایش داده شود یا خیر؟

پس از تعریف این پارامتر ورودی، روش استفاده‌ی از آن در کامپوننت DemoHotel به صورت زیر است:
<EditDeleteButton IsAdmin="true"></EditDeleteButton>


انتقال هر اتاق به کامپوننت مجزای خاص خودش

در ادامه می‌خواهیم محتوای حلقه‌ی foreach (var room in Rooms)@ کامپوننت DemoHotel را به طور کامل cut کرده و در یک کامپوننت جدید paste کنیم تا به حلقه‌ای خواناتر و با مسئولیت‌های کمتری برسیم. نگهداری کدهایی که قسمت‌های مختلف آن از هم ایزوله شده‌اند و دامنه‌ی تغییرات آن‌ها کاملا مشخص و محدود است، در طول زمان بسیار ساده‌تر از نگهداری کدهای UI ای در هم تنیده‌است.
به همین جهت ابتدا کامپوننت جدید Pages\LearnBlazor\LearnBlazor‍Components\IndividualRoom.razor را ایجاد می‌کنیم و سپس، هر آنچه داخل حلقه‌ی foreach یاد شده قرار دارد را انتخاب و cut کرده و درون این کامپوننت جدید paste می‌کنیم:
<div class="bg-light border p-2 col-5 offset-1">
    <h4 class="text-secondary">Room - @Room.Id</h4>

    @Room.Name<br />
    @Room.Price.ToString("c")<br />
    <input type="checkbox" @bind-value="Room.IsActive" checked="@(Room.IsActive?"checked":null)" /> &nbsp; Is Active<br />
    <span>This room is @(Room.IsActive?"Active": "InActive")</span>

    @if (Room.IsActive)
    {
        @foreach (var roomProp in Room.RoomProps)
        {
            <p>@roomProp.Name - @roomProp.Value</p>
        }
    }

    <EditDeleteButton IsAdmin="true"></EditDeleteButton>
</div>

@code
{
    [Parameter]
    public BlazorRoom Room { get; set; }
}
در اینجا پس از paste کدهای داخل حلقه، نیاز به یک پارامتر ورودی که همان شیء Room در حال رندر است، خواهد بود. به همین جهت پارامتر آن‌را تعریف کرده و همچنین کدهای موجود را نیز اندکی ویرایش می‌کنیم، تا از نام این پارامتر جدید استفاده کند.

پس از این تغییر، کدهای حلقه‌ی foreach کامپوننت DemoHotel.razor به صورت زیر خلاصه می‌شوند. در اینجا روش ارسال یک شیء را به پارامتر Room نیز مشاهده می‌کنید (البته ذکر @ در اینجا الزامی نیست و می‌شد از روش مقدار دهی "Room="room نیز استفاده کرد):
<div class="row container">
  @foreach (var room in Rooms)
  {
    <IndividualRoom Room="@room"></IndividualRoom>
  }
</div>
در اینجا می‌توان سلسه مراتب کامپوننت‌ها را مشاهده کرد. کامپوننت DemoHotel، کامپوننت IndividualRoom را فراخوانی می‌کند و این کامپوننت نیز کامپوننت EditDeleteButton را مورد استفاده قرار می‌دهد.


یک تمرین: نمایش لیست امکانات رفاهی هتل

پس از نمایش لیست اتاق‌های یک هتل، قصد داریم لیست امکانات رفاهی آن‌را نیز نمایش دهیم:


 مدل این امکانات را به صورت زیر به پوشه‌ی Models برنامه اضافه می‌کنیم:
namespace BlazorServerSample.Models
{
    public class BlazorAmenity
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }
    }
}
از آنجائیکه قصد داریم لیست آن‌ها را در همان کامپوننت DemoHotel.razor نمایش دهیم، این لیست را به صورت زیر تشکیل می‌دهیم:
@code{

    List<BlazorAmenity> AmenitiesList = new List<BlazorAmenity>();
    // ...

    protected override void OnInitialized()
    {
        base.OnInitialized();

        // ...

        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 111,
            Name = "Gym",
            Description = "24x7 gym room is available."
        });
        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 222,
            Name = "Swimming Pool",
            Description = "Pool room is open from 6am to 10pm."
        });
        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 333,
            Name = "Free Brakfast",
            Description = "Enjoy free breakfast at out hotel."
        });
    }
}
در ابتدا فیلد List<BlazorAmenity> AmenitiesList جهت دسترسی به لیست امکانات رفاهی تعریف شده و سپس آن‌را در رویدادگردان OnInitialized، مقدار دهی اولیه کرده‌ایم. در مورد این متدهای چرخه‌ی حیات، در قسمت بعدی بیشتر بحث خواهیم کرد.

اکنون برای نمایش تک تک عناصر این لیست، ابتدا یک کامپوننت منحصر به یک BlazorAmenity را به نام Pages\LearnBlazor\LearnBlazor‍Components\IndividualAmenity.razor ایجاد می‌کنیم با این محتوا:
<div class="bg-light border p-2 col-5 offset-1 mt-2">
    <h4 class="text-secondary">Amenity - @Amenity.Id</h4>

    @Amenity.Name<br />
    @Amenity.Description<br />
</div>

@code
{
    [Parameter]
    public BlazorAmenity Amenity { get; set; }
}
این کامپوننت، یک شیء BlazorAmenity را به عنوان پارامتر دریافت کرده و سپس Id، نام و توضیحات آن‌را نمایش می‌دهد.

در آخر پس از تعریف کامپوننت IndividualAmenity.razor، روش استفاده‌ی از آن در کامپوننت DemoHotel به صورت زیر است:
<div class="col-12 mt-4">
    <h4 class="text-info">Hotel Amenities</h4>
</div>
@foreach (var amenity in AmenitiesList)
{
    <IndividualAmenity Amenity="@amenity"></IndividualAmenity>
}
در اینجا بر روی لیست امکانات، یک حلقه را تشکیل داده و سپس توسط کامپوننت IndividualAmenity، هر کدام از امکانات را جداگانه نمایش داده‌ایم.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-05.zip