مطالب
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
نظرات مطالب
پیاده سازی Remote Validation در Blazor
یک نکته‌ی تکمیلی: امکان اجرای ساده‌تر اعمال async پس از رخ‌داد onchange در Blazor 7x

پیشنیاز: برای اجرای نکات زیر، نیاز به حداقل NET SDK 7.0.101. است و اگر از ویژوال استودیو استفاده می‌کنید، باید شماره نگارش آن حداقل 17.4.3 باشد؛ در غیراینصورت با خطای «'cannot convert from 'method group' to 'Action» مواجه خواهید شد.


همانطور که در مطلب فوق هم مشاهده کردید، در جهت انجام اعتبارسنجی از راه دور async پس از ورود اطلاعات، تنها رخ‌دادی که در اینجا در اختیار ما قرار می‌گیرد، رخ‌داد submit (در حالت موفقیت اعتبارسنجی سمت کلاینت و یا تنها submit معمولی) است. بنابراین برای دسترسی به رخ‌دادهای بیشتر EditForm، نیاز است با EditContext آن کار کنیم تا بتوانیم برای مثال به کمک رویداد OnFieldChanged آن، این عملیات async را انجام دهیم. در دات نت 7.0.1، این وضعیت با معرفی modifier جدیدی به نام bind:after@ تغییر کرده‌است که در ادامه توضیحات آن‌را ملاحظه خواهید کرد.


تعاریف زیر را جهت پیاده سازی یک انقیاد دوطرفه (two-way data-binding) درنظر بگیرید:
<input @bind="username" />

<InputText @bind-Value="Model.Name" />
که در اولی با درج bind@ بر روی یک المان استاندارد HTML و در دومی با ذکر bind-Value@ میسر شده‌است. در این حالت هر تغییری در مقدار کنترل قرار گرفته‌ی بر روی صفحه، به خاصیت متصل به آن منعکس می‌شود (با پیاده سازی خودکار یک رویدادگردان onchange توسط Blazor در پشت صحنه) و برعکس.
مشکل! اگر در اینجا نیاز باشد تا در حین ورود اطلاعات، کدی نیز اجرا شود چه باید کرد؟
متاسفانه در این حالت نمی‌توانیم رویدادگردان onchange را به صورت دستی، به تعاریف فوق اضافه کنیم و اگر چنین کاری را انجام دهیم، با خطای زیر مواجه خواهیم شد:
RZ10008 The attribute 'onchange' is used two or more times for this element.
Attributes must be unique (case-insensitive). 
The attribute 'onchange' is used by the '@bind' directive attribute.
عنوان می‌کند که چون ما خودمان onchange را راسا پیاده سازی کرده‌ایم، شما دیگر نمی‌توانید اینکار را مجددا انجام دهید!

راه حل‌های ممکن انجام اعمال async پس از بروز تغییرات تا پیش از دات نت 7

الف) username متصل را تبدیل به یک خاصیت get و set دار کرده و اکنون در قسمت set آن می‌توان عملیات synchronous ای را انجام داد که متاسفانه در این حالت، امکان انجام اعمال async میسر نیست.
ب) چون می‌خواهیم عملیات async ای را پس از تغییرات انجام دهیم، باید از انقیاد دوطرفه صرفنظر کنیم و مدیریت رویداد onchange را خودمان به‌دست بگیریم؛ برای نمونه در مثال زیر می‌توان با پیاده سازی async متد CheckUsername به هدف خود رسید؛ اما همانطور که مشاهده می‌کنید، این عملیات اکنون one-way binding است:
<input value="@username" @onchange="CheckUsername" />
ج) اگر از EditForm و کنترل‌های آن استفاده می‌کنیم، می‌توان همانند مثال مطلب جاری از رویداد OnFieldChanged استفاده کرد یا راه دیگر آن شکستن bind-Value@ به اجزای تشکیل دهنده‌ی آن است که سه جزء Value ،ValueExpression و ValueChanged را تشکیل می‌دهد و اینبار می‌توان رویداد ValueChanged آن‌را دستی پیاده سازی کرد:
<InputText
  Value="@Model.Name"
  ValueExpression="()=>Model.Name"
  ValueChanged="(string s)=>CheckUsername(s)" />  
<ValidationMessage For="() => Model.Name" />

راه حل جدید انجام اعمال async پس از بروز تغییرات در دات نت 7

Blazor در دات نت 7، به همراه یک bind:after modifier@ است که امکان اجرای متدی را (چه همزمان یا غیرهمزمان) پس از بروز تغییرات، میسر می‌کند و مزیت آن عدم نیاز به بازنویسی متد onchange و از دست دادن انقیاد دوطرفه است:
<input @bind="username" @bind:after="CheckUsername" />
همانطور که مشاهده می‌کنید هنوز در این حالت bind@ وجود دارد (یعنی two-way data-binding هنوز هم برقرار است) و توسط bind:after@، متدی را که قرار است پس از تغییرات اجرا شود، مشخص کرده‌ایم.

این modifier را حتی می‌توان به کنترل‌های EditForm نیز اعمال کرد؛ بدون اینکه نیازی به استفاده از راه‌حل‌های پیشین (حالت ج عنوان شده) باشد:
<InputText
  @bind-Value="Model.Name"
  @bind-Value:after="CheckUsername" />   
<ValidationMessage For="() => Model.Name" />
در اینجا نیز هنوز از مزایای two-way data-binding برخورداریم و همچنین می‌توانیم پس از تغییری، یک متد sync و یا async را فراخوانی کنیم. برای نمونه پیاده سازی اعتبارسنجی از راه دور مطلب جاری، اینبار به صورت زیر ساده می‌شود:
async Task CheckUsername()
{
    if (!string.IsNullOrWhiteSpace(Model.Name))
    {
        _messageStore?.Clear(EditContext.Field(nameof(UserDto.Name)));


        var response = await HttpClient.PostAsJsonAsync(
                    UserValidationUrl,
                    new UserDto { Name = Model.Name });
        var responseContent = await response.Content.ReadAsStringAsync();
        if (string.Equals(responseContent, "false", StringComparison.OrdinalIgnoreCase))
        {
            _messageStore?.Add(EditContext.Field(nameof(UserDto.Name)), 
                      $"`{Model.Name}` is in use. Please choose another name.");        
        }

        EditContext.NotifyValidationStateChanged();
    }
}
مطالب
EF Code First #6

ادامه بررسی Fluent API جهت تعریف نگاشت کلاس‌ها به بانک اطلاعاتی

در قسمت‌های قبل با استفاده از متادیتا و data annotations جهت بررسی نحوه نگاشت اطلاعات کلاس‌ها به جداول بانک اطلاعاتی آشنا شدیم. اما این موارد تنها قسمتی از توانایی‌های Fluent API مهیا در EF Code first را ارائه می‌دهند. یکی از دلایل آن هم به محدود بودن توانایی‌های ذاتی Attributes بر می‌گردد. برای مثال حین کار با Attributes امکان استفاده از متغیرها یا lambda expressions و امثال آن وجود ندارد. به علاوه شاید عده‌ای علاقمند نباشند تا کلاس‌های خود را با data annotations شلوغ کنند.

در قسمت دوم این سری، مروری مقدماتی داشتیم بر Fluent API. در آنجا ذکر شد که امکان تعریف نگاشت‌ها به کمک توانایی‌های Fluent API به دو روش زیر میسر است:
الف) می‌توان از متد protected override void OnModelCreating در کلاس مشتق شده از DbContext کار را شروع کرد.
ب) و یا اگر بخواهیم کلاس Context برنامه را شلوغ نکنیم بهتر است به ازای هر کلاس مدل برنامه، یک کلاس mapping مشتق شده از EntityTypeConfiguration را تعریف نمائیم. سپس می‌توان این کلاس‌ها را در متد OnModelCreating یاد شده، توسط متد modelBuilder.Configurations.Add جهت استفاده و اعمال، معرفی کرد.

کلاس‌های مدلی را که در این قسمت بررسی خواهیم کرد، همان کلاس‌های User و Project قسمت سوم هستند و هدف این قسمت بیشتر تطابق Fluent API با اطلاعات ارائه شده در قسمت سوم است؛ برای مثال در اینجا چگونه باید از خاصیتی صرفنظر کرد، مسایل همزمانی را اعمال نمود و امثال آن.
بنابراین یک پروژه جدید کنسول را آغاز نمائید. سپس با کمک NuGet ارجاعات لازم را به اسمبلی‌های EF اضافه نمائید.
در پوشه Models این پروژه، سه کلاس تکمیل شده زیر، از قسمت سوم وجود دارند:
using System;
using System.Collections.Generic;

namespace EF_Sample03.Models
{
public class User
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Name { set; get; }
public string LastName { set; get; }

public string FullName
{
get { return Name + " " + LastName; }
}

public string Email { set; get; }
public string Description { set; get; }
public byte[] Photo { set; get; }
public IList<Project> Projects { set; get; }
public byte[] RowVersion { set; get; }
public InterestComponent Interests { set; get; }

public User()
{
Interests = new InterestComponent();
}
}
}

using System;

namespace EF_Sample03.Models
{
public class Project
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
public string Description { set; get; }
public virtual User User { set; get; }
public byte[] RowVesrion { set; get; }
}
}

namespace EF_Sample03.Models
{
public class InterestComponent
{
public string Interest1 { get; set; }
public string Interest2 { get; set; }
}
}


سپس یک پوشه جدید به نام Mappings را به پروژه اضافه نمائید. به ازای هر کلاس فوق، یک کلاس جدید را جهت تعاریف اطلاعات نگاشت‌ها به کمک Fluent API اضافه خواهیم کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;

namespace EF_Sample03.Mappings
{
public class InterestComponentConfig : ComplexTypeConfiguration<InterestComponent>
{
public InterestComponentConfig()
{
this.Property(x => x.Interest1).HasMaxLength(450);
this.Property(x => x.Interest2).HasMaxLength(450);
}
}
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;

namespace EF_Sample03.Mappings
{
public class ProjectConfig : EntityTypeConfiguration<Project>
{
public ProjectConfig()
{
this.Property(x => x.Description).IsMaxLength();
this.Property(x => x.RowVesrion).IsRowVersion();
}
}
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample03.Mappings
{
public class UserConfig : EntityTypeConfiguration<User>
{
public UserConfig()
{
this.HasKey(x => x.Id);
this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.ToTable("tblUser", schemaName: "guest");
this.Property(p => p.AddDate).HasColumnName("CreateDate").HasColumnType("date").IsRequired();
this.Property(x => x.Name).HasMaxLength(450);
this.Property(x => x.LastName).IsMaxLength().IsConcurrencyToken();
this.Property(x => x.Email).IsFixedLength().HasMaxLength(255); //nchar(128)
this.Property(x => x.Photo).IsOptional();
this.Property(x => x.RowVersion).IsRowVersion();
this.Ignore(x => x.FullName);
}
}
}

توضیحاتی در مورد کلاس‌های تنظیمات نگاشت‌های خواص به جداول و فیلدهای بانک اطلاعاتی

نظم بخشیدن به تعاریف نگاشت‌ها
همانطور که ملاحظه می‌کنید، جهت نظم بیشتر پروژه و شلوغ نشدن متد OnModelCreating کلاس Context برنامه، که در ادامه کدهای آن معرفی خواهد شد، به ازای هر کلاس مدل، یک کلاس تنظیمات نگاشت‌ها را اضافه کرده‌ایم.
کلاس‌های معمولی نگاشت‌ها ازکلاس EntityTypeConfiguration مشتق خواهند شد و جهت تعریف کلاس InterestComponent به عنوان Complex Type، اینبار از کلاس ComplexTypeConfiguration ارث بری شده است.

تعیین طول فیلدها
در کلاس InterestComponentConfig، به کمک متد HasMaxLength، همان کار ویژگی MaxLength را می‌توان شبیه سازی کرد که در نهایت، طول فیلد nvarchar تشکیل شده در بانک اطلاعاتی را مشخص می‌کند. اگر نیاز است این فیلد nvarchar از نوع max باشد، نیازی به تنظیم خاصی نداشته و حالت پیش فرض است یا اینکه می‌توان صریحا از متد IsMaxLength نیز برای معرفی nvarchar max استفاده کرد.

تعیین مسایل همزمانی
در قسمت سوم با ویژگی‌های ConcurrencyCheck و Timestamp آشنا شدیم. در اینجا اگر نوع خاصیت byte array بود و نیاز به تعریف آن به صورت timestamp وجود داشت، می‌توان از متد IsRowVersion استفاده کرد. معادل ویژگی ConcurrencyCheck در اینجا، متد IsConcurrencyToken است.

تعیین کلید اصلی جدول
اگر پیش فرض‌های EF Code first مانند وجود خاصیتی به نام Id یا ClassName+Id رعایت شود، نیازی به کار خاصی نخواهد بود. اما اگر این قراردادها رعایت نشوند،‌ می‌توان از متد HasKey (که نمونه‌ای از آن‌را در کلاس UserConfig فوق مشاهده می‌کنید)، استفاده کرد.

تعیین فیلدهای تولید شده توسط بانک اطلاعاتی
به کمک متد HasDatabaseGeneratedOption،‌ می‌توان مشخص کرد که آیا یک فیلد Identity است و یا یک فیلد محاسباتی ویژه و یا هیچکدام.

تعیین نام جدول و schema آن
اگر نیاز است از قراردادهای نامگذاری خاصی پیروی شود، ‌می‌توان از متد ToTable جهت تعریف نام جدول متناظر با کلاس جاری استفاده کرد. همچنین در اینجا امکان تعریف schema نیز وجود دارد.

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

معرفی فیلدها به صورت nchar بجای nvarchar
برای نمونه اگر قرار است هش کلمه عبور در بانک اطلاعاتی ذخیره شود، چون طول آن ثابت می‌باشد، توصیه شده‌است که بجای nvarchar از nchar برای تعریف آن استفاده شود. برای این منظور تنها کافی است از متد IsFixedLength استفاده شود. در این حالت طول پیش فرض 128 برای فیلد درنظر گرفته خواهد شد. بنابراین اگر نیاز است از طول دیگری استفاده شود، می‌توان همانند سابق از متد HasMaxLength کمک گرفت.
ضمنا این فیلدها همگی یونیکد هستند و با n شروع شده‌اند. اگر می‌خواهید از varchar یا char استفاده کنید، می‌توان از متد IsUnicode با پارامتر false استفاده کرد.

معرفی یک فیلد به صورت null پذیر در سمت بانک اطلاعاتی
استفاده از متد IsOptional، فیلد را در سمت بانک اطلاعاتی به صورت فیلدی با امکان پذیرش مقادیر null معرفی می‌کند.
البته در اینجا به صورت پیش فرض byte arrayها به همین نحو معرفی می‌شوند و تنظیم فوق صرفا جهت ارائه توضیحات بیشتر در نظر گرفته شد.

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


معرفی کلاس‌های تعاریف نگاشت‌ها به برنامه

استفاده از کلاس‌های Config فوق خودکار نیست و نیاز است توسط متد modelBuilder.Configurations.Add معرفی شوند:

using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample03.Mappings;
using EF_Sample03.Models;

namespace EF_Sample03.DataLayer
{
public class Sample03Context : DbContext
{
public DbSet<User> Users { set; get; }
public DbSet<Project> Projects { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new InterestComponentConfig());
modelBuilder.Configurations.Add(new ProjectConfig());
modelBuilder.Configurations.Add(new UserConfig());

//modelBuilder.ComplexType<InterestComponent>();
//modelBuilder.Ignore<InterestComponent>();

base.OnModelCreating(modelBuilder);
}
}

public class Configuration : DbMigrationsConfiguration<Sample03Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}

protected override void Seed(Sample03Context context)
{
base.Seed(context);
}
}
}

در اینجا کلاس Context برنامه مثال جاری را ملاحظه می‌کنید؛ به همراه کلاس Configuration مهاجرت خودکار که در قسمت‌های قبل بررسی شد.
در متد OnModelCreating نیز می‌توان یک کلاس را از نوع Complex معرفی کرد تا برای آن در بانک اطلاعاتی جدول جداگانه‌ای تعریف نشود. اما باید دقت داشت که اینکار را فقط یکبار می‌توان انجام داد؛ یا توسط کلاس InterestComponentConfig و یا توسط متد modelBuilder.ComplexType. اگر هر دو با هم فراخوانی شوند، EF یک استثناء را صادر خواهد کرد.

و در نهایت، قسمت آغازین برنامه اینبار به شکل زیر خواهد بود که از آغاز کننده MigrateDatabaseToLatestVersion (قسمت چهارم این سری) نیز استفاده کرده است:

using System;
using System.Data.Entity;
using EF_Sample03.DataLayer;

namespace EF_Sample03
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample03Context, Configuration>());

using (var db = new Sample03Context())
{
var project1 = db.Projects.Find(1);
if (project1 != null)
{
Console.WriteLine(project1.Title);
}
}
}
}
}

ضمنا رشته اتصالی مورد استفاده تعریف شده در فایل کانفیک برنامه نیز به صورت زیر تعریف شده است:

<connectionStrings>
<clear/>
<add
name="Sample03Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
/connectionStrings>


در قسمت‌های بعد مباحث پیشرفته‌تری از تنظیمات نگاشت‌ها را به کمک Fluent API، بررسی خواهیم کرد. برای مثال روابط ارث بری، many-to-many و ... چگونه تعریف می‌شوند.


مطالب
تبدیل زیرنویس‌های خاص پلورال‌سایت به فرمت SRT - قسمت دوم
قسمت اول این مطلب را در اینجا می‌توانید مطالعه کنید. از سه سال قبل تا به این تاریخ، فرمت زیرنویس‌های این سایت به صورت JSON تغییر پیدا کرده‌است و یک چنین ساختار جدیدی را دارد:
{
    "userIsAuthorizedForCourseTranscripts": false,
    "modules": [
        {
            "title": "Course Overview",
            "clips": [
                {
                    "title": "Course Overview",
                    "playerParameters": "author=scott-allen&name=aspdotnet-core-1-0-fundamentals-m0&mode=live&clip=0&course=aspdotnet-core-1-0-fundamentals",
                    "transcripts": [ ]
                }
            ]
        },
        {
            "title": "Building Your First ASP.NET Core Application",
            "clips": [
                {
                    "title": "Introduction",
                    "playerParameters": "author=scott-allen&name=aspdotnet-core-1-0-fundamentals-m1&mode=live&clip=0&course=aspdotnet-core-1-0-fundamentals",
                    "transcripts": [
                        {
                            "displayTime": 0.0,
                            "text": "Hi! This is Scott, and this course will help you build your first application with ASP.NET Core."
                        },
                        {
                            "displayTime": 7.0,
                            "text": "In this course, we'll be using Visual Studio and the new ASP.NET Framework to build a web application that"
                        }
                    ]
                }
            ]
        }
    ]
}
که اگر بخواهیم کلاس‌های معادل آن‌را تشکیل دهیم، به ساختار زیر خواهیم رسید:
public class PluralsightCourse
{
    public bool UserIsAuthorizedForCourseTranscripts { get; set; }
    public PluralsightCourseItems[] Modules { get; set; }
}
public class PluralsightCourseItems
{
    public string Title { get; set; }
    public PluralsightCourseClip[] Clips { get; set; }
}
public class PluralsightCourseClip
{
    public string Title { get; set; }
    public string PlayerParameters { get; set; }
    public PluralsightCourseClipTranscript[] Transcripts { get; set; }
}
public class PluralsightCourseClipTranscript
{
    public float DisplayTime { get; set; }
    public string Text { get; set; }
}
و بعد از تشکیل این ساختار که توسط منوی edit و گزینه‌ی paste special ویژوال استودیو تشکیل شده‌است:


بارگذاری آن توسط کتابخانه‌ی JSON.NET به صورت ذیل خواهد بود:
public static PluralsightCourse ProcessJsonFile(string jsonData)
{
    return JsonConvert.DeserializeObject<PluralsightCourse>(jsonData);
}
و پس از آن اگر حلقه‌ای را بر روی ماژول‌ها و سپس آیتم‌های هر ماژول تشکیل دهیم، می‌توان فرمت آن‌را به فرمت استاندارد srt که قابلیت پخش در اکثر برنامه‌های video player را دارد (مانند kmplayer)، تبدیل کرد.


کدهای کامل این برنامه را از اینجا می‌توانید دریافت کنید:
PluralsightJsonTranscripts.V1.0.7z
مطالب
نحوه استفاده از Text template ها در دات نت - قسمت سوم
 خوب در دو قسمت قبلی (^ و ^) با T4 و قواعد کد نویسی Text Template آشنا شدید. در این قسمت برخی مفاهیم را با یک مثال کاربردی‌تر بررسی می‌کنیم.
در ادامه قواعد زیر را در نظر بگیرید :
1 - برای استفاده از یک کتابخانه خارجی (dll) در داخل کد Text Template از بلوک <#@ assembly #> استفاده می‌شود .
مثلا برای استفاده از System.xml کد <#@ assembly name="System.xml" #> رو قرار بدید .
2- برای import کردن یک فضای نام نیز می‌توانید از بلوک #> <#@ استفاده نمایید .
به عنوان مثال : <#@ import namespace="System.Data" #>
3- encoding خروجی قابل تظیم می‌باشد. مثال: <#@ " output  extension = ".html" encoding = "utf-8 #>
4- برای استفاده از یک فایل tt درون فایل دیگر میتوان از include  استفاده کرد. مثال :   <#@ " include file=" testpath \basetest.tt #>
 
حالا به مثال زیر توجه کنید:

در بسیاری از پروژه‌ها، معادل تمامی جداول موجود در یک دیتابیس Class هایی به نام DTO  یا Data transfer object ساخته میشود که عموما" کلاسهای سبکی هستند که فقط شامل خصوصیت‌های معادل فیلدهای جداول می‌باشند و از آن‌ها جهت مدل کردن داده‌ها و ... استفاده می‌گردد. تولید این کلاسهای ساده می‌تواند بصورت اتوماتیک صورت گیرد و از این جهت در زمان تولید پروژه صرفه جویی شود. همچنین با تغییر ساختار دیتابیس می‌توان همواره کلاسها را بروزرسانی کرد.  نمونه ای از کد T4 برای تولید تمامی کلاسهای DTO به شکل زیر میباشد . فقط برای تست کد زیر دقت کنید که ConnectionString  را در داخل کد متناسب با دیتابیس خود تغییر دهید:  
<#@ template language="C#" debug="True" hostspecific="True" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Data" #>

<#@ assembly name="System.xml" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="System.Data" #>

using System;
namespace MyProject.Entities 
{    
        <#
            string connectionString = "Password=22125110;Persist Security Info=True;User ID=sa;Initial Catalog=DnnDB;Data Source=ABBASPOOR299"; 
            SqlConnection conn = new SqlConnection(connectionString); 
            conn.Open(); 
            System.Data.DataTable schema = conn.GetSchema("TABLES"); 
            string selectQuery = "select * from @tableName"; 
            SqlCommand command = new SqlCommand(selectQuery,conn); 
            SqlDataAdapter ad = new SqlDataAdapter(command); 
            System.Data.DataSet ds = new DataSet(); 
            
            foreach(System.Data.DataRow row in schema.Rows) 
            { 
            #> 
            public class <#= row["TABLE_NAME"].ToString().Trim('s') #>                
            
            {
                <#                 
                    command.CommandText = selectQuery.Replace("@tableName",row["TABLE_NAME"].ToString()); 
                    ad.FillSchema(ds, SchemaType.Mapped, row["TABLE_NAME"].ToString());

                    foreach (DataColumn dc in ds.Tables[row["TABLE_NAME"].ToString()].Columns)
                    {
                        #>
                        
                    private <#= dc.DataType.Name  #> _<#= dc.ColumnName.Replace(dc.ColumnName[0].ToString(), dc.ColumnName[0].ToString().ToLower())      #>;
                    public <#= dc.DataType.Name #> <#= dc.ColumnName #> 
                    {
                        get { return _<#= dc.ColumnName.Replace(dc.ColumnName[0].ToString(), dc.ColumnName[0].ToString().ToLower()) #>; } 
                        set { _<#= dc.ColumnName.Replace(dc.ColumnName[0].ToString(), dc.ColumnName[0].ToString().ToLower()) #> = value; }
                    }                    
                    
                <#    }                 #>
                
            }                
            
            <#    
            }  #>            
}
خوب؛ خودتون می‌توانید تست کنید ... این یکی از رایج‌ترین استفاده‌های T4 Text Template هست که برنامه نویسان از آن استفاده می‌کنند. 
مطالب دوره‌ها
نگاهی به انواع Aspects موجود در کتابخانه PostSharp
تعدادی Aspect توکار در کتابخانه PostSharp قرار دارند که نقطه آغازین کار با آن‌را تشکیل می‌دهند. نمونه‌ای از آن‌را در قسمت قبل به نام OnMethodBoundaryAspect بررسی کردیم. اغلب این‌ها کلاس‌هایی هستند Abstract که با تهیه‌ی کلاس‌هایی مشتق شده از آن‌ها و override نمودن متدهای کلاس پایه، می‌توان Aspect جدیدی را ایجاد نمود. تمام این نوع Aspects در حقیقت نوعی مزین کننده به شمار می‌روند. در ادامه قصد داریم نگاهی داشته باشیم به سایر Aspects مهیای در کتابخانه PostSharp.

1) OnExceptionAspect

از OnExceptionAspect برای مدیریت استثناءهای متدها استفاده می‌شود. کار این Aspect، اضافه کردن try/catch به کدهای یک متد است و سپس فراخوانی متد OnException در صورت بروز خطایی در این بین.
using System;
using System.Reflection;
using PostSharp.Aspects;

namespace AOP03
{
    public class ApplicationExceptionHandlerAspect : OnExceptionAspect
    {
        public override void OnException(MethodExecutionArgs args)
        {
            Console.WriteLine("Exception Type: {0}, StackTrace: {1}",
                               args.Exception.GetType().Name,
                               args.Exception.StackTrace);
        }

        public override Type GetExceptionType(MethodBase targetMethod)
        {
            return typeof(ApplicationException);
        }
    }
}
مثالی را در این زمینه در کدهای فوق ملاحظه می‌کنید. اگر تنها متد OnException تحریف شود، try/catch خودکار اضافه شده به کدها، هر نوع استثنایی را مدیریت خواهد کرد. اما اگر متد GetExceptionType نیز در این بین مقدار دهی گردد، بر اساس نوع استثنای تعریف شده، کار فیلتر استثناها انجام می‌پذیرد و از مابقی صرفنظر خواهد شد.
نحوه استفاده از این Aspect نیز همانند مثال قسمت قبل است و جزئیات آن تفاوتی نمی‌کند.


2) LocationInterceptionAspect

این Aspect برخلاف سایر Aspectهایی که تاکنون بررسی کردیم، تنها در سطح خواص و فیلدهای یک کلاس عمل می‌کند. کار Interception در اینجا به معنای تحت کنترل قرار دادن اعمال set (پیش از فراخوانی set) و get (پیش از بازگشت مقدار) این خواص عمومی و حتی خصوصی تعریف شده است. کلمه Location در این Aspect به معنای متادیتای زمینه کاری است؛ مانند Name و FullName خواصی که مشغول به کار با آن‌ها هستیم.
using System;
using PostSharp.Aspects;

namespace AOP03
{
    public class ObjectInitializationAspect : LocationInterceptionAspect
    {
        public override void OnGetValue(LocationInterceptionArgs args)
        {
            if (args.GetCurrentValue() == null)
            {
                Console.WriteLine("Property {0} is null.", args.LocationFullName);
            }
        }
    }
}
یک نمونه از کاربرد آن‌را در مثال فوق مشاهده می‌کنید. در اینجا با تحریف متد OnGetValue، پیش از بازگشت مقداری از یک خاصیت، بررسی می‌شود که آیا مقدار آن null است یا خیر.
برای استفاده از آن نیز کافی است تا ویژگی ObjectInitializationAspect به خاصیتی دلخواه اضافه شود.
در اینجا 4 متد args.GetCurrentValue برای دریافت مقدار جاری خاصیت، args.SetNewValue جهت تنظیم مقداری جدید، args.ProceedGetValue و args.ProceedSetValue سبب اجرای حالت‌های get و set می‌شوند (چیزی شبیه به عملکرد اینترفیس IInterceptor که در قسمت‌های قبلی بررسی کردیم).


3) EventInterceptionAspect

EventInterceptionAspect همانطور که از نام آن نیز پیدا است، در سطح رخدادهای یک کلاس عمل می‌کند. سه متدی که این کلاس پایه برای تحت نظر قرار دادن اعمال رویدادگردان‌های یک کلاس در اختیار ما قرار می‌دهند شامل OnAddHandler، OnRemoveHandler و OnInvokeHandler هستند.
using PostSharp.Aspects;
using System;

namespace AOP03
{
    public class LogEventAspect : EventInterceptionAspect
    {
        public override void OnAddHandler(EventInterceptionArgs args)
        {
            Console.WriteLine("Event {0} added", args.Event.Name);
            args.ProceedAddHandler();
        }

        public override void OnRemoveHandler(EventInterceptionArgs args)
        {
            Console.WriteLine("Event {0} removed", args.Event.Name);
            args.ProceedRemoveHandler();
        }

        public override void OnInvokeHandler(EventInterceptionArgs args)
        {
            Console.WriteLine("Event {0} invoked", args.Event.Name);
            args.ProceedInvokeHandler();
        }
    }
}
مثالی را از نحوه تعریف یک EventInterceptionAspect مشاهده می‌کنید. در تمام حالاتی که متدهای کلاس پایه تحریف شده‌اند نیاز است از متدهای Proceed متناظر نیز استفاده شود تا برای مثال اضافه شدن، حذف و یا اجرای یک رویداد رخ دهند.


مدیریت اعمال Aspects در زمان کامپایل

یکی از متدهایی که در کلیه Aspects توکار فوق قابل تحریف است، CompileTimeValidate نام دارد.
    public class LoggingAspect : OnMethodBoundaryAspect
    {
        public override bool CompileTimeValidate(System.Reflection.MethodBase method)
        {
            return !method.IsStatic;
        }
برای نمونه اگر آن‌را به OnMethodBoundaryAspect پیاده سازی شده در قسمت قبل، با تعاریف فوق اعمال کنیم، این Aspect سفارشی دیگر به متدهای استاتیک، اعمال نخواهد شد. به این ترتیب می‌توان بر روی نحوه کامپایل ثانویه کدهایی که قرار است به اسمبلی برنامه اضافه شوند، تاثیر گذار بود.


چند نکته تکمیلی در مورد توزیع برنامه‌های مبتنی بر PostSharp

الف) اگر نیاز است به اسمبلی‌های خود امضای دیجیتال اضافه کنید، در حالت استفاده از PostSharp به علت بازنویسی کدهای IL اسمبلی تولیدی، نیاز است حالت delay signing انتخاب شود. به این معنا که ابتدا اسمبلی به صورت متداول کامپایل می‌شود. سپس PostSharp کار خود را انجام داده و در نهایت با استفاده از ابزارهای اعمال امضای دیجیتال باید کار افزودن آن‌ها در مرحله آخر انجام شود.
ب) در حال حاضر تنها برنامه Dotfuscator است که با PostSharp برای obfuscation سازگاری دارد.
مطالب
نمایش HTML در برنامه‌های Angular
فرض کنید قصد داریم خاصیت htmlContent زیر را در قالب این کامپوننت نمایش دهیم:
export class ShowHtmlComponent {
  htmlContent = "Template <script>alert(\"Hello!\")</script> <b>Syntax</b>";
}
اگر از روش متداول binding استفاده شود:
<h3>Binding innerHTML</h3>
<p>Bound value:</p>
<p>{{htmlContent}}</p>
چنین خروجی حاصل خواهد شد:


همچنین اگر به کنسول developer tools مرورگر مراجعه کنیم، چنین اخطاری نیز درج شده است:
 WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).
به این معنا که Angular به صورت توکار تمام خروجی‌ها را به صورت encode شده نمایش می‌دهد و در مقابل حملات XSS مقاوم است. Sanitizing نیز در اینجا به معنای تغییر ورودی و تبدیل آن به مقداری است که جهت درج در DOM امن است.


روش نمایش HTML در برنامه‌های Angular

اما اگر خواستیم اطلاعات HTML ایی را به همان صورتی که هستند نمایش دهیم چطور؟ در این حالت باید از روش ویژه‌ی ذیل استفاده کرد:
<p>Result of binding to innerHTML:</p>
<p [innerHTML]="htmlContent"></p>
برای نمایش HTML نیاز است آن‌را به ویژگی innerHTML متصل کرد؛ با این خروجی:


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

روش دیگر کار با innerHTML، تعریف یک template reference variable در قالب کامپوننت است:
<p #dataContainer></p>
و سپس دسترسی به آن از طریق یک ViewChild و انتساب مقداری بهinnerHTML  آن به صورت ذیل:
export class ShowHtmlComponent implements OnInit {

  @ViewChild("dataContainer") dataContainer: ElementRef;

  ngOnInit() {
    this.dataContainer.nativeElement.innerHTML = "nativeElement <script>alert(\"Hello!\")</script> <b>Syntax</b>";
  }
}
با این خروجی:


که اینبار قسمت script آن به طور کامل حذف شده‌است.


حالات مختلفی که Angular برنامه را از حملات XSS محافظت می‌کند

در ذیل، لیست مواردی را مشاهده می‌کنید که به صورت پیش‌فرض توسط Angular در مقابل حملات XSS محافظت می‌شوند و اطلاعات انتساب داده شده‌ی به آن‌ها تمیزسازی خواهند شد:
HTML 
Attributes – 
<div [innerHTML]="UNTRUSTED"></div> 
OR <input value="UNTRUSTED">

Style— 
<div [style]="height:UNTRUSTED"></div>

URL — 
<a [href]="UNTRUSTED-URL"></a> 
OR <script [src]="UNTRUSTED-URL"></script> 
OR <iframe src="UNTRUSTED-URL" />

GET Parameter – 
<a href="/user?id=UNTRUSTED">link</a>

JavaScript Variable –
<script> var value='UNTRUSTED';</script>


تبدیل کردن یک HTML نا امن ورودی به یک HTML امن در Angular

بهتر است اطلاعات دریافتی از کاربران پیش از ارسال به سرور تمیز شوند. برای این منظور می‌توان از سرویس ویژه‌ای به نام DomSanitizer کمک گرفت. کار این سرویس، امن سازی اطلاعات نمایش داده شده‌ی در برنامه‌های Angular است.
export class ShowHtmlComponent implements OnInit {
  sanitizedHtml: string;

  constructor(private sanitizer: DomSanitizer) { }

  ngOnInit() {
    this.sanitizedHtml = this.sanitizer.sanitize(SecurityContext.HTML, "<b>Sanitize</b><script>attackerCode()</script>");
  }
}
در این حالت سرویس DomSanitizer به سازنده‌ی کلاس تزریق شده و سپس می‌توان از متدهای مختلف آن مانند sanitize استفاده کرد. خروجی آن صرفا حذف تگ اسکریپت و نگهداری کدهای درون آن است.


در این حالت می‌توان موارد ذیل را کنترل کرد. برای مثال اگر مقدار دریافتی CSS است، می‌توان از SecurityContext.STYLE استفاده کرد و سایر حالات آن مانند امن سازی HTML، اسکریپت و آدرس‌های اینترنتی به شرح ذیل هستند:
SecurityContext.NONE
SecurityContext.HTML
SecurityContext.STYLE
SecurityContext.SCRIPT
SecurityContext.URL
SecurityContext.RESOURCE_URL


غیرفعال کردن سیستم امنیتی Angular جهت نمایش کامل یک مقدار HTML ایی

اگر خواستیم اطلاعات HTML ایی را با فرض امن بودن آن، به همان نحوی که هست نمایش دهیم چطور؟
سرویس DomSanitizer شامل متدهای ذیل نیز می‌باشد:
export enum SecurityContext { NONE, HTML, STYLE, SCRIPT, URL, RESOURCE_URL }

export abstract class DomSanitizer implements Sanitizer {
  abstract sanitize(context: SecurityContext, value: SafeValue|string|null): string|null;
  abstract bypassSecurityTrustHtml(value: string): SafeHtml;
  abstract bypassSecurityTrustStyle(value: string): SafeStyle;
  abstract bypassSecurityTrustScript(value: string): SafeScript;
  abstract bypassSecurityTrustUrl(value: string): SafeUrl;
  abstract bypassSecurityTrustResourceUrl(value: string): SafeResourceUrl;
}
اولین متد آن sanitize است که در مورد آن توضیح داده شد. سایر متدها، کار غیرفعال سازی سیستم امنیتی توکار Angular را انجام می‌دهند.
برای کار با آن‌ها همانند مثال استفاده‌ی از متد sanitize می‌توان سرویس DomSanitizer را به سازنده‌ی یک کامپوننت تزریق کرد و یا می‌توان این عملیات تکراری فرمت اطلاعات ورودی را تبدیل به یک Pipe جدید کرد:
import { Pipe, PipeTransform } from "@angular/core";
import { DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl } from "@angular/platform-browser";

@Pipe({
  name: "safe"
})
export class SafePipe implements PipeTransform {
  constructor(protected sanitizer: DomSanitizer) { }

  public transform(value: any, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
    switch (type) {
      case "html":
        return this.sanitizer.bypassSecurityTrustHtml(value);
      case "style":
        return this.sanitizer.bypassSecurityTrustStyle(value);
      case "script":
        return this.sanitizer.bypassSecurityTrustScript(value);
      case "url":
        return this.sanitizer.bypassSecurityTrustUrl(value);
      case "resourceUrl":
        return this.sanitizer.bypassSecurityTrustResourceUrl(value);
      default:
        throw new Error(`Invalid safe type specified: ${type}`);
    }
  }
}
کار این Pipe غیرفعال کردن سیستم امنیتی Angular و نمایش html، style و غیره به همان صورتی که هستند، می‌باشد.
برای استفاده‌ی از آن، ابتدا این Pipe به قسمت declarations ماژول مدنظر اضافه خواهد شد:
@NgModule({
  imports: [
  // ...
  ],
  declarations: [ SafePipe]
})
و سپس در قالب کامپوننت به نحو ذیل می‌توان با آن کار کرد:
<p [innerHTML]="htmlContent | safe: 'html'"></p>
در این حالت متد bypassSecurityTrustHtml بر روی htmlContent، فراخوانی شده و نتیجه‌ی نهایی نمایش داده خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
پیاده سازی عملیات CRUD در Kendo UI Treeview یک پروژه‌ی ASP.NET MVC
در این مقاله می‌خواهیم عملیات CRUD را بر روی Telerik kendo treeview  در یک پروژه‌ی ASP.NET MVC پیاده سازی کنیم. شکل کلی این پروژه به صورت زیر می‌باشد:


که اینجا دکمه‌ها از سمت راست به چپ، عملیات افزودن، عدم انتخاب، ویرایش و حذف را انجام می‌دهند. کدهای HTML این پنل را در ادامه مشاهده می‌کنید:

<div id="CrudPanel" class="row treeview-panel" >
      <div class="col-lg-7 pull-right">
           <input type="text" id="txtLocationTitle" class="form-control" />
      </div>
      <div class="col-lg-5 pull-left" style="text-align: left;">
           <button data-toggle="tooltip" data-placement="left" title="افزودن" id="btnAddLocation" class="btn btn-sm btn-success">
                <i class="fa fa-plus"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="عدم انتخاب" id="btnUnSelect" class="btn btn-sm btn-info">
                <i class="fa fa-square-o"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="ویرایش" id="btnEditLocation" class="btn btn-sm btn-warning">
                <i class="fa fa-pencil"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="حذف" id="btnDeleteLocation" class="btn btn-sm btn-danger">
                <i class="fa fa-times"></i>
           </button>
      </div>
</div>


و قطعه کد ذیل مربوط به پنل ویرایش است که در ابتدای کار کلاس hide به آن انتساب داده شده و پنهان می‌شود:

<div id="EditPanel" class="row edit hide treeview-panel">
     <div class="col-lg-7 pull-right">
          <input type="text" id="txtLocationEditTitle" class="form-control" />
     </div>
     <div class="col-lg-5 pull-left" style="text-align: left">
          <input type="button" value="ویرایش" id="btnEditPanelLocation" data-code="" data-parentId="" class="btn btn-sm btn-success" />
          <input type="button" value="انصراف" id="btnCancle" class="btn btn-sm btn-info" />
     </div>
</div>


در آخر این تکه کد نیز مربوط به KendoUI TreeView است:

 <div class="col-lg-6 k-rtl treeview-style">
                    @(Html.Kendo()
                          .TreeView()
                          .Name("treeview")
                          .DataTextField("Title")
                          .DragAndDrop(false)
                          .DataSource(dataSource => dataSource
                          .Model(model => model.Id("Id"))
                          .Read(read => read.Action(MVC.Admin.Location.ActionNames.GetAllAssetGroupTree, MVC.Admin.Location.Name)))
                    )
                </div>


یک نکته

- کلاس k-rtl مربوط به خود treeview می‌باشد و با این کلاس، درخت ما راست به چپ می‌شود.


در ادامه css‌های مربوط به کلاس‌های treeview-style ،hide و treeview-panel بررسی خواهند شد:

.treeview-style {
    min-height: 86px;
    max-height: 300px;
    overflow: scroll;
    overflow-x: hidden;
    position: relative;
}
.treeview-panel {
    background-color: #eee;
    padding: 25px 0 25px 0;
}
.hide {
    display: none;
}


تا اینجای مقاله، کدهای Html و Css موجود را بررسی کردیم. حالا سراغ قسمت اصلی خواهیم رفت. یعنی عملیات CRUD.


لازم به ذکر است در ابتدای قسمت script  باید این چند خط کد نوشته شود:

 var treeview = null;
    $(window).load(function () {
        treeview = $("#treeview").data("kendoTreeView");
    });

در اینجا بعد از بارگذاری کامل صفحه، درخت مورد نظر ما ساخته خواهد شد و می‌توان به متغیر treeview در تمام قسمت script دسترسی داشت.


پیاده سازی عملیات افزودن: 

 $(document).on('click', '#btnAddLocation', function () {
        var title = $('#txtLocationTitle').val();
        var selectedNodeId = null;
        var selectedNode = treeview.select();
        if (selectedNode.length == 0) {
            selectedNode = null;
        }
        else {
            selectedNodeId = treeview.dataItem(selectedNode).id;// گرفتن آی دی گره انتخاب شده
        }
        $.ajax({
            url: '@Url.Action(MVC.Admin.Location.CreateByAjax())',
            type: 'POST',
            data: { Title: title, ParentId: selectedNodeId },
            success: function (data) {
                debugger;
                showMessage(data.message, data.notificationType);
                if (data.result)
                    treeview.dataSource.read();
            },
            error: function () {
                showMessage('لطفا مجددا تلاش نمایید', 'warning');
            }
        });

    });

توضیحات: مقدار گره جدید را خوانده و در متغیر title قرار می‌دهیم. گره انتخاب شده را توسط این خط

var selectedNode = treeview.select();

می گیریم و سپس در ادامه بررسی خواهیم کرد تا اگر گره‌ای انتخاب نشده باشد، به کاربر پیغامی را نشان دهد؛ در غیر این صورت توسط ajax، مقادیر مورد نظر، به اکشن ما در LocationController ارسال می‌شوند:

 [HttpPost]
        public virtual ActionResult CreateByAjax(AddLocationViewModel locationViewModel)
        {
            if (ModelState.IsNotValid())
                return JsonResult(false, "عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error);
            var result = _locationService.Add(locationViewModel);//سرویس مورد نظر برای اضافه کردن به دیتابیس
            switch (result)
            {
                case AddStatus.AddSuccessful:
                    _uow.SaveChanges();
                    return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success);
                case AddStatus.Faild:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
                case AddStatus.Exists:
                    return JsonResult(false, Messages.DataExists, NotificationType.Warning);
                default:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
            }
        }


   public virtual JsonResult JsonResult(bool result, string message, string notificationType)
        {
            return Json(new { result = result, message = message, notificationType = notificationType }, JsonRequestBehavior.AllowGet);
        }

اکشن JsonResult  که مقادیر نتیجه، پیغام و نوع اطلاع رسانی را می‌گیرد و یک آبجکت از نوع json را به تابع success ای‌جکس، ارسال می‌کند.


 public class AddLocationViewModel
    {
        [DisplayName("عنوان")]
        [Required(ErrorMessage ="لطفا عنوان گروه را وارد نمایید"),MinLength(2,ErrorMessage ="طول عنوان خیلی کوتاه می‌باشد ")]
        public string Title { get; set; }
        [DisplayName("گروه پدر")]
        public Guid? ParentId { get; set; }

    }

این کلاس viewModel ما می‌باشد.


  public enum AddStatus
    {
        AddSuccessful,
        Faild,
        Exists
    }

و این مورد هم کلاس AddStatus از نوع enum.


  public class Messages
    {
        #region  Fields

        public const string SaveSuccessfull = "اطلاعات با موفقیت ذخیره شد";
        public const string SaveFailed = "خطا در ثبت اطلاعات";
        public const string DeleteMessage = "کابر گرامی ، آیا از حذف کردن این رکورد مطمئن هستید ؟";
        public const string DeleteSuccessfull = "اطلاعات با موفقیت حذف شد";
        public const string DeleteFailed = "خطا در حذف اطلاعات ، لطفا مجددا تلاش نمایید";
        public const string DeleteHasInclude = "کاربر گرامی ، رکورد مورد نظر هم اکنون در بانک اطلاعاتی سیستم در حال استفاده توسط منابع دیگر می‌باشد";
        public const string NotFoundData = "اطلاعات یافت نشد";
        public const string NoAttachmentSelect = "تصویری انتخاب نشده است";
        public const string DataExists = "اطلاعات وارد شده در بانک اطلاعاتی موجود می‌باشد";
        public const string DeletedRowHasIncluded = "کاربر گرامی ، رکوردی که قصد حذف آن را دارید هم اکنون در بانک اطلاعاتی سیستم ، توسط سایر بخش‌ها در حال استفاده می‌باشد";
        
        #endregion
    }

و این موارد هم مقادیر ثابت فیلد‌های مورد استفاده‌ی ما در کلاس Message.


پیاده سازی عملیات حذف

به طور اختصار، عملیات حذف را توضیح می‌دهم تا به قسمت اصلی مقاله یعنی ویرایش بپردازیم:

$(document).on('click', '#btnDeleteLocation', function () {
        var selectedNode = treeview.select();
        var currentNode = treeview.dataItem(selectedNode);
        if (selectedNode.length == 0) {
            showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning');
        } else {
            var selectedNodeId = treeview.dataItem(selectedNode).id;
            if (currentNode.hasChildren) {
                var title = 'کاربر گرامی ، با حذف شدن این گره، تمام زیر شاخه‌های آن حذف می‌شود. آیا مطمئن هستید ؟ ';
                DeleteConfirm(selectedNodeId, '@Url.Action(MVC.Admin.Location.DeleteByAjax())', title);
            } else {
                $.ajax({
                    url: '@Url.Action(MVC.Admin.Location.DeleteByAjax())',
                    type: 'POST',
                    data: { id: selectedNodeId },
                    success: function (data) {
                        debugger;
                        showMessage(data.message, data.notificationType);
                        if (data.result)
                            treeview.remove(selectedNode);
                    },
                    error: function () {
                        showMessage('لطفا مجددا تلاش نمایید', 'warning');
                    }
                });
            }
        }
    });

این مورد نیز همانند عملیات افزودن عمل می‌کند. یعنی ابتدا چک می‌کند که آیا گره‌ای انتخاب شده است یا خیر؟ و اگر گره انتخابی ما دارای فرزند باشد، به کاربر پیغامی را نشان می‌دهد و می‌گوید «گره مورد نظر، دارای فرزند است. آیا مایل به حذف تمام فرزندان آن هستید؟» مانند تصویر زیر:



در نهایت چه گره انتخابی دارای فرزند باشد و چه نباشد، به یک مسیر مشترک ارسال می‌شوند:

  public virtual ActionResult DeleteByAjax(Guid id)
        {
            var result = _locationService.Delete(id);
            switch (result)
            {
                case DeleteStatus.Successfull:
                    _uow.SaveChanges();
                    return DeleteJsonResult(true, Messages.DeleteSuccessfull, NotificationType.Success);
                case DeleteStatus.NotFound:
                    return DeleteJsonResult(false, Messages.NotFoundData, NotificationType.Error);
                case DeleteStatus.Failed:
                    return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error);
                case DeleteStatus.ThisRowHasIncluded:
                    return DeleteJsonResult(false, Messages.DeletedRowHasIncluded, NotificationType.Warning);
                default:
                    return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error);
            }
        }


در سرویس مورد نظر ما یعنی Delete، اگه گره‌ای دارای فرزند باشد، تمام فرزندان آن را حذف می‌کند. حتی فرزندان فرزندان آن را:

  public DeleteStatus Delete(Guid id)
        {
            var model = GetAsModel(id);
            if (model == null) return DeleteStatus.NotFound;
            if (!CanDelete(model)) return DeleteStatus.ThisRowHasIncluded;
            _uow.MarkAsSoftDelete(model, _userManager.GetCurrentUserId());

            if (model.Children.Any())
                DeleteChildren(model);
            return DeleteStatus.Successfull;
        }


  private void DeleteChildren(Location model)
        {
            foreach (var item in model.Children)
            {
                _uow.MarkAsSoftDelete(item, _userManager.GetCurrentUserId());
                if (item.Children.Any())
                    DeleteChildren(item);
            }
        }


  public class Location:BaseEntity,ISoftDelete
    {
        public string Title { get; set; }
        public Location Parent { get; set; }
        public Guid? ParentId { get; set; }
        public bool IsDeleted { get; set; }

        public virtual ICollection<Location> Children { get; set; }
}

 و این هم مدل Location که سمت سرور از مدل استفاده می‌کنیم.


پیاده سازی عملیات ویرایش

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

    // Open Edit Panel
    $(document).on('click', '#btnEditLocation', function () {
        debugger;
        var selectedNode = treeview.select();
        var currentNode = treeview.dataItem(selectedNode);// با استفاده از این خط، گره انتخاب شده جاری را می‌گیریم.


        if (selectedNode.length == 0) {
//این شرط به ما می‌گوید اگر گره ای انتخاب نشده بود پیغامی به کاربر نمایش بده
            showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning');
        } else {
            var selectedNodeCode = treeview.dataItem(selectedNode).Code;
            var selectedNodeTitle = treeview.dataItem(selectedNode).Title;
            var selectedNodeParentId = treeview.dataItem(selectedNode).ParentId;
// آی دی یا کد، عنوان و آی دی پدر گره انتخاب شده را با استفاده از این سه خط در اختیار می‌گیریم
            $('#CrudPanel').toggleClass('hide'); //المنت کرادپنل که در حال حاضر کاربر آن را می‌بیند، با این خط کد، پنهان می‌شود
            $('#EditPanel').toggleClass('hide'); //المنت ادیت پنل که در حال حاضر از دید کاربر پنهان است، قابل نمایش می‌شود

            $("#txtLocationEditTitle").val(selectedNodeTitle);
//عنوان گره ای که می‌خواهیم آن را ویرایش کنیم در تکست باکس مورد نظر قرار می‌گیرد
            $("#txtLocationEditTitle").focusTextToEnd();
// با استفاده از این پلاگین، کرسر ماوس در انتهای مقدار دیفالت تکست باکس قرار می‌گیرد
            $("#btnEditPanelLocation").attr('data-code', selectedNodeCode);
            $("#btnEditPanelLocation").attr('data-parentId', selectedNodeParentId == null ? '' : selectedNodeParentId);
//مقادیر پرنت آی دی و کد را در دیتا اتریبیوت‌های موجود در المنت خودمان قرار می‌دهیم
            // Disable clicking in treeview
            $("#treeview").children().bind('click', function () { return false; });
        }
    });

  (function ($) {
        $.fn.focusTextToEnd = function () {
            this.focus();
            var $thisVal = this.val();
            this.val('').val($thisVal);
            return this;
        }
    }(jQuery));

کد زیر باعث می‌شود تا زمانیکه پنل ویرایش باز است، کاربر نتواند هیچ کلیکی را در عناصر داخل درخت ما، داشته باشد.

            $("#treeview").children().bind('click', function () { return false; });


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


همانطور که در تصویر بالا مشاهده می‌کنید، با انتخاب ساختمان مرکزی و زدن دکمه ویرایش، پنل CRUD ما پنهان و پنل ویرایش ظاهر می‌گردد. همچنین عنوان گره انتخابی به عنوان پیش فرض تکست باکس ما تنظیم می‌شود و کاربر نمی‌تواند گره دیگری را انتخاب کند؛ به شرط آنکه این پنل ویرایش بسته شود.

با تغییر عنوان تکست باکس و زدن دکمه‌ی ویرایش، رویداد زیر رخ می‌دهد:

  // Edit tree node
    $(document).on('click', '#btnEditPanelLocation', function () {
        debugger;
        var code = $("#btnEditPanelLocation").attr('data-code');
        var parentId = $("#btnEditPanelLocation").attr('data-parentId');
        var title = $("#txtLocationEditTitle").val().trim();
        $.ajax({
            url: '@Url.Action(MVC.Admin.Location.EditByAjax())',
            type: 'POST',
            data: { Code: code, Title: title, ParentId: parentId.length === 0 ? null : parentId },
            success: function (data) {
                debugger;
                showMessage(data.message, data.notificationType);
                if (data.result) {
                    treeview.dataSource.read();
                    CloseEditPanel();
                }
            },
            error: function () {
                showMessage('لطفا مجددا تلاش نمایید', 'warning');
            }
        });
    });


  [HttpPost]
        public virtual ActionResult EditByAjax(EditLocationViewModel editLocationViewModel)
        {

            if (ModelState.IsNotValid())
                return JsonResult(false,"عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error);
            var result = _locationService.Edit(editLocationViewModel);
            switch (result)
            {
                case EditStatus.Successful:
                    _uow.SaveChanges();
                    return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success);
                case EditStatus.NotFound:
                    return JsonResult(false, Messages.NotFoundData, NotificationType.Error);
                case EditStatus.Faild:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
                case EditStatus.Exists:
                    return JsonResult(false, Messages.DataExists, NotificationType.Warning);
                default:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
            }
        }


تابع CloseEditPanel  بعد از اتمام ویرایش هر گره و یا با زدن دکمه انصراف در شکل بالا، فراخوانی می‌شود که کد آن به شکل زیر است:

  function CloseEditPanel() {
        $('#CrudPanel').toggleClass('hide');
//پنل کراد ما که در حال حاضر از دید کاربر پنهان است با این خط ظاهر می‌گردد
        $('#EditPanel').toggleClass('hide');
//پنل ویرایش ما که در حال حاضر کاربر آن را می‌بیند، پنهان می‌شود از دید کاربر
        $("#txtLocationEditTitle").val('');
//مقدار تکست باکس خالی می‌شود
        $("#btnEditPanelLocation").attr('data-code', '');
        $("#btnEditPanelLocation").attr('data-parentId', '');
//دیتا اتریبیوت‌های ما که مقادیر کد و آی دی والد در آن قرار گرفته نیز خالی می‌شود
        // Enable clicking in treeview
        $("#treeview").children().unbind('click').bind('click', function () { return true; });
//اگر یادتان باشد با یک خط کد به کاربر اجازه ندادیم که با باز شدن پنل ویرایش، گره دیگری را انتخاب نمایی. حالا این خط کد عکس کد قبلیست و به کاربر اجازه می‌دهد در المنت مورد نظر کلیک کند
    }


   // Cancle edit Node tree
    $(document).on('click', '#btnCancle', function () {
        CloseEditPanel();
    });
  $(document).on('click', '#btnUnSelect', function () {
//رویداد عدم انتخاب
        treeview.select(null);
    });
مطالب
استفاده از Web API در ASP.NET Web Forms
گرچه ASP.NET Web API بهمراه ASP.NET MVC بسته بندی شده و استفاده می‌شود، اما اضافه کردن آن به اپلیکیشن‌های ASP.NET Web Forms کار ساده ای است. در این مقاله مراحل لازم را بررسی می‌کنیم.

برای استفاده از Web API در یک اپلیکیشن ASP.NET Web Forms دو قدم اصلی باید برداشته شود:

  • اضافه کردن یک کنترلر Web API که از کلاس ApiController مشتق می‌شود.
  • اضافه کردن مسیرهای جدید به متد Application_Start.


یک پروژه Web Forms بسازید

ویژوال استودیو را اجرا کنید و پروژه جدیدی از نوع ASP.NET Web Forms Application ایجاد کنید.


کنترلر و مدل اپلیکیشن را ایجاد کنید

کلاس جدیدی با نام Product بسازید و خواص زیر را به آن اضافه کنید.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}
همانطور که مشاهده می‌کنید مدل مثال جاری نمایانگر یک محصول است. حال یک کنترلر Web API به پروژه اضافه کنید. کنترلر‌های Web API درخواست‌های HTTP را به اکشن متدها نگاشت می‌کنند. در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Add, New Item را انتخاب کنید.

در دیالوگ باز شده گزینه Web را از پانل سمت چپ کلیک کنید و نوع آیتم جدید را Web API Controller Class انتخاب نمایید. نام این کنترلر را به "ProductsController" تغییر دهید و OK کنید.

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

namespace WebForms
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;

    public class ProductsController : ApiController
    {

        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public Product GetProductById(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return product;
        }

        public IEnumerable<Product> GetProductsByCategory(string category)
        {
            return products.Where(
                (p) => string.Equals(p.Category, category,
                    StringComparison.OrdinalIgnoreCase));
        }
    }
}
کنترلر جاری لیستی از محصولات را بصورت استاتیک در حافظه محلی نگهداری می‌کند. متدهایی هم برای دریافت لیست محصولات تعریف شده اند.


اطلاعات مسیریابی را اضافه کنید

مرحله بعدی اضافه کردن اطلاعات مسیریابی (routing) است. در مثال جاری می‌خواهیم آدرس هایی مانند "api/products/" به کنترلر Web API نگاشت شوند. فایل Global.asax را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.

using System.Web.Http;
حال کد زیر را به متد Application_Start اضافه کنید.
RouteTable.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = System.Web.Http.RouteParameter.Optional }
    );

برای اطلاعات بیشتر درباره مسیریابی در Web API به این لینک مراجعه کنید.


دریافت اطلاعات بصورت آژاکسی در کلاینت

تا اینجا شما یک API دارید که کلاینت‌ها می‌توانند به آن دسترسی داشته باشند. حال یک صفحهHTML خواهیم ساخت که با استفاده از jQuery سرویس را فراخوانی می‌کند. صفحه Default.aspx را باز کنید و کدی که بصورت خودکار در قسمت Content تولید شده است را حذف کرده و کد زیر را به این قسمت اضافه کنید:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" 
    AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebForms._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>Products</h2>
    <table>
    <thead>
        <tr><th>Name</th><th>Price</th></tr>
    </thead>
    <tbody id="products">
    </tbody>
    </table>
</asp:Content>
حال در قسمت HeaderContent کتابخانه jQuery را ارجاع دهید.
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
</asp:Content>

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

نکته: برای ارجاع دادن اسکریپت‌ها می‌توانید بسادگی فایل مورد نظر را با drag & drop به کد خود اضافه کنید.

زیر تگ jQuery اسکریپت زیر را اضافه کنید.

<script type="text/javascript">
    function getProducts() {
        $.getJSON("api/products",
            function (data) {
                $('#products').empty(); // Clear the table body.

                // Loop through the list of products.
                $.each(data, function (key, val) {
                    // Add a table row for the product.
                    var row = '<td>' + val.Name + '</td><td>' + val.Price + '</td>';
                    $('<tr/>', { text: row })  // Append the name.
                        .appendTo($('#products'));
                });
            });
        }

        $(document).ready(getProducts);
</script>

هنگامی که سند جاری (document) بارگذاری شد این اسکریپت یک درخواست آژاکسی به آدرس "api/products/" ارسال می‌کند. سرویس ما لیستی از محصولات را با فرمت JSON بر می‌گرداند، سپس این اسکریپت لیست دریافت شده را به جدول HTML اضافه می‌کند.

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

نظرات اشتراک‌ها
froala/wysiwyg-editor 1.2 منتشر شد
من دقیقا همون دستورات رو نوشتم
public virtual ActionResult FroalaUploadFile(HttpPostedFileBase file
اما موقعی که اطلاعات فرستاده میشه مقدار null داخل پارامتر file قرار میگیره

توی حالتی که تصویر آپلود کنیم توی قسمت Params  ارسالی این اطلاعاتی فرستاده میشه
Content-Disposition: form-data; name="file"; filename="picName.jpg"
اما توی حالت آپلود فایل
Content-Disposition: form-data; name="[object Object]"; filename="1202.pdf"
به اینصورت اطلاعات به اکشن فرستاده میشه