ASP.NET MVC #19
مروری بر امکانات Caching اطلاعات در ASP.NET MVC
در برنامههای وب، بالاترین حد کارآیی برنامهها از طریق بهینه سازی الگوریتمها حاصل نمیشود، بلکه با بکارگیری امکانات Caching سبب خواهیم شد تا اصلا کدی اجرا نشود. در ASP.NET MVC این هدف از طریق بکارگیری فیلتری به نام OutputCache میسر میگردد:
using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
return View();
}
}
}
همانطور که ملاحظه میکنید، OutputCache را به یک اکشن متد یا حتی به یک کنترلر نیز میتوان اعمال کرد. به این ترتیب HTML نهایی حاصل از View متناظر با اکشن متد جاری فراخوانی شده، Cache خواهد شد. سپس زمانیکه درخواست بعدی به سرور ارسال میشود، نتیجه دریافت شده، همان اطلاعات Cache شده قبلی است و عملا در سمت سرور کدی اجرا نخواهد شد. در اینجا توسط پارامتر Duration، مدت زمان معتبر بودن کش حاصل، برحسب ثانیه مشخص میشود. VaryByParam مشخص میکند که اگر متدی پارامتری را دریافت میکند، آیا باید به ازای هر مقدار دریافتی، مقادیر کش شده متفاوتی ذخیره شوند یا خیر. در اینجا چون متد Index پارامتری ندارد، از مقدار none استفاده شده است.
مثال یک
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس کنترلر جدید Home را نیز به آن اضافه نمائید:
using System;
using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}
همچنین کدهای View متد Index را نیز به نحو زیر تغییر دهید:
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>
در اینجا نمایش دو زمان دریافتی از کنترلر و زمان محاسبه شده در View را مشاهده میکنید. هدف این است که بررسی کنیم آیا فیلتر OutputCache بر روی این دو مقدار تاثیری دارد یا خیر.
برنامه را اجرا نمائید. سپس چند بار صفحه را Refresh کنید. مشاهده خواهید کرد که هر دو زمان یاد شده تا 60 ثانیه، تغییری نخواهند کرد و حاصل نهایی از Cache خواهنده میشود.
کاربرد یک چنین حالتی برای مثال نمایش اطلاعات بازدیدهای یک سایت است. نباید به ازای هر کاربر وارد شده به سایت، یکبار به بانک اطلاعاتی مراجعه کرد و آمار جدیدی را تهیه نمود. یا برای نمونه اگر جایی قرار است اطلاعات وضعیت آب و هوا نمایش داده شود، بهتر است این اطلاعات، مثلا هر نیم ساعت یکبار به روز شود و نه به ازای هر بازدید جدید از سایت، توسط صدها بازدید کننده همزمان. یا برای مثال کش کردن خروجی فید RSS یک بلاگ به مدت چند ساعت نیز ایده خوبی است. از این لحاظ که اگر اطلاعات بلاگ شما روزی یکبار به روز میشود، نیازی نیست تا به ازای هر برنامه فیدخوان، یکبار اطلاعات از بانک اطلاعاتی دریافت شده و پروسه رندر نهایی فید صورت گیرد. منوهای پویای یک سایت نیز در همین رده قرار میگیرند. دریافت اطلاعات منوهای پویای سایت به ازای هر درخواست رسیده کاربری جدید، کار اشتباهی است. این اطلاعات نیز باید کش شوند تا بار سرور کاهش یابد. البته تمام اینها زمانی میسر خواهند شد که اطلاعات سمت سرور کش شوند.
مثال دو
همان مثال قبلی را در اینجا جهت بررسی پارامتر VaryByParam به نحو زیر تغییر میدهیم:
using System;
using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index(string parameter)
{
ViewBag.Msg = parameter ?? string.Empty;
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}
در اینجا یک پارامتر به متد Index اضافه شده است. مقدار آن به ViewBag.Msg انتساب داده شده و سپس در View ، در بین تگهای h2 نمایش داده خواهد شد. همچنین یک فرم ساده هم جهت ارسال parameter به متد Index اضافه شده است:
@{
ViewBag.Title = "Index";
}
<h2>@ViewBag.Msg</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>
@using (Html.BeginForm())
{
@Html.TextBox("parameter")
<input type="submit" />
}
اکنون برنامه را اجرا کنید. در TextBox نمایش داده شده یکبار مثلا بنویسید Test1 و فرم را به سرور ارسال نمائید. سپس مقدار Test2 را وارد کرده و ارسال نمائید. در بار دوم، خروجی صفحه همانند زمانی است که مقدار Test1 ارسال شده است. علت این است که مقدار VaryByParam به none تنظیم شده است و صرفنظر از ورودی کاربر، همان اطلاعات کش شده قبلی بازگشت داده خواهد شد. برای رفع این مشکل، متد Index را به نحو زیر تغییر دهید، به طوریکه مقدار VaryByParam به نام پارامتر متد جاری اشاره کند:
[OutputCache(Duration = 60, VaryByParam = "parameter")]
public ActionResult Index(string parameter)
در ادامه مجددا برنامه را اجرا کنید. اکنون یکبار مقدار Test1 را به سرور ارسال کنید. سپس مقدار Test2 را ارسال نمائید. مجددا همین دو مرحله را با مقادیر Test1 و Test2 تکرار کنید. مشاهده خواهید کرد که اینبار اطلاعات بر اساس مقدار پارامتر ارسالی کش شده است.
تنظیمات متفاوت OutputCache
الف) VaryByParam : اگر مساوی none قرار گیرد، همواره همان مقدار کش شده قبلی نمایش داده میشود. اگر مقدار آن به نام پارامتر خاصی تنظیم شود، اطلاعات کش شده بر اساس مقادیر متفاوت پارامتر دریافتی، متفاوت خواهند بود. در اینجا پارامترهای متفاوت را با یک «,» میتوان از هم جدا ساخت. اگر تعداد پارامترها زیاد است میتوان مقدار VaryByParam را مساوی با * قرار داد. در این حالت به ازای مقادیر متفاوت دریافتی پارامترهای مختلف، اطلاعات مجزایی در کش قرار خواهد گرفت. این روش آخر آنچنان توصیه نمیشود چون سربار بالایی دارد و حجم بالایی از اطلاعات بر اساس پارامترهای مختلف، باید در کش قرار گیرند.
ب) Location : مکان قرارگیری اطلاعات کش شده را مشخص میکند. مقدار آن نیز بر اساس یک enum به نام OutputCacheLocation مشخص میگردد. در این حالت برای مثال میتوان مکانهای Server، Client و ServerAndClient را مقدار دهی نمود. مقدار Downstream به معنای کش شدن اطلاعات بر روی پروکسی سرورهای بین راه و یا مرورگرها است. پیش فرض آن Any است که ترکیبی از Server و Downstream میباشد.
اگر قرار است اطلاعات یکسانی به تمام کاربران نمایش داده شود، مثلا محتوای لیست یک منوی پویا، محل قرارگیری اطلاعات کش باید سمت سرور باشد. اگر نیاز است به ازای هر کاربر محتوای اطلاعات کش شده متفاوت باشد، بهتر است محل سمت کلاینت را مقدار دهی نمود.
ج) VaryByHeader : اطلاعات، بر اساس هدرهای مشخص شده، کش میشوند. برای مثال مرسوم است که از Accept-Language در اینجا استفاده شود تا اطلاعات مثلا فرانسوی کش شده، به کاربر آلمانی تحویل داده نشود.
د) VaryByCustom : در این حالت نام یک متد استاتیک تعریف شده در فایل global.asax.cs باید مشخص گردد. توسط این متد کلید رشتهای اطلاعاتی که قرار است کش شود، بازگشت داده خواهد شد.
ه) SqlDependency : در این حالت اطلاعات تا زمانیکه تغییری در جداول بانک اطلاعاتی SQL Server صورت نگیرد، کش خواهد شد.
و) Nostore : به پروکسی سرورهای بین راه و همچنین مرورگرها اطلاع میدهد که اطلاعات را نباید کش کنند. اگر قسمت اعتبار سنجی این سری را به خاطر داشته باشید، چنین تعریفی در قسمت Remote validation بکارگرفته شد:
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
و یا میتوان برای اینکار یک فیلتر سفارشی را نیز تهیه کرد:
using System;
using System.Web.Mvc;
namespace MvcApplication16.Helper
{
/// <summary>
/// Adds "Cache-Control: private, max-age=0" header,
/// ensuring that the responses are not cached by the user's browser.
/// </summary>
public class NoCachingAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
filterContext.HttpContext.Response.CacheControl = "private";
filterContext.HttpContext.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));
}
}
}
کار این فیلتر اضافه کردن هدر «Cache-Control: private, max-age=0» به Response است.
استفاده از فایل Web.Config برای معرفی تنظیمات Caching
یکی دیگر از تنظیمات ویژگی OutputCache، پارامتر CacheProfile است که امکان تنظیم آن در فایل web.config نیز وجود دارد. برای نمونه تنظیمات زیر را به قسمت system.web فایل وب کانفیگ برنامه اضافه کنید:
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="Aggressive" location="ServerAndClient" duration="300"/>
<add name="Mild" duration="100" location="Server" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
سپس مثلا برای استفاده از پروفایلی به نام Aggressive، خواهیم داشت:
[OutputCache(CacheProfile = "Aggressive", VaryByParam = "parameter")]
public ActionResult Index(string parameter)
استفاده از ویژگی به نام donut caching
تا اینجا به این نتیجه رسیدیم که OutputCache، کل خروجی یک View را بر اساس پارامترهای مختلفی که دریافت میکند، کش خواهد کرد. در این بین اگر بخواهیم تنها قسمت کوچکی از صفحه کش نشود چه باید کرد؟ برای حل این مشکل قابلیتی به نام cache substitution که به donut caching هم معروف است (چون آنرا میتوان به شکل یک donut تصور کرد!) در ASP.NET MVC قابل استفاده است.
@{ Response.WriteSubstitution(ctx => DateTime.Now.ToShortTimeString()); }
همانطور که ملاحظه میکنید برای تعریف یک چنین اطلاعاتی باید از متد Response.WriteSubstitution در یک view استفاده کرد. در این مثال، نمایش زمان جاری معرفی شده، صرف نظر از وضعیت کش صفحه جاری، کش نخواهد شد.
عکس آن هم ممکن است. فرض کنید که صفحه جاری شما از سه partial view تشکیل شده است. هر کدام از این partial viewها نیز مزین به OutpuCache هستند. اما صفحه اصلی درج کننده اطلاعات این سه partial view فاقد ویژگی Output کش است. در این حالت تنها اطلاعات این partial viewها کش خواهند شد و سایر قسمتهای صفحه با هر بار درخواست از سرور، مجددا بر اساس اطلاعات جدید به روز خواهند شد. حالت توصیه شده نیز همین مورد است و متد Response.WriteSubstitution را صرفا جهت اطلاعات عمومی درنظر داشته باشید.
استفاده از امکانات Data Caching به صورت مستقیم
مطالبی که تا اینجا عنوان شدند به کش کردن اطلاعات Response اختصاص داشتند. اما امکانات Caching موجود، به این مورد خلاصه نشده و میتوان اطلاعات و اشیاء را نیز کش کرد. برای مثال اطلاعات «با سطح دسترسی عمومی» دریافتی از بانک اطلاعاتی توسط یک کوئری را نیز میتوان کش کرد. جهت انجام اینکار میتوان از متدهای HttpRuntime.Cache.Insert و یا HttpContext.Cache.Insert استفاده کرد. استفاده از HttpContext.Cache.Insert حین نوشتن Unit tests دردسر کمتری دارد و mocking آن ساده است؛ از این جهت که بر اساس HttpContextBase تعریف شدهاست.
در ادامه یک کلاس کمکی نوشتن اطلاعات در cache و سپس بازیابی آنرا ملاحظه میکنید:
using System;
using System.Web;
using System.Web.Caching;
namespace MvcApplication16.Helper
{
public static class CacheManager
{
public static void CacheInsert(this HttpContextBase httpContext, string key, object data, int durationMinutes)
{
if (data == null) return;
httpContext.Cache.Add(
key,
data,
null,
DateTime.Now.AddMinutes(durationMinutes),
TimeSpan.Zero,
CacheItemPriority.AboveNormal,
null);
}
public static T CacheRead<T>(this HttpContextBase httpContext, string key)
{
var data = httpContext.Cache[key];
if (data != null)
return (T)data;
return default(T);
}
public static void InvalidateCache(this HttpContextBase httpContext, string key)
{
httpContext.Cache.Remove(key);
}
}
}
و برای استفاده از آن در یک اکشن متد، ابتدا نیاز است فضای نام این کلاس تعریف شود و سپس برای نمونه متد HttpContext.CacheInsert در دسترس خواهد بود. HttpContext یکی از خواص تعریف شده در شیء کنترلر است که با ارث بری کنترلرها از آن، همواره در دسترس میباشد.
در اینجا برای نمونه اطلاعات یک لیست جنریک دریافتی از بانک اطلاعاتی را مثلا 10 دقیقه (بسته به پارامتر durationMinutes آن) میتوان کش کرد و سپس توسط متد CacheRead آنرا دریافت نمود. اگر متد CacheRead نال برگرداند به معنای خالی بودن کش است. بنابراین یکبار اطلاعات را از بانک اطلاعاتی دریافت نموده و سپس آنرا کش خواهیم کردیم.
البته هستند ORMهایی که یک چنین کارهایی را به صورت توکار پشتیبانی کنند. به مکانیزم آن، Second level cache هم گفته میشود؛ به علاوه امکان استفاده از پروایدرهای دیگری را بجز کش IIS برای ذخیره سازی موقتی اطلاعات نیز فراهم میکنند.
همچنین باید دقت داشت این اعداد مدت زمان، هیچگونه ضمانتی ندارند. اگر IIS احساس کند که با کمبود منابع مواجه شده است، به سادگی شروع به حذف اطلاعات موجود در کش خواهد کرد.
نکته امنیتی مهم!
به هیچ عنوان از OutputCache در صفحاتی که نیاز به اعتبار سنجی دارند، استفاده نکنید و به همین جهت در قسمت کش کردن اطلاعات، بر روی «اطلاعاتی با سطح دسترسی عمومی» تاکید شد.
فرض کنید کارمندی به صفحه مشاهده فیش حقوقی خودش مراجعه کرده است. این ماه هم اضافه حقوق آنچنانی داشته است. شما هم این صفحه را به مدت سه ساعت کش کردهاید. آیا میتوانید تصور کنید اگر همین گزارش کش شده با این اطلاعات، به سایر کارمندان نمایش داده شود چه قشقرقی به پا خواهد شد؟!
بنابراین هیچگاه اطلاعات مخصوص به یک کاربر اعتبار سنجی شده را کش نکنید و «تنها» اطلاعاتی نیاز به کش شدن دارند که عمومی باشند. برای مثال لیست آخرین اخبار سایت؛ لیست آخرین مدخلهای فید RSS سایت؛ لیست اطلاعات منوی عمومی سایت؛ لیست تعداد کاربران مراجعه کننده به سایت در طول یک روز؛ گزارش آب و هوا و کلیه اطلاعاتی با سطح دسترسی عمومی که کش شدن آنها مشکل ساز نباشد.
به صورت خلاصه هیچگاه در کدهای شما چنین تعریفی نباید مشاهده شود:
[Authorize]
[OutputCache(Duration = 60)]
public ActionResult Index()
در خیلی مواقع ملاحظه میشود که برای نمایش تعدادی از رکوردهای یک جدول در پایگاه داده، کل مقادیر موجود درآن توسط یک دستور select به دست میآید و صفحهبندی خروجی، به کنترلهای موجود سپرده میشود. اگر پایگاه داده ما دارای تعداد زیادی رکورد باشد، آن موقع است که دچار مشکل میشویم. فرض کنید به طور همزمان ۵ نفر (که تعداد زیادی نیستند) از برنامه ما که شامل ۱۰۰۰۰۰ سطر داده میباشد استفاده کنند و در هر صفحه، ۱۰ رکورد نمایش داده شود و صفحهبندی ما از نوع معقولی نباشد. در این صورت به جای اینکه با ۵×۱۰ رکورد داده را بارگزاری کنیم، ۵×۱۰۰۰۰۰ رکورد یعنی ۵۰۰۰۰۰ رکورد را برای به دست آوردن ۵۰ رکورد بارگزاری میکنیم. در زیر روشی شرح داده میشود که توسط آن، این سربار اضافه از روی برنامه و سرورهای مربوطه حذف شود. به stored procedure و توضیحات مربوط به آن توجه فرمایید :
CREATE PROCEDURE sp_PagedItems ( @Page int, @RecsPerPage int ) AS -- We don't want to return the # of rows inserted -- into our temporary table, so turn NOCOUNT ON SET NOCOUNT ON --Create a temporary table CREATE TABLE #TempItems ( ID int IDENTITY, Name varchar(50), Price currency ) -- Insert the rows from tblItems into the temp. table INSERT INTO #TempItems (Name, Price) SELECT Name,Price FROM tblItem ORDER BY Price -- Find out the first and last record we want DECLARE @FirstRec int, @LastRec int SELECT @FirstRec = (@Page - 1) * @RecsPerPage SELECT @LastRec = (@Page * @RecsPerPage + 1) -- Now, return the set of paged records, plus, an indiciation of we -- have more records or not! SELECT *, MoreRecords = ( SELECT COUNT(*) FROM #TempItems TI WHERE TI.ID >= @LastRec ) FROM #TempItems WHERE ID > @FirstRec AND ID < @LastRec -- Turn NOCOUNT back OFF SET NOCOUNT OFF
در مرحله بعد شماره اولین و آخرین سطر مورد نظر را بر اساس پارامترهای ورودی محاسبه کرده و در متغیرهای @FirstRec و @LastRec میریزیم.
برای
استفاده از این کد فقط کافیست که پارامترهای ورودی را مقداردهی نمایید.
مثلا اگر میخواهید در یک کنترل Grid از آن استفاده کنید باید ابتدا یک
کوئری داشته باشید که تعداد کل سطرها را به شما بدهد و بر اساس این مقدار
تعداد صفحات مورد نظر را به دست آورید. پس از آن با کلیک روی هر کدام از
شماره صفحات آن را به عنوان مقدار به پارامتر مورد نظر بفرستید و از آن لذت
ببرید.
برپایی پیشنیازها
در اینجا برای بررسی 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
- استفاده گزارشات به صورت کلاسهای #C در برنامه شما (با استفاده از Designer و Save as آن به صورت کلاس #C). در این صورت گزارش همزمان با کامپایل برنامه شما، کامپایل شده و در زمان فراخوانی، دیگر نیاز به کامپایل مجدد آن نیست.
اشکال: در صورتیکه قالب گزارش شما نیاز به تغییر داشته باشد، برنامهی شما باید با کلاسهای جدید گزارش مجددا کامپایل شود. - میتوان گزارش را از طریق برنامه Designer به صورت اسمبلی ( dll ) ذخیره کرد.
اشکال: این روش برای مدیریت گزارشها در طول ارتقاء آنها مفید نمیباشد. - از گزارش کامپایل شده استفاده نکنید و به جای آن از تفسیر عبارات در گزارش استفاده کنید. برای فعالسازی این حالت، خصوصیت Calculation Mode گزارش را در پنجره خصوصیتها در برنامه Designer بر روی Interpretation قرار دهید.
اشکال: این روش به این معنی است که تمامی اصطلاحات زبان C# / VB.NET تفسیر نمیشوند. همچنین استفاده از رخدادهای مربوط به اجزاء گزارش غیرممکن میباشد. - کامپایل و رندر گزارش، در دامنهی برنامهی دیگری انجام شود. در این مورد میتوان پس از رندر گزارش، اسمبلی را در دامنهی برنامه جاری بارگذاری کرد و فایل قفل شدهی آن، بعدا حذف خواهد شد. با استفاده از متد ()CreateReportInNewAppDomain از کلاس StiReport میتوان گزارش را ایجاد کرد و با استفاده از متد ()UnloadReportAppDomain آن را بارگذاری کرد.
اشکال در این روش متدهای RegData و RegBusinessObject با سرعت خیلی کمی اجرا میشوند؛ چرا که هدایت دادهها به دامنهی دیگری استفاده شده است. ایجاد و بارگذاری در دامنهی برنامه نیازمند زمان میباشد. - کارآمدترین روش: کامپایل گزارش تنها در زمان اولین فراخوانی انجام شود و هر زمان که درخواست فراخوانی گزارش انجام میشود، گزارش را از اسمبلی کامپایل شده، در حافظه بارگذاری میکنیم. در این مورد تنها یک کپی از گزارش در حافظه ذخیره میشود و تنها یک اسمبلی در درایو وجود دارد. پوشه مربوط به گزارشهای کامپایل شده میتواند قبل از استفاده از گزارشات حذف شود، چرا که در اولین درخواست گزارش، دوباره ایجاد خواهد شد.
مثال:
var report = new StiReport(); report.Load( "Report.mrt" ); var folder = Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData ); folder = Path.Combine( folder, "Stimulsoft\\CompiledReports" ); folder = Path.Combine( folder, System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion() ); var compiledReportFile = Path.Combine( folder, report.GetReportAssemblyCacheName() ); if ( File.Exists( compiledReportFile ) ) report = StiReport.GetReportFromAssembly( compiledReportFile, true ); else { if ( !Directory.Exists( folder ) ) Directory.CreateDirectory( folder ); report.Compile( compiledReportFile ); } report.RegBusinessObject( "TestData", new { Title = "Vahid " + DateTime.Now.Millisecond } ); report.Render( false );
ASP.NET MVC #21
آشنایی با تکنیکهای Ajax در ASP.NET MVC
اهمیت آشنایی با Ajax، ارائه تجربه کاربری بهتری از برنامههای وب، به مصرف کنندگان نهایی آن میباشد. به این ترتیب میتوان درخواستهای غیرهمزمانی (asynchronous) را با فرمت XML یا Json به سرور ارسال کرد و سپس نتیجه نهایی را که حجم آن نسبت به یک صفحه کامل بسیار کمتر است، به کاربر ارائه داد. غیرهمزمان بودن درخواستها سبب میشود تا ترد اصلی رابط کاربری برنامه قفل نشده و کاربر در این بین میتواند به سایر امور خود بپردازد. به این ترتیب میتوان برنامههای وبی را که شبیه به برنامههای دسکتاپ هستند تولید نمود؛ کل صفحه مرتبا به سرور ارسال نمیشود، flickering و چشمک زدن صفحه کاهش خواهد یافت (چون نیازی به ترسیم مجدد کل صفحه نخواهد بود و عموما قسمتی جزئی از یک صفحه به روز میشود) یا بدون نیاز به ارسال کل صفحه به سرور، به کاربری خواهیم گفت که آیا اطلاعاتی که وارد کرده است معتبر میباشد یا نه (نمونهای از آن را در قسمت Remote validation اعتبار سنجی اطلاعات ملاحظه نمودید).
مروری بر محتویات پوشه Scripts یک پروژه جدید ASP.NET MVC در ویژوال استودیو
با ایجاد هر پروژه ASP.NET MVC جدیدی در ویژوال استودیو، یک سری اسکریپت هم به صورت خودکار در پوشه Scripts آن اضافه میشوند. تعدادی از این فایلها توسط مایکروسافت پیاده سازی شدهاند. برای مثال:
MicrosoftAjax.debug.js
MicrosoftAjax.js
MicrosoftMvcAjax.debug.js
MicrosoftMvcAjax.js
MicrosoftMvcValidation.debug.js
MicrosoftMvcValidation.js
این فایلها از ASP.NET MVC 3 به بعد، صرفا جهت سازگاری با نگارشهای قبلی قرار دارند و استفاده از آنها اختیاری است. بنابراین با خیال راحت آنها را delete کنید! روش توصیه شده جهت پیاده سازی ویژگیهای Ajax ایی، استفاده از کتابخانههای مرتبط با jQuery میباشد؛ از این جهت که 100ها افزونه برای کار با آن توسط گروه وسیعی از برنامه نویسها در سراسر دنیا تاکنون تهیه شده است. به علاوه فریم ورک jQuery تنها منحصر به اعمال Ajax ایی نیست و از آن جهت دستکاری DOM (document object model) و CSS صفحه نیز میتوان استفاده کرد. همچنین حجم کمی نیز داشته، با انواع و اقسام مرورگرها سازگار است و مرتبا هم به روز میشود.
در این پوشه سه فایل دیگر پایه کتابخانه jQuery نیز قرار دارند:
jquery-xyz-vsdoc.js
jquery-xyz.js
jquery-xyz.min.js
فایل vsdoc برای ارائه نهایی برنامه طراحی نشده است. هدف از آن ارائه Intellisense بهتری از jQuery در VS.NET میباشد. فایلی که باید به کلاینت ارائه شود، فایل min یا فشرده شده آن است. اگر به آن نگاهی بیندازیم به نظر obfuscated مشاهده میشود. علت آن هم حذف فواصل، توضیحات و همچنین کاهش طول متغیرها است تا اندازه فایل نهایی به حداقل خود کاهش پیدا کند. البته این فایل از دیدگاه مفسر جاوا اسکریپت یک مرورگر، فایل بینقصی است!
اگر علاقمند هستید که سورس اصلی jQuery را مطالعه کنید، به فایل jquery-xyz.js مراجعه نمائید.
محل الحاق اسکریپتهای عمومی مورد نیاز برنامه نیز بهتر است در فایل master page یا layout برنامه باشد که به صورت پیش فرض اینکار انجام شده است.
سایر فایلهای اسکریپتی که در این پوشه مشاهده میشوند، یک سری افزونه عمومی یا نوشته شده توسط تیم ASP.NET MVC برفراز jQuery هستند.
به چهار نکته نیز حین استفاده از اسکریپتهای موجود باید دقت داشت:
الف) همیشه از متد Url.Content همانند تعاریفی که در فایل Views\Shared\_Layout.cshtml مشاهده میکنید، برای مشخص سازی مسیر ریشه سایت، استفاده نمائید. به این ترتیب صرفنظر از آدرس جاری صفحه، همواره آدرس صحیح قرارگیری پوشه اسکریپتها در صفحه ذکر خواهد شد.
ب) ترتیب فایلهای js مهم هستند. ابتدا باید کتابخانه اصلی jQuery ذکر شود و سپس افزونههای آنها.
ج) اگر اسکریپتهای jQuery در فایل layout سایت تعریف شدهاند؛ نیازی به تعریف مجدد آنها در Viewهای سایت نیست.
د) اگر View ایی به اسکریپت ویژهای جهت اجرا نیاز دارد، بهتر است آنرا به شکل یک section داخل view تعریف کرد و سپس به کمک متد RenderSection این قسمت را در layout سایت مقدار دهی نمود. مثالی از آنرا در قسمت 20 این سری مشاهده نمودید (افزودن نمایش جمع هر ستون گزارش).
یک نکته
اگر آخرین به روز رسانیهای ASP.NET MVC را نیز نصب کرده باشید، فایلی به نام packages.config به صورت پیش فرض به هر پروژه جدید ASP.NET MVC اضافه میشود. به این ترتیب VS.NET به کمک NuGet این امکان را خواهد یافت تا شما را از آخرین به روز رسانیهای این کتابخانهها مطلع کند.
آشنایی با Ajax Helpers توکار ASP.NET MVC
اگر به تعاریف خواص و متدهای کلاس WebViewPage دقت کنیم:
using System;
namespace System.Web.Mvc
{
public abstract class WebViewPage<TModel> : WebViewPage
{
protected WebViewPage();
public AjaxHelper<TModel> Ajax { get; set; }
public HtmlHelper<TModel> Html { get; set; }
public TModel Model { get; }
public ViewDataDictionary<TModel> ViewData { get; set; }
public override void InitHelpers();
protected override void SetViewData(ViewDataDictionary viewData);
}
}
علاوه بر خاصیت Html که وهلهای از آن امکان دسترسی به Html helpers توکار ASP.NET MVC را در یک View فراهم میکند، خاصیتی به نام Ajax نیز وجود دارد که توسط آن میتوان به تعدادی متد AjaxHelper توکار دسترسی داشت. برای مثال توسط متد Ajax.ActionLink میتوان قسمتی از صفحه را به کمک ویژگیهای Ajax، به روز رسانی کرد.
مثالی در مورد به روز رسانی قسمتی از صفحه به کمک متد Ajax.ActionLink
ابتدا نیاز است فایل Views\Shared\_Layout.cshtml را اندکی ویرایش کرد. برای این منظور سطر الحاق jquery.unobtrusive-ajax.min.js را به فایل layout برنامه اضافه نمائید (اگر این سطر اضافه نشود، متد Ajax.ActionLink همانند یک لینک معمولی رفتار خواهد کرد):
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
</head>
سپس مدل ساده و منبع داده زیر را نیز به پروژه اضافه کنید:
namespace MvcApplication18.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}
using System.Collections.Generic;
namespace MvcApplication18.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
for (int i = 0; i < 1000; i++)
{
list.Add(new Employee { Id = i + 1, Name = "name " + i });
}
return list;
}
}
}
در ادامه کنترلر جدیدی را به برنامه با محتوای زیر اضافه کنید:
using System.Linq;
using System.Web.Mvc;
using MvcApplication18.Models;
namespace MvcApplication18.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost] //for IE-8
public ActionResult EmployeeInfo(int? id)
{
if (!Request.IsAjaxRequest())
return View("Error");
if (!id.HasValue)
return View("Error");
var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return View("Error");
return PartialView(viewName: "_EmployeeInfo", model: data);
}
}
}
بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. یک View خالی را به آن اضافه نمائید. همچنین بر روی متد EmployeeInfo کلیک راست کرده و با انتخاب گزینه Add view در صفحه ظاهر شده یک partial view را اضافه نمائید. جهت تمایز بین partial view و view هم بهتر است نام partial view با یک underline شروع شود.
کدهای partial view مورد نظر را به نحو زیر تغییر دهید:
@model MvcApplication18.Models.Employee
<strong>Name:</strong> @Model.Name
سپس کدهای View متناظر با متد Index را نیز به صورت زیر اعمال کنید:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div id="EmployeeInfo">
@Ajax.ActionLink(
linkText: "Get Employee-1 info",
actionName: "EmployeeInfo",
controllerName: "Home",
routeValues: new { id = 1 },
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
})
</div>
<div id="Progress" style="display: none">
<img src="@Url.Content("~/Content/images/loading.gif")" alt="loading..." />
</div>
توضیحات جزئیات کدهای فوق
متد Ajax.ActionLink لینکی را تولید میکند که با کلیک کاربر بر روی آن، اطلاعات اکشن متد واقع در کنترلری مشخص، به کمک ویژگیهای jQuery Ajax دریافت شده و سپس در مقصدی که توسط UpdateTargetId مشخص میگردد، بر اساس مقدار InsertionMode، درج خواهد شد (میتواند قبل از آن درج شود یا پس از آن و یا اینکه کل محتوای مقصد را بازنویسی کند). HttpMethod آن هم به POST تنظیم شده تا با IE مشکلی نباشد. از این جهت که IE پیغامهای GET را کش میکند و مساله ساز خواهد شد. توسط پارامتر routeValues، آرگومان مورد نظر به متد EmployeeInfo ارسال خواهد شد.
به علاوه یکی دیگر از خواص کلاس AjaxOptions، برای معرفی حالت بروز خطایی در سمت سرور به نام OnFailure در نظر گرفته شده است. در اینجا میتوان نام یک متد JavaScript ایی را مشخص کرده و پیغام خطای عمومی را در صورت فراخوانی آن به کاربر نمایش داد. یا توسط خاصیت Confirm آن میتوان یک پیغام را پیش از ارسال اطلاعات به سرور به کاربر نمایش داد.
به این ترتیب در مثال فوق، id=1 به متد EmployeeInfo به صورت غیرهمزمان ارسال میگردد. سپس کارمندی بر این اساس یافت شده و در ادامه partial view مورد نظر بر اساس اطلاعات کاربر مذکور، رندر خواهد شد. نتیجه کار، در یک div با id مساوی EmployeeInfo درج میگردد (InsertionMode.Replace). متد Ajax.ActionLink از این جهت داخل div تعریف شدهاست که پس از کلیک کاربر و جایگزینی محتوا، محو شود. اگر نیازی به محو آن نبود، آنرا خارج از div تعریف کنید.
عملیات دریافت اطلاعات از سرور ممکن است مدتی طول بکشد (برای مثال دریافت اطلاعات از بانک اطلاعاتی). به همین جهت بهتر است در این بین از تصاویری که نمایش دهنده انجام عملیات است، استفاده شود. برای این منظور یک div با id مساوی Progress تعریف شده و id آن به LoadingElementId انتساب داده شده است. این div با توجه به display: none آن، در ابتدای امر به کاربر نمایش داده نخواهد شد؛ در آغاز کار دریافت اطلاعات از سرور توسط متد Ajax.ActionLink نمایان شده و پس از خاتمه کار مجددا مخفی خواهد شد.
به علاوه اگر به کدهای فوق دقت کرده باشید، از متد Request.IsAjaxRequest نیز استفاده شده است. به این ترتیب میتوان تشخیص داد که آیا درخواست رسیده از طرف jQuery Ajax صادر شده است یا خیر. البته آنچنان روش قابل ملاحظهای نیست؛ چون امکان دستکاری Http Headers همیشه وجود دارد؛ اما بررسی آن ضرری ندارد. البته این نوع بررسیها را در ASP.NET MVC بهتر است تبدیل به یک فیلتر سفارشی نمود؛ به این ترتیب حجم if و else نویسی در متدهای کنترلرها به حداقل خواهد رسید. برای مثال:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
base.OnActionExecuting(filterContext);
}
else
{
throw new InvalidOperationException("This operation can only be accessed via Ajax requests");
}
}
}
و برای استفاده از آن خواهیم داشت:
[AjaxOnly]
public ActionResult SomeAjaxAction()
{
return Content("Hello!");
}
در مورد کلمه unobtrusive در قسمت بررسی نحوه اعتبار سنجی اطلاعات، توضیحاتی را ملاحظه نمودهاید. در اینجا نیز از ویژگیهای data-* برای معرفی پارامترهای مورد نیاز حین ارسال اطلاعات به سرور، استفاده میگردد. برای مثال خروجی متد Ajax.ActionLink به شکل زیر است. به این ترتیب امکان حذف کدهای جاوا اسکریپت از صفحه فراهم میشود و توسط یک فایل jquery.unobtrusive-ajax.min.js که توسط تیم ASP.NET MVC تهیه شده، اطلاعات مورد نیاز به سرور ارسال خواهد گردید:
<a data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
href="/Home/EmployeeInfo/1">Get Employee-1 info</a>
در کل این روش قابلیت نگهداری بهتری نسبت به روش اسکریپت نویسی مستقیم داخل صفحات را به همراه دارد. به علاوه جدا سازی افزونه اسکریپت وفق دهنده این اطلاعات با متد jQuery.Ajax از صفحه جاری، که امکان کش شدن آنرا به سادگی میسر میسازد.
به روز رسانی اطلاعات قسمتی از صفحه بدون استفاده از متد Ajax.ActionLink
الزامی به استفاده از متد Ajax.ActionLink و فایل jquery.unobtrusive-ajax.min.js وجود ندارد. اینکار را مستقیما به کمک jQuery نیز میتوان به نحو زیر انجام داد:
<a href="#" onclick="LoadEmployeeInfo()">Get Employee-1 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfo() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfo",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning a simple text, not json
complete: function (xhr, status) {
var data = xhr.responseText;
if (status === 'error' || !data) {
//handleError
}
else {
$('#EmployeeInfo').html(data);
}
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}
توضیحات:
توسط متد jQuery.Ajax نیز میتوان درخواستهای Ajax ایی خود را به سرور ارسال کرد. در اینجا type نوع http verb مورد نظر را مشخص میکند که به POST تنظیم شده است. Url آدرس کنترلر را دریافت میکند. البته حین استفاده از متد توکار Ajax.ActionLink، این لینک به صورت خودکار بر اساس تعاریف مسیریابی برنامه تنظیم میشود. اما در صورت استفاده مستقیم از jQuery.Ajax باید دقت داشت که با تغییر تعاریف مسیریابی برنامه نیاز است تا این Url نیز به روز شود.
سه سطر بعدی نوع اطلاعاتی را که باید به سرور POST شوند مشخص میکند. نوع json است و همچنین contentType آن برای ارسال اطلاعات یونیکد ضروری است. از متد JSON.stringify برای تبدیل اشیاء به رشته کمک گرفتهایم. این متد در تمام مرورگرهای امروزی به صورت توکار پشتیبانی میشود و استفاده از آن سبب خواهد شد تا اطلاعات به نحو صحیحی encode شده و به سرور ارسال شوند. بنابراین این رشته ارسالی اطلاعات را به صورت دستی تهیه نکنید؛ چون کاراکترهای زیادی هستند که ممکن است مشکل ساز شده و باید پیش از ارسال به سرور اصطلاحا escape یا encode شوند.
متداول است از پارامتر success برای دریافت نتیجه عملیات متد jQuery.Ajax استفاده شود. اما در اینجا از پارامتر complete آن استفاده شده است. علت هم اینجا است که return PartialView یک رشته را بر میگرداند. پارامتر success انتظار دریافت خروجی از نوع json را دارد. به همین جهت در این مثال خاص باید از پارامتر complete استفاده کرد تا بتوان به رشته بدون فرمت خروجی بدون مشکل دسترسی پیدا کرد.
به علاوه چون از یک section برای تعریف اسکریپتهای مورد نیاز استفاده کردهایم، برای درج خودکار آن در هدر صفحه باید قسمت هدر فایل layout برنامه را به صورت زیر مقدار دهی کرد:
@RenderSection("javascript", required: false)
دسترسی به اطلاعات یک مدل در View، به کمک jQuery Ajax
اگر جزئی از صفحه که قرار است به روز شود، پیچیده است، روش استفاده از partial viewها توصیه میشود؛ برای مثال میتوان اطلاعات یک مدل را به همراه یک گرید کامل از اطلاعات، رندر کرد و سپس در صفحه درج نمود. اما اگر تنها به اطلاعات چند خاصیت از مدلی نیاز داشتیم، میتوان از روشهایی با سربار کمتر نیز استفاده کرد. برای مثال متد جدید زیر را به کنترلر Home اضافه کنید:
[HttpPost] //for IE-8
public ActionResult EmployeeInfoData(int? id)
{
if (!Request.IsAjaxRequest())
return Json(false);
if (!id.HasValue)
return Json(false);
var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return Json(false);
return Json(data);
}
سپس View برنامه را نیز به نحو زیر تغییر دهید:
<a href="#" onclick="LoadEmployeeInfoData()">Get Employee-2 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfoData() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfoData",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning the json data
success: function (result) {
if (result) {
alert(result.Id + ' - ' + result.Name);
}
hideProgress();
},
error: function (result) {
alert(result.status + ' ' + result.statusText);
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}
در این مثال، کنترلر برنامه، اطلاعات مدل را تبدیل به Json کرده و بازگشت خواهد داد. سپس میتوان به اطلاعات این مدل و خواص آن در View برنامه، در پارامتر success متد jQuery.Ajax، مطابق کدهای فوق دسترسی یافت. اینبار چون خروجی کنترلر تعریف شده از نوع Json است، امکان استفاده از پارامتر success فراهم شده است. همه چیز هم در اینجا خودکار است؛ تبدیل یک شیء به Json و برعکس.
یک نکته: اگر نوع متد کنترلر، HttpGet باشد، نیاز خواهد بود تا پارامتر دوم متد بازگشت Json، مساوی JsonRequestBehavior.AllowGet قرار داده شود.
ارسال اطلاعات فرمها به سرور، به کمک ویژگیهای Ajax
متد کمکی توکار دیگری به نام Ajax.BeginForm در ASP.NET MVC وجود دارد که کار ارسال غیرهمزمان اطلاعات یک فرم را به سرور انجام داده و سپس اطلاعاتی را از سرور دریافت و قسمتی از صفحه را به روز خواهد کرد. مکانیزم کاری کلی آن بسیار شبیه به متد Ajax.ActionLink میباشد. در ادامه با تکمیل مثال قسمت جاری، به بررسی این ویژگی خواهیم پرداخت.
ابتدا متد جستجوی زیر را به کنترلر برنامه اضافه کنید:
[HttpPost] //for IE-8
public ActionResult SearchEmployeeInfo(string data)
{
if (!Request.IsAjaxRequest())
return Content(string.Empty);
if (string.IsNullOrWhiteSpace(data))
return Content(string.Empty);
var employeesList = EmployeeDataSource.CreateEmployees();
var list = employeesList.Where(x => x.Name.Contains(data)).ToList();
if (list == null || !list.Any())
return Content(string.Empty);
return PartialView(viewName: "_SearchEmployeeInfo", model: list);
}
سپس بر روی نام متد کلیک راست کرده و گزینه add view را انتخاب کنید. در صفحه باز شده، گزینه create a stronlgly typed view را انتخاب کرده و قالب scaffolding را هم بر روی list قرار دهید. سپس گزینه ایجاد partial view را نیز انتخاب کنید. نام آنرا هم _SearchEmployeeInfo وارد نمائید. برای نمونه خروجی حاصل به نحو زیر خواهد بود:
@model IEnumerable<MvcApplication18.Models.Employee>
<table>
<tr>
<th>
Name
</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}
</table>
تا اینجا یک متد جستجو را ایجاد کردهایم که میتواند لیستی از رکوردهای کارمندان را بر اساس قسمتی از نام آنها که توسط کاربری جستجو شده است، بازگشت دهد. سپس این اطلاعات را به partial view مورد نظر ارسال کرده و یک جدول را بر اساس آن تولید خواهیم نمود.
اکنون به فایل Index.cshtml مراجعه کرده و فرم Ajax ایی زیر را اضافه نمائید:
@using (Ajax.BeginForm(actionName: "SearchEmployeeInfo",
controllerName: "Home",
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
}))
{
@Html.TextBox("data")
<input type="submit" value="Search" />
}
اینبار بجای استفاده از متد Html.BeginForm از متد Ajax.BeginForm استفاده شده است. به کمک آن اطلاعات Html.TextBox تعریف شده، به کنترلر Home و متد SearchEmployeeInfo آن، بر اساس HttpMethod تعریف شده، ارسال گردیده و نتیجه آن در یک div با id مساوی EmployeeInfo درج میگردد. همچنین اگر اطلاعاتی یافت نشد، به کمک متد return Content یک رشته خالی بازگشت داده میشود.
متد Ajax.BeginForm نیز از ویژگیهای data-* برای تعریف اطلاعات مورد نیاز ارسالی به سرور استفاده میکند. بنابراین نیاز به سطر الحاق jquery.unobtrusive-ajax.min.js در فایل layout برنامه جهت وفق دادن این اطلاعات unobtrusive به اطلاعات مورد نیاز متد jQuery.Ajax وجود دارد.
<form action="/Home/SearchEmployeeInfo" data-ajax="true"
data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
id="form0" method="post">
<input id="data" name="data" type="text" value="" />
<input type="submit" value="Search" />
</form>
کتابخانه کمکی «ASP.net MVC Awesome - jQuery Ajax Helpers»
علاوه بر متدهای توکار Ajax همراه با ASP.NET MVC، سایر علاقمندان نیز یک سری Ajax helper را بر اساس افزونههای jQuery تدارک دیدهاند که از آدرس زیر قابل دریافت هستند:
http://awesome.codeplex.com/
افزودن فرمها به کمک jQuery.Ajax و فعال سازی اعتبار سنجی سمت کلاینت
در ASP.NET MVC چون ViewState حذف شده است، امکان تزریق فرمهای جدید به صفحه یا به روز رسانی قسمتی از صفحه توسط jQuery Ajax به سهولت و بدون دریافت پیغام «viewstate is corrupted» در حین ارسال اطلاعات به سرور، میسر است.
در این حالت باید به یک نکته مهم نیز دقت داشت: «اعتبار سنجی سمت کلاینت دیگر کار نمیکند». علت اینجا است که در حین بارگذاری متداول یک صفحه، متد زیر به صورت خودکار فراخوانی میگردد:
$.validator.unobtrusive.parse("#{form-id}");
اما با به روز رسانی قسمتی از صفحه، دیگر اینچنین نخواهد بود و نیاز است این فراخوانی را دستی انجام دهیم. برای مثال:
$.ajax
({
url: "/{controller}/{action}/{id}",
type: "get",
success: function(data)
{
$.validator.unobtrusive.parse("#{form-id}");
}
});
//or
$.get('/{controller}/{action}/{id}', function (data) { $.validator.unobtrusive.parse("#{form-id}"); });
شبیه به همین مساله را حین استفاده از Ajax.BeginForm نیز باید مد نظر داشت:
@using (Ajax.BeginForm(
"Action1",
"Controller",
null,
new AjaxOptions {
OnSuccess = "onSuccess",
UpdateTargetId = "result"
},
null)
)
{
<input type="submit" value="Save" />
}
var onSuccess = function(result) {
// enable unobtrusive validation for the contents
// that was injected into the <div id="result"></div> node
$.validator.unobtrusive.parse("#result");
};
در این مثال در پارامتر UpdateTargetId، مجددا یک فرم رندر میشود. بنابراین اعتبار سنجی سمت کلاینت آن دیگر کار نخواهد کرد مگر اینکه با مقدار دهی خاصیت OnSuccess، مجددا متد unobtrusive.parse را فراخوانی کنیم.
الگوریتم های رند کردن اعداد
سؤال مربوط به حالت نخ نمای Page.IsPostBack نیست. مربوط به حالتی است که دقیقا در اولین بار مشاهدهی عادی یک صفحه، Page_Load دوبار یا بیشتر (!) اجرا میشود.
الف) برنامهی ASP.Net 1.x خود را به نگارشهای 2+ ارتقاء دادهاید.
در این حالت هر چند VS.Net پیغام تبدیل با موفقیت یک پروژهی قدیمی را به شما ارائه خواهد داد اما یک سری موارد را پس از تبدیل، باید اصلاح کرد.
پروژههای قدیمی ASP.Net در روال InitializeComponent خود سطر زیر را همانند یک پروژه WinForm و امثال آن برای معرفی روال رخداد گردان Page_Load دارند.
this.Load += new System.EventHandler(this.Page_Load);
اما در پروژههای ASP.Net 2.0 به بعد دیگر از این روال خبری نیست. پس در این پروژهها کامپایلر چگونه متوجه خواهد شد که Page_Load واقعا یک روال رخ داد گردان است و نه یک روال معمولی؟ در پروژههای جدید VS.Net ، خاصیت AutoEventWireup صفحه به true تنظیم شده و دیگر نیازی به معرفی صریح روالهای رخ داد گردانی مانند Page_Load و یا Page_Init نبوده و تشخیص آنها به صورت خودکار انجام میشود.
<% @Page AutoEventWireup="true" %>
برای حل این مشکل و سازگاری بهتر با نگارشهای جدیدتر، سطر تعریف دستی روال رخ داد گردان متد Page_Load را حذف کنید.
لازم به ذکر است که این سیم کشی خودکار تنها برای متدهای زیر انجام خواهد شد و نسبت به حذف سایر موارد موجود اقدام نکنید!
Page_PreInit
Page_Init
Page_InitComplete
Page_PreLoad
Page_Load
Page_LoadComplete
Page_DataBind
Page_SaveStateComplete
Page_PreRender
Page_PreRenderComplete
Page_Unload
Page_Error
Page_AbortTransaction
Page_CommitTransaction
ب) وجود هر نوع دکمهی تصویری یا کلا تصویری با ویژگی src مقدار دهی نشده در صفحه
مرورگر IE با این مساله مشکلی ندارد. اما فایرفاکسهای جدید اگر به src مقدار دهی نشدهی تصویری برخورد کنند دقیقا آدرس جاری صفحه را بجای مقدار src قرار داده و مجددا صفحه را درخواست میکنند (و البته این مورد ارتباط مستقیمی به ASP.Net یا PHP و امثال آن ندارد و یک مسالهی عمومی است). این مورد سبب خواهد شد که Page_Load صفحه، نه فقط دوبار بلکه به تعداد بار src خالی تصاویری که در صفحه وجود دارند، بر اساس درخواستهای مجدد فایرفاکس از سرور اجرا شود. (مرورگر IE بجای فراخوانی آدرس صفحه جاری، یک null/ را به انتهای آدرس جاری اضافه کرده و آنرا فراخوانی میکند. بنابراین سبب اجرای مجدد هیچ روالی نخواهد شد.)
مآخذ:
Inside AutoEventWireup
How Firefox Handles Empty SRC tags
ترفندهای یونیکد برای زبانهای راست به چپ
یک نکتهی تکمیلی: اگر میخواهید کاربران موبایل به سادگی بتوانند اعداد صحیح را وارد کنند، از یک ورودی با ویژگیهای type=tel و inputmode=numeric استفاده کنید:
<input type="tel" inputmode="numeric">
مزیت اینکار، نمایش خودکار صفحه کلید تمام عددی تنظیم شدهی بر روی حالت انگلیسی است؛ به این ترتیب مشکل «... در دستگاههای موبایل، زمانیکه صفحه کلید در حالت فارسی قرار دارد، اعداد را هم فارسی وارد میکند ...» اصلا رخ نمیدهد.