ModelBinder سفارشی در ASP.NET MVC
روش کار با GitHub NuGet Registry
اگر پیشتر سابقهی کار کردن با ASP.NET MVC را ندارید، نیاز است «15 مورد» ابتدایی مطالب ASP.NET MVC سایت را پیش از ادامهی این سری مطالعه کنید؛ از این جهت که این سری از مطالب «ارتقاء» نام دارند و نه «بازنویسی مجدد». دراینجا بیشتر تفاوتها و روشهای تبدیل کدهای قدیمی، به جدید را بررسی خواهیم کرد؛ تا اینکه بخواهیم تمام مطالبی را که وجود دارند از صفر بازنویسی کنیم.
فعال سازی ASP.NET MVC
تا اینجا خروجی برنامه را صرفا توسط میان افزار app.Run نمایش دادیم. اما در نهایت میخواهیم یک برنامهی ASP.NET MVC را برفراز ASP.NET Core 1.0 اجرا کنیم و این قابلیت نیز به صورت پیش فرض غیرفعال است. برای فعال سازی آن نیاز است ابتدا بستهی نیوگت آنرا نصب کرد. سپس سرویسهای مرتبط با آنرا ثبت و معرفی نمود و در آخر میان افزار خاص آنرا فعال کرد.
نصب وابستگیهای ASP.NET MVC
برای این منظور بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.Mvc را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.AspNetCore.Mvc": "1.0.0" },
تنظیم سرویسها و میان افزارهای ASP.NET MVC
پس از نصب بستهی نیوگت ASP.NET MVC، دو تنظیم ذیل در فایل آغازین برنامه، برای شروع به کار با ASP.NET MVC کفایت میکنند:
الف) ثبت یکجای سرویسهای ASP.NET MVC
public void ConfigureServices(IServiceCollection services) { services.AddMvc();
ب) معرفی میان افزار ASP.NET MVC
public void Configure(IApplicationBuilder app) { app.UseFileServer(); app.UseMvcWithDefaultRoute();
در اینجا دو متد UseMvc و UseMvcWithDefaultRoute را داریم. اولی، امکان تعریف مسیریابیهای سفارشی را میسر میکند و دومی به همراه یک مسیریابی پیش فرض است.
افزودن اولین کنترلر برنامه و معرفی POCO Controllers
در ویژوال استودیو بر روی نام پروژه کلیک راست کرده و پوشهی جدیدی را به نام کنترلر اضافه کنید (تصویر فوق). سپس به این پوشه کلاس جدید HomeController را با این محتوا اضافه کنید:
namespace Core1RtmEmptyTest.Controllers { public class HomeController { public string Index() { return "Running a POCO controller!"; } } }
سپس برنامه را اجرا کنید. این خروجی باید قابل مشاهده باشد:
اگر با نگارشهای قبلی ASP.NET MVC کار کرده باشید، تفاوت این کنترلر با آنها، در عدم ارث بری آن از کلاس پایهی Controller است. به همین جهت به آن POCO Controller نیز میگویند (plain old C#/CLR object).
در ASP.NET Core، همینقدر که یک کلاس public غیر abstract را که نامش به Controller ختم شود، داشته باشید و این کلاس در اسمبلی باشد که ارجاعی را به وابستگیهای ASP.NET MVC داشته باشد، به عنوان یک کنترلر معتبر شناخته شده و مورد استفاده قرار خواهد گرفت. در نگارشهای قبلی، شرط ارث بری از کلاس پایه Controller نیز الزامی بود؛ اما در اینجا خیر. هدف از آن نیز کاهش سربارهای وهله سازی یک کنترلر است. اگر صرفا میخواهید یک شیء را به صورت JSON بازگشت دهید، شاید وهله سازی یک کلاس ساده، بسیار بسیار سریعتر از نمونه سازی یک کلاس مشتق شدهی از Controller، به همراه تمام وابستگیهای آن باشد.
البته هنوز هم مانند قبل، کنترلرهای مشتق شدهی از کلاس پایهی Controller قابل تعریف هستند:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class AboutController : Controller { public IActionResult Index() { return Content("Hello from DNT!"); } } }
تفاوت دیگری را که ملاحظه میکنید، خروجی IActionResult بجای ActionResult نگارشهای قبلی است. در اینجا هنوز هم ActionResult را میتوان بکار برد و اینبار ActionResult، پیاده سازی پیش فرض اینترفیس IActionResult است.
و اگر بخواهیم در POCO Controllers شبیه به return Content فوق را پیاده سازی کنیم، نیاز است تا تمام جزئیات را از ابتدا پیاده سازی کنیم (چون کلاس پایه و ساده ساز Controller در اختیار ما نیست):
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class HomeController { [ActionContext] public ActionContext ActionContext { get; set; } public HttpContext HttpContext => ActionContext.HttpContext; public string Index() { return "Running a POCO controller!"; } public IActionResult About() { return new ContentResult { Content = "Hello from DNT!", ContentType = "text/plain; charset=utf-8" }; } } }
به علاوه در اینجا نحوهی دسترسی به HttpContext را هم مشاهده میکنید. ویژگی ActionContext سبب تزریق اطلاعات آن به کنترلر جاری شده و سپس از طریق آن میتوان به HttpContext و تمام قابلیتهای آن دسترسی یافت.
اینجا است که میتوان میزان سبکی و سریعتر بودن POCO Controllers را احساس کرد. شاید در کنترلری نیاز به این وابستگیها نداشته باشید. اما زمانیکه کنترلری از کلاس پایهی Controller مشتق میشود، تمام این وابستگیها را به صورت پیش فرض و حتی در صورت عدم استفاده، در اختیار خواهد داشت و این در اختیار داشتن یعنی وهله سازی شدن تمام وابستگیهای مرتبط با شیء پایهی Controller. به همین جهت است که POCO Controllers بسیار سبکتر و سریعتر از کنترلرهای متداول مشتق شدهی از کلاس پایهی Controller عمل میکنند.
هم چنین در قسمت Add folders and core references تیک گزینهی Web Api را نیز فعال مینماییم.
حال احتیاج به نصب پکیج OData با استفاده از nuget package manager داریم. کافیست دستور زیر را در package manager console وارد نماییم.
Install-Package Microsoft.AspNet.Odata
این دستور آخرین ورژن Odata package را از nuget دانلود مینماید.
بعد از نصب شدن OData نیاز به اضافه کردن یک Model داریم. کلاسی را به نام Product در پوشهی Models میسازیم.
کلاس Product.cs حاوی فیلدهای زیر است.
namespace ProductService.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Category { get; set; } } }
پراپرتی Id، کلید این entity است و کلاینت میتواند کوئری را بر روی entity، به وسیلهی key بزند. برای مثال برای گرفتن Product با Id برابر 2، باید این url را ارسال نمود "(2)Products/"
پرواضح است که Id در Database به عنوان Primary key در نظر گرفته شده است.
حال احتیاج به نصب Entity Framework داریم که با ارسال دستور زیر از طریق nuget نصب خواهد شد
Install-Package EntityFramework
بعد از نصب کردن ef نیاز به اضافه کردن connection string در web config داریم.
<connectionStrings> <add name="ProductsContext" connectionString="Data Source=.; Initial Catalog=ProductsContext; Integrated Security=True;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" /> </connectionStrings>
الان میتوانیم کلاس ProductsContext را درون پوشهی Models ایجاد نماییم. محتویات آن را به صورت زیر وارد مینماییم
using System.Data.Entity; namespace ProductService.Models { public class ProductsContext : DbContext { public ProductsContext() : base("name=ProductsContext") { } public DbSet<Product> Products { get; set; } } }
درون Constructor کلاس ProductsContext، داریم name=ProductsContext که باید برابر name درون connection string باشد.
حال نیاز به کانفیگ OData داریم. درون پوشهی App_Start و کلاس WebApiConfig.cs محتویات زیر را جایگزین متد register نمایید:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { ODataModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Product>("Products"); config.MapODataServiceRoute( routeName: "ODataRoute", routePrefix: null, model: builder.GetEdmModel()); } }
این کد دو فرآیند زیر را انجام میدهد
1) ساخت Entity Data Model (EDM)
2) اضافه کردن route
EDM یک مدل انتزاعی از data است. EDM برای تولید سند metadata استفاده میشود. کلاس ODataModelBuilder برای ساخت EDM با استفاده از default naming convention میباشد که باعث کاهش کدها میشود. ضمنا کلاس MapODataServiceRoute برای ساخت OData v4 route میباشد. همانگونه که اطلاع دارید، تعریف route برای مدیریت کردن WebApi و چگونگی مسیریابی درخواستهای http میباشد.
اگر application شما احتیاج به چند OData endpoint داشته باشد، میتوانید برای هر کدام routeهای جدا و همچنین نام یکتایی را برای routeName و routePrefix آن در نظر بگیرید.
اضافه کردن OData Controller
یک Controller، کلاسی برای مدیریت کردن درخواستهای http میباشد. شما باید Controllerهای مجزایی را برای هر entity set در OData service خود بسازید. در این مقاله Controller مربوط به موجودیت Product را میسازیم.
در Solution Explorer با کلیک راست بر روی پوشهی Controller، کلاسی به نام ProducsController را میسازیم. دقت کنید نام آن حتما باید به Controller ختم شود.
در OData V3 میتوانیم Controller را با استفاده از Scaffolding بسازیم؛ ولی در V4 این ویژگی وجود ندارد!
محتویات زیر را در این کنترلر اضافه مینماییم:
using ProductService.Models; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Web.Http; using System.Web.OData; namespace ProductService.Controllers { public class ProductsController : ODataController { ProductsContext db = new ProductsContext(); private bool ProductExists(int key) { return db.Products.Any(p => p.Id == key); } protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); } } }
این مرحلهی ابتدایی از پیاده سازی کنترلر میباشد و در قسمت بعد به پیاده سازی CRUD مربوط به آن میپردازیم.
Querying The Entity Set
این 2 متد را به کنترلر خود اضافه مینماییم
[EnableQuery] public IQueryable<Product> Get() { return db.Products; } [EnableQuery] public SingleResult<Product> Get([FromODataUri] int key) { IQueryable<Product> result = db.Products.Where(p => p.Id == key); return SingleResult.Create(result); }
ویژگی EnableQuery به معنای امکان Query زدن از سمت کلاینت به آن میباشد. FromODataUri نیز برای امکان پاس دادن پارامتر از طریق Uri است.
متد Get بدون پارامتر، قادر به برگرداندن تمامی Productها میباشد و متد Get با پارامتر، قادر به برگرداندن آن Product خاص با استفاده از unique Id است.
در صورت داشتن EnableQuery با استفاده از Query Option هایی مثل filter$ و sort$ و غیره از سمت کلاینت قادر به تغییر دادن کوئریهای خود هستیم.
Adding and Entity to Entity Set
برای اجازه دادن به کلاینت، جهت اضافه کردن یک Product به دیتابیس، متد Post زیر را اضافه مینماییم
public async Task<IHttpActionResult> Post(Product product) { if (!ModelState.IsValid) { return BadRequest(ModelState); } db.Products.Add(product); await db.SaveChangesAsync(); return Created(product); }
Updation an Entity
OData از دو روش متفاوت برای Update کردن یک موجودیت استفاده مینماید.
1) Patch : امکان partial update برای موجودیت مربوطه را فراهم میسازد.
2) Put : موجودیت جدید را به صورت کامل جایگزین مینماید.
مشکل روش Put این است که کلاینت مجبور به ارسال تمامی فیلدهای مربوطه میباشد. حتی آن هایی که اساسا تغییری نکردهاند. بنابراین روش Patch ترجیح داده میشود.
در هر صورت ما به پیاده سازی هر دو روش میپردازیم:
public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var entity = await db.Products.FindAsync(key); if (entity == null) { return NotFound(); } product.Patch(entity); try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProductExists(key)) { return NotFound(); } else { throw; } } return Updated(entity); } public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (key != update.Id) { return BadRequest(); } db.Entry(update).State = EntityState.Modified; try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProductExists(key)) { return NotFound(); } else { throw; } } return Updated(update); }
در قسمت Patch کنترلر از <Delta<T استفاده میکند که typeی است برای track کردن تغییرات در مدل مربوطه.
Deleting an Entity
برای حذف هر موجودیت نیز کافیست متد زیر را به کنترلر خود اضافه نمایید:
public async Task<IHttpActionResult> Delete([FromODataUri] int key) { var product = await db.Products.FindAsync(key); if (product == null) { return NotFound(); } db.Products.Remove(product); await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); }
من چند رکورد تستی را به صورت زیر وارد کردهام:
حال پروژهی خود را run نموده و آدرس زیر را وارد نمایید:
http://localhost:YourPort/Products
پاسخ، مجموعهای از entityهای زیر خواهد بود:
{ "@odata.context":"http://localhost:4516/$metadata#Products","value":[ { "Id":1,"Name":"Ali","Price":2.00,"Category":"aaa" },{ "Id":2,"Name":"Reza","Price":1.00,"Category":"bbb" },{ "Id":3,"Name":"Ahmad","Price":0.00,"Category":"ccc" } ] }
شما میتوانید از هر کدام از فیلترهای زیر برای کوئری زدن از کلاینت به سمت سرور استفاده نمایید. بطور مثال هر کدام از اینها پاسخ متفاوت و مربوط به خود را برگشت میدهد:
/Products(2)
Productی با آی دی 2 را بر میگرداند.
/Products?$filter=Id gt 1
محصولی را با آی دی بزرگتر از 1، بر میگرداند.
Products?$select=Name
روی محصولات select زده و فقط فیلد Name آنها را بر میگرداند.
Products?$select=Name,Price
آرایهای از objectهایی با پراپرتی Name و Price را بر میگرداند.
/Products?$top=3
فقط 3 رکورد اول را بر میگرداند.
همانطور که ملاحظه میفرمایید، استفاده از OData باعث کمتر شدن کدهای سمت سرور و همچنین امکان کوئری زدن از سمت کلاینت به سمت سرور را مهیا میکند.
بعد از خواندن این مقاله ممکن است به این مساله فکر کنید که این کار باعث کاهش امنیت میشود. باید عرض کنم که امکانات زیادی برای محدود کردن کوئریها، فراهم شده است و هیچ نگرانی از این بابت وجود ندارد. بطور مثال میتوانید تعیین کنید که از entity مربوطه فقط حداکثر 3 پراپرتی قابلیت کوئری زدن را دارند؛ یا اینکه حداکثر در هر کوئری، 10 رکورد قابلیت پاسخ دادن خواهد داشت.
پس بدین صورت میباشد که شما حداکثر امکانات ممکن را به سمت کلاینت میدهید و اختیار بدان واگذار شده که آیا از این امکانات حداکثری، استفاده نماید یا خیر.
امکانات این پروتکل منحصر به فرد است و در مقالههای بعدی به جزئیات بیشتر و دقیقتری خواهیم پرداخت.
Install-Package Notificatio.TelegramClient
var api = NotificatioApi.Initialize(" Your Hash_Key"); api.SendMessage("Phone Number", "this is a test Message");
$.ajax({ url: "http://www.api.notificatio.me/v1/user/message", type: "POST", dataType: "json", crossDomaint: true, data: { phoneNumber: "your_phone_number", apiHash: "your_api_hash", message: "your_message" }, cache: false, success: function() { // Your code to handle success message sent }, error: function(error) { // Your code to handle error } });
تنظیم مسیریابیها جهت درج پارامترها
پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابیها مشخص کرد.
در ادامهی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه میکنیم:
>ng g c product/ProductDetail >ng g c product/ProductEdit
در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آنرا به شکل ذیل تکمیل خواهیم کرد:
import { ProductEditComponent } from './product-edit/product-edit.component'; import { ProductDetailComponent } from './product-detail/product-detail.component'; import { ProductListComponent } from './product-list/product-list.component'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent }, { path: 'products/:id/edit', component: ProductEditComponent } ];
دومین شیء مسیریابی، از مسیر ریشهی یکسانی استفاده میکند (products) که علت آنرا در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص میکند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری میخواهیم id محصولات را انتقال دهیم، Id نامگرفتهاست؛ وگرنه هر نام دیگری نیز میتواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشهی یکسانی استفاده میکند و تفاوت آنرا با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کردهایم.
در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نامهای پس از : در یک مسیریابی، باید منحصربفرد باشند.
تکمیل کامپوننت نمایش لیست محصولات
پیش از ادامهی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمتهای قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینکهایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه میکنیم:
> ng g i product/IProduct
export interface IProduct { id: number; productName: string; productCode: string; }
تشکیل یک منبع اطلاعات درون حافظهای
یکی از بستههای Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائهی مطلب).
به همین جهت ابتدا بستهی مرتبط با آنرا نصب کنید:
>npm install angular-in-memory-web-api --save
"dependencies": { "angular-in-memory-web-api": "^0.3.1" },
سپس کلاس ProductData را به ماژول محصولات اضافه میکنیم:
> ng g cl product/ProductData
import { IProduct } from './iproduct'; import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api'; export class ProductData implements InMemoryDbService, InMemoryBackendConfig { createDb() { let products: IProduct[] = [ { 'id': 1, 'productName': 'Product 1', 'productCode': '0011' }, { 'id': 2, 'productName': 'Product 2', 'productCode': '0023' }, { 'id': 5, 'productName': 'Product 5', 'productCode': '0048' }, { 'id': 8, 'productName': 'Product 8', 'productCode': '0022' }, { 'id': 10, 'productName': 'Product 10', 'productCode': '0042' } ]; return { products }; } }
در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
import { ProductData } from './product/product-data'; import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), ProductModule, UserModule, AppRoutingModule ],
- در اینجا یک delay را هم در تنظیمات آن مشاهده میکنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.
تعریف سرویس HTTP کار با منبع اطلاعات درون حافظهای
پس از تعریف REST Web API درون حافظهای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
>ng g s product/product -m product/product.module
installing service create src\app\product\product.service.spec.ts create src\app\product\product.service.ts update src\app\product\product.module.ts
پس از ایجاد قالب ابتدایی فایل product.service.ts، آنرا به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/of'; import { IProduct } from './iproduct'; @Injectable() export class ProductService { private baseUrl = 'api/products'; constructor(private http: Http) { } getProducts(): Observable<IProduct[]> { return this.http.get(this.baseUrl) .map(this.extractData) .do(data => console.log('getProducts: ' + JSON.stringify(data))) .catch(this.handleError); } getProduct(id: number): Observable<IProduct> { if (id === 0) { return Observable.of(this.initializeProduct()); }; const url = `${this.baseUrl}/${id}`; return this.http.get(url) .map(this.extractData) .do(data => console.log('getProduct: ' + JSON.stringify(data))) .catch(this.handleError); } deleteProduct(id: number): Observable<Response> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); const url = `${this.baseUrl}/${id}`; return this.http.delete(url, options) .do(data => console.log('deleteProduct: ' + JSON.stringify(data))) .catch(this.handleError); } saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); } private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { product.id = undefined; return this.http.post(this.baseUrl, product, options) .map(this.extractData) .do(data => console.log('createProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { const url = `${this.baseUrl}/${product.id}`; return this.http.put(url, product, options) .map(() => product) .do(data => console.log('updateProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private extractData(response: Response) { let body = response.json(); return body.data || {}; } private handleError(error: Response): Observable<any> { console.error(error); return Observable.throw(error.json().error || 'Server error'); } initializeProduct(): IProduct { // Return an initialized object return { id: 0, productName: null, productCode: null }; } }
private baseUrl = 'api/products';
نمایش لیست محصولات
اکنون پس از این مقدمات میتوانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آنرا به نحو ذیل تکمیل کنید:
import { ActivatedRoute } from '@angular/router'; import { Component, OnInit } from '@angular/core'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { pageTitle = 'Product List'; errorMessage: string; products: IProduct[]; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { this.productService.getProducts() .subscribe(products => this.products = products, error => this.errorMessage = <any>error); } }
<div class="panel panel-default"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> <div class="table-responsive"> <table class="table" *ngIf="products && products.length"> <thead> <tr> <th>Product</th> <th>Code</th> </tr> </thead> <tbody> <tr *ngFor="let product of products"> <td><a [routerLink]="['/products', product.id]"> {{product.productName}} </a> </td> <td>{{ product.productCode}}</td> <td> <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </td> </tr> </tbody> </table> </div> </div> </div>
توضیحات:
پس از تعریف مسیریابیهای صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آنها به لینکهایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحهی مشاهدهی جزئیات آن متصل کردهایم:
<a [routerLink]="['/products', product.id]"> {{product.productName}} </a>
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کردهایم.
<a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a>
به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه میکنیم:
<li> <a [routerLink]="['/products', 0, 'edit']">Add Product</a> </li>
saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); }
همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز میتوان بکار برد. برای مثال:
this.router.navigate(['products', this.product.id]);
خواندن و پردازش پارامترهای مسیریابی
تا اینجا لیست محصولات را نمایش دادیم و همچنین لینکهایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحلهی بعد، پیاده سازی این کامپوننتها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننتها قرار میدهد. بنابراین برای دسترسی به آنها تنها کافی است این سرویس را به سازندهی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم میشود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id میتوان به صورت ذیل عمل کرد:
let id = +this.route.snapshot.params['id'];
بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف میشود:
{ path: 'products/:id', component: ProductDetailComponent }
<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز میباشد:
this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } );
یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشتهی دریافتی، به عدد میگردد.
تکمیل کامپوننت نمایش جزئیات یک محصول
در ادامه قالب ابتدایی مشاهدهی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-detail', templateUrl: './product-detail.component.html', styleUrls: ['./product-detail.component.css'] }) export class ProductDetailComponent implements OnInit { pageTitle = 'Product Detail'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number) { this.productService.getProduct(id).subscribe( product => this.product = product, error => this.errorMessage = <any>error); } }
در متد OnInit، مقدار id، از snapshot دریافت میگردد. سپس این id به متد getProduct ارسال میشود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.
برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
<div class="panel panel-primary" *ngIf="product"> <div class="panel-heading" style="font-size:large"> {{pageTitle + ": " + product.productName}} </div> <div class="panel-body"> <div> Name: {{product.productName}} </div> <div> Code: {{product.productCode}} </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> <div class="panel-footer"> <a class="btn btn-default" [routerLink]="['/products']"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a> <a class="btn btn-primary" style="width:80px;margin-left:10px" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </div> </div>
تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول
از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, FormsModule, ProductRoutingModule ]
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-edit', templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'] }) export class ProductEditComponent implements OnInit { pageTitle = 'Product Edit'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute, private router: Router) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number): void { this.productService.getProduct(id) .subscribe( (product: IProduct) => this.onProductRetrieved(product), (error: any) => this.errorMessage = <any>error ); } onProductRetrieved(product: IProduct): void { this.product = product; if (this.product.id === 0) { this.pageTitle = 'Add Product'; } else { this.pageTitle = `Edit Product: ${this.product.productName}`; } } deleteProduct(): void { if (this.product.id === 0) { // Don't delete, it was never saved. this.onSaveComplete(); } else { if (confirm(`Really delete the product: ${this.product.productName}?`)) { this.productService.deleteProduct(this.product.id) .subscribe( () => this.onSaveComplete(`${this.product.productName} was deleted`), (error: any) => this.errorMessage = <any>error ); } } } saveProduct(): void { if (true === true) { this.productService.saveProduct(this.product) .subscribe( () => this.onSaveComplete(`${this.product.productName} was saved`), (error: any) => this.errorMessage = <any>error ); } else { this.errorMessage = 'Please correct the validation errors.'; } } onSaveComplete(message?: string): void { if (message) { // TODO: show msg } // Navigate back to the product list this.router.navigate(['/products']); } }
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body" *ngIf="product"> <form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm"> <fieldset> <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || productNameVar.dirty) && !productNameVar.valid }"> <label class="col-md-2 control-label" for="productNameId">Product Name</label> <div class="col-md-8"> <input class="form-control" id="productNameId" type="text" placeholder="Name (required)" required minlength="3" [(ngModel)]=product.productName name="productName" #productNameVar="ngModel" /> <span class="help-block" *ngIf="(productNameVar.touched || productNameVar.dirty) && productNameVar.errors"> <span *ngIf="productNameVar.errors.required"> Product name is required. </span> <span *ngIf="productNameVar.errors.minlength"> Product name must be at least three characters. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || productCodeVar.dirty) && !productCodeVar.valid }"> <label class="col-md-2 control-label" for="productCodeId">Product Code</label> <div class="col-md-8"> <input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" required [(ngModel)]=product.productCode name="productCode" #productCodeVar="ngModel" /> <span class="help-block" *ngIf="(productCodeVar.touched || productCodeVar.dirty) && productCodeVar.errors"> <span *ngIf="productCodeVar.errors.required"> Product code is required. </span> </span> </div> </div> <div class="form-group"> <div class="col-md-4 col-md-offset-2"> <span> <button class="btn btn-primary" type="submit" style="width:80px;margin-right:10px" [disabled]="!productForm.valid" (click)="saveProduct()"> Save </button> </span> <span> <a class="btn btn-default" [routerLink]="['/products']"> Cancel </a> </span> <span> <a class="btn btn-default" (click)="deleteProduct()"> Delete </a> </span> </div> </div> </fieldset> </form> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> </div>
توضیحات:
نکتهی مهمی را که در این کدها میخواهیم بررسی کنیم، متد ngOnInit آناست:
ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); }
- ابتدا لیست محصولات را مشاهده کنید.
- سپس بر روی دکمهی edit محصول شماره یک کلیک نمائید:
تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شدهاست. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیدهاست.
- در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:
همانطور که مشاهده میکنید، هرچند URL صفحه تغییر کردهاست، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشدهاند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کردهایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمیشود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمیشود. به همین جهت است که فیلدهای این فرم تغییر نکردهاند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آنها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe میتوان انجام داد:
ngOnInit(): void { this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } ); }
یک نکته: هر زمانیکه از Observableها استفاده میشود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت میشود.
روش خواندن پارامترهای مسیریابی در +Angular 4
روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کردهاند که توسط شیء paramMap مدیریت میشود:
// For Angular 4+ let id = +this.route.snapshot.paramMap.get('id'); this.getProduct(id); // OR this.route.paramMap.subscribe(params => { let id = +params['id']; this.getProduct(id); });
تعریف پارامترهای اختیاری مسیریابی
فرض کنید یک صفحهی جستجو را طراحی کردهاید که در آن میتوان نام و کد محصول را جستجو کرد. سپس میخواهیم این پارامترها را به صفحهی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی میتوان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
{ path: 'products/:name/:code', component: ProductListComponent }
[routerLink]="['/products', productName, productCode]"
{ path: 'products', component: ProductListComponent },
[routerLink]="['/products', {name: productName, code: productCode}]"
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
http://localhost:4200/products;name=Controller;code=gmg
let name = this.route.snapshot.params['name']; let code = this.route.snapshot.params['code'];
این پارامترها پس از هدایت کاربر به مسیری دیگر، حفظ نشده و پاک خواهند شد. به همین جهت کلیدهای تعریف شدهی در اینجا ضرورتی ندارد که یکتا بوده و با سایر قسمتهای برنامه تداخلی نداشته باشند.
تعریف پارامترهای کوئری در مسیریابی
فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کردهاید. اکنون اگر بر روی لینک مشاهدهی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده میشود. در یک چنین حالتی برای بهبود تجربهی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمتهای مختلف حفظ کرد:
http://localhost:42000/products/5?filterBy=product1
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
{ path: 'products', component: ProductListComponent}
<a [routerLink] = "['/products', product.id]" [queryParams] = "{ filterBy: 'er', showImage: true }"> {{product.productName}} </a>
و یا با کدنویسی به صورت ذیل است:
this.router.navigate(['/products'], { queryParams: { filterBy: 'er', showImage: true} } );
یک مشکل: اگر در صفحهی نمایش جزئیات یک محصول، دکمهی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمیشوند. مرحلهی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
[preserveQueryParams] = "true"
<a class="btn btn-default" [routerLink]="['/products']" [preserveQueryParams]="true"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a>
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافتهاست و حالت قبلی منسوخ شده اعلام گردیدهاست.
this.router.navigate(['/products'], { queryParamsHandling: 'preserve'} );
پس از بازگشت به صفحهی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شدهاند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آنها در متد ngOnInit باید به صورت ذیل عمل کرد:
var filter = this.route.snapshot.queryParams['filterBy'] || ''; var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
در مثال تکمیل شدهی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شدهاست:
>ng g p product/ProductFilter
import { PipeTransform, Pipe } from '@angular/core'; import { IProduct } from './iproduct'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], filterBy: string): IProduct[] { filterBy = filterBy ? filterBy.toLocaleLowerCase() : null; return filterBy ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value; } }
<div class="panel-body"> <div class="row"> <div class="col-md-2">Filter by:</div> <div class="col-md-4"> <input type="text" [(ngModel)]="listFilter" /> </div> </div> <div class="row" *ngIf="listFilter"> <div class="col-md-6"> <h3>Filtered by: {{listFilter}} </h3> </div> </div>
<tr *ngFor="let product of products | productFilter:listFilter"> <td><a [routerLink]="['/products', product.id]" [queryParams]="{filterBy: listFilter}"> {{product.productName}} </a> </td>
در اینجا اگر به صفحهی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:
و در این حالت اگر بر روی دکمهی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده میشود:
که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت میشود:
@Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { listFilter: string; ngOnInit(): void { this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
برای رفع این موارد به NuGet Package Restore وارد شوید.
به ویژوال استودیو اجازه دهید بستههای NuGet را در صورت لزوم احیا کند
پیش از آنکه بتوانیم از قابلیت Package Restore استفاده کنیم باید آن را روی ماشین خود فعال کنیم. این کار روی هر ماشین باید انجام شود (per-machine requirement). بدین منظور به منوی Tools -> Options -> Package Manager بروید.
در دیالوگ باز شده تنظیمات مربوطه را مانند تصویر زیر بروز رسانی کنید.
حال که ماشین ما برای بازیابی خودکار بستههای NuGet پیکربندی شده است، باید این قابلیت را برای Solution مورد نظر هم فعال کنیم.
فعال سازی NuGet Package Restore برای پروژهها
بدین منظور روی Solution کلیک راست کنید و گزینه Enable Package Restore را انتخاب نمایید.
این کار ممکن است چند ثانیه زمان ببرد. پس از آنکه ویژوال استودیو پردازشهای لازم را انجام داد، میتوانید ببینید که پوشه جدیدی در مسیر ریشه پروژه ایجاد شده است.
همانطور که میبینید فایلی با نام NuGet.exe در این پوشه قرار دارد که باید به سورس کنترل آپلود شود. هنگامیکه شخصی پروژه شما را از سورس کنترل دریافت کند و بخواهد پروژه را Build کند، بستههای مورد نیاز توسط این ابزار بصورت خودکار دریافت و نصب خواهند شد.
مرحله بعد حذف کردن تمام بستههای NuGet از سورس کنترل است. برای اینکار باید فایل gitignore. را ویرایش کنید. فرض بر این است که سورس کنترل شما Git است، اما قواعد ذکر شده برای دیگر فریم ورکها نیز صادق است. تنها کاری که باید انجام دهید این است که به سورس کنترل خود بگویید چه چیز هایی را در بر گیرد و از چه چیزهایی صرفنظر کند.
ویرایش فایل gitignore. برای حذف بستهها و شامل کردن NuGet.exe
یک پروژه معمولی ASP.NET MVC 5 که توسط قالب استاندارد VS 2013 ایجاد میشود شامل 161 فایل از بستههای مختلف میشود (در زمان تالیف این پست). این مقدار قابل توجهی است که حجم زیادی از اطلاعات غیر ضروری را به مخزن سورس کنترل اضافه میکند. با استفاده از نسخه پیش فرض فایل gitignore. (یا فایلهای مشابه دیگر برای سورس کنترلهای مختلف مثل TFS) تعداد فایل هایی که در کل به مخزن سورس کنترل ارسال میشوند بیش از 200 آیتم خواهد بود. قابل ذکر است که این تعداد فایل شامل فایلهای اجرایی (binary) و متعلق به ویژوال استودیو نیست. به بیان دیگر نزدیک به 75% از فایلهای یک پروژه معمولی ASP.NET MVC 5 که توسط VS 2013 ساخته میشود را بستههای NuGet تشکیل میدهد، که حالا میتوانند بجای ارسال شدن به مخزن سورس کنترل، بصورت خودکار بازیابی و نصب شوند.
برای حذف این فایلها از سورس کنترل، فایل gitignore. را ویرایش میکنیم. اگر از سورس کنترلهای دیگری استفاده میکنید نام این فایل hgignore. یا tfsignore. یا غیره خواهد بود. محتوای فایل شما ممکن است با لیست زیر متفاوت باشد اما جای نگرانی نیست. تنها تغییرات اندکی بوجود خواهیم آورد و مابقی محتویات فایل مهم نیستند.
چشم پوشی از پوشه Packages
فایل gitignore. را باز کنید و برای نادیده گرفتن پوشه بستههای NuGet در سورس، خط زیر را به آن اضافه کنید.
packages*/
استثنایی برای در نظر گرفتن NuGet.exe ایجاد کنید
به احتمال زیاد فایل gitignore. شما از فایل هایی با فرمت .exe چشم پوشی میکند. برای اینکه بستههای NuGet بتوانند بصورت خودکار دریافت شوند باید استثنایی تعریف کنیم. فایل gitignore. خود را باز کنید و به دنبال خط زیر بگردید.
*.exe
*.exe !NuGet.exe
انجام مرحله بالا انتخابی (optional) است. اگر کسی که پروژه را از مخزن سورس کنترل دریافت میکند قابلیت Package Restore را روی Solution فعال کند ابزار NuGet.exe دریافت میشود. اما با انجام این مراحل دیگر نیازی به این فعالسازی نخواهد بود، پس در کار اعضای تیم هم صرفه جویی کرده اید.
اطلاع رسانی به اعضای تیم و مشتریان بالقوه
دیگر نیاز نیست بستههای NuGet را به مخزن سورس کنترل ارسال کنیم. اما باید به مخاطبین خود اطلاع دهید تا پیکربندیهای لازم برای استفاده از قابلیت Package Restore را انجام دهند (مثلا در فایل README.txt پروژه).