EF Core چیست؟
EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانکهای اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر میکند. توسط آن قسمت عمدهی کدهای مستقیم کار با بانکهای اطلاعاتی حذف شده و تبدیل به کدهای دات نتی میشوند. مزیت این لایهی Abstraction اضافی (لایهای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامهاست؛ بدون نیاز به تغییری در سایر قسمتهای برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر میکنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر میرسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئریها، رفتار بسیاری از بانکهای اطلاعاتی با آنها همانند رویههای ذخیره شده است که به عدم تولید Query planهای مجزایی به ازای هر کوئری رسیده منجر میشود که در نهایت سبب بالا رفتن سرعت اجرای کوئریها و مصرف حافظهی کمتری در سمت سرور بانک اطلاعاتی میگردد.
تفاوت EF Core با نگارشهای دیگر Entity framework در چیست؟
سورس باز بودن
EF از نگارشهای آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارشهای پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.
چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفادهی از آن در انواع و اقسام برنامههای دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامههای مختلف به شرح زیر است:
• All .NET application (Console, ASP.NET 4, WinForms, WPF) • Mac and Linux applications (Mono) • UWP (Universal Windows Platform) • ASP.NET Core applications • Can use EF Core in Windows phone and Windows store app
افزایش تعداد بانکهای اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانکهای اطلاعاتی رابطهای بود و همچنین مایکروسافت صرفا نگارشهای مختلف SQL Server را به صورت رسمی پشتیبانی میکرد و برای سایر بانکهای اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانکهای اطلاعاتی رابطهای، پشتیبانی از بانکهای اطلاعاتی NoSQL هم اضافه شدهاست؛ به همراه پروایدر ویژهای به نام In Memory جهت انجام سادهتر Unit testing. کاری که با نگارشهای پیشین EF به سادگی و از روز اول پشتیبانی نمیشد.
حذف و یا عدم پیاده سازی تعدادی از قابلیتهای EF 6.x
اگر موارد فوق جزو مهمترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگیها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامههای UWP نیاز دارد تمام نوعها را همانند زبانهای C و ++C، در زمان کامپایل بداند)، یک سری از قابلیتهای EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمیشوند. لیست کامل و به روز شدهی این موارد را در اینجا میتوانید مطالعه کنید.
بنابراین امکان انتقال برنامههای EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.
برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0
برای نصب تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بستهی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final", "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0" } }
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.EntityFrameworkCore.SqlServer.Design": { "version": "1.0.0", "type": "build" } }, "tools": { // same as before "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "imports": [ "portable-net45+win8" ] } } }
بنابراین از همین ابتدای کار، بدون مراجعهی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آنرا ذخیره کنید؛ تا کار به روز رسانی و نصب بستهها، به صورت خودکار و همچنین صحیحی انجام شود.
فعال سازی صفحات مخصوص توسعه دهندههای EF Core 1.0
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهندهها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بستهی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحهی نمایش خطاهای بانک اطلاعاتی را میسر میکند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{ "dependencies": { // same as before "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0" } }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDatabaseErrorPage(); }
تعریف اولین Context برنامه و مشخص سازی رشتهی اتصالی آن
در این تصویر، زیر ساخت نگاشتهای EF Core را مشاهده میکنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرندهی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل اینها را مشاهده میکنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که اینها نیز به فیلدها و ستونهای آن جدول در سمت بانک اطلاعاتی نگاشت میشوند.
بنابراین برای شروع کار، پوشهای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه میکنیم:
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Person> Persons { get; set; } } }
سازندهی این کلاس نیز به نحو خاصی تعریف شدهاست. اگر به سورسهای EF Core مراجعه کنیم، کلاس پایهی DbContext دارای دو سازندهی با و بدون پارامتر است:
protected DbContext() : this((DbContextOptions) new DbContextOptions<DbContext>()) { } public DbContext([NotNull] DbContextOptions options) { // … }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"... connection string ..."); } } }
اما چون در یک برنامهی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام میشود و در آنجا به سادگی میتوان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشتهی اتصالی را دریافت کرد، مرسوم است از سازندهی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعهی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» به فایل appsettings.json مراجعه کرده و تنظیمات رشتهی اتصالی برنامه را به صورت ذیل در آن مشخص میکنیم:
{ "ConnectionStrings": { "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true" } }
در اینجا به وهلهی پیش فرض SQL Server اشاره شدهاست؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده میشود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیدهاست.
پس از تعریف رشتهی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازندهی با پارامتر آن استفاده میکنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویسهای ASP.NET Core معرفی میکنیم:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); } public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; }); services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]); });
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی میکنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.
یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد
با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگیها نیز در آن وجود دارد. یعنی بجای روش فوق، میتوان IConfigurationRoot را به سازندهی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) //{ //} public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]); } } }
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا میکند و ساده میشود:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>();
یک نکته: امکان تزریق ApplicationDbContext به تمام کلاسهای برنامه وجود دارد
همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامهی ASP.NET Core نیز وجود دارد:
using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestDBController : Controller { private readonly ApplicationDbContext _ctx; public TestDBController(ApplicationDbContext ctx) { _ctx = ctx; } public IActionResult Index() { var name = _ctx.Persons.First().FirstName; return Json(new { firstName = name }); } } }
در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:
علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشدهاست و همچنین ساختار جداول را به آن منتقل نکردهایم که این موارد را در قسمتهای بعدی مرور خواهیم کرد.
استفاده از reCAPTCHA در ASP.NET
ASP.NET MVC #13
اعتبار سنجی اطلاعات ورودی در فرمهای ASP.NET MVC
زمانیکه شروع به دریافت اطلاعات از کاربران کردیم، نیاز خواهد بود تا اعتبار اطلاعات ورودی را نیز ارزیابی کنیم. در ASP.NET MVC، به کمک یک سری متادیتا، نحوهی اعتبار سنجی، تعریف شده و سپس فریم ورک بر اساس این ویژگیها، به صورت خودکار اعتبار اطلاعات انتساب داده شده به خواص یک مدل را در سمت کلاینت و همچنین در سمت سرور بررسی مینماید.
این ویژگیها در اسمبلی System.ComponentModel.DataAnnotations.dll قرار دارند که به صورت پیش فرض در هر پروژه جدید ASP.NET MVC لحاظ میشود.
یک مثال کاربردی
مدل زیر را به پوشه مدلهای یک پروژه جدید خالی ASP.NET MVC اضافه کنید:
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.Models
{
public class Customer
{
public int Id { set; get; }
[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
public string Name { set; get; }
[Display(Name = "Email address")]
[Required(ErrorMessage = "Email address is required.")]
[RegularExpression(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",
ErrorMessage = "Please enter a valid email address.")]
public string Email { set; get; }
[Range(0, 10)]
[Required(ErrorMessage = "Rating is required.")]
public double Rating { set; get; }
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
public DateTime StartDate { set; get; }
}
}
سپس کنترلر جدید زیر را نیز به برنامه اضافه نمائید:
using System.Web.Mvc;
using MvcApplication9.Models;
namespace MvcApplication9.Controllers
{
public class CustomerController : Controller
{
[HttpGet]
public ActionResult Create()
{
var customer = new Customer();
return View(customer);
}
[HttpPost]
public ActionResult Create(Customer customer)
{
if (this.ModelState.IsValid)
{
//todo: save data
return Redirect("/");
}
return View(customer);
}
}
}
بر روی متد Create کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه باز شده، گزینه Create a strongly typed view را انتخاب کرده و مدل را Customer انتخاب کنید. همچنین قالب Scaffolding را نیز بر روی Create قرار دهید.
توضیحات تکمیلی
همانطور که در مدل برنامه ملاحظه مینمائید، به کمک یک سری متادیتا یا اصطلاحا data annotations، تعاریف اعتبار سنجی، به همراه عبارات خطایی که باید به کاربر نمایش داده شوند، مشخص شده است. ویژگی Required مشخص میکند که کاربر مجبور است این فیلد را تکمیل کند. به کمک ویژگی StringLength، حداکثر تعداد حروف قابل قبول مشخص میشود. با استفاده از ویژگی RegularExpression، مقدار وارد شده با الگوی عبارت باقاعده مشخص گردیده، مقایسه شده و در صورت عدم تطابق، پیغام خطایی به کاربر نمایش داده خواهد شد. به کمک ویژگی Range، بازه اطلاعات قابل قبول، مشخص میگردد.
ویژگی دیگری نیز به نام System.Web.Mvc.Compare مهیا است که برای مقایسه بین مقادیر دو خاصیت کاربرد دارد. برای مثال در یک فرم ثبت نام، عموما از کاربر درخواست میشود که کلمه عبورش را دوبار وارد کند. ویژگی Compare در یک چنین مثالی کاربرد خواهد داشت.
در مورد جزئیات کنترلر تعریف شده در قسمت 11 مفصل توضیح داده شد. برای مثال خاصیت this.ModelState.IsValid مشخص میکند که آیا کارmodel binding موفق بوده یا خیر و همچنین اعتبار سنجیهای تعریف شده نیز در اینجا تاثیر داده میشوند. بنابراین بررسی آن پیش از ذخیره سازی اطلاعات ضروری است.
در حالت HttpGet صفحه ورود اطلاعات به کاربر نمایش داده خواهد شد و در حالت HttpPost، اطلاعات وارد شده دریافت میگردد. اگر دست آخر، ModelState معتبر نبود، همان اطلاعات نادرست وارد شده به کاربر مجددا نمایش داده خواهد شد تا فرم پاک نشود و بتواند آنها را اصلاح کند.
برنامه را اجرا کنید. با مراجعه به مسیر http://localhost/customer/create، صفحه ورود اطلاعات کاربر نمایش داده خواهد شد. در اینجا برای مثال در قسمت ورود اطلاعات آدرس ایمیل، مقدار abc را وارد کنید. بلافاصله خطای اعتبار سنجی عدم اعتبار مقدار ورودی نمایش داده میشود. یعنی فریم ورک، اعتبار سنجی سمت کاربر را نیز به صورت خودکار مهیا کرده است.
اگر علاقمند باشید که صرفا جهت آزمایش، اعتبار سنجی سمت کاربر را غیرفعال کنید، به فایل web.config برنامه مراجعه کرده و تنظیم زیر را تغییر دهید:
<appSettings>
<add key="ClientValidationEnabled" value="true"/>
البته این تنظیم تاثیر سراسری دارد. اگر قصد داشته باشیم که این تنظیم را تنها به یک view خاص اعمال کنیم، میتوان از متد زیر کمک گرفت:
@{ Html.EnableClientValidation(false); }
در این حالت اگر مجددا برنامه را اجرا کرده و اطلاعات نادرستی را وارد کنیم، باز هم همان خطاهای تعریف شده، به کاربر نمایش داده خواهد شد. اما اینبار یکبار رفت و برگشت اجباری به سرور صورت خواهد گرفت، زیرا اعتبار سنجی سمت کاربر (که درون مرورگر و توسط کدهای جاوا اسکریپتی اجرا میشود)، غیرفعال شده است. البته امکان غیرفعال کردن جاوا اسکریپت توسط کاربر نیز وجود دارد. به همین جهت بررسی خودکار سمت سرور، امنیت سیستم را بهبود خواهد بخشید.
نحوه تعریف عناصر مرتبط با اعتبار سنجی در Viewهای برنامه نیز به شکل زیر است:
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Customer</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
همانطور که ملاحظه میکنید به صورت پیش فرض از jQuery validator در سمت کلاینت استفاده شده است. فایل jquery.validate.unobtrusive متعلق به تیم ASP.NET MVC است و کار آن وفق دادن سیستم موجود، با jQuery validator میباشد (validation adapter). در نگارشهای قبلی، از کتابخانههای اعتبار سنجی مایکروسافت استفاده شده بود، اما از نگارش سه به بعد، jQuery به عنوان کتابخانه برگزیده مطرح است.
Unobtrusive همچنین در اینجا به معنای مجزا سازی کدهای جاوا اسکریپتی، از سورس HTML صفحه و استفاده از ویژگیهای data-* مرتبط با HTML5 برای معرفی اطلاعات مورد نیاز اعتبار سنجی است:
<input data-val="true" data-val-required="The Birthday field is required." id="Birthday" name="Birthday" type="text" value="" />
اگر خواستید این مساله را بررسی کنید، فایل web.config قرار گرفته در ریشه اصلی برنامه را باز کنید. در آنجا مقدار UnobtrusiveJavaScriptEnabled را false کرده و بار دیگر برنامه را اجرا کنید. در این حالت کلیه کدهای اعتبار سنجی، به داخل سورس View رندر شده، تزریق میشوند و مجزا از آن نخواهند بود.
نحوهی تعریف این اسکریپتها نیز جالب توجه است. متد Url.Content، یک متد سمت سرور میباشد که در زمان اجرای برنامه، مسیر نسبی وارد شده را بر اساس ساختار سایت اصلاح میکند. حرف ~ بکارگرفته شده، در ASP.NET به معنای ریشه سایت است. بنابراین مسیر نسبی تعریف شده از ریشه سایت شروع و تفسیر میشود.
اگر از این متد استفاده نکنیم، مجبور خواهیم شد که مسیرهای نسبی را به شکل زیر تعریف کنیم:
<script src="../../Scripts/customvaildation.js" type="text/javascript"></script>
در این حالت بسته به محل قرارگیری صفحات و همچنین برنامه در سایت، ممکن است آدرس فوق صحیح باشد یا خیر. اما استفاده از متد Url.Content، کار مسیریابی نهایی را خودکار میکند.
البته اگر به فایل Views/Shared/_Layout.cshtml، مراجعه کنید، تعریف و الحاق کتابخانه اصلی jQuery در آنجا انجام شده است. بنابراین میتوان این دو تعریف دیگر مرتبط با اعتبار سنجی را به آن فایل هم منتقل کرد تا همهجا در دسترس باشند.
توسط متد Html.ValidationSummary، خطاهای اعتبار سنجی مدل که به صورت دستی اضافه شده باشند نمایش داده میشود. این مورد در قسمت 11 توضیح داده شد (چون پارامتر آن true وارد شده، فقط خطاهای سطح مدل را نمایش میدهد).
متد Html.ValidationMessageFor، با توجه به متادیتای یک خاصیت و همچنین استثناهای صادر شده حین model binding خطایی را به کاربر نمایش خواهد داد.
اعتبار سنجی سفارشی
ویژگیهای اعتبار سنجی از پیش تعریف شده، پر کاربردترینها هستند؛ اما کافی نیستند. برای مثال در مدل فوق، StartDate نباید کمتر از سال 2000 وارد شود و همچنین در آینده هم نباید باشد. این موارد اعتبار سنجی سفارشی را چگونه باید با فریم ورک، یکپارچه کرد؟
حداقل دو روش برای حل این مساله وجود دارد:
الف) نوشتن یک ویژگی اعتبار سنجی سفارشی
ب) پیاده سازی اینترفیس IValidatableObject
تعریف یک ویژگی اعتبار سنجی سفارشی
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute
{
public int MinYear { set; get; }
public override bool IsValid(object value)
{
if (value == null) return false;
var date = (DateTime)value;
if (date > DateTime.Now || date < new DateTime(MinYear, 1, 1))
return false;
return true;
}
}
}
برای نوشتن یک ویژگی اعتبار سنجی سفارشی، با ارث بری از کلاس ValidationAttribute شروع میکنیم. سپس باید متد IsValid آنرا تحریف کنیم. اگر این متد false برگرداند به معنای شکست اعتبار سنجی میباشد.
در ادامه برای بکارگیری آن خواهیم داشت:
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
[MyDateValidator(MinYear = 2000,
ErrorMessage = "Please enter a valid date.")]
public DateTime StartDate { set; get; }
اکنون مجددا برنامه را اجرا نمائید. اگر تاریخ غیرمعتبری وارد شود، اعتبار سنجی سمت سرور رخ داده و سپس نتیجه به کاربر نمایش داده میشود.
اعتبار سنجی سفارشی به کمک پیاده سازی اینترفیس IValidatableObject
یک سؤال: اگر اعتبار سنجی ما پیچیدهتر باشد چطور؟ مثلا نیاز باشد مقادیر دریافتی چندین خاصیت با هم مقایسه شده و سپس بر این اساس تصمیم گیری شود. برای حل این مشکل میتوان از اینترفیس IValidatableObject کمک گرفت. در این حالت مدل تعریف شده باید اینترفیس یاد شده را پیاده سازی نماید. برای مثال:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MvcApplication9.CustomValidators;
namespace MvcApplication9.Models
{
public class Customer : IValidatableObject
{
//... same as before
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var fields = new[] { "StartDate" };
if (StartDate > DateTime.Now || StartDate < new DateTime(2000, 1, 1))
yield return new ValidationResult("Please enter a valid date.", fields);
if (Rating > 4 && StartDate < new DateTime(2003, 1, 1))
yield return new ValidationResult("Accepted date should be greater than 2003", fields);
}
}
}
در اینجا در متد Validate، فرصت خواهیم داشت تا به مقادیر کلیه خواص تعریف شده در مدل دسترسی پیدا کرده و بر این اساس اعتبار سنجی بهتری را انجام دهیم. اگر اطلاعات وارد شده مطابق منطق مورد نظر نباشند، کافی است توسط yield return new ValidationResult، یک پیغام را به همراه فیلدهایی که باید این پیغام را نمایش دهند، بازگردانیم.
به این نوع مدلها، self validating models هم گفته میشود.
یک نکته:
از MVC3 به بعد، حین کار با ValidationAttribute، امکان تحریف متد IsValid به همراه پارامتری از نوع ValidationContext نیز وجود دارد. به این ترتیب میتوان به اطلاعات سایر خواص نیز دست یافت. البته در این حالت نیاز به استفاده از Reflection خواهد بود و پیاده سازی IValidatableObject، طبیعیتر به نظر میرسد:
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var info = validationContext.ObjectType.GetProperty("Rating");
//...
return ValidationResult.Success;
}
فعال سازی سمت کلاینت اعتبار سنجیهای سفارشی
اعتبار سنجیهای سفارشی تولید شده تا به اینجا، تنها سمت سرور است که فعال میشوند. به عبارتی باید یکبار اطلاعات به سرور ارسال شده و در بازگشت، نتیجه عملیات به کاربر نمایش داده خواهد شد. اما ویژگیهای توکاری مانند Required و Range و امثال آن، علاوه بر سمت سرور، سمت کاربر هم فعال هستند و اگر جاوا اسکریپت در مرورگر کاربر غیرفعال نشده باشد، نیازی به ارسال اطلاعات یک فرم به سرور جهت اعتبار سنجی اولیه، نخواهد بود.
در اینجا باید سه مرحله برای پیاده سازی اعتبار سنجی سمت کلاینت طی شود:
الف) ویژگی سفارشی اعتبار سنجی تعریف شده باید اینترفیس IClientValidatable را پیاده سازی کند.
ب) سپس باید متد jQuery validation متناظر را پیاده سازی کرد.
ج) و همچنین مانند تیم ASP.NET MVC، باید unobtrusive adapter خود را نیز پیاده سازی کنیم. به این ترتیب متادیتای ASP.NET MVC به فرمتی که افزونه jQuery validator آنرا درک میکند، وفق داده خواهد شد.
در ادامه، تکمیل کلاس سفارشی MyDateValidator را ادامه خواهیم داد:
using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using System.Collections.Generic;
namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute, IClientValidatable
{
// ... same as before
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata,
ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "mydatevalidator",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
};
yield return rule;
}
}
}
در اینجا نحوه پیاده سازی اینترفیس IClientValidatable را ملاحظه مینمائید. ValidationType، نام متدی خواهد بود که در سمت کلاینت، کار بررسی اعتبار دادهها را به عهده خواهد گرفت.
سپس برای مثال یک فایل جدید به نام customvaildation.js به پوشه اسکریپتهای برنامه با محتوای زیر اضافه خواهیم کرد:
/// <reference path="jquery-1.5.1-vsdoc.js" />
/// <reference path="jquery.validate-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />
jQuery.validator.addMethod("mydatevalidator",
function (value, element, param) {
return Date.parse(value) < new Date();
});
jQuery.validator.unobtrusive.adapters.addBool("mydatevalidator");
توسط referenceهایی که مشاهده میکنید، intellisense جیکوئری در VS.NET فعال میشود.
سپس به کمک متد jQuery.validator.addMethod، همان مقدار ValidationType پیشین را معرفی و در ادامه بر اساس مقدار value دریافتی، تصمیم گیری خواهیم کرد. اگر خروجی false باشد، به معنای شکست اعتبار سنجی است.
همچنین توسط متد jQuery.validator.unobtrusive.adapters.addBool، این متد جدید را به مجموعه وفق دهندهها اضافه میکنیم.
و در آخر این فایل جدید باید به View مورد نظر یا فایل master page سیستم اضافه شود:
<script src="@Url.Content("~/Scripts/customvaildation.js")" type="text/javascript"></script>
تغییر رنگ و ظاهر پیغامهای اعتبار سنجی
اگر از رنگ پیش فرض قرمز پیغامهای اعتبار سنجی خرسند نیستید، باید اندکی CSS سایت را ویرایش کرد که شامل اعمال تغییرات به موارد ذیل خواهد شد:
1. .field-validation-error
2. .field-validation-valid
3. .input-validation-error
4. .input-validation-valid
5. .validation-summary-errors
6. .validation-summary-valid
نحوه جدا سازی تعاریف متادیتا از کلاسهای مدل برنامه
فرض کنید مدلهای برنامه شما به کمک یک code generator تولید میشوند. در این حالت هرگونه ویژگی اضافی تعریف شده در این کلاسها پس از تولید مجدد کدها از دست خواهند رفت. به همین منظور امکان تعریف مجزای متادیتاها نیز پیش بینی شده است:
[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
class CustomerMetadata
{
}
}
public partial class Customer : IValidatableObject
{
حالت کلی روش انجام آن هم به شکلی است که ملاحظه میکنید. کلاس اصلی، به صورت partial معرفی خواهد شد. سپس کلاس partial دیگری نیز به همین نام که در برگیرنده یک کلاس داخلی دیگر برای تعاریف متادیتا است، به پروژه اضافه میگردد. به کمک ویژگی MetadataType، کلاسی که قرار است ویژگیهای خواص از آن خوانده شود، معرفی میگردد. موارد عنوان شده، شکل کلی این پیاده سازی است. برای نمونه اگر با WCF RIA Services کار کرده باشید، از این روش زیاد استفاده میشود. کلاس خصوصی تو در توی تعریف شده صرفا وظیفه ارائه متادیتاهای تعریف شده را به فریم ورک خواهد داشت و هیچ کاربرد دیگری ندارد.
در ادامه کلیه خواص کلاس Customer به همراه متادیتای آنها باید به کلاس CustomerMetadata منتقل شوند. اکنون میتوان تمام متادیتای کلاس اصلی Customer را حذف کرد.
اعتبار سنجی از راه دور (remote validation)
فرض کنید شخصی مشغول به پر کردن فرم ثبت نام، در سایت شما است. پس از اینکه نام کاربری دلخواه خود را وارد کرد و مثلا به فیلد ورود کلمه عبور رسید، در همین حال و بدون ارسال کل صفحه به سرور، به او پیغام دهیم که نام کاربری وارد شده، هم اکنون توسط شخص دیگری در حال استفاده است. این مکانیزم از ASP.NET MVC3 به بعد تحت عنوان Remote validation در دسترس است و یک درخواست Ajaxایی خودکار را به سرور ارسال خواهد کرد و نتیجه نهایی را به کاربر نمایش میدهد؛ کارهایی که به سادگی توسط کدهای جاوا اسکریپتی قابل مدیریت نیستند و نیاز به تعامل با سرور، در این بین وجود دارد. پیاده سازی آن هم به نحو زیر است:
برای مثال خاصیت Name را در مدل برنامه به نحو زیر تغییر دهید:
[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
[System.Web.Mvc.Remote(action: "CheckUserNameAndEmail",
controller: "Customer",
AdditionalFields = "Email",
HttpMethod = "POST",
ErrorMessage = "Username is not available.")]
public string Name { set; get; }
سپس متد زیر را نیز به کنترلر Customer اضافه کنید:
[HttpPost]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public ActionResult CheckUserNameAndEmail(string name, string email)
{
if (name.ToLowerInvariant() == "vahid") return Json(false);
if (email.ToLowerInvariant() == "name@site.com") return Json(false);
//...
return Json(true);
}
توضیحات:
توسط ویژگی System.Web.Mvc.Remote، نام کنترلر و متدی که در آن قرار است به صورت خودکار توسط jQuery Ajax فراخوانی شود، مشخص خواهند شد. همچنین اگر نیاز بود فیلدهای دیگری نیز به این متد کنترلر ارسال شوند، میتوان آنها را توسط خاصیت AdditionalFields، مشخص کرد.
سپس در کدهای کنترلر مشخص شده، متدی با پارامترهای خاصیت مورد نظر و فیلدهای اضافی دیگر، تعریف میشود. در اینجا فرصت خواهیم داشت تا برای مثال پس از بررسی بانک اطلاعاتی، خروجی Json ایی را بازگردانیم. return Json false به معنای شکست اعتبار سنجی است.
توسط ویژگی OutputCache، از کش شدن نتیجه درخواستهای Ajaxایی جلوگیری کردهایم. همچنین نوع درخواست هم جهت امنیت بیشتر، به HttpPost محدود شده است.
تمام کاری که باید انجام شود همین مقدار است و مابقی مسایل مرتبط با اعمال و پیاده سازی آن خودکار است.
استفاده از مکانیزم اعتبار سنجی مبتنی برمتادیتا در خارج از ASP.Net MVC
مباحثی را که در این قسمت ملاحظه نمودید، منحصر به ASP.NET MVC نیستند. برای نمونه توسط متد الحاقی زیر نیز میتوان یک مدل را مثلا در یک برنامه کنسول هم اعتبار سنجی کرد. بدیهی است در این حالت نیاز خواهد بود تا ارجاعی را به اسمبلی System.ComponentModel.DataAnnotations، به برنامه اضافه کنیم و تمام عملیات هم دستی است و فریم ورک ویژهای هم وجود ندارد تا یک سری از کارها را به صورت خودکار انجام دهد.
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.Helper
{
public static class ValidationHelper
{
public static bool TryValidateObject(this object instance)
{
return Validator.TryValidateObject(instance, new ValidationContext(instance, null, null), null);
}
}
}
همه کنترلها در Xamarin Forms دارای Property ای با نام FlowDirection هستند که مقادیر RightToLeft، LeftToRight و MatchParent را میپذیرد. MatchParent که مقدار پیش فرض است، به این معنی است که مثلا اگر در ContentPage، مقدار FlowDirection را RightToLeft دهیم، تمامی کنترلهای داخل آن صفحه RightToLeft باشند و بالعکس.
مجددا یک Resources file را با نام Strings.fa.resx و یکی دیگر را با نام Strings.en.resx اضافه کنید. برای درک بهتر وضعیت نهایی، پروژه XamApp را Clone/Pull کنید و آن را بررسی کنید.
در فایل Strings.resx یک ردیف جدید اضافه کنید که Name آن برابر با HelloWorld باشد و Value آن خالی است. این نام، در کد نویسی ما استفاده میشود و مثلا نباید شامل Space، علامت ! و ... باشد. در فایل Strings.fa.resx یک ردیف جدید اضافه کنید که Name آن برابر با همان HelloWorld باشد و Value آن برابر با سلام دنیا! در نهایت در فایل Strings.en.resx یک ردیف جدید را اضافه کنید که Name آن HelloWorld بوده و Value آن ! Hello world باشد.
سپس در فایل App.xaml.cs میتوانید قبل از اولین NavigationService.NavigateAsync، از کد زیر را استفاده کنید:
Strings.Culture = CultureInfo.CurrentUICulture = new CultureInfo("en"); // or new CultureInfo("fa");
برای نمایش پیام در View Model با استفاده از IUserDialogs نیز میتوانید به شکل زیر عمل کنید:
await UserDialogs.AlertAsync(message: Strings.HelloWorld);
در صورتیکه بخواهید پارامتری را در stringهای چند زبانه خود داشته باشید نیز میتوانید به شکل زیر عمل کنید:
Name | En Value | Fa Value |
ButtonTappedCount | Button tapped {0} times! | دکمه {0} کلیک شده است |
<Label Text="{Binding StepsCount, StringFormat={x:Static resx:Strings.ButtonTappedCount}}" />
namespace مربوطه یعنی resx هم در بالای فایل Xaml باید قرار داده شود، که میشود:
xmlns:resx="clr-namespace:XamApp.Resources"
await UserDialogs.AlertAsync(string.Format(Strings.ButtonTappedCount, StepsCount));
203 00:16:38,731 --> 00:16:41,325 <i>Happy Christmas, your arse I pray God it's our last</i>
- عدم حذف تگها ، گاها برنامه نویسها از تگ هایی چون Bold,italic,underline,color استفاده میکنند که معدود برنامههایی آن را پشتیبانی کرده و تلویزیون و پلیرها هم که اصلا پشتیبانی نمیکنند و باعث میشود که متن روی تلویزیون مثل کد html ظاهر شود
- بعضی جملات دوبار روی صفحه ظاهر میشوند.
- تنها یک فایل را در هر زمان تبدیل میکند. مثلا اگر یک سریال چند قسمته داشته باشید، برای هر قسمت باید زیرنویس را انتخاب کرده و تبدیل کنید، در صورتی که میتوان دستور داد تمام زیرنویسهای داخل دایرکتوری را تبدیل کرد یا چند زیرنویس را برای این منظور انتخاب کرد.
نحوهی خواندن زیرنویس با کدنویسی
203 00:16:38,731 --> 00:16:41,325 <i>Happy Christmas, your arse I pray God it's our last</i>
private readonly static Regex regex_srt = new Regex(@"(?<sequence>\d+)\r\n(?<start>\d{2}\:\d{2}\:\d{2},\d{3}) --\> " + @"(?<end>\d{2}\:\d{2}\:\d{2},\d{3})\r\n(?<text>[\s\S]*?)\r\n\r\n", RegexOptions.Compiled); public string ToUnicode(string lines) { string subtitle= regex_srt.Replace(lines,delegate(Match m) { string text = m.Groups["text"].Value; //1.remove tags text = CleanScriptTags(text); //2.replace letters PersianReshape reshaper = new PersianReshape(); text = reshaper.reshape(text); string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); text = ""; foreach (string line in splitedlines) { //3.reverse tags text += ReverseText(reshaper.reshape(line))+Environment.NewLine ; } return string.Format("{0}\r\n{1} --> {2}\r\n", m.Groups["sequence"], m.Groups["start"].Value, m.Groups["end"]) + text + Environment.NewLine+Environment.NewLine ; } ); return subtitle; }
m.Groups["text"].Value
private static readonly Regex regex_tags = new Regex("<.*?>", RegexOptions.Compiled); private string CleanScriptTags(string html) { return regex_tags.Replace(html, string.Empty); }
PersianReshape reshaper = new PersianReshape(); text = reshaper.reshape(text); string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); text = ""; foreach (string line in splitedlines) { //3.reverse tags text += ReverseText(reshaper.reshape(line))+Environment.NewLine ; }
Dictionary<int ,int> letters=new Dictionary<int, int>(); //0=0x0 ,1=1x0 ,2=0x1 ,3=1x1 private void FillPrimaryTable() { //آ letters.Add(1570, 65153); //ا letters.Add(1575, 65166); //أ letters.Add(1571, 65155); //ب letters.Add(1576, 65167); //ت letters.Add(1578, 65173); //ث letters.Add(1579, 65177); //ج letters.Add(1580, 65181); ..... } Dictionary<int,byte> specialchars=new Dictionary<int, byte>(); private void SetSpecialChars() { //آ specialchars.Add(1570, 0); //ا specialchars.Add(1575, 0); //د2 specialchars.Add(1583, 1); //ذ2 specialchars.Add(1584, 1); //ر2 specialchars.Add(1585, 1); //ز2 specialchars.Add(1586, 1); //ژ specialchars.Add(1688, 1); //و2 specialchars.Add(1608, 1); //أ specialchars.Add(1571, 1); }
در آن متد هر بار یک حرف را انتخاب میکرد و حرف قبلی و بعدی آن را ارسال میکرد تا تابع CalculateIncrease آن را محاسبه کرده و کاراکتر نهایی را باز گرداند و به متغیر finalText اضافه میکرد. ولی در حین نوشتن، زمانی را به یاد آوردم که اندروید به تازگی آمده بود و هنوز در آن زمان از زبان فارسی پشتیبانی نمیکرد و حروف برنامههایی که مینوشتیم به صورت جدا از هم بود و همین مشکل را داشت که ما این مشکل را با استفاده از یک کلاس جاوا که دوست عزیزی آن را در اینجا به اشتراک گذاشته بود، حل میکردیم. پس به این صورت بود که از ادامهی نوشتن کلاس انصراف دادم و از یک کلاس دقیقتر و آماده استفاده کردم.
PersianReshape reshaper = new PersianReshape(); text = reshaper.reshape(text);
//3.reverse tags text = ReverseText(text);
string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); text = ""; foreach (string line in splitedlines) { //3.reverse tags text += ReverseText(reshaper.reshape(line))+Environment.NewLine ; }
private string Reverse(string text) { return Reverse(text,0,text.Length); } private string Reverse(string text,int start,int end) { if (end < start) return text; string reverseText = ""; for (int i = end-1; i >=start; i--) { reverseText += text[i]; } return reverseText; }
private string ReverseText(string text) { char[] chararray = text.ToCharArray(); string reverseText = ""; bool prefixcomp = false; bool postfixcomp = false; string prefix = ""; string postfix = ""; #region get prefix symbols for (int i = 0; i < chararray.Length; i++) { if (!prefixcomp) { char ch =(char) chararray.GetValue(i) ; if (ch< 130) { prefix += chararray.GetValue(i); } else { prefixcomp = true; break; } } } #endregion }
#region get postfix symbols for (int i = chararray.Length - 1; i >-1 ; i--) { if (!postfixcomp && prefix.Length!=text.Length) { char ch = (char)chararray.GetValue(i); if (ch < 130) { postfix += chararray.GetValue(i); } else { postfixcomp = true; break; } } } #endregion
#region reverse text reverseText = Reverse(text, prefix.Length, text.Length-postfix.Length); reverseText = unTagetdLettersRegex.Replace(reverseText, delegate(Match m) { return Reverse(m.Value); }); #endregion
private static readonly Regex unTagetdLettersRegex = new Regex(@"[A-Za-z0-9]+", RegexOptions.Compiled);
return prefix+ reverseText+postfix;
private static readonly Regex unTagetdLettersRegex = new Regex(@"[A-Za-z0-9]+", RegexOptions.Compiled); private string ReverseText(string text) { char[] chararray = text.ToCharArray(); string reverseText = ""; bool prefixcomp = false; bool postfixcomp = false; string prefix = ""; string postfix = ""; #region get prefix symbols for (int i = 0; i < chararray.Length; i++) { if (!prefixcomp) { char ch =(char) chararray.GetValue(i) ; if (ch< 130) { prefix += chararray.GetValue(i); } else { prefixcomp = true; break; } } } #endregion #region get postfix symbols for (int i = chararray.Length - 1; i >-1 ; i--) { if (!postfixcomp && prefix.Length!=text.Length) { char ch = (char)chararray.GetValue(i); if (ch < 130) { postfix += chararray.GetValue(i); } else { postfixcomp = true; break; } } } #endregion #region reverse text reverseText = Reverse(text, prefix.Length, text.Length-postfix.Length); reverseText = unTagetdLettersRegex.Replace(reverseText, delegate(Match m) { return Reverse(m.Value); }); #endregion return prefix+ reverseText+postfix; }
return string.Format("{0}\r\n{1} --> {2}\r\n", m.Groups["sequence"], m.Groups["start"].Value, m.Groups["end"]) + text + Environment.NewLine+Environment.NewLine ;
نمایی از برنامهی نهایی
روی پلیر یا تلویزیون
نکتهی نهایی: هنگام تست زیرنویس روی فیلم متوجه شدم پلیر خطوط بلند را که در صفحهی نمایش جا نمیشود، میشکند و به دو خط تقسیم میکند. ولی نکتهی خنده دار اینجا بود که خط اول را پایین میاندازد و خط دوم را بالا. برای همین این تکه کد را نوشتم و به طور جداگانه در گیت هاب هم قرار دادهام.
این تکه کد را هم بعد از
//1.remove tags text = CleanScriptTags(text);
text =StringUtils.ConvertToMultiLine(text);
کد متد ConvertToMultiline:
namespace Utils { public static class StringUtils { public static string ConvertToMultiLine(String text, int min = 30, int max = 40) { if (text.Trim() == "") return text; string[] words = text.Split(new string[] { " " }, StringSplitOptions.None); string text1 = ""; string text2 = ""; foreach (string w in words) { if (text1.Length < min) { if (text1.Length == 0) { text1 = w; continue; } if (w.Length + text1.Length <= max) text1 += " " + w; } else text2 += w + " "; } text1 = text1.Trim(); text2 = text2.Trim(); if (text2.Length > 0) { text1 += Environment.NewLine + ConvertToMultiLine(text2, min, max); } return text1; } } }
برنامه مورد نظر را به طور کامل میتوانید از اینجا یا اینجا به صورت فایل نهایی و هم سورس دریافت کنید.
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> <link href="~/content/css/bootstrap-switch.min.css" rel=stylesheet"></link> <script src="~/Scripts/bootstrap-switch.min.js"></script>
نکته مهم: فایل css شامل دو نسخه هست که یکی از آن برای Bootstrap2 و دیگری برای نسخه 3 آن است که نسبت به آن نسخه، استایل مناسب را انتخاب کنید.
var model=BootstrapSwitchModel(); //وضعیت فعال بودن و غیرفعال بودن سوئیچ model.Checked=true; //اندازه آن model.Size=BootstrapSize..normal; //یک انیمیشن ساده موقع سوئیچ کردن دارد model.Animate=true; //به چک باکس عادی تبدیل میشود model.Disabled=true; //غیرفعال شده و به صورت فقط خواندنی قابل دسترس است model.Readonly=true; //رنگ قعال بودن model.OnColor=BootstrapColor.Success; //رنگ غیرفعال بودن model.OffColor=BootstrapColor.Danger; //متن نمایشی در هنگام فعال بودن model.OnText="On"; //متن نمایشی در حالت عدم انتخاب model.OffText="Off"; //بین دو حالت روشن و خاموش نمایش داده میشود model.label="Public Display"; //تعیین میزان اندازه برچسب بالا model.LabelWidth=100; //سوئیچ به صورت آینه ای معکوس میشود model.Inverse=false; //کلاسی جهت تغییر استایل سوئیچ model.BaseClass="myclass"; //تعیین کلاس برای تگ اصلی پدر model.WrapperClass="wclass"; //فقط یکی از چند سوئیچ میتواند فعال باشد model.RadioAllOff=false; //یک سوئیچ در حالت عادی فقط یکی از //وضعیتها را نمایش میده ولی در این حالت //سوئیچ در ابتدا بین این دو وضعیت گیر کرده است model.Indeterminate=true; //اندازه سمت چپ و راست سوئیچ model.HandleWidth=25;
@{ var model=BootstrapSwitchModel(); ....} @HTML.BootstrapSwitch("id",model);
زمانیکه کاربری برنامهی تک صفحهای وب را در مرورگر باز میکند، ابتدا فایل index.html را در پاسخ دریافت خواهد کرد. این فایل تعاریف مداخل مورد نیاز برای رندر آنرا مانند فایلهای جاوا اسکریپت و CSS، به همراه دارد. سپس این فایلها توسط مرورگر از سرور دریافت میشوند. در این حالت با پردازش این فایلها، کامپوننت ریشهی سایت بارگذاری میشود. پس از پایان آن، قالب این کامپوننت به کاربر نمایش داده خواهد شد. بنابر سرعت دریافت فایلها توسط کاربر، این آغاز میتواند اندکی کند باشد. البته با رعایت نکات گفتهی شدهی در مطلب «Angular CLI - قسمت پنجم - ساخت و توزیع برنامه» میتوان این حجم را توسط AoT و Tree-Shaking به میزان قابل ملاحظهای کاهش داد. به علاوه با فعالسازی Lazy loading میتوان قسمتهای مختلف برنامه را تبدیل به یک سری Bundle کرد که در زمان درخواست، بارگذاری میشوند. به این ترتیب حجم فایلهای ابتدایی که باید از سرور دریافت شوند بسیار کمتر شده و به علاوه با کاهش این حجم، مرورگر نیز باید میزان کمتری از کدها را در جهت نمایش اولین کامپوننت، پردازش و اجرا کند. در این حالت زمانیکه کاربری شروع به پیمایش مسیر یک ماژول خاص را میکند، آنگاه فایلهای مرتبط با آن از سرور دریافت و در مرورگر پردازش میشوند. بنابراین اگر کاربری به قسمتی دسترسی ندارد، نیازی هم به دریافت فایلهای آن نخواهد داشت؛ چون کار به فعالسازی مسیریابی آن ماژول نمیرسد.
آماده شدن جهت Lazy loading
پیش از Lazy loading یک قسمت از برنامه (که به آن async routing هم میگویند)، این قسمت باید دارای شرایطی باشد:
- این قسمت از برنامه حتما باید در یک ماژول تعریف شده باشد. از این جهت که Lazy loading، لیست کامپوننتهای قید شدهی در تعریف یک ماژول را بارگذاری میکند.
- تمام مسیرهای این ماژول باید در ذیل یک مسیر والد، گروه بندی شده باشند. از این جهت که Lazy loading فقط بر روی مسیر ریشهی والد تنظیم و بارگذاری میشود.
- این ماژول نباید در هیچ ماژول دیگری import شده باشد. اگر این ماژول ارجاعی را در سایر ماژولها داشته باشد، هیچ راهی بجز دریافت و کامپایل کامل آن توسط Angular وجود نخواهد داشت.
در مثال جاری این سری:
- تمام ویژگیهای قسمت مدیریت محصولات، داخل ماژول product.module.ts تعریف شدهاند. بنابراین اولین شرط Lazy loading آن برقرار است.
- در فایل product-routing.module.ts، کار گروه بندی مسیریابیها ذیل یک والد مشخص انجام شدهاست (همان قسمت ششم این سری). بنابراین شرط دوم lazy loading این ماژول نیز پیشتر پیاده سازی شدهاست.
- اما اگر به فایل src\app\app.module.ts مراجعه کنیم، ارجاعی به این ماژول در قسمت imports آن وجود دارد. بنابراین باید این ارجاع را حذف کنیم. در غیراینصورت کار دریافت کامل آن به همراه سایر ماژولهای برنامه، در همان ابتدای کار صورت خواهد گرفت.
بنابراین در فایل src\app\app.module.ts، ابتدا import فایل آنرا از ابتدای ماژول حذف و سپس ارجاع به نام کلاس کامپوننت ProductModule را نیز حذف میکنیم. در این حالت اگر از طریق منوی سایت سعی در دسترسی به این مسیرها کنیم، خطای 404 را دریافت خواهیم کرد؛ چون اکنون برنامه اطلاعاتی را در مورد نحوهی مسیریابی قسمت محصولات برنامه، ندارد.
Lazy loading یک ماژول
برای بارگذاری غیرهمزمان یک ماژول و یا همان Lazy loading، میتوان از خاصیت loadChildren تنظیمات مسیریابی، استفاده کرد:
{ path: 'products', loadChildren:'app/product/product.module#ProductModule' },
با این تنظیم، زمانیکه مسیر ریشهی produtcs درخواست شد، کار بارگذاری ماژول آن صورت گرفته و تنظیمات مسیریابی آن به سیستم اضافه میشود. به علاوه کار فعالسازی و نمایش کامپوننت آن را نیز انجام خواهد داد.
به همین منظور فایل src\app\app-routing.module.ts را گشوده و تنظیم فوق را به آن اضافه میکنیم:
const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: 'products', loadChildren: 'app/product/product.module#ProductModule' }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 165 kB {4} [initial] chunk {1} main.bundle.js, main.bundle.js.map (main) 32.7 kB {3} [initial] [rendered] chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 129 kB {4} [initial] chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.72 MB [initial] [rendered] chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] chunk {5} 5.chunk.js, 5.chunk.js.map 51.1 kB {1} [rendered]
به علاوه اگر در منوی سایت بر روی لینک نمایش لیست محصولات کلیک کنیم، هنوز خروجی نمایش داده نمیشود (هرچند خطای 404 را هم دریافت نمیکنیم). علت اینجا است که اگر به فایل src\app\product\product-routing.module.ts مراجعه کنیم، تعریف این مسیر ریشه، در این فایل نیز وجود دارد:
const routes: Routes = [ { path: 'products', canActivate: [ AuthGuard ], children: [ ] } ];
import { AuthGuard } from './user/auth.guard'; const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: 'products', loadChildren: 'app/product/product.module#ProductModule', canActivate: [AuthGuard] }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
یک نکته: اکنون تنظیمات مسیریابی فایل src\app\product\product-routing.module.ts چنین شکلی را پیدا کردهاست:
const routes: Routes = [ { path: '', component: ProductListComponent }, { path: ':id', component: ProductDetailComponent, resolve: { product: ProductResolverService } }, { path: ':id/edit', component: ProductEditComponent, resolve: { product: ProductResolverService }, canDeactivate: [ProductEditGuard], children: [ { path: '', redirectTo: 'info', pathMatch: 'full' }, { path: 'info', component: ProductEditInfoComponent }, { path: 'tags', component: ProductEditTagsComponent } ] } ];
در این حالت اگر مسیر نمایش لیست محصولات را درخواست دهیم، مشاهده خواهیم کرد فایل 5.chunk.js که حاوی اطلاعات این ماژول است، به صورت مجزایی بارگذاری شده (lazy loading) و سپس با فعال شدن محافظ مسیر آن، صفحهی لاگین نمایش داده میشود:
این بارگذاری با تاخیر و در صورت نیاز، به دو علت آغاز برنامه را سریعتر میکند:
الف) مرورگر اطلاعی از وجود فایل 5.chunk.js در ابتدای کار نداشته و آنرا بارگذاری نمیکند (دریافت حجم کمتر، در آغاز نمایش برنامه).
ب) چون حجم کمتری از کدهای جاوا اسکریپت توسط مرورگر در آغاز کار دریافت میشود، کار پردازش و اجرای آنها نیز بسیار سریعتر خواهد شد.
بررسی محافظ canLoad
تعدادی از محافظهای مسیرها را در قسمت قبل بررسی کردیم. هنگامیکه کامپوننتها به صورت lazy loading فعالسازی شده و قالب آنها نمایش داده میشوند، میتوان از محافظ مسیر دیگری به نام canLoad نیز استفاده کرد و هدف از آن، بررسی منطقی، پیش از فعالسازی یک مسیر غیرهمزمان است. بنابراین اگر این محافظ false را برگرداند، حتی فایلهای اسکریپت این ماژول، بارگذاری اولیه نیز نخواهد شد. به این ترتیب کسانیکه دسترسی به یک مسیر را نداشته باشند، فایلهای اسکریپت متناظر با آنرا نیز دریافت نخواهند کرد.
در مثال جاری، اگر به برگهی network ابزار developer مرورگر دقت کنید، با درخواست نمایش مسیر لیست محصولات، ابتدا فایل js آن دریافت میشود که حاوی اطلاعات تمام کامپوننتها و قالبهای مرتبط با این مسیر است و سپس صفحهی login نمایش داده خواهد شد. بنابراین اگر کاربر به این قسمت دسترسی نداشته باشد، فایل js آن بیجهت دریافت و بارگذاری شدهاست. برای بهبود این وضعیت میتوان نمایش لاگین را پیش از بارگذاری فایل js این ماژول فعالسازی کرد و این مورد هدف اصلی محافظ canLoad است.
در ادامه برای تکمیل مثال جاری، میتوان AuthGuard را طوری تنظیم کرد که علاوه بر پیاده سازی CanActivate، اینترفیس CanLoad را نیز پیاده سازی کند:
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router, CanLoad, Route } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate, CanLoad { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.checkLoggedIn(state.url); } canLoad(route: Route): boolean { return this.checkLoggedIn(route.path); } // … the same as before }
مرحلهی بعد، تغییر فایل src\app\app-routing.module.ts و جایگزین کردن تعریف فعلی canActivate با canLoad است:
{ path: 'products', loadChildren: 'app/product/product.module#ProductModule', canLoad: [AuthGuard] },
پیش بارگذاری ماژولها
با فعالسازی lazy loading، ماژولهای مورد نیاز کاربر دیگر به همراه فایلهای js ابتدایی برنامه که در فایل index.html ارجاع مستقیمی به آنها دارند، ارائه نمیشوند و تنها در صورت درخواست مشاهدهی مسیری، کار بارگذاری آنها توسط برنامه صورت خواهد گرفت. همین مساله میتواند در بار اول نمایش این ماژولها تاخیر کوتاهی را سبب شود. به همین جهت قابلیت پیش بارگذاری ماژولها نیز در سیستم مسیریاب Angular پیش بینی شدهاست. به این قابلیت preloading و یا eager lazy loading نیز میگویند. در این حالت برنامه در پشت صحنه، کار پیش واکشی ماژولها را انجام میدهد و زمانیکه کاربری مسیری را درخواست میدهد، آْن مسیر را بدون درنگ مشاهده خواهد کرد.
بدیهی است این قابلیت نباید برای ماژولهایی که قرار است توسط کاربرانی خاص مشاهده شوند فعال شود و هدف آن دسترسی سریع به ماژولهای پرکاربرد برنامهاست.
در اینجا سه استراتژی پیش بارگذاری ماژولها میسر است:
- No preloading که حالت پیش فرض است.
- Preload all سبب پیش بارگذاری تمام قسمتهای lazy load برنامه میشود.
- Custom که اجازهی تعریف یک استراتژی سفارشی را میدهد.
برای مثال برای فعالسازی حالت Preload all، باید به فایل src\app\app-routing.module.ts مراجعه کرده و تغییرات ذیل را اعمال کنیم:
import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; @NgModule({ imports: [RouterModule.forRoot( routes, { enableTracing: true, preloadingStrategy: PreloadAllModules /*, useHash: true*/ } )],
یک نکته: وجود محافظ canLoad، هر نوع استراتژی prealoading را غیرفعال میکند. اما prealoading با سایر انواع محافظها کار میکند.
بنابراین برای آزمایش تنظیم preloadingStrategy: PreloadAllModules، تعریف canLoad را به canActivate تغییر دهید.
تعریف استراتژیهای سفارشی پیش بارگذاری ماژولها
اگر نیاز به یک استراتژی پیش بارگذاری بهتر از هیچ یا همه باشد، میتوان یک استراتژی سفارشی را نیز تدارک دید و ایجاد آن سه مرحلهی ایجاد سرویس مرتبط، ثبت آن سرویس در ماژول و در آخر تنظیم مسیریابی را به همراه دارد.
برای این منظور ابتدا دستور ذیل را صادر کنید تا قالب ابتدایی سرویس SelectiveStrategy ایجاد شود:
>ng g s SelectiveStrategy -m app.module
installing service create src\app\selective-strategy.service.spec.ts create src\app\selective-strategy.service.ts update src\app\app.module.ts
سپس کدهای SelectiveStrategyService را به نحو ذیل تغییر دهید:
import { Injectable } from '@angular/core'; import { Route, PreloadingStrategy } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; @Injectable() export class SelectiveStrategyService implements PreloadingStrategy { preload(route: Route, load: Function): Observable<any> { if (route.data && route.data['preload']) { return load(); } return Observable.of(null); } }
- پارامتر اول این متد، اطلاعاتی را در مورد مسیر جاری در اختیار ما قرار میدهد و دومین پارامتر آن متدی است که کار preloading را انجام میدهد.
- در اینجا است که تصمیم میگیریم ماژولی را preload کنیم یا خیر. برای نمونه در اینجا از خاصیت data مسیریابی استفاده شدهاست. این خاصیت نیز به یک مقدار ثابت اشاره میکند (قسمت «ارسال اطلاعات ثابت به مسیرهای مختلف برنامه» قسمت چهارم). برای مثال نام دلخواه آنرا preload گذاشتهایم و اگر مقدار آن به true تنظیم شده بود، آنگاه این مسیر preload خواهد شد. فراخوانی متد load در اینجا به معنای preloading این مسیر است. در غیراینصورت null را بازگشت میدهیم.
در ادامه نیاز است در فایل src\app\app-routing.module.ts، بجای معرفی PreloadAllModules، این استراتژی سفارشی خود را معرفی کرد:
import { SelectiveStrategyService } from './selective-strategy.service'; @NgModule({ imports: [RouterModule.forRoot( routes, { enableTracing: true, preloadingStrategy: SelectiveStrategyService /*, preloadingStrategy: PreloadAllModules*/ /*, useHash: true*/ } )],
{ path: 'products', loadChildren: 'app/product/product.module#ProductModule', //canLoad: [AuthGuard] canActivate: [AuthGuard], data: { preload: true } },
برای آزمایش آن، برنامه را مجدا اجرا کرده و صفحه را refresh کنید. سپس برگهی network ابزار developers را نیز باز نگه دارید. مشاهده خواهید کرد که علاوه بر فایلهای js اصلی برنامه که در فایل index.html ارجاعی را دارند، فایل 5.chunk.js نیز پیش بارگذاری شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-09.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
برپایی پیشنیازها
در اینجا برای بررسی React Hooks، یک پروژهی جدید React را ایجاد میکنیم:
> npm i -g create-react-app > create-react-app sample-30 > cd sample-30 > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
همچنین اگر به فایل package.json موجود در ریشهی پروژه دقت کنیم، برای کار با React-hooks، نیاز است نگارش بستههای React و React-dom، حداقل مساوی 16.7 باشند که در زمان نگارش این مطلب، نگارش 16.12.0 آن به صورت خودکار نصب میشود. بنابراین بدون مشکلی میتوانیم شروع به کار با React hooks کنیم.
معرفی useState Hook
در اینجا قصد داریم یک شمارشگر را به همراه یک دکمه، در صفحه نمایش دهیم؛ بطوریکه این شمارشگر، تعداد بار کلیک بر روی دکمه را ردیابی میکند. از پیش میدانیم که برای ردیابی مقدار تعداد بار کلیک شدن، باید متغیر آنرا درون state یک class component قرار داد:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { count: 0 }; incrementCount = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <button onClick={this.incrementCount} className="btn btn-primary m-3"> I was clicked {this.state.count} times! </button> ); } } export default App;
اکنون میخواهیم همین کامپوننت را توسط React hooks بازنویسی کنیم. در ابتدا، فایل app.js را به AppClass.js، تغییر نام میدهیم تا نگارش قبلی class component را برای مقایسه، در اختیار داشته باشیم. در ادامه فایل جدید AppFunction.js را برای بازنویسی آن توسط یک کامپوننت تابعی، توسط میانبرهای imrc و سپس sfc در VSCode، ایجاد میکنیم. البته این تغییر نام فایلها، نیاز به تغییر نام ماژولهای import شدهی در فایل index.js را نیز به صورت زیر دارد:
//import App from "./AppClass"; import App from "./AppFunction";
اولین سؤالی که اینجا مطرح میشود، این است: در این کامپوننت تابعی جدید، state را از کجا بدست بیاوریم؟
با React Hooks، بجای تعریف یک state به صورت خاصیت، آنرا صرفا use میکنیم و این دقیقا نام اولین React Hooks ای است که بررسی میکنیم؛ یا همان useState. بنابراین ابتدا این شیء را import خواهیم کرد:
import React, { useState } from 'react';
const App = () => { const [count, setCount] = useState(0);
useState<number>(initialState: number | (() => number)): [number, React.Dispatch<React.SetStateAction<number>>]
import React, { useState } from "react"; const App = () => { const [count, setCount] = useState(0); const incrementCount = () => { setCount(count + 1); }; return ( <button onClick={incrementCount} className="btn btn-primary m-3"> I was clicked {count} times! </button> ); }; export default App;
- همچنین در اینجا (داخل این متد) دیگر خبری از thisها نیست؛ onClick، مستقیما به متغیر incrementCount اشاره میکند و {count} نیز مستقیما از خروجی useState، که به مقدار جاری count اشاره میکند، تامین میشود.
- اکنون با هربار کلیک بر روی این دکمه، متد منتسب به متغیر incrementCount فراخوانی شده و در داخل آن، همان متد setCount را جهت به روز رسانی state، فراخوانی میکنیم (بجای فراخوانی this.setState عمومی قبلی). در اینجا ابتدا مقدار جاری متغیر count در state، دریافت شده و سپس یک واحد به آن اضافه میشود. امضای متد جنریک setCount به صورت زیر است:
const setCount: (value: React.SetStateAction<number>) => void
استفاده از مقدار قبلی state توسط useState
زمانیکه متد this.setState فراخوانی میشود، اینکار سبب در صف قرار گرفتن رندر مجدد کامپوننت جاری خواهد شد. همچنین اعمال این متد نیز ممکن است در صف قرار گیرد. یعنی اگر پس از فراخوانی this.setState، سعی در خواندن state به روز شده را داشته باشیم، ممکن است مقدار اشتباهی را دریافت کنیم:
incrementCount = () => { this.setState({ count: this.state.count + 1 }); };
incrementCount = () => { this.setState(prevState => ({ count: prevState.count + 1 })); };
این نکته در مورد کامپوننتهای تابعی نیز وجود دارد:
const incrementCount = () => { setCount(count + 1); };
const incrementCount = () => { setCount(prevCount => prevCount + 1); };
به روز رسانی بیش از یک خاصیت در state
فرض کنید قصد داریم به مثال جاری، یک مربع را در صفحه اضافه کنیم که با کلیک بر روی آن، رنگش تغییر میکند (خاموش و روشن میشود):
در حالت AppClass یا کامپوننت کلاسی، کدهای برنامه به صورت زیر تغییر میکنند:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { count: 0, isOn: false }; incrementCount = () => { this.setState(prevState => ({ count: prevState.count + 1 })); }; toggleLight = () => { this.setState(prevState => ({ isOn: !prevState.isOn })); }; render() { return ( <> <h1>App Class</h1> <h2>Counter</h2> <button onClick={this.incrementCount} className="btn btn-primary m-3"> I was clicked {this.state.count} times! </button> <h2>Toggle Light</h2> <div style={{ height: "50px", width: "50px", cursor: "pointer" }} className={ this.state.isOn ? "alert alert-info m-3" : "alert alert-warning m-3" } onClick={this.toggleLight} /> </> ); } } export default App;
- در متد رندر، نیاز است تا تنها یک child، بازگشت داده شود. به همین جهت میتوان از React.Fragment، برای محصور سازی المانهای تعریف شده، استفاده کرد. البته در React 16.7.0، دیگر نیازی به ذکر صریح React.Fragment نبوده و فقط میتوان نوشت </><> تا بیانگر یک فرگمنت باشد.
- سپس یک div تعریف شده که با استفاده از ویژگی style، یک سری شیوهنامهی ابتدایی، مانند طول و عرض و نوع اشارهگر ماوس آن، تعیین شدهاند.
- اکنون برای اینکه بتوان با کلیک بر روی این div، رنگ آنرا تغییر داد، نیاز است بتوان توسط متغیری، مقدار خاموش و روشن بودن را ردیابی کرد. به همین جهت خاصیت جدید isOn را به state اضافه میکنیم.
- در آخر، رویداد onClick این div را به متد رویدادگران toggleLight متصل میکنیم تا توسط آن و فراخوانی this.setState، بتوان مقدار قبلی isOn را در state، دریافت و سپس آنرا معکوس کرد و بجای مقدار جاری isOn در state درج کنیم. این فراخوانی، سبب رندر مجدد کامپوننت جاری شده و در نتیجهی آن، مقدار className را بر اساس مقدار this.state.isOn، به صورت پویا تغییر میدهد.
برای مشاهدهی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول AppClass تامین شود:
import App from "./AppClass"; //import App from "./AppFunction";
اکنون قصد داریم دقیقا معادل همین قطعه کد را در کامپوننت تابعی خود پیاده سازی کنیم. به همین جهت به فایل src\AppFunction.js بازگشته و تغییرات زیر را اعمال میکنیم:
import React, { useState } from "react"; const App = () => { const [count, setCount] = useState(0); const [isOn, setIsOn] = useState(false); const incrementCount = () => { setCount(prevCount => prevCount + 1); }; const toggleLight = () => { setIsOn(prevIsOn => !prevIsOn); }; return ( <> <h1>App Function</h1> <h2>Counter</h2> <button onClick={incrementCount} className="btn btn-primary m-3"> I was clicked {count} times! </button> <h2>Toggle Light</h2> <div style={{ height: "50px", width: "50px", cursor: "pointer" }} className={isOn ? "alert alert-info m-3" : "alert alert-warning m-3"} onClick={toggleLight} /> </> ); }; export default App;
- اگر دقت کنید، کلیات این کامپوننت تابعی، با کامپوننت کلاسی، آنچنان متفاوت نیست. متد رندر آن دقیقا همان markup را بازگشت میدهد؛ با یک تفاوت مهم: در اینجا دیگر نیازی به ذکر thisها نیست، چون تمام ارجاعات، به متغیرهای داخل تابع App انجام شدهاست و نه به متدها و یا خاصیتهای یک کلاس.
- مرحلهی بعد تغییر، جایگزینی this.state.isOn قبلی، با یک متغیر درون تابع App است. به همین جهت در اینجا یک useState دیگر را تعریف میکنیم. هر useState، تنها به قسمتی از state اشاره میکند و مانند خاصیت کلی state مربوط به یک کلاس نیست. همچنین در خاصیت state یک کلاس، مقدار آن همواره به یک شیء اشاره میکند؛ اما با useState چنین اجباری وجود ندارد و میتواند هر نوع مجاز جاوا اسکریپتی باشد. برای مثال در اینجا مقدار اولیهی آن به false تنظیم شدهاست. پس از آن، خروجی این متد، قسمتی از state را که کنترل میکند، به نام متغیر isOn و تنظیم کنندهی آنرا به نام متد setIsOn، معرفی خواهد کرد. متد useState، یک متد جنریک است. یعنی نوع خروجیهای آن بر اساس نوع مقدار اولیهای که به آن انتساب داده میشود، تعیین میشود. برای مثال اگر نوع مقدار اولیهی آن، Boolean باشد، به صورت خودکار نوع متغیر و پارامتر متد خروجی از آن نیز Boolean خواهند بود.
- در آخر خاصیت toggleLight کلاس کامپوننت، تبدیل به یک متغیر یا ثابت، در تابع جاری App میشود و بجای this.setState کلی قبلی، از متد اختصاصیتر setIsOn، برای تغییر مقدار state متناظر، کمک گرفته خواهد شد. در اینجا با استفاده از prevIsOn، به مقدار دقیق پیشین متغیر isOn در state، دسترسی یافته و سپس آنرا معکوس میکنیم.
برای مشاهدهی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول AppFunction تامین شود:
// import App from "./AppClass"; import App from "./AppFunction";
معرفی useEffect Hook
فرض کنید قصد داریم برچسب دکمهی «I was clicked {this.state.count} times» را در المان Title صفحه، نمایش دهیم. برای انجام چنین کاری نیاز است با DOM API تعامل داشته باشیم؛ اما پیشتر یک چنین کاری را تنها با class components میشد انجام داد. برای رفع این محدودیت در کامپوننتهای تابعی، hook جدیدی به نام useEffect، ارائه شدهاست که باید import شود:
import React, { useEffect, useState } from "react";
در اینجا effect به معنای side effect و یا اثرات جانبی است؛ مانند: تعامل با یک API خارجی، کار با API مرورگر و کلا هر جائیکه در برنامه با دنیای خارج تعاملی وجود دارد، به عنوان یک side effect شناخته میشود. بنابراین با استفاده متد useEffect، میتوان در کامپوننتهای تابعی نیز با دنیای خارج، تعامل برقرار کرد.
import React, { useEffect, useState } from "react"; const App = () => { const [count, setCount] = useState(0); useEffect(() => { document.title = `You have clicked ${count} times`; });
در اولین بار اجرای برنامه، عبارت «You have clicked 0 times»، در عنوان صفحهی جاری، ظاهر میشود که از مقدار پیشفرض count، استفاده کردهاست. اکنون اگر بر روی دکمه، کلیک کنیم، پس از تغییر برچسب دکمه (یا همان رندر کامپوننت، پس از تغییری در state)، آنگاه عنوان نمایش داده شدهی در مرورگر نیز تغییر میکند.
یک نکته: چون useEffect دارای همان scope متغیر count است، نیاز به API خاصی برای خواندن مقدار آن در اینجا نیست.
سؤال: برای پیاده سازی چنین قابلیتی در یک کامپوننت کلاسی چه باید کرد؟ در این مثال، در ابتدای کار باید مقدار پیشفرض موجود در state را در عنوان صفحه مشاهده کرد و پس از هربار به روز رسانی state نیز باید این عنوان، تغییر کند.
برای پیاده سازی معادل متد useEffect ای که در یک کامپوننت تابعی استفاده شد، در اینجا باید از دو life-cycle hook متفاوت، به نامهای componentDidMount و componentDidUpdate، استفاده کنیم:
class App extends Component { componentDidMount() { document.title = `You have been clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You have been clicked ${this.state.count} times`; }
- همچنین چون میخواهیم به ازای هر تغییری در state نیز این عنوان تغییر کند (با هر بار کلیک بر روی دکمه)، باید از متد componentDidUpdate هم استفاده کنیم.
پاکسازی اثرات جانبی در useEffect Hook
فرض کنید قصد داریم موقعیت فعلی کرسر ماوس را در مرورگر نمایش دهیم. برای انجام اینکار در کامپوننت کلاسی، میتوان از متد componentDidMount جهت دسترسی به DOM API و استفاده از متد addEventListener آن، برای گوش فرا دادن به حرکات کرسر ماوس، استفاده کرد:
class App extends Component { componentDidMount() { // ... window.addEventListener("mousemove", this.handleMouseMove); }
handleMouseMove = event => { this.setState({ x: event.pageX, y: event.pageY }); };
class App extends Component { state = { //... x: 0, y: 0 };
<h2>Mouse Position</h2> <p>X position: {this.state.x}</p> <p>Y position: {this.state.y}</p>
- تعدادی از آنها نیازی به پاکسازی و خارج شدن از حافظه را ندارند؛ مانند به روز رسانی عنوان صفحه در مرورگر. میتوان یک چنین side effect هایی را اجرا و سپس آنها را فراموش کرد.
- اما تعدادی از side effectها را حتما باید پاکسازی کرد؛ مانند mousemove listener تعریف شدهی در مثال فوق. در اینجا زمانیکه کامپوننت جاری mount میشود، این listener را تعریف میکنیم؛ اما با Unmount شدن آن، باید این listener را حذف کرد تا برنامه دچار نشتی حافظه نشود (اگر اینکار انجام نشود، در این مثال مرورگر هنگ خواهد کرد!). روش انجام اینکار در متد componentWillUnmount، به صورت زیر است:
componentWillUnmount() { window.removeEventListener("mousemove", this.handleMouseMove); }
در این حالت نیز میتوان از متد useEffect استفاده کرد. البته ابتدا باید state شیء ای را برای نگهداری اطلاعات به روز موقعیت مکانی کرسر ماوس، ایجاد کرد:
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
سپس side effect خود را در قسمت effect function متد useEffect قرار میدهیم که آن نیز به متغیر handleMouseMove اشاره میکند:
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); useEffect(() => { // ... window.addEventListener("mousemove", handleMouseMove); }); const handleMouseMove = event => { setMousePosition({ x: event.pageX, y: event.pageY }); };
سپس برای نمایش x و y به روز رسانی شدهی در state، میتوان از markup زیر در متد رندر استفاده کرد.
<h2>Mouse Position</h2> {JSON.stringify(mousePosition, null, 2)} <br />
اگر effect function تعریف شده، دارای یک خروجی (از نوع تابع) باشد، به این معنا است که این side effect، نیاز به پاکسازی دارد و این متد را در زمان Unmount آن فراخوانی میکند:
useEffect(() => { // … // componentDidMount & componentDidUpdate window.addEventListener("mousemove", handleMouseMove); // componentWillUnmount return () => { window.removeEventListener("mousemove", handleMouseMove); }; });
سؤال: اگر بخواهیم از اجرای یک side effect، به ازای هر بار رندر جلوگیری کنیم، چه باید کرد؟
برای اینکار میتوان آرگومان دومی را به متد useEffect اضافه کرد که آرایهای از مقادیر است. توسط اعضای آن میتوان مقدار و یا مقادیری را مشخص کرد که side effect تعریف شده، به آن وابستهاست. اکنون اگر این مقدار تغییر کند، آنگاه side effect متناظر با آن نیز اجرا میشود:
useEffect(() => { document.title = `You have clicked ${count} times`; window.addEventListener("mousemove", handleMouseMove); return () => { window.removeEventListener("mousemove", handleMouseMove); }; },[]);
برای رفع این مشکل، باید به useEffect اعلام کنیم که side effect تعریف شدهی در آن، وابستهاست به مقدار count و با تغییرات آن در state، نیاز است مجددا اجرا شود:
useEffect(() => { // ... },[count]);
کار با چندین listener مختلف در متد useEffect
سؤال: آیا تنظیم یک وابستگی خاص در متد useEffect، امکان تنظیم event listenerهای دیگر را غیرممکن میکند؟
برای پاسخ به این سؤال، چند event listener دیگر را ثبت میکنیم. برای مثال یکی دیگر از APIهای مرورگر، navigator نام دارد که توسط آن میتوان وضعیت آنلاین و آفلاین بودن را به کمک خروجی خاصیت navigator.onLine آن، مشخص کرد. به کمک این API میخواهیم این وضعیت را نمایش دهیم. برای این منظور ابتدا state آنرا در کامپوننت تابعی، ایجاد میکنیم:
const [status, setStatus] = useState(navigator.onLine);
اکنون برای گوش فرا دادن به تغییرات این خاصیت (online و یا offline شدن کاربر)، نیاز است دو event listener را به کمک متد addEventListener ثبت کنیم و همچنین این متدها نیاز به پاکسازی هم دارند:
useEffect(() => { // ... window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); return () => { // ... window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; }, [count]);
const handleOnline = () => { setStatus(true); }; const handleOffline = () => { setStatus(false); };
<h2>Network Status</h2> <p> You are <strong>{status ? "online" : "offline"}</strong> </p>
برای آزمایش حالت offline آن، فقط کافی است به ابزار توسعه دهندگان مرورگر مراجعه کرده و در برگهی network آن، حالت online را offline کنید:
در این حالت هم نمایش وضعیت آنلاین بودن کاربر به درستی کار میکند و هم سایر قسمتهایی که تاکنون اضافه کردهایم. به این معنا که هر چند توسط ذکر پارامتر [count]، وابستگی خاصی برای side effect ویژهای، مشخص شدهاست، اما ما را از تنظیم event listenerهای دیگری در قسمتهای mount و unmount محروم نمیکند.
پاکسازی اثرات جانبی در useEffect Hook، زمانیکه روشی برای آن وجود ندارد
در مثالی دیگر میخواهیم از API موقعیت جغرافیایی کاربر در مرورگر یا navigator.geolocation استفاده کنیم. توسط این API هم میتوان طول و عرض جغرافیایی را به دست آورد و هم تغییرات آنرا تحت نظر قرار داد. برای مثال با بررسی این تغییرات میتوان سرعت را نیز به دست آورد.
در این حالت نیز ابتدا با تعریف state مختص به آن شروع میکنیم و اینبار به عنوان مثال، مقدار اولیهی آنرا خارج از تابع جاری تنظیم میکنیم (جهت نمایش یک گزینهی مهیای دیگر):
const initialLocationState = { latitude: null, longitude: null, speed: null }; const App = () => { // ... const [location, setLocation] = useState(initialLocationState);
useEffect(() => { // ... navigator.geolocation.getCurrentPosition(handleGeolocation); const watchId = navigator.geolocation.watchPosition(handleGeolocation); return () => { // ... navigator.geolocation.clearWatch(watchId); }; }, [count]);
یک روش برای حل این مشکل و غیرفعال کردن دستی listener متد getCurrentPosition پس از unmount، تعریف یک متغیر mounted پیش از متد useEffect است:
let mounted = true; useEffect(() => { // ... return () => { // ... mounted = false; }; }, [count]);
const handleGeolocation = event => { if (mounted) { setLocation({ latitude: event.coords.latitude, longitude: event.coords.longitude, speed: event.coords.speed }); } };
<h2>Geolocation</h2> <p>Latitude is {location.latitude}</p> <p>Longitude is {location.longitude}</p> <p>Your speed is {location.speed ? location.speed : "0"}</p>
سؤال: در اینجا شیء location چندین بار تکرار شدهاست. آیا میتوان مقادیر خواص آنرا توسط Object Destructuring نیز به دست آورد؟
پاسخ: بله. در اینجا هم Object Destructuring بر روی location state، کار میکند:
const [{ latitude, longitude, speed }, setLocation] = useState( initialLocationState );
<h2>Geolocation</h2> <p>Latitude is {latitude}</p> <p>Longitude is {longitude}</p> <p>Your speed is {speed ? speed : "0"}</p>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-01.zip
PowerShell 7.x - قسمت سیزدهم - ساخت یک Static Site Generator ساده توسط PowerShell و GitHub Actions
├── _layout │ ├── _footer.html │ ├── _header.html │ ├── _nav.html │ └── main.html ├── build ├── img ├── posts └── set-posts.ps1
- دایرکتوری layout_: درون این دایرکتوری، ساختار اصلی بلاگ را قرار دادهایم. در ادامه محتویات هر فایل را مشاهده خواهید کرد:
<!--main.html--> <!DOCTYPE html> <html dir="rtl"> {{header}} <body> {{nav}} <main> {{content}} </main> {{footer}} </body> <!--_header.html--> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{title}}</title> <link href="https://cdn.jsdelivr.net/gh/rastikerdar/samim-font@v4.0.5/dist/font-face.css" rel="stylesheet" type="text/css" /> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet"> <style> * { } </style> </head> <!--_nav.html--> <header> <nav> <div> <div> <a href="#">بلاگ من</a> </div> <div> <ul> {{nav}} </ul> </div> </div> </nav> </header> <!--_footer.html--> <footer> <div> <div> <p> تمامی حقوق محفوظ است </p> </div> </div> </footer>
- دایرکتوری build: درون این دایرکتوری، خروجیهای HTML که قرار است توسط اسکریپت PowerShell جنریت شوند، قرار خواهند گرفت. این پوشه در واقع قرار است توسط GitHub Pages میزبانی شود.
- دایرکتوری img: درون این دایرکتوری، تصاویر مربوط به هر بلاگپست را قرار خواهیم داد.
- دایرکتوری posts: درون این دایرکتوری، مطالبمان را با فرمت Markdown، قرار خواهیم داد. به عنوان مثال در ادامه یک نمونه از آن را مشاهده خواهید کرد (در کد زیر از Front Matter برای اضافه کردن یکسری متادیتای موردنیاز که حین بیلد شدن ضروری هستند استفاده شدهاست)
--- title: اولین پست من slug: hello date: 2023-04-26 author: سیروان عفیفی tags: [tag1, tag2, tag3] excerpt: این یک پست تستی است در مورد اینکه چطور میتوانیم از این قالب استفاده کنیم --- # اولین پست من ## اولین پست من ### اولین پست من #### اولین پست من ##### اولین پست من ###### اولین پست من لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی میباشد. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را میطلبد تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی و فرهنگ پیشرو در زبان فارسی ایجاد کرد. در این صورت میتوان امید داشت که تمام و دشواری موجود در ارائه راهکارها و شرایط سخت تایپ به پایان رسد وزمان مورد نیاز شامل حروفچینی دستاوردهای اصلی و جوابگوی سوالات پیوسته اهل دنیای موجود طراحی اساسا مورد استفاده قرار گیرد. <img src="/img/graphql.jpg"/>
- فایل set_post.ps1: موتور اصلی جنریت کردن صفحات HTML این فایل میباشد. در ادامه محتویات آن را مشاهده خواهید کرد. سپس هر کدام از توابع استفاده شده را یکییکی توضیح خواهیم داد:
Function Get-Layouts { $headerLayout = Get-Content -Path ./_layout/_header.html -Raw $homeLayout = Get-Content -Path ./_layout/main.html -Raw $footerLayout = Get-Content -Path ./_layout/_footer.html -Raw Return @{ Header = $headerLayout Home = $homeLayout Footer = $footerLayout } } Function Get-PostFrontMatter($postContent) { $frontMatter = [regex]::Match($postContent, "(---(?:\r?\n(?!--|\s*$).*)*)\s*((?:\r?\n(?!---).*)*\r?\n---)") Return $frontMatter } Function Set-Headings($postHtml) { Return $postHtml -Replace '<h(\d) id="(.*)">', { $level = $_.Groups[1].Value $id = $_.Groups[2].Value $class = Switch ($level) { '1' { 'text-4xl font-bold mb-2' } '2' { 'text-3xl font-bold mb-2' } '3' { 'text-2xl font-bold mb-2' } '4' { 'text-xl font-bold mb-2' } '5' { 'text-lg font-bold mb-2' } '6' { 'text-base font-bold mb-2' } } "<h$level class='$class' id='$id'>" } } Function ConvertTo-Slug { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$String ) process { $slug = $String -replace '[^\w\s-]', '' # remove non-word characters except hyphens $slug = $slug -replace '\s+', '-' # replace whitespace with a single hyphen $slug = $slug -replace '^-+|-+$', '' # remove leading/trailing hyphens $slug = $slug.ToLower() # convert to lowercase Write-Output $slug } } Function Get-Posts { $markdownPosts = Get-ChildItem -Path ./posts -Filter *.md $posts = @() Foreach ($post in $markdownPosts) { $postContent = Get-Content -Path $post.FullName -Raw $frontMatter = Get-PostFrontMatter $postContent $frontMatterObject = $frontMatter | ConvertFrom-Yaml $slug = $frontMatterObject.slug ?? (ConvertTo-Slug "$($frontMatterObject.date)-$($frontMatterObject.title)") $body = $postContent.Replace($frontMatter.Value, "") | ConvertFrom-Markdown $postHtml = $layouts.Home -replace '{{header}}', $layouts.Header ` -replace '{{title}}', $frontMatterObject.title ` -replace '{{nav}}', (Set-Navs) ` -replace '{{content}}', $body.Html ` -replace '{{footer}}', $layouts.Footer $postHtml = Set-Headings $postHtml $postHtml | Out-File -FilePath ./build/$slug.html $posts += @{ title = $frontMatterObject.title slug = $slug excerpt = $frontMatterObject.excerpt date = $frontMatterObject.date author = $frontMatterObject.author body = $body.Html } } Return $posts } Function Set-Archive { $posts = Get-Posts $archive = @() $archive = @" <ul> $($posts | ForEach-Object { "<li><a href='$($_.slug).html'>$($_.title)</a></li>" }) </ul> "@ Return $archive -join "`r`n" } Function Copy-ToBuild { $layouts = Get-Layouts $latestPosts = Get-Posts | ForEach-Object { @" <div> <img src="https://via.placeholder.com/300x200" alt="$($_.title)"> <h2>$($_.title)</h2> <p>$($_.excerpt)</p> <a href="$($_.slug).html">ادامه مطلب</a> </div> "@ } $homeLayout = $layouts.Home -replace '{{header}}', $layouts.Header ` -replace '{{nav}}', (Set-Navs) ` -replace '{{title}}', 'بلاگ من' ` -replace '{{content}}', ('<div>' + $latestPosts + '</div>') ` -replace '{{footer}}', $layouts.Footer $homeLayout | Out-File -FilePath ./build/index.html Copy-Item -Path ./img -Destination ./build -Recurse -Force } Function Set-Navs { $navs = @( @{ title = "صفحه اصلی" url = "/sample" }, @{ title = "درباره ما" url = "/sample/about.html" }, @{ title = "تماس با ما" url = "/sample/contact.html" } ) $navLayout = Get-Content -Path ./_layout/_nav.html -Raw $navLayout -replace '{{nav}}', ($navs | ForEach-Object { "<li><a href=""$($_.url)""text-gray-700 hover:text-gray-800 m-2"">$($_.title)</a></li>" }) } Copy-ToBuild
name: Deploy static content to Pages on: push: branches: - main workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: "pages" cancel-in-progress: false jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Install powershell-yaml module shell: pwsh run: | Set-PSRepository PSGallery -InstallationPolicy Trusted Install-Module powershell-yaml -ErrorAction Stop - name: Setup Pages uses: actions/configure-pages@v3 - name: Build Static Site shell: pwsh run: | . ./set-posts.ps1 - name: Upload Static Site Artifact uses: actions/upload-pages-artifact@v1 with: path: build - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2
- درون دایرکتوری posts، مطلب موردنظر را به همراه Front Matter زیر ایجاد کرده و سپس محتویات مطلب را بعد از آن وارد کنید:
--- title: اولین پست من slug: hello date: 2023-04-26 author: سیروان عفیفی tags: [tag1, tag2, tag3] excerpt: این یک پست تستی است در مورد اینکه چطور میتوانیم از این قالب استفاده کنیم --- content
- تغییرات ایجاد شده را کامیت و سپس پوش کنید. به محض پوش کردن تغییرات، GitHub Actions پروسه بیلد را انجام خواهد داد و بلافاصله میتوانید تغییرات را مشاهده نمائید.
کدهای این مطلب را میتوانید از اینجا دریافت کنید.