مطالب
Blazor 5x - قسمت 30 - برنامه‌ی Blazor WASM - افزودن پرداخت آنلاین توسط درگاه مجازی پرباد
در ادامه‌ی تمرین قسمت قبل که مقدمات ثبت درخواست رزرو یک اتاق را فراهم کردیم، اکنون می‌خواهیم اگر کاربری بر روی دکمه‌ی checkout now یک اتاق کلیک کرد، به درگاه مجازی پرباد منتقل شده، پرداخت را تکمیل کند، به برنامه هدایت شود و در آخر درخواست او در سیستم ثبت گردد. مزیت کار کردن با درگاه مجازی پرباد، امکان آزمایش محلی برنامه، بدون نیاز به یک درگاه بانکی واقعی است و زمانیکه قرار است با یک درگاه بانکی واقعی کار شود، فقط قسمت معرفی و تنظیمات ابتدایی مشخصات درگاه بانکی آن باید تغییر کند و نه هیچ قسمت دیگری از کدهای برنامه.


نصب پرباد و انجام تنظیمات اولیه‌ی آن

بسته‌های نیوگت پرباد را در دو پروژه‌ی زیر نصب خواهیم کرد:
الف) پروژه‌ی Web API (و یا همان BlazorWasm.WebApi در مثال این سری):
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Parbad.AspNetCore" Version="1.1.0" />
    <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
  </ItemGroup>
</Project>
که شامل بسته‌ها‌ی ASP.NET Core آن و همچنین محل ذخیره سازی مبتنی بر EF-Core آن است.

ب) پروژه‌ای که محل قرارگیری فایل‌های Migration است (و یا همان BlazorServer.DataAccess) در این مثال:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
  </ItemGroup>
</Project>
که در اینجا فقط نیاز به بسته‌ی EF-Core آن است تا بتوان Context مخصوص پرباد را در حین اعمال مهاجرت‌ها شناسایی کرد.

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

        public void ConfigureServices(IServiceCollection services)
        {
           // ...

            var connectionString = Configuration.GetConnectionString("DefaultConnection");

            services.AddParbad()
                    .ConfigureHttpContext(httpContextBuilder => httpContextBuilder.UseDefaultAspNetCore())
                    .ConfigureGateways(gatewayBuilder =>
                    {
                        gatewayBuilder
                            .AddParbadVirtual()
                            .WithOptions(gatewayOptions => gatewayOptions.GatewayPath = "/MyVirtualGateway");
                    })
                    .ConfigureStorage(storageBuilder =>
                    {
                        storageBuilder.UseEfCore(efCoreOptions =>
                            {
                                var assemblyName = typeof(ApplicationDbContext).Assembly.GetName().Name;
                                efCoreOptions.ConfigureDbContext = db =>
                                    db.UseSqlServer(
                                        connectionString,
                                        sqlServerOptionsAction: sqlOptions => sqlOptions.MigrationsAssembly(assemblyName)
                                    );
                            });
                    })
                    .ConfigureAutoTrackingNumber(opt => opt.MinimumValue = 1)
                    .ConfigureOptions(parbadOptions =>
                    {
                        // parbadOptions.Messages.PaymentSucceed = "YOUR MESSAGE";
                    });

           // ...
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
           // ...

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            if (env.IsDevelopment())
            {
                app.UseParbadVirtualGatewayWhenDeveloping();
            }
            else
            {
                app.UseParbadVirtualGateway();
            }
        }
    }
}
چند نکته:
- در متد ConfigureGateways می‌توان چندین درگاه را معرفی کرد که برای مثال در اینجا از درگاه مجازی و محلی آن استفاده شده‌است.
- در متد ConfigureStorage، تنظیمات EF-Core آن‌را مشاهده می‌کنید. پرباد به همراه DbContext خاص خودش است. یعنی در این حالت برنامه‌ی شما حداقل دو DbContext خواهد داشت؛ یکی ApplicationDbContext و دیگری ParbadDataContext.
- می‌خواهیم شماره‌ی تراکنش‌ها را به صورت خودکار توسط پرباد مدیریت کنیم. به همین جهت می‌توان عدد ابتدای آن‌را توسط متد ConfigureAutoTrackingNumber مشخص کرد.
- در پایان هم تعاریف مسیریابی میان‌افزار آن‌را مشاهده می‌کنید که می‌تواند برای حالت توسعه و ارائه‌ی نهایی متفاوت باشد.


تکمیل خواص موجودیت RoomOrderDetail جهت کار با پرباد

موجودیت RoomOrderDetail را در قسمت قبل معرفی کردیم. پرباد به ازای هر تراکنش بانکی که صورت می‌گیرد، یا نیاز به یک TrackingNumber خودکار را دارد و یا دستی. یعنی یا می‌توانیم شماره تراکنش خاص خودمان را تولید کنیم و در اختیار آن قرار دهیم و یا از آن درخواست کنیم تا این شماره را مدیریت کرده و به صورت خودکار تولید کند. در هر دو حالت نیاز است این شماره را به ردیف‌های جدول جزئیات سفارشات اتاق‌های هتل اضافه کرد که در این مثال ParbadTrackingNumber نام دارد:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        // ...

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

        public bool IsPaymentSuccessful { get; set; }

        public string Status { get; set; }
    }
}
همچنین در پایان عملیات هم فیلدهای IsPaymentSuccessful و وضعیت اتاق را تکمیل می‌کنیم.


ایجاد جداول متناظر با ParbadDataContext

همانطور که عنوان شد، اکنون برنامه به همراه دو DbContext است. بنابراین در این حالت در حین اجرای مهاجرت‌ها، ذکر نام Context مدنظر اجباری است.
برای ایجاد مهاجرت‌های متناظر با ParbadDataContext، از طریق خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build

dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbadFields --context ApplicationDbContext
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbad --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext

dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext
چون برنامه دو Context ای است، نیاز است دوبار دستور تولید مهاجرت‌ها و دوبار دستور اعمال آن‌ها را به بانک اطلاعاتی صادر کرد که روش آن‌را در دستورات فوق مشاهده می‌کنید. پس از این دستورات، بانک اطلاعاتی برنامه شامل دو جدول جدید مخصوص پرباد خواهد بود:



روش یکپارچه سازی پرباد با یک برنامه‌ی SPA

روش متداول کار با پرباد، بر اساس طراحی مخصوص ASP.NET Core آن است. ابتدا درخواستی را به آن ارسال می‌کنید. سپس پرباد شماره تراکنشی را تولید کرده و شروع تراکنش را در بانک اطلاعاتی ثبت می‌کند. در ادامه به صورت خودکار، کار ارسال اطلاعات به درگاه بانکی (برای مثال ارسال تمام فیلدهای یک فرم ویژه‌ی آن بانک، بر اساس مستندات آن) و هدایت به درگاه بانکی را انجام می‌دهد. پس از پایان کار پرداخت، کار هدایت به اکشن متد دریافت تائیدیه‌ی نهایی صورت می‌گیرد و همینجا کار به پایان می‌رسد. این روش هرچند برای برنامه‌های سمت سرور ASP.NET Core کار می‌کند، اما ... به همین نحو با برنامه‌های تک صفحه‌ای وب مانند Blazor WASM قابل استفاده نیست. در اینجا روش تبادل اطلاعات با اکشن متدهای وب سرویس‌های برنامه از طریق یک HttpClient است و در این حالت دیگر نمی‌توان از مزایای Post و Redirect خودکار پرباد که در سمت سرور صورت می‌گیرد استفاده کرد. با استفاده از HttpClient، یک شیء را به سمت Web API ارسال می‌کنیم و در پاسخ، فقط یک شیء را دریافت می‌کنیم. در اینجا دیگر خبری از Redirect به درگاه اصلی بانکی و Post اطلاعات به آن نیست. بنابراین روش کار با پرباد در اینجا به صورت زیر خواهد بود:
الف) شماره Id سفارش و مبلغ نهایی آن‌را از طریق یک درخواست Get معمولی به اکشن متدی در سمت سرور ارسال می‌کنیم. یعنی نیاز است ابتدا Url زیر را تشکیل داد که شماره سفارش و مبلغ آن، به صورت کوئری استرینگ‌هایی به اکشن متد PayRoomOrder ارسال می‌شوند:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
برنامه‌ی کلاینت برای اینکه بتواند این هدایت را انجام دهد، نیاز به نکته‌ی خاصی را دارد که در ادامه توضیح داده خواهد شد.
ب) اکنون چون یک redirect سمت سرور صورت گرفته، به صورت معمولی در اکشن متد PayRoomOrder با پرباد پردازش صورت گرفته و به سمت درگاه هدایت می‌شویم. پس از پرداخت نهایی، باز هم به صورت خودکار به اکشن متد دیگری جهت تائید عملیات هدایت خواهیم شد.
ج) در پایان کار، اکشن متد سمت سرور، ما را به سمت کامپوننتی در برنامه‌ی کلاینت Redirect خواهد کرد:
https://localhost:5002/payment-result/OrderId/TrackingNumber/Message
در اینجا شماره سفارش ابتدایی که مشخص است. همان شماره‌ای است که کار را با آن از سمت کلاینت آغاز کردیم. نکته‌ی مهم، TrackingNumber تراکنش است که بر اساس آن رکورد متناظری یافت شده و وضعیت نهایی آن‌را به کاربر نمایش می‌دهیم.

بنابراین روش یکپارچه سازی پربابد با برنامه‌های SPA، بر اساس Redirect‌های کامل است که سبب بارگذاری مجدد کل صفحه و آدرس‌ها می‌شوند و در اینجا از HttpClient برای کار با پرباد استفاده نخواهیم کرد؛ چون تمام اعمال خودکار آن‌را از دست خواهیم داد و مجبور به بازنویسی آن‌ها خواهیم شد که در دراز مدت با تغییرات این کتابخانه، قابل نگهداری نخواهند بود. بنابراین بهتر است خود پرباد کار Redirect‌ها و ارسال اطلاعات به درگاه‌های بانکی را مدیریت کند و نه ما از طریق کار با یک HttpClient.


آشنایی با گردش کار برنامه

در این مثال، مراحل زیر را طی خواهیم کرد:

1- شروع به انتخاب یک بازه‌ی زمانی و تعداد شب اقامت


2- انتخاب یک اتاق از لیست اتاق‌ها با کلیک بر روی دکمه‌ی Book آن


3- کلیک بر روی دکمه‌ی checkout، در صفحه‌ی مشاهده‌ی جزئیات اتاق و شروع به پرداخت


4- هدایت به درگاه مجازی پرباد در سمت برنامه‌ی Web API


5- پرداخت و هدایت خودکار به سمت برنامه‌ی Web API، جهت تائید نهایی


6- هدایت نهایی به سمت برنامه‌ی کلاینت، جهت نمایش اطلاعات پرداخت



ایجاد کنترلر پرداخت، توسط درگاه مجازی پرباد

پس از آشنایی با گردش کاری اطلاعات در اینجا، نیاز است بتوان لینک زیر را در برنامه‌ی کلاینت تولید کرد و سپس کاربر را به سمت اکشن متد PayRoomOrder هدایت نمود:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
این اکشن متد و کنترلر آن به صورت زیر تهیه می‌شود:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class ParbadPaymentController : Controller
    {
        private readonly IConfiguration _configuration;
        private readonly IOnlinePayment _onlinePayment;
        private readonly IRoomOrderDetailsService _roomOrderService;

        public ParbadPaymentController(
            IConfiguration configuration,
            IOnlinePayment onlinePayment,
            IRoomOrderDetailsService roomOrderService)
        {
            _configuration = configuration;
            _onlinePayment = onlinePayment ?? throw new ArgumentNullException(nameof(onlinePayment));
            _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
        }

        [HttpGet]
        public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
        {
            var verifyUrl = Url.Action(
                    action: nameof(ParbadPaymentController.VerifyRoomOrderPayment),
                    controller: nameof(ParbadPaymentController).Replace("Controller", string.Empty),
                    values: null, protocol: Request.Scheme);

            var result = await _onlinePayment.RequestAsync(invoiceBuilder =>
                invoiceBuilder.UseAutoIncrementTrackingNumber()
                            .SetAmount(amount)
                            .SetCallbackUrl(verifyUrl)
                            .UseParbadVirtual()
            );

            if (result.IsSucceed)
            {
                await _roomOrderService.UpdateRoomOrderTrackingNumberAsync(orderId, result.TrackingNumber);

                // It will redirect the client to the gateway.
                return result.GatewayTransporter.TransportToGateway();
            }
            else
            {
                return Redirect(getClientReturnUrl(orderId, result.TrackingNumber, result.Message));
            }
        }

        [HttpGet, HttpPost]
        public async Task<IActionResult> VerifyRoomOrderPayment()
        {
            var invoice = await _onlinePayment.FetchAsync();
            var orderDetail = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(invoice.TrackingNumber);
            if (invoice.Status == PaymentFetchResultStatus.AlreadyProcessed)
            {
                return Redirect(getClientReturnUrl(orderDetail.Id, invoice.TrackingNumber, "The payment is already processed."));
            }

            var verifyResult = await _onlinePayment.VerifyAsync(invoice);
            if (verifyResult.Status == PaymentVerifyResultStatus.Succeed)
            {
                var result = await _roomOrderService.MarkPaymentSuccessfulAsync(verifyResult.TrackingNumber, verifyResult.Amount);
                if (result == null)
                {
                    return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, "Can not mark payment as successful"));
                }
                return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
            }
            return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
        }

        private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
        {
            var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
            return new Uri(new Uri(clientBaseUrl),
                $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
        }
    }
}
توضیحات:
در اینجا کدهای کامل ParbadPaymentController مشاهده می‌کنید.

- گردش کاری پرداخت، با فراخوانی اکشن متد PayRoomOrder شروع می‌شود که دو پارامتر شماره سفارش و مبلغ آن‌را دریافت می‌کند.
[HttpGet]
public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
نوع آن هم عمدا، HttpGet درنظر گرفته شده‌است تا دقیقا مشخص باشد که فقط با Redirect کامل به آن (هدایت کامل از سمت کلاینت به سمت سرور)، کار خواهد کرد و هدف دیگری را دنبال نمی‌کند.

- در اکشن متد PayRoomOrder، نیاز است لینک بازگشت از درگاه بانکی را مشخص کنیم. پس از اینکه کاربر پرداختی را انجام داد، مجددا به صورت خودکار، به سمت آدرسی در همین Web API و نه برنامه‌ی سمت کلاینت هدایت می‌شود؛ چون هنوز کار پرباد به پایان نرسیده و باید عملیات انجام شده را تصدیق کند. به همین جهت ابتدا آدرس اکشن متدی که کار تائید نهایی را انجام می‌دهد، تولید کرده و به متد RequestAsync آن به همراه مبلغ نهایی و نوع درگاه، ارسال می‌کنیم.

- استفاده از UseAutoIncrementTrackingNumber سبب می‌شود تا پرباد خودش مدیریت TrackingNumber را انجام دهد که پس از پایان عملیات، توسط خاصیت result.TrackingNumber در دسترس خواهد بود.

- پس از پایان عملیات ابتدایی RequestAsync که سشن پرباد را ایجاد کرده و همچنین رکوردی را در بانک اطلاعاتی نیز ثبت می‌کند (در جداول درونی خود پرباد)، نیاز است رکورد سفارشی را که با آن کار را شروع کردیم یافته و TrackingNumber آن‌را با مقدار واقعی دریافتی از پرباد، به روز رسانی کنیم. اینکار توسط متد UpdateRoomOrderTrackingNumberAsync انجام می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task UpdateRoomOrderTrackingNumberAsync(int roomOrderId, long trackingNumber)
        {
            var order = await _dbContext.RoomOrderDetails.FindAsync(roomOrderId);
            if (order == null)
            {
                return;
            }

            order.ParbadTrackingNumber = trackingNumber;
            _dbContext.RoomOrderDetails.Update(order);
            await _dbContext.SaveChangesAsync();
        }
    }
}
بر اساس شماره سفارشی که داریم، رکورد متناظر با آن‌را یافته و سپس trackingNumber تولیدی را در آن به روز رسانی می‌کنیم.

- اکنون با فراخوانی متد ()result.GatewayTransporter.TransportToGateway، دو کار مهم رخ می‌دهند:
الف) ارسال خودکار اطلاعات به سمت درگاه بانکی
ب) Redirect خودکار به سمت درگاه بانگی
به همین جهت است که علاقمند نبودیم تا این مراحل را توسط HttpClient برنامه‌ی Blazor WASM مدیریت و بازنویسی کنیم.

- پس از هدایت به سمت درگاه بانکی و تکمیل پرداخت، اکنون مجددا به همان verifyUrl هدایت می‌شویم. یعنی اکنون به مرحله‌ی پردازش اکشن متد VerifyRoomOrderPayment در سمت Web API رسیده‌ایم.
[HttpGet, HttpPost]
public async Task<IActionResult> VerifyRoomOrderPayment()
در اینجا ابتدا invoice.TrackingNumber در حال پردازش را دریافت می‌کنیم. به کمک این عدد می‌توان رکورد سفارش متناظر با آن‌را یافت. به همین جهت است که آن‌را به لیست فیلدهای جدول سفارشات اضافه کردیم. اینکار هم توسط متد GetOrderDetailByTrackingNumberAsync صورت می‌گیرد:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }
    }
}
- در ادامه پرباد کار تصدیق اطلاعات دریافتی از درگاه بانکی را انجام می‌دهد. دراینجا اگر عملیات با موفقیت مواجه شود، سه فیلدی را که در ابتدای بحث در مورد ثبت اطلاعات تراکنش اضافه کردیم، به روز رسانی می‌کنیم:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(long trackingNumber, long amount)
        {
            var order = await _dbContext.RoomOrderDetails.FirstOrDefaultAsync(x => x.ParbadTrackingNumber == trackingNumber);
            if (order?.IsPaymentSuccessful != false || order.TotalCost != amount)
            {
                return null;
            }

            order.IsPaymentSuccessful = true;
            order.Status = BookingStatus.Booked;
            var markPaymentSuccessful = _dbContext.RoomOrderDetails.Update(order);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(markPaymentSuccessful.Entity);
        }
    }
}
- در اینجا بر اساس trackingNumber، سند متناظری را یافته و سپس بررسی می‌کنیم که آیا مبلغ سند، با مبلغ تائید شده، یکی هست یا خیر؟ اگر خیر، نیاز هست پرداخت را برگشت بزنیم که اینکار توسط متد کنسل پرباد قابل انجام است.

- در تمام این مراحل، کار Redirect به سمت کلاینت و کامپوننت payment-result آن، با فراخوانی متد return Redirect اکشن متدها صورت می‌گیرد که Url آن به صورت زیر تامین می‌شود:
        private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
        {
            var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
            return new Uri(new Uri(clientBaseUrl),
                $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
        }
در این متد Client_URL را از فایل appsettings.json برنامه‌ی Web API دریافت می‌کنیم که به آدرس ریشه‌ی برنامه‌ی کلاینت اشاره می‌کند:
{
   "Client_URL": "https://localhost:5002/"
}


تکمیل قسمت سمت کلاینت عملیات پرداخت بانکی، توسط درگاه مجازی پرباد

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

الف) تکمیل کامپوننت RoomDetails.razor جهت شروع به پرداخت آنلاین
کامپوننت RoomDetails.razor را در قسمت قبل آغاز کردیم و توسعه‌ی آن‌را تا جائی پیش بردیم که اعتبارسنجی‌های آن‌را به علت استفاده‌ی از خواص تو در تو، به صورت دستی انجام دادیم. پس از مرحله‌ی اعتبارسنجی، اکنون می‌خواهیم کاربر را به سمت درگاه بانکی جهت پرداخت، هدایت کنیم:
@page "/hotel-room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService
@inject IClientRoomOrderDetailsService RoomOrderDetailsService
@inject NavigationManager NavigationManager
@inject HttpClient HttpClient

// ...

@code {

    // ...

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

        try
        {
            HotelBooking.OrderDetails.ParbadTrackingNumber = -1;
            HotelBooking.OrderDetails.RoomId = HotelBooking.OrderDetails.HotelRoomDTO.Id;
            HotelBooking.OrderDetails.TotalCost = HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount;
            var roomOrderDetailsSaved = await RoomOrderDetailsService.SaveRoomOrderDetailsAsync(HotelBooking.OrderDetails);

            await LocalStorage.SetItemAsync(ConstantKeys.LocalRoomOrderDetails, roomOrderDetailsSaved);

            var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
                            .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
                            .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
                            .Uri;
            NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }

    // ...
}
متد HandleValidation را در انتهای قسمت قبل تکمیل کردیم. اکنون OrderDetails را بر اساس اطلاعات فرم و انتخاب‌های کاربر، تکمیل کرده و به متد SaveRoomOrderDetailsAsync ارسال می‌کنیم تا Id سفارش را تولید کنیم. این همان Id ای است که قرار است به سمت سرور و Web API ارسال کنیم تا بر اساس آن تراکنش و Tracking Number ای را بتوان به رکورد جاری انتساب داد. بنابراین نیاز به کنترلر سمت Web API ای را داریم که بتواند این‌کار را انجام دهد:
namespace BlazorWasm.WebApi.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class RoomOrderController : Controller
    {
        private readonly IRoomOrderDetailsService _roomOrderService;

        public RoomOrderController(IRoomOrderDetailsService roomOrderService)
        {
            _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
        }

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] RoomOrderDetailsDTO details)
        {
            var result = await _roomOrderService.CreateAsync(details);
            return Ok(result);
        }

        [HttpGet]
        public async Task<IActionResult> GetOrderDetail(int trackingNumber)
        {
            var result = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(trackingNumber);
            return Ok(result);
        }
    }
}
- متد Create، بر اساس اطلاعات وارد شده‌ی توسط کاربر، آن‌ها را تبدیل به یک رکورد سفارش جدید می‌کند و به سمت کلاینت بازگشت می‌دهد.
- متد GetOrderDetail، بر اساس trackingNumber دریافتی از پرباد، کار بازگشت رکورد متناظری را انجام می‌دهد. از آن در پایان کار، جهت نمایش وضعیت پرداخت، استفاده می‌کنیم.
این دو متد در سرویس سمت سرور RoomOrderDetailsService، به صورت زیر تامین شده‌اند:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        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 async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);

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

       // ...
    }
}
اکنون که Web API Endpoint مدنظر را ایجاد کردیم، نیاز است سرویس سمت کلاینتی را نیز جهت تعامل با آن تهیه کنیم:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber);
    }
}

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> GetOrderDetailAsync(long trackingNumber)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/roomorder/GetOrderDetail"))
                            .AddParameter("trackingNumber", trackingNumber.ToString())
                            .Uri;
            return _httpClient.GetFromJsonAsync<RoomOrderDetailsDTO>(uri);

        }

        public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            details.UserId = "unknown user!";
            var response = await _httpClient.PostAsJsonAsync("api/roomorder/create", details);
            var responseContent = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {
                return JsonSerializer.Deserialize<RoomOrderDetailsDTO>(responseContent);
            }
            else
            {
                //var errorModel = JsonSerializer.Deserialize<ErrorModel>(responseContent);
                throw new InvalidOperationException(responseContent);
            }
        }
    }
}
- متد GetOrderDetailAsync بر اساس trackingNumber دریافتی پس از عملیات تصدیق پرداخت، کار بازگشت جزئیات رکورد متناظری را انجام می‌دهد.
- متد SaveRoomOrderDetailsAsync، یک رکورد سفارش جدید را ایجاد می‌کند. در اینجا با روش مشاهده‌ی خطای کامل بازگشتی از سمت سرور (در صورت وجود) هم آشنا شده‌ایم که در مواقع لزوم می‌تواند راه‌گشا باشد.
- در متد SaveRoomOrderDetailsAsync فعلا مقدار UserId اجباری را به عبارتی دلخواه، تنظیم کرده‌ایم. این مورد را در قسمت‌های بعدی با معرفی اعتبارسنجی و احراز هویت سمت کلاینت، تکمیل خواهیم کرد.

این سرویس جدید را هم باید به سیستم تزریق وابستگی‌های برنامه‌ی کلاینت معرفی کرد تا قابل استفاده شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
بنابراین در متد HandleCheckout ای که در حال بررسی آن هستیم، ابتدا متد SaveRoomOrderDetailsAsync فوق فراخوانی می‌شود تا توسط Web API Endpoint متناظری، یک رکورد سفارش ابتدایی را ایجاد کرده و Id آن‌را در اختیار ما قرار دهد.
سپس به قطعه کد مهم زیر می‌رسیم:
var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
    .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
    .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
    .Uri;
NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
اینجا است که برای نمونه آدرس https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000 ساخته شده و توسط متد NavigateTo فراخوانی می‌شود. فراخوانی متداول متد NavigateTo در اینجا کارساز نیست؛ چون سبب reload آدرس درخواستی نمی‌شود. یعنی هدایت‌های صورت گرفته‌ی توسط آن، در همان داخل مرورگر رخ می‌دهند و سبب ارسال درخواستی به سمت سرور نخواهند شد. می‌توان این رفتار را با ذکر پارامتر دوم آن تغییر داد. در اینجا اگر پارامتر forceLoad را به true تنظیم کنیم، ابتدا سبب هدایت به آدرس درخواستی و سپس reload کامل صفحه می‌شود (دقیقا مثل اینکه شخصی، آدرسی را در نوار آدرس مرورگر وارد کند و سپس دکمه‌ی enter را بفشارد). این reload است که برنامه‌ی کلاینت را اکنون به سمت برنامه‌ی Web API هدایت می‌کند.


نمایش وضعیت پرداخت، به کاربر در پایان گردش کاری آن

پس از این مراحل، مرحله‌ی آخر کار باقی مانده‌است؛ یعنی بازگشت از اکشن متد VerifyRoomOrderPayment سمت سرور، به کامپوننت PaymentResult سمت کلاینت، برای نمایش نتیجه‌ی عملیات. به همین جهت کامپوننت جدید Pages\HotelRooms\PaymentResult.razor را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
@inject ILocalStorageService LocalStorage
@inject IClientRoomOrderDetailsService RoomOrderDetailService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager

@if (IsLoading)
{
    <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;">
        <img src="images/ajax-loader.gif" />
    </div>
}
else
{
    <div class="container">
        <div class="row mt-4 pt-4">
            <div class="col-10 offset-1 text-center">
            @if(IsPaymentSuccessful)
            {
                <h2 class="text-success">Booking Confirmed!</h2>
                <p>Your room has been booked successfully with order id @OrderId & tracking number @TrackingNumber .</p>
            }
            else
            {
                <h2 class="text-warning">Booking Failed!</h2>
                <p>@Message</p>
            }
            <a class="btn btn-primary" href="hotel-rooms">Back to rooms</a>
            </div>
        </div>
    </div>
}

@code
{
    private bool IsLoading;
    private bool IsPaymentSuccessful;

    [Parameter] public int OrderId { set; get; }
    [Parameter] public long TrackingNumber { set; get; }
    [Parameter] public string Message { set; get; }

    protected override async Task OnInitializedAsync()
    {
        IsLoading = true;
        try
        {
            var finalOrderDetail = await RoomOrderDetailService.GetOrderDetailAsync(TrackingNumber);
            var localOrderDetail = await LocalStorage.GetItemAsync<RoomOrderDetailsDTO>(ConstantKeys.LocalRoomOrderDetails);
            if(finalOrderDetail is not null &&
                finalOrderDetail.IsPaymentSuccessful &&
                finalOrderDetail.Status == BookingStatus.Booked &&
                localOrderDetail is not null &&
                localOrderDetail.TotalCost == finalOrderDetail.TotalCost)
            {
                IsPaymentSuccessful = true;
                await LocalStorage.RemoveItemAsync(ConstantKeys.LocalRoomOrderDetails);
                await LocalStorage.RemoveItemAsync(ConstantKeys.LocalInitialBooking);
            }
            else
            {
                IsPaymentSuccessful = false;
            }
        }
        catch(Exception ex)
        {
            await JsRuntime.ToastrError(ex.Message);
        }
        finally
        {
            IsLoading = false;
        }
    }
}
این کامپوننت بر اساس مسیریابی که دارد:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
سه پارامتر شماره سفارش، شماره تراکنش و پیامی را پس از پایان عملیات تصدیق پرداخت، از Web API، در طی یک redirect کامل دریافت می‌کند. در ادامه به کمک متد RoomOrderDetailService.GetOrderDetailAsync که آن‌را پیشتر توسعه دادیم، اصل رکورد متناظر با این سفارش را بازیابی کرده و فیلدهای IsPaymentSuccessful و Status آن‌را بررسی می‌کنیم (این فیلدها در زمان تصدیق پرداخت، در همان سمت سرور مقدار دهی می‌شوند). همچنین جهت محکم‌کاری، قسمتی از این اطلاعات را با Local Storage نیز انطباق داده‌ایم. اگر پرداخت، موفقیت آمیز باشد، شماره سفارش و همچنین شماره تراکنش را به کاربر نمایش می‌دهیم و یا پیام دریافتی از سرور را در صفحه درج می‌کنیم.


جلوگیری از ثبت سفارش اتاقی که رزرو شده‌است


پس از پایان عملیات سفارش یک اتاق، بهتر است امکان سفارش اتاقی را که دیگر در دسترس نیست، غیرفعال کنیم (تصویر فوق) که اینکار را می‌توان توسط خاصیت IsBooked مدل UI کامپوننت نمایش لیست اتاق‌ها انجام داد:
    public class HotelRoomDTO
    {
        public bool IsBooked { get; set; }

        // ...
    }
این خاصیت را در متدهای بازگشت لیست تمام اتاق‌ها و یا بازگشت اطلاعات یک اتاق، به صورت زیر محاسبه و مقدار دهی می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
       // ...

        public async Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDateStr, DateTime? checkOutDatestr)
        {
            var hotelRooms = await _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .Include(x => x.RoomOrderDetails)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .ToListAsync();

            foreach (var hotelRoom in hotelRooms)
            {
                hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDateStr, checkOutDatestr);
            }

            return hotelRooms;
        }

        public async Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate)
        {
            var hotelRoom = await _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .Include(x => x.RoomOrderDetails)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
            hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDate, checkOutDate);
            return hotelRoom;
        }

        private bool isRoomBooked(HotelRoomDTO hotelRoom, DateTime? checkInDate, DateTime? checkOutDate)
        {
            if (checkInDate == null || checkOutDate == null)
            {
                return false;
            }

            return hotelRoom.RoomOrderDetails.Any(x => x.IsPaymentSuccessful &&
                        //check if checkin date that user wants does not fall in between any dates for room that is booked
                        ((checkInDate < x.CheckOutDate && checkInDate.Value.Date >= x.CheckInDate)
                        //check if checkout date that user wants does not fall in between any dates for room that is booked
                        || (checkOutDate.Value.Date > x.CheckInDate.Date && checkInDate.Value.Date <= x.CheckInDate.Date))
                    );
        }
    }
}
متد isRoomBooked، یک محاسبه‌ی سمت سرور محسوب نمی‌شود؛ چون با استفاده از Include‌های نوشته شده، اطلاعات کامل اتاق و وابستگی‌های آن (سرهای دیگر رابطه‌ی تشکیل شده) را داریم و این محاسبات سبب رفت و برگشتی به سمت سرور نمی‌شوند.

اکنون که خاصیت IsBooked مقدار دهی شده‌است، در دو قسمت از آن استفاده خواهیم کرد:
الف) در کامپوننت نمایش لیست اتاق‌ها
@if (room.IsBooked)
{
    <button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
    <a href="@($"hotel-room-details/{room.Id}")" class="btn btn-success btn-block">Book</a>
}
ب) در کامپوننت نمایش جزئیات یک اتاق
@if (HotelBooking.OrderDetails.HotelRoomDTO.IsBooked)
{
    <button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
    <button type="submit" class="btn btn-success form-control">Checkout Now</button>
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-30.zip
مطالب
بررسی تغییرات Blazor 8x - قسمت یازدهم - قالب جدید پیاده سازی اعتبارسنجی و احراز هویت - بخش اول
قالب‌های پیش‌فرض Blazor 8x، به همراه قسمت بازنویسی شده‌ی ASP.NET Core Identity برای Blazor هم هستند که در این قسمت به بررسی نحوه‌ی عملکرد آن‌ها می‌پردازیم.


معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8 به همراه قسمت Identity

در قسمت دوم این سری، با قالب‌های جدید شروع پروژه‌های Blazor 8x آشنا شدیم و هدف ما در آنجا بیشتر بررسی حالت‌های مختلف رندر Blazor در دات نت 8 بود. تمام این قالب‌ها به همراه یک سوئیچ دیگر هم به نام auth-- هستند که توسط آن و با مقدار دهی Individual که به معنای Individual accounts است، می‌توان کدهای پیش‌فرض و ابتدایی Identity UI جدید را نیز به قالب در حال ایجاد، به صورت خودکار اضافه کرد؛ یعنی به صورت زیر:

اجرای قسمت‌های تعاملی برنامه بر روی سرور؛ به همراه کدهای Identity:
dotnet new blazor --interactivity Server --auth Individual

اجرای قسمت‌های تعاملی برنامه در مرورگر، توسط فناوری وب‌اسمبلی؛ به همراه کدهای Identity:
dotnet new blazor --interactivity WebAssembly --auth Individual

برای اجرای قسمت‌های تعاملی برنامه، ابتدا حالت Server فعالسازی می‌شود تا فایل‌های WebAssembly دریافت شوند، سپس فقط از WebAssembly استفاده می‌کند؛ به همراه کدهای Identity:
dotnet new blazor --interactivity Auto --auth Individual

فقط از حالت SSR یا همان static server rendering استفاده می‌شود (این نوع برنامه‌ها تعاملی نیستند)؛ به همراه کدهای Identity:
dotnet new blazor --interactivity None --auth Individual

 و یا توسط پرچم all-interactive--، که interactive render mode را در ریشه‌ی برنامه قرار می‌دهد؛ به همراه کدهای Identity:
 dotnet new blazor --all-interactive --auth Individual

یک نکته: بانک اطلاعاتی پیش‌فرض مورد استفاده‌ی در این نوع پروژه‌ها، SQLite است. برای تغییر آن می‌توانید از پرچم use-local-db-- هم استفاده کنید تا از LocalDB بجای SQLite استفاده کند.


Identity UI جدید Blazor در دات نت 8، یک بازنویسی کامل است


در نگارش‌های قبلی Blazor هم امکان افزودن Identity UI، به پروژه‌های Blazor وجود داشت (اطلاعات بیشتر)، اما ... آن پیاده سازی، کاملا مبتنی بر Razor pages بود. یعنی کاربر، برای کار با آن مجبور بود از فضای برای مثال Blazor Server خارج شده و وارد فضای جدید ASP.NET Core شود تا بتواند، Identity UI نوشته شده‌ی با صفحات cshtml. را اجرا کند و به اجزای آن دسترسی پیدا کند (یعنی عملا آن قسمت UI اعتبارسنجی، Blazor ای نبود) و پس از اینکار، مجددا به سمت برنامه‌ی Blazor هدایت شود؛ اما ... این مشکل در دات نت 8 با ارائه‌ی صفحات SSR برطرف شده‌است.
همانطور که در قسمت قبل نیز بررسی کردیم، صفحات SSR، طول عمر کوتاهی دارند و هدف آن‌ها تنها ارسال یک HTML استاتیک به مرورگر کاربر است؛ اما ... دسترسی کاملی را به HttpContext برنامه‌ی سمت سرور دارند و این دقیقا چیزی است که زیر ساخت Identity، برای کار و تامین کوکی‌های مورد نیاز خودش، احتیاج دارد. صفحات Identity UI از یک طرف از HttpContext برای نوشتن کوکی لاگین موفقیت آمیز در مرورگر کاربر استفاده می‌کنند (در این سیستم، هرجائی متدهای XyzSignInAsync وجود دارد، در پشت صحنه و در پایان کار، سبب درج یک کوکی اعتبارسنجی و احراز هویت در مرورگر کاربر نیز خواهد شد) و از طرف دیگر با استفاده از میان‌افزارهای اعتبارسنجی و احراز هویت ASP.NET Core، این کوکی‌ها را به صورت خودکار پردازش کرده و از آن‌ها جهت ساخت خودکار شیء User قابل دسترسی در این صفحات (شیء context.User.Identity@)، استفاده می‌کنند.
به همین جهت تمام صفحات Identity UI ارائه شده در Blazor 8x، از نوع SSR هستند و اگر در آن‌ها از فرمی استفاده شده، دقیقا همان فرم‌های تعاملی است که در قسمت چهارم این سری بررسی کردیم. یعنی صرفا بخاطر داشتن یک فرم، ضرورتی به استفاده‌ی از جزایر تعاملی Blazor Server و یا Blazor WASM را ندیده‌اند و اینگونه فرم‌ها را بر اساس مدل جدید فرم‌های تعاملی Blazor 8x پیاده سازی کرده‌اند. بنابراین این صفحات کاملا یکدست هستند و از ابتدا تا انتها، به صورت یکپارچه بر اساس مدل SSR کار می‌کنند (که در اصل خیلی شبیه به Razor pages یا Viewهای MVC هستند) و جزیره، جزیره‌ای، طراحی نشده‌اند.

 
روش دسترسی به HttpContext در صفحات SSR

اگر به کدهای Identity UI قالب آغازین یک پروژه که در ابتدای بحث روش تولید آن‌ها بیان شد، مراجعه کنید، استفاده از یک چنین «پارامترهای آبشاری» را می‌توان مشاهده کرد:

@code {

    [CascadingParameter]
    public HttpContext HttpContext { get; set; } = default!;
متد ()AddRazorComponents ای که در فایل Program.cs اضافه می‌شود، کار ثبت CascadingParameter ویژه‌ی فوق را به صورت زیر انجام می‌دهد که در Blazor 8x به آن یک Root-level cascading value می‌گویند:
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
مقادیر آبشاری برای ارسال اطلاعاتی بین درختی از کامپوننت‌ها، از یک والد به چندین فرزند که چندین لایه ذیل آن واقع شده‌‌اند، مفید است. برای مثال فرض کنید می‌خواهید اطلاعات عمومی تنظیمات یک کاربر را به چندین زیر کامپوننت، ارسال کنید. یک روش آن، ارسال شیء آن به صورت پارامتر، به تک تک آن‌ها است و راه دیگر، تعریف یک CascadingParameter است؛ شبیه به کاری که در اینجا انجام شده‌است تا دیگر نیازی نباشد تا تک تک زیر کامپوننت‌ها این شیء را به یک لایه زیریرین خود، یکی یکی منتقل کنند.

در کدهای Identity UI ارائه شده، از این CascadingParameter برای مثال جهت خروج از برنامه (HttpContext.SignOutAsync) و یا دسترسی به اطلاعات هدرهای رسید (if (HttpMethods.IsGet(HttpContext.Request.Method)))، دسترسی به سرویس‌ها (()<HttpContext.Features.Get<ITrackingConsentFeature)، تامین مقدار Cancellation Token به کمک HttpContext.RequestAborted و یا حتی در صفحه‌ی جدید Error.razor که آن نیز یک صفحه‌ی SSR است، برای دریافت HttpContext?.TraceIdentifier استفاده‌ی مستقیم شده‌است.

نکته‌ی مهم: باید به‌خاطر داشت که فقط و فقط در صفحات SSR است که می‌توان به HttpContext به نحو آبشاری فوق دسترسی یافت و همانطور که در قسمت قبل نیز بررسی شد، سایر حالات رندر Blazor از دسترسی به آن، پشتیبانی نمی‌کنند و اگر چنین پارامتری را تنظیم کردید، نال دریافت می‌کنید.


بررسی تفاوت‌های تنظیمات ابتدایی قالب جدید Identity UI در Blazor 8x با نگارش‌های قبلی آن

مطالب و نکات مرتبط با قالب قبلی را در مطلب «Blazor 5x - قسمت 22 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 2 - ورود به سیستم و خروج از آن» می‌توانید مشاهده کنید.

در قسمت سوم این سری، روش ارتقاء یک برنامه‌ی قدیمی Blazor Server را به نگارش 8x آن بررسی کردیم و یکی از تغییرات مهم آن، حذف فایل‌های cshtml. ای آغاز برنامه و انتقال وظایف آن، به فایل جدید App.razor و انتقال مسیریاب Blazor به فایل جدید Routes.razor است که پیشتر در فایل App.razor نگارش‌های قبلی Blazor وجود داشت.
در این نگارش جدید، محتوای فایل Routes.razor به همراه قالب Identity UI به صورت زیر است:
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
در نگارش‌های قبلی، این مسیریاب داخل کامپوننت CascadingAuthenticationState محصور می‌شد تا توسط آن بتوان AuthenticationState را به تمام کامپوننت‌های برنامه ارسال کرد. به این ترتیب برای مثال کامپوننت AuthorizeView، شروع به کار می‌کند و یا توسط شیء context.User می‌توان به User claims دسترسی یافت و یا به کمک ویژگی [Authorize]، دسترسی به صفحه‌ای را محدود به کاربران اعتبارسنجی شده کرد.
اما ... در اینجا با یک نگارش ساده شده سروکار داریم؛ از این جهت که برنامه‌های جدید، به همراه جزایر تعاملی هم می‌توانند باشند و باید بتوان این AuthenticationState را به آن‌ها هم ارسال کرد. به همین جهت مفهوم جدید مقادیر آبشاری سطح ریشه (Root-level cascading values) که پیشتر در این بحث معرفی شد، در اینجا برای معرفی AuthenticationState استفاده شده‌است.

در اینجا کامپوننت جدید FocusOnNavigate را هم مشاهده می‌کنید. با استفاده از این کامپوننت و براساس CSS Selector معرفی شده، پس از هدایت به یک صفحه‌ی جدید، این المان مشخص شده دارای focus خواهد شد. هدف از آن، اطلاع رسانی به screen readerها در مورد هدایت به صفحه‌ای دیگر است (برای مثال، کمک به نابیناها برای درک بهتر وضعیت جاری).

همچنین در اینجا المان NotFound را هم مشاهده نمی‌کنید. این المان فقط در برنامه‌های WASM جهت سازگاری با نگارش‌های قبلی، هنوز هم قابل استفاده‌است. جایگزین آن‌را در قسمت سوم این سری، برای برنامه‌های Blazor server در بخش «ایجاد فایل جدید Routes.razor و مدیریت سراسری خطاها و صفحات یافت نشده» آن بررسی کردیم.


روش انتقال اطلاعات سراسری اعتبارسنجی یک کاربر به کامپوننت‌ها و جزایر تعاملی واقع در صفحات SSR

مشکل! زمانیکه از ترکیب صفحات SSR و جزایر تعاملی واقع در آن استفاده می‌کنیم ... جزایر واقع در آن‌ها دیگر این مقادیر آبشاری را دریافت نمی‌کنند و این مقادیر در آن‌ها نال است. برای حل این مشکل در Blazor 8x، باید مقادیر آبشاری سطح ریشه را (Root-level cascading values) به صورت زیر در فایل Program.cs برنامه ثبت کرد:
builder.Services.AddCascadingValue(sp =>new Preferences { Theme ="Dark" });
پس از این تغییر، اکنون نه فقط صفحات SSR، بلکه جزایر واقع در آن‌ها نیز توسط ویژگی [CascadingParameter] می‌توانند به این مقدار آبشاری، دسترسی داشته باشند. به این ترتیب است که در برنامه‌های Blazor، کامپوننت‌های تعاملی هم دسترسی به اطلاعات شخص لاگین شده‌ی توسط صفحات SSR را دارند. برای مثال اگر به فایل Program.cs قالب جدید Identity UI عنوان شده مراجعه کنید، چنین سطوری در آن قابل مشاهده هستند
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
متد جدید AddCascadingAuthenticationState، فقط کار ثبت AuthenticationStateProvider برنامه را به صورت آبشاری انجام می‌دهد.
این AuthenticationStateProvider سفارشی جدید هم در فایل اختصاصی IdentityRevalidatingAuthenticationStateProvider.cs پوشه‌ی Identity قالب پروژه‌های جدید Blazor 8x که به همراه اعتبارسنجی هستند، قابل مشاهده‌است.

یا اگر به قالب‌های پروژه‌های WASM دار مراجعه کنید، تعریف به این صورت تغییر کرده‌است؛ اما مفهوم آن یکی است:
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, PersistingServerAuthenticationStateProvider>();
در این پروژه‌ها، یک AuthenticationStateProvider سفارشی دیگری در فایل PersistingServerAuthenticationStateProvider.cs ارائه شده‌است (این فایل‌ها، جزو استاندارد قالب‌های جدید Identity UI پروژه‌های Blazor 8x هستند؛ فقط کافی است، یک نمونه پروژه‌ی آزمایشی را با سوئیچ جدید auth Individual-- ایجاد کنید، تا بتوانید این فایل‌های یاد شده را مشاهده نمائید).

AuthenticationStateProviderهای سفارشی مانند کلاس‌های IdentityRevalidatingAuthenticationStateProvider و PersistingServerAuthenticationStateProvider که در این قالب‌ها موجود هستند، چون به صورت آبشاری کار تامین AuthenticationState را انجام می‌دهند، به این ترتیب می‌توان به شیء context.User.Identity@ در جزایر تعاملی نیز به سادگی دسترسی داشت.

یعنی به صورت خلاصه، یکبار قرارداد AuthenticationStateProvider عمومی (بدون هیچ نوع پیاده سازی) به صورت یک Root-level cascading value ثبت می‌شود تا در کل برنامه قابل دسترسی شود. سپس یک پیاده سازی اختصاصی از آن توسط ()<builder.Services.AddScoped<AuthenticationStateProvider, XyzProvider به برنامه معرفی می‌شود تا آن‌را تامین کند. به این ترتیب زیر ساخت امن سازی صفحات با استفاده از ویژگی attribute [Authorize]@ و یا دسترسی به اطلاعات کاربر جاری با استفاده از شیء context.User@ و یا امکان استفاده از کامپوننت AuthorizeView برای نمایش اطلاعاتی ویژه به کاربران اعتبارسنجی شده مانند صفحه‌ی Auth.razor زیر، مهیا می‌شود:
@page "/auth"

@using Microsoft.AspNetCore.Authorization

@attribute [Authorize]

<PageTitle>Auth</PageTitle>

<h1>You are authenticated</h1>

<AuthorizeView>
    Hello @context.User.Identity?.Name!
</AuthorizeView>

سؤال: چگونه یک پروژه‌ی سمت سرور، اطلاعات اعتبارسنجی خودش را با یک پروژه‌ی WASM سمت کلاینت به اشتراک می‌گذارد (برای مثال در حالت رندر Auto)؟ این انتقال اطلاعات باتوجه به یکسان نبودن محل اجرای این دو پروژه (یکی بر روی کامپیوتر سرور و دیگری بر روی مرورگر کلاینت، در کامپیوتری دیگر) چگونه انجام می‌شود؟ این مورد را در قسمت بعد، با معرفی PersistentComponentState و «فیلدهای مخفی» مرتبط با آن، بررسی می‌کنیم.
مطالب
React component lifecycle
فرض کنید می‌خواهیم داده‌هایی را از دیتابیس بخوانیم و در نهایت درون UI نمایش دهیم. شاید فکر کنید که متد render مکان مناسبی برای اینکار باشد:
render() {
    fetch('https://jsonplaceholder.typicode.com/users')
        .then(response => response.json())
        .then(data => this.setState({ users: data }))
        
    return (
        <ul>
            {this.state.users.map(user => <li key={user.id}>{user.name}</li>)}
        </ul>
    );
}

اما قرار دادن فراخوانی AJAX درون متد render ایده‌ی خوبی نیست؛ زیرا متد render نباید side effectی ایجاد کند. هدف متد render همانطور از نامش پیداست، رندر کردن یک کامپوننت است. در نتیجه اینچنین فراخوانی‌هایی باید درون lifecycle events تعریف شوند که در واقع یکسری رخداد هستند و این امکان را در اختیارمان قرار میدهند که بتوانیم کارهایی از این قبیل را در طول چرخه‌ی حیات یک کامپوننت انجام دهیم. در React به فرآیند ایجاد یک کامپوننت و در نهایت render شدن محتوای آن برای اولین بار، mounting گفته میشود. در این فرآیند سه متد نقش دارند:
  • constructor
  • render
  • componentDidMount
سازنده یک کامپوننت زمانی فراخوانی میشود که React نیاز به ایجاد یک وهله از کامپوننت داشته باشد. در اینجا کامپوننت فرصت این را خواهد داشت تا props مورد نیازش را از والدینش دریافت کند، state dataیش را تعریف کند و یا یکسری کارهای آماده‌سازی را انجام دهد. در مرحله‌ی بعدی، متد render فراخوانی خواهد شد. در اینجا کامپوننت، محتوایی را که قرار است به DOM اضافه شود، در اختیار React قرار میدهد. در نهایت متد componentDidMount فراخوانی خواهد شد و به این معنا است که محتوا، به DOM اضافه شده‌است. در نتیجه متد componentDidMount محل مناسبی برای قرار دادن مثال ابتدای مطلب میباشد. به عنوان مثال در کد زیر هر بار که checkbox را فعال میکنیم، یک وهله از کامپوننت ایجاد خواهد شد. یعنی در واقع هر بار از فاز mounting عبور خواهد کرد.

 

//App.js
import React, { Component } from 'react';
import { Users } from "./Users";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      toggle: false
    };
  }

  handleChange = () => {
    this.setState({ toggle: !this.state.toggle });
  }

  render() {
    return <div className="container text-center">
      <div className="row p-2">
        <div className="form-check">
          <input type="checkbox" className="form-check-input"
            checked={this.state.toggle}
            onChange={this.handleChange} />
          <label className="form-check-label">Toggle</label>
        </div>
        <div className="row p-2">
          {this.state.toggle && <Users />}
        </div>
      </div>
    </div>
  }
}

//Users
import React, { Component } from 'react';

export class Users extends Component {
    constructor(props) {
        super(props);
        this.state = {
            users: []
        }
    }

    componentDidMount() {
        fetch('https://jsonplaceholder.typicode.com/users')
            .then(response => response.json())
            .then(data => {
                this.setState({ users: data });
                console.log(data)
            })
    }


    render() {
        return (
            <div className="bg-info text-white p-2">
                {this.state.users &&
                    <p>{JSON.stringify(this.state.users, null, 2)}</p>
                }
            </div>
        );
    }
}

Update phase
فرآیند پاسخگویی به تغییرات و طی شدن reconciliation،  به عنوان update phase شناخته میشود. در این فاز متد render جهت دریافت محتوا از کامپوننت، فراخوانی خواهد شد و سپس متد componentDidUpdate بعد از آن فراخوانی میشود:
import React, { Component } from 'react';

export class Users extends Component {
    constructor(props) {
        // as before
    }

    componentDidMount() {
        // as before
    }

    componentDidUpdate() {
        console.log("componentDidUpdate Users Component");
    }

    render() {
        // as before
    }
}
بعد از render اولیه کامپوننت، هر درخواست فراخوانی متد render بعد از آن، منجر به فراخوانی متد componentDidUpdate خواهد شد. 

Unmounting phase
در این فاز متد componentWillUnmount زمانی فراخوانی خواهد شد که کامپوننت در حال حذف از DOM است. طبیعتاً این متد میتواند محل خوبی برای آزاد کردن منابع، بستن اتصالات شبکه و یا متوقف کردن درخواست‌های asynchronous باشد؛ برای نمونه در مثال قبل در حین غیرفعال کردن checkbox فراخوانی خواهد شد:
import React, { Component } from 'react';

export class Users extends Component {
    constructor(props) {
        // as before
    }

    componentDidMount() {
        // as before
    }

    componentDidUpdate() {
        console.log("componentDidUpdate Users Component");
    }

    componentWillUnmount() {
        console.log("componentWillUnmount Users Component");
    }

    render() {
        // as before
    }
}


Effect Hook
چرخه‌ی فوق فقط مختص به کامپوننت‌هایی است که به صورت کلاس تعریف شده‌اند. کامپوننت‌هایی که به صورت تابع هستند، نمیتوانند به روش فوق باشند. با استفاده از effect hook میتوانیم مشابه متدهای componentDidMount, componentDidUpdate, componentWillUnmount را پیاده‌سازی کنیم:
import React, { useState, useEffect } from 'react';

export const Users = () => {
    const [users, setUsers] = useState([]);

    useEffect(() => {
        console.log("Same as componentDidMount")
        fetch('https://jsonplaceholder.typicode.com/users')
            .then(response => response.json())
            .then(data => {
                setUsers({ users: data });
            })
    }, []);

    useEffect(() => {
        console.log("Same as componentDidUpdate")
    });

    useEffect(() => {
        return () => {
            console.log("Same as componentWillUnmount")
        }
    }, []);

    return (
        <div className="bg-info text-white p-2">
            {users &&
                <p>{JSON.stringify(users, null, 2)}</p>
            }
        </div>
    );
}

مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت نهم - مثالی از کتابخانه‌ی mobx-react
در ادامه‌ی سری کار با MobX، می‌خواهیم نکاتی را که در سه قسمت قبل مرور کردیم، در قالب یک برنامه پیاده سازی کنیم:


این برنامه از چهار کامپوننت تشکیل شده‌است:
- کامپوننت App که در برگیرنده‌ی سه کامپوننت زیر است:
- کامپوننت BasketItemsCounter: جمع تعداد آیتم‌های انتخابی توسط کاربر را نمایش می‌دهد؛ به همراه دکمه‌ای برای خالی کردن لیست انتخابی.
- کامپوننت ShopItemsList: لیست محصولات موجود در فروشگاه را نمایش می‌دهد. با کلیک بر روی هر آیتم آن، آیتم انتخابی به لیست انتخاب‌های او اضافه خواهد شد.
- کامپوننت BasketItemsList: لیستی را نمایش می‌دهد که حاصل انتخاب‌های کاربر در کامپوننت ShopItemsList است (یا همان سبد خرید). در ذیل این لیست، جمع نهایی قیمت قابل پرداخت نیز درج می‌شود. همچنین اگر کاربر بر روی دکمه‌ی remove هر ردیف کلیک کند، یک واحد از چند واحد انتخابی، حذف خواهد شد.

بنابراین در اینجا سه کامپوننت مجزا را داریم که با هم تبادل اطلاعات می‌کنند. یکی جمع تعداد محصولات خریداری شده را، دیگری لیست محصولات موجود را و آخری لیست خرید نهایی را نمایش می‌دهد. همچنین این سه کامپوننت، فرزند یک دیگر هم محسوب نمی‌شوند و انتقال اطلاعات بین این‌ها نیاز به بالا بردن state هر کدام و قرار دادن آن‌ها در کامپوننت App را دارد تا بتوان پس از آن از طریق props آن‌ها را بین سه کامپوننت فوق که اکنون فرزند کامپوننت App محسوب می‌شوند، به اشتراک گذاشت. روش بهتر اینکار، استفاده از یک مخزن حالت سراسری است تا حالت‌های این کامپوننت‌ها را نگهداری کرده و داده‌‌ها را بین آن‌ها به اشتراک بگذارد که در اینجا برای حل این مساله از کتابخانه‌های mobx و mobx-react استفاده خواهیم کرد.


برپایی پیش‌نیازها

برای پیاده سازی برنامه‌ی فوق، یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> create-react-app state-management-with-mobx-part4
> cd state-management-with-mobx-part4
در ادامه کتابخانه‌ها‌ی زیر را نیز در آن نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save bootstrap mobx mobx-react mobx-react-devtools mobx-state-tree
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود.
- اصل کار برنامه توسط دو کتابخانه‌ی mobx و کتابخانه‌ی متصل کننده‌ی آن به برنامه‌های react که mobx-react نام دارد، انجام خواهد شد.
- چون می‌خواهیم از افزونه‌ی  mobx-devtools نیز استفاده کنیم، نیاز است دو بسته‌ی mobx-react-devtools و همچنین mobx-state-tree را که جزو وابستگی‌های آن است، نصب کنیم.

سپس بسته‌های زیر را که در قسمت devDependencies فایل package.json درج خواهند شد، باید نصب شوند:
> npm install --save-dev babel-eslint customize-cra eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort react-app-rewired typescript
علت آن‌را در قسمت قبل بررسی کردیم. این وابستگی‌ها برای فعالسازی react-app-rewired و همچنین eslint غنی سازی شده‌ی آن مورد استفاده قرار می‌گیرند. به علاوه سه قسمت زیر را نیز از قسمت قبل، به پروژه اضافه می‌کنیم:
- افزودن فایل جدید config-overrides.js به ریشه‌ی پروژه، تا پشتیبانی ازlegacy" decorators spec" فعال شود.
- اصلاح فایل package.json و ویرایش قسمت scripts آن برای استفاده‌ی از react-app-rewired، تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود.
- همچنین فایل غنی شده‌ی eslintrc.json. را نیز به ریشه‌ی پروژه اضافه می‌کنیم.


تهیه سرویس لیست محصولات موجود در فروشگاه

این برنامه از یک لیست درون حافظه‌ای، برای تهیه‌ی لیست محصولات موجود در فروشگاه استفاده می‌کند. به همین جهت پوشه‌ی service را افزوده و سپس فایل جدید src\services\productsService.js را با محتوای زیر، ایجاد می‌کنیم:
const products = [
  {
    id: 1,
    name: "Item 1",
    price: 850
  },
  {
    id: 2,
    name: "Item 2",
    price: 900
  },
  {
    id: 3,
    name: "Item 3",
    price: 1500
  },
  {
    id: 4,
    name: "Item 4",
    price: 1000
  }
];

export default products;


ایجاد کامپوننت نمایش لیست محصولات


پس از مشخص شدن لیست محصولات قابل فروش، کامپوننت جدید src\components\ShopItemsList.jsx را به صورت زیر ایجاد می‌کنیم:
import React from "react";

import products from "../services/productsService";

const ShopItemsList = ({ onAdd }) => {
  return (
    <table className="table table-hover">
      <thead className="thead-light">
        <tr>
          <th>Name</th>
          <th>Price</th>
          <th>Action</th>
        </tr>
      </thead>
      <tbody>
        {products.map(product => (
          <tr key={product.id}>
            <td>{product.name}</td>
            <td>{product.price}</td>
            <td>
              <button
                className="btn btn-sm btn-info"
                onClick={() => onAdd(product)}
              >
                Add
              </button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default ShopItemsList;
- این کامپوننت آرایه‌ی products را از طریق سرویس services/productsService دریافت کرده و سپس با استفاده از متد Array.map، حلقه‌ای را بر روی عناصر آن تشکیل داده که در نتیجه، سبب درج trهای متناظر با آن می‌شود؛ تا هر ردیف این جدول، یک آیتم از محصولات موجود را نیز نمایش دهد.
- در اینجا همچنین هر ردیف، به همراه یک دکمه‌ی Add نیز هست که قرار است با کلیک بر روی آن، متد رویدادگردان onAdd فراخوانی شود. این متد نیز از طریق props این کامپوننت دریافت می‌شود. کتابخانه‌های مدیریت حالت، تمام خواص و رویدادگردان‌های مورد نیاز یک کامپوننت را از طریق props، تامین می‌کنند.
- فعلا این کامپوننت به هیچ مخزن داده‌ای متصل نیست و فقط طراحی ابتدایی آن آماده شده‌است.


ایجاد کامپوننت نمایش لیست خرید کاربر (سبد خرید)


اکنون که می‌توان توسط کامپوننت لیست محصولات، تعدادی از آن‌ها را خریداری کرد، کامپوننت جدید src\components\BasketItemsList.jsx را برای نمایش لیست نهایی خرید کاربر، به صورت زیر پیاده سازی می‌کنیم:
import React from "react";

const BasketItemsList = ({ items, totalPrice, onRemove }) => {
  return (
    <>
      <table className="table table-hover">
        <thead className="thead-light">
          <tr>
            <th>Name</th>
            <th>Price</th>
            <th>Count</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          {items.map(item => (
            <tr key={item.id}>
              <td>{item.name}</td>
              <td>{item.price}</td>
              <td>{item.count}</td>
              <td>
                <button
                  className="btn btn-sm btn-danger"
                  onClick={() => onRemove(item.id)}
                >
                  Remove
                </button>
              </td>
            </tr>
          ))}

          <tr>
            <td align="right">
              <strong>Total: </strong>
            </td>
            <td>
              <strong>{totalPrice}</strong>
            </td>
            <td></td>
            <td></td>
          </tr>
        </tbody>
      </table>
    </>
  );
};

export default BasketItemsList;
- عملکرد این کامپوننت نیز شبیه به کامپوننت نمایش لیست محصولات است؛ با این تفاوت که لیستی که به آن از طریق props ارسال می‌شود:
const BasketItemsList = ({ items, totalPrice, onRemove }) => {
لیست محصولات انتخابی کاربر است.
- همچنین هر ردیف نمایش داده شده، به همراه یک دکمه‌ی Remove آیتم انتخابی نیز هست که به متد رویدادگردان onRemove متصل شده‌است.
- در ردیف انتهایی این لیست، مقدار totalPrice که یک خاصیت محاسباتی است، درج می‌شود.
- فعلا این کامپوننت نیز به هیچ مخزن داده‌ای متصل نیست و فقط طراحی ابتدایی آن آماده شده‌است.


ایجاد کامپوننت نمایش تعداد آیتم‌های خریداری شده


کاربر اگر آیتمی را از لیست محصولات انتخاب کند و یا محصول انتخاب شده را از لیست خرید حذف کند، تعداد نهایی باقی مانده را می‌توان در کامپوننت src\components\BasketItemsCounter.jsx مشاهده کرد:
import React, { Component } from "react";

class BasketItemsCounter extends Component {
  render() {
    const { count, onRemoveAll } = this.props;
    return (
      <div>
        <h1>Total items: {count}</h1>
        <button
          type="button"
          className="btn btn-sm btn-danger"
          onClick={() => onRemoveAll()}
        >
          Empty Basket
        </button>
      </div>
    );
  }
}

export default BasketItemsCounter;
- این کامپوننت یک خاصیت و یک رویدادگردان را از طریق props خود دریافت می‌کند. خاصیت count، جمع نهایی موجود در سبد خرید را نمایش می‌دهد و فراخوانی onRemoveAll، سبب پاک شدن تمام آیتم‌های موجود در سبد خرید خواهد شد.
- فعلا این کامپوننت نیز به هیچ مخزن داده‌ای متصل نیست و فقط طراحی ابتدایی آن آماده شده‌است.


نمایش ابتدایی سه کامپوننت توسط کامپوننت App

اکنون که این سه کامپوننت تکمیل شده‌اند، می‌توان المان‌های آن‌ها را در فایل src\App.js درج کرد تا در صفحه نمایش داده شوند:
import React, { Component } from "react";

import BasketItemsCounter from "./components/BasketItemsCounter";
import BasketItemsList from "./components/BasketItemsList";
import ShopItemsList from "./components/ShopItemsList";

class App extends Component {
  render() {
    return (
      <main className="container">
        <div className="row">
          <BasketItemsCounter />
        </div>

        <hr />

        <div className="row">
          <h2>Products</h2>
          <ShopItemsList />
        </div>

        <div className="row">
          <h2>Basket</h2>
          <BasketItemsList />
        </div>
      </main>
    );
  }
}

export default App;


طراحی مخزن‌های حالت MobX مخصوص برنامه


می‌توان همانند Redux کل state برنامه را داخل یک شیء store ذخیره کرد و یا چون در اینجا می‌توان طراحی مخزن حالت MobX را به دلخواه انجام داد، می‌توان چندین مخزن حالت را تهیه و به هم متصل کرد؛ مانند تصویری که مشاهده می‌کنید. در اینجا:
- src\stores\counter.js: مخزن داده‌ی حالت کامپوننت شمارشگر است.
- src\stores\market.js: مخزن داده‌ی کامپوننت‌های لیست محصولات و سبد خرید است.
- src\stores\index.js: کار ترکیب دو مخزن قبل را انجام می‌دهد.

در ادامه کدهای کامل این مخازن را مشاهده می‌کنید:

مخزن حالت src\stores\counter.js
import { action, observable } from "mobx";

export default class CounterStore {
  @observable totalNumbersInBasket = 0;

  constructor(rootStore) {
    this.rootStore = rootStore;
  }

  @action
  increase = () => {
    this.totalNumbersInBasket++;
  };

  @action
  decrease = () => {
    this.totalNumbersInBasket--;
  };
}
- کار این مخزن، تامین عدد جمع آیتم‌های انتخابی توسط کاربر است که در کامپوننت شمارشگر نمایش داده می‌شود.
- در اینجا خاصیت totalNumbersInBasket به صورت observable تعریف شده‌است و با تغییر آن چه به صورت مستقیم، با مقدار دهی آن و یا توسط دو action تعریف شده، سبب به روز رسانی UI خواهد شد.
- می‌شد این مخزن را با مخزن src\stores\market.js یکی کرد؛ اما جهت ارائه‌ی مثالی در مورد نحوه‌ی تعریف چند مخزن و روش برقراری ارتباط بین آن‌ها، به صورت مجزایی تعریف شد.

مخزن حالت src\stores\market.js
import { action, computed, observable } from "mobx";

export default class MarketStore {
  @observable basketItems = [];

  constructor(rootStore) {
    this.rootStore = rootStore;
  }

  @action
  add = product => {
    const selectedItem = this.basketItems.find(item => item.id === product.id);
    if (selectedItem) {
      selectedItem.count++;
    } else {
      this.basketItems.push({
        ...product,
        count: 1
      });
    }

    this.rootStore.counterStore.increase();
  };

  @action
  remove = id => {
    const selectedItem = this.basketItems.find(item => item.id === id);
    selectedItem.count--;

    if (selectedItem.count === 0) {
      this.basketItems.remove(selectedItem);
    }

    this.rootStore.counterStore.decrease();
  };

  @action
  removeAll = () => {
    this.basketItems = [];
    this.rootStore.counterStore.totalNumbersInBasket = 0;
  };

  @computed
  get totalPrice() {
    return this.basketItems.reduce((previous, current) => {
      return previous + current.price * current.count;
    }, 0);
  }
}
- کار این مخزن تامین مدیریت آرایه‌ی basketItems است که بیانگر اشیاء انتخابی توسط کاربر می‌باشد.
- توسط متد add آن در کامپوننت نمایش لیست محصولات، می‌توان آیتمی را به این آرایه اضافه کرد. در اینجا چون شیء product مورد استفاده دارای خاصیت count نیست، روش افزودن آن‌را توسط spread operator برای درج خواص شیء product اصلی و سپس تعریف آن‌را مشاهده می‌کنید. این فراخوانی، سبب افزایش یک واحد به عدد شمارشگر نیز می‌شود.
- متد remove آن در کامپوننت سبد خرید، مورد استفاده قرار می‌گیرد تا کاربر بتواند اطلاعاتی را از این لیست حذف کند. این فراخوانی، سبب کاهش یک واحد از عدد شمارشگر نیز می‌شود.
- متد removeAll آن در کامپوننت شمارشگر بالای صفحه استفاده می‌شود تا سبب خالی شدن آرایه‌ی آیتم‌های انتخابی گردد و همچنین عدد آن‌را نیز صفر کند.
- خاصیت محاسباتی totalPrice آن در پایین جدول سبد خرید، جمع کل هزینه‌ی قابل پرداخت را مشخص می‌کند.

مخزن حالت src\stores\index.js

در اینجا روش یکی کردن دو مخزن حالت یاد شده را به صورت خاصیت‌های عمومی یک مخزن کد ریشه، مشاهده می‌کنید:
import CounterStore from "./counter";
import MarketStore from "./market";

class RootStore {
  counterStore = new CounterStore(this);
  marketStore = new MarketStore(this);
}

export default RootStore;
هر مخزن مجزایی که تعریف شده، دارای یک پارامتر سازنده‌است که با مقدار شیء this کلاس RootStore مقدار دهی می‌شود. با این روش می‌توان بین مخازن کد مختلف ارتباط برقرار کرد. برای نمونه درمخزن حالت MarketStore، این پارامتر سازنده، امکان دسترسی به خاصیت counterStore و سپس تمام خاصیت‌ها و متدهای عمومی آن‌را فراهم می‌کند:
export default class MarketStore {
  @observable basketItems = [];

  constructor(rootStore) {
    this.rootStore = rootStore;
  }

  @action
  removeAll = () => {
    this.basketItems = [];
    this.rootStore.counterStore.totalNumbersInBasket = 0;
  };
}


تامین مخازن حالت تمام کامپوننت‌های برنامه

پس از ایجاد مخازن حالت، اکنون نیاز است آن‌ها را در اختیار سلسه مراتب کامپوننت‌های برنامه قرار دهیم. به همین جهت به فایل src\index.js مراجعه کرده و آن‌را به صورت زیر تغییر می‌دهیم:
import "./index.css";
import "bootstrap/dist/css/bootstrap.css";

import makeInspectable from "mobx-devtools-mst";
import { Provider } from "mobx-react";
import React from "react";
import ReactDOM from "react-dom";

import App from "./App";
import * as serviceWorker from "./serviceWorker";
import RootStore from "./stores";

const rootStore = new RootStore();

if (process.env.NODE_ENV === "development") {
  makeInspectable(rootStore); // https://github.com/mobxjs/mobx-devtools
}

ReactDOM.render(
  <Provider {...rootStore}>
    <App />
  </Provider>,
  document.getElementById("root")
);

serviceWorker.unregister();
- در اینجا ابتدا import فایل css بوت استرپ را مشاهده می‌کنید که در برنامه استفاده شده‌است.
- سپس یک وهله‌ی جدید از RootStore را که حاوی خاصیت‌های عمومی counterStore و marketStore است، ایجاد می‌کنیم.
- اگر علاقمند باشید تا حین کار با MobX، جزئیات پشت صحنه‌ی آن‌را توسط افزونه‌ی mobx-devtools ردیابی کنید، روش آن‌را در اینجا با فراخوانی متد makeInspectable مشاهده می‌کنید. مقدار process.env.NODE_ENV نیز بر اساس پروسه‌ی جاری node.js اجرا کننده‌ی برنامه‌ی React تامین می‌شود. اطلاعات بیشتر
- قسمت آخر این تنظیمات، محصور کردن کامپوننت App که بالاترین کامپوننت در سلسله مراتب کامپوننت‌های برنامه است، با شیء Provider می‌باشد. در این شیء توسط spread operator، سبب درج خواص عمومی rootStore، به عنوان مخازن قابل استفاده شده‌ایم. تنظیم {rootStore...} معادل عبارت زیر است:
<Provider counterStore={rootStore.counterStore} marketStore={rootStore.marketStore}>
به این ترتیب تمام کامپوننت‌های برنامه می‌توانند با دو مخزن کد ارسالی به آن‌ها کار کنند. در ادامه مشاهده می‌کنیم که چگونه این ویژگی‌ها، سبب تامین props کامپوننت‌ها خواهند شد.


اتصال کامپوننت ShopItemsList به مخزن حالت marketStore

پس از ایجاد rootStore و محصور کردن کامپوننت App توسط شیء Provider در فایل src\index.js، اکنون باید قسمت export default کامپوننت‌های برنامه را جهت استفاده‌ی از مخازن حالت، یکی یکی ویرایش کرد:
import { inject, observer } from "mobx-react";
import React from "react";

import products from "../services/productsService";

const ShopItemsList = ({ onAdd }) => {
  return (
  // ...
  );
};

export default inject(({ marketStore }) => ({
  onAdd: marketStore.add
}))(observer(ShopItemsList));
در اینجا فراخوانی متد inject، سبب دسترسی به ویژگی marketStore تامین شده‌ی توسط شیء Provider می‌شود. تمام ویژگی‌هایی که به شیء Provider ارائه می‌شوند، در اینجا به صورت خواصی که توسط Object Destructuring قابل استخراج هستند، قابل دسترسی می‌شوند. سپس props این کامپوننت را که متد onAdd را می‌پذیرد، از طریق marketStore.add تامین می‌کنیم. در آخر کامپوننت ShopItemsList باید به صورت یک observer بازگشت داده شود تا تغییرات store را تحت نظر قرار داده و به این صورت امکان به روز رسانی UI را پیدا کند.


اتصال کامپوننت BasketItemsList به مخزن حالت marketStore

در اینجا نیز سطر export default را جهت دریافت خاصیت marketStore، از شیء Provider تامین شده‌ی در فایل src\index.js، ویرایش می‌کنیم. به این ترتیب سه props مورد انتظار این کامپوننت، توسط خاصیت‌های basketItems (آرایه‌ی اشیاء انتخابی توسط کاربر)، totalPrice (خاصیت محاسباتی جمع کل هزینه) و  متد رویدادگردان onRemove (برای حذف یک آیتم) تامین می‌شوند. در آخر کامپوننت را به صورت observer محصور کرده و بازگشت می‌دهیم تا تغییرات در مخزن حالت آن، سبب به روز رسانی UI آن شوند:
import { inject, observer } from "mobx-react";
import React from "react";

const BasketItemsList = ({ items, totalPrice, onRemove }) => {
  return (
  // ...
  );
};

export default inject(({ marketStore }) => ({
  items: marketStore.basketItems,
  totalPrice: marketStore.totalPrice,
  onRemove: marketStore.remove
}))(observer(BasketItemsList));


اتصال کامپوننت BasketItemsCounter به دو مخزن حالت counterStore و marketStore

در اینجا روش استفاده‌ی از decorator syntax کتابخانه‌ی mobx-react را بر روی یک کامپوننت کلاسی مشاهده می‌کنید. تزئین کننده‌ی inject، امکان دسترسی به مخازن حالت تزریقی به شیء Provider را میسر کرده و سپس توسط آن می‌توان props مورد انتظار کامپوننت را از مخازن متناظر استخراج کرده و در اختیار کامپوننت قرار داد. همچنین این کامپوننت توسط تزئین کننده‌ی observer نیز علامت گذاری شده‌است. در این حالت نیازی به تغییر سطر export default نیست.
import { inject, observer } from "mobx-react";
import React, { Component } from "react";

@inject(rootStore => ({
  count: rootStore.counterStore.totalNumbersInBasket,
  onRemoveAll: rootStore.marketStore.removeAll
}))
@observer
class BasketItemsCounter extends Component {
  render() {
    const { count, onRemoveAll } = this.props;
    return (
      // ...
    );
  }
}

export default BasketItemsCounter;

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-with-mobx-part4.zip
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت ششم - کامپوننت‌های تو در تو
گاهی از اوقات جهت refactoring یک template بزرگ، بهتر است آن‌را به چند template کوچک خرد کرد و سپس از جمع آن‌ها به صورت یک template اصلی استفاده نمود. در این حالت نیاز است بین این زیر کامپوننت‌ها و کامپوننت‌های دربرگیرنده‌ی آن‌ها ارتباطات لازم را برقرار کرد.
تا اینجا در قسمت سوم، نحوه‌ی قراردادن یک کامپوننت را در کامپوننتی دیگر، توسط مقدار دهی خاصیت directives مزین کننده‌ی Component بررسی کردیم. همینقدر که یک کامپوننت دارای selector باشد، قابلیت قرارگرفتن در یک کامپوننت دیگر را دارد. اما چگونه باید بین این کامپوننت‌ها ارتباط برقرار کرد؟


تهیه کامپوننت نمایش ستاره‌ای امتیازهای محصولات

مثال نمایش لیست محصولات سری جاری، دارای ستون «5Star Rating» است. در این قسمت می‌خواهیم بجای نمایش عددی این امتیازها، کامپوننتی را طراحی کنیم که نماش ستاره‌ای آن‌ها را سبب شود. این کامپوننت باید بتواند یک مقدار ورودی، یا همان عدد امتیاز محصول را از کامپوننت دربرگیرنده‌ی آن دریافت کند. همچنین می‌خواهیم اگر کاربر بر روی این ستاره‌ها کلیک کرد، کامپوننت در برگیرنده را نیز مطلع سازیم.
در این مثال در فایل product-list.component.html چنین سطری تعریف شده‌است:
 <td>{{ product.starRating }}</td>
البته می‌توان در همینجا کدهای نمایش ستاره‌ای را بجای درج مقدار عددی آن قرار داد، اما ... قالب جاری را بیش از اندازه شلوغ خواهد کرد. به همین دلیل بهتر است نمایش آن‌را تبدیل به یک کامپوننت مجزا کرد. به علاوه در این حالت، قابلیت استفاده‌ی مجدد از آن در سایر کامپوننت‌ها نیز وجود خواهد داشت.
با توجه به اینکه کامپوننت نمایش ستاره‌ای امتیازها، قابلیت استفاده‌ی مجدد را دارد و الزامی ندارد که حتما در لیست محصولات، بکار گرفته شود، بهتر است محل تعریف آن‌را به خارج از پوشه‌ی products فعلی منتقل کنیم. برای مثال می‌توان پوشه‌ی app\shared را برای آن و تمامی کامپوننت‌های با قابلیت استفاده‌ی مجدد ایجاد کرد.


برای شروع، فایل جدید App\shared\star.component.ts را اضافه کنید؛ با کدهای کامل ذیل:
import { Component, OnChanges, Input, Output, EventEmitter } from 'angular2/core';
 
@Component({
    selector: 'ai-star',
    templateUrl: 'app/shared/star.component.html',
    styleUrls: ['app/shared/star.component.css']
})
export class StarComponent implements OnChanges {
    @Input() rating: number;
    starWidth: number;
    @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
 
    ngOnChanges(): void {
        this.starWidth = this.rating * 86 / 5;
    }
 
    onClick() {
        this.ratingClicked.emit(`The rating ${this.rating} was clicked!`);
    }
}
روش ساخت این کامپوننت نیز همانند سایر کامپوننت‌ها است و اصول کلی آن تفاوتی نمی‌کند. در اینجا نیز کلاسی وجود دارد که export شده و همچنین به Component مزین است. مقدار selector آن نیز به ai-star تنظیم شده‌است.
سپس مسیر template و مسیر فایل css ویژه‌ی آن، در تزئین کننده‌ی Component مشخص شده‌اند. محتوای کامل این دو فایل را در ذیل مشاهده می‌کنید:
الف) محتوای فایل App\shared\star.component.html
<div class="crop"
     [style.width.px]="starWidth"
     [title]="rating"
     (click)='onClick()'>
    <div style="width: 86px">
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
    </div>
</div>
ب) محتوای فایل App\shared\star.component.css
.crop {
    overflow: hidden;
}
 
div {
    cursor: pointer;
}
قالب star.component.html کار نمایش پنج ستاره را انجام می‌دهد. عرض کلی آن بر اساس مقدار خاصیت starWidth مشخص می‌شود و بر همین اساس، تعداد نمایان ستاره‌ها، مشخص خواهند شد. خاصیت starWidth به width این div بر حسب px، متصل شده‌است (property binding). همچنین خاصیت title این div نیز به مقدار rating متصل شده‌است و اگر بر روی آن کلیک شود، متد onClick را در کلاس متناظر با کامپوننت خود، فراخوانی خواهد کرد (event binding).

معرفی مقدماتی life cycle hooks در قسمت قبل صورت گرفت. در اینجا چون نیاز است به ازای هر بار رندر شدن این کامپوننت، عرض آن متفاوت باشد، بنابراین نیاز است راهی را پیدا کنیم تا بتوان مقدار خاصیت starWidth را متغیر کرد. به همین منظور از hook مخصوص این تغییرات یا همان OnChanges استفاده می‌شود. بنابراین باید کلاس این کامپوننت، اینترفیس OnChanges را پیاده سازی کند. پس از آن، importهای لازم جهت تعریف OnChanges به ابتدای فایل اضافه شده و همچنین متد ngOnChanges نیز جهت تکمیل کار پیاده سازی اینترفیس OnChanges، به کلاس جاری اضافه می‌شود.
کار متد ngOnChanges، تبدیل عدد امتیاز یک محصول، به عرض div نمایش ستاره‌ها است.


مکانیزم کار رخداد ngOnChanges و دریافت اطلاعات از والد

متد ngOnChanges، تنها به خواص ویژه‌ای به نام «input properties» واکنش نشان می‌دهد. اگر یک کامپوننت تو در توی قرار گرفته‌ی در یک کامپوننت دیگر، بخواهد اطلاعاتی را از والد خود دریافت کند، باید خاصیتی را در معرض دید آن دربرگیرنده قرار دهد. این کار توسط decorator ویژه‌ای به نام ()Input@ انجام می‌شود.
به همین جهت است که پیش از خاصیت rating در کلاس  StarComponent، شاهد درج مزین کننده‌ی ویژه‌ی ()Input@ هستیم:
export class StarComponent implements OnChanges {
      @Input() rating: number;
مزین کننده‌ی ()Input@ را به هر خاصیتی با هر نوعی، می‌توان انتساب داد. در اینجا نیاز است کامپوننت نمایش ستاره‌ای امتیازها، عدد امتیاز را از والد خود دریافت کنند. به همین جهت خاصیت امتیاز را از نوع «خاصیت ورودی» مشخص کرده‌ایم.
پس از آن، کامپوننت دربرگیرنده یا والد، این خاصیت ورودی ویژه را از طریق روش property binding متداول، مقدار دهی می‌کند:
 [rating]='product.starRating'

بدیهی است در اینجا چون خاصیت starWidth از نوع ورودی تعریف نشده‌است، قابلیت property binging فوق را در کامپوننت والد، ندارد.

اکنون به ازای هر بار نمایش این کامپوننت فرزند، خاصیت rating ورودی آن مقدار دهی شده و مقدار آن در رخداد ngOnChanges قابل دسترسی و استفاده خواهد بود. اینجا است که می‌توان از این مقدار تغییر یافته، جهت ترجمه‌ی آن به عرض div نمایش ستاره‌ها، استفاده کرد.


ارسال داده‌ها از کامپوننت فرزند به کامپوننت والد

تا اینجا با استفاده از «خواص ورودی» امکان دسترسی به مقادیر ارسالی از طرف والد را در کامپوننت فرزند، پیدا کردیم. عکس آن نیز امکان پذیر است؛ اما توسط رخدادها.
کامپوننت فرزند، با استفاده از decorator ویژه‌ی دیگری به نام ()Output@ امکان ارسال رخدادها را به کامپوننت والد پیدا می‌کند:
export class StarComponent implements OnChanges {
    @Input() rating: number;
    starWidth: number;
    @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
در اینجا خاصیت ratingClicked را که با ()Output@ مزین شده‌است، مشاهده می‌کنید. نوع این خاصیت باید از نوع رخدادها باشد که در AngularJS 2.0 توسط شیء EventEmitter جنریک، تعریف شده‌است. در این مثال، نوع رخداد و مقداری که توسط آن ارسال می‌شود، رشته‌ای درنظر گرفته شده‌است. اگر نیاز به ارسال چندین مقدار بود، می‌توان یک شیء را در اینجا مشخص کرد.
در مثال جاری اگر کاربر بر روی div ستاره‌های نمایش داده شده کلیک کند، اتصال به آن از طریق event binging متداول انجام می‌شود (متد جدید onClick به رخداد click متصل شده‌است):
<div class="crop"
     [style.width.px]="starWidth"
     [title]="rating"
     (click)='onClick()'>
و سپس این رخداد کلیک، در کلاس StarComponent به نحو ذیل به والد خود منتقل خواهد شد:
onClick() {
     this.ratingClicked.emit(`The rating ${this.rating} was clicked!`);
 }
در اینجا متد emit است که اطلاعات را به کامپوننت دربرگیرنده، ارسال می‌کند. نحوه‌ی تعریف این رشته هم توسط back tick مربوط به ES 6 صورت گرفته‌است.

تا اینجا مرحله‌ی تنظیمات رخدادها در کامپوننت فرزند صورت گرفت. ابتدا خاصیتی از نوع Output تعریف شد. سپس در کدهای قالب این کامپوننت جدید، متد onClick به رخداد click متصل گردید و سپس در کدهای مدیریت کننده‌ی این متد، متد ratingClicked.emit جهت ارسال اطلاعات نهایی به والد، فراخوانی گردید.

اکنون در کامپوننت والد، باید این مراحل برای دریافت اطلاعات از کامپوننت فرزند خود، طی شوند:
الف) ابتدا نام خاصیت مزین شده‌ی با Output، به عنوان مقصد event binding مشخص می‌شود و سپس متدی در کلاس کامپوننت والد، به آن متصل می‌گردد:
 (ratingClicked)='onRatingClicked($event)'
event$ مقدار ارسالی از کامپوننت فرزند را به کامپوننت والد منتقل می‌کند.
ب) در ادامه، تعریف این متد جدید متصل شده را به کلاس ProductListComponent اضافه می‌کنیم:
onRatingClicked(message: string): void {
    this.pageTitle = 'Product List: ' + message;
}
نوع پارامتر ورودی این متد را با توجه به نوع رشته‌ای <EventEmitter<string در کلاس StarComponent  تعریف کردیم.
به این ترتیب با کلیک بر روی div هر کامپوننت نمایش ستاره‌ای امتیازها، خاصیت pageTitle درج شده‌ی در صفحه تغییر می‌کند.


استفاده از کامپوننت نمایش ستاره‌ای امتیازها

نکات کلی افزودن این کامپوننت جدید، تفاوتی با مطالب عنوان شده‌ی در قسمت سوم، در حین بررسی مراحل افزودن دایرکتیو نمایش لیست محصولات، به کامپوننت ریشه‌ی سایت ندارد و یکی هستند.
برای افزودن و استفاده از این کامپوننت جدید، ابتدا قالب product-list.component.html را گشوده و سپس سطر نمایش عددی امتیاز یک محصول را به نحو ذیل تغییر می‌دهیم:
<td>
    <ai-star [rating]='product.starRating'
             (ratingClicked)='onRatingClicked($event)'>
    </ai-star>
</td>
همانطور که ملاحظه می‌کنید، ابتدا selector این کامپوننت جدید، به صورت یک المان جدید و سفارشی HTML، در قالب کامپوننت لیست محصولات درج می‌شود. همچنین خاصیت rating ورودی آن به عدد امتیاز محصول جاری در حال رندر، متصل شده‌است. به علاوه توسط event binding به خاصیت ratingClicked که از نوع ()Output@ تعریف شده‌است، متد onRatingClicked متصل گردیده‌است.

سپس باید به کلاس کامپوننت لیست محصولات (کامپوننت در برگیرنده) اعلام کرد که این کامپوننت جدید را باید از کجا پیدا کند. برای این منظور فایل product-list.component.ts را گشوده و خاصیت directives این کامپوننت را مقدار دهی می‌کنیم:
import { Component, OnInit } from 'angular2/core';
import { IProduct } from './product';
import { ProductFilterPipe } from './product-filter.pipe';
import { StarComponent } from '../shared/star.component';
 
@Component({
    selector: 'pm-products',
    templateUrl: 'app/products/product-list.component.html',
    styleUrls: ['app/products/product-list.component.css'],
    pipes: [ProductFilterPipe],
    directives: [StarComponent]
})
در اینجا دو کار جدید انجام شده‌است. ابتدا خاصیت directives، به نام کلاس این کامپوننت جدید (StarComponent)، تنظیم شده‌است. سپس تعریف import ماژول این کامپوننت، به ابتدای فایل جاری اضافه شده‌است.

نمونه‌ای از اجرای برنامه را در تصویر ذیل مشاهده می‌کنید:

در اینجا ستون امتیازهای محصولات با کامپوننت نمایش ستاره‌ای این امتیازها جایگزین شده‌است و همچنین با کلیک بر روی یکی از آن‌ها، عنوان panel جاری تغییر کرده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part6.zip



خلاصه‌ی بحث

در اینجا نحوه‌ی طراحی API عمومی یک کامپوننت را بررسی کردیم. تا زمانیکه خواص کلاس یک کامپوننت به نحو متداولی تعریف می‌شوند، میدان دید آن‌ها محدود است به قالب تعریف شده‌ی متناظر با آن‌ها. اگر نیاز است خاصیتی خارج از این قالب و به صورت عمومی در کامپوننت دربرگیرنده‌ی دیگری در دسترس قرار گیرد، آن‌را با مزین کننده‌ی ()Input@ مشخص می‌کنیم و اگر قرار است این کامپوننت فرزند، اطلاعاتی را به کامپوننت والد ارسال کند، این‌کار را توسط رخدادها و با تعریف ویژگی ()Output@ و EventEmitter انجام می‌دهد. نوع آرگومان جنریک EventEmitter، تعیین کننده‌ی نوع اطلاعاتی است که قرار است به کامپوننت دربرگیرنده ارسال شوند.
پس از تعریف کامپوننت فرزند، برای تعریف آن در کامپوننت والد، از نام selector آن به عنوان یک المان جدید HTML استفاده می‌شود و سپس با استفاده از property binding، اطلاعات لازم، به خاصیت از نوع ()Input@ کامپوننت فرزند ارسال می‌گردد. از event binding برای دریافت رخدادها از کامپوننت فرزند استفاده می‌شود. در اینجا هر رخدادی که توسط مزین کننده‌ی ()Output@ تعریف شده باشد، می‌تواند به عنوان مقصد event binding تعریف شود و اگر نیاز است به رخدادهای property binding از والد به فرزند، گوش فرا داد، می‌توان اینترفیس OnChanges را در کلاس کامپوننت فرزند پیاده سازی کرد.
مطالب
بررسی تغییرات Blazor 8x - قسمت دوم - بررسی حالت رندر سمت سرور
در قسمت قبل، حالت‌های مختلف رندر کامپوننت‌ها را در Blazor 8x معرفی کردیم. در این قسمت می‌خواهیم نحوه‌ی کارکرد دو حالت InteractiveServer و StreamRendering را به همراه چند مثال بررسی کنیم.


معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8

پس از نصب SDK دات نت 8، دیگر خبری از قالب‌های قدیمی پروژه‌های blazor server و blazor wasm نیست! در اینجا در ابتدا باید مشخص کرد که سطح تعاملی برنامه در چه حدی است. در ادامه 4 روش شروع پروژه‌های Blazor 8x را مشاهده می‌کنید که توسط پرچم interactivity--، نوع رندر برنامه در آن‌ها مشخص شده‌است:

اجرای قسمت‌های تعاملی برنامه بر روی سرور:
dotnet new blazor --interactivity Server

اجرای قسمت‌های تعاملی برنامه در مرورگر، توسط فناوری وب‌اسمبلی:
dotnet new blazor --interactivity WebAssembly

برای اجرای قسمت‌های تعاملی برنامه، ابتدا حالت Server فعالسازی می‌شود تا فایل‌های WebAssembly دریافت شوند، سپس فقط از WebAssembly استفاده می‌کند:
dotnet new blazor --interactivity Auto

فقط از حالت SSR یا همان static server rendering استفاده می‌شود (این نوع برنامه‌ها تعاملی نیستند):
dotnet new blazor --interactivity None

سایر گزینه‌ها را با اجرای دستور dotnet new blazor --help می‌توانید مشاهده کنید.


نکته‌ی مهم! در قالب‌های آماده‌ی Blazor 8x، حالت SSR، پیش‌فرض است.

هرچند در تمام پروژه‌های فوق، انتخاب حالت‌های مختلف رندر را مشاهده می‌کنید، اما این انتخاب‌ها صرفا دو مقصود مهم را دنبال می‌کنند:
الف) تنظیم فایل Program.cs برنامه جهت افزودن وابستگی‌های مورد نیاز، به صورت خودکار.
ب) ایجاد پروژه‌ی کلاینت (علاوه بر پروژه‌ی سرور)، در صورت نیاز. برای مثال حالت‌های وب‌اسمبلی و Auto، هر دو به همراه یک پروژه‌ی کلاینت وب‌اسمبلی هم هستند؛ اما حالت‌های Server و None، خیر.

در تمام این پروژه‌ها هر صفحه و یا کامپوننتی که ایجاد می‌شود، به صورت پیش‌فرض بر اساس SSR رندر و نمایش داده خواهد شد؛ مگر اینکه به صورت صریحی این نحوه‌ی رندر را بازنویسی کنیم. برای مثال مشخص کنیم که قرار است بر اساس Blazor Server اجرا شود و یا وب‌اسمبلی و یا حالت Auto.

اما ... اگر علاقمند بودیم تا به حالت‌های پیش‌از دات نت 8 رجوع کنیم و تمام برنامه را به صورت یک‌دست تعاملی کنیم و حالت SSR پیش‌فرض نباشد، می‌توان از پرچم all-interactive-- که به انتهای دستورات فوق قابل افزوده شدن است، کمک گرفت. این پرچم، فایل App.Razor را جهت تنظیم سراسری حالت‌های رندر، ویرایش می‌کند. این مورد را در ادامه‌ی این مطلب، در قسمت «روشی ساده برای تعاملی کردن کل برنامه» بیشتر بررسی می‌کنیم.


بررسی حالت Server side rendering

برای بررسی این حالت یک پوشه‌ی جدید را ایجاد کرده و توسط خط فرمان، دستور dotnet new blazor --interactivity Server را در ریشه‌ی آن اجرا می‌کنیم. پس از ایجاد ساختار ابتدایی پروژه بر اساس این قالب انتخابی، فایل Program.cs جدید آن، چنین شکلی را دارد:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents().AddInteractiveServerComponents();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();
مهم‌ترین قسمت‌های آن، متدهای AddInteractiveServerComponents و AddInteractiveServerRenderMode هستند که server-side rendering را به همراه امکان داشتن کامپوننت‌های تعاملی، میسر می‌کنند.
server-side rendering به این معنا است که برنامه‌ی سمت سرور، کل DOM و HTML نهایی را تولید کرده و به مرورگر کاربر ارائه می‌کند. مرورگر هم این DOM را نمایش می‌دهد. فقط همین! در اینجا هیچ خبری از اتصال دائم SignalR نیست و محتوای ارائه شده، یک محتوای استاتیک است. این حالت رندر، برای ارائه‌ی محتواهای فقط خواندنی غیرتعاملی، فوق العاده‌است؛ امکان از لحظه‌ای که نیاز به کلیک بر روی دکمه‌ای باشد، دیگر پاسخگو نیست. به همین جهت در اینجا امکان تعاملی کردن تعدادی از کامپوننت‌های ویژه و مدنظر نیز پیش‌بینی شده‌اند تا بتوان به ترکیبی از server-side rendering و client-side rendering رسید.
حالت پیش‌فرض در اینجا، ارائه‌ی محتوای استاتیک است. بنابراین هر کامپوننتی در اینجا ابتدا بر روی سرور رندر شده (HTML ابتدایی آن آماده شده) و به سمت مرورگر کاربر ارسال می‌شود. اگر کامپوننتی نیاز به امکانات تعاملی داشت باید آن‌را دقیقا توسط ویژگی InteractiveXYZ مشخص کند؛ مانند مثال زیر:
@page "/counter"
@rendermode InteractiveServer

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
همانطور که مشاهده می‌کنید، این کامپوننت از روش رندر InteractiveServer استفاده می‌کند. برای درک نحوه‌ی کارکرد آن، همین سطر را حذف، یا کامنت کنید و سپس برنامه را اجرا کنید. در حین مشاهده‌ی این صفحه، همه چیز به خوبی نمایش داده می‌شود و حتی دکمه‌ی Click me هم مشخص است. اما ... با کلیک بر روی آن اتفاقی رخ نمی‌دهد! علت اینجا است که اکنون این صفحه، یک صفحه‌ی کاملا استاتیک است و دیگر تعاملی نیست.
در ادامه، مجددا سطر کامنت شده را به حالت عادی برگردانید و سپس برنامه را اجرا کنید. پیش از باز کردن صفحه‌ی Counter، ابتدا developer tools مرورگر خود را گشوده و برگه‌ی network آن‌را انتخاب و سپس صفحه‌ی Counter را باز کنید. در این لحظه‌است که مشاهده می‌کنید یک اتصال وب‌سوکت برقرار شد. این اتصال است که قابلیت‌های تعاملی صفحه را برقرار کرده و مدیریت می‌کند (این اتصال دائم SignalR است که این صفحه را همانند برنامه‌های Blazor Web Server پیشین مدیریت می‌کند).

یک نکته: در برنامه‌های Blazor Server سنتی، امکان فعالسازی قابلیتی به نام prerender نیز وجود دارد. یعنی سرور، ابتدا صفحه را رندر کرده و محتوای استاتیک آن‌را به سمت مرورگر کاربر ارسال می‌کند و سپس اتصال SignalR برقرار می‌شود. در دات نت 8، این حالت، حالت پیش‌فرض است. اگر آن‌را نمی‌خواهید باید به نحو زیر غیرفعالش کنید:
@rendermode InteractiveServerRenderModeWithoutPrerendering

@code{
  static readonly IComponentRenderMode InteractiveServerRenderModeWithoutPrerendering = 
        new InteractiveServerRenderMode(false);
}


روشی ساده برای تعاملی کردن کل برنامه

اگر می‌خواهید رفتار برنامه را همانند Blazor Server سابق کنید و نمی‌خواهید به ازای هر کامپوننت، نحوه‌ی رندر آن‌را به صورت سفارشی انتخاب کنید، فقط کافی است فایل App.razor را باز کرده:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="MyApp.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>

<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>
و قسمت‌های HeadOutlet و مسیریابی آن‌را به صورت زیر تغییر دهید تا به کل برنامه اعمال شود:
<HeadOutlet @rendermode="@InteractiveServer" />
...
<Routes @rendermode="@InteractiveServer" />
این مورد دقیقا همان کاری است که پرچم all-interactive-- در هنگام ایجاد پروژه‌های جدید Blazor 8x به کمک NET CLI.، انجام می‌دهد. یک چنین گزینه‌ای در ویژوال استودیو نیز هنگام ایجاد پروژه‌ها‌ی جدید Blazor وجود دارد و به شما این امکان را می‌دهد که بین حالت‌های تعاملی Per page/component و Global، یکی را انتخاب کنید. در حین استفاده‌ی از CLI، نیازی به ذکر حالت تعاملی Per page/component نیست؛ چون حالت پیش‌فرض، یا همان SSR است. حالت Global هم فقط فایل App.Razor را به صورت فوق، ویرایش و تنظیم می‌کند.


در این حالت دیگر نیازی نیست تا به ازای هر کامپوننت و صفحه، نحوه‌ی رندر را مشخص کنیم؛ چون این نحوه، از بالاترین سطح، به تمام زیرکامپوننت‌ها به ارث می‌رسد (درباره‌ی این نکته در قسمت قبل، توضیحاتی ارائه شد).


بررسی حالت Streaming Rendering

در اینجا مثال پیش‌فرض Weather.razor قالب پیش‌فرض مورد استفاده‌ی جاری را کمی تغییر داده‌ایم که کدهای نهایی آن به صورت زیر است (2 قسمت forecasts.AddRange_ را اضافه‌تر دارد):

@page "/weather"
@attribute [StreamRendering(prerender: true)]

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>

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

@code {
    private List<WeatherForecast>? _forecasts;

    protected override async Task OnInitializedAsync()
    {
    // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(500);

        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[]
                        {
                            "Freezing",
                            "Bracing",
                            "Chilly",
                            "Cool",
                            "Mild",
                            "Warm",
                            "Balmy",
                            "Hot",
                            "Sweltering",
                            "Scorching",
                        };
        _forecasts = GetWeatherForecasts(startDate, summaries).ToList();
        StateHasChanged();

    // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(1000);
        _forecasts.AddRange(GetWeatherForecasts(startDate, summaries));
        StateHasChanged();

        await Task.Delay(1000);
        _forecasts.AddRange(GetWeatherForecasts(startDate, summaries));
    }

    private static IEnumerable<WeatherForecast> GetWeatherForecasts(DateOnly startDate, string[] summaries)
    {
        return Enumerable.Range(1, 5)
                         .Select(index => new WeatherForecast
                                          {
                                              Date = startDate.AddDays(index),
                                              TemperatureC = Random.Shared.Next(-20, 55),
                                              Summary = summaries[Random.Shared.Next(summaries.Length)],
                                          });
    }

    private class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }

}
برای بررسی این مثال، ابتدا سطر ویژگی StreamRendering را حذف و یا کامنت کرده و سپس برنامه را اجرا کنید. پس از اجرای برنامه، با انتخاب مشاهده‌ی صفحه‌ی Weather، هیچگاه قسمت loading که در حالت forecasts == null_ قرار است ظاهر شود، نمایش داده نمی‌شود؛ چون در این حالت (حذف نوع رندر)، صفحه‌ی نهایی که به کاربر ارائه خواهد شد، یک صفحه‌ی استاتیک کاملا رندر شده‌ی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحه‌ی نهایی را دریافت و مشاهده کند. در این حالت امکانات تعاملی Blazor server وجود خارجی ندارند.
در ادامه مجددا سطر ویژگی StreamRendering را به حالت قبلی برگردانید و برنامه را اجرا کنید. در این حالت ابتدا قسمت loading ظاهر می‌شود و سپس در طی چند مرحله با توجه به Task.Delay‌های قرار داده شده، صفحه رندر شده و تکمیل می‌شود.
اتفاقی که در اینجا رخ می‌دهد، استفاده از فناوری  HTML Streaming است که مختص به مایکروسافت هم نیست. در حالت Streaming، هربار قطعه‌ای از HTML ای که قرار است به کاربر ارائه شود، به صورت جریانی به سمت مرورگر ارسال می‌شود و مرورگر این قطعه‌ی جدید را بلافاصله نمایش می‌دهد. نکته‌ی جالب این روش، عدم نیاز به اتصال SignalR و یا اجرای WASM درون مرورگر است.

Streaming rendering حالت بینابین رندر کامل در سمت سرور و رندر کامل در سمت کلاینت است. در حالت رندر سمت سرور، کل HTML صفحه ابتدا توسط سرور تهیه و بازگشت داده می‌شود و کاربر باید تا پایان عملیات تهیه‌ی این HTML نهایی، منتظر باقی بماند و در این بین چیزی را مشاهده نخواهد کرد. در حالت Streaming rendering، هنوز هم همان حالت تهیه‌ی HTML استاتیک سمت سرور برقرار است؛ به همراه تعدادی محل جایگذاری اطلاعات جدید. به محض پایان یک عمل async سمت سرور که سه نمونه‌ی آن را در مثال فوق مشاهده می‌کنید، برنامه، جریان قطعه‌ای از اطلاعات استاتیک را به سمت مرورگر کاربر ارسال می‌کند تا در مکان‌هایی از پیش تعیین شده، درج شوند.
در حالت SSR، فقط یکبار شانس ارسال کل اطلاعات به سمت مرورگر کاربر وجود دارد؛ اما در حالت Streaming rendering، ابتدا می‌توان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آن‌را به محض آماده شدن در طی چند مرحله بازگشت داد. در این حالت نمایش گزارشی از اطلاعاتی که ممکن است با تاخیر در سمت سرور تهیه شوند، ساده‌تر می‌شود. یعنی می‌توان هربار قسمتی را که تهیه شده، برای نمایش بازگشت داد و کاربر تا مدت زیادی منتظر نمایش کل صفحه باقی نخواهد ماند.


روش نهایی معرفی نحوه‌ی رندر صفحات

بجای استفاده از ویژگی‌های RenderModeXyz جهت معرفی نحوه‌ی رندر کامپوننت‌ها و صفحات (که تا پیش از نگارش RTM معرفی شده بودند و چندبار هم تغییر کردند)، می‌توان از دایرکتیو جدیدی به نام rendermode@ با سه مقدار InteractiveServer، InteractiveWebAssembly و InteractiveAuto استفاده کرد. برای سهولت تعریف این موارد باید سطر ذیل را به فایل Imports.razor_ اضافه نمود:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
در این حالت تعریف قدیمی سطر ذیل:
@attribute [RenderModeInteractiveServer]
به صورت زیر:
@rendermode InteractiveServer
تبدیل می‌شود.

اگر هم قصد سفارشی سازی آن‌ها را دارید، برای مثال می‌خواهید prerender را در آن‌ها false کنید، روش کار به صورت زیر است:
@rendermode renderMode

@code {
    static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false);
}
نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
سلام

راستش من منوهای چند سطحی پویا را برای bootstrap navbar نوشتم. الگوریتمش را مجبور شدم به صورت بازگشتی بنویسم.اگر کسی می‌تونه به صورت غیر بازگشتی بگه ممنونش میشم.
این کد یه partialpage برای navbar هست که هرکسی برای bootstrap به راحتی میتونه استفاده کنه.

@model IEnumerable<DomainClasses.Page>
@helper ShowNavBar(IEnumerable<DomainClasses.Page> pages)
{
    
    foreach (var page in pages)
    {
        if (page != null)
        {
            if (page.Children.Count == 0)
            {
                <text><li><a tabindex="-1" href="#">@page.Title</a></li></text>
            }

            if (page.Children.Count > 0 && page.Parent == null)
            {         
                <text><li class="dropdown"><a class="dropdown-toggle" id="dLabel" role="button" data-toggle="dropdown" data-target="#" href="/page.html">@page.Title<b class="caret"></b></a><ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"><li><a tabindex="-1" href="#">@page.Title</a></li></text>
                @ShowNavBar(page.Children)
                @:</ul></li>
            }

            if (page.Children.Count > 0 && page.Parent != null)
            {         
                <text><li class="dropdown-submenu"><a tabindex="-1" href="#">@page.Title</a><ul class="dropdown-menu"></text>
                @ShowNavBar(page.Children)
                @:</ul></li>
            }

        }
    }
    
}
<div class="navbar" style="margin-bottom: 10px;">
    <div class="navbar-inner">
        <a class="brand" href="www.google.com">IT-EBOOK</a>
        <ul class="nav">
            <li class="active"><a href="#">خانه</a></li>
            <li><a href="#">ورود</a></li>
            @ShowNavBar(Model)
            <li><a href="#">ارتباط با ما</a></li>
        </ul>
        <div class="input-append pull-left visible-desktop" style="margin-top: 5px;">
            <input class="span6 search-input" id="Text1" type="text">
            <button class="btn btn-primary" type="button">جست و جو</button>
            <button class="btn btn-info btn-advanced-search" type="button">پیشرفته</button>
        </div>
    </div>
</div>



الان تنها مشکلم اینه که فیلد‌های اضافی هم کوئری گرفته میشه.میدونم فیلدهای اضافی(بر اساس مدلی که ذکر کردم) را چگونه با استفاده از select حذف کنم اما توی viewmodel نمیدونم چه جوری children را از اطلاعات پر کنم؟
 ممنون


 
مطالب
کامپوننت‌ها در AngularJS 1.5 - قسمت دوم - مسیریابی
در این قسمت به معرفی سیستم مسیریاب در Angular 1.5 خواهیم پرداخت. قبل از معرفی این سیستم ابتدا سیستم مسیریاب اصلی در Angular را بررسی خواهیم کرد.

مروری بر مسیریابی در AngularJS
برای استفاده از مسیریاب اصلی Angular کافی است از دایرکتیو ویژه‌ایی با نام ng-view به همراه یکسری تنظیمات پیکربندی استفاده کنیم. به عنوان مثال اگر آدرس صفحه با home/ مطابقت داشته باشد، تمپلیت home.html توسط دایرکتیو ng-view بارگذاری خواهد شد. برای فعال‌سازی این سیستم ابتدا باید پکیج angular-route را به پروژه مثال قسمت قبل اضافه کنید:
bower install angular-route --save
در ادامه لازم است وابستگی فوق را به صفحه‌ی index.html اضافه نمائید:
<script src="bower_components/angular-route/angular-route.min.js" type="text/javascript"></script>
اکنون درون صفحه به جای نمایش مستقیم کامپوننت، از دایرکتیو ng-view استفاده خواهیم کرد:
<div class="col-md-9">
                <ng-view></ng-view>
</div>

 همچنین مقدار فیلد url کامپوننت dntWidget را به صورت زیر تغییر دهید:
      model.panel = {
          title: "Panel Title",
          items: [
              {
                  title: "Home", url: "#/home"
              },
              {
                  title: "Articles", url: "#/articles"
              },
              {
                  title: "Authors", url: "#/authors"
              }
          ]
      };

در ادامه باید سیستم مسیریاب را به عنوان یک وابستگی به اپلیکیشن معرفی کنیم:
var module = angular.module("dntModule", ["ngRoute"]);
اکنون می‌توانیم پیکربندی موردنظر جهت هدایت آدرس‌ها به تمپلیت‌های مربوطه را بنویسیم:
module.config(function ($routeProvider) {
        $routeProvider
            .when("/home", { template: "<app-home></app-home>" })
            .when("/articles", { template: "<app-articles></app-articles>" })
            .when("/authors", { template: "<app-authors></app-authors>" })
            .otherwise({ redirectTo: "/home" });
    });

همانطور که مشاهده می‌کنید به route provider اعلام کرده‌ایم که در صورت مطابقت داشتن آدرس URL با هر کدام از حالت‌های فوق، تمپلیت متناسب با آن را نمایش بدهد. در نهایت توسط otherwise اگر آدرس، با هیچکدام از حالت‌های تعریف شده مطابقت نداشت، کاربر به آدرس home/ هدایت خواهد شد. 
نکته‌ایی که در کد فوق وجود دارد این است که سیستم مسیریاب اصلی Angular تا اینجا هیچ اطلاعی از وجود کامپوننت‌ها ندارد، اما می‌داند یک تمپلیت چیست. بنابراین از تمپلیت، جهت نمایش یک کامپوننت استفاده خواهد کرد.
برای ایجاد کامپوننت‌های فوق نیز می‌توانید آن را به صورت زیر ایجاد کنید:
module.component("appHome", {
        template: `
        <hr><div>
            <div>Panel heading = HomePage</div>
            <div>
                HomePage
            </div>
        </div>`
    });
    module.component("appArticles", {
        template: `
        <hr><div>
            <div>Panel heading = Articles</div>
            <div>
                Articles
            </div>
        </div>`
    });
    module.component("appAuthors", {
        template: `
        <hr><div>
            <div>Panel heading = Authors</div>
            <div>
                Authors
            </div>
        </div>`
    });

اکنون اگر برنامه را اجرا کنید، خواهید دید که به صورت پیش‌فرض به آدرس home/# هدایت خواهیم شد. زیرا آدرسی برای root، درون route configuration تعریف نکرده‌ایم:

اکنون توسط لینک‌های تعریف شده می‌توانیم به راحتی درون تمپلیت‌ها، پیمایش کنیم. همانطور که عنوان شد تا اینجا مسیریاب پیش‌فرض Angular هیچ اطلاعی از کامپوننت‌ها ندارد؛ بلکه آنها را با کمک template، به صورت غیر مستقیم، درون صفحه نمایش داده‌ایم.


معرفی Component Router

مزیت این روتر این است که به صورت اختصاصی برای کار با کامپوننت‌ها طراحی شده است. بنابراین دیگر نیازی به استفاده از template درون route configuration نیست. برای استفاده از این روتر ابتدا باید پکیج آن را نصب کنیم:

bower install angular-component-router --save

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

<script src="bower_components/angular-component-router/angular_1_router.js"></script>

همچنین درون فایل module.js به جای وابستگی ngRoute از ngComponentrouter استفاده خواهیم کرد:

var module = angular.module("dntModule", ["ngComponentRouter"]);

در ادامه به جای تمامی route configurations قبلی، اینبار یک کامپوننت جدید را به صورت زیر ایجاد خواهیم کرد:

module.component("appHome", {
        template: `
        <hr>
        <div>
            <div>Panel heading = HomePage</div>
            <div>
                HomePage
            </div>
        </div>`
    });

همانطور که مشاهده می‌کنید برای پاسخ‌گویی به تغییرات URL، مقدار routeConfig$ را مقداردهی کرده‌ایم. در اینجا به جای بارگذاری تمپلیت، خود کامپوننت، در هر یک از ruleهای فوق بارگذاری خواهد شد. برای حالت otherwise نیز از سینتکس **/ استفاده کرده‌ایم.

تمپلیت کامپوننت فوق نیز به صورت زیر است:

<div class="container">
    <div class="row">
        <div class="col-md-3">
            <hr>
            <dnt-widget></dnt-widget>
        </div>
        <div class="col-md-9">
            <ng-outlet></ng-outlet>
        </div>
    </div>
</div>

لازم به ذکر است دیگر نباید از دایرکتیو  ng-view استفاده کنیم؛ زیرا این دایرکتیو برای استفاده از روتر اصلی طراحی شده است. به جای آن از دایرکتیو ng-outlet استفاده شده است. این کامپوننت به عنوان یک کامپوننت top level عمل خواهد کرد. بنابراین درون صفحه‌ی index.html از کامپوننت فوق استفاده خواهیم کرد:

<html ng-app="dntModule">
<head>
    <meta charset="UTF-8">
    <title>Using Angular 1.5 Component Router</title>
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
    <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css">
</head>
<body>
    
    <dnt-app></dnt-app>

    <script src="bower_components/angular/angular.js" type="text/javascript"></script>
    <script src="bower_components/angular-component-router/angular_1_router.js"></script>
    <script src="scripts/module.js" type="text/javascript"></script>
    <script src="scripts/dnt-app.component.js"></script>
    <script src="scripts/dnt-widget.component.js"></script>
</body>
</html>

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

module.value("$routerRootComponent", "dntApp");

اکنون اگر برنامه را اجرا کنید خواهید دید که همانند قبل، کار خواهد کرد. اما اینبار از روتر جدید و سازگار با کامپوننت‌ها استفاده می‌کند.

کدهای این قسمت را نیز از اینجا می‌توانید دریافت کنید. 

مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت سوم - غنی سازی کامپوننت‌ها
در قسمت قبل، مقدمه‌ای بر نحوه‌ی تعریف یک کامپوننت در AngularJS 2.0 عنوان شد و همچنین نحوه‌ی بوت استرپ و آغاز اینگونه برنامه‌ها بررسی گردید. در این قسمت می‌خواهیم امکانات پیشرفته‌تری از کامپوننت‌ها را بررسی کنیم.


روش‌های مختلف تعریف خاصیت template در یک کامپوننت

در قسمت قبل، روش تعریف inline یک template را مشاهده کردید:
template:`
          <div><h1>{{pageTitle}}</h1>
               <div>My First Component</div>
          </div>
 `
در اینجا رشته‌ی قالب نهایی این View، در همان تعاریف متادیتای Component قرار گرفته‌است (روش inline). اگر این رشته تک سطری باشد، از روش متداول ذکر "" برای تعریف رشته‌ها در جاوا اسکریپت استفاده می‌شود و اگر این رشته چند سطری باشد، از back tick مربوط به ES 6 مانند مثال فوق کمک گرفته خواهد شد. استفاده از back ticks و رشته‌های چند سطری، نحوه‌ی تعریف قالب‌های inline را خواناتر می‌کند.
هر چند این روش تعریف قالب‌ها، مزیت سادگی و امکان مشاهده‌ی View را به همراه کدهای مرتبط با آن، در یک فایل میسر می‌کند، اما به دلیل رشته‌ای بودن، مزیت کار کردن با ادیتورهای وب، مانند داشتن intellisense، فرمت خودکار کدها و بررسی syntax را از دست خواهیم داد و با بیشتر شدن حجم این رشته، این مشکلات بیشتر نمایان خواهند شد.
به همین جهت قابلیت دیگری به نام linked template نیز در اینجا درنظر گرفته شده‌است:
 templateUrl: 'product-list.component.html'
در این حالت، محتوای قالب، به یک فایل html مجزا منتقل شده و سپس لینک آن در خاصیت دیگری از متادیتای Component به نام templateUrl ذکر می‌شود.


ساخت کامپوننت نمایش لیست محصولات

در ادامه می‌خواهیم کامپوننتی را طراحی کنیم که آرایه‌ای از محصولات را نمایش می‌دهد. در اینجا مرسوم است هر ویژگی برنامه، در یک پوشه‌ی مجزا قرار گیرد. به همین جهت در ادامه‌ی مثال قسمت قبل که پوشه‌ی app را به ریشه‌ی پروژه اضافه کردیم و سپس main.ts راه انداز و کامپوننت ریشه‌ی سایت یا app.component.ts را در آن تعریف کردیم، در داخل همین پوشه‌ی app، پوشه‌ی جدیدی را به نام products اضافه می‌کنیم. سپس به این پوشه‌ی جدید محصولات، فایل جدیدی را به نام product-list.component.html اضافه کنید. از این فایل جهت تعریف قالب کامپوننت لیست محصولات استفاده خواهیم کرد. در اینجا نیز مرسوم است نام قالب یک Component را به صورت نام ویژگی ختم شده‌ی به کلمه‌ی Component، با پسوند html تعریف کنیم.


پس از اضافه شدن فایل product-list.component.html، محتوای آن‌را به نحو ذیل تغییر دهید:
<div class='panel panel-default'>
    <div class='panel-heading'>
        {{pageTitle}}
    </div>
    <div class='panel-body'>
        <div class='row'>
            <div class='col-md-2'>Filter by:</div>
            <div class='col-md-4'>
                <input type='text' />
            </div>
        </div>
        <div class='row'>
            <div class='col-md-6'>
                <h3>Filtered by: </h3>
            </div>
        </div>
        <div class='table-responsive'>
            <table class='table'>
                <thead>
                    <tr>
                        <th>
                            <button class='btn btn-primary'>
                                Show Image
                            </button>
                        </th>
                        <th>Product</th>
                        <th>Code</th>
                        <th>Available</th>
                        <th>Price</th>
                        <th>5 Star Rating</th>
                    </tr>
                </thead>
                <tbody>
 
                </tbody>
            </table>
        </div>
    </div>
</div>
در اینجا قصد داریم داخل پنل بوت استرپ 3، لیستی از محصولات را به صورت یک جدول نمایش دهیم. همچنین می‌خواهیم قابلیت جستجوی داخل این لیست را نیز فراهم کنیم. فعلا شکل کلی این قالب را به نحو فوق تهیه می‌کنیم. قسمت tbody جدول آن را که قرار است لیست محصولات را رندر کند، در ادامه‌ی بحث تکمیل خواهیم کرد.
تنها نکته‌ی AngularJS 2.0 قالب فوق، اتصال به pageTitle است که نمونه‌ای از آن‌را در قسمت قبل با معرفی اولین کامپوننت مشاهده کرده‌اید.

در ادامه نیاز است برای این قالب و view، یک کامپوننت را طراحی کنیم که متشکل است از یک کلاس TypeScript ایی مزین شده به Component. بنابراین فایل ts جدیدی را به نام product-list.component.ts به پوشه‌ی App\products اضافه کنید؛ با این محتوا:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-products',
    templateUrl: 'app/products/product-list.component.html'
})
export class ProductListComponent {
    pageTitle: string = 'Product List';
}


با جزئیات نحوه‌ی تعریف یک کامپوننت در قسمت قبل در حین معرفی کامپوننت‌ها آشنا شدیم. در اینجا کلاس ProductListComponent با واژه‌ی کلیدی export همراه است تا توسط module loader برنامه قابلیت بارگذاری را پیدا کند. همچنین خاصیت عمومی pageTitle نیز در آن تعریف شده‌است تا در قالب مرتبط مورد استفاده قرار گیرد.
سپس این کلاس، با decorator ویژه‌ای به نام Component مزین شده‌است تا AngularJS 2.0 بداند که هدف از تعریف آن، ایجاد یک کامپوننت جدید است. مقدار selector آن که تشکیل دهنده‌ی یک تگ HTML سفارشی متناظر با آن خواهد شد، به pm-products تنظیم شده‌است و اینبار بجای تعریف inline قالب آن به صورت یک رشته، از خاصیت templateUrl جهت معرفی مسیر فایل html قالبی که پیشتر آماده کردیم، کمک گرفته شده‌است.


نمایش کامپوننت لیست محصولات در صفحه‌ی اصلی سایت

خوب، تا اینجا یک کامپوننت جدید را به نام لیست محصولات، ایجاد کردیم؛ اما چگونه باید آن‌را نمایش دهیم؟
در قسمت قبل که کامپوننت ریشه‌ی برنامه یا AppComponent را تعریف کردیم، نام selector آن را pm-app درنظر گرفتیم و در نهایت این directive سفارشی را به نحو ذیل در body صفحه‌ی اصلی سایت نمایش دادیم:
    <div>
        @RenderBody()
        <pm-app>Loading App...</pm-app>
    </div>
اما این روش، تنها برای root component سایت مناسب است. برای سایر کامپوننت‌های غیر ریشه‌ای (یعنی تمام کامپوننت‌ها)، سه مرحله‌ی زیر باید طی شوند:
الف) تگ سفارشی این دایرکتیو جدید را به کامپوننت ریشه‌ی سایت یا همان AppComponent اضافه می‌کنیم. بنابراین فایل app.component.ts را گشوده و سپس selector کامپوننت لیست محصولات را به قالب آن اضافه کنید:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <pm-products></pm-products>
    </div>
    `
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
همانطور که مشاهده می‌کنید، تگ جدید pm-products بر اساس نام selector کامپوننت لیست محصولات، به قالب کامپوننت ریشه‌ی سایت اضافه شده‌است.
ب) تا اینجا یک دایرکتیو جدید را به نام pm-products به یک کامپوننت دیگر اضافه کرده‌ایم. اما این کامپوننت نمی‌داند که اطلاعات آن‌را باید از کجا تامین کند. برای این منظور خاصیت جدیدی را به نام directives به لیست خاصیت‌های Component ریشه‌ی سایت اضافه می‌کنیم. این خاصیت، آرایه‌ای از دایرکتیوهای سفارشی را قبول می‌کند:
 directives: [ProductListComponent]
ج) بلافاصله که این تغییر را اعمال کنید، در ادیتور TypeScript ایی موجود، ذیل کلمه‌ی ProductListComponent خط قرمز کشیده خواهد شد. چون هنوز مشخص نکرده‌ایم که این شیء جدید باید از کدام ماژول تامین شود و ناشناخته‌است. بنابراین import مربوطه را به ابتدای فایل اضافه می‌کنیم:
import { Component } from 'angular2/core';
import { ProductListComponent } from './products/product-list.component';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <pm-products></pm-products>
    </div>
    `,
    directives: [ProductListComponent]
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
کدهای فوق، کد نهایی کامپوننت ریشه‌ی سایت هستند که به آن selector جدیدی به نام pm-products اضافه شده‌است. سپس directive متناظر آن به لیست دایرکتیوهای کامپوننت جاری اضافه شده و در نهایت این دایرکتیو، از ماژول مرتبط با آن import شده‌است.

این سه مرحله، مراحلی هستند که جهت افزودن هر دایرکتیو جدید به کامپوننتی مشخص، باید طی شوند.

خوب، اکنون اگر برنامه را اجرا کنیم، چنین خروجی را می‌توان مشاهده کرد:


یک نکته
اگر برنامه را اجرا کردید و خروجی را مشاهده نکردید، مطمئن شوید که فایل‌های ts شما کامپایل شده‌اند. فشردن دکمه‌ی ctrl+s مجدد در این فایل‌ها، سبب کامپایل مجدد آن‌ها می‌شوند و یا انتخاب گزینه‌ی Build و سپس ReBuild solution نیز همینکار را انجام می‌دهد.


غنی سازی کامپوننت‌های AngularJS 2.0 با data-binding

در AngularJS 2.0 عملیات binding، کار مدیریت ارتباطات بین یک کلاس کامپوننت و قالب آن‌را انجام می‌دهد. نمونه‌ای از آن‌را پیشتر با خاصیت pageTitle و سپس نمایش آن در قالب کامپوننت متناظر با آن کلاس، مشاهده کرده‌اید. همچنین در اینجا یک قالب می‌تواند متدهای داخل کلاس کامپوننت خود را توسط رخدادها نیز فراخوانی کند.
به نحوه‌ی نمایش {{pageTitle}} اصطلاحا interpolation می‌گویند. در اینجا خاصیت pageTitle اطلاعات خود را از کلاس کامپوننت دریافت می‌کند. به این نوع binding، انقیاد یک طرفه یا one-way binding نیز گفته می‌شوند؛ از خاصیت کلاس شروع شده و به قالب خاتمه می‌یابد.
ویژگی interpolation فراتر است از صرفا نمایش یک خاصیت و می‌تواند حاوی محاسبات نیز باشد:
{{'Title: ' + pageTitle}}
{{2*20+1}}
و یا حتی در آن می‌توان متدی از کلاس کامپوننت را نیز فراخوانی کرد. در مثال زیر فرض شده‌است که متد getTitle، در کلاس متناظر با کامپوننت این قالب، تعریف شده‌است:
{{'Title: ' + getTitle()}}
کار interpolation درج عبارت محاسبه شده‌ی نهایی بین المان‌های html است؛ مانند:
 <h1>{{pageTitle}}</h1>
و یا حتی می‌توان این مقدار نهایی را به خواص المان‌های html نیز نسبت داد:
 <h1 innerText={{pageTitle}}></h1>
در این مثال خاصیت innerText المان h1 توسط interpolation مقدار دهی شده‌است.

بنابراین به صورت خلاصه هر زمانیکه نیاز به نمایش اطلاعات فقط خواندنی (one-way binding) داریم، ابتدا خاصیتی را در کلاس کامپوننت تعریف کرده و سپس مقدار این خاصیت را توسط interpolation، در قالب کامپوننت درج می‌کنیم. حین استفاده از interpolation نیازی به ذکر "" نیست.
در مورد مباحث تکمیلی binding در قسمت‌های بعدی بیشتر بحث خواهیم کرد.


افزودن منطقی سفارشی به قالب یک کامپوننت

دایرکتیوها به صورت المان‌ها و یا ویژگی‌های سفارشی HTML، قابلیت توسعه‌ی امکانات پیش فرض آن‌را دارند. در اینجا می‌توان دایرکتیوهای سفارشی خود را تولید کرد (مانند pm-products فوق) و یا از دایرکتیوهای توکار AngularJS 2.0 استفاده کرد. برای مثال ngIf* و ngFor* جزو structural directives توکار AngularJS 2.0 هستند. ستاره‌ای که پیش از نام این دایرکتیوها قرار گرفته‌است، آن‌‌ها را در گروه structural directives قرار می‌دهد.
کار دایرکتیوهای ساختاری، تغییر ساختار یا همان view کامپوننت‌ها است؛ با افزودن، حذف و یا تغییر المان‌های HTML تعریف شده‌ی در صفحه.

بررسی ngIf*

فایل قالب product-list.component.html را گشوده و تعریف جدول آن‌را به نحو ذیل تغییر دهید:
 <table class='table' *ngIf='products && products.length'>
کار ngIf* نمایش یا عدم نمایش قسمتی از DOM یا document object model بر اساس برآورده شدن منطقی است که توسط آن بررسی می‌شود. اگر حاصل عبارتی که به ngIf* انتساب داده می‌شود به false تعبیر شود، آن المان و فرزندان آن از DOM حذف می‌شوند و اگر این عبارت به true تعبیر شود، آن المان و فرزندانش مجددا به DOM اضافه خواهند شد.
برای نمونه عبارت انتساب داده شده‌ی به ngIf* در مثال فوق به این معنا است که اگر خاصیت و آرایه‌ی products در کلاس کامپوننت این قالب تعریف شده بود و همچنین دارای اعضایی نیز بود، آنگاه این جدول را نمایش بده.
برای آزمایش آن، فایل product-list.component.ts را گشوده و خاصیت عمومی آرایه‌ی products را به نحو ذیل به آن اضافه کنید:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-products',
    templateUrl: 'app/products/product-list.component.html'
})
export class ProductListComponent {
    pageTitle: string = 'Product List';
    products: any[] = [
        {
            "productId": 2,
            "productName": "Garden Cart",
            "productCode": "GDN-0023",
            "releaseDate": "March 18, 2016",
            "description": "15 gallon capacity rolling garden cart",
            "price": 32.99,
            "starRating": 4.2,
            "imageUrl": "app/assets/images/garden_cart.png"
        },
        {
            "productId": 5,
            "productName": "Hammer",
            "productCode": "TBX-0048",
            "releaseDate": "May 21, 2016",
            "description": "Curved claw steel hammer",
            "price": 8.9,
            "starRating": 4.8,
            "imageUrl": "app/assets/images/rejon_Hammer.png"
        }
    ];
}
فعلا چون اینترفیسی را برای شیء محصول تعریف نکرده‌ایم، نوع این آرایه را any یا همان حالت پیش فرض جاوا اسکریپت تعریف می‌کنیم.
همچنین فعلا در اینجا اطلاعات را بجای دریافت از سرور، توسط آرایه‌ی مشخصی از اشیاء تعریف کرده‌ایم. این موارد را در قسمت‌های بعدی بهبود خواهیم بخشید.

اکنون که خاصیت عمومی products تعریف شده‌است، امکان استفاده‌ی از ngIf* ایی که پیشتر تعریف کردیم، میسر شده‌است. در این حالت اگر برنامه را اجرا کنید، قسمت table header تصویر قبلی نمایش سایت، هنوز نمایان است. یعنی ngIf* تعریف شده کار می‌کند؛ چون خاصیت products تعریف شده‌است و همچنین دارای اعضایی است.
برای آزمایش بیشتر، خاصیت products را کامنت کنید و یکبار نیز فایل ts آن‌را ذخیره کنید تا فایل js متناظر با آن کامپایل شود. سپس مجددا برنامه را اجرا کنید. در این حالت دیگر نباید هدر جدول نمایان باشد؛ چون products تعریف نشده‌است.


بررسی ngFor*

تا اینجا بر اساس داشتن لیستی از محصولات یا عدم آن، جدول متناظری را نمایش داده و یا مخفی کردیم. اما این جدول هنوز فاقد ردیف‌های نمایش اعضای آرایه‌ی products است.
برای این منظور مجددا فایل قالب product-list.component.html را گشوده و سپس بدنه‌ی جدول را به نحو ذیل تکمیل کنید:
<tbody>
    <tr *ngFor='#product of products'>
        <td></td>
        <td>{{ product.productName }}</td>
        <td>{{ product.productCode }}</td>
        <td>{{ product.releaseDate }}</td>
        <td>{{ product.price }}</td>
        <td>{{ product.starRating }}</td>
    </tr>
</tbody>
یکی دیگر از دایرکتیوهای ساختاری، ngFor* نام دارد. کار آن تکرار قسمتی از DOM، به ازای تک تک عناصر لیست انتساب داده شده‌ی به آن است.
بنابراین ابتدا قسمتی از عناصر HTML را طوری کنار هم قرار می‌دهیم که جمع آن‌ها یک تک آیتم را تشکیل دهند. سپس با استفاده از ngFor* به AngularJS 2.0 اعلام می‌کنیم که این قطعه را به ازای عناصر لیست دریافتی، تکرار و رندر کند.
برای نمونه در مثال فوق می‌خواهیم ردیف‌های جدول تکرار شوند. بنابراین هر ردیف را به عنوان یک قطعه‌ی تکرار شونده‌ی توسط ngFor* مشخص می‌کنیم. به این ترتیب این ردیف و عناصر فرزند آن، به ازای تک تک محصولات موجود در آرایه‌ی products، تکرار خواهند شد.
علامت # در اینجا (product#) یک متغیر محلی را تعریف می‌کند که تنها در قالب جاری قابل استفاده خواهد بود و همچنین فقط در فرزندان tr تعریف شده قابل دسترسی هستند.
به علاوه در اینجا بجای in از of استفاده شده‌است. این of از ES 6 گرفته شده‌است. زمانیکه از حلقه‌ی جدید for...of استفاده می‌شود، متغیر محلی product حاوی یک عنصر از لیست product خواهد بود؛ اما اگر از حلقه‌ی قدیمی for...in استفاده می‌شد، تنها ایندکس عددی این عناصر در دسترس قرار می‌گرفتند. به همین جهت است که در این حلقه، اکنون product.productName به نام محصول آن عنصر آرایه‌ی دریافتی اشاره می‌کند و قابل استفاده است.

تا اینجا اگر برنامه را اجرا کنید، چنین خروجی را مشاهده خواهید کرد:


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part3.zip


خلاصه‌ی بحث

از inline templateها جهت معرفی قالب‌های کوتاه استفاده می‌شود. در اینجا از "" برای معرفی قالب یک سطری و یا از back tickهای ES 6، برای تعریف قالب‌های چندسطری استفاده خواهد شد. برای قالب‌های مفصل‌تر، بهتر است Linked templateها استفاده شود؛ با پشتیبانی کامل ادیتورهای موجود از لحاظ تکمیل و بررسی کدها.
برای استفاده از یک کامپوننت در کامپوننتی دیگر، نام selector آن‌را به صورت یک المان جدید HTML در قالب دیگری ذکر کرده و سپس با استفاده از خاصیت directives، نام کلاس متناظر با آن‌را نیز ذکر می‌کنیم. همچنین کار import ماژول آن نیز باید در ابتدای فایل صورت گیرد.
جهت غنی سازی قالب‌ها و کامپوننت‌ها و نمایش اطلاعات فقط خواندنی می‌توان از binding یک طرفه‌ی ویژه‌ای به نام interpolation استفاده کرد. کار آن اتصال یک خاصیت عمومی کلاس کامپوننت، به قالب آن است. interpolation توسط {{}} تعریف می‌شود و می‌تواند شامل محاسبات نیز باشد.
همچنین در ادامه‌ی بحث، نحوه‌ی کار با دو دایرکتیو توکار ساختاری AngularJS 2.0 را نیز بررسی کردیم. این دایرکتیوهای ساختاری نیاز است با ستاره شروع شوند و عبارت انتساب داده شده‌ی به آن‌ها باید داخل "" قرار گیرد (برخلاف interpolation که نیازی به اینکار ندارد). از ngIf* برای حذف یا افزودن یک المان و فرزندان آن از/به DOM استفاده می‌شود. اگر عبارت منتسب به آن به true ارزیابی شود، این المان از صفحه حذف خواهد شد. از ngFor* برای تکرار المانی مشخص به همراه فرزندان آن به تعداد اعضای لیستی که برای آن تعیین می‌گردد، استفاده می‌شود. متغیر محلی این پیمایشگر با # مشخص شده و حلقه‌ی آن با of بجای in تعریف می‌شود.
مطالب
آموزش فریم ورک Vuetify قسمت دوم - UI Components بخش دوم
در بخش قبل با تعدادی از UI Component های vutify آشنا شدیم. در ادامه به بررسی و یادگیری تعدادی دیگر از این UI Component‌ها می‌پردازیم.

این کامپوننت یک کامپوننت همه کاره است. card‌ها میتوانند حاوی محتوا و اقداماتی در مورد یک موضوع واحد باشند. card ها ممکن است حاوی یک عکس، متن و یک لینک در مورد یک موضوع باشند. 
هر card دارای این سه جزء یا کامپوننت اساسی است: v-card-title , v-card-text , v-card-action که به ترتیب حاوی عنوان card ، متن card و عملیاتی که انجام میدهد می‌باشد.
عناصر تشکیل دهنده یک card می‌توانند به صورت زیر باشند:

قطعه کد زیر یک نمونه ساده از ایجاد یک card را به ما نشان می‌دهد.
<div id="app">
  <v-app id="inspire">
    <v-layout>
      <v-flex xs12 sm6 offset-sm3>
        <v-card>
          <v-img height="200px" src="https://cdn.vuetifyjs.com/images/cards/docks.jpg">
            <v-container fill-height fluid>
              <v-layout fill-height>
                <v-flex xs12 align-end flexbox>
                  <span>Top 10 Australian beaches</span>
                </v-flex>
              </v-layout>
            </v-container>
          </v-img>
          <v-card-title>
            <div>
              <span>Number 10</span><br>
              <span>Whitehaven Beach</span><br>
              <span>Whitsunday Island, Whitsunday Islands</span>
            </div>
          </v-card-title>
          <v-card-actions>
            <v-btn flat color="orange">Share</v-btn>
            <v-btn flat color="orange">Explore</v-btn>
          </v-card-actions>
        </v-card>
      </v-flex>
    </v-layout>
  </v-app>
</div>

از این کامپوننت جهت ساخت اسلایدر می‌توان استفاده کرد . اسلایدرها معمولا می‌توانند حاوی عکس و یا متن و یا ترکیبی از هر دو باشند. این کامپوننت دقیقا مشابه carousel در bootstrap عمل می‌کند .
<div id="app">
  <v-app id="inspire">
    <v-carousel>
      <v-carousel-item  v-for="(color, i) in colors" :key="color">
        <v-sheet :color="color" height="100%" tile>
          <v-layout align-center fill-height justify-center>
            <div>Slide {{ i + 1 }}</div>
          </v-layout>
        </v-sheet>
      </v-carousel-item>
    </v-carousel>
  </v-app>
</div>


از این کامپوننت برای انتقال اطلاعات به صورت قطعه‌های کوچک استفاده می‌شود. chip‌ها دارای چهار نوع اولیه می‌باشند. منظم، با آیکون، با پرتره و قابل تعویض.
در پایین با یک مثال، این چهار نوع اولیه نمایش داده شده‌اند.
<div id="app">
  <v-app id="inspire">
    <v-container fluid>
      <v-layout row wrap>
        <v-flex md6 sm12>
          <div>
            <v-chip close>Example Chip</v-chip>
          </div>
          <div>
            <v-chip>Example Chip</v-chip>
          </div>
        </v-flex>
        <v-flex md6 sm12 xs12>
          <div>
            <v-chip close>
              <v-avatar>
                <img src="https://randomuser.me/api/portraits/men/35.jpg" alt="trevor">
              </v-avatar>
              Trevor Hansen
            </v-chip>
          </div>
          <div>
            <v-chip>
              <v-avatar>A</v-avatar>
              ANZ Bank
            </v-chip>
          </div>
        </v-flex>
      </v-layout>
    </v-container>
  </v-app>
</div>

این کامپوننت برای نشان دادن اطلاعات مورد استفاده قرار می‌گیرد و به ما این اجازه را میدهد که بتوانیم چگونگی نمایش اطلاعات را سازماندهی کنیم. این امکانات عبارتند از مرتب سازی، جستجو، صفحه بندی و انتخاب اطلاعات. جهت نمایش این اطلاعات می‌توان از card ها و یا جداول استفاده نمود.
در مثال پایین جهت نمایش اطلاعات از card ها استفاده شده‌است:
<div id="app">
  <v-app id="inspire">
    <v-container fluid grid-list-md>
      <v-data-iterator
        :items="items"
        :rows-per-page-items="rowsPerPageItems"
        :pagination.sync="pagination" content-tag="v-layout" row wrap>
        <template v-slot:item="props">
           <v-card>
              <v-card-title><h4>{{ props.item.name }}</h4></v-card-title>
              <v-divider></v-divider>
              <v-list dense>
                <v-list-tile>
                  <v-list-tile-content>Calories:</v-list-tile-content>
                  <v-list-tile-content>{{ props.item.calories }}</v-list-tile-content>
                </v-list-tile>
                <v-list-tile>
                  <v-list-tile-content>Fat:</v-list-tile-content>
                  <v-list-tile-content>{{ props.item.fat }}</v-list-tile-content>
                </v-list-tile>
                <v-list-tile>
                  <v-list-tile-content>Carbs:</v-list-tile-content>
                  <v-list-tile-content>{{ props.item.carbs }}</v-list-tile-content>
                </v-list-tile>
                <v-list-tile>
                  <v-list-tile-content>Protein:</v-list-tile-content>
                  <v-list-tile-content>{{ props.item.protein }}</v-list-tile-content>
                </v-list-tile>
                <v-list-tile>
                  <v-list-tile-content>Sodium:</v-list-tile-content>
                  <v-list-tile-content>{{ props.item.sodium }}</v-list-tile-content>
                </v-list-tile>
                <v-list-tile>
                  <v-list-tile-content>Calcium:</v-list-tile-content>
                  <v-list-tile-content>{{ props.item.calcium }}</v-list-tile-content>
                </v-list-tile>
                <v-list-tile>
                  <v-list-tile-content>Iron:</v-list-tile-content>
                  <v-list-tile-content>{{ props.item.iron }}</v-list-tile-content>
                </v-list-tile>
              </v-list>
            </v-card>
          </v-flex>
        </template>
      </v-data-iterator>
    </v-container>
  </v-app>
</div>
new Vue({
  el: '#app',
  data () {
    return {
      headers: [
        {
          text: 'Dessert (100g serving)',
          align: 'left',
          sortable: false,
          value: 'name'
        },
        { text: 'Calories', value: 'calories' },
        { text: 'Fat (g)', value: 'fat' },
        { text: 'Carbs (g)', value: 'carbs' },
        { text: 'Protein (g)', value: 'protein' },
        { text: 'Iron (%)', value: 'iron' }
      ],
      desserts: [
        {
          name: 'Ice cream sandwich',
          calories: 237,
          fat: 9.0,
          carbs: 37,
          protein: 4.3,
          iron: '1%'
        }
        
      ]
    }
  }
})

این کامپوننت جهت نمایش اطلاعات، به صورت یک جدول مورد استفاده قرار می‌گیرد. امکاناتی که برای این جداول می‌توان در نظر گرفت عبارتند از مرتب سازی عناصر جدول، جستجوی عناصر جدول، صفحه بندی جدول در صورتیکه اطلاعات موجود، بیش از یک صفحه باشند؛ ویرایش خطی، راهنمایی‌های سربرگ و انتخاب ردیفهای جدول. جداول استاندارد حاوی اطلاعات، بدون هیچ گونه قابلیت اضافی هستند.
برای کار با این کامپوننت میتوان از تنظیمات بسیار زیادی استفاده کرد که سعی شده در مثال پایین تعدادی از این تنظیمات استفاده شود.
<div id="app">
  <v-app id="inspire">
    <v-data-table :headers="headers" :items="desserts">
      <template v-slot:items="props">
        <td>{{ props.item.name }}</td>
        <td>{{ props.item.calories }}</td>
        <td>{{ props.item.fat }}</td>
        <td>{{ props.item.carbs }}</td>
        <td>{{ props.item.protein }}</td>
        <td>{{ props.item.iron }}</td>
      </template>
    </v-data-table>
  </v-app>
</div>
new Vue({
  el: '#app',
  data () {
    return {
      headers: [
        {
          text: 'Dessert (100g serving)',
          align: 'left',
          sortable: false,
          value: 'name'
        },
        { text: 'Calories', value: 'calories' },
        { text: 'Fat (g)', value: 'fat' },
        { text: 'Carbs (g)', value: 'carbs' },
        { text: 'Protein (g)', value: 'protein' },
        { text: 'Iron (%)', value: 'iron' }
      ],
      desserts: [
        {
          name: 'Frozen Yogurt',
          calories: 159,
          fat: 6.0,
          carbs: 24,
          protein: 4.0,
          iron: '1%'
        },
        {
          name: 'Ice cream sandwich',
          calories: 237,
          fat: 9.0,
          carbs: 37,
          protein: 4.3,
          iron: '1%'
        },
        {
          name: 'Eclair',
          calories: 262,
          fat: 16.0,
          carbs: 23,
          protein: 6.0,
          iron: '7%'
        },
        {
          name: 'Cupcake',
          calories: 305,
          fat: 3.7,
          carbs: 67,
          protein: 4.3,
          iron: '8%'
        },
        {
          name: 'Gingerbread',
          calories: 356,
          fat: 16.0,
          carbs: 49,
          protein: 3.9,
          iron: '16%'
        }
      ]
    }
  }
})  

این کامپوننت به کاربران در مورد یک کار یا موضوع خاص اطلاع می‌دهد و ممکن است حاوی اطلاعات بحرانی، تصمیم گیرنده، یا شامل چندین وظیفه باشد. 
این کامپوننت حاوی دو بخش می‌باشد، یکی برای فعال کردن dialog و دیگری جهت نمایش محتوای آن (به طور پیش فرض). معمولا از این کامپوننت جهت نمایش خطاهایی که روند اجرای برنامه را متوقف میکنند و اطلاعاتی که نیاز به یک کار خاص، تصمیم گیری یا تایید کاربر دارند مانند سیاست‌های حفظ حریم خصوصی استفاده می‌شود. 
<div id="app">
  <v-app id="inspire">
    <div>
      <v-dialog v-model="dialog" width="500">
        <template v-slot:activator="{ on }">
          <v-btn color="red lighten-2" dark v-on="on">
            Click Me
          </v-btn>
        </template>
  
        <v-card>
          <v-card-title primary-title>
            Privacy Policy
          </v-card-title>
  
          <v-card-text>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
          </v-card-text>
  
          <v-divider></v-divider>
  
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="primary" flat @click="dialog = false">
              I accept
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>
    </div>
  </v-app>
</div>
در این مثال کاربر ابتدا فقط یک صفحه حاوی یک دکمه را مشاهده می‌نماید که پس از کلیک بر روی دکمه، یک dialog box برای او ظاهر می‌شود. همانطور که گفته شد این dialog box می‌تواند حاوی اطلاعات مختلفی باشد.


این کامپوننت برای کاهش فضای عمودی برای زمانیکه مقدار زیادی اطلاعات وجود دارد می‌تواند مفید باشد. 
لازم به ذکر است که به طور پیش فرض در یک زمان تنها یک پنل عمودی می‌تواند باز باشد.
<div id="app">
  <v-app id="inspire">
    <v-expansion-panel>
      <v-expansion-panel-content v-for="(item,i) in 5" :key="i">
        <template v-slot:header>
          <div>Item</div>
        </template>
        <v-card>
          <v-card-text>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</v-card-text>
        </v-card>
      </v-expansion-panel-content>
    </v-expansion-panel>
  </v-app>
</div>