ng add @angular/material
فراخوانی Web API از طریق مرورگر
با فشردن کلید F5، پروژه را اجرا کنید. شکل ذیل ظاهر میشود.
صفحه ای که ظاهر میشود، یک View است که توسط HomeController و متد Index آن برگشت داده شده است. برای فراخوانی متدهای موجود در کلاس Controller مثال قسمت قبل که مربوط به Web API است، باید به یکی از آدرسهای اشاره شده در قسمت قبل برویم. به عنوان مثال، برای به دست آوردن لیست تمامی محصولات، به آدرس http://localhost:xxxx/api/products بروید. xxxx، شمارهی پورتی است که Web Server داخلی Visual Studio در هنگام اجرای پروژه به آن اختصاص میدهد. آن را نسبت به پروژهی خود تغییر دهید.
نتیجهی دریافتی بستگی به نوع مرورگری دارد که استفاده میکنید. Internet Explorer از شما در مورد باز کردن یا ذخیرهی فایلی با نام products پرسش میکند (شکل ذیل).
محتوای فایل، بدنهی پاسخ دریافتی است. اگر این فایل را باز کنید، خواهید دید که که محتوای آن، لیستی از محصولات با فرمت JSON مانند ذیل است.
[{"Id":1,"Name":"Tomato soup","Category":"Groceries","Price":1.39},{"Id":2,"Name": "Yo-yo","Category":"Toys","Price":3.75},{"Id":3,"Name":"Hammer","Category": "Hardware","Price":16.99}]
دلیل تفاوت در نتیجهی دریافتی این است که مرورگر Internet Explorer و Firefox، هر یک مقدار متفاوتی را در هدر Accept درخواست، ارسال میکنند. بنابراین، Web API نیز مقدار متفاوتی را در پاسخ برگشت میدهد.
حال به آدرسهای ذیل بروید:
http://localhost:xxxx/api/products/1
http://localhost:xxxx/api/products?category=hardware
اولین آدرس، باید محصولی با مشخصهی 1 را برگشت دهد و دومین آدرس، لیستی از تمامی محصولاتی که در دستهی hardware قرار دارند را برگشت میدهد (در مثال ما فقط یک آیتم این شرط را دارد).
نکته: در صورتی که در هنگام فراخوانی هر یک از متدهای Web API با خطای ذیل مواجه شدید، دستور [("AcceptVerbs("GET", "POST] را به ابتدای متدها اضافه کنید.
The requested resource does not support http method 'GET'
فراخوانی Web API با استفاده از کتابخانهی jQuery
در قسمت قبل، متدهای Web API را مستقیماً از طریق وارد کردن آدرس آنها در نوار آدرس مرورگر فراخوانی کردیم. اما در اکثر اوقات، این متدها با روشهای برنامه نویسی توسط یک Client فراخوانی میشوند. اجازه بدهید Clientیی ایجاد کنیم که با استفاده از jQuery، متدهای ما را فراخوانی میکند.
در Solution Explorer، از پوشهی Views و سپس Home، فایل Index.cshtml را باز کنید.
تمامی محتویات این View را حذف و کدهای ذیل را در آن قرار دهید.
<!DOCTYPE html> <html> <head> <title>ASP.NET Web API</title> <script src="../../Scripts/jquery-1.7.2.min.js" type="text/javascript"></script> </head> <body> <div> <h1>All Products</h1> <ul id='products' /> </div> <div> <label for="prodId">ID:</label> <input type="text" id="prodId" size="5"/> <input type="button" value="Search" onclick="find();" /> <p id="product" /> </div> </body> </html>
بازیابی لیستی از محصولات
برای بازیابی لیستی از محصولات، فقط کافی است تا یک درخواست از نوع GET به آدرس "/api/products" بفرستید. این کار با jQuery به صورت ذیل انجام میشود.
<script type="text/javascript"> $(document).ready(function () { // Send an AJAX request $.getJSON("api/products/", function (data) { // On success, 'data' contains a list of products. $.each(data, function (key, val) { // Format the text to display. var str = val.Name + ': $' + val.Price; // Add a list item for the product. $('<li/>', { html: str }) .appendTo($('#products')); }); }); }); </script>
متد getJSON، یک درخواست AJAX از نوع GET را ارسال میکند و پاسخ دریافتی آن نیز با فرمت JSON خواهد بود. دومین پارامتر متد getJSON، یک callback است که پس از دریافت موفقیت آمیز پاسخ اجرا میشود.
بازیابی یک محصول با استفاده از مشخصهی آن
برای بازیابی یک محصول با استفاده از مشخصهی آن، یک درخواست از نوع GET به آدرس "api/products/id/" ارسال کنید. id، مشخصهی محصول است. کد ذیل را در ادامهی کد قبل و پیش از تگ <script/> قرار دهید.
function find() { var id = $('#prodId').val(); $.getJSON("api/products/" + id, function (data) { var str = data.Name + ': $' + data.Price; $('#product').html(str); }) .fail( function (jqXHR, textStatus, err) { $('#product').html('Error: ' + err); }); }
باز هم از متد getJSON استفاده کردیم، اما این بار مقدار id برای آدرس از یک Text Box خوانده و آدرس ایجاد میشود. پاسخ دریافتی، یک محصول در قالب JSON است.
اجرای پروژه
پروژه را با فشردن کلید F5 اجرا کنید. پس از نمایش فرم، تمامی محصولات بر روی صفحه نمایش داده میشوند. عدد 1 را وارد و بر روی دکمهی Search کلیک کنید، محصولی که مشخصهی آن 1 است نمایش داده میشود (شکل ذیل).
اگر مشخصه ای را وارد کنید که وجود ندارد، خطای 404 با مضمون "Error: Not Found" بر روی صفحه نمایش داده میشود و در صورتی که به جای عدد، عبارتی غیر عددی وارد کنید، خطای 400 با مضمون: "Error: Bad Request" نمایش داده میشود. در Web API، تمامی پاسخها باید در قالب کدهای وضعیت HTTP باشند (شکل ذیل). این یکی از اصول اساسی کار با وب سرویسها است. وفادار ماندن به مفاهیم پایهی وب، دید بهتری در مورد اتفاقاتی که میافتد به شما میدهد.
در قسمت بعد با مفهوم مسیریابی در ASP.NET Web API آشنا میشوید.
نگاهی به محتوای JSON Web Token تولیدی
اگر مطلب قسمت قبل را پیگیری کرده باشید، پس از لاگین، یک چنین خروجی را در کنسول توسعه دهندگان مرورگر میتوان مشاهده کرد که همان return Ok(new { access_token = jwt }) دریافتی از سمت سرور است:
اکنون این رشتهی طولانی را در حافظه کپی کرده و سپس به سایت https://jwt.io/#debugger-io مراجعه و در قسمت دیباگر آن، این رشتهی طولانی را paste میکنیم تا آنرا decode کند:
برای نمونه payload آن حاوی یک چنین اطلاعاتی است:
{ "jti": "b2921057-32a4-fbb2-0c18-5889c1ab8e70", "iss": "https://localhost:5001/", "iat": 1576402824, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid N.", "DisplayName": "Vahid N.", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1576402824, "exp": 1576402944, "aud": "Any" }
استخراج اطلاعات کاربر وارد شدهی به سیستم، از JSON Web Token دریافتی
همانطور که در payload توکن دریافتی از سرور نیز مشخص است، اطلاعات ارزشمندی از کاربر، به همراه آن ارائه شدهاند و مزیت کار با آن، عدم نیاز به کوئری گرفتن مداوم از سرور و بانک اطلاعاتی، جهت دریافت مجدد این اطلاعات است. بنابراین اکنون در برنامهی React خود، قصد داریم مشابه کاری را که سایت jwt.io انجام میدهد، پیاده سازی کرده و به این اطلاعات دسترسی پیدا کنیم و برای مثال DisplayName را در Navbar نمایش دهیم. برای این منظور فایل app.js را گشوده و تغییرات زیر را به آن اعمال میکنیم:
- میخواهیم اطلاعات کاربر جاری را در state کامپوننت مرکزی App قرار دهیم. سپس زمانیکه کار رندر کامپوننت NavBar درج شدهی در متد رندر آن فرا میرسد، میتوان این اطلاعات کاربر را به صورت props به آن ارسال کرد؛ و یا به هر کامپوننت دیگری در component tree برنامه.
- بنابراین ابتدا کامپوننت تابعی بدون حالت App را تبدیل به یک کلاس کامپوننت استاندارد مشتق شدهی از کلاس پایهی Component میکنیم. اکنون میتوان state را نیز به آن اضافه کرد:
class App extends Component { state = {};
- برای decode کردن توکن، نیاز به نصب کتابخانهی زیر را داریم:
> npm install --save jwt-decode
import jwtDecode from "jwt-decode"; // ... class App extends Component { state = {}; componentDidMount() { try { const jwt = localStorage.getItem("token"); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); this.setState({ currentUser }); } catch (ex) { console.log(ex); } }
- اکنون میتوان شیء currentUser را به صورت props، به کامپوننت NavBar ارسال کرد:
render() { return ( <React.Fragment> <ToastContainer /> <NavBar user={this.state.currentUser} /> <main className="container">
نمایش اطلاعات کاربر وارد شدهی به سیستم در NavBar
پس از ارسال شیء کاربر به صورت props به کامپوننت src\components\navBar.jsx، کدهای این کامپوننت را به صورت زیر جهت نمایش نام کاربر جاری وارد شدهی به سیستم تغییر میدهیم:
const NavBar = ({ user }) => {
سپس میتوان لینکهای Login و Register را به صورت شرطی رندر کرد و نمایش داد:
{!user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/login"> Login </NavLink> <NavLink className="nav-item nav-link" to="/register"> Register </NavLink> </React.Fragment> )}
شبیه به همین حالت را برای هنگامیکه کاربر، تعریف شدهاست، جهت نمایش نام او و لینک به Logout، نیاز داریم:
{user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink> <NavLink className="nav-item nav-link" to="/profile"> {user.DisplayName} </NavLink> </React.Fragment> )}
فعلا تا پیش از پیاده سازی Logout، برای آزمایش آن، به کنسول توسعه دهندگان مرورگر مراجعه کرده و توکن ذخیره شدهی در ذیل قسمت application->storage را دستی حذف کنید. سپس صفحه را ریفرش کنید. اینبار لینکهای به Login و Register نمایان میشوند.
یک مشکل! در این حالت (زمانیکه توکن حذف شدهاست)، از طریق قسمت Login به برنامه وارد شوید. هرچند این قسمتها به درستی کار خود را انجام میدهند، اما هنوز در منوی بالای سایت، نام کاربری و لینک به Logout ظاهر نشدهاند. علت اینجا است که در کامپوننت App، کار دریافت توکن در متد componentDidMount انجام میشود و این متد نیز تنها یکبار در طول عمر برنامه فراخوانی میشود. برای رفع این مشکل به src\components\loginForm.jsx مراجعه کرده و بجای استفاده از history.push برای هدایت کاربر به صفحهی اصلی برنامه، نیاز خواهیم داشت تا کل برنامه را بارگذاری مجدد کنیم. یعنی بجای:
this.props.history.push("/");
window.location = "/";
پیاده سازی Logout کاربر وارد شدهی به سیستم
برای logout کاربر تنها کافی است توکن او را از local storage حذف کنیم. به همین جهت مسیریابی جدید logout را که به صورت لینکی به NavBar اضافه کردیم:
<NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink>
import Logout from "./components/logout"; // ... class App extends Component { render() { return ( // ... <Switch> // ... <Route path="/logout" component={Logout} />
import { Component } from "react"; class Logout extends Component { componentDidMount() { localStorage.removeItem("token"); window.location = "/"; } render() { return null; } } export default Logout;
بهبود کیفیت کدهای نوشته شده
اگر به کامپوننت App دقت کنید، کلید token استفاده شدهی در آن، در چندین قسمت برنامه مانند login و logout، تکرار و پراکنده شدهاست. بنابراین بهتر است جزئیات پیاده سازی مرتبط با اعتبارسنجی کاربران، به ماژول مختص به آنها (src\services\authService.js) منتقل شود تا سایر قسمتهای برنامه، به صورت یکدستی از آن استفاده کنند و اگر در این بین نیاز به تغییری بود، فقط یک ماژول نیاز به تغییر، داشته باشد.
برای این منظور، ابتدا متد login قبلی را طوری تغییر میدهیم که کار ذخیره سازی توکن را نیز در authService.js انجام دهد:
const tokenKey = "token"; export async function login(email, password) { const { data: { access_token } } = await http.post(apiEndpoint + "/login", { email, password }); console.log("JWT", access_token); localStorage.setItem(tokenKey, access_token); }
const { data } = this.state; await auth.login(data.username, data.password); window.location = "/";
همینکار را برای logout نیز در authService انجام داده:
export function logout() { localStorage.removeItem(tokenKey); }
import * as auth from "../services/authService"; class Logout extends Component { componentDidMount() { auth.logout();
import jwtDecode from "jwt-decode"; //... export function getCurrentUser() { try { const jwt = localStorage.getItem(tokenKey); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); return currentUser; } catch (ex) { console.log(ex); return null; } }
import * as auth from "./services/authService"; class App extends Component { state = {}; componentDidMount() { const currentUser = auth.getCurrentUser(); this.setState({ currentUser }); }
جای دیگری که از localStorage استفاده شده، متد doSumbit کامپوننت ثبت نام کاربران است. این قسمت را نیز به صورت زیر به authService اضافه میکنیم:
export function loginWithJwt(jwt) { localStorage.setItem(tokenKey, jwt); }
import * as auth from "../services/authService"; // ... const response = await userService.register(this.state.data); auth.loginWithJwt(response.headers["x-auth-token"]);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-27-backend.zip و sample-27-frontend.zip
بررسی ساختار کامپوننت Pagination
شبیه به کامپوننت Like که در قسمت قبل ایجاد کردیم، میخواهیم کامپوننت جدید Pagination نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\pagination.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد میکنیم. هرچند میتوان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت میکند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import میکنیم:
import Pagination from "./common/pagination";
<Pagination itemsCount={this.state.movies.length} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} />
state = { movies: getMovies(), pageSize: 4 };
handlePageChange = page => { console.log("handlePageChange", page); };
نمایش شماره صفحات گرید، در کامپوننت صفحه بندی
برای رندر کامپوننت صفحه بندی، از کلاسهای مخصوص اینکار که در بوت استرپ تعریف شدهاند، استفاده میکنیم که ساختار کلی آن به صورت زیر است و از یک المان nav که داخل آن ul ای با کلاس pagination و liهایی با کلاس page-item هستند، تشکیل میشود. هر li، به همراه یک anchor است؛ با کلاس page-link تا لینک به صفحهای خاص را ارائه دهد که در اینجا بجای لینک، از کلیک بر روی آنها استفاده خواهیم کرد:
import React, { Component } from "react"; class Pagination extends Component { render() { return ( <nav> <ul className="pagination"> <li className="page-item"> <a className="page-link">1</a> </li> </ul> </nav> ); } } export default Pagination;
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، عدد 1 را در پایین جدول فیلمها مشاهده خواهید کرد:
اکنون باید رندر این liها را بر اساس ورودیهای این کامپوننت که پیشتر معرفی کردیم، یعنی pageSize و itemsCount، پویا کنیم. به همین جهت نیاز به آرایهای داریم که بر اساس این ورودیها، شمارهی صفحات مانند [1,2,3] را ارائه دهد تا بر روی آن متد Array.map را فراخوانی کرده و liهای مورد نیاز را به صورت پویا رندر کنیم:
class Pagination extends Component { // ... getPageNumbersArray() { const { itemsCount, pageSize } = this.props; const pagesCount = Math.ceil(itemsCount / pageSize); if (pagesCount === 1) { return null; } const pages = new Array(); for (let i = 1; i <= pagesCount; i++) { pages.push(i); } return pages; } }
سپس به کمک متد map، اعضای این آرایه را تبدیل به لیست liهای نمایش شماره صفحات میکنیم. در اینجا key هر li را نیز به شماره صفحه که منحصربفرد است، تنظیم کردهایم:
class Pagination extends Component { render() { const pages = this.getPageNumbersArray(); if (!pages) { return null; } return ( <nav> <ul className="pagination"> {pages.map(page => ( <li key={page} className="page-item"> <a className="page-link">{page}</a> </li> ))} </ul> </nav> ); }
پس از ذخیرهی این کامپوننت و بارگذاری مجدد برنامه در مرورگر، شمارهی صفحات رندر شده، در پایین جدول مشخص هستند:
با داشتن 9 فیلم در آرایهی movies و نمایش 4 فیلم به ازای هر صفحه، به 3 صفحه خواهیم رسید که به درستی در اینجا رندر شدهاست. یکبار هم برای آزمایش بیشتر، مقدار pageSize را در کامپوننت movies به 10 تنظیم کنید. در این حالت کامپوننت صفحه بندی نباید رندر شود.
مدیریت انتخاب شمارههای صفحات
در این قسمت میخواهیم مدیریت onPageChange={this.handlePageChange} را که به تعریف المان صفحه بندی در کامپوننت movies اضافه کردیم، تکمیل کنیم. برای این منظور در کامپوننت صفحه بندی، قسمت anchor را به صورت زیر تغییر میدهیم تا با کلیک بر روی آن، رخداد onPageChange صادر شود:
<a onClick={() => this.props.onPageChange(page)} className="page-link" style={{ cursor: "pointer" }} > {page} </a>
تا اینجا اگر برنامه را آزمایش کنیم، با کلیک بر روی لینکهای شماره صفحات، شماره صفحهی انتخابی، در کنسول توسعه دهندگان مرورگر لاگ میشود.
اکنون میخواهیم اگر صفحهای انتخاب شد، شمارهی آن صفحه با رنگی دیگر نمایش داده شود. در بوت استرپ برای اینکار تنها کافی است کلاس active را به className هر li اضافه کنیم و برعکس. یعنی اگر page ای مساوی صفحهی جاری انتخاب شده بود (currentPage در اینجا)، آنگاه کلاس page-item active، به المان li اضافه شود. بنابراین در این کامپوننت نیاز است عدد currentPage را نیز دریافت کنیم. به همین جهت ویژگی currentPage را به تعریف المان Pagination در کامپوننت movies اضافه میکنیم:
<Pagination itemsCount={this.state.movies.length} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} currentPage={this.state.currentPage} />
class Movies extends Component { state = { movies: getMovies(), pageSize: 4, currentPage: 1 };
handlePageChange = page => { console.log("handlePageChange", page); this.setState({currentPage: page}); };
پس از این تغییرات، به کامپوننت صفحه بندی مراجعه کرده و بر اساس currentPage دریافتی، کلاس active را به المان li اعمال میکنیم:
<li key={page} className={ page === this.props.currentPage ? "page-item active" : "page-item" } >
در اولین بار نمایش برنامه، عدد 1 در حالت انتخاب شده قرار دارد؛ چون مقدار currentPage موجود در state، همان عدد 1 است. پس از آن با کلیک بر روی اعداد دیگر، با به روز رسانی state، مقدار currentPage تغییر کرده و کامپوننت صفحه بندی نسبت به آن واکنش نشان میدهد.
نمایش صفحه بندی شدهی اطلاعات
تا اینجا لیستی که نمایش داده میشود، حاوی تمام اطلاعات آرایهی this.state.movies است و بر اساس شمارهی صفحهی انتخابی، تغییر نمیکند. به همین جهت با استفاده از متد slice، تکهای از آرایهی movies را که بر اساس شماره صفحهی انتخابی و تعداد ردیفها در هر صفحه نیاز است نمایش داده شود، انتخاب کرده و بازگشت میدهیم:
paginate() { const first = (this.state.currentPage - 1) * this.state.pageSize; const last = first + this.state.pageSize; return this.state.movies.slice(first, last); }
render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; const movies = this.paginate();
پس از ذخیرهی تغییرات و بارگذاری مجدد برنامه، اکنون میتوان نمایش صفحه بندی شدهی اطلاعات را شاهد بود:
بررسی صحت نوع پارامترهای ارسالی به کامپوننتها
تا اینجا فرض بر این است که مصرف کنندهی کامپوننت صفحه بندی، دقیقا همان ویژگیهایی را که ما طراحی کردهایم، با همان نامها و همان نوعها را حتما به آن ارسال میکند. همچنین اگر افزونهی eslint را هم در VSCode نصب کرده باشید، به همراه نصب وابستگیهای زیر در خط فرمان:
> npm i -g typescript eslint tslint eslint-plugin-react-hooks jshint babel-eslint eslint-plugin-react eslint-plugin-mocha
به ازای هر خاصیت props استفاده شدهی در کامپوننت صفحه بندی، اخطاری را مانند «'currentPage' is missing in props validation eslint(react/prop-types)» مشاهده خواهید کرد:
که عنوان میکند props validation این خاصیت استفاده شده، فراموش شدهاست.
در نگارشهای قبلی React، امکانات بررسی نوعهای ارسالی به کامپوننتها، جزئی از بستهی اصلی آن بود؛ اما از نگارش 15 به بعد، به بستهی مستقلی منتقل شدهاست که باید به صورت جداگانهای در ریشهی پروژه نصب شود:
> npm i prop-types --save
البته اگر TypeScript را بر روی سیستم خود نصب کرده باشید، دیگر نیازی به نصب بستهی npm فوق را ندارید و prop-types، جزئی از آن است که عموما در یک چنین مسیری قرار دارد و برای کار کردن با آن، تنها ذکر import مرتبط با PropType در ماژولهای برنامه کافی بوده و برنامه در این حالت بدون مشکل کامپایل میشود:
C:/Users/{username}/AppData/Local/Microsoft/TypeScript/3.6/node_modules/@types/prop-types/index
اکنون در ابتدای فایل کامپوننت صفحه بندی، تعریف زیر را اضافه میکنیم:
import PropTypes from "prop-types";
Pagination.propTypes = { itemsCount: PropTypes.number.isRequired, pageSize: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired, onPageChange: PropTypes.func.isRequired }; export default Pagination;
سپس مقدار این خاصیت جدید را به شیءای تنظیم میکنیم که نام خواص آن، دقیقا همان نام خواص و رویدادهای props استفاده شدهی در این کامپوننت است. در ادامه توسط PropTypes ای که در ابتدای ماژول import میشود، کار تعریف نوع این خواص و اجباری بودن آنها را میتوان مشخص کرد که برای مثال در اینجا سه خاصیت تعریف شده از نوع عددی و اجباری بوده و onPageChange، از نوع func است.
پس از اضافه کردن Pagination.propTypes و مقدار دهی آن، خطاهای eslint ای که در تصویر فوق مشاهده کردید، برطرف میشوند. همچنین اگر فراخوان کامپوننت Pagination مثلا بجای itemsCount یک رشتهی فرضی را وارد کند (برای آزمایش آن در کامپوننت movies، در تعریف المان Pagination، مقدار itemsCount را یک رشته وارد کنید)، چنین خطایی در مرورگر ظاهر خواهد شد که عنوان میکند itemsCount یک عدد را میپذیرد و نوع ورودی آن اشتباه است:
البته این خطا فقط در حالت development مشاهده میشود و در حالت توزیع برنامه، خیر.
بنابراین تعریف propTypes یک best practice ایجاد کامپوننتهای React است که نه فقط بررسی نوعها را فعال میکند، بلکه میتواند به عنوان مستندات آن نیز در جهت تعیین props مورد نیاز، همچنین نوع و اجباری بودن آنها، اطلاعات کاملی را ارائه کند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-11.zip
تقویم شمسی Vue
[HttpGet] public JsonResult Get(int id) { return Json(repository.Find(id), JsonRequestBehavior.AllowGet); }
A circular reference was detected while serializing an object of type ‘System.Data.Entity.DynamicProxies.ItemCategory_A79…’
public class Item { public int Id { get; set; } [ForeignKey] public int ItemId { get; set; } public string Name { get; set; } public ICollection<Item> Items { get; set; } }
[HttpGet] public JsonResult List() { var data = repository.AllIncluding(itemcategory => itemcategory.Items); var collection = data.Select(x => new { id = x.Id, name = x.Name, items = x.Items.Select(item => new { id=item.Id, name = item.Name }) }); return Json(collection, JsonRequestBehavior.AllowGet); }