اشتراکها
نگاهی به Angular 2.0 Core
مطالب
ASP.NET MVC #12
تولید خودکار فرمهای ورود و نمایش اطلاعات در ASP.NET MVC بر اساس اطلاعات مدلها
در الگوی MVC، قسمت M یا مدل آن یک سری ویژگیهای خاص خودش را دارد:
شما را وادار نمیکند که مدل را به نحو خاصی طراحی کنید. شما را مجبور نمیکند که کلاسهای مدل را برای نمونه همانند کلاسهای کنترلرها، از کلاس خاصی به ارث ببرید. یا حتی در مورد نحوهی دسترسی به دادهها نیز، نظری ندارد. به عبارتی برنامه نویس است که میتواند بر اساس امکانات مهیای در کل اکوسیستم دات نت، در این مورد آزادانه تصمیم گیری کند.
بر همین اساس ASP.NET MVC یک سری قرارداد را برای سهولت اعتبار سنجی یا تولید بهتر رابط کاربری بر اساس اطلاعات مدلها، فراهم آورده است. این قراردادها هم چیزی نیستند جز یک سری metadata که نحوهی دربرگیری اطلاعات را در مدلها توضیح میدهند. برای دسترسی به آنها پروژه جاری باید ارجاعی را به اسمبلیهای System.ComponentModel.DataAnnotations.dll و System.Web.Mvc.dll داشته باشد (که VS.NET به صورت خودکار در ابتدای ایجاد پروژه اینکار را انجام میدهد).
یک مثال کاربردی
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. در پوشه مدلهای آن، مدل اولیهای را با محتوای زیر ایجاد نمائید:
using System;
namespace MvcApplication8.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
public decimal Salary { set; get; }
public string Address { set; get; }
public bool IsMale { set; get; }
public DateTime AddDate { set; get; }
}
}
سپس یک کنترلر جدید را هم به نام EmployeeController با محتوای زیر به پروژه اضافه نمائید:
using System;
using System.Web.Mvc;
using MvcApplication8.Models;
namespace MvcApplication8.Controllers
{
public class EmployeeController : Controller
{
public ActionResult Create()
{
var employee = new Employee { AddDate = DateTime.Now };
return View(employee);
}
}
}
بر روی متد Create کلیک راست کرده و یک View ساده را برای آن ایجاد نمائید. سپس محتوای این View را به صورت زیر تغییر دهید:
@model MvcApplication8.Models.Employee
@{
ViewBag.Title = "Create";
}
<h2>Create An Employee</h2>
@using (Html.BeginForm(actionName: "Create", controllerName: "Employee"))
{
@Html.EditorForModel()
<input type="submit" value="Save" />
}
اکنون اگر پروژه را اجرا کرده و مسیر http://localhost/employee/create را وارد نمائید، یک صفحه ورود اطلاعات تولید شده به صورت خودکار را مشاهده خواهید کرد. متد Html.EditorForModel بر اساس اطلاعات خواص عمومی مدل، یک فرم خودکار را تشکیل میدهد.
البته فرم تولیدی به این شکل شاید آنچنان مطلوب نباشد، از این جهت که برای مثال Id را هم لحاظ کرده، در صورتیکه قرار است این Id توسط بانک اطلاعاتی انتساب داده شود و نیازی نیست تا کاربر آنرا وارد نماید. یا مثلا برچسب AddDate نباید به این شکل صرفا بر اساس نام خاصیت متناظر با آن تولید شود و مواردی از این دست. به عبارتی نیاز به سفارشی سازی کار این فرم ساز توکار ASP.NET MVC وجود دارد که ادامه بحث جاری را تشکیل خواهد داد.
سفارشی سازی فرم ساز توکار ASP.NET MVC با کمک Metadata خواص
برای اینکه بتوان نحوه نمایش فرم خودکار تولید شده را سفارشی کرد، میتوان از یک سری attribute و data annotations توکار دات نت و ASP.NET MVC استفاده کرد و نهایتا این metadata توسط فریم ورک، مورد استفاده قرار خواهند گرفت. برای مثال:
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace MvcApplication8.Models
{
public class Employee
{
//[ScaffoldColumn(false)]
[HiddenInput(DisplayValue=false)]
public int Id { set; get; }
public string Name { set; get; }
[DisplayName("Annual Salary ($)")]
public decimal Salary { set; get; }
public string Address { set; get; }
[DisplayName("Is Male?")]
public bool IsMale { set; get; }
[DisplayName("Start Date")]
[DataType(DataType.Date)]
public DateTime AddDate { set; get; }
}
}
در اینجا به کمک ویژگی HiddenInput از نمایش عمومی خاصیت Id جلوگیری خواهیم کرد یا توسط ویژگی DisplayName، برچسب دلخواه خود را به عناصر فرم تشکیل شده، انتساب خواهیم داد. اگر نیاز باشد تا خاصیتی کلا از رابط کاربری حذف شود میتوان از ویژگی ScaffoldColumn با مقدار false استفاده کرد. یا توسط DataType، مشخص کردهایم که نوع ورودی فقط قرار است Date باشد و نیازی به قسمت Time آن نداریم.
DataType شامل نوعهای از پیش تعریف شده دیگری نیز هست. برای مثال اگر نیاز به نمایش TextArea بود از مقدار MultilineText، استفاده کنید:
[DataType(DataType.MultilineText)]
یا برای نمایش PasswordBox از مقدار Password میتوان کمک گرفت. اگر نیاز دارید تا آدرس ایمیلی به شکل یک لینک mailto نمایش داده شود از مقدار EmailAddress استفاده کنید. به کمک مقدار Url، متن خروجی به صورت خودکار تبدیل به یک آدرس قابل کلیک خواهد شد.
اکنون اگر پروژه را مجددا کامپایل کنیم و به آدرس ایجاد یک کارمند جدید مراجعه نمائیم، با رابط کاربری بهتری مواجه خواهیم شد.
سفارشی سازی ظاهر فرم ساز توکار ASP.NET MVC
در ادامه اگر بخواهیم ظاهر این فرم را اندکی سفارشیتر کنیم، بهتر است به سورس صفحه تولیدی در مرورگر مراجعه کنیم. در اینجا یک سری عناصر HTML محصور شده با div را خواهیم یافت. هر کدام از اینها هم با classهای css خاص خود تعریف شدهاند. بنابراین اگر علاقمند باشیم که رنگ و قلم و غیره این موارد تغییر دهیم، تنها کافی است فایل css برنامه را ویرایش کنیم و نیازی به دستکاری مستقیم کدهای برنامه نیست.
انتساب قالبهای سفارشی به خواص یک شیء
تا اینجا در مورد نحوه سفارشی سازی رنگ، قلم، برچسب و نوع دادههای هر کدام از عناصر نهایی نمایش داده شده، توضیحاتی را ملاحظه نمودید.
در فرم تولیدی نهایی، خاصیت bool تعریف شده به صورت خودکار به یک checkbox تبدیل شده است. چقدر خوب میشد اگر امکان تبدیل آن مثلا به RadioButton انتخاب مرد یا زن بودن کارمند ثبت شده در سیستم وجود داشت. برای اصلاح یا تغییر این مورد، باز هم میتوان از متادیتای خواص، جهت تعریف قالبی خاص برای هر کدام از خواص مدل استفاده کرد.
به پوشه Views/Shared مراجعه کرده و یک پوشه جدید به نام EditorTemplates را ایجاد نمائید. بر روی این پوشه کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه باز شده، گزینه «Create as a partial view» را انتخاب نمائید و نام آنرا هم مثلا GenderOptions وارد کنید. همچنین گزینه «Create a strongly typed view» را نیز انتخاب کنید. مقدار Model class را مساوی bool وارد نمائید. فعلا یک hello داخل این صفحه جدید وارد کرده و سپس خاصیت IsMale را به نحو زیر تغییر دهید:
[DisplayName("Gender")]
[UIHint("GenderOptions")]
public bool IsMale { set; get; }
توسط ویژگی UIHint، میتوان یک خاصیت را به یک partial view متصل کرد. در اینجا خاصیت IsMale به partial view ایی به نام GenderOptions متصل شده است. اکنون اگر برنامه را کامپایل و اجرا کرده و آدرس ایجاد یک کارمند جدید را ملاحظه کنید، بجای Checkbox باید یک hello نمایش داده شود.
محتویات این Partial view هم نهایتا به شکل زیر خواهند بود:
@model bool
<p>@Html.RadioButton("", false, !Model) Female</p>
<p>@Html.RadioButton("", true, Model) Male</p>
در اینجا Model که از نوع bool تعریف شده، به خاصیت IsMale اشاره خواهد کرد. دو RadioButton هم برای انتخاب بین حالت زن و مرد تعریف شدهاند.
یا یک مثال جالب دیگر در این زمینه میتواند تبدیل enum به یک Dropdownlist باشد. در این حالت partial view ما شکل زیر را خواهد یافت:
@model Enum
@Html.DropDownListFor(m => m, Enum.GetValues(Model.GetType())
.Cast<Enum>()
.Select(m => {
string enumVal = Enum.GetName(Model.GetType(), m);
return new SelectListItem() {
Selected = (Model.ToString() == enumVal),
Text = enumVal,
Value = enumVal
};
}))
و برای استفاده از آن، از ویژگی زیر میتوان کمک گرفت (مزین کردن خاصیتی از نوع یک enum دلخواه، جهت تبدیل خودکار آن به یک دراپ داون لیست):
[UIHint("Enum")]
سایر متدهای کمکی تولید و نمایش خودکار اطلاعات از روی اطلاعات مدلهای برنامه
متدهای دیگری نیز در ردهی Templated helpers قرار میگیرند. اگر از متد Html.EditorFor استفاده کنیم، از تمام این اطلاعات متادیتای تعریف شده نیز استفاده خواهد کرد. همانطور که در قسمت قبل (قسمت 11) نیز توضیح داده شد، صفحه استاندارد Add view در VS.NET به همراه یک سری قالب تولید فرمهای Create و Edit هم هست که دقیقا کد نهایی تولیدی را بر اساس همین متد تولید میکند.
استفاده از Html.EditorFor انعطاف پذیری بیشتری را به همراه دارد. برای مثال اگر یک طراح وب، طرح ویژهای را در مورد ظاهر فرمهای سایت به شما ارائه دهد، بهتر است از این روش استفاده کنید. اما خروجی نهایی Html.EditorForModel به کمک تعدادی متادیتا و اندکی دستکاری CSS، از دیدگاه یک برنامه نویس بی نقص است!
به علاوه، متد Html.DisplayForModel نیز مهیا است. بجای اینکه کار تولید رابط کاربری اطلاعات نمایش جزئیات یک شیء را انجام دهید، اجازه دهید تا متد Html.DisplayForModel اینکار را انجام دهد. سفارشی سازی آن نیز همانند قبل است و بر اساس متادیتای خواص انجام میشود. در این حالت، مسیر پیش فرض جستجوی قالبهای UIHint آن، Views/Shared/DisplayTemplates میباشد. همچنین Html.DisplayFor نیز جهت کار با یک خاصیت مدل تدارک دیده شده است. البته باید درنظر داشت که استفاده از پوشه Views/Shared اجباری نیست. برای مثال اگر از پوشه Views/Home/DisplayTemplates استفاده کنیم، قالبهای سفارشی تهیه شده تنها جهت Viewهای کنترلر home قابل استفاده خواهند بود.
یکی دیگر از ویژگیهایی که جهت سفارشی سازی نحوه نمایش خودکار اطلاعات میتواند مورد استفاده قرار گیرد، DisplayFormat است. برای مثال اگر مقدار خاصیت در حال نمایش نال بود، میتوان مقدار دیگری را نمایش داد:
[DisplayFormat(NullDisplayText = "-")]
یا اگر علاقمند بودیم که فرمت اطلاعات در حال نمایش را تغییر دهیم، به نحو زیر میتوان عمل کرد:
[DisplayFormat(DataFormatString = "{0:n}")]
مقدار DataFormatString در پشت صحنه در متد string.Format مورد استفاده قرار میگیرد.
و اگر بخواهیم که این ویژگی در حالت تولید فرم ویرایش نیز درنظر گرفته شود، میتوان خاصیت ApplyFormatInEditMode را نیز مقدار دهی کرد:
[DisplayFormat(DataFormatString = "{0:n}", ApplyFormatInEditMode = true)]
بازنویسی قالبهای پیش فرض تولید فرم یا نمایش اطلاعات خودکار ASP.NET MVC
یکی دیگر از قرارداهای بکارگرفته شده در حین استفاده از قالبهای سفارشی، استفاده از نام اشیاء میباشد. مثلا در پوشه Views/Shared/DisplayTemplates، اگر یک Partial view به نام String.cshtml وجود داشته باشد، از این پس نحوه رندر کلیه خواص رشتهای تمام مدلها، بر اساس محتوای فایل String.cshtml مشخص میشود؛ به همین ترتیب در مورد datetime و سایر انواع مهیا.
برای مثال اگر خواستید تمام تاریخهای میلادی دریافتی از بانک اطلاعاتی را شمسی نمایش دهید، فقط کافی است یک فایل datetime.cshtml سفارشی را تولید کنید که Model آن تاریخ میلادی دریافتی است و نهایتا کار این Partial view، رندر تاریخ تبدیل شده به همراه تگهای سفارشی مورد نظر میباشد. در این حالت نیازی به ذکر ویژگی UIHint نیز نخواهد بود و همه چیز خودکار است.
به همین ترتیب اگر نام مدل ما Employee باشد و فایل Partial view ایی به نام Employee.cshtml در پوشه Views/Shared/DisplayTemplates قرار گیرد، متد Html.DisplayForModel به صورت پیش فرض از محتوای این فایل جهت رندر اطلاعات نمایش جزئیات شیء Employee استفاده خواهد کرد.
داخل Partial viewهای سفارشی تعریف شده به کمک خاصیت ViewData.TemplateInfo.FormattedModelValue مقدار نهایی فرمت شده قابل استفاده را فراهم میکند. این مورد هم از این جهت حائز اهمیت است که نیازی نباشد تا ویژگی DisplayFormat را به صورت دستی پردازش کنیم. همچنین اطلاعات ViewData.ModelMetadata نیز دراینجا قابل دسترسی هستند.
سؤال: Partial View چیست؟
همانطور که از نام Partial view برمیآید، هدف آن رندر کردن قسمتی از صفحه است به همراه استفاده مجدد از کدهای تولید رابط کاربری در چندین و چند View؛ چیزی شبیه به User controls در ASP.NET Web forms البته با این تفاوت که Page life cycle و Code behind و سایر موارد مشابه آن در اینجا حذف شدهاند. همچنین از Partial viewها برای به روز رسانی قسمتی از صفحه حین فراخوانیهای Ajaxایی نیز استفاده میشود. مهمترین کاربرد Partial views علاوه بر استفاده مجدد از کدها، خلوت کردن Viewهای شلوغ است جهت سادهتر سازی نگهداری آنها در طول زمان (یک نوع Refactoring فایلهای View محسوب میشوند).
پسوند این فایلها نیز بسته به موتور View مورد استفاده تعیین میشود. برای مثال حین استفاده از Razor، پسوند Partial views همان cshtml یا vbhtml میباشد. یا اگر از web forms view engine استفاده شود، پسوند آنها ascx است (همانند User controls در وب فرمها).
البته چون در حالت استفاده از موتور Razor، پسوند View و Partial viewها یکی است، مرسوم شده است که نام Partial viewها را با یک underline شروع کنیم تا بتوان بین این دو تمایز قائل شد.
اگر این فایلها را در پوشه Views/Shared تعریف کنیم، در تمام Viewها قابل استفاده خواهند بود. اما اگر مثلا در پوشه Views/Home آنهارا قرار دهیم، تنها در Viewهای متعلق به کنترلر Home، قابل بکارگیری میباشند.
Partial views را نیز میتوان strongly typed تعریف کرد و به این ترتیب با مشخص سازی دقیق نوع model آن، علاوه بر بهرهمندی از Intellisense خودکار، رندر آنرا نیز تحت کنترل کامپایلر قرار داد.
مقدار Model در یک View بر اساس اطلاعات مدلی که به آن ارسال شده است تعیین میگردد. اما در یک Partial view که جزئی از یک View را نهایتا تشکیل خواهد داد، بر اساس مقدار ارسالی از طریق View معین میگردد.
یک مثال
در ادامه قصد داریم کد حلقه نمایش لیستی از عناصر تولید شده توسط VS.NET را به یک Partial view منتقل و Refactor کنیم.
ابتدا یک منبع داده فرضی زیر را در نظر بگیرید:
using System;
using System.Collections.Generic;
namespace MvcApplication8.Models
{
public class Employees
{
public IList<Employee> CreateEmployees()
{
return new[]
{
new Employee { Id = 1, AddDate = DateTime.Now.AddYears(-3), Name = "Emp-01", Salary = 3000},
new Employee { Id = 2, AddDate = DateTime.Now.AddYears(-2), Name = "Emp-02", Salary = 2000},
new Employee { Id = 3, AddDate = DateTime.Now.AddYears(-1), Name = "Emp-03", Salary = 1000}
};
}
}
}
سپس از آن در یک کنترلر برای بازگشت لیستی از کارکنان استفاده خواهیم کرد:
public ActionResult EmployeeList()
{
var list = new Employees().CreateEmployees();
return View(list);
}
View متناظر با این متد را هم با کلیک راست بر روی متد، انتخاب گزینه Add view و سپس ایجاد یک strongly typed view از نوع کلاس Employee، ایجاد خواهیم کرد.
در ادامه قصد داریم بدنه حلقه زیر را refactor کنیم و آنرا به یک Parial view منتقل نمائیم تا View ما اندکی خلوتتر و مفهومتر شود:
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Salary)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Html.DisplayFor(modelItem => item.IsMale)
</td>
<td>
@Html.DisplayFor(modelItem => item.AddDate)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
@Html.ActionLink("Details", "Details", new { id=item.Id }) |
@Html.ActionLink("Delete", "Delete", new { id=item.Id })
</td>
</tr>
}
سپس بر روی پوشه Views/Employee کلیک راست کرده و گزینه Add|View را انتخاب کنید. در اینجا نام _EmployeeItem را وارد کرده و همچنین گزینه Create as a partial view و create a strongly typed view را نیز انتخاب کنید. نوع مدل هم Employee خواهد بود. به این ترتیب فایل زیر تشکیل خواهد شد:
\Views\Employee\_EmployeeItem.cshtml
ابتدای نام فایلرا با underline شروع کردهایم تا بتوان بین Viewها و Partial views تفاوت قائل شد. همچنین این Partial view چون داخل پوشه Employee تعریف شده، فقط در Viewهای کنترلر Employee در دسترس خواهد بود.
در ادامه کل بدنه حلقه فوق را cut کرده و در این فایل جدید paste نمائید. مرحله اول refactoring یک view به همین نحو آغاز میشود. البته در این حالت قادر به استفاده از Partial view نخواهیم بود چون اطلاعاتی که به این فایل ارسال میگردد و مدلی که در دسترس آن است از نوع Employee است و نه لیستی از کارمندان. به همین جهت باید item را با Model جایگزین کرد:
@model MvcApplication8.Models.Employee
<tr>
<td>
@Html.DisplayFor(x => x.Name)
</td>
<td>
@Html.DisplayFor(x => x.Salary)
</td>
<td>
@Html.DisplayFor(x => x.Address)
</td>
<td>
@Html.DisplayFor(x => x.IsMale)
</td>
<td>
@Html.DisplayFor(x => x.AddDate)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
@Html.ActionLink("Details", "Details", new { id = Model.Id }) |
@Html.ActionLink("Delete", "Delete", new { id = Model.Id })
</td>
</tr>
سپس برای استفاده از این Partial view در صفحه نمایش لیست کارمندان خواهیم داشت:
@foreach (var item in Model) {
@Html.Partial("_EmployeeItem", item)
}
متد Html.Partial، اطلاعات یک Partial view را پردازش و تبدیل به یک رشته کرده و در اختیار Razor قرار میدهد تا در صفحه نمایش داده شود. پارامتر اول آن نام Partial view مورد نظر است (نیازی به ذکر پسوند فایل نیست) و پارامتر دوم، اطلاعاتی است که به آن ارسال خواهد شد.
متد دیگری هم وجود دارد به نام Html.RenderPartial. کار این متد نوشتن مستقیم در Response است، برخلاف Html.Partial که فقط یک رشته را بر میگرداند.
نمایش اطلاعات از کنترلرهای مختلف در یک صفحه
Html.Partial بر اساس اطلاعات مدل ارسالی از یک کنترلر، کار رندر قسمتی از آنرا در یک View خاص عهده دار خواهد شد. اما اگر بخواهیم مثلا در یک صفحه یک قسمت را به نمایش آخرین اخبار و یک قسمت را به نمایش آخرین وضعیت آب و هوا اختصاص دهیم، از روش دیگری به نام RenderAction میتوان کمک گرفت. در اینجا هم دو متد Html.Action و Html.RenderAction وجود دارند. اولی یک رشته را بر میگرداند و دومی اطلاعات را مستقیما در Response درج میکند.
یک مثال:
کنترلر جدیدی را به نام MenuController به پروژه اضافه کنید:
using System.Web.Mvc;
namespace MvcApplication8.Controllers
{
public class MenuController : Controller
{
[ChildActionOnly]
public ActionResult ShowMenu(string options)
{
return PartialView(viewName: "_ShowMenu", model: options);
}
}
}
سپس بر روی نام متد کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا قصد داریم یک partial view که نامش با underline شروع میشود را اضافه کنیم. مثلا با محتوای زیر ( با توجه به اینکه مدل ارسالی از نوع رشتهای است):
@model string
<ul>
<li>
@Model
</li>
</ul>
حین فراخوانی متد Html.Action، یک متد در یک کنترلر فراخوانی خواهد شد (که شامل ارائه درخواست و طی سیکل کامل پردازشی آن کنترلر نیز خواهد بود). سپس آن متد با بازگشت دادن یک PartialView، اطلاعات پردازش شده یک partial view را به فراخوان بازگشت میدهد. اگر نامی ذکر نشود، همان نام متد در نظر گرفته خواهد شد. البته از آنجائیکه در این مثال در ابتدای نام Partial view یک underline قرار دادیم، نیاز خواهد بود تا این نام صریحا ذکر گردد (چون دیگر هم نام متد یا ActionName آن نیست). ویژگی ChildActionOnly سبب میشود تا این متد ویژه تنها از طریق فراخوانی Html.Action در دسترس باشد.
برای استفاده از آن هم در Viewایی دیگر خواهیم داشت:
@Html.Action(actionName: "ShowMenu", controllerName: "Menu",
routeValues: new { options = "some data..." })
در اینجا هم پارامتر ارسالی به کمک anonymously typed objects مشخص و مقدار دهی شده است.
سؤال مهم: چه تفاوتی بین RenderPartial و RenderAction وجود دارد؟ به نظر هر دو یک کار را انجام میدهند، هر دو مقداری HTML را پس از پرداش به صفحه تزریق میکنند.
پاسخ: اگر View والد، دارای کلیه اطلاعات لازم جهت نمایش اطلاعات Partial view است، از RenderPartial استفاده کنید. به این ترتیب برخلاف حالت RenderAction درخواست جدیدی به ASP.NET Pipeline صادر نشده و کارآیی نهایی بهتر خواهد بود. صرفا یک الحاق ساده به صفحه انجام خواهد شد.
اما اگر برای رندر کردن این قسمت از صفحه که قرار است اضافه شود، نیاز به دریافت اطلاعات دیگری خارج از اطلاعات مهیا میباشد، از روش RenderAction استفاده کنید. برای مثال اگر در صفحه جاری قرار است لیست پروژهها نمایش داده شود و در کنار صفحه مثلا منوی خاصی باید قرار گیرد، اطلاعات این منو در View جاری فراهم نیست (و همچنین مرتبط به آن هم نیست). بنابراین از روش RenderAction برای حل این مساله میتوان کمک گرفت.
به صورت خلاصه برای نمایش اطلاعات تکراری در صفحات مختلف سایت در حالتیکه این اطلاعات از قسمتهای دیگر صفحه ایزوله است (مثلا نمایش چند ویجت مختلف در صفحه)، روش RenderAction ارجحیت دارد.
یک نکته
فراخوانی متدهای RenderAction و RenderPartial در حین کار با Razor باید به شکل فراخوانی یک متد داخل {} باشند:
@{ Html.RenderAction("About"); }
And not @Html.RenderAction("About")
علت این است که @ به تنهایی به معنای نوشتن در Response است. متد RenderAction هم خروجی ندارد و مستقیما در Response اطلاعات خودش را درج میکند. بنابراین این دو با هم همخوانی ندارند و باید به شکل یک متد معمولی با آن رفتار کرد.
اگر حجم اطلاعاتی که قرار است در صفحه درج شود بالا است، متدهای RenderAction و RenderPartial نسبت به Html.Action و Html.Partial کارآیی بهتری دارند؛ چون یک مرحله تبدیل کل اطلاعات به رشته و سپس درج نتیجه در Response، در آنها حذف شده است.
دات نت 7 به همراه دو متد جدید Order و OrderDescending است که مرتب سازی مجموعههای ساده را انجام میدهند.
روش متداول مرتب سازی مجموعههای ساده تا پیش از دات نت 7
فرض کنید لیستی از اعداد را داریم:
تا پیش از دات نت 7 با استفاده از متدهای OrderBy و OrderByDescending موجود به همراه LINQ، امکان مرتب سازی صعودی و نزولی این لیست وجود دارد:
که در اینجا ذکر پارامتر keySelector ضروری است:
هرچند میشد طراحی آن سادهتر باشد و حداقل برای مجموعههای ساده، نیازی به ذکر آن نباشد.
روش جدید مرتب سازی مجموعههای ساده در دات نت 7
دات نت 7 به همراه دو متد جدید Order و OrderDescending است که دیگر نیازی به ذکر پارامتر keySelector ذکر شده را ندارند:
و امضای آنها به صورت زیر است:
که در حقیقت دو متد الحاقی جدید قابل اعمال بر روی انواع و اقسام IEnumerableها هستند.
در مورد سایر مجموعههای پیچیده چطور؟
فرض کنید کلاس User را:
به همراه لیستی از آن تعریف کردهایم:
سؤال: آیا اگر متد Order را بر روی این لیست فراخوانی کنیم:
برای مثال این مجموعه بر اساس نام و سن مرتب خواهد شد؟ که پاسخ آن خیر است و همچنین استثنائی را صادر میکند بر این مبنا که باید کلاس User، اینترفیس IComparable را پیاده سازی کند تا بتوان آنها را مقایسه کرد؛ برای مثال چیزی شبیه به تغییرات زیر:
که در یک چنین مواردی شاید بهتر باشد از همان متد OrderBy پیشین استفاده کرد که الزامی به پیاده سازی اینترفیس IComparable را ندارد:
روش متداول مرتب سازی مجموعههای ساده تا پیش از دات نت 7
فرض کنید لیستی از اعداد را داریم:
var numbers = new List<int> { -7, 1, 5, -6 };
var sortedNumbers1 = numbers.OrderBy(n => n); var sortedNumbers2 = numbers.OrderByDescending(n => n);
public static IOrderedEnumerable<TSource> OrderBy<TSource,TKey>( [NotNull] this IEnumerable<TSource> source, [NotNull] Func<TSource,TKey> keySelector)
روش جدید مرتب سازی مجموعههای ساده در دات نت 7
دات نت 7 به همراه دو متد جدید Order و OrderDescending است که دیگر نیازی به ذکر پارامتر keySelector ذکر شده را ندارند:
var sortedNumbers3 = numbers.Order(); var sortedNumbers4 = numbers.OrderDescending();
public static IOrderedEnumerable<T> Order<T>(this IEnumerable<T> source) public static IOrderedEnumerable<T> OrderDescending<T>(this IEnumerable<T> source)
در مورد سایر مجموعههای پیچیده چطور؟
فرض کنید کلاس User را:
public class User { public string Name { set; get; } public int Age { set; get; } }
List<User> users = new() { new User { Name = "User 1", Age = 34 }, new User { Name = "User 2", Age = 24 }, };
var orderedUsers = users.Order();
public class User : IComparable<User> { public string Name { set; get; } public int Age { set; get; } public int CompareTo(User? other) { if (ReferenceEquals(this, other)) { return 0; } if (ReferenceEquals(null, other)) { return 1; } var nameComparison = string.Compare(Name, other.Name, StringComparison.Ordinal); if (nameComparison != 0) { return nameComparison; } return Age.CompareTo(other.Age); } }
var orderedUsers2 = users.OrderBy(user => user.Name).ThenBy(user => user.Age);
مطالب دورهها
نحوه برقراری ارتباطات بین صفحات، سیستم راهبری و ViewModelها در قالب پروژه WPF Framework
هدف از قالب پروژه WPF Framework ایجاد یک پایه، برای شروع سریع یک برنامه تجاری WPF جدید است. بنابراین فرض کنید که این قالب، هم اکنون در اختیار شما است و قصد دارید یک صفحه جدید، مثلا تغییر مشخصات کاربری را به آن اضافه کنید. کدهای کامل این قابلیت هم اکنون در قالب پروژه موجود است و به این ترتیب توضیح جزئیات روابط آن در اینجا سادهتر خواهد بود.
1) ایجاد صفحه تغییر مشخصات کاربر
کلیه Viewهای برنامه، در پروژه ریشه، ذیل پوشه Views اضافه خواهند شد. همچنین چون در آینده تعداد این فایلها افزایش خواهند یافت، بهتر است جهت مدیریت آنها، به ازای هر گروه از قابلیتها، یک پوشه جدید را ذیل پوشه Views اضافه کرد.
همانطور که ملاحظه میکنید در اینجا پوشه UserInfo به همراه یک فایل جدید XAML به نام ChangeProfile.xaml، ذیل پوشه Views پروژه ریشه اصلی اضافه شدهاند.
ChangeProfile.xaml از نوع Page است؛ از این جهت که اگر به فایل MainWindow.xaml که سیستم راهبری برنامه در آن تعبیه شده است مراجعه کنید، یک چنین تعریفی را ملاحظه خواهید نمود:
سورس کامل کنترل سفارشی FrameFactory.cs را در پروژه Infrastructure برنامه میتوانید مشاهده کنید. FrameFactory در حقیقت یک کنترل Frame استاندارد است که مباحث تزریق وابستگیها و همچنین راهبری خودکار سیستم در آن تعریف شدهاند.
مرحله بعد، تعریف محتویات فایل ChangeProfile.xaml است. در این فایل اطلاعات انقیاد دادهها از ViewModel مرتبط که در ادامه توضیح داده خواهد شد دریافت میگردد. مثلا مقدار خاصیت ChangeProfileData.Password، از ViewModel به صورت خودکار تغذیه خواهد شد.
در این فایل یک سری DynamicResource را هم برای تعریف دکمههای سبک مترو ملاحظه میکنید. کلیدهای متناظر با آن در فایل Icons.xaml که در فایل App.xaml برای کل برنامه ثبت شده است، تامین میگردد.
2) تنظیم اعتبارسنجی صفحه اضافه شده
پس از اینکه صفحه جدید اضافه شد، نیاز است وضعیت دسترسی به آن مشخص شود:
برای این منظور به فایل code behind این صفحه یعنی ChangeProfile.xaml.cs مراجعه و تنها، ویژگی فوق را به آن اضافه خواهیم کرد. ویژگی PageAuthorization به صورت خودکار توسط فریم ورک تهیه شده خوانده و اعمال خواهد شد. برای نمونه در اینجا کلیه کاربران اعتبارسنجی شده در سیستم میتوانند مشخصات کاربری خود را تغییر دهند.
در مورد نحوه تعیین نقشهای متفاوت در صورت نیاز، در قسمت قبل بحث گردید.
3) تغییر منوی برنامه جهت اشاره به صفحه جدید
خوب، ما تا اینجا یک صفحه جدید را تهیه کردهایم. در مرحله بعد باید مدخلی را در منوی برنامه جهت اشاره به آن تهیه کنیم.
منوی برنامه در فایل MainMenu.xaml قرار دارد. اطلاعات متناظر با دکمه ورود به صفحه تغییر مشخصات کاربری نیز به شکل ذیل تعریف شده است:
به ازای هر صفحه جدیدی که تعریف میشود تنها کافی است CommandParameter ایی مساوی مسیر فایل XAML مورد نظر، در فایل منوی برنامه قید شود. منوی اصلی دارای ViewModel ایی است به نام MainMenuViewModel.cs که در پروژه Infrastructure پیشتر تهیه شده است.
در این ViewModel تعاریف DoNavigate و پردازش پارامتر دریافتی به صورت خودکار صورت خواهد گرفت و سورس کامل آن در اختیار شما است. بنابراین تنها کافی است CommandParameter را مشخص کنید، DoNavigate کار هدایت به آنرا انجام خواهد داد.
4) ایجاد ViewModel متناظر با صفحه
مرحله آخر افزودن یک صفحه، تعیین ViewModel متناظر با آن است. عنوان شد که اطلاعات مورد نیاز جهت عملیات Binding در این فایل قرار میگیرند و اگر به فایل ChangeProfileViewModel.cs مراجعه کنید (نام آن مطابق قرارداد، یک کلمه ViewModel را نسبت به نام View متناظر بیشتر دارد)، چنین خاصیت عمومی را در آن خواهید یافت.
مطابق قراردادهای توکار قالب تهیه شده:
- نیاز است ViewModel تعریف شده از کلاس پایه BaseViewModel مشتق شود تا علاوه بر تامین یک سری کدهای تکراری مانند:
سبب شناسایی این کلاس به عنوان ViewModel و برقرار تزریق وابستگیهای خودکار در سازنده آن نیز گردد.
- پس از اضافه شدن کلاس پایه BaseViewModel نیاز است تکلیف خاصیت public override bool ViewModelContextHasChanges را نیز مشخص کنید. در اینجا به سیستم راهبری اعلام میکنید که آیا در ViewModel جاری تغییرات ذخیره نشدهای وجود دارند؟ فقط باید true یا false را بازگردانید. برای مثال خاصیت uow.ContextHasChanges برای این منظور بسیار مناسب است و از طریق پیاده سازی الگوی واحد کار به صورت خودکار چنین اطلاعاتی را در اختیار برنامه قرار میدهد.
در ViewModelها هرجایی که نیاز به اطلاعات کاربر وارد شده به سیستم داشتید، از اینترفیس IAppContextService در سازنده کلاس ViewModel جاری استفاده کنید. اینترفیس IUnitOfWork امکانات ذخیره سازی اطلاعات و همچنین مشخص سازی وضعیت Context جاری را در اختیار شما قرار میدهد.
کلیه کدهای کار کردن با یک موجودیت باید در کلاس سرویس متناظر با آن قرار گیرند و این کلاس سرویس توسط اینترفیس آن مانند IUsersService در اینجا باید توسط سازنده کلاس در اختیار ViewModel قرار گیرد.
تزریق وابستگیها در اینجا خودکار بوده و تنظیمات آن در فایل IocConfig.cs پروژه Infrastructure قرار دارد. این کلاس آنچنان نیازی به تغییر ندارد؛ اگر پیش فرضهای نامگذاری آنرا مانند کلاسهای Test و اینترفیسهای ITest، در لایه سرویس برنامه رعایت شوند.
1) ایجاد صفحه تغییر مشخصات کاربر
کلیه Viewهای برنامه، در پروژه ریشه، ذیل پوشه Views اضافه خواهند شد. همچنین چون در آینده تعداد این فایلها افزایش خواهند یافت، بهتر است جهت مدیریت آنها، به ازای هر گروه از قابلیتها، یک پوشه جدید را ذیل پوشه Views اضافه کرد.
همانطور که ملاحظه میکنید در اینجا پوشه UserInfo به همراه یک فایل جدید XAML به نام ChangeProfile.xaml، ذیل پوشه Views پروژه ریشه اصلی اضافه شدهاند.
ChangeProfile.xaml از نوع Page است؛ از این جهت که اگر به فایل MainWindow.xaml که سیستم راهبری برنامه در آن تعبیه شده است مراجعه کنید، یک چنین تعریفی را ملاحظه خواهید نمود:
<CustomControls:FrameFactory x:Name="ActiveScreen" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" NavigationUIVisibility="Hidden" Grid.Column="1" Margin="0" />
مرحله بعد، تعریف محتویات فایل ChangeProfile.xaml است. در این فایل اطلاعات انقیاد دادهها از ViewModel مرتبط که در ادامه توضیح داده خواهد شد دریافت میگردد. مثلا مقدار خاصیت ChangeProfileData.Password، از ViewModel به صورت خودکار تغذیه خواهد شد.
در این فایل یک سری DynamicResource را هم برای تعریف دکمههای سبک مترو ملاحظه میکنید. کلیدهای متناظر با آن در فایل Icons.xaml که در فایل App.xaml برای کل برنامه ثبت شده است، تامین میگردد.
2) تنظیم اعتبارسنجی صفحه اضافه شده
پس از اینکه صفحه جدید اضافه شد، نیاز است وضعیت دسترسی به آن مشخص شود:
/// <summary> /// تغییر مشخصات کاربر جاری /// </summary> [PageAuthorization(AuthorizationType.FreeForAuthenticatedUsers)] public partial class ChangeProfile
در مورد نحوه تعیین نقشهای متفاوت در صورت نیاز، در قسمت قبل بحث گردید.
3) تغییر منوی برنامه جهت اشاره به صفحه جدید
خوب، ما تا اینجا یک صفحه جدید را تهیه کردهایم. در مرحله بعد باید مدخلی را در منوی برنامه جهت اشاره به آن تهیه کنیم.
منوی برنامه در فایل MainMenu.xaml قرار دارد. اطلاعات متناظر با دکمه ورود به صفحه تغییر مشخصات کاربری نیز به شکل ذیل تعریف شده است:
<Button Style="{DynamicResource MetroCircleButtonStyle}" Height="55" Width="55" Command="{Binding DoNavigate}" CommandParameter="\Views\UserInfo\ChangeProfile.xaml" Margin="2"> <Rectangle Width="28" Height="17.25"> <Rectangle.Fill> <VisualBrush Stretch="Fill" Visual="{StaticResource appbar_user_tie}" /> </Rectangle.Fill> </Rectangle> </Button>
در این ViewModel تعاریف DoNavigate و پردازش پارامتر دریافتی به صورت خودکار صورت خواهد گرفت و سورس کامل آن در اختیار شما است. بنابراین تنها کافی است CommandParameter را مشخص کنید، DoNavigate کار هدایت به آنرا انجام خواهد داد.
4) ایجاد ViewModel متناظر با صفحه
مرحله آخر افزودن یک صفحه، تعیین ViewModel متناظر با آن است. عنوان شد که اطلاعات مورد نیاز جهت عملیات Binding در این فایل قرار میگیرند و اگر به فایل ChangeProfileViewModel.cs مراجعه کنید (نام آن مطابق قرارداد، یک کلمه ViewModel را نسبت به نام View متناظر بیشتر دارد)، چنین خاصیت عمومی را در آن خواهید یافت.
مطابق قراردادهای توکار قالب تهیه شده:
- نیاز است ViewModel تعریف شده از کلاس پایه BaseViewModel مشتق شود تا علاوه بر تامین یک سری کدهای تکراری مانند:
public abstract class BaseViewModel : DataErrorInfoBase, INotifyPropertyChanged, IViewModel
- پس از اضافه شدن کلاس پایه BaseViewModel نیاز است تکلیف خاصیت public override bool ViewModelContextHasChanges را نیز مشخص کنید. در اینجا به سیستم راهبری اعلام میکنید که آیا در ViewModel جاری تغییرات ذخیره نشدهای وجود دارند؟ فقط باید true یا false را بازگردانید. برای مثال خاصیت uow.ContextHasChanges برای این منظور بسیار مناسب است و از طریق پیاده سازی الگوی واحد کار به صورت خودکار چنین اطلاعاتی را در اختیار برنامه قرار میدهد.
در ViewModelها هرجایی که نیاز به اطلاعات کاربر وارد شده به سیستم داشتید، از اینترفیس IAppContextService در سازنده کلاس ViewModel جاری استفاده کنید. اینترفیس IUnitOfWork امکانات ذخیره سازی اطلاعات و همچنین مشخص سازی وضعیت Context جاری را در اختیار شما قرار میدهد.
کلیه کدهای کار کردن با یک موجودیت باید در کلاس سرویس متناظر با آن قرار گیرند و این کلاس سرویس توسط اینترفیس آن مانند IUsersService در اینجا باید توسط سازنده کلاس در اختیار ViewModel قرار گیرد.
تزریق وابستگیها در اینجا خودکار بوده و تنظیمات آن در فایل IocConfig.cs پروژه Infrastructure قرار دارد. این کلاس آنچنان نیازی به تغییر ندارد؛ اگر پیش فرضهای نامگذاری آنرا مانند کلاسهای Test و اینترفیسهای ITest، در لایه سرویس برنامه رعایت شوند.
نظرات مطالب
EF Code First #2
- این هم EF هست. یکی database first، یکی code first و یکی model first. ولی زیر ساخت همشون یکی هست.
- اکثر خطاهای EF به صورت inner exception است. یعنی صفحه نمایش استثناء رو باید باز کنید و کمی درخت نمایش داده شده را پیمایش کنید تا به inner exception برسید.
- ریز مسایل به روز رسانی بانک اطلاعاتی، در قسمتهای 4 و 5 این سری بررسی شده. عجله نکنید. قدم به قدم ...
- اکثر خطاهای EF به صورت inner exception است. یعنی صفحه نمایش استثناء رو باید باز کنید و کمی درخت نمایش داده شده را پیمایش کنید تا به inner exception برسید.
- ریز مسایل به روز رسانی بانک اطلاعاتی، در قسمتهای 4 و 5 این سری بررسی شده. عجله نکنید. قدم به قدم ...
- به احتمال زیاد مسیرهای فایل tsconfig.json را بر اساس پوشههای پروژهی خودتان وارد نکردهاید و با آن تطابقی ندارند. برای مثال تنظیم زیر در فایل tsconfig.json قرار گرفته در پوشهای که فایل angular-cli.json. هست :
به معنای وجود پوشهی src/app/core در پروژهی شما است که به نام مستعار app/core@ نگاشت شدهاست.
- برای نمونه نکات این مطلب به پروژه ASPNETCore2JwtAuthentication اعمال شدهاند و مشکلی در اجرای برنامه نیست.
{ "compilerOptions": { "baseUrl": "src", "paths": { "@app/core/*": [ "app/core/*" ], } } }
- برای نمونه نکات این مطلب به پروژه ASPNETCore2JwtAuthentication اعمال شدهاند و مشکلی در اجرای برنامه نیست.
تا اینجا دو مثالی را که از Mobx بررسی کردیم (مثال ورود متن و مثال کامپوننت شمارشگر)، به عمد به همراه decoratorهای @ دار آن نبودند. برای مثال در قسمت قبل، یک کلاس را با یک خاصیت ایجاد کردیم که روش مزین سازی خاصیت value آن را با observable decorator، توسط متد decorate انجام دادیم و این هم یک روش کار با MobX است؛ بدون اینکه نیاز به تنظیمات خاصی را داشته باشد:
اما اگر همان مثال بسیار سادهی ورود متن را بخواهیم توسط decoratorهای @ دار MobX پیاده سازی کنیم ... پروژهی استاندارد React ما کامپایل نخواهد شد که در این قسمت، روش رفع این مشکل را بررسی میکنیم.
بازنویسی مثال ورود متن و نمایش آن با Mobx decorators
در اینجا یک text-box، به همراه دو div در صفحه رندر خواهند شد که قرار است با ورود اطلاعاتی در text-box، یکی از آنها (text-display) این اطلاعات را به صورت معمولی و دیگری (text-display-uppercase) آنرا به صورت uppercase نمایش دهد. روش کار انجام شده هم مستقل از React است و به صورت مستقیم با استفاده از DOM API عمل شدهاست. این مثال را پیشتر در اولین قسمت بررسی MobX، ملاحظه کردید. اکنون اگر بخواهیم بجای شیءای که توسط متد observable کتابخانهی MobX محصور شدهاست:
از یک کلاس ES6 به همراه Mobx decorators استفاده کنیم، به یک چنین پروژهی جدیدی خواهیم رسید:
ابتدا یک پروژهی جدید React را ایجاد میکنیم:
در ادامه کتابخانهی mobx را نیز نصب میکنیم. برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
در ادامه، ابتدا فایل public\index.html را جهت نمایش دو div و یک text-box، ویرایش میکنیم:
سپس محتویات فایل src\index.js را نیز به نحو زیر تغییر میدهیم:
تنها تفاوت این نگارش با نگارش قبلی آن، استفاده از کلاس Text که یک کلاس ES6 به همراه MobX Decorators است، بجای یک شیء سادهی جاوا اسکریپتی میباشد. در اینجا خاصیت value به صورت observable@ تعریف شده و در نتیجهی تغییر مقدار آن در کدهای برنامه، خاصیت محاسباتی وابستهی به آن یا همان uppercase که با computed@ تزئین شده، به صورت خودکار به روز رسانی خواهد شد. متد autorun نیز به این تغییرات که حاصل فشرده شدن کلیدها هستند، واکنش نشان داده و متن دو div موجود در صفحه را به روز رسانی میکند.
اکنون اگر در همین حال، برنامه را با دستور npm start اجرا کنیم، با خطای زیر متوقف خواهیم شد:
راه حل اول: از Decorators استفاده نکنیم!
یک راه حل مشکل فوق این است که بدون هیچ تغییری در ساختار پروژهی React خود، اصلا از decorator syntax استفاده نکنیم. برای مثال اگر یک کلاس متداول MobX ای چنین شکلی را دارد:
میتوان آنرا بدون استفاده از decorator syntax، به صورت زیر نیز تعریف کرد:
نمونهی این روش را در قسمت قبل با تعریف شیء شمارشگر مشاهده کردهاید. در اینجا با توجه به اینکه Decorators در جاوا اسکریپت چیزی نیستند بجز بیان زیبای higher-order functions و higher-order functions هم توابعی هستند که توابع دیگر را با ارائهی قابلیتهای بیشتری، محصور میکنند، به همین جهت هر کاری را که بتوان با تزئین کنندهها انجام داد، همان را با توابع معمولی جاوا اسکریپتی نیز میتوان انجام داد. اینکار را در مثال فوق توسط متد decorate مشاهده میکنید. این متد ابتدا نوع کلاس خاصی را دریافت کرده و سپس در پارامتر دوم آن میتوان شیءای را تعریف کرد که خواص آن، همان خواص کلاس پارامتر اول است و مقادیر این خواص، تزئین کنندههایی هستند که قرار است برای آنها بکار گرفته شوند. مزیت این روش بدون تغییر باقی ماندن تعریف کلاس Timer در اینجا و همچنین انجام هیچگونه تغییری در ساختار پروژهی React، بدون نیاز به نصب بستههای کمکی اضافی است.
همچنین در این حالت بجای استفاده از کامپوننتهای کلاسی، باید از روش بکارگیری متد observer برای محصور کردن کامپوننت تابعی تعریف شده استفاده کرد (تا دیگر نیازی به ذکر observer class@ نباشد):
راه حل دوم: از تایپاسکریپت استفاده کنید!
create-react-app امکان ایجاد پروژههای React تایپاسکریپتی را با ذکر سوئیچ typescript نیز دارد:
پس از ایجاد پروژه، فایل tsconfig.json آنرا یافته و experimentalDecorators آنرا به true تنظیم کنید:
این تنها تغییری است که مورد نیاز میباشد و پس از آن برنامهی React جاری، بدون مشکلی میتواند با decorators کار کند.
فعالسازی MobX Decorators در پروژههای استاندارد React مبتنی بر ES6
MobX از legacy" decorators spec" پشتیبانی میکند. یعنی اگر پروژهای از spec جدید استفاده کند، دیگر نخواهد توانست با MobX فعلی کار کند. این هم مشکل MobX نیست. مشکل اینجا است که باید دانست کلا decorators در زبان جاوااسکریپت هنوز در مرحلهی آزمایشی قرار دارند و تکلیف spec نهایی و تائید شدهی آن مشخص نیست.
برای فعالسازی decorators در یک پروژهی React استاندارد مبتنی بر ES6، شاید کمی جستجو کنید و به نتایجی مانند افزودن فایل babelrc. به ریشهی پروژه و نصب افزونههایی مانند babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties@ برسید. اما ... اینها بدون اجرای دستور npm run eject کار نمیکنند و اگر این دستور را اجرا کنیم، در نهایت به یک فایل package.json بسیار شلوغ خواهیم رسید (اینبار ارجاعات به Babel، Webpack و تمام ابزارهای دیگر نیز ظاهر میشوند). همچنین این عملیات نیز یک طرفهاست. یعنی از این پس قرار است کنترل تمام این پشت صحنه، در اختیار ما باشد و به روز رسانیهای بعدی create-react-app را با مشکل مواجه میکند. این گزینه صرفا مختص توسعه دهندگان پیشرفتهی React است. به همین جهت نیاز به روشی را داریم تا بتوانیم تنظیمات Webpack و کامپایلر Babel را بدون اجرای دستور npm run eject، تغییر دهیم تا در نتیجه، decorators را در آن فعال کنیم و خوشبختانه پروژهی react-app-rewired دقیقا برای همین منظور طراحی شدهاست.
بنابراین ابتدا بستههای زیر را نصب میکنیم:
بستهی react-app-rewired، امکان بازنویسی تنظیمات webpack پروژهی react را بدون eject آن میسر میکند. customize-cra نیز با استفاده از امکانات همین بسته، نگارشهای جدیدتر create-react-app را پشتیبانی میکند.
پس از نصب این پیشنیازها، فایل جدید config-overrides.js را به ریشهی پروژه، جائیکه فایل package.json قرار گرفتهاست، با محتوای زیر اضافه کنید تا پشتیبانی ازlegacy" decorators spec" فعال شوند:
در ادامه فایل package.json را گشوده و قسمت scripts آنرا برای استفادهی از react-app-rewired، به صورت زیر بازنویسی کنید تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود:
پس از این تغییرات، نیاز است دستور npm start را یکبار دیگر از ابتدا اجرا کنید. اکنون برنامه بدون مشکل کامپایل شده و خروجی بدون خطایی در مرورگر نمایش داده خواهد شد.
تنظیمات ESLint مخصوص کار با decorators
فایل ویژهی eslintrc.json. که در ریشهی پروژه قرار میگیرد (این فایل بدون نام است و فقط از پسوند تشکیل شده)، برای پروژههای MobX، باید حداقل تنظیم زیر را داشته باشد تا ESLint بتواند legacyDecorators را نیز پردازش کند:
و یا یک نمونهی غنی شدهی فایل eslintrc.json. مخصوص برنامههای React به صورت زیر است:
البته برای اینکه این تنظیمات کار کند، باید افزونههای زیر را نیز به صورت محلی در ریشهی پروژهی جاری نصب کنید (این مورد از ESLint 6x به بعد اجباری است و از بستههای global استفاده نمیکند):
پس از آن میتوان فایل config-overrides.js را به صورت زیر نیز بر اساس تنظیمات فوق، بهبود بخشید:
رفع اخطار مرتبط با decorators در VSCode
تا اینجا کار تنظیم کامپایلر babel، جهت پردازش decorators انجام شد. اما خود VSCode نیز چنین اخطاری را در پروژههایی که از decorates استفاده میکنند، نمایش میدهد:
برای رفع آن، فایل جدید tsconfig.json را در ریشهی پروژه ایجاد کرده و آنرا به صورت زیر تکمیل کنید تا ادیتور تایپاسکریپتی VSCode، دیگر خطاهای مرتبط با decorators را نمایش ندهد:
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part3.zip
import { decorate } from "mobx"; class Count { value = 0; } decorate(Count, { value: observable }); const count = new Count();
بازنویسی مثال ورود متن و نمایش آن با Mobx decorators
در اینجا یک text-box، به همراه دو div در صفحه رندر خواهند شد که قرار است با ورود اطلاعاتی در text-box، یکی از آنها (text-display) این اطلاعات را به صورت معمولی و دیگری (text-display-uppercase) آنرا به صورت uppercase نمایش دهد. روش کار انجام شده هم مستقل از React است و به صورت مستقیم با استفاده از DOM API عمل شدهاست. این مثال را پیشتر در اولین قسمت بررسی MobX، ملاحظه کردید. اکنون اگر بخواهیم بجای شیءای که توسط متد observable کتابخانهی MobX محصور شدهاست:
const text = observable({ value: "Hello world!", get uppercase() { return this.value.toUpperCase(); } });
ابتدا یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app state-management-with-mobx-part3 > cd state-management-with-mobx-part3 > npm start
> npm install --save mobx
<!DOCTYPE html> <html lang="en"> <head> <title>MobX Basics, part 3</title> <meta charset="UTF-8" /> <link href="src/styles.css" /> </head> <body> <main> <input id="text-input" /> <p id="text-display"></p> <p id="text-display-uppercase"></p> </main> <script src="src/index.js"></script> </body> </html>
import { autorun, computed, observable } from "mobx"; const input = document.getElementById("text-input"); const textDisplay = document.getElementById("text-display"); const loudDisplay = document.getElementById("text-display-uppercase"); class Text { @observable value = "Hello World"; @computed get uppercase() { return this.value.toUpperCase(); } } const text = new Text(); input.addEventListener("keyup", event => { text.value = event.target.value; }); autorun(() => { input.value = text.value; textDisplay.textContent = text.value; loudDisplay.textContent = text.uppercase; });
اکنون اگر در همین حال، برنامه را با دستور npm start اجرا کنیم، با خطای زیر متوقف خواهیم شد:
./src/index.js SyntaxError: \src\index.js: Support for the experimental syntax 'decorators-legacy' isn't currently enabled (8:3): 6 | 7 | class Text { > 8 | @observable value = "Hello World"; | ^ 9 | @computed get uppercase() { 10 | return this.value.toUpperCase(); 11 | }
راه حل اول: از Decorators استفاده نکنیم!
یک راه حل مشکل فوق این است که بدون هیچ تغییری در ساختار پروژهی React خود، اصلا از decorator syntax استفاده نکنیم. برای مثال اگر یک کلاس متداول MobX ای چنین شکلی را دارد:
import { observable, computed, action } from "mobx"; class Timer { @observable start = Date.now(); @observable current = Date.now(); @computed get elapsedTime() { return this.current - this.start + "milliseconds"; } @action tick() { this.current = Date.now(); } }
import { observable, computed, action, decorate } from "mobx"; class Timer { start = Date.now(); current = Date.now(); get elapsedTime() { return this.current - this.start + "milliseconds"; } tick() { this.current = Date.now(); } } decorate(Timer, { start: observable, current: observable, elapsedTime: computed, tick: action });
همچنین در این حالت بجای استفاده از کامپوننتهای کلاسی، باید از روش بکارگیری متد observer برای محصور کردن کامپوننت تابعی تعریف شده استفاده کرد (تا دیگر نیازی به ذکر observer class@ نباشد):
const Counter = observer(({ count }) => { return ( // ... ); });
راه حل دوم: از تایپاسکریپت استفاده کنید!
create-react-app امکان ایجاد پروژههای React تایپاسکریپتی را با ذکر سوئیچ typescript نیز دارد:
> create-react-app my-proj1 --typescript
{ "compilerOptions": { // ... "experimentalDecorators": true } }
فعالسازی MobX Decorators در پروژههای استاندارد React مبتنی بر ES6
MobX از legacy" decorators spec" پشتیبانی میکند. یعنی اگر پروژهای از spec جدید استفاده کند، دیگر نخواهد توانست با MobX فعلی کار کند. این هم مشکل MobX نیست. مشکل اینجا است که باید دانست کلا decorators در زبان جاوااسکریپت هنوز در مرحلهی آزمایشی قرار دارند و تکلیف spec نهایی و تائید شدهی آن مشخص نیست.
برای فعالسازی decorators در یک پروژهی React استاندارد مبتنی بر ES6، شاید کمی جستجو کنید و به نتایجی مانند افزودن فایل babelrc. به ریشهی پروژه و نصب افزونههایی مانند babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties@ برسید. اما ... اینها بدون اجرای دستور npm run eject کار نمیکنند و اگر این دستور را اجرا کنیم، در نهایت به یک فایل package.json بسیار شلوغ خواهیم رسید (اینبار ارجاعات به Babel، Webpack و تمام ابزارهای دیگر نیز ظاهر میشوند). همچنین این عملیات نیز یک طرفهاست. یعنی از این پس قرار است کنترل تمام این پشت صحنه، در اختیار ما باشد و به روز رسانیهای بعدی create-react-app را با مشکل مواجه میکند. این گزینه صرفا مختص توسعه دهندگان پیشرفتهی React است. به همین جهت نیاز به روشی را داریم تا بتوانیم تنظیمات Webpack و کامپایلر Babel را بدون اجرای دستور npm run eject، تغییر دهیم تا در نتیجه، decorators را در آن فعال کنیم و خوشبختانه پروژهی react-app-rewired دقیقا برای همین منظور طراحی شدهاست.
بنابراین ابتدا بستههای زیر را نصب میکنیم:
> npm i --save-dev customize-cra react-app-rewired
پس از نصب این پیشنیازها، فایل جدید config-overrides.js را به ریشهی پروژه، جائیکه فایل package.json قرار گرفتهاست، با محتوای زیر اضافه کنید تا پشتیبانی ازlegacy" decorators spec" فعال شوند:
const { override, addDecoratorsLegacy, disableEsLint } = require("customize-cra"); module.exports = override( // enable legacy decorators babel plugin addDecoratorsLegacy(), // disable eslint in webpack disableEsLint() );
"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject" },
تنظیمات ESLint مخصوص کار با decorators
فایل ویژهی eslintrc.json. که در ریشهی پروژه قرار میگیرد (این فایل بدون نام است و فقط از پسوند تشکیل شده)، برای پروژههای MobX، باید حداقل تنظیم زیر را داشته باشد تا ESLint بتواند legacyDecorators را نیز پردازش کند:
{ "extends": "react-app", "parserOptions": { "ecmaFeatures": { "legacyDecorators": true } } }
{ "env": { "node": true, "commonjs": true, "browser": true, "es6": true, "mocha": true }, "settings": { "react": { "version": "detect" } }, "parserOptions": { "ecmaFeatures": { "jsx": true, "legacyDecorators": true }, "ecmaVersion": 2018, "sourceType": "module" }, "plugins": [ "babel", "react", "react-hooks", "react-redux", "no-async-without-await", "css-modules", "filenames", "simple-import-sort" ], "rules": { "no-const-assign": "warn", "no-this-before-super": "warn", "constructor-super": "warn", "strict": [ "error", "safe" ], "no-debugger": "error", "brace-style": [ "error", "1tbs", { "allowSingleLine": true } ], "no-trailing-spaces": "error", "keyword-spacing": "error", "space-before-function-paren": [ "error", "never" ], "spaced-comment": [ "error", "always" ], "vars-on-top": "error", "no-undef": "error", "no-undefined": "warn", "comma-dangle": [ "error", "never" ], "quotes": [ "error", "double" ], "semi": [ "error", "always" ], "guard-for-in": "error", "no-eval": "error", "no-with": "error", "valid-typeof": "error", "no-unused-vars": "error", "no-continue": "warn", "no-extra-semi": "warn", "no-unreachable": "warn", "no-unused-expressions": "warn", "max-len": [ "warn", 80, 4 ], "react/prefer-es6-class": "warn", "react/jsx-boolean-value": "warn", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "react/prop-types": "off", "react-redux/mapDispatchToProps-returns-object": "off", "react-redux/prefer-separate-component-file": "off", "no-async-without-await/no-async-without-await": "warn", "css-modules/no-undef-class": "off", "filenames/match-regex": [ "off", "^[a-zA-Z]+\\.*\\b(typescript|module|locale|validate|test|action|api|reducer|saga)?\\b$", true ], "filenames/match-exported": "off", "filenames/no-index": "off", "simple-import-sort/sort": "error" }, "extends": [ "react-app", "eslint:recommended", "plugin:react/recommended", "plugin:react-redux/recommended", "plugin:css-modules/recommended" ], "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly", "process": true } }
>npm i --save-dev eslint babel-eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-react eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort eslint-loader typescript
const { override, addDecoratorsLegacy, useEslintRc } = require("customize-cra"); module.exports = override( addDecoratorsLegacy(), useEslintRc(".eslintrc.json") );
رفع اخطار مرتبط با decorators در VSCode
تا اینجا کار تنظیم کامپایلر babel، جهت پردازش decorators انجام شد. اما خود VSCode نیز چنین اخطاری را در پروژههایی که از decorates استفاده میکنند، نمایش میدهد:
Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.ts(1219)
{ "compilerOptions": { "experimentalDecorators": true, "allowJs": true } }
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part3.zip
واکنش نشان دادن به تغییرات صفحات وب، قسمت مهم و عمدهای از کار توسعهی برنامههای وب را تشکیل میدهد. مفاهیم مرتبط با DOM events از زمان IE 4.0 و Netscape Navigator version 2 به مرورگرها اضافه شدند و به مرور تکامل یافتند. پیش از ظهور مرورگرهای مدرن (به IE 9.0 و مرورگرهای پیش از آن، مرورگرهای «باستانی» گفته میشود) به علت عدم هماهنگی آنها با استانداردهای وب و تفاوت روشهای رخدادگردانی، jQuery نقش مهمی را در زمینهی یکدست سازی کدهای مدیریت رخدادها در بین مرورگرهای مختلف ارائه کرد. اما با پیشرفتهای صورت گرفته و هماهنگی بیشتر مرورگرها با استانداردهای وب، دیگر نیازی به jQuery برای ارائهی کدهای یکدست رخدادگردانی نیست و کار مستقیم با API وب مرورگرها برای این منظور کافی است.
انواع رخدادها: بومی و سفارشی
دو رده بندی عمومی رخدادها در مرورگرها وجود دارند: بومی و سفارشی.
بومیها همانهایی هستند که در مستندات رسمی استانداردهای وب ذکر شدهاند؛ مانند click که توسط ماوس و یا صفحه کلید فعال میشود و یا load که در زمان بارگذاری کامل صفحه، تصاویر و یا یک iframe رخ میدهد.
رخدادهای سفارشی مواردی هستند که توسط یک کتابخانهی خاص و یا جهت یک برنامهی خاص تهیه شدهاند. مانند یک رخداد سفارشی که زمان شروع آپلود یک فایل را اعلام میکند.
رخدادهای سفارشی که بدون jQuery ایجاد و رخمیدهند، توسط jQuery نیز قابل بررسی و مدیریت هستند و نه برعکس. به عبارتی رخدادهای سفارشی ایجاد شدهی توسط jQuery غیراستاندارد بوده و صرفا مختص به API آن هستند.
در این بین، شیء استاندارد Event کار اتصال رخدادهای سفارشی و استاندارد را انجام میدهد. هر نوع رخداد DOM (سفارشی و یا بومی)، توسط یک شیء Event بیان میشود که آن نیز به همراه تعدادی خاصیت و متد، جهت مدیریت این رخداد است. برای مثال رخداد click دارای خاصیت type ایی به نام click است که در شیء Event متناظر با آن تعریف شدهاست.
انتشار رخدادها در صفحه
در روزهای آغازین وب، Netscape روش event capturing را برای انتشار رخدادها در صفحه ارائه داد و در مقابل آن IE روش event bubbling را معرفی کرد که متضاد یکدیگر بودند. در سال 2000 با ارائه استاندارد DOM Level 2 Events Specification، این وضعیت تغییر کرد و شامل هر دو مورد event capturing و event bubbling است و در حال حاضر تمام مرورگرهای مدرن این استاندارد را پیاده سازی کردهاند. بر اساس این استاندارد، زمانیکه رویدادی خلق میشود، فاز capturing آغاز میگردد که از شیء window شروع، سپس به شیء document منتشر میشود و این روند تا رسیدن به المانی که سبب بروز رخداد شدهاست ادامه پیدا میکند. پس از پایان فاز capturing، فاز جدید bubbling شروع میشود. در این فاز، رخداد از تمام والدین شیء هدف عبور میکند تا به شیء window برسد.
برای مثال اگر سند HTML ما چنین تعریفی را داشته باشد و بر روی المان «child of child of one» کلیک شده باشد:
این رخداد در فاز capturing از این المانها عبور میکند:
و در فاز Bubbling از این المانها:
هرچند به دلایل تاریخی و همچنین عدم پشتیبانی jQuery از فاز capturing، بیشتر از فاز Bubbling به صورت پیشفرض در رخدادگردانی استفاده میشود. همچنین صدور رخداد از المانی که آنرا ایجاد کردهاست، بیشتر منطقی به نظر میرسد تا عکس آن.
البته باید درنظر داشت که jQuery از روش ارائه شدهی توسط مرورگر برای فاز Bubbling استفاده نمیکند و این مسیر را خودش مجددا محاسبه و رخدادگردانهای این مسیر را به صورت دستی اجرا میکند. به همین جهت کارآیی آن نسبت به روش توکار و بومی مرورگرها کمتر است.
ایجاد رخدادهای DOM و صدور آنها در jQuery
برای نمایش ایجاد و صدور رخدادهای DOM با و بدون jQuery، از قطعه کد HTML زیر استفاده میکنیم:
jQuery به همراه دو متد trigger و triggerHandler برای ایجاد و انتشار رخدادها در طول DOM است. متد trigger سبب ایجاد، صدور و انتشار یک رخداد به تمام والدهای شیء صادر کنندهی رخداد میشود. نوع این انتشار نیز bubbling است. متد triggerHandler، فقط بر روی المانی که فراخوانی میشود، عمل کرده و سبب انتشار این رخداد از طریق bubbling نمیشود:
در این مثالها توسط متد trigger، به دو روش سبب submit یک فرم و همچنین در ابتدا سبب focus یک تکست باکس و سپس رفع آن شدهایم.
هرچند روش دومی نیز در jQuery API برای انجام همینکارها نیز پیش بینی شدهاست:
در اینجا به ازای هر رخداد، یک نام مستعار در jQuery API تدارک دیده شدهاست.
در ادامه فرض کنید یک دکمه داخل یک div قرار گرفتهاست و آن div نیز به همراه یک مدیریت کنندهی رخداد کلیک است. در این حالت اگر بخواهیم با کلیک بر روی دکمه سبب اجرای رویدادگردان div والد نشویم، میتوان از متد triggerHandler استفاده کرد:
ایجاد رخدادهای DOM و صدور آنها در جاوا اسکریپت (بدون استفاده از jQuery)
در web API مرورگرها، برای انجام بروز رخدادهای معادل مثالی که با jQuery مطرح شد، میتوان متدهای بومی متناظر با این رخدادها را بر روی المانها فراخوانی کرد:
قطعه کد فوق به علت استفادهی از querySelector، با IE 8.0 و تمام مرورگرهای پس از آن سازگار است.
متدهای توکار و بومی click ،focus و blur بر روی تمام عناصر DOM که از اینترفیس HTMLElement مشتق شده باشند، وجود دارند. متد submit فقط بر روی المانهایی از نوع <form> وجود دارد و قابل فراخوانی است.
باید دقت داشت که فراخوانی متدهای click و submit از نوع bubbling است؛ اما متدهای focus و blur خیر. از این جهت که این دو رخداد فاز capturing را سبب میشوند.
متدهای یاد شده را توسط سازندهی شیء Event و یا متد createEvent شیء document نیز میتوان ایجاد کرد. یکی از کاربردهای آن، ارائهی رفتاری سفارشی مانند triggerHandler جیکوئری است:
کار با سازندهی شیء Event در تمام مرورگرهای جدید، منهای IE (تمام نگارشهای آن) پشتیبانی میشود. در اینجا اگر این پشتیبانی وجود داشت، از خاصیت bubbles: false شیء Event استفاده میشود و اگر مرورگری قدیمی بود، از متد document.createEvent برای این منظور کمک گرفته میشود. در این حالت دومین پارامتر متد initEvent، همان bubbles است.
ایجاد و صدور رخدادهای سفارشی
فرض کنید در حال تهیهی کتابخانهای هستیم که افزودن و حذف آیتمها را به یک گالری عکس ارائه میدهد. میخواهیم روشی را در اختیار مصرف کننده قرار دهیم تا بتواند به این رخدادهای سفارشی (غیر استانداردی که جزو W3C نیستند) گوش فرا دهد.
در جیکوئری برای ایجاد رخدادهای سفارشی به صورت زیر عمل میشود:
در اینجا نیز صدور رخدادها همانند قبل و توسط همان متد trigger است. اما مشکلی که با آن وجود دارد این است که گوش فرا دهندهی به این رخداد نیز باید توسط جیکوئری ارائه شود و خارج از این کتابخانه قابل دریافت و پیگیری نیست.
در خارج از جیکوئری و توسط web API استاندارد مرورگرها ایجاد و صدور رخدادهای سفارشی به همراه bubbling آن به صورت زیر است:
البته باید بهخاطر داشته باشید این روش صرفا با مرورگرهای جدید (منهای تمام نگارشهای IE) کار میکند. در اینجا اگر نیاز به ارائهی راه حلی سازگار با IE نیز وجود داشت میتوان از document.createEvent استفاده کرد:
و اگر بخواهیم بررسی وجود IE و یا پشتیبانی از CustomEvent را نیز قید کنیم، به قطعه کد زیر خواهیم رسید که با تمام مرورگرهای موجود کار میکند:
گوش فرادادن به رخدادهای صادر شده، توسط jQuery
در جیکوئری با استفاده از متد on آن میتوان به تمام رخدادهای استاندارد و همچنین سفارشی گوش فرا داد:
در ادامه برای حذف تمام گوش فرا دهندههای به رخداد resize میتوان از متد off آن استفاده کرد:
اما مشکلی را که این روش به همراه دارد، از کار انداختن تمام قسمتهای دیگری است که هم اکنون به صدور این رخداد گوش فرا میدهند.
روش بهتر انجام اینکار، ذخیرهی ارجاعی به متدی است که قرار است این رویداد گردانی را انجام دهد:
و در آخر تنها این گوش فرا دهندهی خاص را در صورت نیاز غیرفعال میکنیم و نه تمام گوش فرادهندههای سراسر برنامه را.
همچنین اگر یک گوش فراهندهی به رخدادی تنها قرار است یکبار در طول عمر برنامه اجرا شود، میتوان از متد one استفاده کرد:
پس از یکبار اجرای رخدادگردان کلیک در اینجا، از کلیکهای بعدی صرفنظر خواهد شد.
گوش فرادادن به رخدادهای صادر شده، توسط جاوا اسکریپت خالص (یا همان web API مرورگرها)
ابتداییترین روش گوش فرادادن به رخدادها که از زمان آغاز معرفی آنها در دسترس بودهاست، روش تعریف inline آنها است:
در اینجا متد رویدادگردان به یکی از ویژگی المان انتساب داده میشود. مشکل این روش، نیاز به سراسری تعریف کردن متد handleButtonClick است و دیگر نمیتوان آنرا در فضای نامی خاص و یا سفارشی قرار داد.
روش دیگر ثبت رویدادگردان click، انتساب متد آن به خاصیت رخداد متناظری در آن المان ویژه است:
مزیت این روش، عدم نیاز به استفادهی از متدهای سراسری است.
البته باید دقت داشت که یکی از دو روش یاد شده را میتوانید استفاده کنید. در اینجا آخرین رویدادگردان متصل شدهی به المان، همواره تمام نمونههای موجود دیگر را بازنویسی میکند.
اگر نیاز به معرفی رویدادگردانهای متعددی برای یک المان در ماژولهای مختلف برنامه وجود داشت، از زمان IE 9.0 به بعد، متد addEventListener برای این منظور تدارک دیده شدهاست و syntax آن بسیار شبیه به متد on جیکوئری است:
در این حالت دیگر مشکل نیاز به متدهای سراسری و یا عدم امکان تعریف بیش از یک رویدادگردان خاص برای المانی مشخص، دیگر وجود ندارد.
برای نمونه معادل قطعه کد جیکوئری که پیشتر با متد on نوشتیم، با جاوا اسکریپت خالص به صورت زیر است:
در اینجا برای حذف یک رویدادگردان میتوان از متد removeEventListener استفاده کرد. تفاوت مهم آن با متد off جیکوئری این است که در اینجا حتما باید مشخص باشد کدام رویدادگردان را میخواهید حذف کنید:
یعنی روش حذف رویدادگردانها در اینجا شبیه به مثال دومی است که برای متد off جیکوئری ارائه کردیم. ابتدا باید ارجاعی را به متد رویدادگردان تهیه کنیم و سپس بر اساس این ارجاع، امکان حذف آن وجود خواهد داشت.
در اینجا حتی امکان تعریف متد one جیکوئری نیز پیش بینی شدهاست (البته جزو استانداردهای جدید وب از سال 2016 است):
اگر بخواهیم متد one سازگار با مرورگرهای قدیمیتر را نیز ارائه کنیم، چنین شکلی را پیدا میکند:
در اینجا پس از بروز رخداد، کار removeEventListener آن به صورت خودکار صورت میگیرد.
کنترل انتشار رخدادها
فرض کنید میخواهیم جلوی انتخاب المانهای صفحه مانند تصاویر و متن را توسط ماوس بگیریم. روش انجام اینکار با jQuery به صورت زیر است:
و یا توسط web API مرورگرها به این صورت:
مطابق «W3C DOM Level 3 Events specification» عملکرد پیشفرض رخداد mousedown با انتخاب متون و یا کشیدن و رها کردن المانها آغاز میشود. متد preventDefault یکی از متدهای شیء event است که به رویدادگردانهای تعریف شده ارسال میشود و توسط آن عملکرد پیشفرض آن رخداد لغو میشود.
برای جلوگیری کردن از انتشار رخدادی مانند click جهت رسیدن به سایر رویدادگردانهای ثبت شدهی در بین راه فاز bubbling، میتوان از متد stopPropagation استفاده کرد. روش انجام اینکار در جیکوئری:
البته jQuery صرفا فاز انتشار از نوع bubbling را پشتیبانی میکند.
و با web Api جهت جلوگیری از انتشار رخدادها در فاز capturing (این تنها راه مدیریت فاز capturing است):
و یا استفاده از web API برای جلوگیری از انتشار رخدادها در فاز bubbling:
البته باید درنظر داشت که متد stopPropagation از انتشار رخدادها به سایر گوش فرا دهندههای همان المان صادر کنندهی رخداد جلوگیری نمیکند. برای این منظور باید از متد stopImmediatePropagation استفاده کرد؛ در جیکوئری:
و توسط web API مرورگرها:
یک نکته: در این حالت اگر متد رویدادگردانی مقدار false را برگرداند، به معنای فراخوانی هر دوی متد preventDefault و stopPropagation است.
ارسال اطلاعات به رویدادگردانها
روش ارسال اطلاعات اضافی به رویداد گردانها در جیکوئری به صورت زیر است:
و رویدادگردان گوش فرا دهندهی به آن، به این نحو میتواند به filename دسترسی پیدا کند:
در اینجا دومین پارامتر تعریف شده، امکان دسترسی به تمام خواص سفارشی ارسالی را میسر میکند.
روش انجام اینکار با web API مرورگرها به صورت زیر است:
این روش با تمام مرورگرهای مدرن (منهای تمام نگارشهای IE) کار میکند و روش دسترسی به خاصیت detail سفارشی تعریف و ارسال شده، از طریق همان خاصیت event ارسالی به رویدادگردان است.
و اگر میخواهید از IE هم پشتیبانی کنید، روش جایگزین کردن شیء CustomEvent با createEvent به صورت زیر است:
متوجه شدن زمان بارگذاری یک شیء در صفحه
در حین توسعهی برنامههای وب، با این نوع سؤالات زیاد مواجه خواهید شد: چه زمانی تمام و یا بعضی از المانهای صفحه کاملا بارگذاری و رندر شدهاند؟
پاسخ به این نوع سؤالات در W3C UI Events specification توسط رویداد استاندارد load داده شدهاست.
- چه زمانی تمام المانهای موجود در صفحه کاملا بارگذاری و رندر شده و همچنین شیوهنامههای تعریف شده نیز به آنها اعمال گردیدهاند؟
روش انجام اینکار با jQuery:
و توسط web API بومی مرورگرها که بسیار مشابه نمونهی jQuery است:
- چه زمانی markup استاتیک صفحهی جاری در جای خود قرار گرفتهاند؟
اهمیت این موضوع، به دسترسی به زمان مناسب و امن ایجاد تغییرات در DOM بر میگردد. برای این منظور رویداد استاندارد DOMContentLoaded پیشبینی شدهاست که زودتر از رویداد load، در دسترس برنامه نویس قرار میگیرد. در جیکوئری توسط یکی از دو روش معروف زیر به رویداد یاد شده دسترسی خواهید داشت:
و معادل web API آن در تمام مرورگرهای جدید، همان تعریف رویدادگردانی برای DOMContentLoaded استاندارد است:
یک نکته: بهتر است این تعریف web API را پیش از تگهای <link> قرار دهید. زیرا بارگذاری آنها، اجرای هر نوع اسکریپتی را تا زمان پایان عملیات، سد میکند.
- چه زمانی المانی خاص در صفحه بارگذاری شدهاست و چه زمانی بارگذاری یک المان با شکست مواجه شدهاست؟
در جیکوئری توسط بررسی رویدادهای load و error میتوان به وضعیت نهایی بارگذاری المانهایی خاص دسترسی یافت:
روش انجام اینکار با web API مرورگرها نیز یکی است:
- جلوگیری از ترک اتفاقی صفحهی جاری
گاهی از اوقات نیاز است برای از جلوگیری از تخریب صفحهی جاری و از دست رفتن اطلاعات ذخیره نشدهی کاربر، اگر بر روی دکمهی close بالای صفحه کلیک کرد و یا کاربر به اشتباه به صفحهی دیگری هدایت شد، جلوی اینکار را بگیریم. برای این منظور رخداد استاندارد beforeunload درنظر گرفته شدهاست. روش استفادهی از این رویداد در جیکوئری:
و در web API مرورگرها:
در حالت web API بعضی از مرورگرها از نتیجهی متد استفاده میکنند و بعضی دیگر از مقدار خاصیت event.returnValue. به همین جهت هر دو مورد در اینجا مقدار دهی شدهاند.
انواع رخدادها: بومی و سفارشی
دو رده بندی عمومی رخدادها در مرورگرها وجود دارند: بومی و سفارشی.
بومیها همانهایی هستند که در مستندات رسمی استانداردهای وب ذکر شدهاند؛ مانند click که توسط ماوس و یا صفحه کلید فعال میشود و یا load که در زمان بارگذاری کامل صفحه، تصاویر و یا یک iframe رخ میدهد.
رخدادهای سفارشی مواردی هستند که توسط یک کتابخانهی خاص و یا جهت یک برنامهی خاص تهیه شدهاند. مانند یک رخداد سفارشی که زمان شروع آپلود یک فایل را اعلام میکند.
رخدادهای سفارشی که بدون jQuery ایجاد و رخمیدهند، توسط jQuery نیز قابل بررسی و مدیریت هستند و نه برعکس. به عبارتی رخدادهای سفارشی ایجاد شدهی توسط jQuery غیراستاندارد بوده و صرفا مختص به API آن هستند.
در این بین، شیء استاندارد Event کار اتصال رخدادهای سفارشی و استاندارد را انجام میدهد. هر نوع رخداد DOM (سفارشی و یا بومی)، توسط یک شیء Event بیان میشود که آن نیز به همراه تعدادی خاصیت و متد، جهت مدیریت این رخداد است. برای مثال رخداد click دارای خاصیت type ایی به نام click است که در شیء Event متناظر با آن تعریف شدهاست.
انتشار رخدادها در صفحه
در روزهای آغازین وب، Netscape روش event capturing را برای انتشار رخدادها در صفحه ارائه داد و در مقابل آن IE روش event bubbling را معرفی کرد که متضاد یکدیگر بودند. در سال 2000 با ارائه استاندارد DOM Level 2 Events Specification، این وضعیت تغییر کرد و شامل هر دو مورد event capturing و event bubbling است و در حال حاضر تمام مرورگرهای مدرن این استاندارد را پیاده سازی کردهاند. بر اساس این استاندارد، زمانیکه رویدادی خلق میشود، فاز capturing آغاز میگردد که از شیء window شروع، سپس به شیء document منتشر میشود و این روند تا رسیدن به المانی که سبب بروز رخداد شدهاست ادامه پیدا میکند. پس از پایان فاز capturing، فاز جدید bubbling شروع میشود. در این فاز، رخداد از تمام والدین شیء هدف عبور میکند تا به شیء window برسد.
برای مثال اگر سند HTML ما چنین تعریفی را داشته باشد و بر روی المان «child of child of one» کلیک شده باشد:
<!DOCTYPE html> <html> <head> <title>event propagation demo</title> </head> <body> <section> <h1>nested divs</h1> <div>one <div>child of one <div>child of child of one</div> </div> </div> </section> </body> </html>
1.window 2.document 3.<html> 4.<body> 5.<section> 6.<div>one 7.<div>child of one 8.<div>child of child of one
9.<div>child of child of one 10.<div>child of one 11.<div>one 12.<section> 13.<body> 14.<html> 15.document 16.window
البته باید درنظر داشت که jQuery از روش ارائه شدهی توسط مرورگر برای فاز Bubbling استفاده نمیکند و این مسیر را خودش مجددا محاسبه و رخدادگردانهای این مسیر را به صورت دستی اجرا میکند. به همین جهت کارآیی آن نسبت به روش توکار و بومی مرورگرها کمتر است.
ایجاد رخدادهای DOM و صدور آنها در jQuery
برای نمایش ایجاد و صدور رخدادهای DOM با و بدون jQuery، از قطعه کد HTML زیر استفاده میکنیم:
<div> <button type="button">do something</button> </div> <form method="POST" action="/user"> <label>Enter user name: <input name="user"> </label> <button type="submit">submit</button> </form>
// submits the form $('FORM').trigger('submit'); // submits the form by clicking the button $('BUTTON[type="submit"]').trigger('click'); // focuses the text input $('INPUT').trigger('focus'); // removes focus from the text input $('INPUT').trigger('blur');
هرچند روش دومی نیز در jQuery API برای انجام همینکارها نیز پیش بینی شدهاست:
// submits the form $('FORM').submit(); // submits the form by clicking the button $('BUTTON[type="submit"]').click(); // focuses the text input $('INPUT').focus(); // removes focus from the text input $('INPUT').blur();
در ادامه فرض کنید یک دکمه داخل یک div قرار گرفتهاست و آن div نیز به همراه یک مدیریت کنندهی رخداد کلیک است. در این حالت اگر بخواهیم با کلیک بر روی دکمه سبب اجرای رویدادگردان div والد نشویم، میتوان از متد triggerHandler استفاده کرد:
// clicks the first button - the click event does not bubble $('BUTTON[type="button"]').triggerHandler('click');
ایجاد رخدادهای DOM و صدور آنها در جاوا اسکریپت (بدون استفاده از jQuery)
در web API مرورگرها، برای انجام بروز رخدادهای معادل مثالی که با jQuery مطرح شد، میتوان متدهای بومی متناظر با این رخدادها را بر روی المانها فراخوانی کرد:
// submits the form document.querySelector('FORM').submit(); // submits the form by clicking the button document.querySelector('BUTTON[type="submit"]').click(); // focuses the text input document.querySelector('INPUT').focus(); // removes focus from the text input document.querySelector('INPUT').blur();
متدهای توکار و بومی click ،focus و blur بر روی تمام عناصر DOM که از اینترفیس HTMLElement مشتق شده باشند، وجود دارند. متد submit فقط بر روی المانهایی از نوع <form> وجود دارد و قابل فراخوانی است.
باید دقت داشت که فراخوانی متدهای click و submit از نوع bubbling است؛ اما متدهای focus و blur خیر. از این جهت که این دو رخداد فاز capturing را سبب میشوند.
متدهای یاد شده را توسط سازندهی شیء Event و یا متد createEvent شیء document نیز میتوان ایجاد کرد. یکی از کاربردهای آن، ارائهی رفتاری سفارشی مانند triggerHandler جیکوئری است:
var clickEvent; if (typeof Event === 'function') { clickEvent = new Event('click', {bubbles: false}); } else { clickEvent = document.createEvent('Event'); clickEvent.initEvent('click', false, true); } document.querySelector('BUTTON[type="button"]').dispatchEvent(clickEvent);
ایجاد و صدور رخدادهای سفارشی
فرض کنید در حال تهیهی کتابخانهای هستیم که افزودن و حذف آیتمها را به یک گالری عکس ارائه میدهد. میخواهیم روشی را در اختیار مصرف کننده قرار دهیم تا بتواند به این رخدادهای سفارشی (غیر استانداردی که جزو W3C نیستند) گوش فرا دهد.
در جیکوئری برای ایجاد رخدادهای سفارشی به صورت زیر عمل میشود:
// Triggers a custom "image-removed" element, // which bubbles up to ancestor elements. $libraryElement.trigger('image-removed', {id: 1});
در خارج از جیکوئری و توسط web API استاندارد مرورگرها ایجاد و صدور رخدادهای سفارشی به همراه bubbling آن به صورت زیر است:
var event = new CustomEvent('image-removed', { bubbles: true, detail: {id: 1} }); libraryElement.dispatchEvent(event);
var event = document.createEvent('CustomEvent'); event.initCustomEvent('image-removed', false, true, {id: 1}); libraryElement.dispatchEvent(event);
var event; // If the `CustomEvent` constructor function is not supported, // fall back to `createEvent` method. if (typeof CustomEvent === 'function') { event = new CustomEvent('image-removed', { bubbles: true, detail: {id: 1} }); } else { event = document.createEvent('CustomEvent'); event.initCustomEvent('image-removed', false, true, { id: 1 }); } libraryElement.dispatchEvent(event);
گوش فرادادن به رخدادهای صادر شده، توسط jQuery
در جیکوئری با استفاده از متد on آن میتوان به تمام رخدادهای استاندارد و همچنین سفارشی گوش فرا داد:
$(window).on('resize', function() { // react to new window size });
// remove all resize listeners - usually a bad idea $(window).off('resize');
روش بهتر انجام اینکار، ذخیرهی ارجاعی به متدی است که قرار است این رویداد گردانی را انجام دهد:
var resizeHandler = function() { // react to new window size }; $(window).on('resize', resizeHandler); // ...later // remove only our resize handler $(window).off('resize', resizeHandler);
همچنین اگر یک گوش فراهندهی به رخدادی تنها قرار است یکبار در طول عمر برنامه اجرا شود، میتوان از متد one استفاده کرد:
$(someElement).one('click', function() { // handle click event });
گوش فرادادن به رخدادهای صادر شده، توسط جاوا اسکریپت خالص (یا همان web API مرورگرها)
ابتداییترین روش گوش فرادادن به رخدادها که از زمان آغاز معرفی آنها در دسترس بودهاست، روش تعریف inline آنها است:
<button onclick="handleButtonClick()">click me</button>
روش دیگر ثبت رویدادگردان click، انتساب متد آن به خاصیت رخداد متناظری در آن المان ویژه است:
buttonEl.onclick = function() { // handle button click };
البته باید دقت داشت که یکی از دو روش یاد شده را میتوانید استفاده کنید. در اینجا آخرین رویدادگردان متصل شدهی به المان، همواره تمام نمونههای موجود دیگر را بازنویسی میکند.
اگر نیاز به معرفی رویدادگردانهای متعددی برای یک المان در ماژولهای مختلف برنامه وجود داشت، از زمان IE 9.0 به بعد، متد addEventListener برای این منظور تدارک دیده شدهاست و syntax آن بسیار شبیه به متد on جیکوئری است:
buttonEl.addEventListener('click', function() { // handle button click });
برای نمونه معادل قطعه کد جیکوئری که پیشتر با متد on نوشتیم، با جاوا اسکریپت خالص به صورت زیر است:
window.addEventListener('resize', function() { // react to new window size });
var resizeHandler = function() { // react to new window size }; window.addEventListener('resize', resizeHandler); // ...later // remove only our resize handler window.removeEventListener('resize', resizeHandler);
در اینجا حتی امکان تعریف متد one جیکوئری نیز پیش بینی شدهاست (البته جزو استانداردهای جدید وب از سال 2016 است):
someElement.addEventListener('click', function(event) { // handle click event }, { once: true });
var clickHandler = function() { // handle click event // ...then unregister handler someElement.removeEventListener('click', clickHandler); }; someElement.addEventListener('click', clickHandler);
کنترل انتشار رخدادها
فرض کنید میخواهیم جلوی انتخاب المانهای صفحه مانند تصاویر و متن را توسط ماوس بگیریم. روش انجام اینکار با jQuery به صورت زیر است:
$(window).on('mousedown', function(event) { event.preventDefault(); });
window.addEventListener('mousedown', function(event) { event.preventDefault(); });
برای جلوگیری کردن از انتشار رخدادی مانند click جهت رسیدن به سایر رویدادگردانهای ثبت شدهی در بین راه فاز bubbling، میتوان از متد stopPropagation استفاده کرد. روش انجام اینکار در جیکوئری:
$someElement.on('click', function(event) { event.stopPropagation(); });
و با web Api جهت جلوگیری از انتشار رخدادها در فاز capturing (این تنها راه مدیریت فاز capturing است):
// stop propagation during capturing phase someElement.addEventListener('click', function(event) { event.stopPropagation(); }, true);
// stop propagation during bubbling phase someElement.addEventListener('click', function(event) { event.stopPropagation(); });
$someElement.on('click', function(event) { event.stopImmediatePropagation(); });
someElement.addEventListener('click', function(event) { event.stopImmediatePropagation(); });
یک نکته: در این حالت اگر متد رویدادگردانی مقدار false را برگرداند، به معنای فراخوانی هر دوی متد preventDefault و stopPropagation است.
ارسال اطلاعات به رویدادگردانها
روش ارسال اطلاعات اضافی به رویداد گردانها در جیکوئری به صورت زیر است:
$uploaderElement.trigger('uploadError', { filename: 'picture.jpeg' });
$uploaderParent.on('uploadError', function(event, data) { showAlert('Failed to upload ' + data.filename); });
روش انجام اینکار با web API مرورگرها به صورت زیر است:
// send the failed filename w/ an error event var event = new CustomEvent('uploadError', { bubbles: true, detail: {filename: 'picture.jpeg'} }); uploaderElement.dispatchEvent(event); // ...and this is a listener for the event uploaderParent.addEventListener('uploadError', function(event) { showAlert('Failed to upload ' + event.detail.filename); });
و اگر میخواهید از IE هم پشتیبانی کنید، روش جایگزین کردن شیء CustomEvent با createEvent به صورت زیر است:
// send the failed filename w/ an error event var event = document.createEvent('CustomEvent'); event.initCustomEvent('uploadError', true, true, { filename: 'picture.jpeg' }); uploaderElement.dispatchEvent(event); // ...and this is a listener for the event uploaderParent.addEventListener('uploadError', function(event) { showAlert('Failed to upload ' + event.detail.filename); });
متوجه شدن زمان بارگذاری یک شیء در صفحه
در حین توسعهی برنامههای وب، با این نوع سؤالات زیاد مواجه خواهید شد: چه زمانی تمام و یا بعضی از المانهای صفحه کاملا بارگذاری و رندر شدهاند؟
پاسخ به این نوع سؤالات در W3C UI Events specification توسط رویداد استاندارد load داده شدهاست.
- چه زمانی تمام المانهای موجود در صفحه کاملا بارگذاری و رندر شده و همچنین شیوهنامههای تعریف شده نیز به آنها اعمال گردیدهاند؟
روش انجام اینکار با jQuery:
$(window).on('load', function() { // page is fully rendered });
window.addEventListener('load', function() { // page is fully rendered });
- چه زمانی markup استاتیک صفحهی جاری در جای خود قرار گرفتهاند؟
اهمیت این موضوع، به دسترسی به زمان مناسب و امن ایجاد تغییرات در DOM بر میگردد. برای این منظور رویداد استاندارد DOMContentLoaded پیشبینی شدهاست که زودتر از رویداد load، در دسترس برنامه نویس قرار میگیرد. در جیکوئری توسط یکی از دو روش معروف زیر به رویداد یاد شده دسترسی خواهید داشت:
$(document).ready(function() { // markup is on the page }); //or $(function() { // markup is on the page });
document.addEventListener('DOMContentLoaded', function() { // markup is on the page });
یک نکته: بهتر است این تعریف web API را پیش از تگهای <link> قرار دهید. زیرا بارگذاری آنها، اجرای هر نوع اسکریپتی را تا زمان پایان عملیات، سد میکند.
- چه زمانی المانی خاص در صفحه بارگذاری شدهاست و چه زمانی بارگذاری یک المان با شکست مواجه شدهاست؟
در جیکوئری توسط بررسی رویدادهای load و error میتوان به وضعیت نهایی بارگذاری المانهایی خاص دسترسی یافت:
$('IMG').on('load', function() { // image has successfully loaded }); $('IMG').on('error', function() { // image has failed to load });
document.querySelector('IMG').addEventListener('load', function() { // image has successfully loaded }); document.querySelector('IMG').addEventListener('error', function() { // image has failed to load });
- جلوگیری از ترک اتفاقی صفحهی جاری
گاهی از اوقات نیاز است برای از جلوگیری از تخریب صفحهی جاری و از دست رفتن اطلاعات ذخیره نشدهی کاربر، اگر بر روی دکمهی close بالای صفحه کلیک کرد و یا کاربر به اشتباه به صفحهی دیگری هدایت شد، جلوی اینکار را بگیریم. برای این منظور رخداد استاندارد beforeunload درنظر گرفته شدهاست. روش استفادهی از این رویداد در جیکوئری:
$(window).on('beforeunload', function() { return 'Are you sure you want to unload the page?'; });
window.addEventListener('beforeunload', function(event) { var message = 'Are you sure you want to unload the page?'; event.returnValue = message; return message; });