<script src="assets/js/core/app-menu.js"></script> <script src="assets/js/core/app.js"></script>
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>http://www.example.com/</loc> <lastmod>2005-01-01</lastmod> <changefreq>monthly</changefreq> <priority>0.8</priority> </url> </urlset>
که یک فایل XML متشکل از یک تگ urlset است و این تگ نیز حاوی یک یا چند تگ url میباشد. با توجه به تعاریف بالا به یک چنین کلاسی خواهیم رسید:
public enum ChangeFreq { Always, Hourly, Daily, Weekly, Monthly, Yearly, Never } [XmlElement("loc")] public string Url { get; set; } [XmlElement("lastmod")] public DateTime? LastModified { get; set; } public bool ShouldSerializeLastModified() { return LastModified.HasValue; } [XmlElement("changefreq")] public ChangeFreq? ChangeFrequency { get; set; } public bool ShouldSerializeChangeFrequency() { return ChangeFrequency.HasValue; } [XmlElement("priority")] public float? Priority { get; set; } public bool ShouldSerializePriority() { return Priority.HasValue; } }
دقت داشته باشید که چون پروپرتیهای LastModified ، ChangeFrequency و Priority از نوع Nullable تعریف شدهاند، پس باید کاری کنیم در صورتیکه این پروپرتیها نال بودند سریالیز نشوند. بدین منظور از تابع ShouldSerialize[MemberName] استفاده میشود. این تابع جزئی از دات نت است. کافی است بعد از ShouldSerialize نام پروپرتی را ذکر کنید. حال به کلاس دیگری نیاز داریم تا لیستی از کلاس فوق را دربر داشته باشد.
[XmlRoot("urlset",Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9")] public class SiteMp { private readonly List<Location> _locations; public SiteMp() { _locations = new List<Location>(); } [XmlElement("url")] public List<Location> Locations { get { return _locations; } set { foreach (var location in value) { Add(location); } } } public void Add(Location location) { _locations.Add(location); } }
حال برای پردازش کلاس بالا لازم است ActionResultی را طراحی کنیم تا خروجی Response را به فرمت XML پردازش کند:
public class XmlResult : ActionResult { private readonly object _objectToSerialize; public XmlResult(object objectToSerialize) { _objectToSerialize = objectToSerialize; } public override void ExecuteResult(ControllerContext context) { if (_objectToSerialize == null) return; context.HttpContext.Response.Clear(); var xmlSerializer = new XmlSerializer(_objectToSerialize.GetType()); context.HttpContext.Response.ContentType = "text/xml"; xmlSerializer.Serialize(context.HttpContext.Response.Output, _objectToSerialize); } }
و در آخر یک کنترلر ساخته و به صورت زیر از آن استفاده میکنیم:
public class SiteMapController : Controller { // GET: SiteMap public ActionResult Index() { SiteMp siteMap = new SiteMp(); siteMap.Add(new Location { Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/Index" }); siteMap.Add(new Location { Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/NewRequest", ChangeFrequency = Location.ChangeFreq.Always, LastModified = DateTime.UtcNow, Priority = 0.5f }); siteMap.Add(new Location { Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/FindRequest", ChangeFrequency = Location.ChangeFreq.Always, LastModified = DateTime.UtcNow, Priority = 0.5f }); siteMap.Add(new Location { Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/ContactUs/Index", ChangeFrequency = Location.ChangeFreq.Daily, LastModified = DateTime.UtcNow, Priority = 0.5f }); return new XmlResult(siteMap); }
اگر دقت کنید لینکهای ثابت باید به صورت دستی اضافه شوند. سناریویی را تصور کنید که لینکها زیاد باشند(جدای از لینک هایی که از دیتابیس لود میشوند) این کار کمی ناجور به نظر میرسد. در اینجا میخواهیم از طریق امکانات ،Reflection عمل اضافه کردن لینک به صورت خودکار انجام شود.
public class ControllerScanner { public static List<string> ScanAllControllers(HttpRequestBase requestBase) { Assembly asm = Assembly.GetAssembly(typeof(MvcApplication)); var controllerActionlist = asm.GetTypes() .Where(type => typeof (Controller).IsAssignableFrom(type)) .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public)) .Where((returnType => returnType.ReturnType == (typeof(ViewResult)) || returnType.ReturnType==(typeof(ActionResult)))) .Select( x => new { Controller = x.DeclaringType.Name, Action = x.Name, ReturnType = x.ReturnType.Name }) .OrderBy(x => x.Controller).ThenBy(x => x.Action).Distinct().ToList(); if (requestBase.Url == null) return null; var url = requestBase.Url.GetLeftPart(UriPartial.Authority); return controllerActionlist.Select(controller => $"{url}/{controller.Controller}/{controller.Action}").ToList(); } }
حال از کلاس بالا در کنترلر SiteMap به صورت زیر استفاده میکنیم :
public class SiteMapController : Controller { // GET: SiteMap public ActionResult Index() { var siteMap = new SiteMap(); var controllers = ControllerScanner.ScanAllControllers(Request); foreach (var controller in controllers) { siteMap.Add(new Location { Url = controller, ChangeFrequency = Location.ChangeFreq.Always, LastModified = DateTime.UtcNow, Priority = 0.5f }); } return new XmlResult(siteMap); } }
در آخر نیز سطر زیر را به سیستم مسیریابی اضافه نمایید تا در صورت درخواست فایل sitemap.xml اکشن Index از کنترلر SiteMap فراخوانی شود.
routes.MapRoute( "SiteMap", // Route name "sitemap.xml", // URL with parameters new { controller = "Sitemap", action = "Index", name = UrlParameter.Optional, area = "" } );
صفحات مودال در بوت استرپ 3
- استفاده از modal dialogs مجموعه Twitter Bootstrap برای گرفتن تائید از کاربر
- نمایش فرمهای مودال Ajax ایی در ASP.NET MVC به کمک Twitter Bootstrap
این کدها نیاز به اندکی تغییر دارند تا با سیستم بوت استرپ 3 سازگار شوند.
ارتقاء کدهای صفحات مودال بوت استرپ 2 به 3
- اگر پیشتر به کلاس modal، کلاس hide را نیز اضافه میکردید، اکنون دیگر نیازی نیست؛ زیرا hide بودن به صورت پیش فرض اعمال میشود (بودن آن هم سبب میشود تا یک صفحه خاکستری نمایش داده شود؛ اما از صفحه مودال خبری نباشد).
- کلاسهای modal-header، modal-body و modal-footer بوت استرپ 2، باید داخل یک div با کلاس modal-content محصور شوند.
- کلاس modal-content باید داخل کلاس modal-dialog محصور شود.
یک مثال:
<div class="container"> <h4 class="alert alert-info"> فرمهای مودال بوت استرپ 3</h4> <div class="row"> <a data-toggle="modal" href="#myModal" class="btn btn-primary">نمایش صفحه مودال</a> <div class="modal" id="myModal"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true"> ×</button> <h4 class="modal-title"> عنوان</h4> </div> <div class="modal-body"> محتوای صفحه در اینجا </div> <div class="modal-footer"> <a href="#" data-dismiss="modal" class="btn">بستن</a> <a href="#" class="btn btn-primary"> ذخیره سازی تغییرات</a> </div> </div> </div> </div> </div> <!-- end row --> </div> <!-- /container -->
در این مثال، سلسله مراتب کلاسهای modal ایی که باید تعریف شوند را ملاحظه میکنید. همچنین لینکی با ویژگی data-toggle مساوی modal سبب نمایش این قسمت مخفی از صفحه، به صورت مودال خواهد شد.
در مثالهایی که با بوت استرپ 2 مشاهده کردید (در مقدمه بحث جاری)، این محتوای مخفی به صورت پویا با جاوا اسکریپت به body صفحه اضافه میشود.
بارگذاری یک صفحه مودال Ajax ایی
در بوت استرپ سه میتوان با استفاده از خاصیت remote تنظیمات نمایش یک صفحه مودال، به صورت خودکار اینگونه صفحات را بارگذاری کرد:
$('#myModal').modal({ show: true, remote: '/myNestedContent' });
<a data-toggle="modal" class="btn btn-primary" href="@renderModalPartialViewUrl" data-target="#myModal">Click me</a> <div class="modal fade" id="myModal" tabindex="-1" role="dialog"></div>
نکته مهم: در حالت ریموت، طراحی محتوایی که باید نمایش داده شود، نباید شامل سطر ذیل باشد. در غیراینصورت اطلاعاتی نمایش داده نخواهد شد:
<div class="modal" id="myModal">
به روز رسانی مثالهای ASP.NET MVC جهت سازگاری با بوت استرپ 3
مثال فوق را به همراه کدهای اصلاح شده دو مثال ابتدای بحث (jquery.bootstrap-modal-ajax-form.js و jquery.bootstrap-modal-confirm.js)، از لینک ذیل میتوانید دریافت کنید. این مثال به همراه قالب t4 افزودن Viewهای مودال بوت استرپ (CreateBootstrap3ModalForm.tt) نیز هست.
bs3-sample06.zip
همانطور که در قسمت اول گفته شد، اجزای رابط کاربری (تگهای HTML) در کتابخانهی React به عنوان کامپوننتها (مؤلفههای جزء) شناخته میشوند. React تگها را به عنوان اجزایی مستقل و با وضعیتی مشخص در حافظه میشناسد. دلایل ارزشمند بودن این روش در ادامه بررسی میشود.
خوانایی بهتر (Readability)
React میتواند تگهای یگانه یا مخلوطی از تگهای به هم مرتبط را در پس زمینه ساخته و با یک نام واحد (کامپوننت) به HTML DOM ارسال کند. یعنی اگر جایی یک کامپوننت صدا زده شود، تگ یا تگهای مرتبط به آن کامپوننت را به عنوان خروجی خواهیم داشت. همانطور که میشود تگهای مختلف را به صورت تو در تو استفاده کرد، کامپوننتها را هم میشود به همین روش فراخوانی کرد. در مثال زیر روش صدا زدن چند کامپوننت و تگهایی را که ارائه میدهد، داریم.
// Components in a JavaScript file. <clickableImage href="http://google.com" src="google.png" /> <LinksContainer> <LinksList> <clickableImage href="http://yahoo.com" src="yahoo.png" /> </LinksList> </LinksContainer> <!--Output in HTML DOM--> <a href="http://google.com"> <img src="google.png" /> </a> <div> <div> <ul> <li> <a href="http://google.com"> <img src="google.png" /> </a> </li> </ul> </div> </div>
در قسمت کامپوننتها میبینیم که چطور کامپوننتها یکبار به صورت تکی و یک بار به صورت تو در تو اجرا میشوند. خروجی در قسمت Output واضح است که با نام کامپوننتها هماهنگی دارد. با این مثال چند مورد مشخص میشود.
- به هر کامپوننت قبلا گفته شده چه تگهایی را باید ایجاد کند. در نتیجه با هر بار فراخوانی در هر مکان، تگ یا تگهایی که به آن معرفی شده را میسازد.
- هر کامپوننت میتواند مقادیری را به عنوان ورودی دریافت کند و آنها را به تگها در خروجی اعمال کند. در مثال بالا href و src در فراخوانیهای مختلف، مقادیر متفاوتی را به خروجی میفرستند.
- با انتخاب نام مناسب برای کامپوننتها، بدون آنکه بدانیم چطور ساخته شدهاند میتوانیم حدس بزنیم چه تگهایی را خواهند ساخت و این دلیلی است که خوانایی برنامه افزایش میابد.
- دلیل دیگر که باعث خوانایی برنامه میشود، این است که هر یک از این کامپوننتها میتوانند تگهای زیادی را یک جا بسازند که این کار منجر به کم شدن مقدار کد برنامه میشود. برنامه هر چه کم کدتر، با خوانایی بیشتر!
قابلیت استفاده مجدد
در ادامه وقتی با روش ساخت کامپوننتها آشنا شدیم، متوجه میشویم که کامپوننتها چیزی بیشتر از یک تابع نیستند. وقتی نام یک کامپوننت را فراخوانی کنیم در واقع یک تابع را اجرا میکنیم، به آن پارامتر ورودی را میدهیم و از آن خروجی میگیریم. میدانیم که توابع را میشود یکبار ساخت و چندبار استفاده کرد. بخصوص اگر این توابع به متغیرهای سراسری و سایر توابع وابسته نباشند و به صورت مستقل عمل کنند، میشود آنها را به برنامههای دیگر هم انتقال داد.
نحوه ساخت یک کامپوننت در React
در React به سه روش میشود کامپوننتها را ایجاد کرد. در روش اول توضیحات زیاد خواهند بود، اما در دو روش بعدی فقط نکات کلیدی گفته خواهد شد.
Stateless function components
میخواهیم یک منو از نوشیدنیها را با استفاده از کامپوننتها نمایش دهیم. در یک فایل جاوااسکریپت کدهای زیر را وارد کنید. در ادامه هر بخش توضیح داده خواهد شد.
var hotDrinks = [ { item: "Tea", price: "7000" }, { item: "Espresso", price: "10000" }, { item: "Hot Chocolate", price: "12000" } ]; var MenuItem = function (props) { return ( <li className="list-group-item"> <span className="badge">{props.price}</span> <p>{props.item}</p> </li> ) }; var Menu = function (props) { return ( <div className="row"> <div className="col-md-4"> <ul className="list-group"> {props.data.map(item => <MenuItem {...item} />)} </ul> </div> </div> ) }; ReactDOM.render( <Menu data={hotDrinks} />, document.getElementById("reactTestContainer") )
- فرض میکنیم که لیست نوشیدنیها و قیمت آنها را به فرمتی که میبینید از سرور دریافت کردهایم. (hotDrinks)
- شیء MenuItem یک تابع بدون نام را اجرا میکند. از دیدگاه React این تابع یک کامپوننت است. کامپوننت با هر بار فراخوانی مقادیری را برای یک نوشیدنی و قیمت آن، دریافت میکند.کامپوننت به عنوان خروجی یک تگ <li>، پر شده با مقادیر ورودی را بازگشت میدهد.
- شیء Menu یک تابع بدون نام را اجرا میکند. از دید React این تابع یک کامپوننت است. کامپوننت با هر بار فراخوانی، مجموعهای از نوشیدنیها و قیمت آنها را دریافت میکند. متد map به کمک یک Arrow Function آرایهای از کامپوننت MenuItem ایجاد میکند که به ازای هر عضو ایجاد شده، یکبار MenuItem اجرا میشود. هر عضو (item) دارای یک نام نوشیدنی و قیمت آن است. سه نقطه در {…item} برای پر کردن جای خالی نیست! این عبارت یعنی اینکه مقادیر نام و قیمت را به صورت جداگانه (یعنی دو پارامتر مجزا) به کامپوننت MenuItem ارسال میکند. کامپوننت، به عنوان خروجی یک تگ <ul>، پر شده با آرایهای از کامپوننت MenuItem را بازگشت میدهد.
- متد render از شیء ReactDOM وظیفه ساخت تگهای JSX واقع در کامپوننتها را در HTML DOM به عهده دارد. پارامتر اول render، کامپوننت Menu است با ورودی دادههای گرفته شده از سرور. همانطور که شرح داده شد، کامپوننت Menu با فراخوانی و به کمک دادههای ورودی، کامپوننت MenuItem را پیادهسازی خواهد کرد. پارامتر دوم render، محلی است که تگها باید در آن ساخته شوند. مثلا یک تگ <div>
- در هر کدام از کامپوننتها و در قسمت ReactDOM.render میشود از کامپوننتهای دیگر به صورت تو در تو استفاده کرد.
React.createClass
React یک API درونی برای ایجاد کامپوننتها، به نام createClass دارد. این تابع باید یک شیء پیکربندی درون خود داشته باشد که در آن و بین دو آکولاد {} خواص و متدها تعریف میشوند. تابع createClass برای کار حداقل باید یک متد به نام render داشته باشد که در آن تگهای JSX را قرار میدهیم. کامپوننت MenuItem را که به صورت Stateless ساختیم، دوباره با createClass ایجاد میکنیم.
var MenuItem = React.createClass({ render: function () { return ( <li className="list-group-item"> <span className="badge">{this.props.price}</span> <p>{this.props.item}</p> </li> ) } });
برای خواندن مقادیر ورودی در این روش باید از this استفاده کنیم. بر اساس قواعد شیء گراییِ، MenuItem و Menu کلاس هستند و هر بار در ReactDOM.render کامپوننت Menu را به HTML DOM ارسال میکنیم. یک نمونه از این کلاس ساخته میشود و کلاس Menu، نمونههایی از کلاس MenuItem را میسازد. this به نمونهی ساخته شده از یک کلاس اشاره دارد.
React.Component
در روش آخر با استفاده از extend، از کلاس React.Component ارث بری میکنیم و کامپوننت را میسازیم. مفاهیم کلاس و ارث بری در جاوااسکریپ را میشود از اینجا یاد گرفت. مجددا MenuItem را با این روش ایجاد میکنیم.
class MenuItem extends React.Component { render() { return ( <li className="list-group-item"> <span className="badge">{this.props.price}</span> <p>{this.props.item}</p> </li> ); } }
همانطور که میبینید بین دو روش React.Component و React.createClass تفاوتی جز در syntax آنها نیست. در اینجا از سایر امکانات کلاس در جاوااسکریپت مثل سازنده کلاس میشود استفاده کرد. کامپوننتها در React میتوانند کاری بیشتر از ساخت تگها در HTML DOM را انجام دهند. در قسمت بعد به قابلیت مهم حفظ و دنبال کردن تغییرات در وضعیت کامپوننتها میپردازیم.
برپایی تنظیمات اولیهی سیستم مسیریابی در AngularJS 2.0
برای کار با سیستم مسیریابی AngularJS 2.0، ابتدا باید اسکریپتهای آن به صفحه اضافه شوند. در ادامه المان پایهای تعریف شده و سپس باید سرویس پروایدر مسیریابی را رجیستر کرد. جزئیات این موارد را در ادامه بررسی میکنیم:
الف) سرویس مسیریابی، جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن باید به صفحهی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفتهاست:
<!-- Required for routing --> <script src="~/node_modules/angular2/bundles/router.dev.js"></script>
ب) افزودن المان base به ابتدای صفحه:
<!DOCTYPE html> <html> <head> <base href="/">
از آنجائیکه فایل index.html در ریشهی سایت قرار گرفتهاست، مقدار آغازین href آن به / تنظیم شدهاست.
ج) شبیه به حالت ثبت پروایدر HTTP در قسمت قبل، برای ثبت پروایدر مسیریابی نیز به فایل App\app.component.ts مراجعه میکنیم:
//same as before ... import { ROUTER_PROVIDERS } from 'angular2/router'; //same as before ... @Component({ //same as before … providers: [ ProductService, HTTP_PROVIDERS, ROUTER_PROVIDERS ] }) //same as before ...
علت ختم شدن نام این سرویسها به PROVIDERS این است که این تعاریف، امکان استفادهی از چندین سرویس زیر مجموعهی آنها را فراهم میکنند و صرفا یک سرویس نیستند.
ساخت کامپوننت نمایش جزئیات محصولات
در ادامه میخواهیم جزئیات هر محصول را با کلیک بر روی نام آن در لیست محصولات، در آدرسی دیگر به صورتی مجزا مشاهده کنیم. به همین منظور به پوشهی products برنامه مراجعه کرده و دو فایل جدید product-detail.component.ts و product-detail.component.html را ایجاد کنید؛ با این محتوا:
الف) محتوای فایل product-detail.component.html
<div class='panel panel-primary'> <div class='panel-heading'> {{pageTitle}} </div> </div>
import { Component } from 'angular2/core'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; }
اگر دقت کنید، این کامپوننت ویژه دارای خاصیت selector نیست. ذکر selector تنها زمانی اجباری است که بخواهیم این کامپوننت را داخل کامپوننتی دیگر قرار دهیم. در اینجا قصد داریم این کامپوننت را به صورت یک View جدید، توسط سیستم مسیریابی نمایش دهیم و نه به صورت جزئی از یک کامپوننت دیگر.
افزودن تنظیمات مسیریابی به برنامه
مسیریابی در AngularJS 2.0، مبتنی بر کامپوننتها است. به همین جهت، ابتدای کار مسیریابی، مشخص سازی تعدادی از کامپوننتها هستند که قرار است به عنوان مقصد سیستم راهبری (navigation) مورد استفاده قرار گیرند و به ازای هر کدام، یک مسیریابی و Route جدید را تعریف میکنیم. تعریف هر Route جدید شامل انتساب نامی به آن، تعیین مسیر مدنظر و مشخص سازی کامپوننت مرتبط است:
{ path: '/products', name: 'Products', component: ProductListComponent },
این تنظیمات به عنوان یک متادیتای جدید دیگر به کلاس AppComponent، در فایل app.component.ts اضافه میشوند:
//same as before … import { ROUTER_PROVIDERS, RouteConfig } from 'angular2/router'; //same as before … @Component({ //same as before … }) @RouteConfig([ { path: '/welcome', name: 'Welcome', component: WelcomeComponent, useAsDefault: true }, { path: '/products', name: 'Products', component: ProductListComponent } ]) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
همانطور که ملاحظه میکنید، یک کلاس میتواند بیش از یک decorator داشته باشد.
()RouteConfig@ را به کامپوننتی الصاق میکنیم که قصد میزبانی مسیریابی را دارد (Host component). این مزین کننده، آرایهای از اشیاء را قبول میکند و هر شیء آن دارای خواصی مانند مسیر، نام و کامپوننت است. باید دقت داشت که نام هر مسیریابی تعریف شده باید pascal case باشد. در غیراینصورت مسیریاب ممکن است این نام را با path اشتباه کند.
همچنین امکان تعریف خاصیت دیگری به نام useAsDefault نیز در اینجا میسر است. از آن جهت تعریف مسیریابی پیش فرض سیستم، در اولین بار نمایش آن، استفاده میشود.
در اینجا نام کامپوننت، رشتهای ذکر نمیشود و دقیقا اشاره دارد به نام کلاس متناظر. بنابراین هر نام کلاسی که در اینجا اضافه میشود، باید به همراه import ماژول آن نیز در ابتدای فایل جاری باشد. به همین جهت اگر تنظیمات فوق را اضافه کنید، ذیل کلمهی WelcomeComponent یک خط قرمز مبتنی بر عدم تعریف آن کشیده میشود. برای تعریف آن، پوشهی جدیدی را به ریشهی سایت به نام home اضافه کنید و به آن دو فایل ذیل را اضافه نمائید:
الف) محتوای فایل welcome.component.ts
import { Component } from 'angular2/core'; @Component({ templateUrl: 'app/home/welcome.component.html' }) export class WelcomeComponent { public pageTitle: string = "Welcome"; }
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <h3 class="text-center">Default page</h3> </div> </div>
پس از تعریف این کامپوننت، اکنون باید import ماژول آنرا به ابتدای فایل app.component.ts اضافه کنیم، تا مشکل عدم شناسایی نام کلاس WelcomeComponent برطرف شود:
import { WelcomeComponent } from './home/welcome.component';
فعال سازی مسیریابیهای تعریف شده
روشهای مختلفی برای دسترسی به اجزای یک برنامه وجود دارند؛ برای مثال کلیک بر روی یک لینک، دکمه و یا تصویر و سپس فعال سازی مسیریابی متناظر با آن. همچنین کاربر میتواند آدرس صفحهای را مستقیما در نوار آدرسهای مرورگر وارد کند. به علاوه امکان کلیک بر روی دکمههای back و forward مرورگر نیز همواره وجود دارند. تنظیمات مسیریابیهای انجام شده، دو مورد آخر را به صورت خودکار مدیریت میکنند. در اینجا تنها باید مدیریت اولین حالت ذکر شده را با اتصال مسیریابیها به اعمال کاربران، انجام داد.
به همین جهت منویی را به بالای صفحهی برنامه اضافه میکنیم. برای این منظور، فایل app.component.ts را گشوده و خاصیت template کامپوننت AppComponent را به نحو ذیل تغییر میدهیم:
@Component({ //same as before … template: ` <div> <nav class='navbar navbar-default'> <div class='container-fluid'> <a class='navbar-brand'>{{pageTitle}}</a> <ul class='nav navbar-nav'> <li><a [routerLink]="['Welcome']">Home</a></li> <li><a [routerLink]="['Products']">Product List</a></li> </ul> </div> </nav> <div class='container'> <router-outlet></router-outlet> </div> </div> `, //same as before … })
سپس جهت تعریف لینکهای هر آیتم، از یک دایرکتیو توکار AngularJS 2.0 به نام routerLink استفاده میکنیم. هر routerLink به یکی از آیتمهای تنظیم شدهی در RouteConfig بایند میشود. بنابراین نامهایی که در اینجا قید شدهاند، دقیقا نامهایی هستند که در خاصیت name هر کدام از اشیاء تشکیل دهندهی RouteConfig، تعریف و مقدار دهی گردیدهاند.
اکنون اگر کاربر بر روی یکی از لینکهای Home و یا Product List کلیک کند، مسیریابی متناظر با آن فعال میشود (بر اساس این نام، در لیست عناصر RouteConfig جستجویی صورت گرفته و عنصر معادلی بازگشت داده میشود) و سپس View آن کامپوننت نمایش داده خواهد شد.
تا اینجا دایرکتیو جدید routerLink به قالب کامپوننت اضافه شدهاست؛ اما AngularJS 2.0 نمیداند که باید آنرا از کجا دریافت کند. به همین جهت ابتدا import آنرا (ROUTER_DIRECTIVES) به ابتدای ماژول جاری اضافه خواهیم کرد:
import { ROUTER_PROVIDERS, RouteConfig, ROUTER_DIRECTIVES } from 'angular2/router';
directives: [ROUTER_DIRECTIVES],
تا اینجا اگر دقت کرده باشید، کامپوننت نمایش لیست محصولات را از کامپوننت ریشهی سایت حذف کردهایم و بجای آن منوی بالای سایت را نمایش میدهیم که توسط آن میتوان به صفحهی آغازین و یا صفحهی نمایش لیست محصولات، رسید. به همین جهت خاصیت directives دیگر شامل ذکر کلاس کامپوننت لیست محصولات نیست.
در انتهای قالب کامپوننت ریشهی سایت، یک دایرکتیو جدید به نام router-outlet نیز تعریف شدهاست. وقتی یک کامپوننت فعال میشود، نیاز است View مرتبط با آن نیز نمایش داده شود. دایرکتیو router-outlet محل نمایش این View را مشخص میکند.
اکنون اگر برنامه را اجرا کنیم، به این شکل خواهیم رسید:
اگر دقت کنید، آدرس بالای صفحه، در اولین بار نمایش آن به http://localhost:2222/welcome تنظیم شده و این مقدار دهی بر اساس خاصیت useAsDefault تنظیمات مسیریابی سایت انجام شدهاست (نمایش welcome به عنوان صفحهی اصلی و پیش فرض).
همچنین با کلیک بر روی لینک لیست محصولات، کامپوننت آن فعال شده و نمایش داده میشود. محل قرارگیری این کامپوننتها، دقیقا در محل قرارگیری دایرکتیو router-outlet است.
ارسال پارامترها به سیستم مسیریابی
در ابتدا بحث، مقدمات کامپوننت نمایش جزئیات یک محصول انتخابی را تهیه کردیم. برای فعال سازی این کامپوننت و مسیریابی آن، نیاز است بتوان پارامتری را به سیستم مسیریابی ارسال کرد. برای مثال با انتخاب آدرس product/5، جزئیات محصول با ID مساوی 5 نمایش داده شود.
برای این منظور:
الف) اولین قدم، تعریف مسیریابی آن است. به همین جهت به فایل app.component.ts مراجعه و دو تغییر ذیل را به آن اعمال کنید:
//same as before … import { ProductDetailComponent } from './products/product-detail.component'; @Component({ //same as before … }) @RouteConfig([ //same as before … { path: '/product/:id', name: 'ProductDetail', component: ProductDetailComponent } ]) //same as before …
تفاوت این مسیریابی با نمونههای قبلی در تعریف id:/ است. پس از ذکر :/، نام یک متغیر عنوان میشود و اگر نیاز به چندین متغیر بود، همین الگو را تکرار خواهیم کرد.
ب) سپس نحوهی فعال سازی این مسیریابی را توسط تعریف لینکی جدید، معرفی میکنیم. بنابراین فایل قالب product-list.component.html را گشوده و سپس بجای نمایش عنوان محصول:
<td>{{ product.productName }}</td>
<td> <a [routerLink]="['ProductDetail', {id: product.productId}]"> {{product.productName}} </a> </td>
اکنون که از دایرکتیو جدید routerLink در این قالب استفاده شدهاست، نیاز است تعریف دایرکتیو آنرا به متادیتای کلاس کامپوننت لیست محصولات نیز اضافه کنیم تا AngularJS 2.0 بداند آنرا از کجا باید تامین کند:
import { Component, OnInit } from 'angular2/core'; import { ROUTER_DIRECTIVES } from 'angular2/router'; //same as before … @Component({ //same as before … directives: [StarComponent, ROUTER_DIRECTIVES] })
در ادامه اگر برنامه را اجرا کنید، عنوانهای محصولات، به آدرس نمایش جزئیات آنها لینک شدهاند:
ج) در آخر زمانیکه View نمایش جزئیات محصول فعال میشود، نیاز است این id را از url جاری دریافت کند. به همین جهت فایل product-detail.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { RouteParams } from 'angular2/router'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; constructor(private _routeParams: RouteParams) { let id = +this._routeParams.get('id'); this.pageTitle += `: ${id}`; } }
در این حالت، id دریافتی، به متغیر pageTitle اضافه شده و در قالب مربوطه به صورت خودکار نمایش داده میشود.
تا اینجا اگر برنامه را اجرا کنید، صفحهی نمایش جزئیات یک محصول، با کلیک بر روی عناوین آنها به صورت زیر نمایش داده میشود:
افزودن دکمهی back با کدنویسی
اکنون برای بازگشت مجدد به لیست محصولات، میتوان از دکمهی back مرورگر استفاده کرد، اما امکان طراحی این دکمه در قالبها نیز پیش بینی شدهاست.
برای این منظور قالب product-detail.component.html را به نحو ذیل بازنویسی میکنیم:
<div class='panel panel-primary'> <div class='panel-heading'> {{pageTitle}} </div> <div class='panel-footer'> <a class='btn btn-default' (click)='onBack()' style='width:80px'> <i class='glyphicon glyphicon-chevron-left'></i> Back </a> </div> </div>
سپس کدهای product-detail.component.ts را به صورت ذیل تکمیل خواهیم کرد:
import { Component } from 'angular2/core'; import { RouteParams, Router } from 'angular2/router'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; constructor(private _routeParams: RouteParams, private _router: Router) { let id = +this._routeParams.get('id'); this.pageTitle += `: ${id}`; } onBack(): void { this._router.navigate(['Products']); } }
رفع تداخل مسیریابیهای ASP.NET MVC با مسیریابیهای AngularJS 2.0
در طی بحث جاری عنوان شد که اگر کاربر مسیر http://localhost:2222/product/2 را جایی ثبت کرده یا bookmark کند، پس از فراخوانی مستقیم آن در نوار آدرسهای مرورگر، بلافاصله به این آدرس هدایت خواهد شد. این مورد صحیح است اگر از index.html بجای بکارگیری ASP.NET MVC، جهت هاست برنامه استفاده شود. اگر چنین آدرسی را در یک برنامهی ASP.NET MVC فراخوانی کنیم، ابتدا به دنبال کنترلری به نام product میگردد (ابتدا وارد موتور ASP.NET MVC میشود) و چون این کنترلر در سمت سرور تعریف نشدهاست، پیام 404 و یا یافت نشد را مشاهده خواهید کرد و فرصت به اجرای برنامهی AngularJS نخواهد رسید.
برای حل این مشکل نیاز است یک route جدید را به نام catch all، در انتهای مسیریابیهای فعلی اضافه کنید؛ تا سایر درخواستهای رسیده را به صفحهی نمایش برنامهی AngularJS هدایت کند:
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { controller = "Home" } // for catch all to work, Home|About|SomeName ); // Route override to work with Angularjs and HTML5 routing routes.MapRoute( name: "NotFound", url: "{*catchall}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part9.zip
خلاصهی بحث
حین ایجاد کامپوننتها باید به نحوهی نمایش آنها نیز فکر کرد. اگر کامپوننتی قرار است داخل یک کامپوننت دیگر نمایش یابد، باید دارای selector باشد. یک چنین کامپوننتی نیاز به تعریف مسیریابی ندارد. برای کامپوننتهایی که به عنوان یک View مستقل طراحی میشوند و قرار است در یک صفحهی مجزا نمایش داده شوند، نیازی به تعریف selector نیست؛ اما باید برای آنها مسیریابیهای ویژهای را تعریف کرد. همچنین نیاز است مدیریت اعمال کاربران را جهت فعال سازی آنها نیز مدنظر داشت. برای استفاده از امکانات مسیریابی توکار AngularJS 2.0 نیاز است اسکریپت آنرا به صفحهی اصلی اضافه کرد. سپس باید المان base را جهت نحوهی تشکیل آدرسهای مسیریابی، به صفحه اضافه کرد. در ادامه کار ثبت ROUTER_PROVIDERS در بالاترین سطح سلسه مراتب کامپوننتهای سایت انجام میشود. با استفاده از RouteConfig کار تنظیمات ابتدایی مسیریابی صورت خواهد گرفت. این decorator به کامپوننتی که قرار است کار میزبانی مسیریابی را انجام دهد، متصل میشود. پس از تعریف مسیریابیها با ذکر یک نام منحصربفرد، مسیری که باید توسط کاربر وارد شود و نام کامپوننت مدنظر، با استفاده از دایرکتیو routerLink کار تعریف این آدرسها، در رابط کاربری برنامه انجام میشود. این دایرکتیو جدید، جزئی از مجموعهی ROUTER_DIRECTIVES است که باید به لیست دایرکتیوهای کامپوننت ریشههای سایت و هر کامپوننتی که از routeLink استفاده میکند، اضافه شود. برای بایند این دایرکتیو به مسیریابیهای تعریف شده، سمت راست این اتصال باید به آرایهای از مقادیر مقدار دهی شود که اولین عنصر آن، نام یکی از عناصر مسیریابی تعریف شدهی در RouteConfig است. از دومین عنصر آن برای مقدار دهی پارامترهای ارسالی به سیستم مسیریابی استفاده میشود. کار دایرکتیو router-outlet، مشخص سازی محل نمایش یک View است که عموما در قالب میزبان مسیریابی قرار میگیرد. برای تعیین پارامترهای مسیریابی، از الگوی paramName:/ استفاده میشود. برای دسترسی به این مقادیر در یک کامپوننت، میتوان از سرویس RouteParams استفاده کرد. برای فعال سازی یک مسیریابی با کدنویسی، از سرویس Router و متد navigate آن کمک میگیریم.
تا اینجا در قسمت سوم، نحوهی قراردادن یک کامپوننت را در کامپوننتی دیگر، توسط مقدار دهی خاصیت directives مزین کنندهی Component بررسی کردیم. همینقدر که یک کامپوننت دارای selector باشد، قابلیت قرارگرفتن در یک کامپوننت دیگر را دارد. اما چگونه باید بین این کامپوننتها ارتباط برقرار کرد؟
تهیه کامپوننت نمایش ستارهای امتیازهای محصولات
مثال نمایش لیست محصولات سری جاری، دارای ستون «5Star Rating» است. در این قسمت میخواهیم بجای نمایش عددی این امتیازها، کامپوننتی را طراحی کنیم که نماش ستارهای آنها را سبب شود. این کامپوننت باید بتواند یک مقدار ورودی، یا همان عدد امتیاز محصول را از کامپوننت دربرگیرندهی آن دریافت کند. همچنین میخواهیم اگر کاربر بر روی این ستارهها کلیک کرد، کامپوننت در برگیرنده را نیز مطلع سازیم.
در این مثال در فایل product-list.component.html چنین سطری تعریف شدهاست:
<td>{{ product.starRating }}</td>
با توجه به اینکه کامپوننت نمایش ستارهای امتیازها، قابلیت استفادهی مجدد را دارد و الزامی ندارد که حتما در لیست محصولات، بکار گرفته شود، بهتر است محل تعریف آنرا به خارج از پوشهی products فعلی منتقل کنیم. برای مثال میتوان پوشهی app\shared را برای آن و تمامی کامپوننتهای با قابلیت استفادهی مجدد ایجاد کرد.
برای شروع، فایل جدید App\shared\star.component.ts را اضافه کنید؛ با کدهای کامل ذیل:
import { Component, OnChanges, Input, Output, EventEmitter } from 'angular2/core'; @Component({ selector: 'ai-star', templateUrl: 'app/shared/star.component.html', styleUrls: ['app/shared/star.component.css'] }) export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>(); ngOnChanges(): void { this.starWidth = this.rating * 86 / 5; } onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); } }
سپس مسیر template و مسیر فایل css ویژهی آن، در تزئین کنندهی Component مشخص شدهاند. محتوای کامل این دو فایل را در ذیل مشاهده میکنید:
الف) محتوای فایل App\shared\star.component.html
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'> <div style="width: 86px"> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> </div> </div>
.crop { overflow: hidden; } div { cursor: pointer; }
معرفی مقدماتی life cycle hooks در قسمت قبل صورت گرفت. در اینجا چون نیاز است به ازای هر بار رندر شدن این کامپوننت، عرض آن متفاوت باشد، بنابراین نیاز است راهی را پیدا کنیم تا بتوان مقدار خاصیت starWidth را متغیر کرد. به همین منظور از hook مخصوص این تغییرات یا همان OnChanges استفاده میشود. بنابراین باید کلاس این کامپوننت، اینترفیس OnChanges را پیاده سازی کند. پس از آن، importهای لازم جهت تعریف OnChanges به ابتدای فایل اضافه شده و همچنین متد ngOnChanges نیز جهت تکمیل کار پیاده سازی اینترفیس OnChanges، به کلاس جاری اضافه میشود.
کار متد ngOnChanges، تبدیل عدد امتیاز یک محصول، به عرض div نمایش ستارهها است.
مکانیزم کار رخداد ngOnChanges و دریافت اطلاعات از والد
متد ngOnChanges، تنها به خواص ویژهای به نام «input properties» واکنش نشان میدهد. اگر یک کامپوننت تو در توی قرار گرفتهی در یک کامپوننت دیگر، بخواهد اطلاعاتی را از والد خود دریافت کند، باید خاصیتی را در معرض دید آن دربرگیرنده قرار دهد. این کار توسط decorator ویژهای به نام ()Input@ انجام میشود.
به همین جهت است که پیش از خاصیت rating در کلاس StarComponent، شاهد درج مزین کنندهی ویژهی ()Input@ هستیم:
export class StarComponent implements OnChanges { @Input() rating: number;
پس از آن، کامپوننت دربرگیرنده یا والد، این خاصیت ورودی ویژه را از طریق روش property binding متداول، مقدار دهی میکند:
[rating]='product.starRating'
بدیهی است در اینجا چون خاصیت starWidth از نوع ورودی تعریف نشدهاست، قابلیت property binging فوق را در کامپوننت والد، ندارد.
اکنون به ازای هر بار نمایش این کامپوننت فرزند، خاصیت rating ورودی آن مقدار دهی شده و مقدار آن در رخداد ngOnChanges قابل دسترسی و استفاده خواهد بود. اینجا است که میتوان از این مقدار تغییر یافته، جهت ترجمهی آن به عرض div نمایش ستارهها، استفاده کرد.
ارسال دادهها از کامپوننت فرزند به کامپوننت والد
تا اینجا با استفاده از «خواص ورودی» امکان دسترسی به مقادیر ارسالی از طرف والد را در کامپوننت فرزند، پیدا کردیم. عکس آن نیز امکان پذیر است؛ اما توسط رخدادها.
کامپوننت فرزند، با استفاده از decorator ویژهی دیگری به نام ()Output@ امکان ارسال رخدادها را به کامپوننت والد پیدا میکند:
export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
در مثال جاری اگر کاربر بر روی div ستارههای نمایش داده شده کلیک کند، اتصال به آن از طریق event binging متداول انجام میشود (متد جدید onClick به رخداد click متصل شدهاست):
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'>
onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); }
تا اینجا مرحلهی تنظیمات رخدادها در کامپوننت فرزند صورت گرفت. ابتدا خاصیتی از نوع Output تعریف شد. سپس در کدهای قالب این کامپوننت جدید، متد onClick به رخداد click متصل گردید و سپس در کدهای مدیریت کنندهی این متد، متد ratingClicked.emit جهت ارسال اطلاعات نهایی به والد، فراخوانی گردید.
اکنون در کامپوننت والد، باید این مراحل برای دریافت اطلاعات از کامپوننت فرزند خود، طی شوند:
الف) ابتدا نام خاصیت مزین شدهی با Output، به عنوان مقصد event binding مشخص میشود و سپس متدی در کلاس کامپوننت والد، به آن متصل میگردد:
(ratingClicked)='onRatingClicked($event)'
ب) در ادامه، تعریف این متد جدید متصل شده را به کلاس ProductListComponent اضافه میکنیم:
onRatingClicked(message: string): void { this.pageTitle = 'Product List: ' + message; }
به این ترتیب با کلیک بر روی div هر کامپوننت نمایش ستارهای امتیازها، خاصیت pageTitle درج شدهی در صفحه تغییر میکند.
استفاده از کامپوننت نمایش ستارهای امتیازها
نکات کلی افزودن این کامپوننت جدید، تفاوتی با مطالب عنوان شدهی در قسمت سوم، در حین بررسی مراحل افزودن دایرکتیو نمایش لیست محصولات، به کامپوننت ریشهی سایت ندارد و یکی هستند.
برای افزودن و استفاده از این کامپوننت جدید، ابتدا قالب product-list.component.html را گشوده و سپس سطر نمایش عددی امتیاز یک محصول را به نحو ذیل تغییر میدهیم:
<td> <ai-star [rating]='product.starRating' (ratingClicked)='onRatingClicked($event)'> </ai-star> </td>
سپس باید به کلاس کامپوننت لیست محصولات (کامپوننت در برگیرنده) اعلام کرد که این کامپوننت جدید را باید از کجا پیدا کند. برای این منظور فایل product-list.component.ts را گشوده و خاصیت directives این کامپوننت را مقدار دهی میکنیم:
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; import { StarComponent } from '../shared/star.component'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe], directives: [StarComponent] })
نمونهای از اجرای برنامه را در تصویر ذیل مشاهده میکنید:
در اینجا ستون امتیازهای محصولات با کامپوننت نمایش ستارهای این امتیازها جایگزین شدهاست و همچنین با کلیک بر روی یکی از آنها، عنوان panel جاری تغییر کردهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part6.zip
خلاصهی بحث
در اینجا نحوهی طراحی API عمومی یک کامپوننت را بررسی کردیم. تا زمانیکه خواص کلاس یک کامپوننت به نحو متداولی تعریف میشوند، میدان دید آنها محدود است به قالب تعریف شدهی متناظر با آنها. اگر نیاز است خاصیتی خارج از این قالب و به صورت عمومی در کامپوننت دربرگیرندهی دیگری در دسترس قرار گیرد، آنرا با مزین کنندهی ()Input@ مشخص میکنیم و اگر قرار است این کامپوننت فرزند، اطلاعاتی را به کامپوننت والد ارسال کند، اینکار را توسط رخدادها و با تعریف ویژگی ()Output@ و EventEmitter انجام میدهد. نوع آرگومان جنریک EventEmitter، تعیین کنندهی نوع اطلاعاتی است که قرار است به کامپوننت دربرگیرنده ارسال شوند.
پس از تعریف کامپوننت فرزند، برای تعریف آن در کامپوننت والد، از نام selector آن به عنوان یک المان جدید HTML استفاده میشود و سپس با استفاده از property binding، اطلاعات لازم، به خاصیت از نوع ()Input@ کامپوننت فرزند ارسال میگردد. از event binding برای دریافت رخدادها از کامپوننت فرزند استفاده میشود. در اینجا هر رخدادی که توسط مزین کنندهی ()Output@ تعریف شده باشد، میتواند به عنوان مقصد event binding تعریف شود و اگر نیاز است به رخدادهای property binding از والد به فرزند، گوش فرا داد، میتوان اینترفیس OnChanges را در کلاس کامپوننت فرزند پیاده سازی کرد.
بررسی Select For XML
پیشنیاز بحث
در ادامه، از بانک اطلاعاتی معروف northwind برای تهیه کوئریها استفاده خواهیم کرد. بنابراین فرض بر این است که این بانک اطلاعاتی را پیشتر به وهلهی جاری SQL Server خود افزودهاید.
بررسی FOR XML RAW
از نگارش 2005 به بعد، Select for XML علاوه بر خروجی متنی XML، توانایی تولید خروجی از نوع XML را نیز یافته است. در ادامه 4 حالت مختلف خروجی آنرا بررسی خواهیم کرد.
SELECT Customers.CustomerID, Orders.OrderID FROM Customers, Orders WHERE Customers.CustomerID = Orders.CustomerID ORDER BY Customers.CustomerID FOR XML RAW
<row CustomerID="ALFKI" OrderID="10643" /> <row CustomerID="ALFKI" OrderID="10692" />
برای تغییر حالت خروجی آن میتوان از حالت ELEMENTS استفاده کرد:
SELECT Customers.CustomerID, Orders.OrderID FROM Customers, Orders WHERE Customers.CustomerID = Orders.CustomerID ORDER BY Customers.CustomerID FOR XML RAW, ELEMENTS
<row> <CustomerID>ALFKI</CustomerID> <OrderID>10643</OrderID> </row>
حالت پیشرفتهتر FOR XML RAW را در ادامه ملاحظه میکنید:
SELECT Customers.CustomerID, Orders.OrderID FROM Customers, Orders WHERE Customers.CustomerID = Orders.CustomerID ORDER BY Customers.CustomerID FOR XML RAW('Customer'), ELEMENTS XSINIL, ROOT('Customers'), XMLSCHEMA('http://MyCustomers')
<Customers> <Customer xmlns="http://MyCustomers"> <CustomerID>ALFKI</CustomerID> <OrderID>10643</OrderID> </Customer>
ذکر XMLSCHEMA، سبب میشود تا SQL Server به صورت خودکار XML Schema را بر اساس اطلاعات ستونهای رابطهای مورد استفاده تولید کند.
این نکات را برای FOR XML AUTO نیز میتوان بکار برد.
بررسی FOR XML AUTO
حالت دوم بکارگیری Select for XML به همراه عبارت Auto است:
SELECT Customers.CustomerID, Orders.OrderID FROM Customers, Orders WHERE Customers.CustomerID = Orders.CustomerID ORDER BY Customers.CustomerID FOR XML AUTO, ELEMENTS
<Customers> <CustomerID>ALFKI</CustomerID> <Orders> <OrderID>10643</OrderID> </Orders> <Orders> <OrderID>10692</OrderID> </Orders> </Customers>
بررسی For XML Explicit
اگر بخواهیم خروجی را تبدیل به ترکیبی از المانها و ویژگیها کنیم، میتوان از For XML Explicit استفاده کرد:
SELECT 1 AS Tag, NULL AS Parent, Customers.CustomerID AS [Customers!1!CustomerID], NULL AS [Order!2!OrderId] FROM Customers UNION ALL SELECT 2, 1, Customers.CustomerID, Orders.OrderID FROM Customers, Orders WHERE Customers.CustomerID = Orders.CustomerID ORDER BY [Customers!1!CustomerID] FOR XML EXPLICIT
<Customers CustomerID="ALFKI"> <Order OrderId="10643" /> <Order OrderId="10692" /> <Order OrderId="10702" /> <Order OrderId="10835" /> <Order OrderId="10952" /> <Order OrderId="11011" /> </Customers>
به علاوه دو ستون اضافی Tag و Parent نیز باید ذکر شوند. از این دو برای مشخص سازی سلسه مراتب استفاده میشوند.
!1! سبب تولید یک ویژگی در سطح اول میشود و !2! سبب تولید ویژگی دیگری در سطح دوم.
بررسی FOR XML PATH
همانطور که مشاهده میکنید، نوشتن FOR XML EXPLICIT نسبتا طولانی و پیچیدهاست. برای ساده سازی آن از نگارش 2005 به بعد، روش For XML Path معرفی شدهاست:
WITH XMLNAMESPACES('http://somens' AS au) SELECT CustomerID AS [@au:CustomerID], CompanyName AS [Company/Name], ContactName AS [Contact/Name] FROM Customers FOR XML PATH('Customer')
<Customer xmlns:au="http://somens" au:CustomerID="ALFKI"> <Company> <Name>Alfreds Futterkiste</Name> </Company> <Contact> <Name>Maria Anders</Name> </Contact> </Customer>
یک نکته: اگر کوئری FOR XML PATH را اجرا کنید، نام ستون خروجی به صورت خودکار به XML_F5..6B تنظیم میشود. علت اینجا است که در حالت پیش فرض، نوع خروجی این افزونه، استریم است و نه XML. برای تبدیل آن به نوع XML باید یک Type را اضافه کرد:
FOR XML PATH('Customer'), Type
.NET Core -> ASP.NET Core Web Application (.NET Core) -> Select `Empty` Template
در اینجا نقش Solution همانند نگارشهای قبلی ویژوال استودیو است: ظرفی است برای ساماندهی موارد مورد نیاز جهت تشکیل یک برنامهی وب و شامل مواردی است مانند پروژهها، تنظیمات آنها و غیره. بنابراین هنوز در اینجا فایل sln. تشکیل میشود.
نقش فایل global.json
زمانیکه یک پروژهی جدید ASP.NET Core 1.0 را آغاز میکنیم، ساختار پوشههای آن به صورت زیر هستند:
در اینجا هنوز فایل sln. قابل مشاهده است. همچنین در اینجا فایل جدیدی به نام global.json نیز وجود دارد، با این محتوا:
{ "projects": [ "src", "test" ], "sdk": { "version": "1.0.0-preview2-003121" } }
خاصیت projects در اینجا به صورت یک آرایه تعریف شدهاست و بیانگر محل واقع شدن پوشههای اصلی پروژهی جاری هستند. پوشهی src یا source را در تصویر فوق مشاهده میکنید و محلی است که سورسهای برنامه در آن قرار میگیرند. یک پوشهی test نیز در اینجا ذکر شدهاست و اگر در حین ایجاد پروژه، گزینهی ایجاد unit tests را هم انتخاب کرده باشید، این پوشهی مخصوص نیز ایجاد خواهد شد.
نکتهی مهم اینجا است، هرکدی که درون پوشههای ذکر شدهی در اینجا قرار نگیرد، قابلیت build را نخواهد داشت. به عبارتی این نسخهی از ASP.NET پوشهها را قسمتی از پروژه به حساب میآورد. در نگارشهای قبلی ASP.NET، مداخل تعریف فایلهای منتسب به هر پروژه، درون فایلی با پسوند csproj. قرار میگرفتند. معادل این فایل در اینجا اینبار پسوند xproj را دارد و اگر آنرا با یک ادیتور متنی باز کنید، فاقد تعاریف مداخل فایلهای پروژه است.
در این نگارش جدید اگر فایلی را به پوشهی src اضافه کنید یا حذف کنید، بلافاصله در solution explorer ظاهر و یا حذف خواهد شد.
یک آزمایش: به صورت معمول از طریق windows explorer به پوشهی src برنامه وارد شده و فایل پیش فرض Project_Readme.html را حذف کنید. سپس به solution explorer ویژوال استودیو دقت کنید. مشاهده خواهید کرد که این فایل، بلافاصله از آن حذف میشود. در ادامه به recycle bin ویندوز مراجعه کرده و این فایل حذف شده را restore کنید تا مجددا به پوشهی src برنامه اضافه شود. اینبار نیز افزوده شدن خودکار و بلافاصلهی این فایل را میتوان در solution explorer مشاهده کرد.
بنابراین ساختار مدیریت فایلهای این نگارش از ASP.NET در ویژوال استودیو، بسیار شبیه به ساختار مدیریت فایلهای VSCode شدهاست که آن نیز بر اساس پوشهها کار میکند و یک پوشه و تمام محتوای آنرا به صورت پیش فرض به عنوان یک پروژه میشناسد. به همین جهت دیگر فایل csproj ایی در اینجا وجود ندارد و file system همان project system است.
یک نکته: در اینجا مسیرهای مطلق را نیز میتوان ذکر کرد:
"projects": [ "src", "test", "c:\\sources\\Configuration\\src" ],
کامپایل خودکار پروژه در ASP.NET Core 1.0
علاوه بر تشخیص خودکار کم و زیاد شدن فایلهای سیستمی پروژه، بدون نیاز به Add new item کردن آنها در ویژوال استودیو، اگر سورسهای برنامه را نیز تغییر دهید، فایل سورس جدیدی را اضافه کنید و یا فایل سورس موجودی را حذف کنید، کل پروژه به صورت خودکار کامپایل میشود و نیازی نیست اینکار را به صورت دستی انجام دهید.
یک آزمایش: برنامه را از طریق منوی debug و گزینهی start without debugging اجرا کنید. اگر برنامه را در حالت معمول debug->start debugging اجرا کنید، حالت کامپایل خودکار را مشاهده نخواهید کرد. در اینجا (پس از start without debugging) یک چنین خروجی را مشاهده خواهید کرد:
این خروجی حاصل اجرای کدهای درون فایل Startup.cs برنامه است:
app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); });
await context.Response.WriteAsync("Hello DNT!");
این مساله قابلیت استفادهی از ASP.NET Core را در سایر ادیتورهای موجود، مانند VSCode سهولت میبخشد.
نقش فایل project.json
فایل جدید project.json مهمترین فایل تنظیمات یک پروژهی ASP.NET Core است و مهمترین قسمت آن، قسمت وابستگیهای آن است:
"dependencies": { "Microsoft.NETCore.App": { "version": "1.0.0", "type": "platform" }, "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0" },
در نگارشهای قبلی ASP.NET، فایلی XML ایی به نام packages.config حاوی تعاریف مداخل بستههای نیوگت برنامه بود. این فایل در اینجا جزئی از محتوای فایل project.json در قسمت dependencies آن است.
با قسمت وابستگیهای این فایل، به دو طریق میتوان کار کرد:
الف) ویرایش مستقیم این فایل که به همراه intellisense نیز هست. با افزودن مداخل جدید به این فایل و ذخیره کردن آن، بلافاصله کار restore و دریافت و نصب آنها آغاز میشود:
ب) از طریق NuGet package manager
روش دیگر کار با وابستگیها، کلیک راست بر روی گره references و انتخاب گزینهی manage nuget packages است:
برای نمونه جهت نصب ASP.NET MVC 6 این مراحل باید طی شوند:
ابتدا برگهی browse را انتخاب کنید و سپس تیک مربوط به include prerelease را نیز انتخاب نمائید.
البته بستهی اصلی MVC در اینجا Microsoft.AspNetCore.Mvc نام دارد و نه MVC6.
اینبار بستههایی که restore میشوند، در مسیر اشتراکی C:\Users\user_name\.nuget\packages ذخیره خواهند شد.
یک نکتهی مهم:
قرار هست در نگارشهای پس از RTM، فایلهای project.json و xproj را جهت سازگاری با MSBuild، اندکی تغییر دهند (که این تغییرات به صورت خودکار توسط VS.NET انجام میشود). اطلاعات بیشتر
انتخاب فریم ورکهای مختلف در فایل project.json
در قسمت قبل عنوان شد که ASP.NET Core را میتوان هم برفراز NET Core. چندسکویی اجرا کرد و هم NET 4.6. مختص به ویندوز. این انتخابها در قسمت frameworks فایل project.json انجام میشوند:
"frameworks": { "netcoreapp1.0": { "imports": [ "dotnet5.6", "portable-net45+win8" ] } },
در اینجا اگر علاقمند بودید که از دات نت کامل مخصوص ویندوز نیز استفاده کنید، میتوانید آنرا در لیست فریم ورکها اضافه نمائید (در این مثال، دات نت کامل 4.5.2 نیز ذکر شدهاست):
"frameworks": { "netcoreapp1.0": { }, "net452": { } }
- “netcoreapp1.0” برای معرفی و استفادهی از NET Core 1.0. بکار میرود.
- جهت معرفی فریم ورکهای کامل و ویندوزی دات نت، اسامی “net45”, “net451”, “net452”, “net46”, “net461” مجاز هستند.
- “portable-net45+win8” برای معرفی پروفایلهای PCL یا portable class libraries بکار میرود.
- “dotnet5.6”, “dnxcore50” برای معرفی نگارشهای پیش نمایش NET Core.، پیش از ارائهی نگارش RTM استفاده میشوند.
- “netstandard1.2”, “netstandard1.5” کار معرفی برنامههای NET Standard Platform. را انجام میدهند.
بر این مبنا، dotnet5.6 ذکر شدهی در قسمت تنظیمات نگارش RTM، به این معنا است که قادر به استفادهی از بستههای نیوگت و کتابخانههای تولید شدهی با نگارشهای RC نیز خواهید بود (هرچند برنامه از netcoreapp1.0 استفاده میکند).
یک مثال: قسمت فریم ورکهای فایل project.json را به نحو ذیل جهت معرفی دات نت 4.6.1 تغییر دهید:
"frameworks": { "netcoreapp1.0": { "imports": [ "dotnet5.6", "portable-net45+win8" ] }, "net461": { "imports": [ "portable-net45+win8" ], "dependencies": { } } },
در این حالت پس از ذخیرهی فایل و شروع خودکار بازیابی وابستگیها، با پیام خطای Package Microsoft.NETCore.App 1.0.0 is not compatible with net461 متوقف خواهید شد.
برای رفع این مشکل باید وابستگی Microsoft.NETCore.App را حذف کنید، چون با net461 سازگاری ندارد
"dependencies": { //"Microsoft.NETCore.App": { // "version": "1.0.0", // "type": "platform" //},
فریم ورک دوم استفاده شده نیز در اینجا قابل مشاهده است.
فایل project.lock.json چیست؟
ذیل فایل project.json، فایل دیگری به نام project.lock.json نیز وجود دارد. اگر به محتوای آن دقت کنید، این فایل حاوی لیست دقیق بستههای نیوگت مورد استفادهی توسط برنامه است و الزاما با آنچیزی که در فایل project.json قید شده، یکی نیست. از این جهت که در فایل project.json، قید میشود netcoreapp1.0؛ ولی این netcoreapp1.0 دقیقا شامل چه بستههایی است؟ لیست کامل آنها را در این فایل میتوانید مشاهده کنید.
در ابتدای این فایل یک خاصیت locked نیز وجود دارد که مقدار پیش فرض آن false است. اگر به true تنظیم شود، در حین restore وابستگیهای برنامه، تنها از نگارشهای ذکر شدهی در این فایل استفاده میشود. از این جهت که در فایل project.json میتوان شماره نگارشها را با * نیز مشخص کرد؛ مثلا *.1.0.0
پوشهی جدید wwwroot و گره dependencies
یکی از پوشههای جدیدی که در ساختار پروژهی ASP.NET Core معرفی شدهاست، wwwroot نام دارد:
از دیدگاه هاستینگ برنامه، این پوشه، پوشهای است که در معرض دید عموم قرار میگیرد (وب روت). برای مثال فایلهای ایستای اسکریپت، CSS، تصاویر و غیره باید در این پوشه قرار گیرند تا توسط دنیای خارج قابل دسترسی و استفاده شوند. بنابراین سورس کدهای برنامه خارج از این پوشه قرار میگیرند.
گره dependencies که ذیل پوشهی wwwroot قرار گرفتهاست، جهت مدیریت این وابستگیهای سمت کلاینت برنامه است. در اینجا میتوان از NPM و یا Bower برای دریافت و به روز رسانی وابستگیهای اسکریپتی و شیوهنامههای برنامه کمک گرفت (علاوه بر نیوگت که بهتر است صرفا جهت دریافت وابستگیهای دات نتی استفاده شود).
یک مثال: فایل جدیدی را به نام bower.json به پروژهی جاری با این محتوا اضافه کنید:
{ "name": "asp.net", "private": true, "dependencies": { "bootstrap": "3.3.6", "jquery": "2.2.0", "jquery-validation": "1.14.0", "jquery-validation-unobtrusive": "3.2.6" } }
پس از اضافه شدن فایل bower.json، بلافاصله کار restore بستهها از اینترنت شروع میشود:
و یا با کلیک راست بر روی گره dependencies، گزینهی restore packages نیز وجود دارد.
فایلهای نهایی دریافت شده را در پوشهی bower_components خارج از wwwroot میتوانید مشاهده کنید.
در مورد نحوهی توزیع و دسترسی به فایلهای استاتیک یک برنامهی ASP.NET Core 1.0، نکات خاصی وجود دارند که در قسمتهای بعد، بررسی خواهند شد.
یک نکته: اگر خواستید نام پوشهی wwwroot را تغییر دهید، فایل جدیدی را به نام hosting.json با این محتوا به پروژه اضافه کنید:
{ "webroot":"AppWebRoot" }
نقطهی آغازین برنامه کجاست؟
اگر به فایل project.json دقت کنید، چنین تنظیمی در آن موجود است:
"buildOptions": { "emitEntryPoint": true, "preserveCompilationContext": true },
public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .Build(); host.Run(); }
var outputKind = compilerOptions.EmitEntryPoint.GetValueOrDefault() ? OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary;
در فایل Program.cs تنظیماتی را مشاهده میکنید، در مورد راه اندازی Kestler که وب سرور بسیار سریع و چندسکویی کار با برنامههای ASP.NET Core 1.0 است و قسمت مهم دیگر آن به استفادهی از کلاس Startup بر میگردد (()<UseStartup<Startup). این کلاس را در فایل جدید Startup.cs میتوانید ملاحظه کنید که کار تنظیمات آغازین برنامه را انجام میدهد. اگر پیشتر با OWIN، در نگارشهای قبلی ASP.NET کار کرده باشید، قسمتی از این فایل برای شما آشنا است:
public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app) { app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } }
نقش فایل launchsetting.json
محتویات پیش فرض این فایل برای قالب empty پروژههای ASP.NET Core 1.0 به صورت ذیل است:
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:7742/", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Core1RtmEmptyTest": { "commandName": "Project", "launchBrowser": true, "launchUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
پروفایلهایی که در اینجا ذکر شدهاند، در تنظیمات پروژه نیز قابل مشاهده هستند: (کلیک راست بر روی پروژه و مشاهدهی properties آن و یا دوبار کلیک بر روی گره properties)
به علاوه امکان انتخاب این پروفایلها در زمان آغاز برنامه نیز وجود دارند:
نکتهی مهم تمام این موارد به قسمت environment variable قابل مشاهدهی در تصاویر فوق بر میگردد. این متغیر محیطی میتواند سه مقدار Development ، Staging و Production را داشته باشد و بر اساس این متغیر و مقدار آن، میتوان پروفایل جدیدی را تشکیل داد. زمانیکه برنامه بر اساس پروفایل خاصی بارگذاری میشود، اینکه چه متغیر محیطی انتخاب شدهاست، در کلاس Startup قابل استخراج و بررسی بوده و بر این اساس میتوان اقدامات خاصی را انجام داد. برای مثال تنظیمات خاصی را بارگذاری کرد و یا صفحات ویژهای را فعال و غیرفعال کرد (این موارد را در قسمتهای بعدی مرور میکنیم).
همچنین در اینجا به ازای هر پروفایل مختلف میتوان Url آغازین یا launch url و پورت آنرا مجزا درنظر گرفت و یا از وب سرور دیگری استفاده کرد.
طراحی فرم ثبت نام کاربران در سایت با روش model driven
در این قسمت قصد داریم فرم ثبت نام کاربران را به همراه اعتبارسنجیهای پیشرفتهای پیاده سازی کنیم. به همین منظور، ابتدا پوشهی جدید App\users را به مثال سری جاری اضافه کنید و سپس سه فایل user.ts، signup-form.component.ts و signup-form.component.html را به آن اضافه نمائید.
فایل user.ts بیانگر مدل کاربران سایت است؛ با این محتوا:
export interface IUser { id: number; name: string; email: string; password: string; }
قالب فرم یا signup-form.component.html، در حالت ابتدایی آن چنین شکل استانداردی را خواهد داشت و فاقد اعتبارسنجی خاصی است:
<form> <div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" /> </div> <div class="form-group"> <label form="email">Email</label> <input id="email" type="text" class="form-control" /> </div> <div class="form-group"> <label form="password">Password</label> <input id="password" type="password" class="form-control" /> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>
بنابراین ابتدا کلاس کامپوننت این فرم را در فایل signup-form.component.ts به نحو ذیل تکمیل کنید:
import { Component } from '@angular/core'; import { Control, ControlGroup, Validators } from '@angular/common'; @Component({ selector: 'signup-form', templateUrl: 'app/users/signup-form.component.html' }) export class SignupFormComponent { form = new ControlGroup({ name: new Control('', Validators.required), email: new Control('', Validators.required), password: new Control('', Validators.required) }); onSubmit(): void { console.log(this.form.value); } }
<form [ngFormModel]="form" (ngSubmit)="onSubmit()"> <div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" ngControl="name"/> <label class="text-danger" *ngIf="!form.controls['name'].valid"> Username is required. </label> </div> <div class="form-group"> <label form="email">Email</label> <input id="email" type="text" class="form-control" ngControl="email" #email="ngForm"/> <label class="text-danger" *ngIf="email.touched && !email.valid"> Email is required. </label> </div> <div class="form-group"> <label form="password">Password</label> <input id="password" type="password" class="form-control" ngControl="password" #password="ngForm"/> <label class="text-danger" *ngIf="password.touched && !password.valid"> Password is required. </label> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>
تفاوت مهم این فرم و اعتبارسنجیهایش با قسمت قبل، در ایجاد اشیاء Control و ControlGroup به صورت صریح است:
form = new ControlGroup({ name: new Control('', Validators.required), email: new Control('', Validators.required), password: new Control('', Validators.required) });
import { Control, ControlGroup, Validators } from '@angular/common';
یک نکته
اگر محل قرارگیری کلاسی را فراموش کردید، آنرا در مستندات AngularJS 2.0 ذیل قسمت API Review جستجو کنید. نتیجهی جستجو، به همراه نام ماژول کلاسها نیز میباشد.
خاصیت عمومی form که با new ControlGroup تعریف شدهاست، حاوی تعاریف صریح کنترلهای موجود در فرم خواهد بود. در اینجا سازندهی ControlGroup، یک شیء را میپذیرد که کلیدهای آن، همان نام کنترلهای تعریف شدهی در قالب فرم و مقدار هر کدام، یک Control جدید است که پارامتر اول آن یک مقدار پیش فرض و پارامتر دوم، اعتبارسنجی مرتبطی را تعریف میکند (این اعتبارسنجی معرفی شده، یک متد استاتیک در کلاس توکار Validators است).
بنابراین چون سه المان ورودی، در فرم جاری تعریف شدهاند، سه کلید جدید هم نام نیز در پارامتر ورودی ControlGroup ذکر گردیدهاند.
اکنون که خاصیت عمومی form، در کلاس کامپوننت فوق تعریف شد، آنرا در قالب فرم، به ngFormModel بایند میکنیم:
<form [ngFormModel]="form" (ngSubmit)="onSubmit()">
مراحل بعد آن، با مراحلی که در قسمت قبل بررسی کردیم، تفاوتی ندارند:
الف) در اینجا به هر المان موجود، یک ngControl نسبت داده شدهاست تا هر المان را تبدیل به یک کنترل AngularJS2 2.0 کند.
ب) به هر المان، یک متغیر محلی شروع شده با # نسبت داده شدهاست تا با اتصال آن به ngForm بتوان به ngControl تعریف شده دسترسی پیدا کرد.
البته اکنون میتوان از خاصیت form متصل به ngFormModel نیز بجای تعریف این متغیر محلی، به نحو ذیل استفاده کرد:
<label class="text-danger" *ngIf="!form.controls['name'].valid">
د) و در آخر متد onSumbit موجود در کلاس کامپوننت را به رخداد ngSubmit متصل کردهایم. همانطور که ملاحظه میکنید اینبار دیگر پارامتری را به آن ارسال نکردهایم. از این جهت که خاصیت form موجود در سطح کلاس، اطلاعات کاملی را از اشیاء موجود در آن دارد و در متد onSubmit کلاس، به آن دسترسی داریم.
onSubmit(): void { console.log(this.form.value); }
بنابراین تا اینجا تنها تفاوت فرم جدید تعریف شده با قسمت قبل، تعریف صریح ControlGroup و کنترلهای آن در کلاس کامپوننت و اتصال آن به ngFormModel است. به این نوع فرمها، فرمهای model driven هم میگویند.
نمایش فرم افزودن کاربران توسط سیستم Routing
با نحوهی تعریف مسیریابیها در قسمت نهم آشنا شدیم. برای نمایش فرم افزودن کاربران، میتوان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before... import { SignupFormComponent } from './users/signup-form.component'; @Component({ //same as before… template: ` //same as before… <li><a [routerLink]="['AddUser']">Add User</a></li> //same as before… `, //same as before… }) @RouteConfig([ //same as before… { path: '/adduser', name: 'AddUser', component: SignupFormComponent } ]) //same as before...
معرفی کلاس FormBuilder
روش دیگری نیز برای ساخت ControlGroup و کنترلهای آن با استفاده از کلاس و سرویس فرم ساز توکار AngularJS 2.0 وجود دارد:
import { Control, ControlGroup, Validators, FormBuilder } from '@angular/common'; form: ControlGroup; constructor(formBuilder: FormBuilder) { this.form = formBuilder.group({ name: ['', Validators.required], email: ['', Validators.required], password: ['', Validators.required] }); }
پیاده سازی اعتبارسنجی سفارشی
فرض کنید میخواهیم ورود نام کاربرهای دارای فاصله را غیر معتبر اعلام کنیم. برای این منظور فایل جدید usernameValidators.ts را به پوشهی app\users اضافه کنید؛ با این محتوا:
import { Control } from '@angular/common'; export class UsernameValidators { static cannotContainSpace(control: Control) { if (control.value.indexOf(' ') >= 0) { return { cannotContainSpace: true }; } return null; } }
هر متد پیاده سازی کنندهی یک اعتبار سنجی سفارشی در این کلاس، استاتیک تعریف میشود؛ با نام دلخواهی که مدنظر است.
پارامتر ورودی این متدهای استاتیک، یک وهله از شیء کنترل است که توسط آن میتوان برای مثال به خاصیت value آن دسترسی یافت و بر این اساس منطق اعتبارسنجی خود را پیاده سازی نمود. به همین جهت import آن نیز به ابتدای فایل جاری اضافه شدهاست.
خروجی این متد دو حالت دارد:
الف) اگر null باشد، یعنی اعتبارسنجی موفقیت آمیز بودهاست.
ب) اگر اعتبارسنجی با شکست مواجه شود، خروجی این متد یک شیء خواهد بود که کلید آن، نام اعتبارسنجی مدنظر است و مقدار این کلید، هر چیزی میتواند باشد؛ یک true و یا یک شیء دیگر که اطلاعات بیشتری را در مورد این شکست ارائه دهد.
برای مثال اگر اعتبارسنج توکار required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
{ required:true }
{ minlength : { requiredLength : 3, actualLength : 1 } }
پس از پیاده سازی یک اعتبارسنجی سفارشی، برای استفادهی از آن، ابتدا ماژول آنرا به ابتدای ماژول signup-form.component.ts اضافه میکنیم:
import { UsernameValidators } from './usernameValidators';
name: ['', Validators.compose([Validators.required, UsernameValidators.cannotContainSpace])],
و مرحلهی آخر، نمایش یک پیام اعتبارسنجی مناسب و متناظر با متد cannotContainSpace است. برای این منظور فایل signup-form.component.html را گشوده و تغییرات ذیل را اعمال کنید:
<div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" ngControl="name" #name="ngForm" /> <div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.required"> Username is required. </label> <label class="text-danger" *ngIf="name.errors.cannotContainSpace"> Username can't contain space. </label> </div> </div>
نام خاصیت بازگشت داده شدهی در اعتبارسنجی سفارشی، به عنوان یک خاصیت جدید شیء errors قابل استفاده است؛ مانند name.errors.cannotContainSpace.
به عنوان تمرین ماژول جدید emailValidators.ts را افزوده و سپس اعتبارسنجی سفارشی بررسی معتبر بودن ایمیل وارد شده را تعریف کنید:
import {Control} from '@angular/common'; export class EmailValidators { static email(control: Control) { var regEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; var valid = regEx.test(control.value); return valid ? null : { email: true }; } }
یک نکته
اگر نیاز است از regular expressions مانند مثال فوق استفاده شود، میتوان از متد توکار Validators.pattern نیز استفاده کرد و نیازی به تعریف یک متد جداگانه برای آن وجود ندارد؛ مگر اینکه نیاز به بازگشت شیء خطای کاملتری با خواص بیشتری وجود داشته باشد.
اعتبارسنجی async یا اعتبارسنجی از راه دور (remote validation)
یک سری از اعتبارسنجیها را در سمت کلاینت میتوان تکمیل کرد؛ مانند بررسی معتبر بودن فرمت ایمیل وارده. اما تعدادی دیگر، نیاز به اطلاعاتی از سمت سرور دارند. برای مثال آیا نام کاربری در حال ثبت، تکراری است یا خیر؟ این نوع اعتبارسنجیها در ردهی async validation قرار میگیرند.
سازندهی شیء Control در AngularJS 2.0 که در مثالهای بالا نیز مورد استفاده قرار گرفت، پارامتر اختیاری سومی را نیز دارد که یک AsyncValidatorFn را قبول میکند:
control(value: Object, validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn) : Control
nameShouldBeUnique(control: Control) { let name: string = control.value; return new Promise((resolve) => { this._userService.isUserNameUnique(<IUser>{ "name": name }).subscribe( (result: IResult) => { resolve( result.result ? null : { 'nameShouldBeUnique': true } ); }, error => { resolve(null); } ); }); }
this.form = _formBuilder.group({ name: ['', Validators.compose([ Validators.required, UsernameValidators.cannotContainSpace ]), (control: Control) => this.nameShouldBeUnique(control)],
امضای متد nameShouldBeUnique تفاوتی با سایر متدهای اعتبارسنج نداشته و پارامتر ورودی آن، همان کنترل است که توسط آن میتوان به مقدار وارد شدهی توسط کاربر دسترسی یافت. اما تفاوت اصلی آن در اینجا است که این متد باید یک شیء Promise را بازگشت دهد. یک Promise، بیانگر نتیجهی یک عملیات async است. در اینجا دو حالت resolve و error را باید پیاده سازی کرد. در حالت error، یعنی عملیات async صورت گرفته با شکست مواجه شدهاست و در حالت resolve، یعنی عملیات تکمیل شده و اکنون میخواهیم نتیجهی نهایی را بازگشت دهیم. نتیجه نهایی بازگشت داده شدهی در اینجا، همانند سایر validators است و اگر نال باشد، یعنی اعتبارسنجی موفقیت آمیز بوده و اگر یک شیء را بازگشت دهیم، یعنی اعتبارسنجی با شکست مواجه شدهاست.
این Promise، از یک سرویس تعریف شدهی در فایل جدید user.service.ts استفاده میکند:
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import { IUser } from './user'; import { IResult } from './result'; @Injectable() export class UserService { private _checkUserUrl = '/home/checkUser'; constructor(private _http: Http) { } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } isUserNameUnique(user: IUser): Observable<IResult> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._checkUserUrl, JSON.stringify(user), options) .map((response: Response) => <IResult>response.json()) .do(data => console.log("User: " + JSON.stringify(data))) .catch(this.handleError); } }
export interface IResult { result: boolean; }
کدهای سمت سرور برنامه که کار بررسی یکتا بودن نام کاربری را انجام میدهند، به صورت ذیل در فایل Controllers\HomeController.cs تعریف شدهاند:
namespace MVC5Angular2.Controllers { public class HomeController : Controller { [HttpPost] public ActionResult CheckUser(User user) { var isUnique = new { result = true }; if (user.Name?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false) { isUnique = new { result = false }; } return new ContentResult { Content = JsonConvert.SerializeObject(isUnique, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
بنابراین تا اینجا مسیر سمت سرور home/checkuser تکمیل شدهاست. این مسیر توسط سرویس کاربر صدا زده شده و اگر نام کاربری وارد شده موجود باشد، نتیجهای را مطابق امضای قرارداد IResult سفارشی ما بازگشت میدهد.
پس از آن مجددا به فایل signup-form.component.ts مراجعه کرده و سرویس جدید UserService را به سازندهی آن تزریق کردهایم. همچنین قسمت providers این کامپوننت را هم جهت تکمیل اطلاعات تزریق کنندهی توکار AngularJS 2.0 مقدار دهی کردهایم. البته همانطور که در مبحث تزریق وابستگیها نیز عنوان شد، اگر این سرویس قرار نیست در کلاس دیگری استفاده شود، نیازی نیست تا آنرا در بالاترین سطح ممکن و در فایل app.component.ts ثبت و معرفی کرد:
@Component({ selector: 'signup-form', templateUrl: 'app/users/signup-form.component.html', providers: [ UserService ] }) export class SignupFormComponent { constructor(private _formBuilder: FormBuilder, private _userService: UserService) {
اکنون که کدهای فعال سازی اعتبارسنجی از راه دور ما تکمیل شدهاست، به فایل signup-form.component.html مراجعه کرده و پیام مناسبی را نمایش خواهیم داد:
<div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.required"> Username is required. </label> <label class="text-danger" *ngIf="name.errors.cannotContainSpace"> Username can't contain space. </label> <label class="text-danger" *ngIf="name.errors.nameShouldBeUnique"> This username is already taken. </label> </div>
نمایش پیام loading در حین انجام اعتبارسنجی از راه دور
شاید بد نباشد که در حین انجام عملیات اعتبارسنجی از راه دور و ارسال درخواستی به سرور و بازگشت نتیجهی آن، یک پیام loading را نیز نمایش داد. برای انجام اینکار نیاز است تغییرات ذیل را به فایل signup-form.component.html اضافه کنیم:
<input id="name" type="text" class="form-control" ngControl="name" #name="ngForm" /> <div *ngIf="name.control.pending"> Checking server, Please wait ... </div>
اعتبارسنجی ترکیبی در حین submit یک فرم
فرض کنید میخواهید منطقی را که حاصل اعتبارسنجی تمام فیلدهای فرم است (و نه هر کدام به تنهایی)، در حین submit آن اعمال کنید. برای مثال آیا ترکیب نام کاربری و کلمهی عبور شخصی در حین login معتبر است یا خیر؟ در این حالت پس از بررسیهای لازم در متد onSubmit، میتوان با استفاده از متد find شیء form، به یکی از کنترلهای فرم دسترسی یافت و سپس با استفاده از متد setErrors، خطای اعتبارسنجی سفارشی را به آن اضافه کرد:
onSubmit(): void { console.log(this.form.value); this.form.find('name').setErrors({ invalidData : true }); }
<div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.invalidData"> Check the inputs.... </label> </div>
اتصال المانهای فرم به مدلی جهت ارسال به سرور
اکنون که دسترسی به خاصیت this.form را داریم و این خاصیت توسط [ngFormModel] به تمام اشیاء تعریف شدهی در فرم و تغییرات آنها دسترسی دارد، میتوان از آن برای دسترسی به شیءایی که حاوی مدل فرم است، استفاده کرد. برای نمونه در مثال فوق، خاصیت value آن، چنین خروجی را دارد:
{ name="VahidN", email="email@site.com", password="123"}
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import { IUser } from './user'; import { IResult } from './result'; @Injectable() export class UserService { private _addUserUrl = '/home/addUser'; constructor(private _http: Http) { } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } addUser(user: IUser): Observable<IUser> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._addUserUrl, JSON.stringify(user), options) .map((response: Response) => <IUser>response.json()) .do(data => console.log("User: " + JSON.stringify(data))) .catch(this.handleError); } }
[HttpPost] public ActionResult AddUser(User user) { user.Id = 1; //todo: save user and get id from db return new ContentResult { Content = JsonConvert.SerializeObject(user, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; }
onSubmit(): void { console.log(this.form.value); /*this.form.find('name').setErrors({ invalidData : true });*/ this._userService.addUser(<IUser>this.form.value) .subscribe((user: IUser) => { console.log(`ID: ${user.id}`); }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: (این کدها مطابق نگارش RC 1 هستند)
MVC5Angular2.part11.zip
خلاصهی بحث
برای اینکه بتوان کنترل بیشتری را بر روی المانهای فرم داشت، ابتدا سرویس FormBuilder را در سازندهی کلاس کامپوننت فرم تزریق میکنیم. سپس با استفاده از متد group آن، المانهای فرم را به صورت کلیدهای شیء پارامتر آن تعریف میکنیم. در اینجا میتوان اعتبارسنجیهای توکار AngularJS 2.0 را که در کلاس پایهی Validators مانند Validators.required وجود دارند، تعریف کرد. با استفاده از متد compose آنها را ترکیب نمود و یا پارامتر سومی را جهت اعتبارسنجیهای async اضافه نمود. در این حالت شیء form تعریف شده به صورت [ngFormModel] به قالب فرم متصل میشود و از تغییرات آن آگاه خواهد شد.
ویژگی ها
- امکان ایجاد progress bar و همچنین قابلیت لغو آپلود.
- Drag & Drop
- امکان paste کردن از Clipboard و قابلیت drag & drop از browser
- امکان resize کردن تصویر قبل از آپلود شدن
- امکان تغییر دادن orientation تصویر پیش از ارسال
- قابلیت resume و pause/play در آپلود
- امکان فیلتر کردن type، size و طول و عرض تصاویر
- قابلیت تبدیل به Flash shim در صورت عدم پشتیبانی html5 توسط مرورگر
نصب و پیاده سازی
bower install ng-file-upload-shim --save bower install ng-file-upload --save
<input type="file" ngf-select ng-model="picFile" name="file" accept="image/*" ngf-max-size="2MB" required>
<div ngf-drop ng-model="picFile" ngf-pattern="image/*"> ... </div>
<i ng-show="myForm.file.$error.required">*required</i><br> <i ng-show="myForm.file.$error.maxSize"> File too large: max is 2M </i>
app.controller('MyCtrl', ['$scope', 'Upload', '$timeout', function ($scope, Upload, $timeout) { // the method uploadPic called from Angular View $scope.uploadPic = function(file) { file.upload = Upload.upload({ url: 'https://angular-file-upload-cors-srv.appspot.com/upload', data: {file: file, otherData: $scope.otherData}, }); file.upload.then(function (response) { // waiting for response $timeout(function () { file.result = response.data; }); }, function (response) { //error if (response.status > 0) $scope.errorMsg = response.status + ': ' + response.data; }, function (evt) { //progress file.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total)); console.log(file.progress); }); } }]);