پروژه دات نت را از طریق این آدرس دریافت کرده و آن را در حالت اجرا بگذارید.
سپس یک سرویس جدید را در پروژه Angular خود اجرا کنید و متد زیر را به آن اضافه کنید:
login():any{ let urlSearchParams = new URLSearchParams(); urlSearchParams.append('username', 'Vahid'); urlSearchParams.append('password', '1234'); urlSearchParams.append('grant_type', 'password'); let body = urlSearchParams.toString(); let headers = new Headers(); headers.append('Content-Type', 'application/x-www-form-urlencoded'); return this._http.post('http://localhost:9577/login', body, { headers: headers }) .map(res => res.json()); }
username=vahid&password=1234
از آنجاکه پروژه انگیولار ساخته شده در آدرسی دیگر و جدا از پروژهی دات نت قرار دارد و همینطور که میبینید آدرس کامل آن را به این دلیل قرار دادم، ممکن است خطای CORS را دریافت کنید که میتوانید آن را با نصب یک بسته نیوگتی حل کنید.
همچنین برای تست و انجام یک عمل مرتبط با توکن، متد زیر را هم به آن اضافه میکنیم:
ApiAdmin(token:any):any{ let headers = new Headers(); headers.append('Authorization', 'bearer ' + token); return this._http.get('http://localhost:9577/api/MyProtectedApi', { headers: headers }) .map(res => res.json()); }
کد کل سرویس، الان به شکل زیر شده است:
import { Http, Headers, URLSearchParams } from '@angular/http'; import { Injectable } from '@angular/core'; import { Observable } from "rxjs/Observable"; import "rxjs/Rx"; @Injectable() export class MemberShipService { constructor(private _http: Http) { } ApiAdmin(token: any): any { let headers = new Headers(); headers.append('Authorization', 'bearer ' + token); return this._http.get('http://localhost:9577/api/MyProtectedApi', { headers: headers }) .map(res => res.json()); } login(): any { let urlSearchParams = new URLSearchParams(); urlSearchParams.append('username', 'Vahid'); urlSearchParams.append('password', '1234'); urlSearchParams.append('grant_type', 'password'); let body = urlSearchParams.toString(); let headers = new Headers(); headers.append('Content-Type', 'application/x-www-form-urlencoded'); return this._http.post('http://localhost:9577/login', body, { headers: headers }) .map(res => res.json()); } }
Login(){ this._service.login() .subscribe(res => { localStorage.setItem('access_token', res.access_token); localStorage.setItem('refresh_token', res.refresh_token); } , error => console.log(error)); }
در متد بعدی که قرار است توسط آن صحت اعتبارسنجی مورد آزامایش قرار بگیرد، کدهای زیر را مینویسیم:
CallApi() { let item = localStorage.getItem("access_token"); if (item == null) { alert('please Login First.'); return; } this._service.ApiAdmin(item) .subscribe(res => { alert(res); } , error => console.log(error)); }
اعتبارسنجی در مسیریابی
یکی از روشهایی که انگیولار باید بررسی کند این است که کاربر جاری با توجه به نقشهایی که ممکن است داشته باشد، یا اعتبار سنجی شده است یا خیر و میزان دسترسی او به کامپوننتها، باید مشخص گردد. برای این مورد خصوصیتی به مسیریابی اضافه شده است به نام CanActivate که از شما یک کلاس را که در آن اینترفیس CanActivate پیاده سازی شده است، درخواست میکند. در اینجا ما یک Guard را با نام LoginGuard ایجاد میکنیم، تا تنها کاربرانی که لاگین کردهاند، به این صفحه دسترسی داشته باشند:
import { CanActivate } from '@angular/router'; import { Injectable } from '@angular/core'; @Injectable() export class LoginGuard implements CanActivate { constructor() { } canActivate() { let item = localStorage.getItem('access_token'); if (item == null) return false; return true; } }
{ path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard]}
{ path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard,SecondGuard]}
در یک گارد ممکن است کاربر لاگین نکرده باشد و شما نیاز دارید او را به صفحه لاگین هدایت کنید. در این صورت گارد لاگین را به شکل زیر بازنویسی میکنیم:
import { Router } from '@angular/router'; import { CanActivate } from '@angular/router'; import { Injectable } from '@angular/core'; @Injectable() export class LoginGuard implements CanActivate { constructor(public router: Router) { } canActivate() { let item = localStorage.getItem('access_token'); if (item == null) { this.router.navigate(['/app']); return false; } return true; } }
همچنین گارد میتواند اطلاعات مسیر درخواست شده را خوانده و بر اساس آن به شما پاسخ بدهد. به عنوان مثال پارامترهایی را که به سمت مسیر مورد نظر هدایت میشود، بخواند و بر اساس آن، تصمیم گیری کند. به عنوان نمونه کاربر به مسیر ذکرشده دسترسی دارد، ولی با Id که در مسیر ارسال کرده است، دسترسی ندارد:
import { Router } from '@angular/router'; import { CanActivate, ActivatedRouteSnapshot } from '@angular/router'; import { Injectable } from '@angular/core'; @Injectable() export class SecondGuard implements CanActivate { constructor(public router: Router) { } canActivate(route: ActivatedRouteSnapshot) { let id = route.params['id']; if (id == 1) { return false; } return true; } }
متد CanActivate میتواند پارامترهای زیادی را به عنوان ورودی دریافت کند که یکی از آنها ActivatedRouteSnapshot است که اطلاعات خوب و مفیدی را در مورد مسیر ارسال شده از طرف کاربر دارد و با استفاده از آن در اینجا میتوانیم پارامترهای ارسالی را دریافت کنیم. در اینجا ذکر کردهایم که اگر پارامتر Id که بر مبنای مسیر زیر است، برابر 1 بود، مقدار برگشتی برابر false خواهد بود و دسترسی به کامپوننت در اینجا ممکن نخواهد بود.
{ path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard,SecondGuard] }
ویجتهای وب Kendo UI مجموعهای از کنترلهای سفارشی HTML 5 هستند که برفراز jQuery تهیه شدهاند. این کنترلها برای برنامههای وب و همچنین برنامههای دسکتاپ لمسی طراحی شدهاند.
بهترین روش برای مشاهدهی این مجموعه، مراجعه به فایل examples\index.html پوشهی اصلی Kendo UI است که لیست کاملی از این ویجتها را به همراه مثالهای مرتبط ارائه میدهد. تعدادی از اعضای این مجموعه شامل کنترلهای ذیل هستند:
Window, TreeView, Tooltip, ToolBar, TimePicker, TabStrip, Splitter, Sortable, Slider, Gantt, Scheduler, ProgressBar, PanelBar, NumericTextBox, Notification, MultiSelect, Menu, MaskedTextBox, ListView, PivotGrid, Grid, Editor, DropDownList, DateTimePicker, DatePicker, ComboBox, ColorPicker, Calendar, Button, AutoComplete
نحوهی استفاده کلی از ویجتهای وب Kendo UI
با توجه به اینکه کنترلهای Kendo UI مبتنی بر jQuery هستند، نحوهی استفاده از آنها، مشابه سایر افزونههای جیکوئری است. ابتدا المانی به صفحه اضافه میشود:
<input id="pickDate" type="text"/>
<script type="text/javascript"> $(function() { $("#pickDate").kendoDatePicker(); }); </script>
<input id="dateOfBirth" type="text" data-role="datepicker" />
برای فعال سازی حالت declarative initialization باید به دو نکتهی مهم دقت داشت:
الف) در مطلب معرفی Kendo UI اسکریپتهای ذیل برای آماده سازی Kendo Ui معرفی شدند:
<!--KendoUI: Web--> <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" /> <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" /> <script src="js/jquery.min.js" type="text/javascript"></script> <script src="js/kendo.web.min.js" type="text/javascript"></script> <!--KendoUI: DataViz--> <link href="styles/kendo.dataviz.min.css" rel="stylesheet" type="text/css" /> <script src="js/kendo.dataviz.min.js" type="text/javascript"></script> <!--KendoUI: Mobile--> <link href="styles/kendo.mobile.all.min.css" rel="stylesheet" type="text/css" /> <script src="js/kendo.mobile.min.js" type="text/javascript"></script>
<link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" /> <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" /> <script src="js/jquery.min.js" type="text/javascript"></script> <script src="js/kendo.all.min.js" type="text/javascript"></script>
یک مثال کامل:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" /> <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" /> <script src="js/jquery.min.js" type="text/javascript"></script> <script src="js/kendo.all.min.js" type="text/javascript"></script> <script type="text/javascript"> $(function () { $("#pickDate").kendoDatePicker(); }); $(function () { // initialize any widgets in the #container div kendo.init($("#container")); }); </script> </head> <body> <span> Pick a date: <input id="pickDate" type="text" /> </span> <div id="container"> <input id="dateOfBirth" type="text" data-role="datepicker" /> <div id="colors" data-role="colorpalette" data-columns="4" data-tile-size="{ width: 34, height: 19 }"></div> </div> </body> </html>
- در اینجا pickDate به صورت معمولی فعال شدهاست.
- اما در قسمت kendo.init نام یک ناحیه یا نام یک کنترل را میتوان ذکر کرد. برای مثال در اینجا کل ناحیهی مشخص شده توسط یک div با id مساوی container به صورت یکجا با تمام کنترلهای داخل آن فعال گردیدهاست.
بنابراین برای اعمال declarative initialization، یک ناحیه را توسط kendo.init مشخص کرده و سپس توسط data-roleها، نام ویجت وب مورد نظر را به صورت lower case مشخص میکنیم. همچنین فایلهای اسکریپت مورد استفاده نیز نباید تداخلی داشته باشند.
تنظیمات ویجتهای وب Kendo UI
تاکنون نمونهی سادهای از بکارگیری ویجتهای وب Kendo UI را بررسی کردیم؛ اما این ویجتها توسط تنظیمات پیش بینی شده برای آنها بسیار قابل تنظیم و تغییر هستند. تنظیمات آنها نیز بستگی به روش استفاده و آغاز آنها دارد. برای مثال اگر این ویجتها را توسط کدهای جاوا اسکریپتی آغاز کردهاید، در همانجا توسط پارامترهای افزونهی جیکوئری میتوان تنظیمات مرتبط را اعمال کرد:
<script type="text/javascript"> $(function () { $("#pickDate").kendoDatePicker({ format: "yyyy/MM/dd" }); }); </script>
در حالت declarative initialization، پارامتر format تبدیل به ویژگی data-format خواهد شد:
<input id="dateOfBirth" type="text" data-role="datepicker" data-format="yyyy/MM/dd" />
تنظیمات DataSource ویجتهای وب
بسیاری از ویجتهای وب Kendo UI با دادهها سر و کار دارند مانند Grid، Auto Complete، Combo box و غیره. این کنترلها دادههای خود را از طریق خاصیت DataSource دریافت میکنند. برای نمونه در اینجا یک combo box را در نظر بگیرید. در مثال اول، خاصیت dataSource کنترل ComboBox در همان افزونهی جیکوئری تنظیم شدهاست:
<input id="colorPicker1" /> <script type="text/javascript"> $(document).ready(function () { $("#colorPicker1").kendoComboBox({ dataSource: ["Blue", "Green", "Red", "Yellow"] }); }); </script>
<input id="colorPicker2" data-role="combobox" data-source='["Blue", "Green", "Red", "Yellow"]' /> <script type="text/javascript"> $(document).ready(function () { kendo.init($("#colorPicker2")); }); </script>
کار با رویدادهای ویجتهای وب
نحوهی کار با رویدادهای ویجتهای وب نیز بر اساس نحوهی آغاز آنها متفاوت است. در مثالهای ذیل، دو حالت متفاوت تنظیم رویداد change را توسط خواص افزونهی جیکوئری:
<input id="colorPicker3" /> <script type="text/javascript"> function onColorChange(e) { alert('Color Change!'); } $(document).ready(function () { $("#colorPicker3").kendoComboBox({ dataSource: ["Blue", "Green", "Red", "Yellow"], change: onColorChange }); }); </script>
<input id="colorPicker4" data-role="combobox" data-source='["Blue", "Green", "Red", "Yellow"]' data-change="onColorChange" /> <script type="text/javascript"> function onColorChange(e) { alert('Color Change!'); } $(document).ready(function () { kendo.init($("#colorPicker4")); }); </script>
تغییر قالب ویجتهای وب
Kendo UI همیشه یک جفت CSS را جهت تعیین قالبهای ویجتهای خود، مورد استفاده قرار میدهد. برای نمونه در مثالهای فوق، kendo.common.min.css حاوی اطلاعات محل قرارگیری و اندازهی ویجتها است. شیوه نامهی دوم همیشه به شکل kendo.[skin].min.css تعریف میشود که دارای اطلاعات رنگ و پس زمینهی ویجتها خواهد بود؛ مانند kendo.black.min.css، kendo.blueopal.min.css و امثال آن که در پوشهی styles قابل مشاهده هستند.
همچنین باید دقت داشت که همیشه common باید پیش از skin ذکر شود؛ زیرا در تعدادی از حالات، شیوه نامهی skin، اطلاعات common را بازنویسی میکند.
علاوه بر skinهای پیش فرض موجود در پوشهی styles، امکان استفاده از یک theme builder آنلاین نیز وجود دارد: kendo-ui-themebuilder
در مثالهای زیر مجموعهای از Reflectionهای ساده و کاملا کاربردی است که من با آنها روبرو شده ام.
کوتاه سازی کدهای نمایش یک View در ASP.NET MVC با Reflection
یکی از قسمتهایی که مرتبا با آن سر و کار دارید، نمایش اطلاعات است. حتی یک جدول را هم که میخواهید بسازید، باید ستونهای آن جدول را یک به یک معرفی کنید. ولی در عمل، یک Reflection ساده این کار به یک تابع چند خطی و سپس برای ترسیم هر ستون جدول از دو خط استفاده خواهید کرد ولی مزیتی که دارد این است که این تابع برای تمامی جدولها کاربردی عمومی پیدا میکند. برای نمونه دوست داشتم برای بخش مدیر، قسمت پروفایلی را ایجاد کنم و در آن اطلاعاتی چون نام، نام خانوادگی، تاریخ تولد، تاریخ ایجاد و خیلی از اطلاعات دیگر را نمایش دهم. به جای اینکه بیایم برای هر قسمت یک خط partial ایجاد کنم، با استفاده از reflection و یک حلقه، تمامی اطلاعات را به آن پارشال پاس میکنم. مزیت این روش این است که اگر بخواهم در یک جای دیگر، اطلاعات یک محصول یا یک فاکتور را هم نمایش دهم، باز هم همین تابع برایم کاربرد خواهد داشت:
تصویر زیر را که برگرفته از یک قالب Bootstrap است، ملاحظه کنید. اصلا علاقه ندارم که برای یک به یک آنها، یک سطر جدید را تعریف کنم و به View بگویم این پراپرتی را نشان بده؛ دوباره مورد بعدی هم به همین صورت و دوباره و دوباره و ... . دوست دارم یک تابع عمومی، همهی این کارها را خودکار انجام دهد.
ساختار اطلاعاتی تصویر فوق به شرح زیر است:
<div> <div> <div> <p><span>First Name </span>: Jonathan</p> </div> </div> </div>
BioRow_
@model System.Web.UI.WebControls.ListItem <div> <p><span>@Model.Text </span>: @Model.Value</p> </div>
@using System.Web.UI.WebControls @using ZekrWepApp.Filters @model ZekrModel.Admin <div> <h1>Bio Graph</h1> <div> @{ ListItemCollection collection = GetCustomProperties.Get(Model,exclude:new string[]{"Poems","Id"}); foreach (var item in collection) { Html.RenderPartial(MVC.Shared.Views._BioRow, item); } } </div> </div>
کد درون این کلاس ایستا را بررسی میکنیم؛ این کلاس دو متد دارد یکی عمومی و دیگری خصوصی است:
public class GetCustomProperties { private static PropertyInfo[] getObjectsInfos(object obj,string[] inclue,string[] exclude ) { var list = obj.GetType().GetProperties(); PropertyInfo[] outputPropertyInfos = null; if (inclue != null) { return list.Where(propertyInfo => inclue.Contains(propertyInfo.Name)).ToArray(); } if (exclude != null) { return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray(); } return list; } }
متد عمومی که در این کلاس قرار دارد به شرح زیر است:
public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null) { var propertyInfos = getObjectsInfos(obj, inclue, exclude); if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null"); var collection = new ListItemCollection(); foreach (PropertyInfo propertyInfo in propertyInfos) { string name = propertyInfo.Name; foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true)) { DisplayAttribute displayAttribute = attribute as DisplayAttribute; if (displayAttribute != null) { name = displayAttribute.Name; break; } } string value = ""; object objvalue = propertyInfo.GetValue(obj); if (objvalue != null) value = objvalue.ToString(); collection.Add(new ListItem(name,value)); } return collection; }
کد بالا پراپرتیها را دریافت و یک به یک متادیتاهای آن را بررسی کرده و در صورتی که از متادیتای Display استفاده کرده باشند، مقدار آن را جایگزین نام پراپرتی خواهد کرد. در مورد مقدار هم از آنجا که اگر پراپرتی با Null پر شده باشد، تبدیل به رشتهای با پیام خطای روبرو خواهد شد. در نتیجه بهتر است یک شرط احتیاط هم روی آن پیاده شود. در آخر هم از متن و مقدار، یک آیتم ساخته و درون Collection اضافه میکنیم و بعد از اینکه همه پراپرتیها بررسی شدند، Collection را بر میگردانیم.
[Display(Name = "نام کاربری")] public string UserName { get; set; }
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Web; using System.Web.Mvc.Html; using System.Web.UI.WebControls; using Links; namespace ZekrWepApp.Filters { public class GetCustomProperties { public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null) { var propertyInfos = getObjectsInfos(obj, inclue, exclude); if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null"); var collection = new ListItemCollection(); foreach (PropertyInfo propertyInfo in propertyInfos) { string name = propertyInfo.Name; foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true)) { DisplayAttribute displayAttribute = attribute as DisplayAttribute; if (displayAttribute != null) { name = displayAttribute.Name; break; } } string value = ""; object objvalue = propertyInfo.GetValue(obj); if (objvalue != null) value = objvalue.ToString(); collection.Add(new ListItem(name,value)); } return collection; } private static PropertyInfo[] getObjectsInfos(object obj,string[] include,string[] exclude ) { var list = obj.GetType().GetProperties(); PropertyInfo[] outputPropertyInfos = null; if (include != null) { return list.Where(propertyInfo => include.Contains(propertyInfo.Name)).ToArray(); } if (exclude != null) { return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray(); } return list; } } }
لیستی از پارامترها با Reflection
مورد بعدی که سادهتر بوده و از کد بالا مختصرتر هم هست، این است که قرار بود برای یک درگاه، یک سری اطلاعات را با متد Post ارسال کنم که نحوهی ارسال اطلاعات به شکل زیر بود:
amount=1000&orderId=452&Pid=xxx&....
using System; using System.Collections.Generic; using System.Linq; namespace Utils { public class QueryStringParametersList { private string Symbol = "&"; private List<KeyValuePair<string, string>> list { get; set; } public QueryStringParametersList() { list = new List<KeyValuePair<string, string>>(); } public QueryStringParametersList(string symbol) { Symbol = symbol; list = new List<KeyValuePair<string, string>>(); } public int Size { get { return list.Count; } } public void Add(string key, string value) { list.Add(new KeyValuePair<string, string>(key, value)); } public string GetQueryStringPostfix() { return string.Join(Symbol, list.Select(p => Uri.EscapeDataString(p.Key) + "=" + Uri.EscapeDataString(p.Value))); } } }
یک متغیر به نام symbol دارد و در صورتی در شرایط متفاوت، قصد چسپاندن چیزی را به یکدیگر با علامتی خاص داشته باشید، این تابع میتواند کاربرد داشته باشد. این متد از یک لیست کلید و مقدار استفاده کرده و پارامترهایی را که به آن پاس میشود، نگهداری و سپس توسط متد GetQueryStringPostfix آنها را با یکدیگر الحاق کرده و در قالب یک رشته بر میگرداند.
کاربرد Reflection در اینجا این است که من باید دوبار به شکل زیر، دو نوع اطلاعات متفاوت را پست کنم. یکی موقع ارسال به درگاه و دیگری موقع بازگشت از درگاه.
QueryStringParametersList queryparamsList = new QueryStringParametersList(); ueryparamsList.Add("consumer_key", requestPayment.Consumer_Key); queryparamsList.Add("amount", requestPayment.Amount.ToString()); queryparamsList.Add("callback", requestPayment.Callback); queryparamsList.Add("description", requestPayment.Description); queryparamsList.Add("email", requestPayment.Email); queryparamsList.Add("mobile", requestPayment.Mobile); queryparamsList.Add("name", requestPayment.Name); queryparamsList.Add("irderid", requestPayment.OrderId.ToString());
ولی با استفاده از کد Reflection که در بالاتر عنوان شد، باید نام و مقدار پراپرتی را گرفته و در یک حلقه آنها را اضافه کنیم، بدین شکل:
private QueryStringParametersList ReadParams(object obj) { PropertyInfo[] propertyInfos = obj.GetType().GetProperties(); QueryStringParametersList queryparamsList = new QueryStringParametersList(); for (int i = 0; i < propertyInfos.Count(); i++) { queryparamsList.Add(propertyInfos[i].Name.ToLower(),propertyInfos[i].GetValue(obj).ToString() ); } return queryparamsList; }
صورت مساله
میخواهیم اطلاعات نمایش داده شده در گرید را به نحوی فرمت کنیم که
الف) اگر id ردیف مساوی 5 بود، رنگ و پس زمینهی آن تغییر کند.
ب) نام محصول، به جزئیات آن لینک شود و این اطلاعات توسط یک jQuery UI Dialog نمایش داده شود.
ج) عدد قیمت با سه رقم جدا کننده همراه باشد.
تکمیل مدل برنامه
مدل قسمت اول صرفا یک محصول بود. مدل قسمت جاری، اطلاعات تولید/تامین کننده آنرا توسط کلاس Supplier نیز به همراه دارد:
namespace jqGrid02.Models { public class Product { public int Id { set; get; } public string Name { set; get; } public decimal Price { set; get; } public Supplier Supplier { set; get; } } public class Supplier { public int Id { set; get; } public string CompanyName { set; get; } public string Address { set; get; } public string PostalCode { set; get; } public string City { set; get; } public string Country { set; get; } public string Phone { set; get; } public string HomePage { set; get; } } }
کدهای سمت سرور
کدهای سمت سرور مانند متد GetProducts به همراه صفحه بندی و مرتب سازی پویای آن دقیقا مانند قسمت قبل است.
در اینجا فقط یک اکشن متد جدید جهت بازگشت اطلاعات تولید کنندهای مشخص با فرمت JSON، اضافه شدهاست:
public ActionResult GetGetSupplierData(int id) { var list = ProductDataSource.LatestProducts; var product = list.FirstOrDefault(x => x.Id == id); if (product == null) return Json(null, JsonRequestBehavior.AllowGet); return Json(new { product.Supplier.CompanyName, product.Supplier.Address, product.Supplier.PostalCode, product.Supplier.City, product.Supplier.Country, product.Supplier.Phone, product.Supplier.HomePage }, JsonRequestBehavior.AllowGet); }
کدهای سمت کلاینت
صفحه دیالوگی که قرار است اطلاعات تولید کننده را نمایش دهد، یک چنین ساختاری دارد:
<div dir="rtl" id="supplierDialog"> <span id="CompanyName"></span><br /><br /> <span id="Address"></span><br /> <span id="PostalCode"></span>, <span id="City"></span><br /> <span id="Country"></span><br /><br /> <span id="Phone"></span><br /> <span id="HomePage"></span> </div>
<script type="text/javascript"> function showSupplierDialog(linkElement, supplierId) { //request json data $.getJSON('@Url.Action("GetGetSupplierData","Home")', { id: supplierId }, function (data) { //set values in dialog for (var property in data) { if (data.hasOwnProperty(property)) { $('#' + property).text(data[property]); } } //get link position var linkPosition = $(linkElement).offset(); $('#supplierDialog').dialog('option', 'position', [linkPosition.left, linkPosition.top]); //open dialog $('#supplierDialog').dialog('open'); }); } $(document).ready(function () { $('#supplierDialog').dialog({ autoOpen: false, bgiframe: true, resizable: false, title: 'تولید کننده' }); $('#list').jqGrid({ // .... مانند قبل colNames: ['شماره', 'نام محصول', 'قیمت'], //columns model colModel: [ { name: 'Id', index: 'Id', align: 'right', width: 20, formatter: function (cellvalue, options, rowObject) { var cellValueInt = parseInt(cellvalue); if (cellValueInt == 5) { return "<span style='background: brown; color: yellow'>" + cellvalue + "</span>"; } return cellvalue; } }, { name: 'Name', index: 'Name', align: 'right', width: 300, formatter: function (cellvalue, options, rowObject) { return "<a href='#' onclick='showSupplierDialog(this, " + rowObject[0] + ");'>" + cellvalue + "</a>"; } }, { name: 'Price', index: 'Price', align: 'center', width: 50, formatter: 'currency', formatoptions: { decimalSeparator: '.', thousandsSeparator: ',', decimalPlaces: 2, prefix: '$' } } ], // .... مانند قبل }); }); </script>
در حالت ستون Id، از یک formatter سفارشی استفاده شدهاست. در اینجا این فرمت کننده به صورت یک callback عمل کرده و پیش از رندر نهایی اطلاعات، مقدار سلول جاری را توسط cellvalue در اختیار ما قرار میدهد. در این بین هر نوع فرمتی را که نیاز است میتوان اعمال کرد و سپس یک رشته را بازگشت میدهیم. این رشته در سلول جاری درج خواهد شد.
- اگر مانند ستون Name، نیاز به مقادیر سایر سلولها نیز وجود داشت، میتوان از آرایهی rowObject استفاده کرد. برای مثال در این حالت، یک لینک که کلیک بر روی آن سبب فراخوانی تابع showSupplierDialog میشود، در سلولهای ستون Name درج خواهند شد. اولین rowObject که در اینجا مورد استفاده است، به ستون اول یا همان Id محصول اشاره میکند.
- در ستون Price از یک سری formatter از پیش تعریف شده استفاده شدهاست. نمونهای از آن را در قسمت اول در ستون نمایش وضعیت موجود بودن محصول با تنظیم formatter: checkbox مشاهده کردهاید. در اینجا از یک formatter توکار دیگر به نام currency برای کار با مقادیر پولی استفاده شدهاست به همراه تنظیمات خاص آن.
- متد showSupplierDialog طوری تنظیم شدهاست که پس از دریافت Id یک محصول، آنرا به سرور ارسال کرده و مشخصات تولید کنندهی آنرا با فرمت JSON دریافت میکند. سپس در حلقهای که مشاهده میکنید، خواص شیء جاوا اسکریپتی دریافتی استخراج و به spanهای supplierDialog انتساب داده میشوند. جهت سهولت کار، Id این spanها دقیقا مساوی Id خواص شیء دریافتی از سرور، درنظر گرفته شدهاند.
- در مورد راست به چپ نمایش داده شدن عنوان دیالوگ، تغییرات CSS ایی لازم است که در قسمت اول بیان شدند.
برای مطالعه بیشتر
لیست کامل فرمت کنندههای توکار
فرمت کنندههای سفارشی
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid02.zip
گام سوم: افزودن یک button
... <div> <div> <input type="text" id="inputName" maxlength="15"> </div> <div> <button id="generateButton">Aye! Gimme a name!</button> </div> </div> ...
import 'dart:html'; ButtonElement genButton;
void main() { querySelector('#inputName').onInput.listen(updateBadge); genButton = querySelector('#generateButton'); genButton.onClick.listen(generateBadge); }
... void setBadgeName(String newName) { querySelector('#badgeName').text = newName; }
... void generateBadge(Event e) { setBadgeName('Meysam Khoshbakht'); }
void updateBadge(Event e) { String inputName = (e.target as InputElement).value; setBadgeName(inputName); }
void updateBadge(Event e) { String inputName = (e.target as InputElement).value; setBadgeName(inputName); if (inputName.trim().isEmpty) { // To do: add some code here. } else { // To do: add some code here. } }
void updateBadge(Event e) { String inputName = (e.target as InputElement).value; setBadgeName(inputName); if (inputName.trim().isEmpty) { genButton..disabled = false ..text = 'Aye! Gimme a name!'; } else { genButton..disabled = true ..text = 'Arrr! Write yer name!'; } }
گام چهارم: ایجاد کلاس PirateName
در این مرحله فقط کد مربوط به فایل dart را تغییر میدهیم. ابتدا کلاس PirateName را ایجاد میکنیم. با ایجاد نمونه ای از این کلاس، یک نام بصورت تصادفی انتخاب میشود و یا نامی بصورت اختیاری از طریق سازنده انتخاب میگردد.
نخست کتابخانه dart:math را به ابتدای فایل dart اضافه کنید
import 'dart:html'; import 'dart:math' show Random;
توضیحات
- با استفاده از کلمه کلیدی show، شما میتوانید فقط کلاسها، توابع و یا ویژگیهای مورد نیازتان را import کنید.
- کلاس Random یک عدد تصادفی را تولید میکند
در انتهای فایل کلاس زیر را تعریف کنید
... class PirateName { }
در داخل کلاس یک شی از کلاس Random ایجاد کنید
class PirateName { static final Random indexGen = new Random(); }
توضیحات
- با استفاده از static یک فیلد را در سطح کلاس تعریف میکنیم که بین تمامی نمونههای ایجاد شده از کلاس مشترک میباشد
- متغیرهای final فقط خواندنی میباشند و غیر قابل تغییر هستند.
- با استفاده از new میتوانیم سازنده ای را فراخوانی نموده و نمونه ای را از کلاس ایجاد کنیم
دو فیلد دیگر از نوع String و با نامهای _firstName و _appelation به کلاس اضافه میکنیم
class PirateName { static final Random indexGen = new Random(); String _firstName; String _appellation; }
متغیرهای خصوصی با (_) تعریف میشوند. Dart کلمه کلیدی private را ندارد.
دو لیست static به کلاس فوق اضافه میکنیم که شامل لیستی از name و appelation میباشد که میخواهیم آیتمی را بصورت تصادفی از آنها انتخاب کنیم.
class PirateName { ... static final List names = [ 'Anne', 'Mary', 'Jack', 'Morgan', 'Roger', 'Bill', 'Ragnar', 'Ed', 'John', 'Jane' ]; static final List appellations = [ 'Jackal', 'King', 'Red', 'Stalwart', 'Axe', 'Young', 'Brave', 'Eager', 'Wily', 'Zesty']; }
کلاس List میتواند شامل مجموعه ای از آیتمها میباشد که در Dart تعریف شده است.
سازنده ای را بصورت زیر به کلاس اضافه میکنیم
class PirateName { ... PirateName({String firstName, String appellation}) { if (firstName == null) { _firstName = names[indexGen.nextInt(names.length)]; } else { _firstName = firstName; } if (appellation == null) { _appellation = appellations[indexGen.nextInt(appellations.length)]; } else { _appellation = appellation; } } }
توضیحات
- سازنده تابعی همنام کلاس میباشد
- پارامترهایی که در {} تعریف میشوند اختیاری و Named Parameter میباشند. Named Parameterها پارمترهایی هستند که جهت مقداردهی به آنها در زمان فراخوانی، از نام آنها استفاده میشود.
- تابع nextInt() یک عدد صحیح تصادفی جدید را تولید میکند.
- جهت دسترسی به عناصر لیست از [] و شمارهی خانهی لیست استفاده میکنیم.
- ویژگی length تعداد آیتمهای موجود در لیست را بر میگرداند.
در این مرحله یک getter برای دسترسی به pirate name ایجاد میکنیم
class PirateName { ... String get pirateName => _firstName.isEmpty ? '' : '$_firstName the $_appellation'; }
توضیحات
- Getterها متدهای خاصی جهت دسترسی به یک ویژگی به منظور خواندن مقدار آنها میباشند.
- عملگر سه گانه :? دستور میانبر عبارت شرطی if-else میباشد
- $ یک کاراکتر ویژه برای رشتههای موجود در Dart میباشد و میتواند محتوای یک متغیر یا ویژگی را در رشته قرار دهد. در رشته '$_firstName the $_appellation' محتوای دو ویژگی _firstName و _appellation در رشته قرار گرفته و نمایش مییابند.
- عبارت (=> expr;) یک دستور میانبر برای { return expr; } میباشد.
تابع setBadgeName را بصورت زیر تغییر دهید تا یک پارامتر از نوع کلاس PirateName را به عنوان پارامتر ورودی دریافت نموده و با استفاده از Getter مربوط به ویژگی pirateName، مقدار آن را در badge name نمایش دهد.
void setBadgeName(PirateName newName) { querySelector('#badgeName').text = newName.pirateName; }
تابع updateBadge را بصورت زیر تغییر دهید تا یک نمونه از کلاس PirateName را با توجه به مقدار ورودی کاربر در فیلد input تولید نموده و تابع setBadgeName رافراخوانی نماید. همانطور که در کد مشاهده میکنید پارامتر ورودی اختیاری firstName در زمان فراخوانی با ذکر نام پارامتر قبل از مقدار ارسالی نوشته شده است. این همان قابلیت Named Parameter میباشد.
void updateBadge(Event e) { String inputName = (e.target as InputElement).value; setBadgeName(new PirateName(firstName: inputName)); ... }
تابع generateBadge را بصورت زیر تغییر دهید تا به جای نام ثابت Meysam Khoshbakht، از کلاس PirateName به منظور ایجاد نام استفاده کند. همانطور که در کد میبینید، سازندهی بدون پارامتر کلاس PirateName فراخوانی شده است.
void generateBadge(Event e) { setBadgeName(new PirateName()); }
همانند گام سوم برنامه را اجرا کنید و نتیجه را مشاهده نمایید.
آموزش Knockout.Js #4
Click Binding روشی است برای اضافه کردن یک گرداننده رویداد در زمانی که قصد داریم یک تابع جاوااسکریپتی را در هنگام کلیک بر روی المان مورد نظر فراخوانی کنیم. از این مقید سازی عموما در عناصر button و input و تگ a استفاده میشود. اما در حقیقت در تمام عناصر غیر پنهان صفحه مورد استفاده قرار میگیرد.
<div> Number Of Clicks <span data-bind="text: numberOfClicks"></span> times <button data-bind="click: clickMe">Click me</button> </div> <script type="text/javascript"> var viewModel = { numberOfClicks : ko.observable(0), clickMe: function() { var previousCount = this.numberOfClicks(); this.numberOfClicks(previousCount + 1); } }; </script>
*نکته اول: اگر قصد داشته باشیم که عنصر جاری در viewModel را به گرداننده رویداد پاس دهیم چه باید کرد؟
هنگام فراخوانی رویدادها، KO به صورت پیش فرض مقدار جاری مدل را به عنوان اولین پارامتر به این گرداننده پاس میدهد. این روش مخصوصا در هنگامی که قصد اجرای عملیاتی خاص بر روی تک تک عناصر یک مجموعه را داشته باشید(مثل حلقه foreach) بسیار مفید خواهد بود.
<ul data-bind="foreach: places"> <li> <span data-bind="text: $data"></span> <button data-bind="click: $parent.removePlace">Remove</button> </li> </ul> <script type="text/javascript"> function MyViewModel() { var self = this; self.places = ko.observableArray(['Tehran', 'Esfahan', 'Shiraz']); self.removePlace = function(place) { self.places.remove(place) } } ko.applyBindings(new MyViewModel()); </script>
همان طور که پست قبل توضیح داده شد؛ برای اینکه بتوانیم از یک viewModel به مجموعه از عناصر در یک حلقه foreach مقید کنیم امکان استفاده از اشاره گر this میسر نیست. در نتیجه بهتر است در ابتدای viewModel مقدار این اشاره گر را در یک متغیر معمولی (در اینجا به نام self است) ذخیره کنیم و از این پس این متغیر را برای اشاره به عناصر viewModel به کار بریم. در اینجا self به عنواتن یک alias برای this خواهد بود.
*نکته دوم: دسترسی به عنصر رویداد
در بعضی مواقع نیاز است در حین فراخوانی رویداد ،عنصر رویداد DOM به عنوان فرستنده در اختیار تابع گرداننده قرار گیرد. خبر خوش این است که KO به صورت پیش فرض این عنصر را نیز به عنوان پارامتر دوم به توابع گرداننده رویداد پاس میدهد. برای مثال:
<button data-bind="click: myFunction"> Click me </button> <script type="text/javascript"> var viewModel = { myFunction: function(data, event) { if (event.shiftKey) { } else { } } }; ko.applyBindings(viewModel); </script>
*نکته سوم: به صورت پیش فرض KO از اجرای عملیات پیش فرض رویدادها جلوگیری به عمل میآورد. این به این معنی است که اگر برای رویداد کلیک تگ a بک تابع گرداننده تعریف کرده باشید، بعد از کلیک بر روی این المان؛ مرورگر فقط این تابع تعریف شده توسط شما را فراخوانی خواهد کرد و دیگر عملیات راهبری به صفحه مورد نظر در خاصیت href صورت نخواهد گرفت. اگر به هر دلیلی قصد داشته باشیم که این رفتار صورت نگیرد کافیست در انتهای تابع گرداننده رویداد مقدار true برگشت داده شود.
*نکته چهارم: مفهوم clickBubble
ابتدا به کد زیر دقت کنید:
<div data-bind="click: myDivHandler"> <button data-bind="click: myButtonHandler"> Click me </button> </div>
<div data-bind="click: myDivHandler"> <button data-bind="click: myButtonHandler, clickBubble: false"> Click me </button> </div>
OpenCVSharp #12
در تصویر ذیل، تصویر یک راهرو را مشاهده میکنید که توسط ماوس قطعه بندی شدهاست (تصویر اصلی یا سمت چپ). تصویر سمت راست، نسخهی قطعه بندی شدهی این تصویر به کمک الگوریتم watershed است.
همانطور که در تصویر نیز مشخص است، نمایش هر ناحیهی قطعه بندی شده، شبیه به سیلان آب است که با رسیدن به مرز قطعهی بعدی متوقف شدهاست. به همین جهت به آن watershed (آب پخشان) میگویند.
انتخاب نواحی مختلف به کمک ماوس
در اینجا کدهای آغازین مثال بحث جاری را ملاحظه میکنید:
var src = new Mat(@"..\..\Images\corridor.jpg", LoadMode.AnyDepth | LoadMode.AnyColor); var srcCopy = new Mat(); src.CopyTo(srcCopy); var markerMask = new Mat(); Cv2.CvtColor(srcCopy, markerMask, ColorConversion.BgrToGray); var imgGray = new Mat(); Cv2.CvtColor(markerMask, imgGray, ColorConversion.GrayToBgr); markerMask = new Mat(markerMask.Size(), markerMask.Type(), s: Scalar.All(0)); var sourceWindow = new Window("Source (Select areas by mouse and then press space)") { Image = srcCopy }; var previousPoint = new Point(-1, -1); sourceWindow.OnMouseCallback += (@event, x, y, flags) => { if (x < 0 || x >= srcCopy.Cols || y < 0 || y >= srcCopy.Rows) { return; } if (@event == MouseEvent.LButtonUp || !flags.HasFlag(MouseEvent.FlagLButton)) { previousPoint = new Point(-1, -1); } else if (@event == MouseEvent.LButtonDown) { previousPoint = new Point(x, y); } else if (@event == MouseEvent.MouseMove && flags.HasFlag(MouseEvent.FlagLButton)) { var pt = new Point(x, y); if (previousPoint.X < 0) { previousPoint = pt; } Cv2.Line(img: markerMask, pt1: previousPoint, pt2: pt, color: Scalar.All(255), thickness: 5); Cv2.Line(img: srcCopy, pt1: previousPoint, pt2: pt, color: Scalar.All(255), thickness: 5); previousPoint = pt; sourceWindow.Image = srcCopy; } };
همچنین این نسخهی سیاه و سفید تک کاناله به یک تصویر سه کاناله برای نمایش رنگهای قسمتهای مختلف قطعه بندی شده، تبدیل میشود.
سپس پنجرهی نمایش تصویر اصلی برنامه ایجاد شده و در اینجا روال رخدادگردان OnMouseCallback آن به صورت inline مقدار دهی شدهاست. در این روال میتوان مدیریت ماوس را به عهده گرفت و کار نمایش خطوط مختلف را با فشرده شدن و سپس رها شدن کلیک سمت چپ ماوس انجام داد.
خط ترسیم شده بر روی دو تصویر از نوع Mat نمایش داده میشود. تصویر srcCopy، همان تصویر نمایش داده شدهی در پنجرهی اصلی است و تصویر markerMask، بیشتر جنبهی محاسباتی دارد و در متدهای بعدی OpenCV استفاده خواهد شد.
تشخیص کانتورها (Contours) در تصویر
پس از ترسیم نواحی مورد نظر توسط ماوس، یک سری خطوط به هم پیوسته در شکل قابل مشاهده هستند. میخواهیم این خطوط را تشخیص داده و سپس از آنها جهت محاسبات قطعه بندی تصویر استفاده کنیم. تشخیص این خطوط متصل، توسط متدی به نام FindContours انجام میشود. کانتورها، قسمتهای خارجی اجزای متصل به هم هستند.
Point[][] contours; //vector<vector<Point>> contours; HiearchyIndex[] hierarchyIndexes; //vector<Vec4i> hierarchy; Cv2.FindContours( markerMask, out contours, out hierarchyIndexes, mode: ContourRetrieval.CComp, method: ContourChain.ApproxSimple);
متد FindContours یک تصویر 8 بیتی تک کاناله را دریافت میکند. اگر mode آن CCOMP یا FLOODFILL تعریف شود، امکان دریافت یک تصویر 32 بیتی را نیز خواهد داشت.
پارامتر hierarchy آن یک پارامتر اختیاری است که بیانگر اطلاعات topology تصویر است.
توسط پارامتر Mode، نحوهی استخراج کانتور مشخص میشود. اگر به external تنظیم شود، تنها کانتورهای خارجیترین قسمتها را تشخیص میدهد. اگر مساوی list قرار گیرد، تمام کانتورها را بدون ارتباطی با یکدیگر و بدون تشکیل hierarchy استخراج میکند. حالت ccomp تمام کانتورها را استخراج کرده و یک درخت دو سطحی از آنها را تشکیل میدهد. در سطح بالایی مرزهای خارجی اجزاء وجود دارند و در سطح دوم مرزهای حفرهها مشخص شدهاند. حالت و مقدار tree به معنای تشکیل یک درخت کامل از کانتورهای یافت شدهاست.
پارامتر method اگر به none تنظیم شود، تمام نقاط کانتور ذخیره خواهند شد و اگر به simple تنظیم شود، قطعههای افقی، عمودی و قطری، فشرده شده و تنها نقاط نهایی آنها ذخیره میشوند. برای مثال در این حالت یک کانتور مستطیلی، تنها با 4 نقطه ذخیره میشود.
ترسیم کانتورهای تشخیص داده شده بر روی تصویر
میتوان به کمک متد DrawContours، مرزهای کانتورهای یافت شده را ترسیم کرد:
var markers = new Mat(markerMask.Size(), MatType.CV_32S, s: Scalar.All(0)); var componentCount = 0; var contourIndex = 0; while ((contourIndex >= 0)) { Cv2.DrawContours( markers, contours, contourIndex, color: Scalar.All(componentCount + 1), thickness: -1, lineType: LineType.Link8, hierarchy: hierarchyIndexes, maxLevel: int.MaxValue); componentCount++; contourIndex = hierarchyIndexes[contourIndex].Next; }
اعمال الگوریتم watershed
در مرحلهی آخر، تصویر کانتورهای ترسیم شده را به متد Watershed ارسال میکنیم. پارامتر اول آن تصویر اصلی است و پارامتر دوم، یک پارامتر ورودی و خروجی محسوب میشود و کار قطعه بندی تصویر بر روی آن انجام خواهد شد.
کار الگوریتم watershed، ایزوله سازی اشیاء موجود در تصویر از پس زمینهی آنها است. این الگوریتم، یک تصویر سیاه و سفید را دریافت میکند؛ به همراه یک تصویر ویژه به نام marker. تصویر marker کارش مشخص سازی اشیاء، از پس زمینهی آنها است که در اینجا توسط ماوس ترسیم و سپس به کمک یافتن کانتورها و ترسیم آنها بهینه سازی شدهاست.
var rnd = new Random(); var colorTable = new List<Vec3b>(); for (var i = 0; i < componentCount; i++) { var b = rnd.Next(0, 255); //Cv2.TheRNG().Uniform(0, 255); var g = rnd.Next(0, 255); //Cv2.TheRNG().Uniform(0, 255); var r = rnd.Next(0, 255); //Cv2.TheRNG().Uniform(0, 255); colorTable.Add(new Vec3b((byte)b, (byte)g, (byte)r)); } Cv2.Watershed(src, markers); var watershedImage = new Mat(markers.Size(), MatType.CV_8UC3); // paint the watershed image for (var i = 0; i < markers.Rows; i++) { for (var j = 0; j < markers.Cols; j++) { var idx = markers.At<int>(i, j); if (idx == -1) { watershedImage.Set(i, j, new Vec3b(255, 255, 255)); } else if (idx <= 0 || idx > componentCount) { watershedImage.Set(i, j, new Vec3b(0, 0, 0)); } else { watershedImage.Set(i, j, colorTable[idx - 1]); } } } watershedImage = watershedImage * 0.5 + imgGray * 0.5; Cv2.ImShow("Watershed Transform", watershedImage); Cv2.WaitKey(1); //do events
پس از اعمال متد Watershed، هر نقطهی تصویر marker مشخص میکند که متعلق به کدام قطعهی تشخیص داده شدهاست. سپس به این نقطه، رنگ آن قطعه را نسبت داده و آنرا در تصویر جدیدی ترسیم میکنیم.
در آخر، پس زمینه، با نواحی تشخیص داده ترکیب شدهاند (watershedImage * 0.5 + imgGray * 0.5) تا تصویر ابتدای بحث حاصل شود. اگر این ترکیب صورت نگیرد، چنین تصویری حاصل خواهد شد:
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
مسیریابی در +Angular 2
عموما از مسیریابی جهت حرکت بین Viewهای مختلف برنامه استفاده میشود، اما کارهای بیشتری را نیز میتوان با آن انجام داد؛ مانند ارسال اطلاعات، به مسیریابیها، پیش بارگذاری اطلاعات، جهت نمایش در Viewها، گروه بندی و محافظت از مسیریابیها، پویانمایی و انیمیشن و همچنین بهبود کارآیی، با بارگذاری async مسیرهای مختلف.
کار سیستم مسیریاب +Angular 2 زمانی شروع میشود که تغییری را در آدرس درخواستی از برنامه مشاهده میکند؛ یا از طریق درخواست آدرسی توسط مرورگر و یا هدایت به قسمتی خاص، از طریق کدنویسی. سپس مسیریاب به آرایهی تنظیم شدهی مسیرهای سیستم مراجعه میکند تا بتواند تطابقی را بین آدرس درخواستی و یکی از کلیدهای تنظیم شدهی در آن پیدا کند. در این حالت اگر تطابقی یافت نشود، کارمسیریابی خاتمه خواهد یافت. در غیراینصورت کار ادامه یافته و سپس مسیریاب، محافظهای مسیر درخواستی را بررسی میکند تا مشخص شود که آیا کاربر مجاز به هدایت به این قسمت خاص از برنامه هست یا خیر؟ در صورت مثبت بودن پاسخ، مرحلهی بعد، پیش بارگذاری اطلاعات درخواستی جهت نمایش View مرتبط است. در ادامه کامپوننت متناظر با مسیریابی فعالسازی میشود. سپس قالب این کامپوننت را در قسمتی که توسط router-outlet مشخص میگردد، جایگذاری کرده و نمایش میدهد.
تعریف مسیر پایه یا Base path
اولین مرحلهی کار با سیستم مسیریابی +Angular 2، تعریف یک base path است. مسیرپایه، به زیرپوشهای اشاره میکند که برنامهی ما در آن قرار گرفتهاست:
www.mysite.com/myapp
مسیریاب از این مسیرپایه جهت ساخت آدرسهای مسیریابی استفاده میکند. مقدار آن نیز به صورت ذیل در فایل index.html برنامه، درست پس از تگ head تعیین میگردد:
<!DOCTYPE html> <html> <head> <base href="/">
<base href="/myapp/">
تعیین مسیرپایه جهت ارائهی نهایی
استفاده از مسیر پایه / برای حالت توسعه و همچنین زمانیکه برنامهی نهایی شما در ریشهی سایت توزیع میشود، بسیار مناسب است. اما اگر برای حالت توسعه از مقدار / و برای حالت توزیع از مقدار /myapp/ بخواهید استفاده کنید، مدام نیاز خواهید داشت تا فایل index.html نهایی سایت را ویرایش کنید. برای این منظور Angular CLI دارای پرچمی است به نام base-href:
> ng build --base-href /myapp/
حالت پیش فرض تولید برنامههای Angular توسط Angular CLI، تنظیم مسیرپایه در فایل src\index.html به صورت خودکار به / میباشد.
تعریف مسیریاب Angular
مسیریاب Angular در ماژولی به نام RouterModule قرار گرفتهاست و باید در ابتدای کار import شود. این ماژول شامل سرویسی است جهت هدایت کاربران به صفحات دیگر و مدیریت URLها، تنظیماتی برای تعریف جزئیات مسیریابیها و تعدادی دایرکتیو که برای فعالسازی و نمایش مسیرها از آنها استفاده میشود. برای مثال دایرکتیو RouterLink آن یک المان قابل کلیک HTML را به مسیر و کامپوننتی خاص در برنامه متصل میکند. RouterLinkActive، شیوهنامهها را به لینک فعال انتساب میدهد و RouterOutlet محل نمایش قالب کامپوننت فعال شده را مشخص میکند.
یک مثال: در ادامه، یک پروژهی جدید مبتنی بر Angular CLI را به نام angular-routing-lab به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
> ng new angular-routing-lab --routing
> npm install bootstrap --save
"apps": [ { "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ],
در ادامه اگر به فایل src\app\app-routing.module.ts مراجعه کنید، یک چنین محتوایی را خواهید یافت:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: '', children: [] } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
میتوان این قسمت را خلاصه کرد و فایل app-routing.module.ts را نیز حذف کرد و سپس import لازم و تعریف ماژول آنرا به ماژول آغازین برنامه یا همان src\app\app.module.ts نیز منتقل کرد. اما پس از مدتی تنظیمات مسیریابی آن، فایل ماژول اصلی برنامه را بیش از اندازه شلوغ خواهند کرد. بنابراین Angular-CLI تصمیم به ایجاد یک ماژول مستقل را برای تعریف تنظیمات مسیریابی برنامه گرفتهاست. سپس تعریف آن را به فایل src\app\app.module.ts به صورت خودکار اضافه میکند:
import { AppRoutingModule } from './app-routing.module'; @NgModule({ imports: [ AppRoutingModule ],
اگر به قسمت import مربوط به NgModule فایل src\app\app-routing.module.ts دقت کنید، این ماژول به همراه متد forRoot معرفی شدهاست.
@NgModule({ imports: [RouterModule.forRoot(routes)],
الف) forRoot
- کار آن تعریف دایرکتیوهای مسیریابی، مدیریت تنظیمات مسیریابی و ثبت سرویس مسیریابی است.
- نکتهی مهم اینجا است که متد forRoot تنها یکبار باید در طول عمر یک برنامه تعریف شود.
- این متد آرایهای از تنظیمات مسیریابیهای تعریف شده را دریافت میکند.
ب) forChild
- کار آن تعریف دایرکتیوهای مسیریابی و مدیریت تنظیمات مسیریابی است؛ اما سرویس مسیریابی را مجددا ثبت نمیکند.
- از این متد در جهت تعریف مسیریابیهای ماژولهای ویژگیهای مختلف برنامه و نظم بخشیدن به آنها استفاده میشود.
بنابراین زمانیکه از forRoot استفاده میشود، سرویس مسیریابی تنها یکبار ثبت خواهد شد و تنها یک وهله از آن موجود خواهد بود. در ادامه هر کدام از ماژولهای دیگر برنامه میتوانند forChild خاص خودشان را داشته باشند.
اکنون تمام کامپوننتهای قید شدهی در قسمت declaration، امکان دسترسی به دایرکتیوهای مسیریابی را پیدا میکنند. همچنین از آنجائیکه AppRoutingModule به همراه متد forRoot است، سرویس مسیریابی نیز در کل برنامه قابل دسترسی است.
تنظیمات اولیه مسیریابی برنامه
آرایهی const routes: Routes فایل src\app\app-routing.module.ts در ابتدای کار خالی است. در اینجا کار تعریف URL segments و سپس اتصال آنها به کامپوننتهای متناظری جهت فعالسازی و نمایش قالب آنها صورت میگیرد. این نمایش نیز در محل router-outlet تعریف شدهی در فایل src\app\app.component.html انجام میشود:
<h1> {{title}} </h1> <router-outlet></router-outlet>
در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوشآمد گویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه میکنیم:
>ng g c welcome >ng g c PageNotFound
@NgModule({ declarations: [ AppComponent, WelcomeComponent, PageNotFoundComponent ],
سپس فایل src\app\app-routing.module.ts را به نحو ذیل تکمیل نمائید:
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { WelcomeComponent } from './welcome/welcome.component'; import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'welcome', component: WelcomeComponent }, { path: '', redirectTo: 'welcome', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
یک نکته: افزونهی auto import، کار تعریف کامپوننتها را در VSCode بسیار ساده میکند و امکان تشکیل خودکار قسمت import را با ارائهی یک intellisense به همراه دارد.
سپس کار تکمیل آرایهی Routes انجام شدهاست. همانطور که مشاهده میکنید، این آرایه متشکل است از اشیایی که به همراه خاصیت path و سایر پارامترهای مورد نیاز هستند.
کار خاصیت path، تعیین URL segment متناظری است که این مسیریابی را فعال میکند. برای مثال اولین شیء تعریف شده با آدرسهایی مانند www.mysite.com/welcome متناظر است.
{ path: 'welcome', component: WelcomeComponent },
چند نکته:
- در حین تعریف مقدار خاصیت path، هیچ / آغاز کنندهای تعریف نشدهاست.
- مقدار خاصیت path، حساس به کوچکی و بزرگی حروف است.
- WelcomeComponent تعریف شده، یک رشته نیست و ارجاعی را به کامپوننت مرتبط دارد. به همین جهت نیاز به import statement ابتدایی را دارد و وجود آن توسط کامپایلر بررسی میشود.
تعیین مسیریابی پیش فرض سایت
اما زمانیکه برنامه برای بار اول بارگذاری میشود، چطور؟ در این حالت هیچ URL segment ایی وجود ندارد. بنابراین برای تنظیم مسیرپیش فرض سایت، خاصیت path، به یک رشتهی خالی همانند دومین شیء تنظیمات مسیریابی، تنظیم میشود:
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
مدیریت مسیریابی آدرسهای ناموجود در سایت
تنظیم سومی را نیز در اینجا مشاهده میکنید:
{ path: '**', component: PageNotFoundComponent },
یک نکته: ترتیب مسیریابیها در آرایهی تعریف آنها اهمیت دارد. در اینجا از استراتژی «اولین تطابق یافته، برنده خواهد بود» استفاده میشود. بنابراین تنظیم ** باید در انتهای لیست ذکر شود؛ در غیراینصورت هیچکدام از مسیریابیهای تعریف شدهی پس از آن پردازش نخواهند شد.
مدیریت تغییرات آدرسهای برنامه
در طول عمر برنامه ممکن است نیاز به تغییر آدرسهای برنامه باشد. برای مثال بجای مسیر welcome مسیر home نمایش داده شود:
const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: '', redirectTo: 'welcome', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
نکته: redirectToها قابلیت تعریف زنجیرهای را ندارند. به این معنا که اگر ریشهی سایت درخواست شود، ابتدا به مسیر welcome هدایت خواهیم شد. مسیر welcome هم یک redirectTo دیگر به مسیر home را دارد. اما در اینجا کار به این redirectTo دوم نخواهد رسید و این پردازش، زنجیرهای نیست. بنابراین مسیریابی پیشفرض را نیز باید ویرایش کرد و به home تغییر داد:
const routes: Routes = [ { path: 'home', component: WelcomeComponent }, { path: 'welcome', redirectTo: 'home', pathMatch: 'full' }, { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
نکته: redirectToها میتوانند local و یا absolute باشند. تعریف محلی آنها مانند ذکر home و welcome در اینجا است و تنها سبب تغییر یک URL segment میشود. اما اگر در ابتدای مقادیر redirectToها یک / قرار دهیم، به معنای تعریف یک مسیر مطلق است و کل URL را جایگزین میکند.
تعیین محل نمایش قالبهای کامپوننتها
زمانیکه یک کامپوننت فعالسازی میشود، قالب آن در router-outlet نمایش داده خواهد شد. برای این منظور فایل src\app\app.component.html را گشوده و به نحو ذیل تغییر دهید:
<nav class="navbar navbar-default"> <div class="container-fluid"> <a class="navbar-brand">{{title}}</a> <ul class="nav navbar-nav"> <li> <a [routerLink]="['/home']">Home</a> </li> </ul> </div> </nav> <div class="container"> <router-outlet></router-outlet> </div>
یک نکته: چون کامپوننت welcome از طریق مسیریابی نمایش داده میشود و دیگر به صورت مستقیم با درج تگ selector آن در صفحه فعالسازی نخواهد شد، میتوان به تعریف کامپوننت آن مراجعه کرده و selector آنرا حذف کرد.
@Component({ //selector: 'app-welcome', templateUrl: './welcome.component.html', styleUrls: ['./welcome.component.css'] })
تا اینجا اگر دستور ng serve -o را صادر کنیم (کار build درون حافظهای جهت محیط توسعه و نمایش خودکار برنامه در مرورگر)، چنین خروجی در مرورگر نمایان خواهد شد:
اگر به آدرس تنظیم شدهی در مرورگر دقت کنید، http://localhost:4200/home آدرسی است که در ابتدای نمایش سایت نمایان خواهد شد. علت آن نیز به تنظیم مسیریابی پیش فرض سایت برمیگردد.
و اگر یک مسیر غیرموجود را درخواست دهیم، قالب کامپوننت PageNotFound ظاهر میشود:
هدایت کاربران به قسمتهای مختلف برنامه
کاربران را میتوان به روشهای مختلفی به قسمتهای گوناگون برنامه هدایت کرد؛ برای مثال با کلیک بر روی المانهای قابل کلیک HTML و سپس اتصال آنها به کامپوننتهای برنامه. استفادهی کاربر از bookmark مرورگر و یا ورود مستقیم و دستی آدرس قسمتی از برنامه و یا کلیک بر روی دکمههای forward و back مرورگر. تنها مورد اول است که نیاز به تنظیم دارد و سایر قسمتها به صورت خودکار مدیریت خواهند شد. نمونهی آنرا نیز با تعریف لینک Home پیشتر مشاهده کردید:
<a [routerLink]="['/home']">Home</a>
- زمانیکه کاربر بر روی این لینک کلیک میکند، اولین path متناظر با routerLink یافت شده و فعالسازی خواهد شد.
- علت تعریف مقدار routerLink به صورت [] این است که آرایهی پارامترهای لینک را مشخص میکند. بنابراین چون آرایهاست، نیاز به [] دارد. اولین پارامتر این آرایه مفهوم root URL segment را دارد. در اینجا حتما نیاز است URL segment را با یک / شروع کرد. به علاوه باید دقت داشت که خاصیت path تنظیمات مسیریابی، حساس به حروف کوچک و بزرگ است. بنابراین این مورد را باید در اینجا نیز مدنظر داشت.
- پارامترهای دیگر routerLink میتوانند مفهوم پارامترهای این segment و یا حتی segments دیگری باشند.
یک نکته: چون در مثال فوق، آرایهی تعریف شده تنها دارای یک عضو است، آنرا میتوان به صورت ذیل نیز خلاصه نویسی کرد (one-time binding):
<a routerLink="/home">Home</a>
تفاوت بین آدرسهای HTML 5 و Hash-based
زمانیکه مسیریاب Angular کار پردازش آدرسهای رسیده را انجام میدهد، اینکار در سمت کلاینت صورت میگیرد و تنها URL segment مدنظر را تغییر داده و این درخواست را به سمت سرور ارسال نمیکند. به همین جهت سبب reload صفحه نمیشود. دو روش در اینجا جهت مدیریت سمت کلاینت آدرسها قابل استفاده است:
الف) HTML 5 Style
- آدرسی مانند http://localhost:4200/home، یک آدرس به شیوهی HTML 5 است. در اینجا مسیریاب Angular با استفاده از HTML 5 history pushState سبب به روز رسانی History مرورگر شده و آدرسها را بدون ارسال درخواستی به سمت سرور، در همان سمت کلاینت تغییر میدهد.
- این روش حالت پیش فرض Angular است و نحوهی نمایش آن بسیار طبیعی به نظر میرسد.
- در اینجا URL rewriting سمت سرور نیز جهت هدایت آدرسها، به برنامهی Angular ضروری است. برای مثال زمانیکه کاربری آدرس http://localhost:4200/home را مستقیما در مرورگر وارد میکند، این درخواست ابتدا به سمت سرور ارسال خواهد شد و چون چنین صفحهای در سمت سرور وجود ندارد، پیغام خطای 404 را دریافت میکند. اینجا است که URL rewriting سمت سرور به فایل index.html برنامه، جهت مدیریت یک چنین حالتهایی ضروری است.
برای نمونه اگر از وب سرور IIS استفاده میکنید، تنظیم ذیل را به فایل web.config در قسمت system.webServer اضافه کنید (کار کرد آن هم وابستهاست به نصب و فعالسازی ماژول URL Rewrite بر روی IIS):
<rewrite> <rules> <rule name="Angular 2+ pushState routing" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> <add input="{REQUEST_FILENAME}" pattern=".*\.[\d\w]+$" negate="true" /> <add input="{REQUEST_URI}" pattern="^/(api)" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite>
ب) Hash-based
- آدرسی مانند http://localhost:4200/#/home یک آدرس به شیوهی Hash-based بوده و مخصوص مرورگرهایی است بسیار قدیمی که از HTML 5 پشتیبانی نمیکنند. اینبار قطعات قرار گرفتهی پس از علامت # دارای نام URL fragments بوده و قابلیت پردازش در سمت کلاینت را دارا میباشند.
- اگر علاقمند به استفادهی از این روش هستید، نیاز است خاصیت useHash را به true تنظیم کنید:
@NgModule({ imports: [RouterModule.forRoot(routes, { useHash: true })],
ترجیح بنده، استفاده از Angular Material Design است ولی در صورتی که قصد استفاده از بوت استرپ را در برنامههای Angular دارید این لینک میتواند مفید باشد.