var smr = new StateMachineRequest(workflowData, startId); smr.GuardClauseFromToTrigger = new StateMachineRequest.GuardClauseDelegate(this.OnFromStateToState); smr.OnEntry = new StateMachineRequest.EntryExitDelegate(this.OnEntryState); smr.OnExit = new StateMachineRequest.EntryExitDelegate(this.OnExitState); smr.Configure(); public bool OnFromStateToState(string id) { // TODO check can go to next state return true; } public void OnEntryState(string stateId) { // TODO } public void OnExitState(string stateId) { // TODO save data + save state + send an email ,Etc }
<style> :checked { width: 50px; height: 50px; } </style> <input type="checkbox" checked="checked"/> <input type="checkbox"/> <input type="radio" name="test"/> <input type="radio" name="test" checked="checked"/> <input type="radio" name="test"/>
|
|
|
|
| Selector | نسخه CSS |
3.2 | 9.6 | 9.0 | 3.5 | 4.0 | :checked | 3 |
42- :indeterminate
<style> :indeterminate { width: 50px; height: 50px; } </style> <input type="checkbox" id="chk1"/> <input type="checkbox" id="chk2" checked="checked"/> <input type="checkbox" id="chk3"/> <script> document.getElementById("chk1").indeterminate=true; </script>
|
|
|
|
| Selector | نسخه CSS |
No | No | No | No | No | :indeterminate | 4 |
43- :default
<style> :default { width: 50px; height: 50px; background: lime; } </style> <form> <input type="checkbox" checked="checked" /> <input type="checkbox" /> <input type="radio" name="radio1" /> <input type="radio" name="radio1" checked="checked" /> <input type="radio" name="radio1" /> <input type="submit" value="Default" /> <input type="submit" value="Submit 2" /> </form>
|
|
|
|
| Selector | نسخه CSS |
No | No | No | No | No | :default | 4 |
44- :root
<style> :root { background: khaki; } </style>
|
|
|
|
| Selector | نسخه CSS |
3.2 | 9.6 | 9.0 | 3.5 | 4.0 | :root | 3 |
45- :after
<style> form:after { content: "[ * : Required]"; color: red; } input+span:after { content: ' * '; color: red; } </style> <form> <div><input type="text" /><span></span></div> <div><input type="password" /><span></span></div> <div><input type="email" /><span></span></div> </form>
|
|
|
|
| Selector | نسخه CSS |
3.1 | 7.0 | 9.0 | 3.5 | 4.0 | :after | 2 |
46- :before
<style> form:before { content: "[ * : Required]"; color: red; } input + span:before { content: ' * '; color: red; } </style> <form> <div><input type="text" /><span></span></div> <div><input type="password" /><span></span></div> <div><input type="email" /><span></span></div> </form>
|
|
|
|
| Selector | نسخه CSS |
3.1 | 7.0 | 9.0 | 3.5 | 4.0 | :before | 2 |
47- ::selection
<style> ::selection { background: navy; color: orange; } </style> <p> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim. Fusce est. </p>
|
|
|
|
| Selector | نسخه CSS |
3.1 | 9.6 | 9.0 | 2.0 -moz- | 4.0 | ::selection | در CSS 3 معرفی و سپس حذف شد ولی امکان بازگشت مجدد وجود دارد |
48- :not(S1,S2)
<style> :not([readonly]) { background: yellow; } </style> <input type="text"/> <input type="text" readonly="readonly"/> <input type="text"/> <input type="text" readonly="readonly" />
|
|
|
|
| Selector | نسخه CSS |
3.2 | 9.6 | 9.0 | 3.5 | 4.0 | :not(S1,S2) | 3 |
49- :matches(S1,S2)
<style> :matches([readonly]) { background: yellow; } </style> <input type="text" /> <input type="text" readonly="readonly" /> <input type="text" /> <input type="text" readonly="readonly" />
|
|
|
|
| Selector | نسخه CSS |
No | No | No | No | No | :matches(S1,S2) | 4 |
50- :has(S1,S2)
<style> :has(>span) { color: red; } :has(+div) { color: blue; } </style> <div> <p> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas <span>porttitor</span> congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim. Fusce est. </p> <h1>Header 1</h1> <div> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim. Fusce est. </div>
|
|
|
|
| Selector | نسخه CSS |
No | No | No | No | No | :has(S1,S2) | 4 |
در پنجره ای که باز میشود ، لیست را نام گذاری میکنیم
سپس یک View ایحاد میکنیم . برای این کار از نوار Ribbon بالا ، روی List View کلیک میکینم :
سپس نامی برای این View در نظر میگیریم و این Viewرا به عنوان نمای پیش فرض انتخاب میکنیم
در این مرحله نمای نگاهی به لیست در مرور گر میاندازیم و تعدادی داده برای آزمایش در آن وارد میکنیم
حال برای اعمال تغییرات به SPD برمیگردیم و روی View ساخته شده کلیک میکنیم تا وارد محیط ویرایش آن شویم .
در این مثال میخواهم روی فیلد نام شرکت فیلتر انجام دهم
سپس برای تغییر نحوه نمایش ، روی Set Style کلیک میکنیم :
پس از اعمال تغییرات میتوانید تغییرات را در کد مشاهده کنید
برای اعمال تغییرات در خروجی و ستونهای آن ، روی Add/Remove Columns کلیک میکنیم :
برای مثال در اینجا ستون Job Title را به لیست اضافه کردم :
همچنین برای اعمال Inline Editing به عناصر لیست میتوانید روی دکمه ای با همین نام در Ribbon کلیک کنید
در ادامه میتوانید عملیات مرتب سازی و گروه بندی اطلاعات را هم تنها با چند کلیک مدیریت کنید
در اینجا گروه بندی و مرتب سازی را روی نام شرکت اعمال میکنیم :
تا کنون خروجی پیش نمایش داده شده در SPD به این صورت است :
تغییرات را ذخیره میکنیم :
و خروجی را در مرور گر مشاهده میکنید (همانطور که مشاهده میکنید ، ویرایش خطی برای آیتمها فعال شده است و همین طور گروه بندی بر اساس نام شرکت و مرتب سازی نیز اعمال شده است . همچنین رنگ سطر هایی که نام شرکت آنها Alfa است ، تغییر کرده است ) :
اکنون میخواهم قسمتی از تغییرات اعمال شده را از طریق مرور گر ( و نه SPD) تغییر دهم . برای این کار روی List در Ribbon کلیک میکنیم :
در قسمت Group by تنظیمات ستون را روی none قرار میدهیم تا گروه بندی اعمال نشود :
به اشتراک گذاری کدهای ui در اپلیکیشن های ios , android
ASP.NET MVC #8
معرفی HTML Helpers
یک HTML Helper تنها یک متد است که رشتهای را بر میگرداند و این رشته میتواند حاوی هر نوع محتوای دلخواهی باشد. برای مثال میتوان از HTML Helpers برای رندر تگهای HTML، مانند img و input استفاده کرد. یا به کمک HTML Helpers میتوان ساختارهای پیچیدهتری مانند نمایش لیستی از اطلاعات دریافت شده از بانک اطلاعاتی را پیاده سازی کرد. به این ترتیب حجم کدهای تکراری تولید رابط کاربری در Viewهای برنامههای ASP.NET MVC به شدت کاهش خواهد یافت، به همراه قابلیت استفاده مجدد از متدهای الحاقی HTML Helpers در برنامههای دیگر.
HTML Helpers در ASP.NET MVC معادل کنترلهای ASP.NET Web forms هستند اما نسبت به آنها بسیار سبکتر میباشند؛ برای مثال به همراه ViewState و همچنین Event model نیستند.
ASP.NET MVC به همراه تعدادی متد HTML Helper توکار است و برای دسترسی به آنها شیء Html که وهلهای از کلاس توکار HtmlHelper میباشد، در تمام Viewها قابل استفاده است.
نحوه ایجاد یک HTML Helper سفارشی
از دات نت سه و نیم به بعد امکان توسعه اشیاء توکار فریم ورک، به کمک متدهای الحاقی (extension methods) میسر شده است. برای نوشتن یک HTML Helper نیز باید همین شیوه عمل کرد و کلاس HtmlHelper را توسعه داد. در ادامه قصد داریم یک HTML Helper را جهت رندر تگ label در صفحه ایجاد کنیم. برای این منظور پوشهی جدیدی به نام Helper را به پروژه اضافه نمائید (جهت نظم بیشتر). سپس کلاس زیر را به آن اضافه کنید:
using System;
using System.Web.Mvc;
namespace MvcApplication4.Helpers
{
public static class LabelExtensions
{
public static string MyLabel(this HtmlHelper helper, string target, string text)
{
return string.Format("<label for='{0}'>{1}</label>", target, text);
}
}
}
همانطور که ملاحظه میکنید متد Label به شکل یک متد الحاقی توسعه دهنده کلاس HtmlHelper که تنها یک رشته را بر میگرداند، تعریف شده است. اکنون برای استفاده از این متد در View دلخواهی خواهیم داشت:
@using MvcApplication4.Helpers
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
@Html.MyLabel("firstName", "First Name:")
ابتدا فضای نام مرتبط با متد الحاقی باید پیوست شود و سپس از طریق شیء Html میتوان به این متد الحاقی دسترسی پیدا کرد. اگر برنامه را اجرا کنید، این خروجی را مشاهده خواهیم کرد. چرا؟
Index
<label for='firstName'>First Name:</label>
علت این است که Razor، اطلاعات را Html encoded به مرورگر تحویل میدهد. برای تغییر این رویه باید اندکی متد الحاقی تعریف شده را تغییر داد:
using System.Web.Mvc;
namespace MvcApplication4.Helpers
{
public static class LabelExtensions
{
public static MvcHtmlString MyLabel(this HtmlHelper helper, string target, string text)
{
return MvcHtmlString.Create(string.Format("<label for='{0}'>{1}</label>", target, text));
}
}
}
تنها تغییر صورت گرفته، استفاده از MvcHtmlString بجای string معمولی است تا Razor آنرا encode نکند.
تعریف HTML Helpers سفارشی به صورت عمومی:
میتوان فضای نام MvcApplication4.Helpers این مثال را عمومی کرد. یعنی بجای اینکه بخواهیم در هر View آنرا ابتدا تعریف کنیم، یکبار آنرا همانند تعاریف اصلی یک برنامه ASP.NET MVC، عمومی معرفی میکنیم. برای این منظور فایل web.config موجود در پوشه Views را باز کنید (و نه فایل web.config قرار گرفته در ریشه اصلی برنامه). سپس فضای نام مورد نظر را در قسمت namespaces صفحات اضافه نمائید:
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="MvcApplication4.Helpers"/>
</namespaces>
به این ترتیب متدهای الحاقی تعریف شده در فضای نام MvcApplication4.Helpers، در تمام Viewهای برنامه در دسترس خواهند بود.
استفاده از کلاس TagBuilder برای تولید HTML Helpers سفارشی:
using System.Web.Mvc;
namespace MvcApplication4.Helpers
{
public static class LabelExtensions
{
public static MvcHtmlString MyNewLabel(this HtmlHelper helper, string target, string text)
{
var labelTag = new TagBuilder("label");
labelTag.MergeAttribute("for", target);
labelTag.InnerHtml = text;
return MvcHtmlString.Create(labelTag.ToString());
}
}
}
در فضای نام System.Web.Mvc، کلاسی وجود دارد به نام TagBuilder که کار تولید تگهای HTML، مقدار دهی ویژگیها و خواص آنها را بسیار ساده میکند و روش توصیه شدهای است برای تولید متدهای HTML Helper. یک نمونه از کاربرد آنرا برای بازنویسی متد MyLabel ذکر شده در اینجا ملاحظه میکنید.
شبیه به همین کلاس، کلاس دیگری به نام HtmlTextWriter در فضای نام System.Web.UI برای انجام اینگونه کارها وجود دارد.
نوشتن HTML Helpers ویژه، به کمک امکانات Razor
نوع دیگری از این متدهای کمکی، Declarative HTML Helpers نام دارند. از این جهت هم Declarative نامیده شدهاند که مستقیما درون فایلهای cshtml یا vbhtml به کمک امکانات Razor قابل تعریف هستند. تولید این نوع متدهای کمکی به این شکل نسبت به مثلا روش TagBuilder سادهتر است، چون توسط Razor به سادگی و به نحو طبیعیتری میتوان تگهای HTML و کدهای مورد نظر را با هم ترکیب کرد (این رفتار طبیعی و روان، یکی از اهداف Razor است).
به عنوان مثال، تعاریف همان کلاسهای Product و Products قسمت قبل (قسمت هفتم) را در نظر بگیرید. با همان کنترلر و View ایی که ذکر شد.
سپس برای تعریف این نوع خاص از HTML Helpers/Razor Helpers باید به این نحو عمل کرد:
الف) در ریشه پروژه یا سایت، پوشهی جدیدی به نام App_Code ایجاد کنید (دقیقا به همین نام. این پوشه، جزو پوشههای ویژه ASP.NET است).
ب) بر روی این پوشه کلیک راست کرده و گزینه Add|New Item را انتخاب کنید.
ج) در صفحه باز شده، MVC 3 Partial Page/Razor را یافته و مثلا نام ProductsList.cshtml را وارد کرده و این فایل را اضافه کنید.
د) محتوای این فایل جدید را به نحو زیر تغییر دهید:
@using MvcApplication4.Models
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@item.Name ($@item.Price)</li>
}
</ul>
}
در اینجا نحوه تعریف یک helper method مخصوص Razor را مشاهده میکنید که با کلمه @helper شروع شده است. مابقی آن هم ترکیب آشنای code و markup هستند که به کمک امکانات Razor به این شکل روان میسر شده است.
اکنون اگر Viewایی بخواهد از این اطلاعات استفاده کند تنها کافی است به نحو زیر عمل نماید:
@model List<MvcApplication4.Models.Product>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
@ProductsList.GetProductsList(@Model)
ابتدا نام فایل ذکر شده بعد نام متد کمکی تعریف شده در آن. Model هم در اینجا به لیستی از محصولات اشاره میکند.
همچنین چون در پوشه app_code قرار گرفته، تمام Viewها به اطلاعات آن دسترسی خواهند داشت. علت هم این است که ASP.NET به صورت خودکار محتوای این پوشه ویژه را همواره کامپایل میکند و در اختیار برنامه قرار میدهد.
به علاوه در این فایل ProductsList.cshtml، باز هم میتوان متدهای helper دیگری را اضافه کرد و از این بابت محدودیتی ندارد. همچنین میتوان این متد helper را مستقیما داخل یک View هم تعریف کرد. بدیهی است در این حالت قابلیت استفاده مجدد از آنرا به همراه داشتن Viewهایی تمیز و کم حجم، از دست خواهیم داد.
جهت تکمیل بحث
Turn your Razor helpers into reusable libraries
try { int i=0; string s = "hello"; i = Convert.ToInt32(s); } catch (Exception ex) { Console.WriteLine("Error"); } Console.WriteLine("I am here!");
پس فلسفه استفاده از بخش finally چیست؟
در قسمت finally منابع تخصیص داده شده در try را آزاد میکنیم. کد موجود در این قسمت به هر روی اجرا میشود چه استثناء رخ دهد چه ندهد. البته اگر استثناء رخ داده شده در لیست استثناء هایی که برای آنها catch انجام دادیم نباشد، قسمت finally هم عمل نخواهد کرد مگر اینکه از catch به صورت سراسری استفاده کنیم.
اما مهمترین مزیتی که finally ایجاد میکند در این است که حتی اگر در قسمت try با استفاده از دستوراتی مثل return یا break یا continue از ادامه کد منصرف شویم و مثلا مقداری برگردانیم، چه خطا رخ دهد یا ندهد کد موجود در finally اجرا میشود در حالی که کد نوشته شده بعد از try catch finally فقط در صورتی اجرا میشود که به طور منطقی اجرای برنامه به آن نقطه برسد. اجازه بدهید با یک مثال توضیح دهم. اگر کد زیر را اجرا کنیم:
public static int GetMyInt() { try { for (int i=10;i>=0;i--) Console.WriteLine(10/i); return 1; } catch { Console.WriteLine("Error!"); } finally { Console.WriteLine("ok"); } Console.WriteLine("can you reach here?"); return -1; }
برنامه خطای تقسیم بر صفر میدهد اما با توجه به کدی که نوشتیم، عدد -1 به خروجی خواهد رفت. در عین حال عبارت ok و can you reach here در خروجی چاپ شده است. اما حال اگر مشکل تقسیم بر صفر را حل کنیم، آیا باز هم عبارت can you reach here در خروجی چاپ خواهد شد؟
public static int GetMyInt() { try { for (int i=10;i>=1;i--) Console.WriteLine(10/i); return 1; } catch { Console.WriteLine("Error!"); } finally { Console.WriteLine("ok"); } Console.WriteLine("can you reach here?"); return -1; }
مشاهده میکنید که مقدار 1 برگردانده میشود و عبارت can you reach here در خروجی چاپ نمیشود ولی همچنان عبارت ok که در finally ذکر شده در خروجی چاپ میشود. یک مثال خوب استفاده از چنین وضعیتی، زمانی است که شما یک ارتباط با بانک اطلاعاتی باز میکنید، و نتیجه یک عملیات را با دستور return به کاربر بر میگردانید. مسئله این است که در این وضعیت چگونه ارتباط با دیتابیس بسته شده و منابع آزاد میگردند؟ اگر در حین عملیات بانک اطلاعاتی، خطایی رخ دهد یا ندهد، و شما دستور آزاد سازی منابع و بستن ارتباط را در داخل قسمت finally نوشته باشید، وقتی دستور return فراخوانی میشود، ابتدا منابع آزاد و سپس مقدار به خروجی بر میگردد.
public int GetUserId(string nickname) { SqlConnection connection = new SqlConnection(...); SqlCommand command = connection.CreateCommand(); command.CommandText = "select id from users where nickname like @nickname"; command.Parameters.Add(new SqlParameter("@nickname", nickname)); try { connection.Open(); return Convert.ToInt32(command.ExecuteScalar()); } catch(SqlException exception) { // some exception handling return -1; } finally { if (connection.State == ConnectionState.Open) connection.Close(); } // if all things works, you can not reach here }
در ادامه روشهای مختلف ارسال درخواستهای Ajax را توسط jQuery و همچنین معادلهای XMLHttpRequest و Fetch API آنها بررسی میکنیم.
ارسال درخواستهای GET
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد |
توسط jQuery |
fetch('/my/name').then(function(response) { if (response.ok) { return response.text(); } else { throw new Error(); } }).then( function success(name) { console.log('my name is ' + name); }, function failure() { console.error('Name request failed!'); } ); | var xhr = new XMLHttpRequest(); xhr.open('GET', '/my/name'); xhr.onload = function() { if (xhr.status >= 400) { console.error('Name request failed!'); } else { console.log('my name is ' + xhr.responseText); } }; xhr.onerror = function() { console.error('Name request failed!'); }; xhr.send(); | $.get('/my/name').then( function success(name) { console.log('my name is ' + name); }, function failure() { console.error('Name request failed!'); } ); |
jQuery برای پشتیبانی از درخواستهای Ajax، متد ویژهای را به نام ()ajax ارائه میکند که برای ارسال درخواستهایی از هر نوع، مانندGET، POST و غیره کاربرد دارد. همچنین برای بعضی از نوعها، متدهای کوتاهتری را مانند ()get و ()post نیز در اختیار برنامه نویس قرار میدهد. جاوا اسکریپت خالص و Web API مرورگرها نیز دو شیء XMLHttpRequest و fetch را برای ارسال درخواستهای غیرهمزمان، ارائه میکند. XMLHttpRequest در تمام مرورگرهای قدیمی و جدید پشتیبانی میشود، اما fetch API مدتی است که در غالب مرورگرهای امروزی در دسترس است. در جدول فوق روش ارسال درخواستهای Ajax از نوع GET را توسط این سه روش مشاهده میکنید.
در این مثالها درخواستی به آدرس my/name سمت سرور ارسال شده و انتظار میرود که یک plaintext حاوی متن کاربر بازگشت داده شود که در نهایت در console لاگ میشود.
- در حالت استفادهی از jQuery در صورت بازگشت موفقیت آمیز پاسخی از طرف سرور، متد success و در غیراینصورت متد failure اجرا میشود. باید درنظر داشت که متد ajax جیکوئری، چیزی بیشتر از یک محصور کنندهی اشیاء XMLHttpRequest نیست.
- در حالت کار با XMLHttpRequest باید اندکی بیشتر تایپ کرد؛ اما اصول کار یکی است. در اینجا onload زمانی فراخوانی میشود که پاسخی از سرور دریافت شده و عملیات خاتمه یافتهاست؛ هرچند این پاسخ میتواند یک خطا نیز باشد. به همین جهت باید status آنرا بررسی کرد. اگر رخداد onerror فراخوانی شد، یعنی درخواست، در سطوح بسیار پایین آن مانند بروز یک خطای CORS با شکست مواجه شدهاست.
همانطور که مشاهده میکنید در حالت کار با XMLHttpRequest جاوا اسکریپت از اشیاء Promise پشتیبانی نمیکند که این کمبود با معرفی fetch API برطرف شدهاست که نمونهای از آنرا با متد then متصل به fetch مشاهده میکنید؛ دقیقا مشابه متد ajax جیکوئری که آن نیز یک Promise را بازگشت میدهد.
تفاوت Promise جیکوئری با fetch API در این است که جیکوئری در صورتیکه یک status code بیانگر خطا را دریافت کند، قسمت failure را اجرا میکند؛ اما fetch API مانند اشیاء XMLHttpRequest تنها در صورت بروز خطاهای سطح پایین درخواست، این متد را فراخوانی خواهد کرد. هرچند اگر در اینجا response.ok نبود، میتوان با صدور یک استثناء به رفتاری شبیه به jQuery رسید و قسمت then failure را به صورت خودکار اجرا کرد.
ارسال درخواستهای Ajax از نوع POST ، PUT و DELETE
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
fetch('/user/name', { method: 'POST', body: 'some data' }); | var xhr = new XMLHttpRequest(); xhr.open('POST', '/user/name'); xhr.send('some data'); | $.ajax({ method: 'POST', url: '/user/name', contentType: 'text/plain', data: 'some data' }); |
اگر از شیء XMLHttpRequest استفاده شود، Content-Type آن به صورت پیشفرض به text/plain تنظیم شدهاست. در اینجا بدنهی درخواست به متد send ارسال میشود.
متد fetch نیز همانند شیء XMLHttpRequest دارای Content-Type پیشفرض از نوع text/plain است. در اینجا بدنهی درخواست به خاصیت body انتساب داده میشود.
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
fetch('/user/1', { method: 'PUT', body: //record including new mobile number }); | var xhr = new XMLHttpRequest(); xhr.open('PUT', '/user/1'); xhr.send(/* record including new data */); | $.ajax({ method: 'PUT', url: '/user/1', contentType: 'text/plain', data: //record including new data }); |
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
fetch('/user/1', {method: 'DELETE'}); | var xhr = new XMLHttpRequest(); xhr.open('DELETE', '/user/1'); xhr.send(); | $.ajax('/user/1', {method: 'DELETE'}); |
مدیریت Encoding درخواستهای Ajax
در مثالهای قبل، اطلاعاتی از نوع text/plain را به سمت سرور ارسال کردیم که به آن encoding type نیز گفته میشود. برای تکمیل بحث میتوان حالتهای دیگری مانند application/x-www-form-urlencoded، application/json و یا multipart/form-data را که در برنامههای کاربردی زیاد مورد استفاده قرار میگیرند، بررسی کرد.
کار با URL Encoding
عموما URL encoding در دو قسمت آدرس درخواستی به سرور و یا حتی بدنهی درخواست ارسالی تنظیم میشود. MIME type آن نیز application/x-www-form-urlencoded است و اطلاعات آن شامل یکسری جفت کلید/مقدار است. برای متدهای ارسال از نوع GET و DELETE، اطلاعات آن در انتهای آدرس درخواستی و برای سایر حالات در بدنهی درخواست ذکر میشوند.
در جیکوئری با استفاده از متد param آن میتوان یک شیء جاوا اسکریپتی را به معادل URL-encoded string آنها تبدیل کرد:
$.param({ key1: 'some value', 'key 2': 'another value' });
key1=some+value&key+2=another+value
$.ajax({ method: 'POST', url: '/user', data: { name: 'VahidN', address: 'Address 1', phone: '555-555-5555' } });
برخلاف جیکوئری در حین کار با روشهای جاوا اسکریپتی خالص این encoding باید به صورت دستی و صریحی انجام شود. برای این منظور دو متد استاندارد encodeURI و encodeURIComponent در جاوا اسکریپت مورد استفاده قرار میگیرند. هدف متد encodeURI اعمال آن بر روی یک URL کامل است و یا کلید/مقدارهای جدا شدهی توسط & مانند first=Vahid&last=N. اما متد encodeURIComponent صرفا جهت اعمال بر روی یک تک مقدار طراحی شدهاست.
به این ترتیب معادل جاوا اسکریپتی قطعه کد جیکوئری فوق به صورت زیر است:
var xhr = new XMLHttpRequest(); var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555'); xhr.open('POST', '/user'); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send(data);
روش انجام اینکار توسط fetch API به صورت زیر است:
var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555'); fetch('/user', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: data });
function param(object) { var encodedString = ''; for (var prop in object) { if (object.hasOwnProperty(prop)) { if (encodedString.length > 0) { encodedString += '&'; } encodedString += encodeURI(prop + '=' + object[prop]); } } return encodedString; }
کار با JSON Encoding
در عمل JSON نمایش رشتهای یک شیء جاوا اسکریپتی است و هدف آن سهولت نقل و انتقالات این اشیاء به سرور و برعکس است. برخلاف حالت application/x-www-form-urlencoded که اطلاعات آن مسطح است، حالت application/json امکان ارسال اطلاعات سلسله مراتبی را نیز میسر میکند (مانند مثال زیر که phone آن دیگر مسطح نیست و خود آن نیز یک شیء جاوا اسکریپتی است).
در جیکوئری برای ارسال اشیاء جاوا اسکریپتی JSON Encoded به سمت سرور از روش زیر استفاده میشود:
$.ajax({ method: 'POST', url: '/user', contentType: 'application/json', data: JSON.stringify({ name: 'VahidN', address: 'Address 1', phone: { home: '555-555-5555', mobile: '444-444-4444' } }); });
پیاده سازی همین مثال با جاوا اسکریپت خالص و XMLHttpRequest استاندارد به صورت زیر است:
var xhr = new XMLHttpRequest(); var data = JSON.stringify({ name: 'VahidN', address: 'Address 1', phone: { home: '555-555-5555', mobile: '444-444-4444' } }); xhr.open('POST', '/user'); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(data);
در این حالت اگر خروجی سرور نیز JSON باشد، روش دریافت و پردازش آن به صورت زیر است:
var xhr = new XMLHttpRequest(); xhr.open('GET', '/user/1'); xhr.onload = function() { var user = JSON.parse(xhr.responseText); // do something with this user JavaScript object }; xhr.send();
اگر از مرورگر IE صرفنظر کنیم، تمام مرورگرهای دیگر دارای خاصیتی به نام xhr.response نیز هستند که نیاز به تبدیل و Parse دستی رشتهی دریافتی را حذف میکند؛ از این جهت که این خاصیت حاوی شیء جاوا اسکریپتی معادل بدنهی response دریافتی از سمت سرور است. البته با این شرط که سرور، Content-Type مساوی application/json را برای response تنظیم کرده باشد.
و روش انجام این عملیات توسط fetch API به صورت زیر است:
fetch('/user', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name: 'VahidN', address: 'Address 1', phone: { home: '555-555-5555', mobile: '444-444-4444' } }); });
و یا اگر بخواهیم اطلاعات JSON دریافتی از سمت سرور را در اینجا پردازش کنیم، روش کار به صورت زیر است:
fetch('/user/1').then(function(response) { return response.json(); }).then(function(userRecord) { // do something with this user JavaScript object });
همین مثال را اگر بخواهیم توسط ECMAScript 2016 و Arrow functions آن بازنویسی کنیم، به قطعه کد زیر میرسیم:
fetch('/user/1') .then(response => response.json()) // Transform the data into json .then(userRecord => { // do something with this userRecord object });
کار با Multipart Encoding
نوع دیگری از encoding که بیشتر با فرمهای HTML بکار میرود، multipart/form-data نام دارد:
<form action="my/server" method="POST" enctype="multipart/form-data"> <label>First Name: <input name="first"> </label> <label>Last Name: <input name="last"> </label> <button>Submit</button> </form>
-----------------------------1686536745986416462127721994 Content-Disposition: form-data; name="first" Vahid -----------------------------1686536745986416462127721994 Content-Disposition: form-data; name="last" N -----------------------------1686536745986416462127721994--
روش ارسال اطلاعات با این نوع encoding خاص به سمت سرور توسط متد ajax جیکوئری به صورت زیر است:
var formData = new FormData(); formData.append('name', 'VahidN'); formData.append('address', 'Address 1'); formData.append('phone', '555-555-5555'); $.ajax({ method: 'POST', url: '/user', contentType: false, processData: false, data: formData });
در اینجا ذکر processData: false ضروری است. در غیراینصورت jQuery این اطلاعات را به یک URL-encoded string تبدیل میکند. همچنین با اعلام contentType: false، جیکوئری در کار مرورگر دخالت نمیکند. از این جهت که هدر ویژهی این نوع درخواستها توسط خود مرورگر تنظیم میشود و برای مثال یک چنین شکلی را دارد:
multipart/form-data; boundary=—————————1686536745986416462127721994
روش انجام اینکار با XMLHttpRequest و همچنین fetch API به صورت زیر است:
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد |
var formData = new FormData(); formData.append('name', 'VahidN'); formData.append('address', 'Address 1'); formData.append('phone', '555-555-5555'); fetch('/user', { method: 'POST', body: formData }); | var formData = new FormData(), xhr = new XMLHttpRequest(); formData.append('name', 'VahidN'); formData.append('address', 'Address 1'); formData.append('phone', '555-555-5555'); xhr.open('POST', '/user'); xhr.send(formData); |
آپلود فایلها توسط درخواستهای Ajax ایی
تنها راه آپلود فایلها در مرورگرهای قدیمی که شامل IE 9.0 هم میشود، تعریف المان <"input type="file> در داخل المان <form> و سپس submit مستقیم آن فرم است. برای رفع این مشکل در مرورگرهای پس از IE 9.0 و پشتیبانی از Ajax، جهت آپلود فایلها، استاندارد XMLHttpRequest Level 2 معرفی شدهاست. در این حالت اگر المان <input type=file> در صفحه وجود داشته باشد، روش ارسال Ajax ایی آن به سمت سرور به صورت زیر است:
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
var file = document.querySelector( 'INPUT[type="file"]').files[0]; fetch('/uploads', { method: 'POST', body: file }); } | var file = document.querySelector( 'INPUT[type="file"]').files[0], xhr = new XMLHttpRequest(); xhr.open('POST', '/uploads'); xhr.send(file); | var file = $('INPUT[type="file"]')[0].files[0]; $.ajax({ method: 'POST', url: '/uploads', contentType: false, processData: false, data: file }); |
یک نکته: اگر نیاز به آپلود بیش از یک فایل را داشتید و همچنین در اینجا نیاز به اطلاعات دیگری مانند سایر فیلدهای فرم نیز وجود داشت، از همان روش تعریف new FormData و افزودن اطلاعات مورد نیاز به آن استفاده کنید. امکان افزودن شیء file نیز به FormData پیش بینی شدهاست.
دانلود فایلها توسط درخواستهای Ajax ایی
پیشتر در حین بررسی JSON encoding توسط fetch API از متد ()json برای تبدیل اطلاعات دریافتی از سرور به json و بازگشت آن به صورت یک Promise استفاده کردیم:
fetch(url) .then((resp) => resp.json()) // Transform the data into json .then(function(data) { // use data object }) })
- ()clone یک کپی از response را تهیه میکند.
- ()redirect یک response جدید را با URL دیگری ایجاد میکند.
- ()arrayBuffer یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به یک شیء ArrayBuffer تبدیل میکند.
- ()formData یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به یک شیء FormData تبدیل میکند.
- ()blob یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به یک شیء Blob تبدیل میکند.
- ()text یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به string تبدیل میکند.
- ()json یک Promise را بازگشت میدهد که پس از پایان درخواست، response را به یک شیء جاوا اسکریپتی تبدیل میکند.
در اینجا متدی که میتواند برای تبدیل یک byte array بازگشتی از سرور به فایل قابل دریافت در سمت کلاینت مورد استفاده قرار گیرد، متد blob است:
function downloadBlob() { fetch('/Home/InMemoryReport') .then(function(response) { return response.blob(); }) .then(function(xlsxBlob) { var a = document.createElement("a"); document.body.appendChild(a); a.style = "display: none"; let url = window.URL.createObjectURL(xlsxBlob); a.href = url; a.download = "report.xlsx"; a.click(); window.URL.revokeObjectURL(url); }); }
ارسال درخواستهای Ajax به دومینهای دیگر (CORS)
گاهی از اوقات نیاز است اطلاعاتی را توسط درخواستهای Ajax، به سروری دیگر در دومینی دیگر ارسال و یا دریافت کرد. هرچند انجام اینکار به صورت مستقیم و خارج از مرورگر بدون مشکل قابل انجام است، اما مرورگرها برای درخواستهای جاوا اسکریپتی محدودیت «same-origin policy» را اعمال میکنند. به این معنا که XMLHttpRequest بین دومینها به صورت پیشفرض ممنوع است. برای ارسال درخواستهای مجاز و از پیش مشخص شدهی Ajax بین دومینها، تاکنون دو روش پیش بینی شدهاست:
الف) روش JSONP
«same-origin policy» از شروع ارسال درخواستی به خارج از دومین جاری، جلوگیری میکند. هرچند این مورد به درخواستهای XMLHttpRequest اعمال میشود، اما در مورد المانهایی از نوع <a>، <img> و <script> صادق نیست و آنها محدود به این سیاست امنیتی نیستند. روش «JavaScript Object Notation with Padding» و یا به اختصار JSONP از یکی از همین استثناءها جهت ارسال درخواستهایی به سایر دومینها استفاده میکند. البته نام این روش کمی غلط انداز است؛ از این جهت که در این فرآیند اصلا JSON ایی مورد استفاده قرار نمیگیرد؛ خروجی سرور در این حالت یک تابع جاوا اسکریپتی است و نه JSON.
روش انجام این نوع درخواستها را توسط جیکوئری در ذیل مشاهده میکنید:
$.ajax('http://jsonp-aware-endpoint.com/user/1', { jsonp: 'callback', dataType: 'jsonp' }).then(function(response) { // handle user info from server });
انجام اینکار بدون jQuery و در حقیقت کاری که jQuery در پشت صحنه برای ایجاد تگ script انجام میدهد، چنین چیزی است:
window.myJsonpCallback = function(data) { // handle user info from server }; var scriptEl = document.createElement('script'); scriptEl.setAttribute('src', 'http://jsonp-aware-endpoint.com/user/1?callback=myJsonpCallback'); document.body.appendChild(scriptEl);
ب) روش CORS
CORS و یا Cross Origin Resource Sharing روش مدرن و پذیرفته شدهی ارسال درخواستهای Ajax در بین دومینها است و دارای دو نوع ساده و غیرساده است. نوع سادهی آن به همراه هدر مخصوص Origin است که جهت بیان دومین ارسال کنندهی درخواست بکار میرود و تنها از encodingهای “text/plain” و “application/x-www-form-urlencoded” پشتیبانی میکند. نوع غیرسادهی آن که این روزها بیشتر بکار میرود، از نوع «preflight» است. Preflight در اینجا به این معنا است که زمانیکه درخواست Ajax ایی را به دومین دیگری ارسال کردید، پیش از ارسال، مرورگر یک درخواست از نوع OPTIONS را به سمت سرور مقصد ارسال میکند. در این حالت اگر سرور مجوز مناسبی را صادر کرد، آنگاه مرورگر اصل درخواست را به سمت آن سرور ارسال میکند. به همین جهت در این حالت به ازای هر درخواستی که در برنامه ارسال میشود، در برگهی network مرورگر، دو درخواست را مشاهده خواهید کرد. درخواست preflight از نوع OPTIONS به صورت خودکار توسط مرورگر مدیریت میشود و نیازی به کدنویسی خاصی ندارد.
مدیریت کوکیها در درخواستهای Ajax
اگر درخواست Ajax ایی را به دومین دیگری ارسال کنید، به صورت پیشفرض به همراه کوکیهای مرتبط نخواهد بود. برای رفع این مشکل نیاز است خاصیت withCredentials را به true تنظیم کنید:
توسط استاندارد جدید Fetch API | توسط XMLHttpRequest استاندارد | توسط jQuery |
fetch('http://someotherdomain.com', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, credentials: 'include' }); | var xhr = new XMLHttpRequest(); xhr.open('POST', 'http://someotherdomain.com'); xhr.withCredentials = true; xhr.setRequestHeader('Content-Type', 'text/plain'); xhr.send('sometext'); | $.ajax('http://someotherdomain.com', { method: 'POST', contentType: 'text/plain', data: 'sometext', beforeSend: function(xmlHttpRequest) { xmlHttpRequest.withCredentials = true; } }); |
یک نکتهی مهم: در fetch API حتی برای درخواستهای ساده نیز کوکیها ارسال نمیشوند. در این حالت برای کار با دومین جاری و ارسال کوکیهای کاربر به سمت سرور، باید از تنظیم 'credentials: 'same-origin استفاده کرد؛ زیرا مقدار پیشفرض آن omit است.
مدلهای سمت سرور برنامه
در این مطلب قصد داریم لیست گروهها را به همراه محصولات مرتبط با آنها، توسط دو drop down list نمایش دهیم:
public class Category { public int CategoryId { set; get; } public string CategoryName { set; get; } [JsonIgnore] public IList<Product> Products { set; get; } } public class Product { public int ProductId { set; get; } public string ProductName { set; get; } }
منبع داده JSON سمت سرور
پس از مشخص شدن مدلهای برنامه، اکنون توسط دو اکشن متد، لیست گروهها و همچنین لیست محصولات یک گروه خاص را با فرمت JSON بازگشت میدهیم:
namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class ProductController : Controller { [HttpGet("[action]")] public async Task<IActionResult> GetCategories() { await Task.Delay(500); return Json(CategoriesDataSource.Items); } [HttpGet("[action]/{categoryId:int}")] public async Task<IActionResult> GetProducts(int categoryId) { await Task.Delay(500); var products = CategoriesDataSource.Items .Where(category => category.CategoryId == categoryId) .SelectMany(category => category.Products) .ToList(); return Json(products); } } }
- در اینجا از یک Delay نیز استفاده شدهاست تا بتوان آیکنهای چرخندهی Loading سمت کاربر را در حین کار با عملیاتی زمانبر، بهتر مشاهده کرد.
کدهای سمت کاربر برنامه
کدهای سمت کاربر این مثال در ادامهی همان مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» هستند که بر روی آن این دستورات فراخوانی شدهاست:
>ng g m Product -m app.module --routing
>ng g c product/product-group
>ng g cl product/product >ng g cl product/Category >ng g cl product/product-group-form
export class ProductGroupForm { constructor( public categoryId?: number, public productId?: number ) { } } export class Product { constructor( public productId: number, public productName: string ) { } } export class Category { constructor( public categoryId: number, public categoryName: string ) { } }
سپس سرویسی را جهت دریافت اطلاعات دراپ داونها از سرور تهیه کردهایم:
>ng g s product/product-items -m product.module
import { Injectable } from "@angular/core"; import { Http, Response, Headers, RequestOptions } from "@angular/http"; import { Observable } from "rxjs/Observable"; import "rxjs/add/operator/do"; import "rxjs/add/operator/catch"; import "rxjs/add/observable/throw"; import "rxjs/add/operator/map"; import "rxjs/add/observable/of"; import { Category } from "./category"; import { Product } from "./product"; @Injectable() export class ProductItemsService { private baseUrl = "api/product"; constructor(private http: Http) { } private handleError(error: Response): Observable<any> { console.error("observable error: ", error); return Observable.throw(error.statusText); } getCategories(): Observable<Category[]> { return this.http .get(`${this.baseUrl}/GetCategories`) .map(response => response.json() || {}) .catch(this.handleError); } getProducts(categoryId: number): Observable<Product[]> { return this.http .get(`${this.baseUrl}/GetProducts/${categoryId}`) .map(response => response.json() || {}) .catch(this.handleError); } }
پس از این مقدمات اکنون میتوان کدهای ProductGroupComponent را تکمیل کرد.
ابتدا در متد ngOnInit آن کار دریافت لیست آغازین گروههای محصولات را انجام میدهیم:
export class ProductGroupComponent implements OnInit { categories: Category[] = []; model = new ProductGroupForm(); constructor(private productItemsService: ProductItemsService) { } ngOnInit() { this.productItemsService.getCategories().subscribe( data => { this.categories = data; }, err => console.log("get error: ", err) ); }
اکنون چون این خاصیت در دسترس است، میتوان به قالب این کامپوننت مراجعه کرده و قسمت ابتدایی فرم را تکمیل کرد:
<div class="container"> <h3>Cascading Drop-down Lists</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group"> <label class="control-label">Category</label> <span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="categories.length == 0"></span> <select class="form-control" name="categoryCtrl" #categoryCtrl (change)="fetchProducts(categoryCtrl.value)" [(ngModel)]="model.categoryId"> <option value="undefined">Select a Category...</option> <option *ngFor="let category of categories" value="{{category.categoryId}}"> {{ category.categoryName }} </option> </select> </div>
- سپس ngModel به خاصیت categoryId وهلهای از کلاس ProductGroupForm که مدل معادل فرم است، متصل شدهاست.
- همچنین با اتصال به رخداد change، مقدار Id عضو انتخابی به متد fetchProducts ارسال میشود. دسترسی به این Id از طریق یک template reference variable به نام categoryCtrl# انجام شدهاست.
- در آخر، ngFor تعریف شده به ازای هر عضو آرایهی categories، یکبار تگ option را تکرار میکند و در هربار تکرار، مقدار ویژگی value را به categoryId تنظیم میکند و برچسب نمایشی آنرا از categoryName دریافت خواهد کرد.
بنابراین مرحلهی بعدی تکمیل این drop down آبشاری، واکنش نشان دادن به رخداد change و تکمیل متد fetchProducts است:
products: Product[] = []; isLoadingProducts = false; fetchProducts(categoryId?: number) { console.log(categoryId); this.products = []; if (categoryId === undefined || categoryId.toString() === "undefined") { return; } this.isLoadingProducts = true; this.productItemsService.getProducts(categoryId).subscribe( data => { this.products = data; this.isLoadingProducts = false; }, err => { console.log("get error: ", err); this.isLoadingProducts = false; } ); }
- سپس بررسی میکنیم که آیا categoryId دریافتی undefined است یا خیر؟ این مساله دو علت دارد:
الف) اولین عضو drop down انتخاب محصولات را با مقدار undefined مشخص کردهایم:
<option value="undefined">Select a Category...</option>
public categoryId?: number
model = new ProductGroupForm();
- پس از آن همانند قسمت قبل، این categoryId را به سرور ارسال کرده و سپس اطلاعات متناظری را دریافت و به خاصیت عمومی products نسبت دادهایم. همچنین از یک خاصیت عمومی دیگر به نام isLoadingProducts نیز استفاده شدهاست تا مشخص شود چه زمانی کار دریافت اطلاعات از سرور خاتمه پیدا میکند. از آن برای نمایش یک آیکن چرخندهی دیگر استفاده میکنیم:
<div class="form-group"> <label class="control-label">Product</label> <span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="isLoadingProducts"></span> <select class="form-control" name="productCtrl" [(ngModel)]="model.productId"> <option value="undefined">Select a Product...</option> <option *ngFor="let product of products" value="{{product.productId}}"> {{ product.productName }} </option> </select> </div>
/* Spinner */ .spinner { font-size:15px; z-index:10 } .glyphicon-spin { -webkit-animation: spin 1000ms infinite linear; animation: spin 1000ms infinite linear; } @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } } @keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run