function doUpload(response, postdata, rowId) { var result = $.parseJSON(response.responseText); if (result.id < 1) return [false, "عملیات ثبت موفقیت آمیز نبود", result.id]; var fileElementId = 'xThumbnail'; if (rowId) { fileElementId = rowId + "_" + fileElementId; } var val = $("#" + fileElementId).val(); if (val == '' || val === undefined) { return [false, "لطفا فایلی را انتخاب کنید", result.id]; } alert(fileElementId); $('#grid1').block({ message: '<h4>در حال ارسال فایل به سرور</h4>' }); $.ajaxFileUpload({ url: "@Url.Action("UploadFile", "ProductTypes", new { area = "Admin" })", secureuri: false, fileElementId: fileElementId, dataType: 'json', data: { id: result.id }, complete: function () { $('#grid1').unblock(); }, success: function (data, status) { $("#grid").trigger("reloadGrid"); }, error: function (data, status, e) { alert(e); } }); return [true, "با تشکر!", result.id]; }
نمایش منتظر بمانید در حین بارگذاری اولیهی کامپوننت
کامپوننتهایی که قرار است اطلاعات را از یک Web API دریافت کنند، مدتی باید منتظر بمانند تا عملیات رفت و برگشت به سرور، تکمیل شود. در این بین میتوان یک loading را به کاربر نمایش داد:
@page "/hotel/rooms" @if (Rooms is not null && Rooms.Any()) { } else { <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;"> <img src="images/loader.gif" /> </div> } @code { IEnumerable<HotelRoomDTO> Rooms = new List<HotelRoomDTO>(); // ... }
- هر زمانیکه کار روال رویدادگردان OnInitializedAsync به پایان برسد (که شامل اجرای متد LoadRooms نیز هست)، سبب فراخوانی خودکار StateHasChanged میشود. این فراخوانی، UI را مجددا رندر میکند. به همین جهت است که پس از پایان کار، محتوای if، رندر خواهد شد.
- از این loading سفارشی که در میانهی صفحه نمایش داده میشود، میتوان در فایل wwwroot\index.html نیز بجای loading پیشفرض آن استفاده کرد:
<body> <div id="app"> <div style=" position: fixed; top: 50%; left: 50%; margin-top: -50px; margin-left: -100px; " > <img src="images/ajax-loader.gif" /> </div> </div>
افزودن خواصی جدید به HotelRoomDTO
میخواهیم به کاربر امکان تغییر تعداد روزهای اقامت را بدهیم. این انتخاب باید در لیست اتاقهای نمایش داده شده، با تغییر تعداد روزهای اقامت (TotalDays) و هزینهی جدید متناظر با آن (TotalAmount)، منعکس شود. به همین جهت این خواص را به HotelRoomDTO، اضافه میکنیم:
namespace BlazorServer.Models { public class HotelRoomDTO { // ... public int TotalDays { get; set; } public decimal TotalAmount { get; set; } } }
@code { HomeVM HomeModel = new HomeVM(); // ... private async Task LoadRoomsAsync() { Rooms = await HotelRoomService.GetHotelRoomsAsync(HomeModel.StartDate, HomeModel.EndDate); foreach (var room in Rooms) { room.TotalAmount = room.RegularRate * HomeModel.NoOfNights; room.TotalDays = HomeModel.NoOfNights; } } }
افزودن امکان تغییر تعداد روزهای اقامت در همان صفحهی نمایش لیست اتاقها
همانطور که در تصویر فوق هم مشاهده میکنید، میخواهیم در این صفحه نیز کاربر بتواند زمان شروع اقامت و مدت مدنظر را تغییر دهد. به همین جهت، HomeModel ای را که در قسمت قبل از Local Storage دریافت کردیم، به فرم زیر متصل میکنیم تا اجزای آن در این فرم، نمایش داده شده و قابل تغییر شوند:
@if (Rooms is not null && Rooms.Any()) { <EditForm Model="HomeModel" OnValidSubmit="SaveBookingInfo" class="bg-light"> <div class="pt-3 pb-2 px-5 mx-1 mx-md-0 bg-secondary"> <DataAnnotationsValidator /> <div class="row px-3 mx-3"> <div class="col-6 col-md-4"> <div class="form-group"> <label class="text-warning">Check in Date</label> <InputDate @bind-Value="HomeModel.StartDate" class="form-control" /> </div> </div> <div class="col-6 col-md-4"> <div class="form-group"> <label class="text-warning">Check Out Date</label> <input @bind="HomeModel.EndDate" disabled="disabled" readonly="readonly" type="date" class="form-control" /> </div> </div> <div class=" col-4 col-md-2"> <div class="form-group"> <label class="text-warning">No. of nights</label> <select class="form-control" @bind="HomeModel.NoOfNights"> <option value="Select" selected disabled="disabled">(Select No. Of Nights)</option> @for (var i = 1; i <= 10; i++) { <option value="@i">@i</option> } </select> </div> </div> <div class="col-8 col-md-2"> <div class="form-group" style="margin-top: 1.9rem !important;"> @if (IsProcessing) { <button class="btn btn-success btn-block form-control"> <i class="fa fa-spin fa-spinner"></i>Processing... </button> } else { <input type="submit" value="Update" class="btn btn-success btn-block form-control" /> } </div> </div> </div> </div> </EditForm>
@code { bool IsProcessing; // ... private async Task SaveBookingInfo() { IsProcessing = true; HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights); await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel); await LoadRoomsAsync(); IsProcessing = false; } }
سؤال: زمانیکه IsProcessing به true تنظیم میشود که هنوز کار متد رویدادگردان SaveBookingInfo به پایان نرسیدهاست و فراخوانی خودکار StateHasChanged در پایان متدهای رویدادگردان صورت میگیرد. پس چطور است که سبب رندر مجدد UI و تغییر برچسب دکمهی Update میشود؟
پاسخ به این سؤال را در قسمت 6 این سری با بررسی چرخهی حیات کامپوننتها، مشاهده کردیم:
«البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged میشوند؛ یکبار زمانیکه قسمت sync متد به پایان میرسد (در این مثال یعنی تا قبل از اولین await نوشته شده) و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید»
نمایش لیست اتاقها
نمایش لیست اتاقها مطابق تصویر فوق، دو قسمت اصلی را دارد:
الف) نمایش لیست تصاویر منتسب به یک اتاق، توسط کامپوننت carousel بوت استرپ
@foreach (var room in Rooms) { <div class="row p-2 my-3 " style="border-radius:20px; border: 1px solid gray"> <div class="col-12 col-lg-3 col-md-4"> <div id="carouselExampleIndicators_@room.Id" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel"> <ol class="carousel-indicators"> @{ int imageIndex = 0; int innerImageIndex = 0; } @foreach (var image in room.HotelRoomImages) { if (imageIndex == 0) { <li data-target="#carouselExampleIndicators_@room.Id" data-slide-to="@imageIndex" class="active"></li> } else { <li data-target="#carouselExampleIndicators_@room.Id" data-slide-to="@imageIndex"></li> } imageIndex++; } </ol> <div class="carousel-inner"> @foreach (var image in room.HotelRoomImages) { var imageUrl = $"{ImagesBaseAddress}/{image.RoomImageUrl}"; if (innerImageIndex == 0) { <div class="carousel-item active"> <img class="d-block w-100" style="border-radius:20px;" src="@imageUrl" alt="First slide"> </div> } else { <div class="carousel-item"> <img class="d-block w-100" style="border-radius:20px;" src="@imageUrl" alt="First slide"> </div> } innerImageIndex++; } </div> <a class="carousel-control-prev" href="#carouselExampleIndicators_@room.Id" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Previous</span> </a> <a class="carousel-control-next" href="#carouselExampleIndicators_@room.Id" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Next</span> </a> </div> </div> }
- سپس در حلقهای که برای نمایش لیست اتاقها تهیه کردهایم، قسمتهای مختلف carousel را تکمیل میکنیم که در اینجا نیاز به ایندکس تصاویر، لیست تصاویر و یک Id منحصربفرد برای این carousel خاص را دارد تا بتوان چندین وهله از آنرا در صفحه قرار داد که این id را بر اساس Id اتاق مشخص کردهایم.
دو نکته:
- در این مثال برای تعریف لینک به تصاویر، کد زیر را مشاهده میکنید:
var imageUrl = $"{ImagesBaseAddress}/{image.RoomImageUrl}";
@code { string ImagesBaseAddress = "https://localhost:5006";
- کامپوننت carousel برای اجرا، نیاز به فایل lib/bootstrap/dist/js/bootstrap.bundle.min.js را نیز دارد. به همین جهت مدخل اسکریپت آنرا باید به فایل wwwroot\index.html اضافه کرد.
ب) نمایش جزئیات نام و هزینهی اتاق
قسمت دوم حلقهی foreach نمایش لیست اتاقها، جهت نمایش جزئیات هر اتاق تعریف شدهاست:
@foreach (var room in Rooms) { <div class="col-12 col-lg-9 col-md-8"> <div class="row pt-3"> <div class="col-12 col-lg-8"> <p class="card-title text-warning" style="font-size:xx-large">@room.Name</p> <p class="card-text"> @((MarkupString)room.Details) </p> </div> <div class="col-12 col-lg-4"> <div class="row pb-3 pt-2"> <div class="col-12 col-lg-11 offset-lg-1"> <a href="@($"hotel/room-details/{room.Id}")" class="btn btn-success btn-block">Book</a> </div> </div> <div class="row "> <div class="col-12 pb-5"> <span class="float-right"> <span class="float-right">Occupancy : @room.Occupancy adults </span><br /> <span class="float-right pt-1">Room Size : @room.SqFt sqft</span><br /> <h4 class="text-warning font-weight-bold pt-4"> <span style="border-bottom:1px solid #ff6a00"> @room.TotalAmount.ToString("#,#.00;(#,#.00#)") </span> </h4> <span class="float-right">Cost for @room.TotalDays nights</span> </span> </div> </div> </div> </div> </div> </div> }
- هر اتاق نمایش داده شده، لینکی را به صفحهی خاص خودش نیز دارد که آنرا در قسمت بعدی تکمیل میکنیم.
- در اینجا TotalAmount و TotalDays محاسباتی و قابل تغییر بر اساس انتخاب کاربر نیز درج شدهاند.
یک تمرین: در برنامهی Blazor Server، سرویسی را جهت درج مشخصات امکانات رفاهی هتل تهیه کردیم. این امکانات رفاهی را از طریق Web API برنامه دریافت و سپس در برنامهی سمت کلاینت نمایش دهید.
بنابراین تکمیل این تمرین شامل تهیهی موارد زیر است که کدنویسی آن، با دو قسمت اخیر این سری دقیقا یکی است و نکتهی جدیدی را به همراه ندارد (و کدهای کامل آن را از انتهای بحث میتوانید دریافت کنید):
- تهیهی HotelAmenityController در پروژهی Web API که به کمک IAmenityService، لیست امکانات رفاهی را بازگشت میدهد.
- تهیهی ClientHotelAmenityService در پروژهی WASM که همانند ClientHotelRoomService قسمت قبل ، از Web API، لیست HotelAmenityDTOها را دریافت میکند.
- ثبت سرویس جدید ClientHotelAmenityService در Program.cs.
- در آخر حلقهای را بر روی لیست HotelAmenityDTO دریافتی از ClientHotelRoomService در کامپوننت Index.razor تشکیل داده و آنها را نمایش میدهیم.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-28.zip
CoffeeScript #6
Classes
Inheritance & Super
شما میتوانید به راحتی از کلاسهای دیگری که نوشتهاید، با استفاده از کلمهی کلیدی ،extends ارث بری کنید:
class Animal constructor: (@name) -> alive: -> true class Parrot extends Animal constructor: -> super("Parrot") dead: -> not @alive()
همانطوری که در مثال بالا مشاهده میکنید، در کلاس Parrot در تابع constructor، تابع super فراخوانی شده است. با استفاده از کلمهی کلیدی super میتوان تابع سازندهی کلاس پدر را فراخوانی کرد. نتیجهی کامپایل super در مثال بالا به این صورت میشود:
Parrot.__super__.constructor.call(this, "Parrot");
در صورتیکه تابع constructor را در کلاس فرزند ننوشته باشید، به طور پیش فرض CoffeeScript سازنده کلاس پدر را فراخوانی میکند.
CoffeeScript با استفاده از prototypal inheritance، به صورت خودکار تمامی خصوصیات کلاس پدر، به فرزندان انتقال پیدا میکند. این ویژگی سبب داشتن کلاسهای پویا میشود. برای درک بهتر این موضوع، فرض کنید که خصوصیتی را به کلاس پدر بعد از ارث بری کلاس فرزند اضافه میکنید. خصوصیت اضافه شده به تمامی فرزندان کلاس پدر به صورت خودکار اضافه میشود.
class Animal constructor: (@name) -> class Parrot extends Animal Animal::rip = true parrot = new Parrot("Macaw") alert("This parrot is no more") if parrot.rip
Mixins
Mixins توسط CoffeeScript پشتیبانی نمیشود و برای همین نیاز است که این قابلیت را برای خودمان پیاده سازی کنیم، به مثال زیر توجه کنید.extend = (obj, mixin) -> obj[name] = method for name, method of mixin obj include = (klass, mixin) -> extend klass.prototype, mixin # Usage include Parrot, isDeceased: true alert (new Parrot).isDeceased
var extend, include; extend = function(obj, mixin) { var method, name; for (name in mixin) { method = mixin[name]; obj[name] = method; } return obj; }; include = function(klass, mixin) { return extend(klass.prototype, mixin); }; include(Parrot, { isDeceased: true }); alert((new Parrot).isDeceased);
Extending classes
Mixins خیلی مرتب و خوب است اما خیلی شیء گرا نیست؛ در عوض امکان ادغام را در کلاسهای CoffeeScript ایجاد میکند. برای اینکه اصول شیء گرایی را بخواهیم رعایت کنیم و ویژگی ادغام را نیز داشته باشیم، کلاسی با نام Module را پیاده سازی میکنیم و تمامی کلاسهایی را که میخواهیم ویژگی ادغام را داشته باشند، از آن ارث بری میکنیم.
moduleKeywords = ['extended', 'included'] class Module @extend: (obj) -> for key, value of obj when key not in moduleKeywords @[key] = value obj.extended?.apply(@) this @include: (obj) -> for key, value of obj when key not in moduleKeywords # Assign properties to the prototype @::[key] = value obj.included?.apply(@) this
classProperties = find: (id) -> create: (attrs) -> instanceProperties = save: -> class User extends Module @extend classProperties @include instanceProperties # Usage: user = User.find(1) user = new User user.save()
همچنین برای خلاصه نویسی بیشتر میتوان از این الگو استفاده کرد (ساده و زیبا).
ORM = find: (id) -> create: (attrs) -> extended: -> @include save: -> class User extends Module @extend ORM
شاید بعضی از سایتها را دیده باشید که در حین ثبت نام، پس از وارد کردن یک نام کاربری و سپس مشغول شدن به پر کردن فیلد کلمهی عبور، در قسمت نام کاربری شروع به جستجو در مورد آزاد بودن نام کاربری درخواستی میکنند یا نمونهای دیگر، فرم پرداخت الکترونیکی بانک سامان. پس از اینکه شماره قبض را وارد کردید، بلافاصله بدون ریفرش صفحه به شما پیغام میدهد که این شماره معتبر است یا خیر. امروز قصد داریم این قابلیت را با استفاده از کتابخانهی Ajax مجموعه jQuery در ASP.Net پیاده سازی کنیم (بدون استفاده از ASP.Net Ajax مایکروسافت).
ابتدا سورس کامل را ملاحظه نمائید:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AjaxTest.aspx.cs" Inherits="testWebForms87.AjaxTest" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>jQuery Ajax Text</title>
<script src="jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#<%= TextBox1.ClientID %>").blur(function(event) {
$.ajax({
type: "POST",
url: "AjaxTest.aspx/IsUserAvailable",
data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(msg) {
$('#valid').html("<img src='ajaxImages/waiting.gif' alt='لطفا کمی تامل کنید'>");
var delay = function() {
AjaxSucceeded(msg);
};
setTimeout(delay, 2000); //remove this
},
error: AjaxFailed
});
});
});
function AjaxSucceeded(result) {
if (result.d == true)
$('#msg').html("<img src='ajaxImages/available.gif' alt='نام کاربری درخواستی موجود است'>");
else
$('#msg').html("<img src='ajaxImages/taken.gif' alt='متاسفانه نام کاربری مورد نظر پیشتر دریافت شدهاست'>");
}
function AjaxFailed(result) {
alert(result.status + ' ' + result.statusText);
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
user name:
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
<span id="msg"></span>
<br />
pass:
<asp:TextBox ID="TextBox2" TextMode="Password" runat="server"></asp:TextBox>
</div>
<!-- preload -->
<div style="display: none">
<img src="ajaxImages/available.gif" alt="available" />
<img src="ajaxImages/taken.gif" alt="taken" />
<img src="ajaxImages/waiting.gif" alt="waiting" />
</div>
</form>
</body>
</html>
using System;
using System.Web.Services;
namespace testWebForms87
{
public partial class AjaxTest : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
[WebMethod]
public static bool IsUserAvailable(string username)
{
// این مورد را با خواندن اطلاعات از دیتابیس میشود تعویض کرد
return username != "test";
}
}
}
همانطور که ملاحظه میکنید صفحهی ASP.Net ما بسیار ساده است و از دو تکست باکس استاندارد تشکیل میشود، به همراه تصاویر مربوط به Ajax که یک سری تصاویر ساده چرخان معروف منتظر بمانید ، یافت شد یا موجود نیست میباشند. این تصاویر در یک div مخفی (display: none) در صفحه قرار گرفتهاند و در هنگام بارگذاری صفحه، اینها نیز بارگذاری شده و حاضر و آماده خواهند بود. بنابراین هنگام استفاده از آنها، کاربر تاخیری را مشاهده نخواهد کرد. همچنین یک span با id مساوی msg را هم پس از تکست باکس اضافه کردهایم تا تصاویر مربوط به رخدادهای Ajax را با استفاده از تواناییهای jQuery به آن اضافه کنیم.
اسکریپت Ajax ما با دراختیار گرفتن روال رخداد گردان blur شیء textBox1 شروع میشود. همانطور که در مقالات پیشین سایت نیز ذکر شد، روش صحیح دریافت ID یک کنترل ASP.Net در کدهای سمت کلاینت جاوا اسکریپتی، بر اساس خاصیت ClientID آن است که در اولین سطر کدهای ما مشخص است (زیرا در ASP.Net نام و ID یک کنترل در هنگام رندر شدن به همراه ID کنترلهای دربرگیرنده آن نیز خواهد بود، بنابراین بهتر است این مورد را داینامیک کرد).
کار بررسی موجود بودن نام کاربری (یا مثلا یک شماره قبض و امثال آن) توسط WebMethod ایی به نام IsUserAvailable در code behind صفحه انجام میشود که پیاده سازی آنرا ملاحظه میکنید. بدیهی است در این مثال ساده، تنها نام کاربری از پیش رزرو شده، کلمهی test است و در یک کد واقعی این مورد با مقایسهی نام کاربری با اطلاعات موجود در دیتابیس باید صورت گیرد (و حملات تزریق اس کیوال را هم فراموش نکنید. برای رهایی از آنها "حتما" باید از پارامترهای ADO.Net استفاده کرد و گرنه کد شما مستعد به این نوع حملات خواهد بود).
سؤال: چرا از web method استفاده شد و همچنین چرا این متد static است؟
زمانیکه یک متد با کلمه کلیدی static مشخص میشود حالت state less پیدا میکند یعنی مستقل از وهلهی کلاس عمل میکند. در این حالت نیازی به ارسال ViewState نبوده (بنابراین در کوئری مورد نظر ما بسیار بهینه و سبک عمل میکنند) و همچنین نیازی به ایجاد یک وهلهای از کلاس صفحهی ما نیز نخواهد بود. برای توضیحات بیشتر به این مقاله مراجعه نمائید. (به صورت خلاصه، دلیل اصلی، کارآیی بالا و بهینه بودن این روش در این مساله ویژه است و در ASP.Net Ajax مایکروسافت به صورت گستردهای در پشت صحنه مورد استفاده قرار میگیرد)
استفاده از ویژگی WebMethod عملکرد صفحهی ما را شبیه به یک وب سرویس خواهد کرد و امکان دسترسی به آن در متدهای استاندارد POST به صورت ارسال دیتا به آدرس WebService.asmx/WebMethodName خواهد بود. یک مثال ساده و عملی
بررسی تابع Ajax بکار رفته:
این تابع هنگام فراخوانی رخداد blur تکستباکس ما (مطابق کد فوق) فراخوانی میشود. ساختار سادهای دارد که به شرح زیر است:
type: "POST"
url: "AjaxTest.aspx/IsUserAvailable"
data: "{'username': '" + $('#<%= TextBox1.ClientID %>').val() + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
<xx yy="nn"></xx>
{ "xx": {"yy":"nn"} }
success: function(msg)
error: AjaxFailed
در این مثال برای نمایش بهتر عملیات، یک وقفهی 2 ثانیهای توسط setTimeout ایجاد شده و بدیهی است در یک مثال واقعی باید آنرا حذف نمود.
نکته: با استفاده از افزونهی فایرباگ فایرفاکس، میتوان جزئیات این عملیات را بهتر مشاهده نمود:
در کتابخانهی Microsoft AspNetCore Identity میتوان با این کد، فیلد Email را منحصر بهفرد کرد:
//Program.cs file builder.Services.AddIdentity<User, Role>(options => { options.User.RequireUniqueEmail = true; }).AddEntityFrameworkStores<DatabaseContext>();
برنامه را اجرا و درخواستها را یکی یکی به سمت سرور ارسال میکنیم و اگر ایمیل تکراری باشد به ما خطا میده و میگه: "ایمیل تکراری است".
ولی مشکل اینجاست که کد بالا فیلد Email رو داخل دیتابیس منحصر بهفرد نمیکنه و فقط از سمت نرم افزار بررسی تکراری بودن ایمیل رو انجام میده. حالا اگه ما با استفاده از نرم افزارهای "تست برنامههای وب" مثل Apache JMeter تعداد زیادی درخواست را به سمت برنامهمان ارسال کنیم و بعد رکوردهای داخل جدول کاربران را نگاه کنیم، با وجود اینکه داخل نرم افزارمان پراپرتی Email را منحصر بهفرد کردهایم، ولی چندین رکورد، با یک ایمیل مشابه در داخل جدول User وجود خواهد داشت.
برای تست این سناریو، برنامه Apache JMeter را از این لینک دانلود میکنیم (در بخش Binaries فایل zip رو دانلود می کنیم).
نکته: داشتن jdk ورژن 8 به بالا پیش نیاز است. برای اینکه بدونید ورژن جاوای سیستمتون چنده، داخل cmd دستور java -version رو صادر کنید.
اگه تمایل به نصب، یا به روز رسانی jdk را داشتید، میتونید از این لینک استفاده کنید و بسته به سیستم عاملتون، یکی از تبهای Windows, macOS یا Linux رو انتخاب کنید و فایل مورد نظر رو دانلود کنید (برای Windows فایل x64 Compressed Archive رو دانلود و نصب میکنیم).
حالا فایل دانلود شده JMeter رو استخراج میکنیم، وارد پوشهی bin میشیم و فایل jmeter.bat رو اجرا میکنیم تا برنامهی JMeter اجرا بشه.
قبل از اینکه وارد برنامه JMeter بشیم، کدهای برنامه رو بررسی میکنیم.
موجودیت کاربر:
public class User : IdentityUser<int>;
ویوو مدل ساخت کاربر:
public class UserViewModel { public string UserName { get; set; } = null!; public string Email { get; set; } = null!; public string Password { get; set; } = null!; }
کنترلر ساخت کاربر:
[ApiController] [Route("/api/[controller]")] public class UserController(UserManager<User> userManager) : Controller { [HttpPost] public async Task<IActionResult> Add(UserViewModel model) { var user = new User { UserName = model.UserName, Email = model.Email }; var result = await userManager.CreateAsync(user, model.Password); if (result.Succeeded) { return Ok(); } return BadRequest(result.Errors); } }
حالا وارد برنامه JMeter میشیم و اولین کاری که باید انجام بدیم این است که مشخص کنیم چند درخواست را در چند ثانیه قرار است ارسال کنیم. برای اینکار در برنامه JMeter روی TestPlan کلیک راست میکنیم و بعد:
Add -> Threads (Users) -> Thread Group
حالا باید بر روی Thread Group کلیک کنیم و بعد در بخش Number of threads (users) تعداد درخواستهایی را که قرار است به سمت سرور ارسال کنیم، مشخص کنیم؛ برای مثال عدد 100.
گزینه Ramp-up period (seconds) برای اینه که مشخص کنیم این 100 درخواست قرار است در چند ثانیه ارسال شوند که آن را روی 0.1 ثانیه قرار میدهیم تا درخواستها را با سرعت بسیار زیاد ارسال کند.
الان باید مشخص کنیم چه دیتایی قرار است به سمت سرور ارسال شود:
برای اینکار باید یک Http Request اضافه کنیم. برای این منظور روی Thread Group که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:
Add -> Sampler -> Http Request
حالا روی Http Request کلیک میکنیم و متد ارسال درخواست رو که روی Get هست، به Post تغییر میدیم و بعد Path رو هم به آدرسی که قراره دیتا رو بهش ارسال کنیم، تغییر میدهیم:
https://localhost:7091/api/User
حالا پایینتر Body Data رو انتخاب میکنیم و دیتایی رو که قراره به سمت سرور ارسال کنیم، در قالب Json وارد میکنیم:
{ "UserName": "payam${__Random(1000, 9999999)}", "Email": "payam@gmail.com", "Password": "123456aA@" }
چون بخش UserName در پایگاه داده منحصر بهفرد است، با این دستور:
${__Random(1000, 9999999)}
یک عدد Random رو به UserName اضافه میکنیم که دچار خطا نشیم.
حالا فقط باید یک Header رو هم به درخواستمون اضافه کنیم، برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:
Add -> Config Element -> Http Header Manager
حالا روی دکمهی Add در پایین صفحه کلیک میکنیم و این Header رو اضافه میکنیم:
Name: Content-Type Value: application/json
همچنین میتونیم یک View result رو هم اضافه کنیم تا وضعیت تمامی درخواستهای ارسال شده رو مشاهده کنیم. برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:
Add -> Listener -> View Results Tree
فایل Backup، برای اینکه مراحل بالا رو سریعتر انجام بدید:
File -> Open
حالا بر روی دکمهی سبز رنگ Play در Toolbar بالا کلیک میکنیم تا تمامی درخواست ها را به سمت سرور ارسال کنه و همچنین میتونیم از طریق View result tree ببینیم که چند درخواست موفقیت آمیز و چند درخواست ناموفق انجام شدهاست.
حالا اگر وارد پایگاه داده بشیم، میبینیم که چندین رکورد، با Email یکسان، در جدول User وجود داره:
در حالیکه ایمیل رو در تنظیمات کتابخانه Microsoft AspNetCore Identity به صورت Unique تعریف کردهایم:
//Program.cs file builder.Services.AddIdentity<User, Role>(options => { options.User.RequireUniqueEmail = true; }).AddEntityFrameworkStores<DatabaseContext>();
دلیل این مشکل این است که درخواستها در قالب یک صف، یک به یک اجرا نمیشوند؛ بلکه به صورت همزمان فریم ورک ASP.NET Core برای بالا بردن سرعت اجرای درخواستها از تمامی Thread هایی که در اختیارش هست استفاده میکند و در چندین Thread جداگانه، درخواستهایی رو به کنترلر User میفرسته و در نتیجه، در یک زمان مشابه، چندین درخواست ارسال میشه که آیا یک ایمیل برای مثال با مقدار payam@yahoo.com وجود داره یا خیر و در تمامی درخواستها چون همزمان انجام شده، جواب خیر است. یعنی ایمیل تکراری با آن مقدار، در پایگاه داده وجود ندارد و تمامی درخواستهایی که همزمان به سرور رسیدهاند، کاربر جدید را با ایمیل مشابهی ایجاد میکنند.
این مشکل را میتوان حتی در سایتهای فروش بلیط نیز پیدا کرد؛ یعنی چند نفر یک صندلی را رزرو کردهاند و همزمان وارد درگاه پرداخت شده و هزینهایی را برای آن پرداخت میکنند. اگر آن درخواستها را وارد صف نکنیم، امکان دارد که یک صندلی را به چند نفر بفروشیم. این سناریو برای زمانی است که در پایگاه داده، فیلدها را Unique تعریف نکرده باشیم. هر چند که اگر فیلدها را نیز Unique تعریف کرده باشیم تا یک صندلی را به چند نفر نفروشیم، در آن صورت هم برنامه دچار خطای 500 خواهد شد. پس بهتر است که حتی در زمانهایی هم که فیلدها را Unique تعریف میکنیم، باز هم از ورود چند درخواست همزمان به اکشن رزرو صندلی جلوگیری کنیم.
راه حل
برای حل این مشکل میتوان از Lock statement استفاده کرد که این راه حل نیز یک مشکل دارد که در ادامه به آن اشاره خواهم کرد.
Lock statement به ما این امکان رو میده تا اگر بخشی از کد ما در یک Thread در حال اجرا شدن است، Thread دیگری به آن بخش از کد، دسترسی نداشته باشد و منتظر بماند تا آن Thread کارش با کد ما تموم شود و بعد Thread جدید بتونه کد مارو اجرا کنه.
نحوه استفاده از Lock statement هم بسیار سادهاست:
public class TestClass { private static readonly object _lock1 = new(); public void Method1() { lock (_lock1) { // Body } } }
حالا باید کدهای خودمون رو در بخش Body اضافه کنیم تا دیگر چندین Thread به صورت همزمان، کدهای ما رو اجرا نکنند.
اما یک مشکل وجود داره و آن این است که ما نمیتوانیم در Lock statement، از کلمه کلیدی await استفاده کنیم؛ در حالیکه برای ساخت User جدید باید از await استفاده کنیم:
var result = await userManager.CreateAsync(user, model.Password);
برای حل این مشکل میتوان از کلاس SemaphoreSlim بجای کلمهی کلیدی lock استفاده کرد:
[ApiController] [Route("/api/[controller]")] public class UserController(UserManager<User> userManager) : Controller { private static readonly SemaphoreSlim Semaphore = new (initialCount: 1, maxCount: 1); [HttpPost] public async Task<IActionResult> Add(UserViewModel model) { var user = new User { UserName = model.UserName, Email = model.Email }; // Acquire the semaphore await Semaphore.WaitAsync(); try { // Perform user creation var result = await userManager.CreateAsync(user, model.Password); if (result.Succeeded) { return Ok(); } return BadRequest(result.Errors); } finally { // Release the semaphore Semaphore.Release(); } } }
این کلاس نیز مانند lock عمل میکند، ولی تواناییهای بیشتری را در اختیار ما قرار میدهد؛ برای مثال میتوان تعیین کرد که همزمان چند ترد میتوانند به این کد دسترسی داشته باشند؛ در حالیکه در lock statement فقط یک Thread میتوانست به کد دسترسی داشته باشد. مزیت دیگر کلاس SemaphoreSlim این است که میتوان برای اجرای کدمان Timeout در نظر گرفت تا از بلاک شدن نامحدود Thread جلوگیری کنیم.
با فراخوانی await semaphore.WaitAsync، دسترسی کد ما توسط سایر Thread ها محدود و با فراخوانی Release، کد ما توسط سایر Thread ها قابل دسترسی میشود.
مشکل قفل کردن Thread ها
هنگام قفل کردن Thread ها، مشکلی وجود دارد و آن این است که اگر برنامهی ما روی چندین سرور مختلف اجرا شود، این روش جوابگو نخواهد بود؛ چون قفل کردن Thread روی یک سرور تاثیری در سایر سرورها جهت محدود کردن دسترسی به کد ما ندارد. اما به صورت کلی میتوان از این روش برای بخشهایی خاص از برنامههایمان استفاده کنیم.
پیاده سازی با کمک الگوی AOP
برای اینکه کارمون راحت تر بشه، میتونیم کدهای بالا رو به یک Attribute انتقال بدیم و از اون Attribute در بالای اکشنهامون استفاده کنیم تا کل عملیات اکشنهامونو رو در یک Thread قفل کنیم:
[AttributeUsage(AttributeTargets.Method)] public class SemaphoreLockAttribute : Attribute, IAsyncActionFilter { private static readonly SemaphoreSlim Semaphore = new (1, 1); public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // Acquire the semaphore await Semaphore.WaitAsync(); try { // Proceed with the action await next(); } finally { // Release the semaphore Semaphore.Release(); } } }
حالا میتونیم این Attribute را برای هر اکشنی استفاده کنیم:
[HttpPost] [SemaphoreLock] public async Task<IActionResult> Add(UserViewModel model) { var user = new User { UserName = model.UserName, Email = model.Email }; var result = await userManager.CreateAsync(user, model.Password); if (result.Succeeded) { return Ok(); } return BadRequest(result.Errors); }
<input type="file" multiple="multiple" name="FileUpload1" id="FileUpload1" />
<asp:FileUpload runat="server" ID="FileUploadMultiple" AllowMultiple="true" />
در صورتیکه از این روش در پروژههایتان استفاده کنید فقط کافیست با یک حلقه تمامی کنترلهای مورد نظر را پیمایش و هر کدام از فایلها را آپلود و ذخیره نمایید.
<asp:FileUpload runat="server" ID="FileUploadMultiple" AllowMultiple="true" /> <asp:Button runat="server" ID="btnUlpad" Text="Upload" OnClick="btnUlpad_Click" /> <asp:Label runat="server" ID="lblMessage"></asp:Label>
int Count = 0; foreach (var item in FileUploadMultiple.PostedFiles) { string Extension = Path.GetExtension(item.FileName); string FileName = new Random().Next(1, 50).ToString()+Extension; item.SaveAs(Server.MapPath("~")+"//File//"+FileName); Count++; } if (Count == FileUploadMultiple.PostedFiles.Count) lblMessage.Text = string.Format("فایلهای انتخابی با موفقیت آپلود شدند"); else lblMessage.Text = string.Format("{0} از {1} فایل با موفقیت آپلود شد", Count, FileUploadMultiple.PostedFiles.Count);
مدل EAV چیست؟
آموزش QUnit #2
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Refactored date examples</title> <link rel="stylesheet" href="../qunit.css"> <script src="../qunit.js"></script> <script src="prettydate.js"></script> <script> test("prettydate basics", function() { var now = "2013/01/28 22:25:00"; equal(prettyDate(now, "2013/01/28 22:24:30"), "just now"); equal(prettyDate(now, "201308/01/28 22:23:30"), "1 minute ago"); equal(prettyDate(now, "2013/01/28 21:23:30"), "1 hour ago"); equal(prettyDate(now, "2013/01/27 22:23:30"), "Yesterday"); equal(prettyDate(now, "2013/01/26 22:23:30"), "2 days ago"); equal(prettyDate(now, "2012/01/26 22:23:30"), undefined); }); </script> </head> <body> <div id="qunit"></div> </body> </html>
test("prettydate basics", function() { function date(then, expected) { equal(prettyDate("2013/01/28 22:25:00", then), expected); } date("2013/01/28 22:24:30", "just now"); date("2013/01/28 22:23:30", "1 minute ago"); date("2013/01/28 21:23:30", "1 hour ago"); date("2013/01/27 22:23:30", "Yesterday"); date("2013/01/26 22:23:30", "2 days ago"); date("2012/01/26 22:23:30", undefined); });
var prettyDate = { format: function(now, time){ var date = new Date(time || ""), diff = (((new Date(now)).getTime() - date.getTime()) / 1000), day_diff = Math.floor(diff / 86400); if ( isNaN(day_diff) || day_diff < 0 || day_diff >= 31 ) return; return day_diff === 0 && ( diff < 60 && "just now" || diff < 120 && "1 minute ago" || diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" || diff < 7200 && "1 hour ago" || diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") || day_diff === 1 && "Yesterday" || day_diff < 7 && day_diff + " days ago" || day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago"; }, update: function(now) { var links = document.getElementsByTagName("a"); for ( var i = 0; i < links.length; i++ ) { if ( links[i].title ) { var date = prettyDate.format(now, links[i].title); if ( date ) { links[i].innerHTML = date; } } } } };
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Refactored date examples</title> <link rel="stylesheet" href="../qunit.css"> <script src="../qunit.js"></script> <script src="prettydate2.js"></script> <script> test("prettydate.format", function() { function date(then, expected) { equal(prettyDate.format("2013/01/28 22:25:00", then), expected); } date("2013/01/28 22:24:30", "just now"); date("2013/01/28 22:23:30", "1 minute ago"); date("2013/01/28 21:23:30", "1 hour ago"); date("2013/01/27 22:23:30", "Yesterday"); date("2013/01/26 22:23:30", "2 days ago"); date("2012/01/26 22:23:30", undefined); }); function domtest(name, now, first, second) { test(name, function() { var links = document.getElementById("qunit-fixture") .getElementsByTagName("a"); equal(links[0].innerHTML, "January 28th, 2013"); equal(links[2].innerHTML, "January 27th, 2013"); prettyDate.update(now); equal(links[0].innerHTML, first); equal(links[2].innerHTML, second); }); } domtest("prettyDate.update", "2013-01-28T22:25:00Z", "2 hours ago", "Yesterday"); domtest("prettyDate.update, one day later", "2013/01/29 22:25:00", "Yesterday", "2 days ago"); </script> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"> <ul> <li id="post57"> <p>blah blah blah...</p> <small> Posted <span> <a href="/2013/01/blah/57/" title="2013-01-28T20:24:17Z" >January 28th, 2013</a> </span> by <span><a href=""></a></span> </small> </li> <li id="post57"> <p>blah blah blah...</p> <small> Posted <span> <a href="/2013/01/blah/57/" title="2013-01-27T22:24:17Z" >January 27th, 2013</a> </span> by <span><a href=""></a></span> </small> </li> </ul> </div> </body> </html>
import svelte from 'rollup-plugin-svelte'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import livereload from 'rollup-plugin-livereload'; import { terser } from 'rollup-plugin-terser'; const production = !process.env.ROLLUP_WATCH; export default { input: 'src/main.js', output: { sourcemap: true, format: 'iife', name: 'app', file: 'public/bundle.js' }, plugins: [ svelte({ // enable run-time checks when not in production dev: !production, // we'll extract any component CSS out into // a separate file — better for performance css: css => { css.write('public/bundle.css'); } }), // If you have external dependencies installed from // npm, you'll most likely need these plugins. In // some cases you'll need additional configuration — // consult the documentation for details: // https://github.com/rollup/rollup-plugin-commonjs resolve(), commonjs(), // Watch the `public` directory and refresh the // browser on changes when not in production !production && livereload('public'), // If we're building for production (npm run build // instead of npm run dev), minify production && terser() ], watch: { clearScreen: false } };
package.json :
{ "name": "svelte-app", "version": "1.0.0", "devDependencies": { "npm-run-all": "^4.1.5", "rollup": "^1.10.1", "rollup-plugin-commonjs": "^9.3.4", "rollup-plugin-livereload": "^1.0.0", "rollup-plugin-node-resolve": "^4.2.3", "rollup-plugin-svelte": "^5.0.3", "rollup-plugin-terser": "^4.0.4", "sirv-cli": "^0.4.0", "svelte": "^3.0.0" }, "scripts": { "build": "rollup -c", "autobuild": "rollup -c -w", "dev": "run-p start:dev autobuild", "start": "sirv public", "start:dev": "sirv public --dev" } }
build | برای ساخت و ایجاد خروجیهای برنامه توسط rollup مورد قرار استفاده میگیرد. |
autobuild | مانند build برای ساخت خروجیهای نهایی برنامه استفاده میشود. ولی تفاوتی که دارد پس از هر تغییر در سورس کد برنامه به صورت خودکار build جدیدی پس از اجرای آن گرفته میشود. |
dev | برنامه را درحالت Developer Mode اجرا میکند که برای مشاهده تغییرات به صورت خودکار در browser، بدون نیاز به رفرش صفحه و همینطور عیب یابی برنامه مناسب است. |
start | از طریق sirv که یک وب سرور سبک برای هاست کردن سایتهای استاتیک است، برنامه را هاست میکند. |
start:dev | مانند start است با این تفاوت که برنامه را در حالت Developer Mode هاست میکند که میتواند برای عیب یابی برنامه از آن استفاده کرد؛ چرا که سورس برنامه از طریق source Map قابل دسترس خواهد بود. |
دو پوشه src و public هم برای ما به صورت پیش فرض ایجاد شدهاند که فولدر public فایلهای نهایی تولید شده برنامه ما را شامل میشود و src، دربرگیرنده تمام سورس کدهای برنامه ما میباشد.
<script> export let name; </script> <style> h1 { color: purple; } </style> <h1>Hello {name}!</h1>
src/main.js :
import App from './App.svelte'; const app = new App({ target: document.body, props: { name: 'world' } }); export default app;