مطالب
مروری بر قابلیت جدید ASP.NET FriendlyUrls
احتمالا به اهمیت بحث SEO و  آدرس‌های تمیز آن آشنا هستید. ASP.NET MVC از همان ابتدا از قابلیت URL Routing پشتیبانی می‌کرد و ASP.NET Web Forms هم از نسخه NET 4.0. این قابلیت را در خود اضافه نمود. با این حال نوشتن Url Routing برای وب سایت‌ها کمی مشکل و در ASP.NET کمتر استفاده می‌شود. علاوه بر آن تغییر آدرس‌های قدیمی با Url Routing بسیار وقت گیر می‌باشد. به همین خاطر تیم توسعه ASP.NET کتابخانه ای با نام Asp.NET FriendlyUrls که فعلا نسخه قبل از نهایی آن منتشر شده است را در اختیار توسعه دهندگان وب سایت‌ها قرار داده‌اند که با حداقل کد نویسی و بدون اضافه نمودن مسیریابی آدرس‌ها، تمام آدرس‌های قدیم و جدید را مطابق استاندارد SEO تغییر می‌دهد.

دریافت و نصب Asp.NET FriendlyUrls از Nuget
در Visual Studio 2012.2 در صورتی که یک پروژه جدید از نوع ASP.NET Web Forms ایجاد نمایید بصورت پیشفرض این کتابخانه نصب شده است. اما می‌توانید با دستور زیر نیز آن را از Nuget دریافت نمایید:
Install-Package Microsoft.AspNet.FriendlyUrls -Pre

پس از نصب، در کلاس Global می‌بایست کتابخانه را فعال نمایید:
public static class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.EnableFriendlyUrls();
        }
    }
و همچنین در Application_Start
void Application_Start(object sender, EventArgs e)
        {
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
به همین راحتی؛ حال دو صفحه First.aspx و Second.aspx را به پروژه اضافه نمائید و وب سایت را اجرا کنید. بدون نوشتن پسوند aspx به صفحات دسترسی خواهید داشت:

از درون برنامه هم به راحتی می‌توانید صفحات را به همان صورت قبل بدون پسوند صدا بزنید:
Response.Redirect("~\\Second");
دسترسی به پارامترها نیز به صورت قبل امکان پذیر می‌باشد:
Response.Redirect("~\\First?Id=1001");

string Id = Request.QueryString["Id"] as string;
برای آدرس‌های پیشرفته‌تر، 3 متود الحاقی اضافه گردیده است. مثلا اگر آدرس زیر را داشته باشیم
http://localhost:57353/First/Products/NewProduct
  1. ()Request.GetFriendlyUrlFileVirtualPath
    مسیر مجازی فایل را بر می‌گرداند

  2. ()Request.GetFriendlyUrlSegments
    بخش‌های آدرس را بر می‌گرداند

  3. ()Request.GetFriendlyUrlFileExtension
    پسوند فایل آدرس را بر می‌گرداند

کلاس FriendlyUrl دو متود استاتیک و یک خاصیت دارد 

  1. FriendlyUrl.Segments
    شبیه ()Request.GetFriendlyUrlSegments مجموعه بخش‌های آدرس صفحه جاری را بر می‌گرداند 
  2. FriendlyUrl.Resolve 
    آدرس صفحه جاری را بر می‌گرداند 
  3. FriendlyUrl.Href
    امکان ساخت یک آدرس را با استفاده از مسیر مجازی، بخش‌ها و پارامتر‌ها، فراهم می‌نماید

  

در آخر، فضای نام کتابخانه، Microsoft.AspNet.FriendlyUrls می‌باشد.
مطالب
استفاده از قالب مخصوص Redux Toolkit جهت ایجاد پروژه‌های React/Redux
استفاده از Redux درون پروژه‌های React، به روش‌های مختلفی قابل انجام است؛ یعنی محدودیتی از لحاظ نحوه چیدمان فایل‌ها، تغییر state و نحوه‌ی dispatch کردن action وجود ندارد. به عبارتی این آزادی عمل را خواهیم داشت تا خودمان سیم‌کشی پروژه را انجام دهیم؛ ولی مشکل اصلی اینجاست که نمی‌توانیم مطمئن شویم روشی که پروژه را با آن ستاپ کرده‌ایم آیا به عنوان یک best-practice محسوب می‌شود یا خیر. در نهایت خروجی را که خواهیم داشت، حجم انبوهی از کدهای boilerplate و پکیج‌های زیادی است که در حین توسعه‌ی پروژه، به همراه Redux اضافه شده‌اند. همچنین در نهایت یک store پیچیده را خواهیم داشت که مدیریت آن به مراتب سخت خواهد شد. یک مشکل دیگر این است که روال گفته شده را باید به ازای هر پروژه‌ی جدید تکرار کنیم. برای حل این مشکل، یکی از maintainerهای اصلی تیم ریداکس، یک پروژه را تحت عنوان Redux Toolkit، مدتها قبل برای حل مشکلات عنوان شده شروع کرده است و این پکیج، جدیداً به قالب رسمی create-react-app اضافه شده است. که در واقع یک روش استاندارد و به اصطلاح opinionated برای ایجاد پروژه‌های ریداکسی می‌باشد و شامل تمامی وابستگی‌های موردنیاز برای کار با Redux از قبیل redux-thunk و همچنین Redux DevTools است. 

 ایجاد یک برنامه‌ی خالی React با قالب redux
در ادامه برای بررسی این قالب جدید، یک پروژه‌ی جدید React را ایجاد خواهیم کرد:
> npx create-react-app redux-template --template redux
> cd redux-template
> yarn start


بررسی ساختار پروژه‌ی ایجاد شده
ساختار پروژه‌ی ایجاد شده به صورت زیر است:


این ساختار خیلی شبیه به قالب پیش‌فرض create-react-app می‌باشد. همانطور که در تصویر فوق نیز مشاهده میکنید، پروژه‌ی ایجاد شده‌ی با قالب redux (تصویر سمت چپ)، یک فایل با نام store و همچنین یک دایرکتوری را به نام features دارد. اگر به فایل store.js مراجعه کنید، خواهید دید که تنظیمات اولیه‌ی ایجاد store را در قالب یک مثال Counter ایجاد کرده‌است:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

export default configureStore({
  reducer: {
    counter: counterReducer,
  },
});
در کد فوق، نحوه‌ی ایجاد store، نسبت به حالت معمول، خیلی تمیزتر است. نکته‌ی جالب این است که به همراه کد فوق، Redux DevTools و همچنین redux-thunk هم از قبل تنظیم شده‌اند و در نتیجه، نیازی به تنظیم و نصب آنها نیست. تغییر مهم دیگر، پوشه‌ی features می‌باشد که یک روش رایج برای گروه‌بندی کامپوننت‌ها، همراه با فایل‌های وابسته‌ی آن‌ها است. درون این پوشه، یک پوشه جدید را تحت عنوان counter داریم که حاوی فایل‌های زیر می‌باشد: 
Counter
Counter.module.css
counterSlice.js
Counter.js، کامپوننتی است که در نهایت درون صفحه رندر خواهد شد. درون این فایل با استفاده از Redux Hooks کار اتصال به store و همچنین dispatch کردن اکشن‌ها انجام گرفته است:
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState(2);

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
      <div className={styles.row}>
        <input
          className={styles.textbox}
          value={incrementAmount}
          onChange={e => setIncrementAmount(e.target.value)}
        />
        <button
          className={styles.button}
          onClick={() =>
            dispatch(
              incrementByAmount({ amount: Number(incrementAmount) || 0 })
            )
          }
        >
          Add Amount
        </button>
      </div>
    </div>
  );
}

فایل Counter.module.css نیز در واقع استایل‌های مربوط به کامپوننت فوق میباشد که به صورت CSS module اضافه شده‌است. در نهایت فایل counterSlice.js را داریم که  کار همان reducer سابق را برایمان انجام خواهد داد؛ اما اینبار با یک ساختار جدید و تحت عنوان slice. اگر به فایل عنوان شده مراجعه کنید، کدهای زیر را خواهید دید:
import { createSlice } from '@reduxjs/toolkit';

export const slice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: state => {
      // Redux Toolkit allows us to 'mutate' the state. It doesn't actually
      // mutate the state because it uses the immer library, which detects
      // changes to a "draft state" and produces a brand new immutable state
      // based off those changes
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload.amount;
    },
  },
});

export const selectCount = state => state.counter.value;
export const { increment, decrement, incrementByAmount } = slice.actions;

export default slice.reducer;
در این قالب جدید، ترکیب این قطعات هستند که شیء اصلی یا در واقع همان state کلی پروژه را تشکیل خواهند داد. همانطور که مشاهده میکنید، برای ایجاد یک قطعه جدید، از تابع createSlice استفاده شده است. این تابع، تعدادی پارامتر را از ورودی دریافت می‌کند:
  • name: برای هر بخش از state، می‌توانیم یک نام را تعیین کنیم و این همان عنوانی خواهد بود که می‌توانید توسط Redux DevTools مشاهده کنید.
  • initialValue: در اینجا می‌توانیم مقادیر اولیه‌ای را برای این بخش از state، تعیین کنیم که در مثال فوق، value به مقدار صفر تنظیم شده‌است.
  • reducers: این قسمت محل تعریف actionهایی هستند که قرار است state را تغییر دهند. نکته جالب توجه این است که state در هر کدام از متدهای فوق، به ظاهر mutate شده است؛ اما همانطور که به صورت کامنت نیز نوشته‌است، در پشت صحنه از کتابخانه‌ای با عنوان immer استفاده می‌کند که در عمل بجای تغییر state اصلی، یک کپی از state جدید را جایگزین state قبلی خواهد کرد.
توسط selectCount نیز کار انتخاب مقدار موردنظر از state انجام شده‌است که معادل همان mapPropsToState است و در اینجا امکان دسترسی به state ذخیره شده در مخزن redux فراهم شده است. همچنین در خطوط پایانی فایل نیز اکشن‌ها برای دسترسی ساده‌تر به درون کامپوننت، به صورت Object Destructuring به بیرون export شده‌اند. در نهایت reducer نهایی را از slice ایجاد شده استخراج کرده‌ایم. این پراپرتی برای ایجاد store مورداستفاده قرار می‌گیرد.

چرا قالب Redux Toolkit از immer برای تغییر state استفاده میکند؟
همانطور که در این قسمت از سری Redux توضیح داده شد، اعمال تغییرات بر روی آرایه‌ها و اشیاء، باعث ایجاد ناخالصی خواهد شد؛ بنابراین به جای تغییر شیء اصلی، باید توسط یکی از روش‌های Object.assign و یا spread operator، یک clone از state اصلی را ایجاد کرده و آن را به عنوان state نهایی لحاظ کنیم. اما در حین کار با اشیاء nested، انجام اینکار سخت خواهد شد و همچنین خوانایی کد را نیز کاهش می‌دهد:
return {
    ...state,
    models: state.models.map(c =>
        c.model === action.payload.model
          ? {
              ...c,
              on: action.payload.toggle
            }
          : c
      )
  };
اما با کمک immer می‌توانیم به صورت مستقیم state را تغییر دهیم:
state.models.forEach(item => {
    if (item.model === action.payload.model) {
      item.on = action.payload.toggle;
    }
 });
کاری که immer انجام می‌دهد، تغییر یک شیء، به صورت مستقیم نیست؛ در واقع یک draftState را ایجاد خواهد کرد که در عمل یک proxy برای state فعلی می‌باشد. یعنی با mutate کردن state، یک شیء جدید را در نهایت clone خواهد کرد و به عنوان state نهایی برمی‌گرداند.
نظرات مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core
یک نکته‌ی تکمیلی
به همراه نگارش Angular 4.3، روش جدیدی برای کار با HTTP، توسط ماژول جدید HTTP Client آن ارائه شده‌است که ساختار آن بسیار شبیه به ماژول فعلی HTTP آن است و کدهای فعلی را به سادگی می‌توان به آن انتقال داد. یکی از تغییرات آن داشتن رخ‌دادهای درصد آپلود و دانلود یک درخواست است:
import {  HttpEventType,  HttpClient,  HttpRequest } from '@angular/common/http';

http.request(new HttpRequest(
  'POST',
  URL,
  body, 
  {
    reportProgress: true
  })).subscribe(event => {
  if (event.type === HttpEventType.DownloadProgress) {
  }

  if (event.type === HttpEventType.UploadProgress) {
  }

  if (event.type === HttpEventType.Response) {
    console.log(event.body);
  }
})
به این ترتیب بدون نیاز به کامپوننت اضافه‌تری می‌توان درصد پیشرفت آپلود را نمایش داد.

برای مطالعه‌ی بیشتر:
The Angular HTTP Client - Quickstart Guide 
A Taste From The New Angular HTTP Client  
مطالب
ساخت یک مثال Todo با MobX و React

پیشنیاز این مطلب مطالعه قسمت MobX می‌باشد. در این مثال قصد داریم  یک برنامه‌ی Todo را با استفاده از MobX و React ایجاد کنیم.


ایجاد ساختار ابتدایی پروژه

برای ساخت پروژه، به خط فرمان مراجعه کرده و با دستور زیر، یک پروژه‌ی react از نوع typescript را ایجاد می‌کنیم. 

npx create-react-app todo-mobx --template typescript 
cd todo-mobx


برای توسعه‌ی این مثال، از محیط توسعه‌ی VSCode استفاده می‌کنیم. اگر VSCode بر روی سیستم شما نصب باشد، در همان مسیری که خط فرمان باز است، دستور زیر را اجرا کنید؛ پروژه‌ی شما در VSCode باز می‌شود:

code


سپس در محیط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:

npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest
npm install  mobx mobx-react-lite --save


در ادامه برای استایل بندی بهتر برنامه از کتابخانه‌های bootstrap و  font-awesome استفاده می‌کنیم: 

npm install bootstrap --save
npm install font-awesome --save

سپس فایل index.tsx را باز کرده و دو خط زیر را به آن اضافه می‌کنیم: 

import "bootstrap/dist/css/bootstrap.css";
import "font-awesome/css/font-awesome.css";


کتابخانه‌ی MobX، از تزئین کننده‌ها یا decorators استفاده می‌کند. بنابراین نیاز است به tsconfig پروژه مراجعه کرده و خط زیر را به آن اضافه کنیم:  

"compilerOptions": {
   .... ,
   "experimentalDecorators": true
}


ایجاد مخازن حالت MobX

در ادامه نیاز است store‌های MobX را ایجاد کنیم و بعد آن‌ها را به react اتصال دهیم. بدین منظور یک پوشه‌ی جدید را در مسیر src، به نام stores ایجاد می‌کنیم و سپس فایل جدیدی را به نام todo-item.ts در آن با محتوای زیر ایجاد می‌کنیم: 

import { observable, action } from "mobx";

export default class TodoItem {
    id = Date.now();

    @observable text: string = '';
    @observable isDone: boolean = false;

    constructor(text: string) {
        this.text = text;
    }

    @action
    toggleIsDone = () => {
        this.isDone = !this.isDone
    }

    @action
    updateText = (text: string) => {
        this.text = text;
    }
}


در همان مسیر stores، فایل دیگری را نیز به نام todo-list.ts، با محتوای زیر ایجاد می‌کنیم:

import { observable, computed, action } from "mobx";

import TodoItem from "./todo-item";

export class TodoList {
    @observable.shallow list: TodoItem[] = [];

    constructor(todos: string[]) {
        todos.forEach(this.addTodo);
    }

    @action
    addTodo = (text: string) => {
        this.list.push(new TodoItem(text));
    }

    @action
    removeTodo = (todo: TodoItem) => {
        this.list.splice(this.list.indexOf(todo), 1);
    };

    @computed
    get finishedTodos(): TodoItem[] {
        return this.list.filter(todo => todo.isDone);
    }

    @computed
    get openTodos(): TodoItem[] {
        return this.list.filter(todo => !todo.isDone);
    }
}

توضیحات:

مفهوم observable@: کل شیء state را به صورت یک شیء قابل ردیابی JavaScript ای ارائه می‌کند.

مفهوم computed@: این نوع خواص، مقدار خود را زمانیکه observable‌های وابسته‌ی به آن‌ها تغییر کنند، به روز رسانی می‌کنند.

مفهوم action@: جهت به روز رسانی state و سپس نمایش تغییرات یا نمایش نمونه‌ی دیگری در DOM می‌باشند.



برپایی Context
در این مثال از شیء Provider خود MobX استفاده نمی‌کنیم؛ بلکه از React Context استفاده می‌کنیم. به همین جهت در مسیر src، یک پوشه‌ی جدید دیگر را به نام Providers ایجاد می‌کنیم. سپس فایلی را به نام store-provider.ts ایجاد کرده و کدهای زیر را به آن اضافه می‌کنیم: 
import { createContext, useContext } from "react";
import { TodoList } from "../stores/todo-list";

export const StoreContext = createContext<TodoList>({} as TodoList);
export const StoreProvider = StoreContext.Provider;

export const useStore = (): TodoList => useContext(StoreContext);
توضیحات:
- در اینجا StoreContext را ایجاد کرده و سپس به آن یک مقدار پیش فرض از نوع یک object خالی را ارسال کرده‌ایم.
- سپس بر اساس آن، شیء StoreProvider را که از نوع ReactConxtext می‌باشد، ایجاد کردیم. 
- متد useStore که به صورت export و نوعی از useContext می‌باشد، برای دسترسی ساده‌تر به Context معرفی شده‌است که در ادامه کاربرد آن‌را خواهید دید.
- برای اعمال StoreProvider در شروع کننده‌ی برنامه React، به فایل index.tsx مراجعه کرده و آن‌را به صورت زیر ویرایش می‌کنیم: 
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import "bootstrap/dist/css/bootstrap.css";
import "font-awesome/css/font-awesome.css";
import { TodoList } from './stores/todo-list';
import { StoreProvider } from './providers/store-provider';


const todoList = new TodoList([
    'Read Book',
    'Do exercise',
    'Watch Walking dead series'
]);

ReactDOM.render(
    <StoreProvider value={todoList}>
        <App />
    </StoreProvider>
    , document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
توضیحات:  StoreProvider ای را که در فایل store-provider.ts ایجاد کردیم، در اینجا به شروع کننده‌ی React معرفی می‌کنیم و سه مقدار پیش فرض را نیز به آن اعمال می‌کنیم.

افزودن کامپوننت‌های برنامه
برای نمایش لیست Todo‌ها و عملیات حذف، اضافه و ویرایش، نیاز به سه کامپوننت تابعی را داریم: 

اضافه کردن کامپوننت TodoNew
در مسیر src، یک پوشه‌ی جدید را به نام components ایجاد می‌کنیم. سپس فایلی را در آن به نام TodoNew.tsx ایجاد کرده و کدهای زیر را به آن اضافه می‌کنیم: 
import React, { useState } from 'react';
import { useStore } from '../providers/store-provider';

export const TodoNew = () => {
    const [newTodo, setTodo] = useState('');
    const todoList = useStore();

    const addTodo = () => {
        todoList.addTodo(newTodo);
        setTodo('');
    };

    return (

        <div className="input-group mb-3">
            <input type="text" className="form-control" placeholder="Add To do" value={newTodo} onChange={(e) => setTodo(e.target.value)} />
            <div className="input-group-append">
                <button className="btn btn-success" type="submit" onClick={addTodo}>Add Todo</button>
            </div>
        </div>
    )
};
توضیحات: 
- useStore ای را که در مرحله‌ی قبل ایجاد کردیم، در اینجا برای دسترسی به state‌های MobX استفاده می‌کنیم.
- در input و رویداد onChange آن، مقدار ورودی کاربر را به متد newTodo اعمال می‌کنیم و بعد از اینکه کاربر دکمه‌ی Add Todo را زد، مقدار newTodo را به تابع addTodo که در useStore می‌باشد، اعمال می‌کنیم.

افزودن کامپوننت نمایش لیست کارها: TodoList
در مسیر src و پوشه‌ی components آن، فایل جدیدی را به نام TodoList.tsx ایجاد کرده و کدهای زیر را به آن اضافه می‌کنیم: 
import React from 'react';

import { TodoItem } from "./TodoItem";
import { useObserver } from "mobx-react-lite";
import { useStore } from '../providers/store-provider';

export const TodoList = () => {
    const todoList = useStore();

    return useObserver(() => (
        <div>
            <h1>Open Todos</h1>
            <table className="table">
                <thead className="thead-dark">
                    <tr>
                        <th>Name</th>
                        <th className="text-left">Do It?</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    {
                        todoList.openTodos.map(todo =>
                            <tr key={`${todo.id}-${todo.text}`}>
                                <TodoItem todo={todo} />
                            </tr>)
                    }

                </tbody>
            </table>

            <h1>Finished Todos</h1>
            <table className="table">
                <thead className="thead-light">
                    <tr>
                        <th>Name</th>
                        <th className="text-left">Do It?</th>
                        <th>Actions</th>
                    </tr>
                </thead>
                <tbody>
                    {
                        todoList.finishedTodos.map(todo =>
                            <tr key={`${todo.id}-${todo.text}`}>
                                <TodoItem todo={todo} />
                            </tr>)
                    }

                </tbody>
            </table>
        </div>
    ));
};
توضیحات:
- useStore را به ثابت todoList انتساب می‌دهیم.
- سپس برای نمایش Todo ‌ها، یک جدول را طراحی می‌کنیم و همچنین برای نمایش کارهای تکمیل شده (Finish Todo) جدول دیگری را ایجاد می‌کنیم.
- در کلاس TodoList، که پیشتر آن‌را ایجاد کردیم، از دو خاصیت openTodos و finishedTodos از نوع get که با Decorator از نوع computed@ هستند، برای نمایش Open Todos و Finished Todos استفاده می‌کنیم. خروجی این خواص، لیستی از نوع TodoItem می‌باشند که با کمک متد map، به فیلد‌های TodoItem آن‌ها دسترسی پیدا می‌کنیم.

برای منظم کردن کدها، کامپوننت دیگری را در مسیر src/components به نام TodoItem.tsx ایجاد کرده و کدهای زیر را به آن اضافه می‌کنیم: 
import React, { useState } from 'react';
import TodoItemClass from "../stores/todo-item";


import { useStore } from '../providers/store-provider';

interface Props {
    todo: TodoItemClass;
}

export const TodoItem = ({ todo }: Props) => {
    const todoList = useStore();
    const [newText, setText] = useState('');
    const [isEditing, setEdit] = useState(false);

    const saveText = () => {
        todo.updateText(newText);
        setEdit(false);
        setText('');
    };

    return (
        <React.Fragment>
            {
                isEditing ?
                    <React.Fragment>
                        <td>
                            <input className="form-control" placeholder={todo.text} type="text" onChange={(e) => setText(e.target.value)} />
                        </td>
                        <td></td>
                        <td>
                            <button className="btn btn-xs btn-success " onClick={saveText}>Save</button>
                        </td>
                    </React.Fragment>
                    :
                    <React.Fragment>
                        <td>
                            {todo.text}
                        </td>

                        <td className="text-left">
                            <input className="form-check-input" type="checkbox" onChange={todo.toggleIsDone} defaultChecked={todo.isDone}></input>
                        </td>
                        <td>
                            <button className="btn btn-xs btn-warning " onClick={() => setEdit(true)}>
                                <i className="fa fa-edit"></i>
                            </button>
                            <button className="btn btn-xs btn-danger ml-2" onClick={() => todoList.removeTodo(todo)}>
                                <i className="fa fa-remove"></i>
                            </button>
                        </td>
                    </React.Fragment>
            }

        </React.Fragment>

    )
};
توضیحات:
- در کامپوننت قبلی TodoList.tsx، متدهای TodoItem را به کامپوننت TodoItem.tsx پاس داده و آن را در دو حالت ویرایش و نمایش، نشان می‌دهیم.
- در جدول، امکان ویرایش، حذف و ثبت رکوردها را قرار داده‌ایم. برای ویرایش، مقدار input وارد شده را به متد (todo.updateText(newText پاس می‌دهیم و برای حذف، (todoList.removeTodo(todo را فراخوانی می‌کنیم.
 
کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید  Github
مطالب
استفاده از #F در پروژه های MVC4
در این پست با روش پیاده سازی پروژه‌های WPF با استفاده از #F آشنا شدید. قصد دارم در طی چند پست روش پیاده سازی پروژه‌های Asp.Net MVC 4  با استفاده از #F را شرح دهم.
»اگر با #F آشنایی ندارید می‌توانید از اینجا شروع کنید.
به صورت کلی برای استفاده گسترده از #F در پروژه‌های وب نیاز به یک سری template‌های آماده داریم در غیر این صورت کار کمی سخت خواهد شد. به تصویر زیر دقت نمایید:

واضح است که با توجه به تصویر بالا کنترلر‌ها و البته مدل‌های app  و هر آنچه که سمت سرور به آن نیاز است باید با استفاده از #F پیاده سازی شوند. اما هنگامی که کنترلر‌ها با استفاده از #F نوشته شوند سیستم مسیر یابی نیز تحت تاثیر قرار خواهد گرفت. علاوه بر آن باید فکری برای بخش Bundling و همچنین فیلتر‌هاو... نمود. در نتیجه با توجه به template پروژه مورد نظر بر خلاف حالت پیش فرض #C آن که در قالب یک پروژه ارائه می‌شود در این جا حداقل به دو پروژه نیاز داریم. خوشبختانه همانند پروژه FSharpX که برای WPF مناسب است برای MVC نیز template آماده موجود است که در ادامه با آن آشنا خواهیم شد.

شروع به کار

ابتدا در VS.Net یک پروژه جدید ایجاد نمایید. از بخش Online Template گزینه FSharp MVC 4 را جستجو کنید. 

بعد از انتخاب نام پروژه و  کلیک بر روی Ok ( و البته دانلود حدود ده MB اطلاعات) صفحه زیر نمایان می‌شود. در این قسمت تنظیمات مربوط به انتخاب View Engine و نوع قالب پروژه را وجود دارد. در صورتی که قصد استفاده از Web Api را دارید گزینه  Web Api Project را انتخاب کنید در غیر این صورت گزینه Empty Project.

البته از Visual Studio 2012 به بعد این بخش به صورت زیر خواهد بود که قسمت Single Page App به آن اضافه شده است:

بعد از کلیک بر روی Ok یک پروژه بر اساس Template مورد نظر ساخته می‌شود. همانند تصویر زیر:

بررسی تغییرات

در یک نگاه به راحتی می‌توان تغییرات زیر را در پروژه Web تشخیص داد:

»پوشه Controller وجود ندارد؛

»پوشه مدل وجود ندارد؛

»فایل Global.asax دیگر فایلی به نام Global.asax.cs را همراه با خود ندارد.

دلیل اصلی عدم وجود موارد بالا این است که تمام این موارد باید به صورت #F پیاده سازی شوند در نتیجه به پروژه #F ساخته شده منتقل شده اند. فایل Global.asax را باز نمایید. سورس زیر قابل مشاهده است:

<%@ Application Inherits="FsWeb.Global" Language="C#" %> <script Language="C#" RunAt="server">
 // Defines the Application_Start method and calls Start in // System.Web.HttpApplication from which Global inherits. protected void Application_Start(Object sender, EventArgs e) { base.Start(); } </script>
تمامی کاری که تکه کد بالا انجام می‌دهد فراخواهی متد Start در فایل Global متناظر در پروژه #F است. اگر به قسمت reference‌های پروژه Web دقت کنید خواهید که به پروژه #F متناظر رفرنس دارد.
حال به بررسی پروژه #F  ساخته شده خواهیم پرداخت. در این پروژه یک فایل  Global.fs وجود دارد که سورس آن به صورت زیر است:

namespace FsWeb

open System
open System.Web
open System.Web.Mvc
open System.Web.Routing

type Route = { controller : string
               action : string
               id : UrlParameter }

type Global() =
    inherit System.Web.HttpApplication() 

    static member RegisterRoutes(routes:RouteCollection) =
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
        routes.MapRoute("Default", 
                        "{controller}/{action}/{id}", 
                        { controller = "Home"; action = "Index"
                          id = UrlParameter.Optional } )

    member this.Start() =
        AreaRegistration.RegisterAllAreas()
        Global.RegisterRoutes(RouteTable.Routes)
  در پروژه‌های MVC بر اساس زبان #C،  کلاسی به نام RouteConfig وجود دارد که در فایل Global.asax.cs فراخوانی می‌شود:
RouteConfig.RegisterRoutes(RouteTable.Routes);
و کد‌های مورد نظر جهت تعیین الگوی مسیر یابی کنترلر‌ها و درخواست‌ها در کلاس RouteConfig نیز به صورت زیر می‌باشد:
public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
اما در اینجا بر خلاف #C، تعریف و اجرای سیستم مسیر یابی در یک فایل انجام می‌شود. در بخش اول ابتدا یک type تعریف می‌شود که معادل با تعیین Routing در زبان #C است. علاوه بر آن متد RegisterRoutes جهت تعیین و انتساب Route Type تعریف شده به Route Collection مورد نظر نیز در این فایل می‌باشد. 

//تعریف الکوی مسیر یابی
type Route = { controller : string
               action : string
               id : UrlParameter }

type Global() =
    inherit System.Web.HttpApplication() 

    static member RegisterRoutes(routes:RouteCollection) =
       //فراخوانی  و انتساب الگوی مسیر یابی به  مسیر‌های تعریف شده
       routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
        routes.MapRoute("Default", 
                        "{controller}/{action}/{id}", 
                        { controller = "Home"; action = "Index"
                          id = UrlParameter.Optional } )
در نهایت نیز تابع RegisterRoute در تابع Start فراخوانی خواهد شد.
Global.RegisterRoutes(RouteTable.Routes)
نتیجه کلی تا اینجا:
در این پست با Template پروژه‌های F# MVC 4 اشنا شدیم و از طرفی مشخص شد که برای پیاده سازی این گونه پروژه‌ها حداقل نیاز به دو پروژه داریم. یک پروژه که از نوع #C است ولی در آن فقط View ‌ها و فایل جاوااسکریپتی و البته Css وجود دارد. از طرف دیگر کنترلر‌ها و مدل‌ها و هر چیز دیگر که مربوط به سمت سرور است در قالب یک پروژه #F پیاده سازی می‌شود.
در پست بعدی با روش تعریف و توسعه کنترلر‌ها و مدل‌ها آشنا خواهیم شد.
نظرات مطالب
غیرمعتبر کردن توکن و یا کوکی سرقت شده در برنامه‌های مبتنی بر ASP.NET Core
یک نکته‌ی تکمیلی
« fingerprintjs » کتابخانه‌ای است برای انتساب یک device-id به کاربر (بر اساس user-agent، اندازه‌ی صفحه، فونت‌های نصب شده، پلاگین‌های در حال استفاده و غیره). برای استفاده‌ی از آن، ابتدا اسکریپت آن باید اضافه شود:
<script src="https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@3.12.1/dist/fingerprint2.min.js"></script>
سپس یک کوکی از نتیجه‌ی آن تهیه می‌شود:
string fingerprint = "";
string fingerprintScript = @"<script>
    const fpPromise = FingerprintJS.load();
    fpPromise.then(fp => {
        fp.get().then(result => {
            const values = result.values;
            const fingerprint = values.join('');
            document.cookie = 'DeviceFingerprint=' + fingerprint + '; path=/';
        });
    });
</script>";
Response.Write(fingerprintScript);
و این کوکی که با نام DeviceFingerprint ذخیره شده، به صورت زیر قابل خواندن است:
string fingerprint = Request.Cookies["DeviceFingerprint"]?.Value;
if (fingerprint == null)
{
    // Device fingerprint cookie not found
}
مطالب
breeze js به همراه ایجاد سایت آگهی قسمت دوم
نصب: پکیج‌های متنوعی از breeze وجود دارند. برای ما بسته‌ی زیر بهترین انتخاب می‌باشد. با نصب پکیج زیر، breeze در سمت سرور و کلاینت، به همراه ASP.NET Web API 2.2 and Entity Framework 6 نصب می‌شود:
Install-Package Breeze.WebApi2.EF6
بعد از نصب، دو فایل جاوا اسکریپتی به پروژه اضافه میشوند: breeze.debug.js  فایل اصلی breeze می‌باشد که از Backbone و Knockout پشتیبانی می‌کند و breeze.min.js که فایل فشرده شده breeze.debug میباشد. برای بهتر کار کردن با angularjs و breezejs، کتابخانه‌ی Breeze.Angular را  نصب نمایید. یکی از مواردی که این سرویس برای ما انجام می‌دهد،interceptor ایی را برای درخواست‌های http فعال می‌کند. یکی از موارد استفاده‌ی آن، ارسال token امنیتی، قبل از درخواست‌های breeze به کنترلر میباشد:
var instance = breeze.config.initializeAdapterInstance("ajax", "angular");
instance.setHttp($http);
Install-Package Breeze.Angular
 قلب تپنده‌ی breezejs در کلاینت EntityManager است که نقش data context را در کلاینت، بازی می‌کند. به برخی از خصوصیات آن می‌پردازیم:
  var manager = new breeze.EntityManager({  
  dataService: dataService,          
  metadataStore: metadataStore,                     
  saveOptions: new breeze.SaveOptions({    allowConcurrentSaves: true, tag: [{}] })   
                 });

var dataService = new breeze.DataService({  
serviceName: "/breeze/"+ "Automobile",             
hasServerMetadata: false,
namingConvention: breeze.NamingConvention.camelCase        
});
var metadataStore = new breeze.MetadataStore({});

- serviceName: نام سرویس دهنده یا کنترلر سمت سرور میباشد. درمورد کنترلر سمت سرور کمی جلوتر بحث می‌کنیم.
- metadataStore: اطلاعاتی را در مورد تمام آبجکت‌ها (جداول دیتابیس) می‌دهد. مثل نام فیلدها، نوع فیلدها و...
برای کار با متادیتا دو راه وجود دارد:
1- متا دیتا را خودتان در سمت کلاینت ایجاد نمایید:
var myMetadataStore = new breeze.MetadataStore();
myMetadataStore.addEntityType({...});
یا برای اضافه کردن فیلد شهر به جدول customer:
 var customer = function () {
                    this.City = "";
                };
myMetadataStore.registerEntityTypeCtor("Customer", customer);
2- اطلاعات  را از سرور دریافت  نمایید. در این صورت  کنترلر شما باید دارای متد Metadata باشد. بنابراین کنترلی را در سرور به نام Automobile و با محتویات زیر ایجاد نمایید. همانطور که مشاهده می‌کنید، این کنترلر از ApiController مشتق شده است که تفاوت خاصی با Api‌‌های دیگر ندارد و تنها به BreezeController مزین شده است. این attribute به NET WebApi  کمک میکند که فیلترینگ و مرتب سازی با فرمت oData را فراهم کند و همچنین درک صحیح فرمت json را نیز به کنترلر می‌دهد.
EFContextProvider: کامپوننتی که تعامل بین کنترلر breeze با Entity Framework را ساده‌تر می‌کند و در واقع یک  wrapper بر روی دیتاکانتکس یا آبجکت کانتکس می‌باشد. یکی از وظایف آن  ارسال متا دیتا، برای کلاینت‌های breeze است.
[BreezeController]
public class AutomobileController : ApiController
    {
        readonly EFContextProvider<ApplicationDbContext> _contextProvider =
        new EFContextProvider<ApplicationDbContext>();
        [HttpGet]
        public string Metadata()
        {
            return _contextProvider.Metadata();
        }
        [HttpGet]
        public IQueryable<Customer> Customers() {
           return _contextProvider.Context.Customers;
        }

        [System.Web.Http.HttpPost]
        public SaveResult SaveChanges(JObject saveBundle)
        {
           _contextProvider.BeforeSaveEntitiesDelegate = BeforeSaveEntities;
           _contextProvider.AfterSaveEntitiesDelegate = afterSaveEntities;
            return _contextProvider.SaveChanges(saveBundle);
        }
protected Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap)
        {
        }
private void afterSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings)
        {
        }
    }
در اینجا متدی مانند Customers، از طریق کلاینت‌های breeze قابل دسترسی می‌باشد.

- saveOptions: نحوه‌ی چگونگی برخورد با ذخیره کردن اطلاعات را مشخص می‌کند. با ذخیره سازی تغییرات، متد SaveChanges سمت سرور فراخوانی می‌شود. در breeze می‌توان به قبل و بعد از ذخیره سازی اطلاعات دسترسی داشت. یکی از موارد رایج کاربرد آن، اعمال چک کردن دسترسی‌ها، قبل از ذخیره سازی می‌باشد.
برای ذخیره سازی تغییرات:
manger.saveChanges().then(function success() {
                    }, function failer(e) {
                    });
برای نادیده گرفتن تغییرات:
manger.rejectChanges()

کوئری:
بعد از تعریف Entity Manger می‌توانیم کوئری خود را اجرا نماییم. کوئری ما شامل گرفتن اطلاعات از جدول Customer، با مرتب سازی بر روی فیلد آیدی می‌باشد و با اجرا کردن کوئری می‌توانیم موفقیت یا عدم موفقیت آن‌را بررسی نماییم. 
   var query = breeze.EntityQuery
            .from("Customer")   
            .orderBy("Id");
   var result= manager.executeQuery(query);
   result.then(querySucceeded)
    .fail(queryFailed);

   query = query.where("Id", "==", 1)
با نوشتن Predicate تکی یا ترکیب آنها نیز می‌توان شرط‌های پیچیده‌تری را ایجاد کرد:
var predicate = new breeze.Predicate("Id", "==", false);
query = query.where(predicate)

var p1 = new breeze.Predicate("IsArchived", "==", false);
var p2 = breeze.Predicate("IsDone", "==", false); 
var predicate = p1.and(p2);
query = query.where(predicate).orderBy("Id")  
در اینجا خروجی مشابه زیر برای کنترلر ارسال میشود:
?$filter=IsArchived eq false&IsDone eq false  &$orderby=Id

اعتبارسنجی
:اعتبارسنجی در breeze، هم در سمت کلاینت و هم در سمت سرور امکان پذیر می‌باشد که در مثالی، در قسمت بعدی، validator سفارشی خودمان را خواهیم ساخت و به entity مورد نظر اعمال خواهیم کرد.
breeze دارای یک سری Validator در سطح پراپرتی‌ها است:
- برای انواع اقسام dataType ها مانند Int,string,..
- برای نیازهای رایجی چون: emailAddress,creditCard,maxLength,phone,regularExpression,required,url 
هم چنین در breeze امکان تغییر دادن اعتبارسنجی‌های پیش فرض نیز وجود دارند. برای مثال برای اینکه در فیلدهای required بتوان متن خالی هم وارد کرد، از دستور زیر می‌توان استفاده کرد:
breeze.Validator.required({ allowEmptyStrings: true });

ردیابی تغییرات
: هر آیتم Entity دارای EntityAspect است که وضعیت آن‌را مشخص می‌کند و می‌تواند یکی از وضعیت‌های Added،Modified،Deleted،Detached،Unchanged باشد. با مشخص کردن حالت هر آیتم، با فراخوانی SaveChanges تغییرات بر روی دیتابیس اعمال می‌گردد.
ایجاد آیتم جدید:
manager.createEntity('Customer', jsonValue);
 ویرایش اطلاعات:
manager.createEntity("Customer", jsonValue, breeze.EntityState.Modified, breeze.MergeStrategy.OverwriteChanges)
 حذف اطلاعات:
manager.createEntity("Customer", item, breeze.EntityState.Deleted)

برای اشنایی بیشتر با امکانات Breeze، قصد داریم یک سایت ایجاد آگهی را راه اندازی کنیم. پیش نیازهای ضروری این بخش typescript ،angularjs ،requirejs هستند. قصد داریم سایتی را برای آگهی‌های خرید و فروش خودرو، مشابه با سایت باما ایجاد نماییم:

امکانات این سایت:
- ثبت نام کاربران 
- ثبت آگهی توسط کاربران 
- ایجاد برچسب‌های آگهی‌ها 
- امتیاز دهی به آگهی‌ها
- جستجوی آگهی‌ها
- و....
ابتدا نصب پکیج‌های زیر 
Install-Package angularjs
Install-Package angularjs.TypeScript.DefinitelyTyped

Install-Package bootstrap
Install-Package bootstrap.TypeScript.DefinitelyTyped

Install-Package jQuery
Install-Package jquery.TypeScript.DefinitelyTyped

Install-Package RequireJS
Install-Package requirejs.TypeScript.DefinitelyTyped

bower install angularAMD

مدلهای برنامه:
ایجاد کلاس BaseEntity 
 public  class BaseEntity
    {
        public int Id { get; set; }
        public bool Status { get; set; }
        public DateTime CreatedDateTime { get; set; }
    }
ایجاد جدول آگهی
    public class Ad : BaseEntity
    {
        public string Title { get; set; }
        public float Price { get; set; }
        public double Rating { get; set; }
        public int? RatingNumber { get; set; }
        public string UserId { get; set; }
        public DateTime ModifieDateTime { get; set; }
        public string Description { get; set; }
        public virtual ICollection<Comment> Comments { get; set; }
        public virtual IdentityUser User { get; set; }
        public virtual ICollection<AdLabel> Labels { get; set; }
        public virtual ICollection<AdMedia> Medias { get; set; }
    }
ایجاد جدول برچسب 
public class Label 
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public int? ParentId { get; set; }
        public virtual Label Parent { get; set; }
        public virtual ICollection<Label> Items { get; set; }
    }
ایجاد جدول مدیا
 public class Media 
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string MimeType { get; set; }
    }
ایجاد جدول واسط برچسب‌های آگهی
  public class AdLabel
    {
        public int Id { get; set; }
        public virtual Ad Ad { get; set; }
        public virtual Label Label { get; set; }
        [Index("IX_AdLabel", 1, IsUnique = true)]
        public int AdId { get; set; }
        [Index("IX_AdLabel", 2, IsUnique = true)]
        public int LabelId { get; set; }
        public string Value { get; set; }
    }
ایجاد جدول واسط مدیا‌های مرتبط با آگهی
 public class AdMedia
    {
        public int Id { get; set; }
        public virtual Ad Ad { get; set; }
        public virtual Media Media { get; set; }
        [Index("IX_AdMedia", 1, IsUnique = true)]
        public int AdId { get; set; }
        [Index("IX_AdMedia", 2, IsUnique = true)]
        public int MediaId { get; set; }
    }
ایجاد جدول کامنت‌ها
  public class Comment : BaseEntity
    {
        public string Body { get; set; }
        public double Rating { get; set; }
        public int? RatingNumber { get; set; }
        public string EntityName { get; set; }
        public string UserId { get; set; }
        public int? ParentId { get; set; }
        public int? AdId { get; set; }
        public virtual Comment Parent { get; set; }
        public virtual Ad Ad { get; set; }
        public virtual ICollection<Comment> Items { get; set; }
        public virtual IdentityUser User { get; set; }
    }
ایجاد جدول اعضاء
public class Customer:BaseEntity
    {
        public string UserId { get; set; }
        public virtual string DisplayName { get; set; }
        public virtual string BirthDay { get; set; }
        public string City { get; set; }
        public string Address { get; set; }
        public int? MediaId { get; set; }
        public bool? NewsLetterSubscription { get; set; }
        public string PhoneNumber { get; set; }
        public virtual IdentityUser User { get; set; }
        public virtual Media Media { get; set; }
    }
ایجاد جدول امتیاز دهی به آگهی‌ها
public class Rating 
    {
      public int Id { get; set; }
       public string UserId { get; set; }
       public Double Rate { get; set; }
       public string EntityName { get; set; }
       public int DestinationId { get; set; }
    }

اضافه کردن مدلهای برنامه به ApplicationDbContext 
 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }
        public DbSet<Ad> Ads { get; set; }
        public DbSet<AdLabel> AdLabels { get; set; }
        public DbSet<AdMedia> AdMedias { get; set; }
        public DbSet<Comment> Comments { get; set; }
        public DbSet<Label> Labels { get; set; }
        public DbSet<Media> Medias { get; set; }
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }

لود کردن فایل main.js در فایل layout.cshtml ترجیحا در انتهای body
    <script src="~/Scripts/require.js" data-main="/app/main"></script>
RequireJS  کتابخانه‌ی جاوااسکریپتی برای بارگزاری فایل‌ها در صورت نیاز می‌باشد. تنها کاری که ما باید انجام بدهیم این است که کدهای خود را داخل module‌ها قرار دهیم (در فایل‌های جداگانه) و RequireJS در صورت نیاز آنها را load خواهد کرد. همچنین RequireJS وابستگی بین module‌ها را نیز مدیریت می‌کند.

ایجاد فایل main.ts 
path: مسیر فایل‌های جاوا اسکریپتی
shim: وابستگی‌های فایل‌ها(ماژول ها) و export کردن آنها را مشخص می‌کند.
requirejs.config({
    paths: {
        "app": "app",
        "angularAmd":"/Scripts/angularAmd",
        "angular": "/Scripts/angular",
        "bootstrap": "/Scripts/bootstrap",
        "angularRoute": "/Scripts/angular-route",
        "jquery": "/Scripts/jquery-2.2.2",
    },
    waitSeconds: 0,
    shim: {
        "angular": { exports: "angular" },
        "angularRoute": { deps: ["angular"] },
        "bootstrap": { deps: ["jquery"] },
        "app": {
            deps: ["bootstrap","angularRoute"]
        }
    }
});
require(["app"]);

ایجاد فایل app.ts: کارهایی که در فایل app انجام داده‌ایم:
ایجاد کنترلر SecurityCtrl و اعمال آن به تگ body
<body ng-controller="SecurityCtrl">
...
</body>
ایجاد ماژول AdApps و قرار دادن کلاس SecurityCtrl در آن. از این به بعد برای مدیریت بهتر، تمام کدهای خود را درون ماژول‌ها قرار می‌دهیم. 
"use strict";
module AdApps {
    class SecurityCtrl {
        private $scope: Interfaces.IAdvertismentScope;
        constructor($scope: Interfaces.IAdvertismentScope) {
           // security check
      this.$scope = $scope;
        }
    }
 define(["angularAmd", "angular"], (angularAmd, ng) => {
   angularAmd = angularAmd.__proto__;
        var app = ng.module("AngularTypeScript", ['ngRoute']);
        var viewPath = "app/views/";
        var controllerPath = "app/controller/";
        app.config(['$routeProvider', $routeProvider => {
                $routeProvider
                    .when("/", angularAmd.route({
                        templateUrl: viewPath + "home.html",
                        controllerUrl: controllerPath + "home .js"
                    }))
                    .otherwise({ redirectTo: '/' });
            }
        ]);
        app.controller('SecurityCtrl', ['$scope', SecurityCtrl]);
        return angularAmd.bootstrap(app);
 })}
مطالب
React 16x - قسمت 30 - React Hooks - بخش 1 - معرفی useState و useEffect
با استفاده از React Hooks که در نگارش 16.7.0 آن معرفی شدند، می‌توان در کامپوننت‌های تابعی «تا پیش از این» بدون حالت، به state و تمام قابلیت‌های دیگر React، دسترسی یافت. جهت یادآوری، در قسمت 8 این سری، کامپوننت‌های تابعی بدون حالت را معرفی کردیم. تا پیش از معرفی React Hooks، برای ردیابی تغییرات مقادیری خاص، می‌بایستی آن‌ها را در خاصیت state کامپوننت‌هایی که به صورت کلاس تعریف شده بودند، قرار می‌دادیم. بنابراین class components، تنها نوع کامپوننت‌هایی در React بودند که دسترسی به state را داشتند. شبیه به همین مورد، برای life-cycle hooks معرفی شده‌ی در قسمت 9 برقرار بود. برای مثال متد componentDidMount، تنها در class components، جهت دسترسی به یک API خارجی و انجام اعمال Ajax ای، قابل تعریف بود و کار کامپوننت‌های تابعی بدون حالت، بیشتر نمایش عناصر HTML و دریافت مقادیر خود از class components، توسط props بود. به این ترتیب امکان تجزیه‌ی یک UI پیچیده، به یک component tree با خوانایی بیشتری، میسر می‌شد. اما با معرفی React v16.7.0، از لحاظ فنی دیگر الزاما نیازی به class components وجود ندارد و می‌توان با استفاده از React Hooks، تمام قابلیت‌هایی را که پیشتر فقط توسط class components در اختیار داشتیم، اکنون با کامپوننت‌های تابعی نیز پیاده سازی کنیم.


برپایی پیش‌نیازها

در اینجا برای بررسی React Hooks، یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> npm i -g create-react-app
> create-react-app sample-30
> cd sample-30
> npm start
در ادامه توئیتر بوت استرپ 4 را نیز نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save bootstrap
سپس برای افزودن فایل bootstrap.css به پروژه‌ی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.

همچنین اگر به فایل package.json موجود در ریشه‌ی پروژه دقت کنیم، برای کار با React-hooks، نیاز است نگارش بسته‌های React و React-dom، حداقل مساوی 16.7 باشند که در زمان نگارش این مطلب، نگارش 16.12.0 آن به صورت خودکار نصب می‌شود. بنابراین بدون مشکلی می‌توانیم شروع به کار با React hooks کنیم.


معرفی useState Hook

در اینجا قصد داریم یک شمارشگر را به همراه یک دکمه، در صفحه نمایش دهیم؛ بطوریکه این شمارشگر، تعداد بار کلیک بر روی دکمه را ردیابی می‌کند. از پیش می‌دانیم که برای ردیابی مقدار تعداد بار کلیک شدن، باید متغیر آن‌را درون state یک class component قرار داد:
import "./App.css";

import React, { Component } from "react";

class App extends Component {
  state = { count: 0 };

  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <button onClick={this.incrementCount} className="btn btn-primary m-3">
        I was clicked {this.state.count} times!
      </button>
    );
  }
}

export default App;
در اینجا یک class component متداول را داریم که در آن دکمه‌ای تعریف شده‌است. سپس برای شمارش تعداد بار کلیک بر روی آن، خاصیت count را به شیء منتسب به state، با مقدار اولیه‌ی صفر، اضافه کرده‌ایم. اکنون هربار که بر روی آن کلیک می‌شود، رویدادگردان incrementCount فراخوانی شده و توسط متد this.setState، مقدار پیشین خاصیت count، بر اساس مقدار فعلی آن، یک واحد افزایش می‌یابد. نتیجه‌ی آن نیز در برچسب دکمه، نمایش داده می‌شود:


اکنون می‌خواهیم همین کامپوننت را توسط React hooks بازنویسی کنیم. در ابتدا، فایل app.js را به App‍Class.js، تغییر نام می‌دهیم تا نگارش قبلی class component را برای مقایسه، در اختیار داشته باشیم. در ادامه فایل جدید AppFunction.js را برای بازنویسی آن توسط یک کامپوننت تابعی، توسط میانبرهای imrc و سپس sfc در VSCode، ایجاد می‌کنیم. البته این تغییر نام فایل‌ها، نیاز به تغییر نام ماژول‌های import شده‌ی در فایل index.js را نیز به صورت زیر دارد:
//import App from "./App‍Class";
import App from "./AppFunction";

اولین سؤالی که اینجا مطرح می‌شود، این است: در این کامپوننت تابعی جدید، state را از کجا بدست بیاوریم؟
با React Hooks، بجای تعریف یک state به صورت خاصیت، آن‌را صرفا use می‌کنیم و این دقیقا نام اولین React Hooks ای است که بررسی می‌کنیم؛ یا همان useState. بنابراین ابتدا این شیء را import خواهیم کرد:
import React, { useState } from 'react';
useState یک تابع است و باید در ابتدای کامپوننت، مورد استفاده قرار گیرد. این متد برای شروع به کار، نیاز به یک state آغازین را دارد؛ دقیقا مانند همان‌کاری که در class component فوق انجام دادیم:
const App = () => {
    const [count, setCount] = useState(0);
در اینجا عدد صفر، همان مقدار آغازین متغیر count است (شبیه به کاری که در state = { count: 0 } انجام دادیم). سپس اولین خروجی متد useState که داخل یک آرایه مشخص شده‌است (JavaScript array destructuring ؛ با مزیت امکان انتخاب نام‌هایی دلخواه، بدون نیاز به تعریف alias، برخلاف حالت object destructuring)، همان متغیر count است که توسط state ردیابی خواهد شد. اینبار بجای متد this.setState قبلی که یک متد عمومی بود، متدی اختصاصی را صرفا جهت تغییر مقدار همین متغیر count، به نام setCount به عنوان دومین خروجی متد useState، تعریف می‌کنیم. در حقیقت تا اینجا امضای متد جنریک useState استفاده شده، به صورت زیر تغییر کرده‌است:
useState<number>(initialState: number | (() => number)): [number, React.Dispatch<React.SetStateAction<number>>]
متد useState، یک initialState را دریافت می‌کند و سپس یک عدد را (در اینجا چون مقدار آغازین، عددی است)، به همراه یک متد، جهت تغییر state آن، بازگشت می‌دهد:
import React, { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <button onClick={incrementCount} className="btn btn-primary m-3">
      I was clicked {count} times!
    </button>
  );
};

export default App;
- پس از تعریف useState، متد رویدادگردان onClick را همانند قبل تعریف می‌کنیم؛ با یک تغییر مهم: چون این متد داخل یک کامپوننت تابعی تعریف شده‌است، باید با const شروع شود و همانند یک متغیر که به آن متدی انتساب داده شده، معرفی گردد. پیشتر incrementCount تعریف شده‌ی داخل یک class component، یک خاصیت بود که متدی به آن انتساب داده شده بود.
- همچنین در اینجا (داخل این متد) دیگر خبری از this‌ها نیست؛ onClick، مستقیما به متغیر incrementCount اشاره می‌کند و {count} نیز مستقیما از خروجی useState، که به مقدار جاری count اشاره می‌کند، تامین می‌شود.
- اکنون با هربار کلیک بر روی این دکمه، متد منتسب به متغیر incrementCount فراخوانی شده و در داخل آن، همان متد setCount را جهت به روز رسانی state، فراخوانی می‌کنیم (بجای فراخوانی this.setState عمومی قبلی). در اینجا ابتدا مقدار جاری متغیر count در state، دریافت شده و سپس یک واحد به آن اضافه می‌شود. امضای متد جنریک setCount به صورت زیر است:
 const setCount: (value: React.SetStateAction<number>) => void


استفاده از مقدار قبلی state توسط useState

زمانیکه متد this.setState فراخوانی می‌شود، اینکار سبب در صف قرار گرفتن رندر مجدد کامپوننت جاری خواهد شد. همچنین اعمال این متد نیز ممکن است در صف قرار گیرد. یعنی اگر پس از فراخوانی this.setState، سعی در خواندن state به روز شده را داشته باشیم، ممکن است مقدار اشتباهی را دریافت کنیم:
  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  };
برای نمونه در فراخوانی فوق، منظور از this.state.count، مقدار جاری یا همان مقدار قبلی count است که قرار است یک واحد به آن اضافه شود. اما چون متد this.setState کار به روز رسانی‌های state را نیز در صف قرار می‌دهد، دفعه‌ی بعدی که بر روی آن کلیک شد، الزامی ندارد که this.state.count، حتما در همان لحظه به روز رسانی شده باشد. برای رفع این مشکل می‌توان نوشت:
  incrementCount = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };
prevState در اینجا یعنی state قبلی و توسط متد setState، به صورت یک arrow function قابل دریافت است که در نهایت یک شیء را باز می‌گرداند. اکنون بر اساس state قبلی می‌توان به روز رسانی دقیقی را انجام داد.

این نکته در مورد کامپوننت‌های تابعی نیز وجود دارد:
  const incrementCount = () => {
    setCount(count + 1);
  };
در اینجا متغیر count، همانند خاصیت this.state.count کامپوننت‌های کلاسی عمل می‌کند. بنابراین الزامی ندارد که با هر بار فراخوانی setCount، مقدار جاری متغیر count، دقیقا به مقدار قبلی تنظیم شده‌ی آن اشاره کند. برای انجام مشابه اینکار با کامپوننت‌های تابعی که از useState استفاده می‌کنند، می‌توان به صورت زیر عمل کرد:
const incrementCount = () => {
    setCount(prevCount => prevCount + 1);
  };
در اینجا نیز امکان دسترسی به مقدار قبلی count، توسط یک arrow function وجود دارد که برخلاف حالت prevState، فقط یک مقدار عددی مرتبط با count را باز می‌گرداند.


به روز رسانی بیش از یک خاصیت در state

فرض کنید قصد داریم به مثال جاری، یک مربع را در صفحه اضافه کنیم که با کلیک بر روی آن، رنگش تغییر می‌کند (خاموش و روشن می‌شود):


در حالت AppClass یا کامپوننت کلاسی، کدهای برنامه به صورت زیر تغییر می‌کنند:
import "./App.css";

import React, { Component } from "react";

class App extends Component {
  state = {
    count: 0,
    isOn: false
  };

  incrementCount = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };

  toggleLight = () => {
    this.setState(prevState => ({
      isOn: !prevState.isOn
    }));
  };

  render() {
    return (
      <>
        <h1>App Class</h1>
        <h2>Counter</h2>
        <button onClick={this.incrementCount} className="btn btn-primary m-3">
          I was clicked {this.state.count} times!
        </button>

        <h2>Toggle Light</h2>
        <div
          style={{
            height: "50px",
            width: "50px",
            cursor: "pointer"
          }}
          className={
            this.state.isOn ? "alert alert-info m-3" : "alert alert-warning m-3"
          }
          onClick={this.toggleLight}
        />
      </>
    );
  }
}

export default App;
توضیحات:
- در متد رندر، نیاز است تا تنها یک child، بازگشت داده شود. به همین جهت می‌توان از React.Fragment، برای محصور سازی المان‌های تعریف شده، استفاده کرد. البته در React 16.7.0، دیگر نیازی به ذکر صریح React.Fragment نبوده و فقط می‌توان نوشت </><> تا بیانگر یک فرگمنت باشد.
- سپس یک div تعریف شده که با استفاده از ویژگی style، یک سری شیوه‌نامه‌ی ابتدایی، مانند طول و عرض و نوع اشاره‌گر ماوس آن، تعیین شده‌اند.
- اکنون برای اینکه بتوان با کلیک بر روی این div، رنگ آن‌را تغییر داد، نیاز است بتوان توسط متغیری، مقدار خاموش و روشن بودن را ردیابی کرد. به همین جهت خاصیت جدید isOn را به state اضافه می‌کنیم.
- در آخر، رویداد onClick این div را به متد رویدادگران toggleLight متصل می‌کنیم تا توسط آن و فراخوانی this.setState، بتوان مقدار قبلی isOn را در state، دریافت و سپس آن‌را معکوس کرد و بجای مقدار جاری isOn در state درج کنیم. این فراخوانی، سبب رندر مجدد کامپوننت جاری شده و در نتیجه‌ی آن، مقدار className را بر اساس مقدار this.state.isOn، به صورت پویا تغییر می‌دهد.

برای مشاهده‌ی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول App‍Class تامین شود:
import App from "./App‍Class";
//import App from "./AppFunction";

اکنون قصد داریم دقیقا معادل همین قطعه کد را در کامپوننت تابعی خود پیاده سازی کنیم. به همین جهت به فایل src\AppFunction.js بازگشته و تغییرات زیر را اعمال می‌کنیم:
import React, { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const [isOn, setIsOn] = useState(false);

  const incrementCount = () => {
    setCount(prevCount => prevCount + 1);
  };

  const toggleLight = () => {
    setIsOn(prevIsOn => !prevIsOn);
  };

  return (
    <>
      <h1>App Function</h1>
      <h2>Counter</h2>
      <button onClick={incrementCount} className="btn btn-primary m-3">
        I was clicked {count} times!
      </button>

      <h2>Toggle Light</h2>
      <div
        style={{
          height: "50px",
          width: "50px",
          cursor: "pointer"
        }}
        className={isOn ? "alert alert-info m-3" : "alert alert-warning m-3"}
        onClick={toggleLight}
      />
    </>
  );
};

export default App;
توضیحات:
- اگر دقت کنید، کلیات این کامپوننت تابعی، با کامپوننت کلاسی، آنچنان متفاوت نیست. متد رندر آن دقیقا همان markup را بازگشت می‌دهد؛ با یک تفاوت مهم: در اینجا دیگر نیازی به ذکر thisها نیست، چون تمام ارجاعات، به متغیرهای داخل تابع App انجام شده‌است و نه به متدها و یا خاصیت‌های یک کلاس.
- مرحله‌ی بعد تغییر، جایگزینی this.state.isOn قبلی، با یک متغیر درون تابع App است. به همین جهت در اینجا یک useState دیگر را تعریف می‌کنیم. هر useState، تنها به قسمتی از state اشاره می‌کند و مانند خاصیت کلی state مربوط به یک کلاس نیست. همچنین در خاصیت state یک کلاس، مقدار آن همواره به یک شیء اشاره می‌کند؛ اما با useState چنین اجباری وجود ندارد و می‌تواند هر نوع مجاز جاوا اسکریپتی باشد. برای مثال در اینجا مقدار اولیه‌ی آن به false تنظیم شده‌است. پس از آن، خروجی این متد، قسمتی از state را که کنترل می‌کند، به نام متغیر isOn و تنظیم کننده‌ی آن‌را به نام متد setIsOn، معرفی خواهد کرد. متد useState، یک متد جنریک است. یعنی نوع خروجی‌های آن بر اساس نوع مقدار اولیه‌ای که به آن انتساب داده می‌شود، تعیین می‌شود. برای مثال اگر نوع مقدار اولیه‌ی آن، Boolean باشد، به صورت خودکار نوع متغیر و پارامتر متد خروجی از آن نیز Boolean خواهند بود.
- در آخر خاصیت toggleLight کلاس کامپوننت، تبدیل به یک متغیر یا ثابت، در تابع جاری App می‌شود و بجای this.setState کلی قبلی، از متد اختصاصی‌تر setIsOn، برای تغییر مقدار state متناظر، کمک گرفته خواهد شد. در اینجا با استفاده از prevIsOn، به مقدار دقیق پیشین متغیر isOn در state، دسترسی یافته و سپس آن‌را معکوس می‌کنیم.

برای مشاهده‌ی خروجی برنامه در این حالت، نیاز است به index.js مراجعه و تغییر زیر را اعمال کرد تا کامپوننت App، از ماژول App‍Function تامین شود:
// import App from "./App‍Class";
import App from "./AppFunction";


معرفی useEffect Hook

فرض کنید قصد داریم برچسب دکمه‌ی «I was clicked {this.state.count} times» را در المان Title صفحه، نمایش دهیم. برای انجام چنین کاری نیاز است با DOM API تعامل داشته باشیم؛ اما پیشتر یک چنین کاری را تنها با class components می‌شد انجام داد. برای رفع این محدودیت در کامپوننت‌های تابعی، hook جدیدی به نام useEffect، ارائه شده‌است که باید import شود:
import React, { useEffect, useState } from "react";
اکنون این سؤال مطرح می‌شود که در متد useEffect، واژه‌ی Effect به چه چیزی اشاره می‌کند؟
در اینجا effect به معنای side effect و یا اثرات جانبی است؛ مانند: تعامل با یک API خارجی، کار با API مرورگر و کلا هر جائیکه در برنامه با دنیای خارج تعاملی وجود دارد، به عنوان یک side effect شناخته می‌شود. بنابراین با استفاده متد useEffect، می‌توان در کامپوننت‌های تابعی نیز با دنیای خارج، تعامل برقرار کرد.
import React, { useEffect, useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You have clicked ${count} times`;
  });
متد useEffect، یک تابع را به عنوان ورودی دریافت کرده (effect function) و آن‌را پس از هر بار رندر کامپوننت جاری، اجرا می‌کند. برای مثال با تغییر state، کار رندر کامپوننت جاری، در صف قرار می‌گیرد و پس از اتمام رندر، تابع effect ذکر شده، اجرا می‌شود. برای مثال در اینجا پس از هر بار رندر کامپوننت،  document.title با عبارتی که به همراه تعداد بار کلیک کردن بر روی دکمه‌است، به روز رسانی می‌شود:


در اولین بار اجرای برنامه، عبارت «You have clicked 0 times»، در عنوان صفحه‌ی جاری، ظاهر می‌شود که از مقدار پیش‌فرض count، استفاده کرده‌است. اکنون اگر بر روی دکمه، کلیک کنیم، پس از تغییر برچسب دکمه (یا همان رندر کامپوننت، پس از تغییری در state)، آنگاه عنوان نمایش داده شده‌ی در مرورگر نیز تغییر می‌کند.

یک نکته: چون useEffect دارای همان scope متغیر count است، نیاز به API خاصی برای خواندن مقدار آن در اینجا نیست.

سؤال: برای پیاده سازی چنین قابلیتی در یک کامپوننت کلاسی چه باید کرد؟ در این مثال، در ابتدای کار باید مقدار پیش‌فرض موجود در state را در عنوان صفحه مشاهده کرد و پس از هربار به روز رسانی state نیز باید این عنوان، تغییر کند.
برای پیاده سازی معادل متد useEffect ای که در یک کامپوننت تابعی استفاده شد، در اینجا باید از دو life-cycle hook متفاوت، به نام‌های componentDidMount و componentDidUpdate، استفاده کنیم:
class App extends Component {

  componentDidMount() {
    document.title = `You have been clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You have been clicked ${this.state.count} times`;
  }
- برای به روز رسانی عنوان صفحه، در اولین بار نمایش آن، باید از متد componentDidMount استفاده کرد.
- همچنین چون می‌خواهیم به ازای هر تغییری در state نیز این عنوان تغییر کند (با هر بار کلیک بر روی دکمه)، باید از متد componentDidUpdate هم استفاده کنیم.


پاکسازی اثرات جانبی در useEffect Hook

فرض کنید قصد داریم موقعیت فعلی کرسر ماوس را در مرورگر نمایش دهیم. برای انجام اینکار در کامپوننت کلاسی، می‌توان از متد componentDidMount جهت دسترسی به DOM API و استفاده از متد addEventListener آن، برای گوش فرا دادن به حرکات کرسر ماوس، استفاده کرد:
class App extends Component {

  componentDidMount() {
    // ...
    window.addEventListener("mousemove", this.handleMouseMove);
  }
 دومین پارامتر این متد، یک callback function است که این حرکات را ردیابی می‌کند:
  handleMouseMove = event => {
    this.setState({
      x: event.pageX,
      y: event.pageY
    });
  };
در اینجا می‌توان با استفاده از شیء رخداد دریافتی، موقعیت x و y کرسر ماوس را در صفحه دریافت کرد و سپس با فراخوانی متد this.setState، این خواص را در state کامپوننت، اضافه و یا به روز رسانی نمود. بنابراین در این حالت الزامی به تعریف این خواص در شیء منتسب به state نیست؛ اما حداقل با تعریف آن‌ها می‌توان مقدار اولیه‌ای را برایشان درنظر گرفت:
class App extends Component {
  state = {
    //...
    x: 0,
    y: 0
  };
در آخر برای نمایش این اطلاعات موجود در state، می‌توان چنین المان‌هایی را به متد رندر کامپوننت، اضافه کرد:
<h2>Mouse Position</h2>
<p>X position: {this.state.x}</p>
<p>Y position: {this.state.y}</p>
بنابراین ردیابی تغییرات محل کرسر ماوس نیز یک side effect است که برای دسترسی به آن، نیاز است با window API کار کرد و این side effect‌ها دو نوع هستند:
- تعدادی از آن‌ها نیازی به پاکسازی و خارج شدن از حافظه را ندارند؛ مانند به روز رسانی عنوان صفحه در مرورگر. می‌توان یک چنین side effect هایی را اجرا و سپس آن‌ها را فراموش کرد.
- اما تعدادی از side effect‌ها را حتما باید پاکسازی کرد؛ مانند mousemove listener تعریف شده‌ی در مثال فوق. در اینجا زمانیکه کامپوننت جاری mount می‌شود، این listener را تعریف می‌کنیم؛ اما با Unmount شدن آن، باید این listener را حذف کرد تا برنامه دچار نشتی حافظه نشود (اگر اینکار انجام نشود، در این مثال مرورگر هنگ خواهد کرد!). روش انجام اینکار در متد componentWillUnmount، به صورت زیر است:
  componentWillUnmount() {
    window.removeEventListener("mousemove", this.handleMouseMove);
  }
سؤال: یک چنین قابلیتی را چگونه می‌توان در یک کامپوننت تابعی تعریف کرد؟
در این حالت نیز می‌توان از متد useEffect استفاده کرد. البته ابتدا باید state شیء ای را برای نگهداری اطلاعات به روز موقعیت مکانی کرسر ماوس، ایجاد کرد:
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
همانطور که عنوان شد، متد useState، برخلاف خاصیت کلی state یک کامپوننت کلاسی که فقط اشیاء را می‌پذیرد، هر نوع جاوا اسکریپتی را می‌تواند بپذیرد که در اینجا برای نمونه به یک شیء، تنظیم شده‌است.
سپس side effect خود را در قسمت effect function متد useEffect قرار می‌دهیم که آن نیز به متغیر handleMouseMove اشاره می‌کند:
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    // ...
    window.addEventListener("mousemove", handleMouseMove);
  });

  const handleMouseMove = event => {
    setMousePosition({
      x: event.pageX,
      y: event.pageY
    });
  };
به متغیر handleMouseMove، متدی انتساب داده شده‌است که با فراخوانی آن توسط mousemove listener، یک شیء evenet دریافت و سپس بر اساس خواص آن، خواص x و y شیء موجود در state، توسط متد setMousePosition، به روز رسانی می‌شود.
سپس برای نمایش x و y به روز رسانی شده‌ی در state، می‌توان از markup زیر در متد رندر استفاده کرد.
<h2>Mouse Position</h2>
{JSON.stringify(mousePosition, null, 2)}
<br />
اکنون سؤال اینجا است که معادل متد componentWillUnmount در اینجا چیست؟
اگر effect function تعریف شده، دارای یک خروجی (از نوع تابع) باشد، به این معنا است که این side effect، نیاز به پاکسازی دارد و این متد را در زمان Unmount آن فراخوانی می‌کند:
  useEffect(() => {
    // …
    // componentDidMount & componentDidUpdate
    window.addEventListener("mousemove", handleMouseMove);

    // componentWillUnmount
    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
    };
  });
پیشتر آموختیم که effect function، تنها یکبار اجرا نمی‌شود؛ بلکه به ازای هر بار رندر، یکبار پس از آن اجرا می‌شود. یعنی خروجی آن نه فقط در زمان Unmount اجرا می‌شود، بلکه یکبار هم پیش از هر بار اجرای خود effect اجرا می‌گردد. به این ترتیب در اینجا فرصت پاکسازی اجرای قبلی را نیز خواهیم داد. بنابراین روش پاکسازی آن نسبت به متد componentWillUnmount کامپوننت‌های تابعی، پیشرفته‌تر است. یعنی توسط آن می‌توان یک side effect خاص را پیش و پس از هر بار رندر، در صورت نیاز فراخوانی کرد.

سؤال: اگر بخواهیم از اجرای یک side effect، به ازای هر بار رندر جلوگیری کنیم، چه باید کرد؟

برای اینکار می‌توان آرگومان دومی را به متد useEffect اضافه کرد که آرایه‌ای از مقادیر است. توسط اعضای آن می‌توان مقدار و یا مقادیری را مشخص کرد که side effect تعریف شده، به آن وابسته‌است. اکنون اگر این مقدار تغییر کند، آنگاه side effect متناظر با آن نیز اجرا می‌شود:
  useEffect(() => {
    document.title = `You have clicked ${count} times`;
    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove);
    };
  },[]);
در این مثال، چون پارامتر دوم را به صورت یک آرایه‌ی خالی مقدار دهی کرده‌ایم، effect function تعریف شده تنها در زمان mount و unmount اجرا می‌شود. البته اگر این تغییر را ذخیره کرده و برنامه را اجرا کنیم، تمام قسمت‌های تعریف شده‌ی آن به خوبی کار می‌کنند (با کلیک بر روی دکمه، برچسب آن به روز رسانی می‌شود و یا با حرکت کرسر ماوس در صفحه، موقعیت آن گزارش داده می‌شود)، منهای قسمت به روز رسانی عنوان صفحه با هر بار کلیک. علت اینجا است که با تغییر معرفی شده، دیگر حالت componentDidUpdate توسط متد useEffect پوشش داده نمی‌شود و به این ترتیب با هر بار به روز رسانی state و رندر کامپوننت، دیگر کار به اجرای مجدد side effect آن نمی‌رسد.
برای رفع این مشکل، باید به useEffect اعلام کنیم که side effect تعریف شده‌ی در آن، وابسته‌است به مقدار count و با تغییرات آن در state، نیاز است مجددا اجرا شود:
  useEffect(() => {
   // ...
  },[count]);
به همین جهت در آرایه‌ی تعریف شده، مقداری را که به آن وابستگی وجود دارد، مشخص می‌کنیم. با این تغییر اگر بر روی دکمه کلیک کنیم، چون اکنون useEffect می‌داند که باید تغییرات count را ردیابی کند، با اجرای مجدد effect function خود، عنوان صفحه نیز به روز رسانی خواهد شد.


کار با چندین listener مختلف در متد useEffect

سؤال: آیا تنظیم یک وابستگی خاص در متد useEffect، امکان تنظیم event listenerهای دیگر را غیرممکن می‌کند؟
برای پاسخ به این سؤال، چند event listener دیگر را ثبت می‌کنیم. برای مثال یکی دیگر از APIهای مرورگر، navigator نام دارد که توسط آن می‌توان وضعیت آنلاین و آفلاین بودن را به کمک خروجی خاصیت navigator.onLine آن، مشخص کرد. به کمک این API می‌خواهیم این وضعیت را نمایش دهیم. برای این منظور ابتدا state آن‌را در کامپوننت تابعی، ایجاد می‌کنیم:
const [status, setStatus] = useState(navigator.onLine);
مقدار اولیه‌ی این state را نیز توسط مقدار جاری خاصیت navigator.onLine، مشخص کرده‌ایم.
اکنون برای گوش فرا دادن به تغییرات این خاصیت (online و یا offline شدن کاربر)، نیاز است دو event listener را به کمک متد addEventListener ثبت کنیم و همچنین این متدها نیاز به پاکسازی هم دارند:
  useEffect(() => {
    // ...

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      // ...
  
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, [count]);
پس از ثبت event listener ها، اکنون می‌توان با هربار فراخوانی آن‌ها توسط مرورگر، وضعیت state آن‌را به true و یا false، تغییر داد:
  const handleOnline = () => {
    setStatus(true);
  };

  const handleOffline = () => {
    setStatus(false);
  };
و در آخر مقدار متغیر status را به نحو زیر در متد رندر، نمایش داد:
<h2>Network Status</h2>
<p>
  You are <strong>{status ? "online" : "offline"}</strong>
</p>

برای آزمایش حالت offline آن، فقط کافی است به ابزار توسعه دهندگان مرورگر مراجعه کرده و در برگه‌ی network آن، حالت online را offline کنید:


در این حالت هم نمایش وضعیت آنلاین بودن کاربر به درستی کار می‌کند و هم سایر قسمت‌هایی که تاکنون اضافه کرده‌ایم. به این معنا که هر چند توسط ذکر پارامتر [count]، وابستگی خاصی برای side effect ویژه‌ای، مشخص شده‌است، اما ما را از تنظیم event listener‌های دیگری در قسمت‌های mount و unmount محروم نمی‌کند.


پاکسازی اثرات جانبی در useEffect Hook، زمانیکه روشی برای آن وجود ندارد

در مثالی دیگر می‌خواهیم از API موقعیت جغرافیایی کاربر در مرورگر یا navigator.geolocation استفاده کنیم. توسط این API هم می‌توان طول و عرض جغرافیایی را به دست آورد و هم تغییرات آن‌را تحت نظر قرار داد. برای مثال با بررسی این تغییرات می‌توان سرعت را نیز به دست آورد.
در این حالت نیز ابتدا با تعریف state مختص به آن شروع می‌کنیم و اینبار به عنوان مثال، مقدار اولیه‌ی آن‌را خارج از تابع جاری تنظیم می‌کنیم (جهت نمایش یک گزینه‌ی مهیای دیگر):
const initialLocationState = {
  latitude: null,
  longitude: null,
  speed: null
};

const App = () => {
  // ...
  const [location, setLocation] = useState(initialLocationState);
سپس جهت کار با این API، مقدار اولیه‌ی موقعیت کاربر و همچنین ردیابی تغییرات آن‌را در متد useEffect، تنظیم می‌کنیم:
  useEffect(() => {
    // ...

    navigator.geolocation.getCurrentPosition(handleGeolocation);
    const watchId = navigator.geolocation.watchPosition(handleGeolocation);

    return () => {
      // ...
      navigator.geolocation.clearWatch(watchId);
    };
  }, [count]);
در اینجا متد getCurrentPosition، با متد addEventListener متفاوت است و تنها یک callback function را دریافت می‌کند که در آن می‌توان setLocation را جهت به روز رسانی موقعیت جاری جغرافیایی کاربر، فراخوانی کرد. مشکلی که با این متد وجود دارد، نداشتن معادلی شبیه به متد removeEventListener است. یعنی API آن روشی را برای unmount آن فراهم نکرده‌است. البته متد watchPosition که تغییرات موقعیت کاربر را ردیابی می‌کند، دارای API مخصوص پاکسازی آن نیز هست که در این کدها ذکر شده‌است.

یک روش برای حل این مشکل و غیرفعال کردن دستی listener متد getCurrentPosition پس از unmount، تعریف یک متغیر mounted پیش از متد useEffect است:
  let mounted = true;

  useEffect(() => {
   // ...

    return () => {
      // ...
      mounted = false;
    };
  }, [count]);
و آن‌را در زمان پاکسازی useEffect، با false مقدار دهی می‌کنیم. اکنون زمانیکه می‌خواهیم رویدادگردان این Listener را تعریف کنیم، تنها بر اساس مقدار متغیر mounted عمل خواهیم کرد که از نشتی حافظه‌ی آن جلوگیری می‌کند:
  const handleGeolocation = event => {
    if (mounted) {
      setLocation({
        latitude: event.coords.latitude,
        longitude: event.coords.longitude,
        speed: event.coords.speed
      });
    }
  };
در آخر هم برای نمایش آن در متد رندر به صورت زیر عمل می‌کنیم:
<h2>Geolocation</h2>
<p>Latitude is {location.latitude}</p>
<p>Longitude is {location.longitude}</p>
<p>Your speed is {location.speed ? location.speed : "0"}</p>

سؤال: در اینجا شیء location چندین بار تکرار شده‌است. آیا می‌توان مقادیر خواص آن‌را توسط Object Destructuring نیز به دست آورد؟
پاسخ: بله. در اینجا هم Object Destructuring بر روی location state، کار می‌کند:
const [{ latitude, longitude, speed }, setLocation] = useState(
    initialLocationState
  );
که پس از آن، دیگر نیازی به تکرار شیء location نخواهد بود:
<h2>Geolocation</h2>
<p>Latitude is {latitude}</p>
<p>Longitude is {longitude}</p>
<p>Your speed is {speed ? speed : "0"}</p>


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-30-part-01.zip
مطالب
بررسی تغییرات Blazor 8x - قسمت ششم - نکات تکمیلی ویژگی راهبری بهبود یافته‌ی صفحات SSR


در قسمت قبل، در حین بررسی رفتار جزیره‌های تعاملی Blazor Server، نکته‌ی زیر را هم درباره‌ی راهبری صفحات SSR مرور کردیم:
« اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمی‌شود (و یا حتی برنامه‌های MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت می‌گیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم به‌روز رسانی نمی‌کند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.»
در این قسمت، نکات تکمیلی این قابلیت را که به آن enhanced navigation هم گفته می‌شود، بررسی می‌کنیم.


روش غیرفعال کردن راهبری بهبودیافته برای بعضی از لینک‌ها

ویژگی راهبری بهبودیافته فقط در حین هدایت بین صفحات مختلف یک برنامه‌ی Blazor 8x SSR، فعال است. اگر در این بین، کاربری به یک صفحه‌ی غیر بلیزری هدایت شود، راهبری بهبود یافته شکست خورده و سعی می‌کند حالت full document load را پیاده سازی و اجرا کند. مشکل اینجاست که در این حالت دو درخواست ارسال می‌شود: ابتدا حالت راهبری بهبودیافته فعال می‌شود و در ادامه پس از شکست این راهبری، هدایت مستقیم صورت می‌گیرد. برای رفع این مشکل می‌توان ویژگی جدید data-enhance-nav را با مقدار false، به لینک‌های خارجی مدنظر اضافه کرد تا برای این حالت‌ها دیگر ویژگی راهبری بهبودیافته فعال نشود:
<a href="/not-blazor" data-enhance-nav="false">A non-Blazor page</a>


فعالسازی مدیریت بهبودیافته‌ی فرم‌های SSR

در قسمت چهارم این سری با فرم‌های جدید SSR مخصوص Blazor 8x آشنا شدیم. این فرم‌ها هم می‌توانند از امکانات راهبری بهبود یافته استفاده کنند (یعنی مدیریت ارسال آن، توسط fetch API انجام شده و به روز رسانی قسمت‌های تغییریافته‌ی صفحه را Ajax ای انجام دهند)؛ برای نمونه اینبار همانند تصویر زیر، از fetch استاندارد برای ارسال اطلاعات به سمت سرور کمک گرفته می‌شود (یعنی عملیات Ajax ای شده‌؛ بجای یک post-back معمولی):


 اما ... این قابلیت به صورت پیش‌فرض در فرم‌های تعاملی SSR غیرفعال است. چون همانطور که عنوان شد، اگر مقصد این فرم، یک آدرس غیربلیزری باشد، دوبار ارسال فرم صورت خواهد گرفت؛ یکبار با استفاده از fetch API و بار دیگر پس از شکست، به صورت معمولی. اما اگر مطمئن هستید که endpoint این فرم، قطعا یک کامپوننت بلیزری است، بهتر است این قابلیت را در یک چنین فرم‌هایی نیز به صورت زیر فعال کنید:
<form method="post" @onsubmit="() => submitted = true" @formname="name" data-enhance>
    <AntiforgeryToken />
    <InputText @bind-Value="Name" />
    <button>Submit</button>
</form>

@if (submitted)
{
    <p>Hello @Name!</p>
}

@code {

    bool submitted;

    [SupplyParameterFromForm]
    public string Name { get; set; } = "";
}
البته باتوجه به اینکه در اینجا هم می‌توان از EditForm‌ها استفاده کرد، در این حالت فقط کافی است ویژگی Enhance را به آن‌ها اضافه نمائید:
<EditForm method="post" Model="NewCustomer" OnValidSubmit="() => submitted = true" FormName="customer" Enhance>
    <DataAnnotationsValidator />
    <ValidationSummary/>
    <p>
        <label>
            Name: <InputText @bind-Value="NewCustomer.Name" />
        </label>
    </p>
    <button>Submit</button>
</EditForm>

@if (submitted)
{
    <p id="pass">Hello @NewCustomer.Name!</p>
}

@code {
    bool submitted = false;

    [SupplyParameterFromForm]
    public Customer? NewCustomer { get; set; }

    protected override void OnInitialized()
    {
        NewCustomer ??= new();
    }

     public class Customer
    {
        [StringLength(3, ErrorMessage = "Name is too long")]
        public string? Name { get; set; }
    }
}

نکته‌ی مهم: در این حالت فرض بر این است که هیچگونه هدایتی به یک Non-Blazor endpoint صورت نمی‌گیرید؛ وگرنه با یک خطا مواجه خواهید شد.



غیرفعال کردن راهبری بهبودیافته برای قسمتی از صفحه

اگر با استفاده از جاواسکریپت و خارج از کدهای بیلزر، اطلاعات DOM را به‌روز رسانی می‌کنید، ویژگی راهبری بهبودیافته، از آن آگاهی نداشته و به صورت خودکار تمام تغییرات شما را بازنویسی می‌کند. به همین جهت اگر نیاز است قسمتی از صفحه را که مستقیما توسط کدهای جاواسکریپتی تغییر می‌دهید، از به‌روز رسانی‌های این قابلیت مصون نگه‌دارید، می‌توانید ویژگی جدید data-permanent را به آن قسمت اضافه کنید:
<div data-permanent>
    Leave me alone! I've been modified dynamically.
</div>


امکان آگاه شدن از بروز راهبری بهبودیافته در کدهای جاواسکریپتی

اگر به هردلیلی در کدهای جاواسکریپتی خودنیاز به آگاه شدن از وقوع یک هدایت بهبودیافته را دارید (برای مثال جهت بازنویسی تغییرات ایجاد شده‌ی توسط آن)، می‌توانید به نحو زیر، مشترک رخ‌دادهای آن شوید:
<script>
    Blazor.addEventListener('enhancedload', () => {
        console.log('enhanced load event occurred');
    });
</script>


ویژگی جدید Named Element Routing در Blazor 8x

Blazor 8x از ویژگی مسیریابی سمت کلاینت به کمک تعریف URL fragments پشتیبانی می‌کند. به این صورت رسیدن (اسکرول) به یک قسمت از صفحه‌ای طولانی، بسیار ساده می‌شود.
برای مثال المان h2 با id مساوی targetElement را درنظر بگیرید:
<div class="border border-info rounded bg-info" style="height:500px"></div>

<h2 id="targetElement">Target H2 heading</h2>
<p>Content!</p>
در Blazor 8x برای رسیدن به آن، هر سه حالت زیر میسر هستند:
<a href="/counter#targetElement">

<NavLink href="/counter#targetElement">

Navigation.NavigateTo("/counter#targetElement");


معرفی متد جدید Refresh در Blazor 8x

در Blazor 8x، امکان بارگذاری مجدد صفحه با فراخوانی متد جدید NavigationManager.Refresh(bool forceLoad = false) میسر شده‌است. این متد در حالت پیش‌فرض از قابلیت راهبری بهبودیافته برای به روز رسانی صفحه استفاده می‌کند؛ مگر اینکه اینکار میسر نباشد. اگر آن‌را با پارامتر true فراخوانی کنید، full-page reload رخ خواهد داد.
همین اتفاق در مورد متد Navigation.NavigateTo نیز رخ‌داده‌است. این متد نیز در Blazor 8x به صورت پیش‌فرض بر اساس قابلیت راهبری بهبود یافته کار می‌کند؛ مگر اینکه اینکار میسر نباشد و یا پارامتر forceLoad آن‌را به true مقدار دهی کنید.