نظرات مطالب
Angular Material 6x - قسمت اول - افزودن آن به برنامه
جهت اطلاع
- این پروژه به Angular Material 9x ارتقاء داده شده؛ جزئیات بیشتر
- کلیات نهایی آن با مطالب این سری یکی است. فقط روش importهای آن تغییر کرده که در سورس نهایی قابل مشاهده‌است.
مطالب
ASP.NET Web API - قسمت سوم
در قسمت اول به دلایل ایجاد Web API پرداخته شد و در قسمت دوم مثالی ساده از Web API را بررسی کردیم. در این قسمت، مثال قبل را تست کرده و نحوه‌ی تعامل jQuery با آن را بررسی می‌کنیم.

فراخوانی 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}]
اما مرورگر Firefox، محصولات را در قالب XML نشان می‌دهد (شکل ذیل).


دلیل تفاوت در نتیجه‌ی دریافتی این است که مرورگر 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 آشنا می‌شوید.

مطالب
React 16x - قسمت 27 - احراز هویت و اعتبارسنجی کاربران - بخش 2 - استخراج و نمایش اطلاعات JWT و خروج از سیستم
در قسمت قبل، در هر دو حالت ثبت نام یک کاربر جدید و همچنین ورود به سیستم، یک JSON Web Token را از سرور دریافت کرده و در local storage مرورگر، ذخیره کردیم. اکنون قصد داریم محتوای این توکن را استخراج کرده و از آن جهت نمایش اطلاعات کاربر وارد شده‌ی به سیستم، استفاده کنیم. همچنین کار بهبود کیفیت کدهایی را هم که تاکنون پیاده سازی کردیم، انجام خواهیم داد.


نگاهی به محتوای 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"
}
در اینجا یک‌سری از اطلاعات کاربر، مانند id ، name ، DisplayName و یا حتی role او درج شده‌است؛ به همراه تاریخ صدور (iat) و انقضای (exp) این token که به صورت Unix time format بیان می‌شوند. به هر کدام از این خواصی که در اینجا ذکر شده‌اند، یک user claim گفته می‌شود. به عبارتی، این token ادعا می‌کند (claims) که نقش کاربر وارد شده‌ی به سیستم، Admin است. برای بررسی صحت این ادعا نیز یک امضای دیجیتال (مشخص شده‌ی با رنگ آبی) را به همراه این توکن سه قسمتی (قسمت‌های مختلف آن، با 2 نقطه از هم جدا شده‌اند که در تصویر نیز با سه رنگ متمایز، مشخص است)، ارائه کرده‌است. به این معنا که اگر قسمتی از اطلاعات این توکن، در سمت کاربر دستکاری شود، دیگر در سمت سرور تعیین اعتبار مجدد نخواهد شد؛ چون نیاز به یک امضای دیجیتال جدید را دارد که کلیدهای خصوصی تولید آن، تنها در سمت سرور مهیا هستند و به سمت کلاینت ارسال نمی‌شوند.


استخراج اطلاعات کاربر وارد شده‌ی به سیستم، از 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 = {};
- سپس متد componentDidMount را به این کامپوننت اضافه می‌کنیم؛ در آن ابتدا token ذخیره شده‌ی در local storage را دریافت کرده و سپس decode می‌کنیم تا payload اطلاعات کاربر وارد شده‌ی به سیستم را استخراج کنیم. در آخر state را توسط این اطلاعات به روز می‌کنیم.
- برای decode کردن توکن، نیاز به نصب کتابخانه‌ی زیر را داریم:
> npm install --save jwt-decode
- پس از نصب آن، ابتدا امکانات آن‌را import کرده و سپس از آن در متد componentDidMount استفاده می‌کنیم:
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);
    }
  }
ابتدا آیتمی با کلید token از localStorage استخراج می‌شود. سپس توسط متد jwtDecode، تبدیل به یک شیء حاوی اطلاعات کاربر جاری وارد شده‌ی به سیستم گشته و در آخر در state درج می‌شود. در اینجا درج try/catch ضروری است؛ از این جهت که متد jwtDecode، در صورت برخورد به توکنی غیرمعتبر، یک استثناء را صادر می‌کند و این استثناء نباید بارگذاری برنامه را با اخلال مواجه کند. از این جهت که اگر توکنی غیرمعتبر است (و یا حتی در localStorage وجود خارجی ندارد؛ برای کاربران لاگین نشده)، کاربر باید مجددا برای دریافت نمونه‌ی معتبر آن، لاگین کند.

- اکنون می‌توان شیء 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 }) => {
چون این کامپوننت به صورت یک کامپوننت تابعی بدون حالت تعریف شده، برای دریافت props می‌توان یا آن‌را به صورت مستقیم به عنوان پارامتر تعریف کرد و یا خواص مدنظر را با استفاده از Object Destructuring به عنوان پارامتر دریافت نمود.
سپس می‌توان لینک‌های 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>
)}
در اینجا اگر شیء user تعریف شده باشد (یعنی کاربر، توکن ذخیره شده‌ای در local storage داشته باشد)، دیگر لینک‌های login و register نمایش داده نمی‌شوند. به علاوه برای اعمال && به چند المان React، نیاز است آن‌ها را داخل یک والد، مانند 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>
)}
user.DisplayName درج شده‌ی در اینجا، اطلاعات خودش را از payload توکن decode شده‌ی دریافتی از سرور، تامین می‌کند؛ با این خروجی:


فعلا تا پیش از پیاده سازی Logout، برای آزمایش آن، به کنسول توسعه دهندگان مرورگر مراجعه کرده و توکن ذخیره شده‌ی در ذیل قسمت application->storage را دستی حذف کنید. سپس صفحه را ریفرش کنید. اینبار لینک‌های به Login و Register نمایان می‌شوند.
یک مشکل! در این حالت (زمانیکه توکن حذف شده‌است)، از طریق قسمت Login به برنامه وارد شوید. هرچند این قسمت‌ها به درستی کار خود را انجام می‌دهند، اما هنوز در منوی بالای سایت، نام کاربری و لینک به Logout ظاهر نشده‌اند. علت اینجا است که در کامپوننت App، کار دریافت توکن در متد componentDidMount انجام می‌شود و این متد نیز تنها یکبار در طول عمر برنامه فراخوانی می‌شود. برای رفع این مشکل به src\components\loginForm.jsx مراجعه کرده و بجای استفاده از history.push برای هدایت کاربر به صفحه‌ی اصلی برنامه، نیاز خواهیم داشت تا کل برنامه را بارگذاری مجدد کنیم. یعنی بجای:
this.props.history.push("/");
باید از سطر زیر استفاده کرد:
window.location = "/";
این سطر سبب full page reload برنامه شده و در نتیجه متد componentDidMount کامپوننت App، یکبار دیگر فراخوانی خواهد شد. شبیه به همین کار را در کامپوننت src\components\registerForm.jsx نیز باید انجام داد.


پیاده سازی Logout کاربر وارد شده‌ی به سیستم

برای logout کاربر تنها کافی است توکن او را از local storage حذف کنیم. به همین جهت مسیریابی جدید logout را که به صورت لینکی به NavBar اضافه کردیم:
<NavLink className="nav-item nav-link" to="/logout">
   Logout
</NavLink>
به فایل src\App.js اضافه می‌کنیم.
import Logout from "./components/logout";
// ...

class App extends Component {
  render() {
    return (
          // ...
          <Switch>
            // ...
            <Route path="/logout" component={Logout} />
البته برای اینکار نیاز است کامپوننت جدید src\components\logout.jsx را با محتوای زیر ایجاد کنیم:
import { Component } from "react";

class Logout extends Component {
  componentDidMount() {
    localStorage.removeItem("token");
    window.location = "/";
  }

  render() {
    return null;
  }
}

export default Logout;
که در متد componentDidMount آن، کار حذف توکن ذخیره شده‌ی در localStorage انجام شده و سپس کاربر را با یک full page reload، به ریشه‌ی سایت هدایت می‌کنیم.


بهبود کیفیت کدهای نوشته شده

اگر به کامپوننت 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);
}
سپس متد doSumbit کامپوننت src\components\loginForm.jsx، به صورت زیر ساده می‌شود:
const { data } = this.state;
await auth.login(data.username, data.password);
window.location = "/";

همین‌کار را برای logout نیز در authService انجام داده:
export function logout() {
  localStorage.removeItem(tokenKey);
}
و در ادامه متد componentDidMount کامپوننت Logout را برای استفاده‌ی از آن، اصلاح می‌کنیم:
import * as auth from "../services/authService";

class Logout extends Component {
  componentDidMount() {
    auth.logout();

منطق دریافت اطلاعات کاربر جاری نیز باید به authService منتقل شود؛ چون مسئولیت دریافت توکن و سپس decode آن، نباید به کامپوننت App واگذار شود:
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;
  }
}
سپس متد componentDidMount کامپوننت App، به صورت زیر خلاصه خواهد شد:
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);
}
سپس ابتدای متد doSumbit را برای استفاده‌ی از آن به صورت زیر تغییر می‌دهیم:
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
مطالب
React 16x - قسمت 11 - طراحی یک گرید - بخش 1 - کامپوننت صفحه بندی
در طی چند قسمت قصد داریم مثال قسمت قبل را که کار نمایش لیستی از فیلم‌ها را انجام می‌دهد، تبدیل به یک کامپوننت گرید کنیم که دارای امکانات صفحه بندی، فیلتر کردن و مرتب سازی اطلاعات است.


بررسی ساختار کامپوننت Pagination

شبیه به کامپوننت Like که در قسمت قبل ایجاد کردیم، می‌خواهیم کامپوننت جدید Pagination نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\pagination.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد می‌کنیم. هرچند می‌توان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت می‌کند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import می‌کنیم:
import Pagination from "./common/pagination";
پس از آن به متد رندر کامپوننت movies مراجعه کرده و پس از بسته شدن tag المان جدول، قصد داریم این کامپوننت صفحه بندی را نمایش دهیم. به همین جهت المان آن‌را در این محل قرار می‌دهیم تا بتوانیم اینترفیس ابتدایی آن‌را پیش از پیاده سازی آن، طراحی کنیم:
<Pagination
  itemsCount={this.state.movies.length}
  pageSize={this.state.pageSize}
  onPageChange={this.handlePageChange}
/>
این کامپوننت نیاز به تعداد کل آیتم‌های فیلم‌ها را دارد؛ به علاوه‌ی اندازه‌ی صفحه، یا همان تعداد ردیفی که قرار است در هر صفحه نمایش داده شود. به این ترتیب کامپوننت صفحه بندی می‌تواند تعداد صفحات مورد نیاز را محاسبه کرده و سپس آن‌ها را رندر کند.
  state = {
    movies: getMovies(),
    pageSize: 4
  };

به همین جهت دو ویژگی itemsCount و pageSize را پیش از هرکاری به تعریف المان صفحه بندی اضافه کرده‌ایم تا داده‌های ورودی آن مشخص شوند. ویژگی itemsCount از تعداد اعضای آرایه‌ی movies حاصل می‌شود و pageSize را به عنوان یک خاصیت جدید شیء منتسب به state تعریف و با عدد 4 مقدار دهی کرده‌ایم.
همچنین در لیست صفحاتی که توسط این کامپوننت رندر می‌شود، باید با کلیک بر روی هر کدام، اطلاعات آن صفحه رندر شود. به همین جهت در اینجا ویژگی onPageChange تعریف شده و به متد جدید handlePageChange در کامپوننت movies، متصل گردیده تا عدد page را دریافت کرده و به آن واکنش نشان دهد:
  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;
  }
}
در اینجا بر اساس ورودی‌ها، تعداد صفحات محاسبه شده و سپس بر اساس آن‌ها آرایه‌ای از این شماره صفحه‌ها تشکیل و بازگشت داده می‌شود. همچنین اگر تعداد صفحات 1 بود، می‌خواهیم این کامپوننت چیزی را رندر نکند. به همین جهت در اینجا null بازگشت داده شده‌است. دلیل استفاده‌ی از Math.ceil که کوچکترین عدد صحیح بزرگتر یا مساوی خروجی را بازگشت می‌دهد، نیز همین مورد است. توسط این متد، خروجی float دریافتی به integer تبدیل شده و سپس قابلیت مقایسه‌ی با 1 را پیدا می‌کند. برای مثال اگر تعداد ردیف‌های صفحه را به 10 تنظیم کنیم، خروجی این تقسیم در این مثال، 0.9 خواهد بود که شرط بررسی pagesCount === 1 را برآورده نمی‌کند. به همین جهت توسط متد Math.ceil، این خروجی به عدد 1 تقریب زده شده و سبب بازگشت نال از این متد خواهد شد.
سپس به کمک متد 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}
/>
این ویژگی نیز مقدار خودش را از state به روز شده دریافت می‌کند:
class Movies extends Component {
  state = {
    movies: getMovies(),
    pageSize: 4,
    currentPage: 1
  };
به روز رسانی state نیز در متد handlePageChange که به ویژگی onPageChange متصل است، بر اساس page دریافتی از کامپوننت صفحه بندی، رخ می‌دهد:
  handlePageChange = page => {
    console.log("handlePageChange", page);
    this.setState({currentPage: page});
  };
بنابراین هرگاه که بر روی یک شماره صفحه در کامپوننت صفحه بندی کلیک می‌شود، رخ‌داد onPageChange متصل به تعریف المان Pagination درج شده‌ی در کامپوننت movies، روی داده و به همراه آن شماره صفحه‌ای، به متد رخ‌دادگران متصل به آن در کامپوننت movies که در اینجا handlePageChange نام دارد، ارسال می‌شود. در این متد state کامپوننت به روز شده و این امر سبب فراخوانی مجدد متد رندر می‌شود که در انتهای آن کامپوننت Pagination درج شده‌است. بنابراین به روز رسانی state، سبب رندر مجدد کامپوننت صفحه بندی با currentPage جدیدی که به آن رسیده‌است، خواهد شد.

پس از این تغییرات، به کامپوننت صفحه بندی مراجعه کرده و بر اساس 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);
  }
اکنون در متد رندر کامپوننت movies، بجای کار با کل آرایه‌ی this.state.movies، آرایه‌ی جدید slice شده را توسط متد paginate دریافت کرده:
  render() {
    const { length: count } = this.state.movies;

    if (count === 0) return <p>There are no movies in the database.</p>;

    const movies = this.paginate();
و سپس در همین متد رندر، فراخوانی this.state.movies.map را به movies.map تغییر می‌دهیم، تا تنها لیست مرتبط با هر صفحه‌ی انتخابی نمایش داده شود.

پس از ذخیره‌ی تغییرات و بارگذاری مجدد برنامه، اکنون می‌توان نمایش صفحه بندی شده‌ی اطلاعات را شاهد بود:



بررسی صحت نوع پارامترهای ارسالی به کامپوننت‌ها

تا اینجا فرض بر این است که مصرف کننده‌ی کامپوننت صفحه بندی، دقیقا همان ویژگی‌هایی را که ما طراحی کرده‌ایم، با همان نام‌ها و همان نوع‌ها را حتما به آن ارسال می‌کند. همچنین اگر افزونه‌ی 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";
سپس در انتهای این فایل، اعتبارسنجی props آن‌را تعریف خواهیم کرد:
Pagination.propTypes = {
  itemsCount: PropTypes.number.isRequired,
  pageSize: PropTypes.number.isRequired,
  currentPage: PropTypes.number.isRequired,
  onPageChange: PropTypes.func.isRequired
};

export default Pagination;
همانطور که مشاهده می‌کنید، در اینجا خاصیت جدید propTypes (دقیقا با همین نگارش؛ در غیراینصورت بررسی نوع‌ها کار نخواهد کرد)، به تعریف کلاس 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

تقویم بسیار زیبایی با طراحی متریال و کاربرد در Vue

تقویم شمسی Vue
مطالب
بررسی خطای Circular References در ASP.NET MVC Json Serialization
خیلی وقت‌ها لازم است تا نتیجه کوئری حاصله را بصورت Json به ویوی مورد نظر ارسال نمایید. برای اینکار کافیست مانند زیر عمل کنیم
[HttpGet]
public JsonResult Get(int id)
{
    return Json(repository.Find(id), JsonRequestBehavior.AllowGet);
}
اما اگر کوئری پیچیده و یا یک مدل سلسله مراتبی داشته باشید که با خودش کلید خارجی داشته باشد، هنگام تبدیل نتایج به خروجی Json، با خطای Circular References مواجه می‌شوید.
A circular reference was detected while serializing an object of type ‘System.Data.Entity.DynamicProxies.ItemCategory_A79…’
علت این مشکل این است که Json Serialization پش فرض ASP.NET MVC فقط یک سطح پایین‌تر را لود می‌کند و در مدل‌های که خاصیتی از نوع خودشان داشته باشند خطای Circular References را فرا می‌خواند. کلاس نمونه در زیر آوره شده است.
    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; }
    }

راه حل:
چندین راه حل برای رفع این خطا وجود دارد؛ یکی استفاده از  Automapper و راه حل دیگر استفاده از کتابخانه‌های‌های قوی‌تر کار بار Json مثل Json.net است. اما راه حل ساده‌تر تبدیل خروجی کوئری به یک شی بی نام و سپس تبدیل به Json می‌باشد
[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);
}
همین طور که در مثال بالا مشاهده می‌نمایید ابتدا همه رکورد‌ها در متغییر data ریخته شده و سپس با یک کوئری دیگر که در آن دوباره از پروپرتی items که از نوع کلاس item می‌باشد شی بی نامی ایجاد نموده ایم. با این کار براحتی این خطا رفع می‌گردد.