ASP.NET Web API فریم ورکی برای ساختن APIهای وب بر روی فریم ورک دات نت است. در این مقاله با استفاده از این فریم ورک، API وبی خواهیم ساخت که لیستی از محصولات را بر میگرداند. صفحه وب کلاینت، با استفاده از jQuery نتایج را نمایش خواهد داد.
یک پروژه Web API بسازید
در ویژوال استودیو 2013 پروژه جدیدی از نوع ASP.NET Web Application بسازید و نام آن را "ProductsApp" انتخاب کنید.
در دیالوگ New ASP.NET Project قالب Empty را انتخاب کنید و در قسمت "Add folders and core references for" گزینه Web API را انتخاب نمایید.
می توانید از قالب Web API هم استفاده کنید. این قالب با استفاده از ASP.NET MVC صفحات راهنمای API را خواهد ساخت. در این مقاله از قالب Empty استفاده میکنیم تا تمرکز اصلی، روی خود فریم ورک Web API باشد. بطور کلی برای استفاده از این فریم ورک لازم نیست با ASP.NET MVC آشنایی داشته باشید.
افزودن یک مدل
یک مدل (model) آبجکتی است که داده اپلیکیشن شما را معرفی میکند. ASP.NET Web API میتواند بصورت خودکار مدل شما را به JSON, XML و برخی فرمتهای دیگر مرتب (serialize) کند، و سپس داده مرتب شده را در بدنه پیام HTTP Response بنویسد. تا وقتی که یک کلاینت بتواند فرمت مرتب سازی دادهها را بخواند، میتواند آبجکت شما را deserialize کند. اکثر کلاینتها میتوانند XML یا JSON را تفسیر کنند. بعلاوه کلاینتها میتوانند فرمت مورد نظرشان را با تنظیم Accept header در پیام HTTP Request مشخص کنند.
بگذارید تا با ساختن مدلی ساده که یک محصول (product) را معرفی میکند شروع کنیم.
کلاس جدیدی در پوشه Models ایجاد کنید.
نام کلاس را به "Product" تغییر دهید، و خواص زیر را به آن اضافه کنید.
namespace ProductsApp.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
افزودن یک کنترلر
در Web API کنترلرها آبجکت هایی هستند که درخواستهای HTTP را مدیریت کرده و آنها را به اکشن متدها نگاشت میکنند. ما کنترلری خواهیم ساخت که میتواند لیستی از محصولات، یا محصولی بخصوص را بر اساس شناسه برگرداند. اگر از ASP.NET MVC استفاده کرده اید، با کنترلرها آشنا هستید. کنترلرهای Web API مشابه کنترلرهای MVC هستند، با این تفاوت که بجای ارث بری از کلاس Controller از کلاس ApiController مشتق میشوند.
کنترلر جدیدی در پوشه Controllers ایجاد کنید.
در دیالوگ Add Scaffold گزینه Web API Controller - Empty را انتخاب کرده و روی Add کلیک کنید.
در دیالوگ Add Controller نام کنترلر را به "ProductsController" تغییر دهید و روی Add کلیک کنید.
توجه کنید که ملزم به ساختن کنترلرهای خود در پوشه Controllers نیستید، و این روش صرفا قراردادی برای مرتب نگاه داشتن ساختار پروژهها است. کنترلر ساخته شده را باز کنید و کد زیر را به آن اضافه نمایید.
using ProductsApp.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; namespace ProductsApp.Controllers { public class ProductsController : ApiController { Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; public IEnumerable<Product> GetAllProducts() { return products; } public IHttpActionResult GetProduct(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { return NotFound(); } return Ok(product); } }
کنترلر ما دو متد برای دریافت محصولات تعریف میکند:
- متد GetAllProducts لیست تمام محصولات را در قالب یک <IEnumerable<Product بر میگرداند.
- متد GetProductById سعی میکند محصولی را بر اساس شناسه تعیین شده پیدا کند.
همین! حالا یک Web API ساده دارید. هر یک از متدهای این کنترلر، به یک یا چند URI پاسخ میدهند:
URI | Controller Method |
api/products/ | GetAllProducts |
api/products/id/ | GetProductById |
برای اطلاعات بیشتر درباره نحوه نگاشت درخواستهای HTTP به اکشن متدها توسط Web API به این لینک مراجعه کنید.
فراخوانی Web API با جاوا اسکریپت و jQuery
در این قسمت یک صفحه HTML خواهیم ساخت که با استفاده از AJAX متدهای Web API را فراخوانی میکند. برای ارسال درخواستهای آژاکسی و بروز رسانی صفحه بمنظور نمایش نتایج دریافتی از jQuery استفاده میکنیم.
در پنجره Solution Explorer روی نام پروژه کلیک راست کرده و گزینه Add, New Item را انتخاب کنید.
در دیالوگ Add New Item قالب HTML Page را انتخاب کنید و نام فایل را به "index.html" تغییر دهید.
حال محتوای این فایل را با لیست زیر جایگزین کنید.
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Product App</title> </head> <body> <div> <h2>All Products</h2> <ul id="products" /> </div> <div> <h2>Search by ID</h2> <input type="text" id="prodId" size="5" /> <input type="button" value="Search" onclick="find();" /> <p id="product" /> </div> <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.min.js"></script> <script> var uri = 'api/products'; $(document).ready(function () { // Send an AJAX request $.getJSON(uri) .done(function (data) { // On success, 'data' contains a list of products. $.each(data, function (key, item) { // Add a list item for the product. $('<li>', { text: formatItem(item) }).appendTo($('#products')); }); }); }); function formatItem(item) { return item.Name + ': $' + item.Price; } function find() { var id = $('#prodId').val(); $.getJSON(uri + '/' + id) .done(function (data) { $('#product').text(formatItem(data)); }) .fail(function (jqXHR, textStatus, err) { $('#product').text('Error: ' + err); }); } </script> </body> </html>
گرفتن لیستی از محصولات
برای گرفتن لیستی از محصولات، یک درخواست HTTP GET به آدرس "api/products/" ارسال کنید.
تابع getJSON یک درخواست آژاکسی ارسال میکند. پاسخ دریافتی هم آرایه ای از آبجکتهای JSON خواهد بود. تابع done در صورت موفقیت آمیز بودن درخواست، اجرا میشود. که در این صورت ما DOM را با اطلاعات محصولات بروز رسانی میکنیم.
$(document).ready(function () { // Send an AJAX request $.getJSON(apiUrl) .done(function (data) { // On success, 'data' contains a list of products. $.each(data, function (key, item) { // Add a list item for the product. $('<li>', { text: formatItem(item) }).appendTo($('#products')); }); }); });
گرفتن محصولی مشخص
برای گرفتن یک محصول توسط شناسه (ID) آن کافی است یک درخواست HTTP GET به آدرس "api/products/id/" ارسال کنید.
function find() { var id = $('#prodId').val(); $.getJSON(apiUrl + '/' + id) .done(function (data) { $('#product').text(formatItem(data)); }) .fail(function (jqXHR, textStatus, err) { $('#product').text('Error: ' + err); }); }
اجرای اپلیکیشن
اپلیکیشن را با F5 اجرا کنید. صفحه وب باز شده باید چیزی مشابه تصویر زیر باشد.
برای گرفتن محصولی مشخص، شناسه آن را وارد کنید و روی Search کلیک کنید.
اگر شناسه نامعتبری وارد کنید، سرور یک خطای HTTP بر میگرداند.
استفاده از F12 برای مشاهده درخواستها و پاسخ ها
هنگام کار با سرویسهای HTTP، مشاهدهی درخواستهای ارسال شده و پاسخهای دریافتی بسیار مفید است. برای اینکار میتوانید از ابزار توسعه دهندگان وب استفاده کنید، که اکثر مرورگرهای مدرن، پیاده سازی خودشان را دارند. در اینترنت اکسپلورر میتوانید با F12 به این ابزار دسترسی پیدا کنید. به برگه Network بروید و روی Start Capturing کلیک کنید. حالا صفحه وب را مجددا بارگذاری (reload) کنید. در این مرحله اینترنت اکسپلورر ترافیک HTTP بین مرورگر و سرور را تسخیر میکند. میتوانید تمام ترافیک HTTP روی صفحه جاری را مشاهده کنید.
به دنبال آدرس نسبی "api/products/" بگردید و آن را انتخاب کنید. سپس روی Go to detailed view کلیک کنید تا جزئیات ترافیک را مشاهده کنید. در نمای جزئیات، میتوانید headerها و بدنه درخواستها و پاسخها را ببینید. مثلا اگر روی برگه Request headers کلیک کنید، خواهید دید که اپلیکیشن ما در Accept header دادهها را با فرمت "application/json" درخواست کرده است.
اگر روی برگه Response body کلیک کنید، میتوانید ببینید چگونه لیست محصولات با فرمت JSON سریال شده است. همانطور که گفته شده مرورگرهای دیگر هم قابلیتهای مشابهی دارند. یک ابزار مفید دیگر Fiddler است. با استفاده از این ابزار میتوانید تمام ترافیک HTTP خود را مانیتور کرده، و همچنین درخواستهای جدیدی بسازید که این امر کنترل کاملی روی HTTP headers به شما میدهد.
قدمهای بعدی
این برنامه از چهار کامپوننت تشکیل شدهاست:
- کامپوننت App که در برگیرندهی سه کامپوننت زیر است:
- کامپوننت BasketItemsCounter: جمع تعداد آیتمهای انتخابی توسط کاربر را نمایش میدهد؛ به همراه دکمهای برای خالی کردن لیست انتخابی.
- کامپوننت ShopItemsList: لیست محصولات موجود در فروشگاه را نمایش میدهد. با کلیک بر روی هر آیتم آن، آیتم انتخابی به لیست انتخابهای او اضافه خواهد شد.
- کامپوننت BasketItemsList: لیستی را نمایش میدهد که حاصل انتخابهای کاربر در کامپوننت ShopItemsList است (یا همان سبد خرید). در ذیل این لیست، جمع نهایی قیمت قابل پرداخت نیز درج میشود. همچنین اگر کاربر بر روی دکمهی remove هر ردیف کلیک کند، یک واحد از چند واحد انتخابی، حذف خواهد شد.
بنابراین در اینجا سه کامپوننت مجزا را داریم که با هم تبادل اطلاعات میکنند. یکی جمع تعداد محصولات خریداری شده را، دیگری لیست محصولات موجود را و آخری لیست خرید نهایی را نمایش میدهد. همچنین این سه کامپوننت، فرزند یک دیگر هم محسوب نمیشوند و انتقال اطلاعات بین اینها نیاز به بالا بردن state هر کدام و قرار دادن آنها در کامپوننت App را دارد تا بتوان پس از آن از طریق props آنها را بین سه کامپوننت فوق که اکنون فرزند کامپوننت App محسوب میشوند، به اشتراک گذاشت. روش بهتر اینکار، استفاده از یک مخزن حالت سراسری است تا حالتهای این کامپوننتها را نگهداری کرده و دادهها را بین آنها به اشتراک بگذارد که در اینجا برای حل این مساله از کتابخانههای mobx و mobx-react استفاده خواهیم کرد.
برپایی پیشنیازها
برای پیاده سازی برنامهی فوق، یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app state-management-with-mobx-part4 > cd state-management-with-mobx-part4
> npm install --save bootstrap mobx mobx-react mobx-react-devtools mobx-state-tree
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود.
- اصل کار برنامه توسط دو کتابخانهی mobx و کتابخانهی متصل کنندهی آن به برنامههای react که mobx-react نام دارد، انجام خواهد شد.
- چون میخواهیم از افزونهی mobx-devtools نیز استفاده کنیم، نیاز است دو بستهی mobx-react-devtools و همچنین mobx-state-tree را که جزو وابستگیهای آن است، نصب کنیم.
سپس بستههای زیر را که در قسمت devDependencies فایل package.json درج خواهند شد، باید نصب شوند:
> npm install --save-dev babel-eslint customize-cra eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort react-app-rewired typescript
- افزودن فایل جدید config-overrides.js به ریشهی پروژه، تا پشتیبانی ازlegacy" decorators spec" فعال شود.
- اصلاح فایل package.json و ویرایش قسمت scripts آن برای استفادهی از react-app-rewired، تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود.
- همچنین فایل غنی شدهی eslintrc.json. را نیز به ریشهی پروژه اضافه میکنیم.
تهیه سرویس لیست محصولات موجود در فروشگاه
این برنامه از یک لیست درون حافظهای، برای تهیهی لیست محصولات موجود در فروشگاه استفاده میکند. به همین جهت پوشهی service را افزوده و سپس فایل جدید src\services\productsService.js را با محتوای زیر، ایجاد میکنیم:
const products = [ { id: 1, name: "Item 1", price: 850 }, { id: 2, name: "Item 2", price: 900 }, { id: 3, name: "Item 3", price: 1500 }, { id: 4, name: "Item 4", price: 1000 } ]; export default products;
ایجاد کامپوننت نمایش لیست محصولات
پس از مشخص شدن لیست محصولات قابل فروش، کامپوننت جدید src\components\ShopItemsList.jsx را به صورت زیر ایجاد میکنیم:
import React from "react"; import products from "../services/productsService"; const ShopItemsList = ({ onAdd }) => { return ( <table className="table table-hover"> <thead className="thead-light"> <tr> <th>Name</th> <th>Price</th> <th>Action</th> </tr> </thead> <tbody> {products.map(product => ( <tr key={product.id}> <td>{product.name}</td> <td>{product.price}</td> <td> <button className="btn btn-sm btn-info" onClick={() => onAdd(product)} > Add </button> </td> </tr> ))} </tbody> </table> ); }; export default ShopItemsList;
- در اینجا همچنین هر ردیف، به همراه یک دکمهی Add نیز هست که قرار است با کلیک بر روی آن، متد رویدادگردان onAdd فراخوانی شود. این متد نیز از طریق props این کامپوننت دریافت میشود. کتابخانههای مدیریت حالت، تمام خواص و رویدادگردانهای مورد نیاز یک کامپوننت را از طریق props، تامین میکنند.
- فعلا این کامپوننت به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
ایجاد کامپوننت نمایش لیست خرید کاربر (سبد خرید)
اکنون که میتوان توسط کامپوننت لیست محصولات، تعدادی از آنها را خریداری کرد، کامپوننت جدید src\components\BasketItemsList.jsx را برای نمایش لیست نهایی خرید کاربر، به صورت زیر پیاده سازی میکنیم:
import React from "react"; const BasketItemsList = ({ items, totalPrice, onRemove }) => { return ( <> <table className="table table-hover"> <thead className="thead-light"> <tr> <th>Name</th> <th>Price</th> <th>Count</th> <th>Action</th> </tr> </thead> <tbody> {items.map(item => ( <tr key={item.id}> <td>{item.name}</td> <td>{item.price}</td> <td>{item.count}</td> <td> <button className="btn btn-sm btn-danger" onClick={() => onRemove(item.id)} > Remove </button> </td> </tr> ))} <tr> <td align="right"> <strong>Total: </strong> </td> <td> <strong>{totalPrice}</strong> </td> <td></td> <td></td> </tr> </tbody> </table> </> ); }; export default BasketItemsList;
const BasketItemsList = ({ items, totalPrice, onRemove }) => {
- همچنین هر ردیف نمایش داده شده، به همراه یک دکمهی Remove آیتم انتخابی نیز هست که به متد رویدادگردان onRemove متصل شدهاست.
- در ردیف انتهایی این لیست، مقدار totalPrice که یک خاصیت محاسباتی است، درج میشود.
- فعلا این کامپوننت نیز به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
ایجاد کامپوننت نمایش تعداد آیتمهای خریداری شده
کاربر اگر آیتمی را از لیست محصولات انتخاب کند و یا محصول انتخاب شده را از لیست خرید حذف کند، تعداد نهایی باقی مانده را میتوان در کامپوننت src\components\BasketItemsCounter.jsx مشاهده کرد:
import React, { Component } from "react"; class BasketItemsCounter extends Component { render() { const { count, onRemoveAll } = this.props; return ( <div> <h1>Total items: {count}</h1> <button type="button" className="btn btn-sm btn-danger" onClick={() => onRemoveAll()} > Empty Basket </button> </div> ); } } export default BasketItemsCounter;
- فعلا این کامپوننت نیز به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
نمایش ابتدایی سه کامپوننت توسط کامپوننت App
اکنون که این سه کامپوننت تکمیل شدهاند، میتوان المانهای آنها را در فایل src\App.js درج کرد تا در صفحه نمایش داده شوند:
import React, { Component } from "react"; import BasketItemsCounter from "./components/BasketItemsCounter"; import BasketItemsList from "./components/BasketItemsList"; import ShopItemsList from "./components/ShopItemsList"; class App extends Component { render() { return ( <main className="container"> <div className="row"> <BasketItemsCounter /> </div> <hr /> <div className="row"> <h2>Products</h2> <ShopItemsList /> </div> <div className="row"> <h2>Basket</h2> <BasketItemsList /> </div> </main> ); } } export default App;
میتوان همانند Redux کل state برنامه را داخل یک شیء store ذخیره کرد و یا چون در اینجا میتوان طراحی مخزن حالت MobX را به دلخواه انجام داد، میتوان چندین مخزن حالت را تهیه و به هم متصل کرد؛ مانند تصویری که مشاهده میکنید. در اینجا:
- src\stores\counter.js: مخزن دادهی حالت کامپوننت شمارشگر است.
- src\stores\market.js: مخزن دادهی کامپوننتهای لیست محصولات و سبد خرید است.
- src\stores\index.js: کار ترکیب دو مخزن قبل را انجام میدهد.
در ادامه کدهای کامل این مخازن را مشاهده میکنید:
مخزن حالت src\stores\counter.js
import { action, observable } from "mobx"; export default class CounterStore { @observable totalNumbersInBasket = 0; constructor(rootStore) { this.rootStore = rootStore; } @action increase = () => { this.totalNumbersInBasket++; }; @action decrease = () => { this.totalNumbersInBasket--; }; }
- در اینجا خاصیت totalNumbersInBasket به صورت observable تعریف شدهاست و با تغییر آن چه به صورت مستقیم، با مقدار دهی آن و یا توسط دو action تعریف شده، سبب به روز رسانی UI خواهد شد.
- میشد این مخزن را با مخزن src\stores\market.js یکی کرد؛ اما جهت ارائهی مثالی در مورد نحوهی تعریف چند مخزن و روش برقراری ارتباط بین آنها، به صورت مجزایی تعریف شد.
مخزن حالت src\stores\market.js
import { action, computed, observable } from "mobx"; export default class MarketStore { @observable basketItems = []; constructor(rootStore) { this.rootStore = rootStore; } @action add = product => { const selectedItem = this.basketItems.find(item => item.id === product.id); if (selectedItem) { selectedItem.count++; } else { this.basketItems.push({ ...product, count: 1 }); } this.rootStore.counterStore.increase(); }; @action remove = id => { const selectedItem = this.basketItems.find(item => item.id === id); selectedItem.count--; if (selectedItem.count === 0) { this.basketItems.remove(selectedItem); } this.rootStore.counterStore.decrease(); }; @action removeAll = () => { this.basketItems = []; this.rootStore.counterStore.totalNumbersInBasket = 0; }; @computed get totalPrice() { return this.basketItems.reduce((previous, current) => { return previous + current.price * current.count; }, 0); } }
- توسط متد add آن در کامپوننت نمایش لیست محصولات، میتوان آیتمی را به این آرایه اضافه کرد. در اینجا چون شیء product مورد استفاده دارای خاصیت count نیست، روش افزودن آنرا توسط spread operator برای درج خواص شیء product اصلی و سپس تعریف آنرا مشاهده میکنید. این فراخوانی، سبب افزایش یک واحد به عدد شمارشگر نیز میشود.
- متد remove آن در کامپوننت سبد خرید، مورد استفاده قرار میگیرد تا کاربر بتواند اطلاعاتی را از این لیست حذف کند. این فراخوانی، سبب کاهش یک واحد از عدد شمارشگر نیز میشود.
- متد removeAll آن در کامپوننت شمارشگر بالای صفحه استفاده میشود تا سبب خالی شدن آرایهی آیتمهای انتخابی گردد و همچنین عدد آنرا نیز صفر کند.
- خاصیت محاسباتی totalPrice آن در پایین جدول سبد خرید، جمع کل هزینهی قابل پرداخت را مشخص میکند.
مخزن حالت src\stores\index.js
در اینجا روش یکی کردن دو مخزن حالت یاد شده را به صورت خاصیتهای عمومی یک مخزن کد ریشه، مشاهده میکنید:
import CounterStore from "./counter"; import MarketStore from "./market"; class RootStore { counterStore = new CounterStore(this); marketStore = new MarketStore(this); } export default RootStore;
export default class MarketStore { @observable basketItems = []; constructor(rootStore) { this.rootStore = rootStore; } @action removeAll = () => { this.basketItems = []; this.rootStore.counterStore.totalNumbersInBasket = 0; }; }
پس از ایجاد مخازن حالت، اکنون نیاز است آنها را در اختیار سلسه مراتب کامپوننتهای برنامه قرار دهیم. به همین جهت به فایل src\index.js مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
import "./index.css"; import "bootstrap/dist/css/bootstrap.css"; import makeInspectable from "mobx-devtools-mst"; import { Provider } from "mobx-react"; import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import RootStore from "./stores"; const rootStore = new RootStore(); if (process.env.NODE_ENV === "development") { makeInspectable(rootStore); // https://github.com/mobxjs/mobx-devtools } ReactDOM.render( <Provider {...rootStore}> <App /> </Provider>, document.getElementById("root") ); serviceWorker.unregister();
- سپس یک وهلهی جدید از RootStore را که حاوی خاصیتهای عمومی counterStore و marketStore است، ایجاد میکنیم.
- اگر علاقمند باشید تا حین کار با MobX، جزئیات پشت صحنهی آنرا توسط افزونهی mobx-devtools ردیابی کنید، روش آنرا در اینجا با فراخوانی متد makeInspectable مشاهده میکنید. مقدار process.env.NODE_ENV نیز بر اساس پروسهی جاری node.js اجرا کنندهی برنامهی React تامین میشود. اطلاعات بیشتر
- قسمت آخر این تنظیمات، محصور کردن کامپوننت App که بالاترین کامپوننت در سلسله مراتب کامپوننتهای برنامه است، با شیء Provider میباشد. در این شیء توسط spread operator، سبب درج خواص عمومی rootStore، به عنوان مخازن قابل استفاده شدهایم. تنظیم {rootStore...} معادل عبارت زیر است:
<Provider counterStore={rootStore.counterStore} marketStore={rootStore.marketStore}>
اتصال کامپوننت ShopItemsList به مخزن حالت marketStore
پس از ایجاد rootStore و محصور کردن کامپوننت App توسط شیء Provider در فایل src\index.js، اکنون باید قسمت export default کامپوننتهای برنامه را جهت استفادهی از مخازن حالت، یکی یکی ویرایش کرد:
import { inject, observer } from "mobx-react"; import React from "react"; import products from "../services/productsService"; const ShopItemsList = ({ onAdd }) => { return ( // ... ); }; export default inject(({ marketStore }) => ({ onAdd: marketStore.add }))(observer(ShopItemsList));
اتصال کامپوننت BasketItemsList به مخزن حالت marketStore
در اینجا نیز سطر export default را جهت دریافت خاصیت marketStore، از شیء Provider تامین شدهی در فایل src\index.js، ویرایش میکنیم. به این ترتیب سه props مورد انتظار این کامپوننت، توسط خاصیتهای basketItems (آرایهی اشیاء انتخابی توسط کاربر)، totalPrice (خاصیت محاسباتی جمع کل هزینه) و متد رویدادگردان onRemove (برای حذف یک آیتم) تامین میشوند. در آخر کامپوننت را به صورت observer محصور کرده و بازگشت میدهیم تا تغییرات در مخزن حالت آن، سبب به روز رسانی UI آن شوند:
import { inject, observer } from "mobx-react"; import React from "react"; const BasketItemsList = ({ items, totalPrice, onRemove }) => { return ( // ... ); }; export default inject(({ marketStore }) => ({ items: marketStore.basketItems, totalPrice: marketStore.totalPrice, onRemove: marketStore.remove }))(observer(BasketItemsList));
اتصال کامپوننت BasketItemsCounter به دو مخزن حالت counterStore و marketStore
در اینجا روش استفادهی از decorator syntax کتابخانهی mobx-react را بر روی یک کامپوننت کلاسی مشاهده میکنید. تزئین کنندهی inject، امکان دسترسی به مخازن حالت تزریقی به شیء Provider را میسر کرده و سپس توسط آن میتوان props مورد انتظار کامپوننت را از مخازن متناظر استخراج کرده و در اختیار کامپوننت قرار داد. همچنین این کامپوننت توسط تزئین کنندهی observer نیز علامت گذاری شدهاست. در این حالت نیازی به تغییر سطر export default نیست.
import { inject, observer } from "mobx-react"; import React, { Component } from "react"; @inject(rootStore => ({ count: rootStore.counterStore.totalNumbersInBasket, onRemoveAll: rootStore.marketStore.removeAll })) @observer class BasketItemsCounter extends Component { render() { const { count, onRemoveAll } = this.props; return ( // ... ); } } export default BasketItemsCounter;
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part4.zip
علت نیاز به Child Routes
در مثال این سری، منوی اصلی آن به صورت ذیل تعریف شدهاست:
<ul class="nav navbar-nav"> <li><a [routerLink]="['/home']">Home</a></li> <li><a [routerLink]="['/products']">Product List</a></li> <li><a [routerLink]="['/products', 0, 'edit']">Add Product</a></li> </ul>
<div class="container"> <router-outlet></router-outlet> </div>
کاربردهای Child routes
- امکان تقسیم فرمهای طولانی به چند Tab
- امکان طراحی طرحبندیهای Master/Layout
- قرار دادن قالب یک کامپوننت، درون قالب کامپوننتی دیگر
- بهبود کپسوله سازی ماژولهای برنامه
- جزو الزامات Lazy loading هستند
تنظیم کردن Child Routes
مثال جاری این سری، تنها به همراه یک سری primary routes است؛ مانند صفحهی خوشآمد گویی، نمایش لیست محصولات، افزودن و ویرایش محصولات. قالبهای کامپوننتهای اینها نیز در router-outlet اصلی برنامه نمایش داده میشوند. در ادامه میخواهیم کامپوننت ویرایش محصولات را تغییر داده و تعدادی برگه را به آن اضافه کنیم. برای اینکار، نیاز به تعریف Child routes است تا بتوان قالبهای کامپوننتهای هر برگه را در router-outlet کامپوننت والد که در درون router-outlet اصلی برنامه قرار دارد، نمایش داد.
به همین جهت دو کامپوننت جدید ProductEditInfo و ProductEditTags را نیز به ماژول محصولات اضافه میکنیم:
>ng g c product/ProductEditInfo >ng g c product/ProductEditTags
به علاوه اینترفیس src\app\product\iproduct.ts را نیز جهت افزودن گروه محصولات و همچنین آرایهی برچسبهای یک محصول تکمیل میکنیم:
export interface IProduct { id: number; productName: string; productCode: string; category: string; tags?: string[]; }
در ادامه برای تنظیم Child Routes، فایل src\app\product\product-routing.module.ts را گشوده و آنرا به نحو ذیل تکمیل کنید:
import { ProductEditTagsComponent } from './product-edit-tags/product-edit-tags.component'; import { ProductEditInfoComponent } from './product-edit-info/product-edit-info.component'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent, resolve: { product: ProductResolverService } }, { path: 'products/:id/edit', component: ProductEditComponent, resolve: { product: ProductResolverService }, children: [ { path: '', redirectTo: 'info', pathMatch: 'full' }, { path: 'info', component: ProductEditInfoComponent }, { path: 'tags', component: ProductEditTagsComponent } ] } ];
- در اولین Child Route تعریف شده، مقدار path به '' تنظیم شدهاست. به این ترتیب مسیریابی پیش فرض آن (در صورت عدم ذکر صریح آنها در URL) به صورت خودکار به مسیریابی info هدایت خواهد شد. بنابراین درخواست مسیر products/:id/edit به دومین Child Route تنظیم شده هدایت میشود.
- دومین Child Route تعریف شده با مسیری مانند products/:id/edit/info تطابق پیدا میکند.
- سومین Child Route تعریف شده با مسیری مانند products/:id/edit/tags تطابق پیدا میکند.
تعیین محل نمایش Child Views
برای نمایش قالب یک Child Route درون قالب والد آن، نیاز به تعریف یک دایرکتیو router-outlet جدید، درون قالب والد است و نحوهی تعریف آن با primary outlet تعریف شدهی در فایل src\app\app.component.html تفاوتی ندارد.
برای پیاده سازی این مفهوم، نیاز است از قالب ویرایش محصولات و یا فایل src\app\product\product-edit\product-edit.component.html که قالب والد این Child Routes است شروع و آنرا به دو Child View تقسیم کنیم. این قالب، تاکنون حاوی فرمی جهت ویرایش و افزودن محصولات است. در ادامه میخواهیم بجای آن چند برگه را نمایش دهیم. به همین جهت این فرم را حذف کرده و با دو برگهی جدید جایگزین میکنیم. در اینجا نحوهی تعریف لینکهای جدید، به Child Routes و همچنین محل قرارگیری router-outlet ثانویه را نیز مشاهده میکنید:
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body" *ngIf="product"> <div class="wizard"> <a [routerLink]="['info']"> Basic Information </a> <a [routerLink]="['tags']"> Search Tags </a> </div> <router-outlet></router-outlet> </div> <div class="panel-footer"> <div class="row"> <div class="col-md-6 col-md-offset-2"> <span> <button class="btn btn-primary" type="button" style="width:80px;margin-right:10px" [disabled]="!isValid()" (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> </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div>
فعالسازی Child Routes
دو روش برای فعالسازی Child Routes وجود دارند:
الف) با ذکر مسیر مطلق
<a [routerLink]="['/products',product.id,'edit','info']">Info</a>
ب) با ذکر مسیر نسبی
<a [routerLink]="['info']">Info</a>
در این حالت اگر تنظیمات والد این مسیریابی تغییر کنند، نیازی به تغییر مسیر نسبی تعریف شده نیست (برخلاف حالت مطلق که بر اساس قید کامل تمام اجزای مسیریابی والد آن کار میکند).
دقیقا همین پارامترها، قابلیت استفادهی در متد this.route.navigate را نیز دارند:
الف) برای حالت ذکر مسیر مطلق:
this.router.navigate(['/products', this.product.id,'edit','info']);
this.router.navigate(['info', { relativeTo: this.route }]);
تکمیل Child Viewهای برنامه
تا اینجا لینکهایی نسبی را به مسیریابیهای info و tags اضافه کردیم. در ادامه قالبها و کامپوننتهای آنها را تکمیل میکنیم:
الف) تکمیل کامپوننت ProductEditInfoComponent در فایل src\app\product\product-edit\product-edit.component.ts
import { ActivatedRoute } from '@angular/router'; import { NgForm } from '@angular/forms'; import { Component, OnInit, ViewChild } from '@angular/core'; import { IProduct } from './../iproduct'; @Component({ //selector: 'app-product-edit-info', templateUrl: './product-edit-info.component.html', styleUrls: ['./product-edit-info.component.css'] }) export class ProductEditInfoComponent implements OnInit { @ViewChild(NgForm) productForm: NgForm; errorMessage: string; product: IProduct; constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.route.parent.data.subscribe(data => { this.product = data['product']; if (this.productForm) { this.productForm.reset(); } }); } }
<div class="panel-body"> <form class="form-horizontal" novalidate #productForm="ngForm"> <fieldset> <legend>Basic Product Information</legend> <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || productNameVar.dirty || product.id !== 0) && !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 || product.id !== 0) && 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 || product.id !== 0) && !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 || product.id !== 0) && productCodeVar.errors"> <span *ngIf="productCodeVar.errors.required"> Product code is required. </span> </span> </div> </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </fieldset> </form> </div>
ب) تکمیل کامپوننت ProductEditTagsComponent در فایل src\app\product\product-edit-tags\product-edit-tags.component.ts
import { ActivatedRoute } from '@angular/router'; import { IProduct } from './../iproduct'; import { Component, OnInit } from '@angular/core'; @Component({ //selector: 'app-product-edit-tags', templateUrl: './product-edit-tags.component.html', styleUrls: ['./product-edit-tags.component.css'] }) export class ProductEditTagsComponent implements OnInit { errorMessage: string; newTags = ''; product: IProduct; constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.route.parent.data.subscribe(data => { this.product = data['product']; }); } // Add the defined tags addTags(): void { let tagArray = this.newTags.split(','); this.product.tags = this.product.tags ? this.product.tags.concat(tagArray) : tagArray; this.newTags = ''; } // Remove the tag from the array of tags. removeTag(idx: number): void { this.product.tags.splice(idx, 1); } }
<div class="panel-body"> <form class="form-horizontal" novalidate> <fieldset> <legend>Product Search Tags</legend> <div class="form-group" [ngClass]="{'has-error': (categoryVar.touched || categoryVar.dirty || product.id !== 0) && !categoryVar.valid }"> <label class="col-md-2 control-label" for="categoryId">Category</label> <div class="col-md-8"> <input class="form-control" id="categoryId" type="text" placeholder="Category (required)" required minlength="3" [(ngModel)]="product.category" name="category" #categoryVar="ngModel" /> <span class="help-block" *ngIf="(categoryVar.touched || categoryVar.dirty || product.id !== 0) && categoryVar.errors"> <span *ngIf="categoryVar.errors.required"> A category must be entered. </span> <span *ngIf="categoryVar.errors.minlength"> The category must be at least 3 characters in length. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (tagVar.touched || tagVar.dirty || product.id !== 0) && !tagVar.valid }"> <label class="col-md-2 control-label" for="tagsId">Search Tags</label> <div class="col-md-8"> <input class="form-control" id="tagsId" type="text" placeholder="Search keywords separated by commas" minlength="3" [(ngModel)]="newTags" name="tags" #tagVar="ngModel" /> <span class="help-block" *ngIf="(tagVar.touched || tagVar.dirty || product.id !== 0) && tagVar.errors"> <span *ngIf="tagVar.errors.minlength"> The search tag must be at least 3 characters in length. </span> </span> </div> <div class="col-md-1"> <button type="button" class="btn btn-default" (click)="addTags()"> Add </button> </div> </div> <div class="row col-md-8 col-md-offset-2"> <span *ngFor="let tag of product.tags; let i = index"> <button class="btn btn-default" style="font-size:smaller;margin-bottom:12px" (click)="removeTag(i)"> {{tag}} <span class="glyphicon glyphicon-remove"></span> </button> </span> </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </fieldset> </form> </div>
دریافت اطلاعات جهت Child Routes
روشهای متعددی برای دریافت اطلاعات جهت Child Routes وجود دارند:
الف) میتوان از متد this.productService.getProduct جهت دریافت اطلاعات یک محصول استفاده کرد. اما همانطور که در قسمت قبل نیز بررسی کردیم، این روش سبب نمایش ابتدایی یک قالب خالی و پس از مدتی، نمایش اطلاعات آن میشود.
ب) میتوان توسط this.route.snapshot.data['product'] اطلاعات را از Route Resolver، پس از پیش واکشی آنها از وب سرور، دریافت کرد.
ج) اگر قسمتهای مختلف Child Routes قرار است با اطلاعاتی یکسان کار کنند که قرار است بین برگههای مختلف آن به اشتراک گذاشته شوند، این اطلاعات را میتوانند از Route Resolver والد خود به کمک this.route.snapshot.data['product'] دریافت کنند.
در این مثال ما هرچند چندین برگهی مختلف را طراحی کردهایم، اما اطلاعات نمایش داده شدهی توسط آنها متعلق به یک شیء محصول میباشند. بنابراین نیاز است بتوان این اطلاعات را بین کامپوننتهای مختلف این Child Routes به اشتراک گذاشت و تنها با یک وهلهی آن کار کرد. به همین جهت با this.route.parent در هر یک از Child Components تعریف شده کار میکنیم تا بتوان به یک وهلهی شیء محصول، دسترسی یافت.
د) همچنین میتوان از روش this.route.parent.data.subscribe نیز استفاده کرد. البته در اینجا چون صفحهی افزودن محصولات با صفحهی ویرایش محصولات، دارای root URL Segment یکسانی است، نیاز است از این روش استفاده کرد تا بتوان از تغییرات بعدی پارامتر id آن مطلع شد. این مورد روشی است که در کدهای ProductEditInfoComponent مشاهده میکنید.
ngOnInit(): void { this.route.parent.data.subscribe(data => { this.product = data['product']; if (this.productForm) { this.productForm.reset(); } }); }
شبیه به همین روش را در ProductEditTagsComponent نیز بکار گرفتهایم و در آنجا نیز با شیء this.route.parent و دسترسی به اطلاعات دریافتی از Route Resolver، کار میکنیم. به این ترتیب مطمئن خواهیم شد که this.product این دو کامپوننت مختلف، هر دو به یک وهله از شیء product دریافتی از سرور، اشاره میکنند.
به این ترتیب دکمهی Save ذیل هر دو برگه، به درستی عمل کرده و میتواند اطلاعات نهایی یک شیء محصول را ذخیره کند.
رفع مشکلات اعتبارسنجی فرمهای قرار گرفتهی در برگههای مختلف
علت استفادهی از ViewChild در ProductEditInfoComponent
@ViewChild(NgForm) productForm: NgForm;
<form class="form-horizontal" novalidate #productForm="ngForm">
انجام اینکار برای برگههای دوم به بعد ضروری نیست. از این جهت که با اولین بار نمایش این صفحه، تمام آنها از حافظه خارج میشوند و مجددا بازیابی خواهند شد.
مشکل دوم اعتبارسنجی این فرم چند برگهای این است که هرچند خالی کردن نام یا کد محصول، سبب نمایش خطای اعتبارسنجی میشود، اما سبب غیرفعال شدن دکمهی Save نخواهند شد؛ از این جهت که این دکمه در قالب والد قرار دارد و نه در قالب فرزندان.
در اولین بار نمایش Child Routes، کامپوننت ویرایش اطلاعات در router-outlet آن نمایش داده میشود. در این حالت اگر کاربر بر روی لینک نمایش کامپوننت edit tags کلیک کند، قالب کامپوننت edit info به طور کامل از router-outlet حذف میشود و با قالب کامپوننت edit tags جایگزین میشود. این فرآیند به این معنا است که فرم edit info به همراه تمام اطلاعات اعتبارسنجی آن unload میشوند. به همین ترتیب زمانیکه کاربر درخواست نمایش برگهی ویرایش اطلاعات را میکند، قالب edit tags و اطلاعات اعتبارسنجی آن unload میشوند. به این معنا که در یک router-outlet در هر زمان تنها یک فرم، به همراه اطلاعات اعتبارسنجی آن در دسترس هستند.
راه حلهای ممکن:
الف) بدنهی اصلی فرم را در کامپوننت والد قرار دهیم و سپس هر کدام از فرزندان، المانهای فرمهای مرتبط را ارائه دهند. این روش کار نمیکند چون Angular المانهای فرمهای قرار گرفتهی درون router-outlet را شناسایی نمیکند.
ب) قرار دادن فرمها، به صورت مجزا در هر کامپوننت فرزند (مانند روش فعلی) و سپس اعتبارسنجی دستی در کامپوننت والد.
تغییرات مورد نیاز کامپوننت ProductEditComponent را جهت افزودن اعتبارسنجی فرمهای فرزند آنرا در اینجا ملاحظه میکنید:
export class ProductEditComponent implements OnInit { private dataIsValid: { [key: string]: boolean } = {}; isValid(path: string): boolean { this.validate(); if (path) { return this.dataIsValid[path]; } return (this.dataIsValid && Object.keys(this.dataIsValid).every(d => this.dataIsValid[d] === true)); } saveProduct(): void { if (this.isValid(null)) { 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.'; } } validate(): void { // Clear the validation object this.dataIsValid = {}; // 'info' tab if (this.product.productName && this.product.productName.length >= 3 && this.product.productCode) { this.dataIsValid['info'] = true; } else { this.dataIsValid['info'] = false; } // 'tags' tab if (this.product.category && this.product.category.length >= 3) { this.dataIsValid['tags'] = true; } else { this.dataIsValid['tags'] = false; } } }
- سپس متد validate اضافه شدهاست تا کار اعتبارسنجی را انجام دهد. در اینجا از خود شیء this.product که بین دو برگه به اشتراک گذاشته شدهاست برای انجام اعتبارسنجی استفاده میکنیم. از این جهت که برگهها نیز با استفاده از this.route.parent.data، دقیقا به همین وهله دسترسی دارند. بنابراین هرتغییری که در برگهها بر روی این وهله اعمال شود، به کامپوننت والد نیز منعکس میشود.
- متد isValid، مسیر هر برگه را دریافت میکند و سپس به متغیر dataIsValid مراجعه کرده و وضعیت آن برگه را باز میگرداند. اگر path در اینجا قید نشود، وضعیت تمام برگهها بررسی میشوند؛ مانند if (this.isValid(null)) در متد ذخیره سازی اطلاعات.
- در آخر در فایل product-edit.component.html، وضعیت فعال و غیرفعال دکمهی ثبت را نیز به این متد متصل میکنیم:
<button class="btn btn-primary" type="button" style="width:80px;margin-right:10px" [disabled]="!isValid()" (click)="saveProduct()"> Save </button>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-04.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
استفادهی استاتیک از افزونه Typeahead
منظور از استفادهی استاتیک، مشخص بودن آرایه عناصر و هچنین درج آن به صورت html encoded در صفحه است. برای این منظور، کنترلر برنامه چنین شکلی را خواهد داشت:
using System.Web.Mvc; using System.Web.Script.Serialization; namespace Mvc4TwitterBootStrapTest.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index() { var array = new[] { "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and/or Barbuda" }; ViewBag.JsonString = new JavaScriptSerializer().Serialize(array); return View(); } } }
View متناظر با آن به نحو ذیل با مشخص سازی نوع data-provide (تا به کتابخانهی جاوا اسکریپتی همراه bootstrap اعلام کند از چه افزونهای در اینجا قرار است استفاده شود)، منبع داده data-source و حداکثر تعداد آیتم ظاهر شونده data-items، میتواند طراحی شود:
@{ ViewBag.Title = "Index"; } <h2> Typeahead</h2> @Html.TextBox("search", null, htmlAttributes: new { autocomplete = "off", data_provide = "typeahead", data_items = 8, data_source = @ViewBag.JsonString })
<input autocomplete="off" data-items="8" data-provide="typeahead" data-source="["Afghanistan","Albania","Algeria","American Samoa","Andorra","Angola","Anguilla","Antarctica","Antigua and/or Barbuda"]" id="search" name="search" type="text" value="" />
اگر هم بخواهیم برای آن یک Html Helper درست کنیم، میتوان به نحو ذیل عمل کرد:
public static MvcHtmlString TypeaheadFor<TModel, TValue>( this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression, IEnumerable<string> source, int items = 8) { var jsonString = new JavaScriptSerializer().Serialize(source); return htmlHelper.TextBoxFor( expression, new { autocomplete = "off", data_provide = "typeahead", data_items = items, data_source = jsonString } ); }
استفاده پویا و Ajax ایی از افزونه Typeahead
اگر بخواهیم data-source را به صورت پویا، هربار از بانک اطلاعاتی دریافت و ارائه دهیم، نیاز به کمی اسکریپت نویسی خواهد بود:
using System; using System.Linq; using System.Web.Mvc; using System.Web.Script.Serialization; namespace Mvc4TwitterBootStrapTest.Controllers { public class HomeController : Controller { [HttpGet] public JsonResult GetNames(string term) { var array = new[] { "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and/or Barbuda" }; var results = array.Where(n => n.StartsWith(term, StringComparison.OrdinalIgnoreCase)); return Json(results.ToArray(), JsonRequestBehavior.AllowGet); } } }
سپس در تعاریف View، قسمت data-source مرتبط با TextBox حذف و از طریق فراخوانی مستقیم کدهای افزونه typeahead مقدار دهی میگردد:
@{ ViewBag.Title = "Index"; var url = Url.Action("GetNames", "Home"); } <h2> Typeahead</h2> @Html.TextBox("search", null, htmlAttributes: new { autocomplete = "off", data_provide = "typeahead", data_items = 8 }) @section JavaScript { <script type="text/javascript"> $(function () { $('#search').typeahead({ source: function (term, process) { return $.getJSON('@url', { term: term }, function (data) { return process(data); }); } }); }); </script> }
CSRF یا Cross Site Request Forgery به صورت خلاصه به این معنا است که شخص مهاجم اعمالی را توسط شما و با سطح دسترسی شما بر روی سایت انجام دهد و اطلاعات مورد نظر خود را استخراج کرده (محتویات کوکی یا سشن و امثال آن) و به هر سایتی که تمایل دارد ارسال کند. اینکار عموما با تزریق کد در صفحه صورت میگیرد. مثلا ارسال تصویری پویا به شکل زیر در یک صفحه فوروم، بلاگ یا ایمیل:
شخصی که این صفحه را مشاهده میکند، متوجه وجود هیچگونه مشکلی نخواهد شد و مرورگر حداکثر جای خالی تصویر را به او نمایش میدهد. اما کدی با سطح دسترسی شخص بازدید کننده بر روی سایت اجرا خواهد شد.
روشهای مقابله:
- هر زمانیکه کار شما با یک سایت حساس به پایان رسید، log off کنید. به این صورت بجای منتظر شدن جهت به پایان رسیدن خودکار طول سشن، سشن را زودتر خاتمه دادهاید یا برنامه نویسها نیز باید طول مدت مجاز سشن در برنامههای حساس را کاهش دهند. شاید بپرسید این مورد چه اهمیتی دارد؟ مرورگری که امکان اجازهی بازکردن چندین سایت با هم را به شما در tab های مختلف میدهد، ممکن است سشن یک سایت را در برگهای دیگر به سایت مهاجم ارسال کند. بنابراین زمانیکه به یک سایت حساس لاگین کردهاید، سایتهای دیگر را مرور نکنید. البته مرورگرهای جدید مقاوم به این مسایل شدهاند ولی جانب احتیاط را باید رعایت کرد.
- در برنامه خود قسمت Referrer header را بررسی کنید. آیا متد POST رسیده، از سایت شما صادر شده است یا اینکه صفحهای دیگر در سایتی دیگر جعل شده و به برنامه شما ارسال شده است؟ هر چند این روش آنچنان قوی نیست و فایروالهای جدید یا حتی بعضی از مرورگرها با افزونههایی ویژه، امکان عدم ارسال این قسمت از header درخواست را میسر میسازند.
- برنامه نویسها نباید مقادیر حساس را از طریق GET requests ارسال کنند. استفاده از روش POST نیز به تنهایی کارآمد نیست و آنرا باید با random tokens ترکیب کرد تا امکان جعل درخواست منتفی شود. برای مثال استفاده از ViewStateUserKey در ASP.Net . جهت خودکار سازی اعمال این موارد در ASP.Net، اخیرا HTTP ماژول زیر ارائه شده است:
تنها کافی است که فایل dll آن در دایرکتوری bin پروژه شما قرار گیرد و در وب کانفیگ برنامه ارجاعی به این ماژول را لحاظ نمائید.
کاری که این نوع ماژولها انجام میدهند افزودن نشانههایی اتفاقی ( random tokens ) به صفحه است که مرورگر آنها را بخاطر نمیسپارد و این token به ازای هر سشن و صفحه منحصر بفرد خواهد بود.
برای PHP نیز چنین تلاشهایی صورت گرفته است:
http://csrf.htmlpurifier.org/
مراجعی برای مطالعه بیشتر
Prevent Cross-Site Request Forgery (CSRF) using ASP.NET MVC’s AntiForgeryToken() helper
Cross-site request forgery
Top 10 2007-Cross Site Request Forgery
CSRF - An underestimated attack method
آشنایی با NHibernate - قسمت دوم
- ضمن تشکر از لطف شما، بنده استاد نیستم. یک سری مطلب رو از این طرف اون طرف پیدا میکنم و با هم تقسیم میکنیم. فقط همین و لطفا این لفظ رو دیگر بکار نبرید.
- خیر. میشد برای آزمایش یک برنامه کنسول هم نوشت. اما دیگر مرسوم نیست. بجای استفاده از یک برنامه کنسول، آزمایش واحد بنویسید. هم روشی است استاندارد، هم به عنوان مستندات نحوه استفاده از متدهای پروژه میتونه مورد استفاده قرار بگیره، هم سبب میشه کد بهتری بنویسید چون مجبور خواهید شد در هم تنیدگی کدهای خودتون رو برای متد تست نوشتن کمتر کنید و هم .... در مقالات مربوطه (تگ unit test سمت راست صفحه) مابقی مزایا، نحوه تولید استفاده و غیره را لطفا مطالعه کنید.
ASP.NET MVC #12
تولید خودکار فرمهای ورود و نمایش اطلاعات در ASP.NET MVC بر اساس اطلاعات مدلها
در الگوی MVC، قسمت M یا مدل آن یک سری ویژگیهای خاص خودش را دارد:
شما را وادار نمیکند که مدل را به نحو خاصی طراحی کنید. شما را مجبور نمیکند که کلاسهای مدل را برای نمونه همانند کلاسهای کنترلرها، از کلاس خاصی به ارث ببرید. یا حتی در مورد نحوهی دسترسی به دادهها نیز، نظری ندارد. به عبارتی برنامه نویس است که میتواند بر اساس امکانات مهیای در کل اکوسیستم دات نت، در این مورد آزادانه تصمیم گیری کند.
بر همین اساس ASP.NET MVC یک سری قرارداد را برای سهولت اعتبار سنجی یا تولید بهتر رابط کاربری بر اساس اطلاعات مدلها، فراهم آورده است. این قراردادها هم چیزی نیستند جز یک سری metadata که نحوهی دربرگیری اطلاعات را در مدلها توضیح میدهند. برای دسترسی به آنها پروژه جاری باید ارجاعی را به اسمبلیهای System.ComponentModel.DataAnnotations.dll و System.Web.Mvc.dll داشته باشد (که VS.NET به صورت خودکار در ابتدای ایجاد پروژه اینکار را انجام میدهد).
یک مثال کاربردی
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. در پوشه مدلهای آن، مدل اولیهای را با محتوای زیر ایجاد نمائید:
using System;
namespace MvcApplication8.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
public decimal Salary { set; get; }
public string Address { set; get; }
public bool IsMale { set; get; }
public DateTime AddDate { set; get; }
}
}
سپس یک کنترلر جدید را هم به نام EmployeeController با محتوای زیر به پروژه اضافه نمائید:
using System;
using System.Web.Mvc;
using MvcApplication8.Models;
namespace MvcApplication8.Controllers
{
public class EmployeeController : Controller
{
public ActionResult Create()
{
var employee = new Employee { AddDate = DateTime.Now };
return View(employee);
}
}
}
بر روی متد Create کلیک راست کرده و یک View ساده را برای آن ایجاد نمائید. سپس محتوای این View را به صورت زیر تغییر دهید:
@model MvcApplication8.Models.Employee
@{
ViewBag.Title = "Create";
}
<h2>Create An Employee</h2>
@using (Html.BeginForm(actionName: "Create", controllerName: "Employee"))
{
@Html.EditorForModel()
<input type="submit" value="Save" />
}
اکنون اگر پروژه را اجرا کرده و مسیر http://localhost/employee/create را وارد نمائید، یک صفحه ورود اطلاعات تولید شده به صورت خودکار را مشاهده خواهید کرد. متد Html.EditorForModel بر اساس اطلاعات خواص عمومی مدل، یک فرم خودکار را تشکیل میدهد.
البته فرم تولیدی به این شکل شاید آنچنان مطلوب نباشد، از این جهت که برای مثال Id را هم لحاظ کرده، در صورتیکه قرار است این Id توسط بانک اطلاعاتی انتساب داده شود و نیازی نیست تا کاربر آنرا وارد نماید. یا مثلا برچسب AddDate نباید به این شکل صرفا بر اساس نام خاصیت متناظر با آن تولید شود و مواردی از این دست. به عبارتی نیاز به سفارشی سازی کار این فرم ساز توکار ASP.NET MVC وجود دارد که ادامه بحث جاری را تشکیل خواهد داد.
سفارشی سازی فرم ساز توکار ASP.NET MVC با کمک Metadata خواص
برای اینکه بتوان نحوه نمایش فرم خودکار تولید شده را سفارشی کرد، میتوان از یک سری attribute و data annotations توکار دات نت و ASP.NET MVC استفاده کرد و نهایتا این metadata توسط فریم ورک، مورد استفاده قرار خواهند گرفت. برای مثال:
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace MvcApplication8.Models
{
public class Employee
{
//[ScaffoldColumn(false)]
[HiddenInput(DisplayValue=false)]
public int Id { set; get; }
public string Name { set; get; }
[DisplayName("Annual Salary ($)")]
public decimal Salary { set; get; }
public string Address { set; get; }
[DisplayName("Is Male?")]
public bool IsMale { set; get; }
[DisplayName("Start Date")]
[DataType(DataType.Date)]
public DateTime AddDate { set; get; }
}
}
در اینجا به کمک ویژگی HiddenInput از نمایش عمومی خاصیت Id جلوگیری خواهیم کرد یا توسط ویژگی DisplayName، برچسب دلخواه خود را به عناصر فرم تشکیل شده، انتساب خواهیم داد. اگر نیاز باشد تا خاصیتی کلا از رابط کاربری حذف شود میتوان از ویژگی ScaffoldColumn با مقدار false استفاده کرد. یا توسط DataType، مشخص کردهایم که نوع ورودی فقط قرار است Date باشد و نیازی به قسمت Time آن نداریم.
DataType شامل نوعهای از پیش تعریف شده دیگری نیز هست. برای مثال اگر نیاز به نمایش TextArea بود از مقدار MultilineText، استفاده کنید:
[DataType(DataType.MultilineText)]
یا برای نمایش PasswordBox از مقدار Password میتوان کمک گرفت. اگر نیاز دارید تا آدرس ایمیلی به شکل یک لینک mailto نمایش داده شود از مقدار EmailAddress استفاده کنید. به کمک مقدار Url، متن خروجی به صورت خودکار تبدیل به یک آدرس قابل کلیک خواهد شد.
اکنون اگر پروژه را مجددا کامپایل کنیم و به آدرس ایجاد یک کارمند جدید مراجعه نمائیم، با رابط کاربری بهتری مواجه خواهیم شد.
سفارشی سازی ظاهر فرم ساز توکار ASP.NET MVC
در ادامه اگر بخواهیم ظاهر این فرم را اندکی سفارشیتر کنیم، بهتر است به سورس صفحه تولیدی در مرورگر مراجعه کنیم. در اینجا یک سری عناصر HTML محصور شده با div را خواهیم یافت. هر کدام از اینها هم با classهای css خاص خود تعریف شدهاند. بنابراین اگر علاقمند باشیم که رنگ و قلم و غیره این موارد تغییر دهیم، تنها کافی است فایل css برنامه را ویرایش کنیم و نیازی به دستکاری مستقیم کدهای برنامه نیست.
انتساب قالبهای سفارشی به خواص یک شیء
تا اینجا در مورد نحوه سفارشی سازی رنگ، قلم، برچسب و نوع دادههای هر کدام از عناصر نهایی نمایش داده شده، توضیحاتی را ملاحظه نمودید.
در فرم تولیدی نهایی، خاصیت bool تعریف شده به صورت خودکار به یک checkbox تبدیل شده است. چقدر خوب میشد اگر امکان تبدیل آن مثلا به RadioButton انتخاب مرد یا زن بودن کارمند ثبت شده در سیستم وجود داشت. برای اصلاح یا تغییر این مورد، باز هم میتوان از متادیتای خواص، جهت تعریف قالبی خاص برای هر کدام از خواص مدل استفاده کرد.
به پوشه Views/Shared مراجعه کرده و یک پوشه جدید به نام EditorTemplates را ایجاد نمائید. بر روی این پوشه کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه باز شده، گزینه «Create as a partial view» را انتخاب نمائید و نام آنرا هم مثلا GenderOptions وارد کنید. همچنین گزینه «Create a strongly typed view» را نیز انتخاب کنید. مقدار Model class را مساوی bool وارد نمائید. فعلا یک hello داخل این صفحه جدید وارد کرده و سپس خاصیت IsMale را به نحو زیر تغییر دهید:
[DisplayName("Gender")]
[UIHint("GenderOptions")]
public bool IsMale { set; get; }
توسط ویژگی UIHint، میتوان یک خاصیت را به یک partial view متصل کرد. در اینجا خاصیت IsMale به partial view ایی به نام GenderOptions متصل شده است. اکنون اگر برنامه را کامپایل و اجرا کرده و آدرس ایجاد یک کارمند جدید را ملاحظه کنید، بجای Checkbox باید یک hello نمایش داده شود.
محتویات این Partial view هم نهایتا به شکل زیر خواهند بود:
@model bool
<p>@Html.RadioButton("", false, !Model) Female</p>
<p>@Html.RadioButton("", true, Model) Male</p>
در اینجا Model که از نوع bool تعریف شده، به خاصیت IsMale اشاره خواهد کرد. دو RadioButton هم برای انتخاب بین حالت زن و مرد تعریف شدهاند.
یا یک مثال جالب دیگر در این زمینه میتواند تبدیل enum به یک Dropdownlist باشد. در این حالت partial view ما شکل زیر را خواهد یافت:
@model Enum
@Html.DropDownListFor(m => m, Enum.GetValues(Model.GetType())
.Cast<Enum>()
.Select(m => {
string enumVal = Enum.GetName(Model.GetType(), m);
return new SelectListItem() {
Selected = (Model.ToString() == enumVal),
Text = enumVal,
Value = enumVal
};
}))
و برای استفاده از آن، از ویژگی زیر میتوان کمک گرفت (مزین کردن خاصیتی از نوع یک enum دلخواه، جهت تبدیل خودکار آن به یک دراپ داون لیست):
[UIHint("Enum")]
سایر متدهای کمکی تولید و نمایش خودکار اطلاعات از روی اطلاعات مدلهای برنامه
متدهای دیگری نیز در ردهی Templated helpers قرار میگیرند. اگر از متد Html.EditorFor استفاده کنیم، از تمام این اطلاعات متادیتای تعریف شده نیز استفاده خواهد کرد. همانطور که در قسمت قبل (قسمت 11) نیز توضیح داده شد، صفحه استاندارد Add view در VS.NET به همراه یک سری قالب تولید فرمهای Create و Edit هم هست که دقیقا کد نهایی تولیدی را بر اساس همین متد تولید میکند.
استفاده از Html.EditorFor انعطاف پذیری بیشتری را به همراه دارد. برای مثال اگر یک طراح وب، طرح ویژهای را در مورد ظاهر فرمهای سایت به شما ارائه دهد، بهتر است از این روش استفاده کنید. اما خروجی نهایی Html.EditorForModel به کمک تعدادی متادیتا و اندکی دستکاری CSS، از دیدگاه یک برنامه نویس بی نقص است!
به علاوه، متد Html.DisplayForModel نیز مهیا است. بجای اینکه کار تولید رابط کاربری اطلاعات نمایش جزئیات یک شیء را انجام دهید، اجازه دهید تا متد Html.DisplayForModel اینکار را انجام دهد. سفارشی سازی آن نیز همانند قبل است و بر اساس متادیتای خواص انجام میشود. در این حالت، مسیر پیش فرض جستجوی قالبهای UIHint آن، Views/Shared/DisplayTemplates میباشد. همچنین Html.DisplayFor نیز جهت کار با یک خاصیت مدل تدارک دیده شده است. البته باید درنظر داشت که استفاده از پوشه Views/Shared اجباری نیست. برای مثال اگر از پوشه Views/Home/DisplayTemplates استفاده کنیم، قالبهای سفارشی تهیه شده تنها جهت Viewهای کنترلر home قابل استفاده خواهند بود.
یکی دیگر از ویژگیهایی که جهت سفارشی سازی نحوه نمایش خودکار اطلاعات میتواند مورد استفاده قرار گیرد، DisplayFormat است. برای مثال اگر مقدار خاصیت در حال نمایش نال بود، میتوان مقدار دیگری را نمایش داد:
[DisplayFormat(NullDisplayText = "-")]
یا اگر علاقمند بودیم که فرمت اطلاعات در حال نمایش را تغییر دهیم، به نحو زیر میتوان عمل کرد:
[DisplayFormat(DataFormatString = "{0:n}")]
مقدار DataFormatString در پشت صحنه در متد string.Format مورد استفاده قرار میگیرد.
و اگر بخواهیم که این ویژگی در حالت تولید فرم ویرایش نیز درنظر گرفته شود، میتوان خاصیت ApplyFormatInEditMode را نیز مقدار دهی کرد:
[DisplayFormat(DataFormatString = "{0:n}", ApplyFormatInEditMode = true)]
بازنویسی قالبهای پیش فرض تولید فرم یا نمایش اطلاعات خودکار ASP.NET MVC
یکی دیگر از قرارداهای بکارگرفته شده در حین استفاده از قالبهای سفارشی، استفاده از نام اشیاء میباشد. مثلا در پوشه Views/Shared/DisplayTemplates، اگر یک Partial view به نام String.cshtml وجود داشته باشد، از این پس نحوه رندر کلیه خواص رشتهای تمام مدلها، بر اساس محتوای فایل String.cshtml مشخص میشود؛ به همین ترتیب در مورد datetime و سایر انواع مهیا.
برای مثال اگر خواستید تمام تاریخهای میلادی دریافتی از بانک اطلاعاتی را شمسی نمایش دهید، فقط کافی است یک فایل datetime.cshtml سفارشی را تولید کنید که Model آن تاریخ میلادی دریافتی است و نهایتا کار این Partial view، رندر تاریخ تبدیل شده به همراه تگهای سفارشی مورد نظر میباشد. در این حالت نیازی به ذکر ویژگی UIHint نیز نخواهد بود و همه چیز خودکار است.
به همین ترتیب اگر نام مدل ما Employee باشد و فایل Partial view ایی به نام Employee.cshtml در پوشه Views/Shared/DisplayTemplates قرار گیرد، متد Html.DisplayForModel به صورت پیش فرض از محتوای این فایل جهت رندر اطلاعات نمایش جزئیات شیء Employee استفاده خواهد کرد.
داخل Partial viewهای سفارشی تعریف شده به کمک خاصیت ViewData.TemplateInfo.FormattedModelValue مقدار نهایی فرمت شده قابل استفاده را فراهم میکند. این مورد هم از این جهت حائز اهمیت است که نیازی نباشد تا ویژگی DisplayFormat را به صورت دستی پردازش کنیم. همچنین اطلاعات ViewData.ModelMetadata نیز دراینجا قابل دسترسی هستند.
سؤال: Partial View چیست؟
همانطور که از نام Partial view برمیآید، هدف آن رندر کردن قسمتی از صفحه است به همراه استفاده مجدد از کدهای تولید رابط کاربری در چندین و چند View؛ چیزی شبیه به User controls در ASP.NET Web forms البته با این تفاوت که Page life cycle و Code behind و سایر موارد مشابه آن در اینجا حذف شدهاند. همچنین از Partial viewها برای به روز رسانی قسمتی از صفحه حین فراخوانیهای Ajaxایی نیز استفاده میشود. مهمترین کاربرد Partial views علاوه بر استفاده مجدد از کدها، خلوت کردن Viewهای شلوغ است جهت سادهتر سازی نگهداری آنها در طول زمان (یک نوع Refactoring فایلهای View محسوب میشوند).
پسوند این فایلها نیز بسته به موتور View مورد استفاده تعیین میشود. برای مثال حین استفاده از Razor، پسوند Partial views همان cshtml یا vbhtml میباشد. یا اگر از web forms view engine استفاده شود، پسوند آنها ascx است (همانند User controls در وب فرمها).
البته چون در حالت استفاده از موتور Razor، پسوند View و Partial viewها یکی است، مرسوم شده است که نام Partial viewها را با یک underline شروع کنیم تا بتوان بین این دو تمایز قائل شد.
اگر این فایلها را در پوشه Views/Shared تعریف کنیم، در تمام Viewها قابل استفاده خواهند بود. اما اگر مثلا در پوشه Views/Home آنهارا قرار دهیم، تنها در Viewهای متعلق به کنترلر Home، قابل بکارگیری میباشند.
Partial views را نیز میتوان strongly typed تعریف کرد و به این ترتیب با مشخص سازی دقیق نوع model آن، علاوه بر بهرهمندی از Intellisense خودکار، رندر آنرا نیز تحت کنترل کامپایلر قرار داد.
مقدار Model در یک View بر اساس اطلاعات مدلی که به آن ارسال شده است تعیین میگردد. اما در یک Partial view که جزئی از یک View را نهایتا تشکیل خواهد داد، بر اساس مقدار ارسالی از طریق View معین میگردد.
یک مثال
در ادامه قصد داریم کد حلقه نمایش لیستی از عناصر تولید شده توسط VS.NET را به یک Partial view منتقل و Refactor کنیم.
ابتدا یک منبع داده فرضی زیر را در نظر بگیرید:
using System;
using System.Collections.Generic;
namespace MvcApplication8.Models
{
public class Employees
{
public IList<Employee> CreateEmployees()
{
return new[]
{
new Employee { Id = 1, AddDate = DateTime.Now.AddYears(-3), Name = "Emp-01", Salary = 3000},
new Employee { Id = 2, AddDate = DateTime.Now.AddYears(-2), Name = "Emp-02", Salary = 2000},
new Employee { Id = 3, AddDate = DateTime.Now.AddYears(-1), Name = "Emp-03", Salary = 1000}
};
}
}
}
سپس از آن در یک کنترلر برای بازگشت لیستی از کارکنان استفاده خواهیم کرد:
public ActionResult EmployeeList()
{
var list = new Employees().CreateEmployees();
return View(list);
}
View متناظر با این متد را هم با کلیک راست بر روی متد، انتخاب گزینه Add view و سپس ایجاد یک strongly typed view از نوع کلاس Employee، ایجاد خواهیم کرد.
در ادامه قصد داریم بدنه حلقه زیر را refactor کنیم و آنرا به یک Parial view منتقل نمائیم تا View ما اندکی خلوتتر و مفهومتر شود:
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Salary)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Html.DisplayFor(modelItem => item.IsMale)
</td>
<td>
@Html.DisplayFor(modelItem => item.AddDate)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
@Html.ActionLink("Details", "Details", new { id=item.Id }) |
@Html.ActionLink("Delete", "Delete", new { id=item.Id })
</td>
</tr>
}
سپس بر روی پوشه Views/Employee کلیک راست کرده و گزینه Add|View را انتخاب کنید. در اینجا نام _EmployeeItem را وارد کرده و همچنین گزینه Create as a partial view و create a strongly typed view را نیز انتخاب کنید. نوع مدل هم Employee خواهد بود. به این ترتیب فایل زیر تشکیل خواهد شد:
\Views\Employee\_EmployeeItem.cshtml
ابتدای نام فایلرا با underline شروع کردهایم تا بتوان بین Viewها و Partial views تفاوت قائل شد. همچنین این Partial view چون داخل پوشه Employee تعریف شده، فقط در Viewهای کنترلر Employee در دسترس خواهد بود.
در ادامه کل بدنه حلقه فوق را cut کرده و در این فایل جدید paste نمائید. مرحله اول refactoring یک view به همین نحو آغاز میشود. البته در این حالت قادر به استفاده از Partial view نخواهیم بود چون اطلاعاتی که به این فایل ارسال میگردد و مدلی که در دسترس آن است از نوع Employee است و نه لیستی از کارمندان. به همین جهت باید item را با Model جایگزین کرد:
@model MvcApplication8.Models.Employee
<tr>
<td>
@Html.DisplayFor(x => x.Name)
</td>
<td>
@Html.DisplayFor(x => x.Salary)
</td>
<td>
@Html.DisplayFor(x => x.Address)
</td>
<td>
@Html.DisplayFor(x => x.IsMale)
</td>
<td>
@Html.DisplayFor(x => x.AddDate)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id = Model.Id }) |
@Html.ActionLink("Details", "Details", new { id = Model.Id }) |
@Html.ActionLink("Delete", "Delete", new { id = Model.Id })
</td>
</tr>
سپس برای استفاده از این Partial view در صفحه نمایش لیست کارمندان خواهیم داشت:
@foreach (var item in Model) {
@Html.Partial("_EmployeeItem", item)
}
متد Html.Partial، اطلاعات یک Partial view را پردازش و تبدیل به یک رشته کرده و در اختیار Razor قرار میدهد تا در صفحه نمایش داده شود. پارامتر اول آن نام Partial view مورد نظر است (نیازی به ذکر پسوند فایل نیست) و پارامتر دوم، اطلاعاتی است که به آن ارسال خواهد شد.
متد دیگری هم وجود دارد به نام Html.RenderPartial. کار این متد نوشتن مستقیم در Response است، برخلاف Html.Partial که فقط یک رشته را بر میگرداند.
نمایش اطلاعات از کنترلرهای مختلف در یک صفحه
Html.Partial بر اساس اطلاعات مدل ارسالی از یک کنترلر، کار رندر قسمتی از آنرا در یک View خاص عهده دار خواهد شد. اما اگر بخواهیم مثلا در یک صفحه یک قسمت را به نمایش آخرین اخبار و یک قسمت را به نمایش آخرین وضعیت آب و هوا اختصاص دهیم، از روش دیگری به نام RenderAction میتوان کمک گرفت. در اینجا هم دو متد Html.Action و Html.RenderAction وجود دارند. اولی یک رشته را بر میگرداند و دومی اطلاعات را مستقیما در Response درج میکند.
یک مثال:
کنترلر جدیدی را به نام MenuController به پروژه اضافه کنید:
using System.Web.Mvc;
namespace MvcApplication8.Controllers
{
public class MenuController : Controller
{
[ChildActionOnly]
public ActionResult ShowMenu(string options)
{
return PartialView(viewName: "_ShowMenu", model: options);
}
}
}
سپس بر روی نام متد کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا قصد داریم یک partial view که نامش با underline شروع میشود را اضافه کنیم. مثلا با محتوای زیر ( با توجه به اینکه مدل ارسالی از نوع رشتهای است):
@model string
<ul>
<li>
@Model
</li>
</ul>
حین فراخوانی متد Html.Action، یک متد در یک کنترلر فراخوانی خواهد شد (که شامل ارائه درخواست و طی سیکل کامل پردازشی آن کنترلر نیز خواهد بود). سپس آن متد با بازگشت دادن یک PartialView، اطلاعات پردازش شده یک partial view را به فراخوان بازگشت میدهد. اگر نامی ذکر نشود، همان نام متد در نظر گرفته خواهد شد. البته از آنجائیکه در این مثال در ابتدای نام Partial view یک underline قرار دادیم، نیاز خواهد بود تا این نام صریحا ذکر گردد (چون دیگر هم نام متد یا ActionName آن نیست). ویژگی ChildActionOnly سبب میشود تا این متد ویژه تنها از طریق فراخوانی Html.Action در دسترس باشد.
برای استفاده از آن هم در Viewایی دیگر خواهیم داشت:
@Html.Action(actionName: "ShowMenu", controllerName: "Menu",
routeValues: new { options = "some data..." })
در اینجا هم پارامتر ارسالی به کمک anonymously typed objects مشخص و مقدار دهی شده است.
سؤال مهم: چه تفاوتی بین RenderPartial و RenderAction وجود دارد؟ به نظر هر دو یک کار را انجام میدهند، هر دو مقداری HTML را پس از پرداش به صفحه تزریق میکنند.
پاسخ: اگر View والد، دارای کلیه اطلاعات لازم جهت نمایش اطلاعات Partial view است، از RenderPartial استفاده کنید. به این ترتیب برخلاف حالت RenderAction درخواست جدیدی به ASP.NET Pipeline صادر نشده و کارآیی نهایی بهتر خواهد بود. صرفا یک الحاق ساده به صفحه انجام خواهد شد.
اما اگر برای رندر کردن این قسمت از صفحه که قرار است اضافه شود، نیاز به دریافت اطلاعات دیگری خارج از اطلاعات مهیا میباشد، از روش RenderAction استفاده کنید. برای مثال اگر در صفحه جاری قرار است لیست پروژهها نمایش داده شود و در کنار صفحه مثلا منوی خاصی باید قرار گیرد، اطلاعات این منو در View جاری فراهم نیست (و همچنین مرتبط به آن هم نیست). بنابراین از روش RenderAction برای حل این مساله میتوان کمک گرفت.
به صورت خلاصه برای نمایش اطلاعات تکراری در صفحات مختلف سایت در حالتیکه این اطلاعات از قسمتهای دیگر صفحه ایزوله است (مثلا نمایش چند ویجت مختلف در صفحه)، روش RenderAction ارجحیت دارد.
یک نکته
فراخوانی متدهای RenderAction و RenderPartial در حین کار با Razor باید به شکل فراخوانی یک متد داخل {} باشند:
@{ Html.RenderAction("About"); }
And not @Html.RenderAction("About")
علت این است که @ به تنهایی به معنای نوشتن در Response است. متد RenderAction هم خروجی ندارد و مستقیما در Response اطلاعات خودش را درج میکند. بنابراین این دو با هم همخوانی ندارند و باید به شکل یک متد معمولی با آن رفتار کرد.
اگر حجم اطلاعاتی که قرار است در صفحه درج شود بالا است، متدهای RenderAction و RenderPartial نسبت به Html.Action و Html.Partial کارآیی بهتری دارند؛ چون یک مرحله تبدیل کل اطلاعات به رشته و سپس درج نتیجه در Response، در آنها حذف شده است.