مطالب
React 16x - قسمت 18 - کار با فرم‌ها - بخش 1 - دریافت ورودی‌ها از کاربر
تقریبا تمام برنامه‌ها نیاز دارند فرم‌های مخصوصی را داشته باشند. به همین جهت در این قسمت، برنامه‌ی نمایش لیست فیلم‌ها را که تا این مرحله تکمیل کردیم، با افزودن تعدادی فرم بهبود می‌بخشیم؛ مانند فرم لاگین، فرم ثبت نام، فرمی برای ثبت و ویرایش فیلم‌ها و یک فرم جستجوی سریع در لیست فیلم‌های موجود.


ایجاد فرم لاگین

فرم لاگینی را که به برنامه‌ی نمایش لیست فیلم‌های تکمیل شده‌ی تا قسمت 17، اضافه خواهیم کرد، یک فرم بوت استرپی است و می‌توانید جزئیات بیشتر مزین سازی المان‌های این نوع فرم‌ها را با کلاس‌های بوت استرپ، در مطلب «کار با شیوه‌نامه‌های فرم‌ها در بوت استرپ 4» مطالعه کنید.
در ابتدا فایل جدید src\components\loginForm.jsx را ایجاد کرده و سپس توسط میان‌برهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت جدید LoginForm را ایجاد می‌کنیم:
import React, { Component } from "react";


class LoginForm extends Component {
  render() {
    return <h1>Login</h1>;
  }
}

export default LoginForm;
در ادامه یک Route جدید را در فایل app.js برای این فرم، با مسیر login/ و کامپوننت LoginForm، در ابتدای Switch موجود، تعریف می‌کنیم:
import LoginForm from "./components/loginForm";
//...

function App() {
  return (
    <React.Fragment>
      <NavBar />
      <main className="container">
        <Switch>
          <Route path="/login" component={LoginForm} />
          <Route path="/movies/:id" component={MovieForm} />
          // ...
        </Switch>
      </main>
    </React.Fragment>
  );
}
پس از تعریف این مسیریابی، نیاز است لینک آن‌را نیز به منوی راهبری سایت اضافه کنیم. به همین جهت در فایل navBar.jsx که آن‌را در قسمت قبل تکمیل کردیم، در انتهای لیست موجود و پس از Rentals، لینک لاگین را نیز قرار می‌دهیم:
<NavLink className="nav-item nav-link" to="/login">
   Login
</NavLink>
که در نهایت حاصل این تغییرات، به صورت زیر در مرورگر ظاهر می‌شود:


اکنون نوبت به افزودن فرم بوت استرپی لاگین به فایل loginForm.jsx رسیده‌است:
import React, { Component } from "react";


class LoginForm extends Component {
  render() {
    return (
      <form>
        <div className="form-group">
          <label htmlFor="username">Username</label>
          <input id="username" type="text" className="form-control" />
        </div>
        <div className="form-group">
          <label htmlFor="password">Password</label>
          <input id="password" type="password" className="form-control" />
        </div>
        <button className="btn btn-primary">Login</button>
      </form>
    );
  }
}

export default LoginForm;
توضیحات:
- ابتدا المان form به صفحه اضافه می‌شود.
- سپس هر ورودی، داخل یک div با کلاس form-group، محصور می‌شود. کار آن تبدیل یک برچسب و فیلد ورودی، به یک گروه از ورودی‌های بوت استرپ است.
- در اینجا هر برچسب دارای یک ویژگی for است. اما چون قرار است عبارات jsx، به معادل‌های جاوا اسکریپتی ترجمه شوند، نمی‌توان از واژه‌ی کلیدی for در اینجا استفاده کرد. به همین جهت از معادل react ای آن که htmlFor است، در کدهای فوق استفاده کرده‌ایم؛ شبیه به نکته‌ای که در مورد تبدیل ویژگی class به className وجود دارد. مقدار هر ویژگی htmlFor نیز به id فیلد ورودی متناظر با آن تنظیم می‌شود. به این ترتیب اگر کاربر بر روی این برچسب کلیک کرده و آن‌را انتخاب کند، فیلد متناظر با آن، دارای focus می‌شود.
- فیلدهای ورودی نیز دارای کلاس form-control هستند.

با این خروجی نهایی در مرورگر:



مدیریت ارسال فرم‌ها

به صورت پیش فرض و استاندارد، دکمه‌ی افزوده شده‌ی به المان form، سبب ارسال اطلاعات آن به سرور و سپس بارگذاری کامل صفحه می‌شود. این رفتاری نیست که در یک برنامه‌ی SPA مدنظر باشد. برای مدیریت این حالت، می‌توان از رخ‌داد onSubmit هر المان فرم، استفاده کرد:
class LoginForm extends Component {
  handleSubmit = e => {
    console.log("handleSubmit", e);
    e.preventDefault();

    // call the server
  };

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
      //...
در اینجا یک متد رویدادگردان را برای رخ‌داد onSubmit تعریف کرده‌ایم که توسط آن رخ‌داد جاری، دریافت و متد preventDefault آن فراخوانی می‌شود تا دیگر پس از کلیک بر روی دکمه‌ی submit، حالت پیش‌فرض و استاندارد full page reload و post back به سمت سرور، رخ ندهد.


دسترسی مستقیم به المان‌های فرم‌ها

پس از فراخوانی متد preventDefault، کار مدیریت ارسال فرم به سرور را باید خودمان مدیریت کنیم و دیگر رخ‌داد full post back استاندارد به سمت سرور را نخواهیم داشت. در جاوا اسکریپت خالص برای دریافت مقادیر وارد شده‌ی توسط کاربر می‌توان نوشت:
const username = document.getElementById("username").value;
اما در React و کدهای یک کامپوننت، نباید ارجاع مستقیمی را به شیء document و DOM اصلی مرورگر داشته باشیم. در برنامه‌های React هیچگاه نباید با شیء document کار کرد؛ چون کل فلسفه‌ی آن ایجاد یک abstraction بر فراز DOM اصلی مرورگر است که به آن DOM مجازی گفته می‌شود. به این ترتیب مدیریت برنامه و همچنین آزمون نویسی برای آن نیز ساده‌تر می‌شود. اما اگر واقعا نیاز به دسترسی به یک المان DOM در React وجود داشت، چه باید کرد؟
برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت، ایجاد کرده و آن‌را با React.RefObject، مقدار دهی اولیه می‌کنیم:
class LoginForm extends Component {
  username = React.createRef();
سپس ویژگی ref المان مدنظر را به این RefObject تنظیم می‌کنیم:
<input
  ref={this.username}
  id="username"
  type="text"
  className="form-control"
/>
اکنون زمان submit فرم، اگر نیاز به مقدار username وجود داشت، می‌توان توسط خاصیت ارجاعی username تعریف شده، به خاصیت current آن که DOM element مدنظر را بازگشت می‌دهد، دسترسی یافت و مانند مثال زیر، مقدار آن‌را مورد استفاده قرار داد:
  handleSubmit = e => {
    e.preventDefault();

    // call the server
    const username = this.username.current.value;
    console.log("handleSubmit", username);
  };

البته در حالت کلی باید استفاده‌ی از RefObjectها را به حداقل رساند (راه حل بهتری برای دریافت ورودی‌ها وجود دارد) و جاهائی از آن‌ها استفاده کرد که واقعا راه حل دیگری وجود ندارد؛ مانند تنظیم focus بر روی یک المان DOM. در این حالت حتما باید ارجاعی را از آن المان DOM در دسترس داشت و یا برای پویانمایی (animation) نیز مجبور به استفاده‌ی از RefObjectها هستیم.
برای نمونه روش تنظیم focus بر روی یک فیلد ورودی توسط RefObjectها به صورت زیر است:
class LoginForm extends Component {
  username = React.createRef();

  componentDidMount = () => {
    this.username.current.focus();
  };
در life-cycle hook ای به نام componentDidMount که پس از رندر کامپوننت در DOM فراخوانی می‌شود، می‌‌توان توسط RefObject تعریف شده، به شیء current که معادل DOM Element متناظر است، دسترسی یافت و سپس متد focus آن‌را فراخوانی کرد. در این حالت در اولین بار نمایش فرم، یک چنین تصویری حاصل می‌شود:


البته روش بهتری نیز برای انجام اینکار وجود دارد. المان‌های JSX دارای ویژگی autoFocus نیز هستند که دقیقا همین کار را انجام می‌دهد:
<input
  autoFocus
  ref={this.username}
  id="username"
  type="text"
  className="form-control"
/>
برای آزمایش آن، قطعه کد componentDidMount را کامنت کرده و برنامه را اجرا کنید.


تبدیل المان‌های فرم‌ها به Controlled elements

در بسیاری از اوقات، فرم‌های ما state خود را از سرور دریافت می‌کنند. فرض کنید که در حال ایجاد یک فرم ثبت اطلاعات فیلم‌ها هستیم. در این حالت باید بر اساس id فیلم، اطلاعات آن را از سرور دریافت و در state ذخیره کرد؛ سپس فیلدهای فرم را بر اساس آن مقدار دهی اولیه کرد. برای نمونه در فرم لاگین می‌توان state را با شیء account، به صورت زیر مقدار دهی اولیه کرد:
class LoginForm extends Component {
  state = {
    account: { username: "", password: "" }
  };
تا اینجا فیلدهای فرم لاگین، از این state مطلع نبوده و تغییرات داده‌های ورودی در آن‌ها، به شیء account منعکس نمی‌شوند. علت اصلی هم اینجا است که هر کدام از فیلدهای ورودی در React، دارای state خاص خود بوده و مستقل از state کامپوننت جاری هستند. برای رفع این مشکل باید آن‌ها را تبدیل به controlled element هایی کرد که دارای state خاص خود نبوده، تمام اطلاعات مورد نیاز خود را از طریق props دریافت می‌کنند و تغییرات در داده‌های خود را از طریق صدور رخ‌دادهایی اطلاع رسانی می‌کنند. برای اینکار باید مراحل زیر طی شوند:
ابتدا ویژگی value فیلد برای مثال username را به خاصیت username شیء account موجود در state متصل می‌کنیم:
<input 
  value={this.state.account.username}
به این ترتیب دیگر این المان، state خاص خود را نداشته و از طریق props، مقادیر خود را دریافت می‌کند. تا اینجا username، به رشته‌ی خالی دریافتی از شیء state و خاصیت account آن، به صورت یک طرفه متصل شده‌است. یعنی زمانیکه فرم نمایش داده می‌شود، دارای یک مقدار خالی است. برای اینکه تغییرات رخ‌داده‌ی در این المان را به state منعکس کرد، باید رخ‌داد change آن‌را مدیریت نمود. به این ترتیب زمانیکه کاربری اطلاعاتی را در اینجا وارد می‌کند، رخ‌داد change صادر شده و پس از آن می‌توان اطلاعات وارد شده را دریافت و state را به روز رسانی کرد. به روز رسانی state نیز سبب رندر مجدد فرم می‌شود. بنابراین فیلدهای ورودی، با اطلاعات state جدید، به روز رسانی و رندر می‌شوند. به همین جهت ابتدا رویداد onChange را به فیلد username اضافه کرده:
<input 
  value={this.state.account.username}
  onChange={this.handleChange}
و متد مدیریت کننده‌ی آن‌را به صورت زیر تعریف می‌کنیم:
  handleChange = e => {
    const account = { ...this.state.account }; //cloning an object
    account.username = e.currentTarget.value;
    this.setState({ account });
  };
در اینجا، هدف به روز رسانی this.state.account، بر اساس رخ‌داد رسیده (پارامتر e) است و چون نمی‌توان state را مستقیما به روز رسانی کرد، ابتدا یک clone از آن را تهیه می‌کنیم. سپس توسط e.currentTarget به المان در حال به روز رسانی دسترسی یافته و مقدار آن‌را به مقدار خاصیت username انتساب می‌دهیم. در آخر state را بر اساس این تغییرات، به روز رسانی می‌کنیم. این انعکاس در state را توسط افزونه‌ی react developer tools هم می‌توان مشاهده کرد:



مدیریت دریافت اطلاعات چندین فیلد ورودی

تا اینجا موفق شدیم اطلاعات state را به تغییرات فیلد username در فرم لاگین متصل کنیم؛ اما فیلد password را چگونه باید مدیریت کرد؟ برای اینکه تمام این مراحل را مجددا تکرار نکنیم، می‌توان از مقدار دهی پویای خواص در جاوا اسکریپت که توسط [] انجام می‌شود استفاده کرد:
  handleChange = e => {
    const account = { ...this.state.account }; //cloning an object
    account[e.currentTarget.name] = e.currentTarget.value;
    this.setState({ account });
  };
البته برای اینکه این قطعه کد کار کند، نیاز است ویژگی name فیلدهای ورودی را نیز تنظیم کرد تا e.currentTarget.name، به نام یکی از خواص شیء account تعریف شده‌ی در state اشاره کند. برای نمونه فیلد کلمه‌ی عبور، ابتدا دارای ویژگی value متصل به خاصیت password شیء account موجود در state می‌شود. سپس تغییرات آن توسط رویداد onChange، به متد handleChange منتقل شده و خاصیت name آن نیز مقدار دهی شده‌است تا مقدار دهی پویای خواص، در این متد میسر شود:
<input
  id="password"
  name="password"
  value={this.state.account.password}
  onChange={this.handleChange}
  type="password"
  className="form-control"
/>
که در نهایت سبب مقدار دهی صحیح state، با هر دو فیلد تغییر یافته می‌شود:


یک نکته: می‌توان توسط Object Destructuring، تکرار e.currentTarget را حذف کرد:
  handleChange = ({ currentTarget: input }) => {
    const account = { ...this.state.account }; //cloning an object
    account[input.name] = input.value;
    this.setState({ account });
  };
ما از شیء e دریافتی، تنها به خاصیت currentTarget آن نیاز داریم. بنابراین آن‌را از طریق Object Destructuring در همان پارامتر ورودی متد جاری دریافت کرده و سپس آن‌را به نام input، تغییر نام می‌دهیم.


آشنایی با خطاهای متداول دریافتی در حین کار با فرم‌ها

فرض کنید خاصیت username را از شیء account موجود در state حذف کرده‌ایم. در زمان نمایش ابتدایی فرم، خطایی را دریافت نخواهیم کرد، اما اگر اطلاعاتی را در آن وارد کنیم، بلافاصله در کنسول توسعه دهندگان مرورگر چنین اخطاری ظاهر می‌شود:
Warning: A component is changing an uncontrolled input of type text to be controlled.
Input elements should not switch from uncontrolled to controlled (or vice versa).
Decide between using a controlled or uncontrolled input element for the lifetime of the component.
More info: https://fb.me/react-controlled-components
چون خاصیت username را حذف کرده‌ایم، اینبار که در textbox مقداری را وارد می‌کنیم، سبب انتساب undefined و یا null به مقدار المان خواهد شد. در این حالت React چنین المانی را به صورت controlled element درنظر نمی‌گیرد و دارای state خاص خودش خواهد بود. به همین جهت عنوان می‌کند که بین یک المان کنترل شده و نشده، یکی را انتخاب کنید.
دقیقا چنین اخطاری را با ورود null/undefined بجای "" در حین مقدار دهی اولیه‌ی username در شیء account نیز دریافت خواهیم کرد:
Warning: `value` prop on `input` should not be null.
Consider using an empty string to clear the component or `undefined` for uncontrolled components.
بنابراین به عنوان یک قاعده در فرم‌های React، المان‌های یک فرم را باید توسط یک "" مقدار دهی اولیه کرد و یا با مقداری که از سمت سرور دریافت می‌شود.


ایجاد یک کامپوننت ورود اطلاعات با قابلیت استفاده‌ی مجدد

هر چند در پیاده سازی فعلی سعی کردیم با بکارگیری مقداردهی پویای خواص اشیاء، تکرار کدها را کاهش دهیم، اما باز هم به ازای هر فیلد ورودی باید این مسایل تکرار شوند:
- ایجاد یک div با کلاس‌های بوت استرپی.
- ایجاد label و همچنین فیلد ورودی.
- در اینجا مقدار htmlFor باید با مقدار id فیلد ورودی یکی باشد.
- مقدار دهی ویژگی‌های value و onChange نیز باید تکرار شوند.

بنابراین بهتر است این تعاریف را استخراج و به یک کامپوننت با قابلیت استفاده‌ی مجدد منتقل کرد. به همین جهت فایل جدید src\components\common\input.jsx را در پوشه‌ی common ایجاد کرده و سپس توسط میانبرهای imrc و sfc، این کامپوننت تابعی بدون حالت را تکمیل می‌کنیم:
import React from "react";

const Input = ({ name, label, value, onChange }) => {
  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <input
        value={value}
        onChange={onChange}
        id={name}
        name={name}
        type="text"
        className="form-control"
      />
    </div>
  );
};

export default Input;
در اینجا کل تگ div مرتبط با username را از کامپوننت فرم لاگین cut کرده و در اینجا در قسمت return، قرار داده‌ایم. سپس شروع به تبدیل مقادیر قبلی به مقادیری که قرار است از props تامین شوند، کرده‌ایم. یا می‌توان props را به عنوان آرگومان این متد تعریف کرد و یا می‌توان توسط Object Destructuring، خواصی را که از props نیاز داریم، در پارامتر متد Input ذکر کنیم که این روش چون به نوعی اینترفیس کامپوننت را نیز مشخص می‌کند و همچنین کدهای تکراری دسترسی به props را به حداقل می‌رساند، تمیزتر و با قابلیت نگهداری بالاتری است. برای مثال هر جائیکه نام username استفاده شده بود، با خاصیت name جایگزین شده و بجای برچسب از label، بجای مقدار username از متغیر value و بجای رخ‌داد تعریف شده نیز onChange قرار گرفته‌است.

سپس به کامپوننت فرم لاگین بازگشته و ابتدا آن‌را import می‌کنیم:
import Input from "./common/input";
اکنون متد رندر ماژول src\components\loginForm.jsx، به صورت زیر با درج دو Input، خلاصه می‌شود که دیگر در آن خبری از تگ‌ها و کدهای تکراری نیست:
  render() {
    const { account } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <Input
          name="username"
          label="Username"
          value={account.username}
          onChange={this.handleChange}
        />
        <Input
          name="password"
          label="Password"
          value={account.password}
          onChange={this.handleChange}
        />
        <button className="btn btn-primary">Login</button>
      </form>
    );


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:  sample-18.zip
مطالب
نمایش در حال بارگذاری بودن صفحه در برنامه‌های Angular
پیشتر در مورد HTTP Client جدید Angular، مطلب «ارتقاء به HTTP Client در Angular 4.3» را بررسی کردیم. یکی دیگر از قابلیت‌های HttpClient که روش اصلی دسترسی به اطلاعات از راه دور در Angular 5 نیز می‌باشد، امکان تعریف Interceptors سفارشی برای آن است. به کمک Interceptors می‌توان به تمامی درخواست‌های HTTP ارسالی و پاسخ‌های دریافتی از سرور گوش فرا داد و در این بین منطق ویژه‌ای را پیاده سازی کرد؛ مانندcaching ،logging و غیره. در مطلب جاری قصد داریم از این قابلیت برای نمایش یک loading bar مشخص کننده‌ی شروع و پایان تمامی درخواست‌های HTTP استفاده کنیم.


دریافت و نصب پیشنیازها

برای نمایش loading bar در بالای صفحه، از کامپوننت ng2-slim-loading-bar استفاده خواهیم کرد. دمویی از آن‌را در اینجا می‌توانید مشاهده کنید.
برای نصب آن، ابتدا دستور ذیل را در ریشه‌ی پروژه اجرا کنید:
 npm install ng2-slim-loading-bar --save
پس از آن فایل angular-cli.json. را گشوده و فایل css آن‌را تعریف نمائید:
"styles": [
   "../node_modules/ng2-slim-loading-bar/style.css",
   "styles.css"
],
در ادامه به فایل app.module.ts مراجعه کرد و ماژول آن‌را نیز به نحو ذیل به برنامه معرفی کنید:
import {SlimLoadingBarModule} from 'ng2-slim-loading-bar';

@NgModule({
imports: [
     //...
     SlimLoadingBarModule.forRoot()
  ]
})
export class AppModule {
}

پس از این مقدمات، هرجایی که نیاز به استفاده‌ی از آن با کدنویسی باشد، ابتدا سرویس SlimLoadingBarService آن‌را به سازنده‌ی کلاس کامپوننت مدنظر تزریق می‌کنیم و سپس از متدهای start ،stop و complete آن می‌توان استفاده کرد.
 import {SlimLoadingBarService} from 'ng2-slim-loading-bar';
همچنین برای نمایش این کامپوننت باید selector آن را در فایل app.component.html در بالای صفحه اضافه کرد:
  <ng2-slim-loading-bar></ng2-slim-loading-bar>


تدارک یک LoaderInterceptor برای استفاده از ng2-slim-loading-bar

در ادامه می‌خواهیم هر زمانیکه در سراسر برنامه، درخواست HTTP ایی شروع شد، این کامپوننت نمایش داده شود و در پایان درخواست و یا درصورت بروز خطایی، پایان یابد و مخفی شود. برای این منظور یک Interceptor جدید را به صورت ذیل به پوشه‌ی Core برنامه اضافه می‌کنیم:
 ng g s core/interceptors/LoaderInterceptor --spec false
با این کدها:
import { Injectable } from "@angular/core";
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import { SlimLoadingBarService } from "ng2-slim-loading-bar";


@Injectable()
export class LoaderInterceptorService implements HttpInterceptor {

  constructor(private loadingBar: SlimLoadingBarService) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // start our loader here
    this.loadingBar.start();
    return next.handle(req).do(
      (event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          this.loadingBar.complete();
        }
      },
      (err: any) => {
        this.loadingBar.complete();
      });
  }
}
توضیحات:
- برای پیاده سازی یک interceptor جدید، نیاز است کلاس سرویسی را که HttpInterceptor را پیاده سازی می‌کند، ایجاد کنیم. برای تکمیل این پیاده سازی نیاز است متد intercept را با امضایی که مشاهده می‌کنید، تعریف کنیم:
 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
- در اینجا پارامتر req به خود درخواست Http اشاره می‌کند.
- کار پارامتر next تنظیم و بازگشت یک HttpEvent observable توسط متد handle آن است.

برای نمونه در اینجا ابتدا سرویس SlimLoadingBarService به سازنده‌ی کلاس interceptor‌  تزریق شده‌است. سپس توسط آن می‌توان به متدهای start و complete این کامپوننت دسترسی یافت. برای مثال در ابتدای کار گوش فرادادن به درخواست جاری، متد start فراخوانی شده‌است و سپس زمانیکه پاسخی از سرور دریافت شده و یا خطایی رخ‌داده، متد complete آن فراخوانی شده‌است.


ثبت و معرفی LoaderInterceptorService به سیستم

برای معرفی interceptor تهیه شده، به فایل app.module.ts مراجعه کرد و قسمت providers آن‌را به نحو ذیل تکمیل می‌کنیم: 
 @NgModule({
  providers: [
   { provide: HTTP_INTERCEPTORS, useClass: LoaderInterceptorService, multi: true }
  ]
})
export class AppModule {}


آزمایش برنامه

اکنون اگر قسمت‌های مختلف برنامه را که با HttpClient جدید کار می‌کنند بررسی کنید، متوجه خواهید شد که با شروع هر درخواست، loading bar قرمزی در بالای صفحه ظاهر می‌شود و در پایان درخواست، به صورت خودکار مخفی می‌گردد. نکته‌ی مهم این روش عدم نیاز به تغییری در قسمت‌های مختلف برنامه است. این interceptor سراسری است و به صورت یکسانی بر روی کل برنامه تاثیر می‌گذارد.
نظرات مطالب
مدیریت سراسری خطاها در یک برنامه‌ی Angular
- در مورد اینکه چه استثناهایی باید مدیریت شوند یا خیر، مطلب «نکات کار با استثناءها در دات نت» را مطالعه کنید.
- علت عمل نکردن فیلتری که به آن لینک دادید (که من با آن موافق نیستم)، این است که دیگر نباید از میان‌افزار مدیریت استثناهای مخصوص توسعه دهنده‌های ASP.NET Core در این حالت استفاده کنید، چون با آن تداخل می‌کند و پیش از آن وارد عمل می‌شود. علت دریافت صفحه‌ی HTML ایی که مشاهده می‌کنید، همین مورد است. این صفحه برای برنامه‌های ASP.NET Core دارای Viewهای Razor طراحی شده‌است و نه مخصوص حالت کار صرفا Web API آن.
- یکی از مشکلات آن فیلتر هم این است که به هیچ عنوان نباید اصل خطای رخ‌داده‌ی در سمت سرور را به سمت کلاینت ارسال کرد و به کاربر نمایش داد. این مورد امکان دیباگ از راه دور برنامه‌ی شما را توسط یک مهاجم سهولت می‌بخشد و از دیدگاه امنیتی اشتباه است. این موارد را فقط باید توسط امکانات Logging توکار ASP.NET Core ثبت و در سمت سرور با «دسترسی ادمین» بررسی کنید. کاربر هم فقط باید جمله‌ی کلی «خطایی رخ داده‌است» را مشاهده کند و نه جزئیات آن‌را.
مطالب
پلاگین DataTables کتابخانه jQuery - قسمت چهارم
همان طور که قبلا اشاره کردیم، این پلاگین می‌تواند از یک زبان برنامه نویسی سمت سرور داده‌های مورد نیاز خودش را دریافت کند. می‌توانید داده‌ها را با استفاده از AJAX و به صورت JSON از سرور دریافت کرده و با استفاده از DataTables آنها را در جدول تزریق کنید. در این قسمت سعی خواهیم کرد تا با استفاده از jQuery DataTables یک گرید را در MVC ایجاد کنیم.  البته برای حذف جزئیات داده‌ها به جای این که از یک بانک اطلاعاتی دریافت شوند، در حافظه ساخته می‌شوند. در هر صورت اساس کار یکی است.

قصد داریم تا مانند مثال قسمت قبل، مجموعه ای از اطلاعات مربوط به مرورگرهای مختلف را در یک جدول نشان دهیم، اما این بار منبع داده ما فرق می‌کند. منبع داده از طرف سرور فراهم می‌شود. هر مرورگر - همان طور که در قسمت قبل مشاهده نمودید - شامل اطلاعات زیر خواهد بود:
  1. موتور رندرگیری (Engine)
  2. نام مرورگر (Name)
  3. پلتفرم (Platform)
  4. نسخه موتور (Version)
  5. نمره سی اس اس (Grade)

به همین دلیل در سمت سرور، کلاسی خواهیم ساخت که نمایانگر یک مرورگر باشد. بدین صورت:

public class Browser
{
    public int Id { get; set; }
    public string Engine { get; set; }
    public string Name { get; set; }
    public string Platform { get; set; }
    public float Version { get; set; }
    public string Grade { get; set; }
}

استفاده از روش server side processing برای دریافت داده‌ها از سرور

این روش، یکی از امکانات jQuery DataTables است که با استفاده از آن، کلاینت تنها یک مصرف کننده صرف خواهد بود و وظیفه پردازش اطلاعات - یعنی تعداد رکوردهایی که برگشت داده می‌شود، صفحه بندی، مرتب سازی، جستجو، و غیره - به عهده سرور خواهد بود.

برای به کار گیری این روش، اولین کار این است که ویژگی bServerSide را true کنیم، مثلا بدین صورت:
var $table = $('#browsers-grid');
$table.dataTable({
      "bServerSide": true,
      "sAjaxSource": "/Home/GetBrowsers"
 });

همچنین ویژگی sAjaxSource را به Url ی که باید داده‌ها از آن دریافت شوند مقداردهی می‌کنیم.

به صورت پیش فرض مقدار ویژگی bServerSide مقدار false است؛ که یعنی منبع داده این پلاگین از سمت سرور خوانده نشود. اگر true باشد منبع داده و خیلی اطلاعات دیگر مربوط به داده‌های درون جدول باید از سرور به مرورگر کاربر پس فرستاده شوند. با true کردن مقدار bServerSide، آنگاه DataTables اطلاعاتی را راجع به شماره صفحه جاری، اندازه هر صفحه، شروط فیلتر کردن داده ها، مرتب سازی ستون ها، و غیره را به سرور می‌فرستد. همجنین انتظار می‌رود تا سرور در پاسخ به این درخواست، داده‌های مناسبی را به فرمت JSON به مرورگر پس بفرستد. در حالتی که bServerSide مقدار true به خود بگیرد، پلاگین فقط رابطه متقابل بین کاربر و سرور را مدیریت می‌کند و هیچ پردازشی را انجام نمی‌دهد.


در این درخواست XHR یا Ajax ی پارامترهایی که به سرور ارسال می‌شوند این‌ها هستند:

iDisplayStart عدد صحیح
نقظه شروع مجموعه داده جاری

iDisplayLength عدد صحیح
تعداد رکوردهایی که جدول می‌تواند نمایش دهد. تعداد رکوردهایی که از طرف سرور برگشت داده می‌شود باید با این عدد یکسان باشند.

iColumns عدد صحیح
تعداد ستونهایی که باید نمایش داده شوند.

sSearch رشته
فیلد جستجوی عمومی

bRegex بولین
اگر true باشد معنی آن این است که می‌توان از عبارات باقاعده برای جستجوی عبارتی خاص در کل ستون‌های جدول استفاده کرد. مثلا در کادر جستجو نوشت :

^[1-5]$
که یعنی 1 و 5 همه عددهای بین 1و 5.

bSearchable_(int)    بولین
نمایش می‌دهد که یک ستون در طرف کاربر قابلیت searchable آن true هست یا نه.

sSearch_(int)   رشته
فیلتر مخصوص هر ستون. اگر از ویژگی multi column filtering پلاگین استفاده شود به صورت sSearch0 ، sSearch1 ، sSeach2 و ... به طرف سرور ارسال می‌شوند. شماره انتهای هر کدام از پارامترها بیانگر شماره ستون جدول است.

bRegex_(int)  بولین
اگر true باشد، بیان می‌کند که می‌توان از عبارت با قاعده در ستون شماره int جهت جستجو استفاده کرد.

bSortable_(int) بولین
مشخص می‌کند که آیا یک ستون در سمت کلاینت، قابلیت مرتب شدن بر اساس آن وجود دارد یا نه. (در اینجا int اندیس ستون را مشخص می‌کند)

iSortingCols   عدد صحیح
تعداد ستون هایی که باید مرتب سازی بر اساس آنها صورت پذیرد. در صورتی که از امکان multi column sorting استفاده کنید این مقدار می‌تواند بیش از یکی باشد.

iSortCol_(int)   عدد صحیح
شماره ستونی که باید بر اساس آن عملیات مرتب سازی صورت پذیرد.

sSortDir_(int)    رشته
نحوه مرتب سازی ؛ شامل صعودی (asc) یا نزولی (desc)

mDataProp_(int)    رشته
اسم ستون‌های درون جدول را مشخص می‌کند.

sEcho     رشته
اطلاعاتی که datatables از آن برای رندر کردن جدول استفاده می‌کند.

شکل زیر نشان می‌دهد که چه پارامترهایی به سرور ارسال می‌شوند.



شکل ب ) پارامترهای ارسالی به سرور به صورت json

بعضی از این پارامترها بسته به تعداد ستون‌ها قابل تغییر هستند. (آن پارامترهایی که آخرشان یک عدد هست که نشان دهنده شماره ستون مورد نظر می‌باشد)

در پاسخ به هر درخواست XHR که datatables به سرور می‌فرستد، انتظار دارد تا سرور نیز یک شیء json را با فرمت مخصوص که شامل پارامترهای زیر می‌شود به او پس بفرستد:

iTotalRecords    عدد صحیح
تعداد کل رکوردها (قبل از عملیات جستجو) یا به عبارت دیگر تعداد کل رکوردهای درون آن جدول از دیتابیس که داده‌ها باید از آن دریافت شوند. تعداد کل رکوردهایی که در طرف سرور وجود دارند. این مقدار فقط برای نمایش به کاربر برگشت داده می‌شود و نیز از آن برای صفحه بندی هم استفاده می‌شود. 


iTotalDisplayRecords    عدد صحیح
تعداد کل رکوردها (بعد از عملیات جستجو) یا به عبارت دیگر تعداد کل رکوردهایی که بعد از عملیات جستجو پیدا می‌شوند نه فقط آن تعداد رکوردی که به کاربر پس فرستاده می‌شوند. تعداد کل رکوردهایی که با شرط جستجو مطابقت دارند. اگر کاربر چیزی را جستجو نکرده باشد مقدار این پارامتر با پارامتر iTotalRecords یکسان خواهد بود.  

sEcho    عدد صحیح 
یک عدد صحیح است که در قالب رشته در تعامل بین سرور و کلاینت جا به جا می‌شود. این مقدار به ازاء هر درخواست تغییر می‌کند. همان مقداری که مرورگر به سرور می‌دهد را سرور هم باید به مرورگر تحویل بدهد. برای جلوگیری از حملات XSS باید آن را تبدیل به عدد صحیح کرد. پلاگین DataTables مقدار این پارامتر را برای هماهنگ کردن و منطبق کردن درخواست ارسال شده و جواب این درخواست استفاده می‌کند. همان مقداری که مروگر به سرور می‌دهد را باید سرور تحویل به مرورگر بدهد. 

sColumns    رشته
اسم ستون‌ها که با استفاده از کاما از هم جدا شده اند. استفاده از آن اختیاری است و البته منسوخ هم شده است و در نسخه‌های جدید jQuery DataTables از آن پشتیبانی نمی‌شود.

aaData    آرایه
همان طور که قبلا هم گفتیم، مقادیر سلول هایی را که باید در جدول نشان داده شوند را در خود نگهداری می‌کند. یعنی در واقع داده‌های جدول در آن ریخته می‌شوند. هر وقت که DataTables داده‌های مورد نیازش را دریافت می‌کند، سلول‌های جدول html مربوطه اش را از روی آرایه aaData ایجاد می‌کند. تعداد ستون‌ها در این آرایه دو بعدی، باید با تعداد ستون‌های جدول html مربوطه به آن یکسان باشد

شکل زیر پارامترها دریافتی از سرور را نشان می‌دهند:


شکل ب ) پارامترهای دریافتی از سرور به صورت json

استفاده از روش server side processing در mvc
همان طور که گفتیم، کلاینت به سرور یک سری پارامترها را ارسال می‌کند و آن پارامترها را هم شرح دادیم. برای دریافت این پارامتر‌ها طرف سرور، احتیاج به یک مدل هست. این مدل به صورت زیر پیاده سازی خواهد شد:
/// <summary>
/// Class that encapsulates most common parameters sent by DataTables plugin
/// </summary>
public class jQueryDataTableParamModel
{
    /// <summary>
    /// Request sequence number sent by DataTable,
    /// same value must be returned in response
    /// </summary>
    public string sEcho { get; set; }
    /// <summary>
    /// Text used for filtering
    /// </summary>
    public string sSearch { get; set; }
    /// <summary>
    /// Number of records that should be shown in table
    /// </summary>
    public int iDisplayLength { get; set; }
    /// <summary>
    /// First record that should be shown(used for paging)
    /// </summary>
    public int iDisplayStart { get; set; }
    /// <summary>
    /// Number of columns in table
    /// </summary>
    public int iColumns { get; set; }
    /// <summary>
    /// Number of columns that are used in sorting
    /// /// </summary>
    public int iSortingCols { get; set; }
    /// <summary>
    /// Comma separated list of column names
    /// </summary>
    public string sColumns { get; set; }
}

مدل بایندر mvc وظیفه مقداردهی به خصوصیات درون این کلاس را بر عهده دارد، بقیه پارامترهایی که به سرور ارسال می‌شوند و در این کلاس نیامده اند، از طریق شیء Request در دسترس خواهند بود.


اکشن متدی که مدل بالا را دریافت می‌کند، می‌تواند به صورت زیر پیاده سازی شود. این اکشن متد وظیفه پاسخ دادن به درخواست DataTables بر اساس پارامترهای ارسال شده در مدل DataTablesParam را دارد. خروجی این اکشن متد شامل پارارمترهای مورد نیاز پلاگین DataTables برای تشکیل جدول است که آنها را هم شرح دادیم.

public JsonResult GetBrowsers(jQueryDataTableParamModel param)
{
        IQueryable<Browser> allBrowsers = new Browsers().CreateInMemoryDataSource().AsQueryable();

        IEnumerable<Browser> filteredBrowsers;

        // Apply Filtering
        if (!string.IsNullOrEmpty(param.sSearch))
        {
                filteredBrowsers = new Browsers().CreateInMemoryDataSource()
                    .Where(x => x.Engine.Contains(param.sSearch)
                                       || x.Grade.Contains(param.sSearch)
                                       || x.Name.Contains(param.sSearch)
                                       || x.Platform.Contains(param.sSearch)
                    ).ToList();
                float f;
                if (float.TryParse(param.sSearch, out f))
                {
                    filteredBrowsers = filteredBrowsers.Where(x => x.Version.Equals(f));
                }
        }
        else
        {
                filteredBrowsers = allBrowsers;
        }

        // Apply Sorting
        var sortColumnIndex = Convert.ToInt32(Request["iSortCol_0"]);
        Func<Browser, string> orderingFunction = (x => sortColumnIndex == 0 ? x.Engine :
                                                            sortColumnIndex == 1 ? x.Name :
                                                            sortColumnIndex == 2 ? x.Platform :
                                                            sortColumnIndex == 3 ? x.Version.ToString() :
                                                            sortColumnIndex == 4 ? x.Grade :
                                                                x.Name);

        var sortDirection = Request["sSortDir_0"]; // asc or desc
        filteredBrowsers = sortDirection == "asc" ? filteredBrowsers.OrderBy(orderingFunction) : filteredBrowsers.OrderByDescending(orderingFunction);

        // Apply Paging
        var enumerable = filteredBrowsers.ToArray();
        IEnumerable<Browser> displayedBrowsers = enumerable.Skip(param.iDisplayStart).
                Take(param.iDisplayLength).ToList();

        return Json(new
        {
                sEcho = param.sEcho,
                iTotalRecords = allBrowsers.Count(),
                iTotalDisplayRecords = enumerable.Count(),
                aaData = displayedBrowsers
        }, JsonRequestBehavior.AllowGet);
}

تشریح اکشن متد GetBrowsers :

این اکشن متد از مدل jQueryDataTableParamModel به عنوان پارامتر ورودی خود استفاده می‌کند. این مدل همان طور هم که گفتیم، شامل یک سری خصوصیت است که توسط پلاگین jQuery DataTables مقداردهی می‌شوند و همچنین مدل بایندر mvc وظیفه بایند کردن این مقادیر به خصوصیات درون این کلاس را بر عهده خواهد داشت. درون بدنه اکشن متد GetBrowsers داده‌ها بعد از اعمال عملیات فیلترینگ، مرتب سازی، و صفحه بندی به فرمت مناسبی درآمده و به طرف مرورگر فرستاده خواهند شد.

برای پیاده سازی کدهای طرف کلاینت نیز، درون یک View کدهای زیر قرار خواهند گرفت:
$(function () {
        var $table = $('#browsers-grid');
        $table.dataTable({
                "bProcessing": true,
                "bStateSave": true,
                "bServerSide": true,
                "bFilter": true,
                "sDom": 'T<"clear">lftipr',
                "aLengthMenu": [[5, 10, 25, 50, -1], [5, 10, 25, 50, "All"]],
                "bAutoWidth": false,
                "sAjaxSource": "/Home/GetBrowsers",
                "fnServerData": function (sSource, aoData, fnCallback) {
                    $.ajax({
                        "dataType": 'json',
                        "type": "POST",
                        "url": sSource,
                        "data": aoData,
                        "success": fnCallback
                    });
                },
                "aoColumns": [
                    { "mDataProp": "Engine" },
                    { "mDataProp": "Name" },
                    { "mDataProp": "Platform" },
                    { "mDataProp": "Version" },
                    { "mDataProp": "Grade" }
                ],
                "oLanguage": {
                        "sUrl": "/Content/dataTables.persian.txt"
                }
        });
});

تشریح کدها:

fnServerData :

این متد، در واقع نحوه تعامل سرور و کلاینت را با استفاده از درخواستهای XHR مشخص خواهد کرد.

oLanguage :

برای فعال سازی زبان فارسی، فیلدهای مورد نیاز ترجمه شده و در یک فایل متنی قرار داده شده اند. کافی است آدرس این فایل متنی به ویژگی oLanguage اختصاص داده شوند.

مثال این قسمت را از لینک زیر دریافت کنید:
DataTablesTutorial04.zip

لازم به ذکر است پوشه bin، obj، و packages جهت کاهش حجم این مثال از solution حذف شده اند. برای اجرای این مثال از اینجا کمک بگیرید.


مطالعه بیشتر

برای مطالعه بیشتر در مورد این پلاگین و نیز پیاده سازی آن در MVC می‌توانید به لینک زیر نیز مراجعه بفرمائید که بعضی از قسمتهای این مطلب هم از مقاله زیر استفاده کرده است:
jQuery DataTables and ASP.NET MVC Integration - Part I



مطالب
پروکسی‌های اشیاء در ES 6
پروکسی‌ها، پایه‌ی مباحث AOP هستند. این اشیاء ویژه‌ی ES 6، امکان ردیابی تغییرات را بر روی اشیاء جاوا اسکریپتی فراهم می‌کنند. ابتدایی‌ترین مثالی را که در این زمینه می‌توان ارائه داد، بررسی تغییرات خواص Get و Set اشیاء هستند. فرض کنید شیء unicorn به صورت زیر تعریف شده‌است:
var unicorn = {
   legs: 4,
   color: 'brown',
   horn: true
};
اکنون می‌خواهیم اگر کسی درخواست مقدار خاصیت color این شیء را ارائه داد، بجای رنگ قهوه‌ای، یک مقدار سفارشی سازی شده را دریافت کند. برای تداخل در این بین و کنترل مقدار بازگشت داده شده‌ی توسط یک شیء مفروض، می‌توان از شیء جدیدی به نام Proxy استفاده کرد:
var proxyUnicorn = new Proxy(unicorn, {
      get: function(target, property) {
            if(property === 'color') {
                  return 'awesome ' + target[property];
              } else {
                  return target[property];
           }
      }
});
کار شیء پروکسی، ایجاد یک شیء جدید از unicorn نیست. بلکه به صورت غشایی نامرئی ظاهر شده و محصور کننده‌ی این شیء می‌شود. بنابراین کلیه‌ی درخواست‌های رسیده‌ی به unicorn، ابتدا باید از این غشاء رد شود و سپس به unicorn برسد. اینجا است که امکان ردیابی و همچنین سفارشی سازی دسترسی به خواص را می‌توان پیاده سازی کرد.
شیء Proxy در ES 6 دو پارامتر را دریافت می‌کند. پارامتر اول آن، شیء اصلی است که باید محصور شود و پارامتر دوم آن مشخص می‌کند که چه عملیاتی باید تحت کنترل و سفارشی سازی قرار گیرد. در این مثال عملیات get تحت نظر قرار گرفته‌است و برای اینکار متدی که تعریف شده (به آن handler نیز می‌گویند)، پارامتر اول آن target یا همان unicorn در این مثال است و property نام خاصیتی است که هم اکنون قرار است مقدار آن بازگشت داده شود. در مثال فوق دو حالت دسترسی به خاصیتی به نام color و همچنین سایر خواصی که این نام را ندارند، پیاده سازی شده‌است.
پس از این عملیات، اگر به خواص ارائه شده‌ی توسط شیء پروکسی دسترسی پیدا کنیم، یک چنین خروجی را دریافت خواهیم کرد:
 console.log(proxyUnicorn.legs); //4
console.log(proxyUnicorn.color); //'awesome brown'

مثالی دیگر در این زمینه می‌تواند کنترل عملیات دسترسی به حالت set باشد (هر دوی این حالت‌ها را با یک شیء پروکسی نیز می‌توان مدیریت کرد):
var proxyUnicorn = new Proxy(unicorn, {
           set: function(target, property, value) {
                    if(property === 'horn' && value === false) {
                         console.log('unicorn cannot ever lose its horn!');
                      } else {
                         target[property] = value;
                      }
           }
});
در حالت set، متد handler تعریف شده، پارامتر سومی را به نام value دارد و این مقدار، مساوی مقداری است که هم اکنون توسط کاربر تنظیم شده‌است. بنابراین در اینجا می‌توان پیاده سازی منطق خاصی را پیگیری کرد. برای مثال در اینجا اگر خاصیت مدنظر horn باشد و کاربر سعی کند مقدار false را به آن نسبت دهد، توسط این پروکسی از انجام عملیات منع خواهد شد.


ردگیری فراخوانی‌های توابع توسط پروکسی‌های ES 6

در ادامه همان شیء unicorn را مشاهده می‌کنید که متد hornAttack نیز به آن اضافه شده‌است.
var unicorn = {
   legs: 4,
   color: 'brown',
   horn: true,
   hornAttack: function(target) {
        return target.name + ' was obliterated!';
   }
};
اکنون می‌خواهیم دسترسی به متد hornAttack را تحت نظر قرار داده و اجازه‌ی استفاده‌ی از آن‌را به سایر اشیاء ندهیم.
 var thief = { name: 'Rupert'}
thief.attack = unicorn.hornAttack;
thief.attack();
برای نمونه در این حالت نمی‌خواهیم که شیء thief بتواند از hornAttack یک unicorn استفاده کند. به همین جهت برای unicorn.hornAttack یک پروکسی جدید را تعریف می‌کنیم که دسترسی به آن‌را تحت نظر قرار دهد:
unicorn.hornAttack = new Proxy(unicorn.hornAttack, {
        apply: function(target, context, args) {
                  if(context !== unicorn) {
                     return 'nobody can use unicorn horn but unicorn!';
                   } else {
                     return target.apply(context, args);
                 }
        }
});
پس از این تعریف و انتساب، unicorn.hornAttack دیگر همان unicorn.hornAttack اصلی نخواهد بود و اکنون یک proxy object است. در این پروکسی برای کنترل متدها از کلید apply استفاده می‌شود. متد handler آن دارای سه پارامتر است. پارامتر target همان متد مدنظر است. پارامتر context شیءایی است که قرار است این متد را فراخوانی کند. پارامتر سوم نیز لیست پارامترهای ارسالی به متد است.
در ادامه اگر متد thief.attack فراخوانی شود، این فراخوانی عمل نکرده (چون حالت context !== unicorn برقرار است) و پیام «nobody can use unicorn horn but unicorn» نمایش داده می‌شود.

در این متد handler (پارامتر دوم شیء پروکسی) مواردی مانند افزودن یا حذف خواص را نیز می‌توان تحت کنترل قرار داد:
function NOPE() {
  throw new Error("can't modify read-only view");
}

var handler = {
  // Override all five mutating methods.
  set: NOPE,
  defineProperty: NOPE,
  deleteProperty: NOPE,
  preventExtensions: NOPE,
  setPrototypeOf: NOPE
};
مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت دوم - الگوی Service Locator
در قسمت قبل برای دریافت وهله‌ای از سرویس TestService، به صورت ()<serviceProvider.GetService<ITestService عمل کردیم. این روش در اصل الگوی Service Locator نام دارد که جزئیات بیشتری از آن‌را در این قسمت بررسی خواهیم کرد.


قلب سیستم تزریق وابستگی‌های NET Core. اینترفیس IServiceProvider است

IServiceProvider که اساس IoC Container برنامه‌های مبتنی بر NET Core. را تشکیل می‌دهد، در اسمبلی System.ComponentModel و در فضای نام System تعریف شده‌است:
namespace System
{
    public interface IServiceProvider
    {
        object GetService(Type serviceType);
    }
}
زمانیکه به کمک IServiceCollection، تمام اینترفیس‌ها و کلاس‌های خود را به IoC Container معرفی کردیم، مرحله‌ی بعدی، فراهم آوردن روشی برای دریافت وهله‌ای از این سرویس‌ها توسط متد GetService است.
استفاده‌ی مستقیم از اینترفیس IServiceProvider برای دسترسی به وهله‌های سرویس‌ها، اصطلاحا الگوی Service Locator نامیده می‌شود و باید تا حد ممکن از آن پرهیز کرد؛ چون وابستگی مستقیمی از IoC Container را درون کدهای ما قرار می‌دهد و به این ترتیب یک مرحله، نوشتن آزمون‌های واحد برای آن‌را مشکل‌تر می‌کند؛ چون زمان وهله سازی از یک سرویس، دقیقا مشخص نیست به چه وابستگی‌هایی نیاز دارد. به همین جهت همیشه باید با روش تزریق وابستگی‌ها در سازنده‌ی کلاس شروع کرد و اگر به هر دلیلی این روش مهیا نبود و توسط سیستم تزریق وابستگی‌های جاری شناسایی و یا پشتیبانی نمی‌شد (مانند تزریق وابستگی در سازنده‌های Attributes)، آنگاه می‌توان به الگوی Service Locator مراجعه کرد.
برای مثال در اکثر قسمت‌های برنامه‌های ASP.NET Core امکان تزریق وابستگی‌ها در سازنده‌ی کنترلرها، میان افزارها و سایر اجزای آن وجود دارد و در این حالات نیازی به مراجعه‌ی مستقیم به IServiceProvider برای دریافت وهله‌های سرویس‌های مورد نیاز نیست. به عبارتی نگرانی در مورد IServiceProvider بهتر است مشکل IoC Container باشد و نه ما.

در مثال زیر، روش استفاده‌ی از IServiceProvider را جهت انجام تزریق وابستگی‌ها (یا به عبارتی بهتر، روش دسترسی به وهله‌های وابستگی‌ها) را مشاهده می‌کنید:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace CoreIocServices
{
    public interface IProductService
    {
        void Delete(int id);
    }

    public class ProductService : IProductService
    {
        private readonly ITestService _testService;
        private readonly ILogger<ProductService> _logger;

        public ProductService(IServiceProvider serviceProvider)
        {
            _testService = serviceProvider.GetRequiredService<ITestService>();
            _logger = serviceProvider.GetService<ILogger<ProductService>>() ?? NullLogger<ProductService>.Instance;
        }

        public void Delete(int id)
        {
            _testService.Run();
            _logger.LogInformation($"Deleted a product with id = {id}");
        }
    }
}
این روش یا کار مستقیم با Service locator، هر چند کار می‌کند، اما روشی است که باید تا حد ممکن از آن پرهیز کنید؛ زیرا:
- با نگاه کردن به امضای سازنده‌ی این سرویس مشخص نیست که دقیقا از چه وابستگی‌هایی استفاده می‌کند. اینکار نوشتن آزمون‌های واحد آن‌را مشکل می‌کند.
- این سرویس یک وابستگی اضافه‌تر را به نام IServiceProvider، نیز پیدا کرده‌است که اگر از روش متداول تزریق وابستگی‌ها در سازنده‌ی کلاس استفاده می‌شد، نیازی به ذکر آن نبود.
- پیاده سازی Dispose Pattern در این حالت مشکل‌تر است و در قسمتی دیگر بررسی خواهد شد.



تفاوت‌های بین متدهای ()<GetService<T  و  ()<GetRequiredService<T

از آنجائیکه دیگر از NET 1.0. استفاده نمی‌کنیم، استفاده‌ی از متد GetService با امضایی که در اینترفیس IServiceProvider تعریف شده و strongly typed نیست، بیشتر برای کارهای پویا مناسب است. به همین جهت دو نگارش جنریک از آن در اسمبلی Microsoft.Extensions.DependencyInjection.Abstractions با امضای زیر تعریف شده‌اند که نمونه‌ای از آن‌را در قسمت قبل نیز استفاده کردیم و برای استفاده‌ی از آن‌ها ذکر فضای نام Microsoft.Extensions.DependencyInjection ضروری است:
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceProviderServiceExtensions
    {
        public static T GetRequiredService<T>(this IServiceProvider provider);
        public static T GetService<T>(this IServiceProvider provider);
    }
}
اکنون این سؤال مطرح می‌شود که تفاوت‌های بین این دو متد چیست؟
- متد GetService یک شیء سرویس از نوع T را بازگشت می‌دهد و یا نال؛ اگر سرویسی از نوع T، پیشتر به سیستم معرفی نشده باشد.
- متد GetRequiredService یک شیء سرویس از نوع T را بازگشت می‌دهد و یا اگر سرویسی از نوع T پیشتر به سیستم معرفی نشده باشد، استثنای InvalidOperationException را صادر می‌کند.

بنابراین تنها تفاوت این دو متد، در نحوه‌ی رفتار آن‌ها با درخواست وهله‌ای از یک سرویس پیشتر ثبت نشده‌است؛ یکی نال را باز می‌گرداند و دیگری یک استثناء را صادر می‌کند.


با توجه به این تفاوت‌ها کدامیک از متدهای GetService و یا GetRequiredService را باید استفاده کرد؟

همانطور که پیشتر نیز در توضیحات الگوی Service locator عنوان شد، هیچکدام! ابتدا با تزریق وابستگی‌های در سازنده‌ی کلاس شروع کنید و اگر تامین این وابستگی، توسط IoC Container جاری پشتیبانی نمی‌شد، آنگاه نیاز به استفاده‌ی از یکی از نگارش‌های متد GetService خواهد بود و متد توصیه شده نیز GetRequiredService است و نه GetService؛ به این دلایل:
- حذف کدهای تکراری: اگر از GetService استفاده کنید، نیاز خواهید داشت پس از تمام فراخوانی‌های آن، بررسی نال بودن آن‌را نیز انجام دهید. برای حذف این نوع کدهای تکراری، بهتر است از همان متد GetRequiredService استفاده کنید که به صورت توکار این بررسی را نیز انجام می‌دهد.
- پشتیبانی از روش Fail Fast و یا همان Defensive programming: اگر بررسی نال بودن GetService را فراموش کنید، در سطرهای بعدی، یافتن علت NullReferenceException صادر شده مشکل‌تر از رسیدگی به InvalidOperationException صادر شده‌ی توسط GetRequiredService خواهد بود که توضیحات دقیقی را در مورد سرویس ثبت نشده ارائه می‌دهد.
- اگر بر روی IoC Container پیش‌فرض NET Core. یک IoC Container دیگر را مانند AutoFac قرار داده‌اید، استفاده‌ی از GetRequiredService، سبب می‌شود تا اینگونه IoC Containerهای ثالث بتوانند اطلاعات مفیدتری را از سرویس‌های ثبت نشده ارائه دهند.

تنها حالتی که استفاده‌ی از روش GetService را نیاز دارد، شرطی کردن ثبت و معرفی کردن سرویس‌ها به IoC Container است؛ اگر سرویسی ثبت شده بود، آنگاه قطعه کدی اجرا شود.
نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا
- لیست صفحاتی را که کاربر به آن دسترسی دارد، پس از لاگین برای او ارسال کنید (یا به صورت یک آرایه که در سمت Angular کش شود یا به صورت Claims سفارشی داخل توکن JWT او؛ مانند Claims سفارشی نقش‌ها که هم اکنون موجود است).
- سپس توسط یک محافظ مسیر سفارشی، دسترسی‌ها را از این آرایه خوانده و اعمال کنید. برای نمونه محافظ مسیر سفارشی یاد شده، بر اساس Claims مربوط به نقش‌های کاربر که پس از لاگین به سمت کلاینت ارسال می‌شوند، دسترسی به مسیرها را کنترل می‌کند.
نظرات مطالب
مدیریت سفارشی سطوح دسترسی کاربران در MVC
بنده در حال توسعه‌ی یه سیستم ساده Membership متن باز هستم که توانایی تعریف گروه و تعیین سطوح دسترسی به صورت پویا را دارد. کار خیلی زیادی ازش باقی نمانده و در صورت اتمام حتما به اشتراک می‌گذارم.
مطالب
MTOM در WCF
در  WCF سه نوع Message Encoder وجود دارد:
  • Text(Xml) Message Encoder(به صورت پیش فرض در تمام Http-Base Binding‌ها از این Encoder استفاده می‌شود)
  • Binary Message Encoder(به صورت پیش فرض در تمام Net* Binding‌ها از این encoder استفاده می‌شود که برای سرویس‌های وب مناسب نیست)
  • MTOM Message Encoder (در حالت استفاده از Http-Base Binding‌ها و انتقال اطلاعات به صورت باینری از این گزینه استفاده می‌شود که به صورت پیش فرض غیر فعال است)
Encoding یا رمزگذاری در WCF به این معنی است که داده‌های مورد نظر برای انتقال، به یکی از فرمت‌های MTOM ، Text-Xml یا  Binary سریالایز شوند.
 وضعیتی را در نظر بگیرید که در یک پروژه مبتنی بر WCF قصد دارید حجمی زیاد از داده به فرمت باینری  (نظیر فایل ها) را بین سرور و کلاینت رد و بدل کنید. به صورت معمول بسیاری از برنامه نویسان، یک کلاس به همراه DataContractAttribute ایجاد می‌کنند که در آن خاصیتی به صورت آرایه ای از نوع بایت که DataMemberAttribute را نیز دارد برای انتقال محتویات فایل استفاده می‌شود. اما باید یک نکته را مد نظر داشت و آن این است که به صورت پیش فرض فرمت انتقال داده‌ها در WCF به صورت Text/Xml است  و برای انتقال داده‌ها نیز از فرمت Base 64 استفاده خواهد شد. مشکل اصلی این جاست که در حالت Text/Xml Encoding برای انتقال داده‌های باینری، برای هر سه بایت، چهار کاراکتر استفاده می‌شود در نتیجه، این باعث افزایش حجم داده تا 33 درصد خواهد شد که کارایی سیستم را تحت تاثیر قرار می‌دهد.
اما خبر خوب این است که استانداردی وجود دارد به نام MTOM یا همان Message Transmission Optimization Mechanism، برای این که بتوان محتوای باینری را بدون افزایش حجم داده انتقال داد. برای پیاده سازی این روش باید موارد زیر را در نظر داشته باشید:

»متد یا همان OperationContract که وظیفه آن ارسال یا دریافت داده‌ها با فرمت MTOM است فقط کلاس هایی را انتقال دهد که دارای MessageContractAttribute هستند. نباید از DataContractAttribute استفاده نمایید.
»خاصیتی که نوع آن آرایه ای از بایت‌ها است نباید دارای DataMemberAttribute باشد؛ بلکه به جای آن باید از MessageBodyMember استفاده نمایید.
»به جای []Byte می‌توان از نوع Stream نیز استفاده کرد(الزامی نیست).
»مقدار خاصیت MessageEncoding در Binding استفاده شده باید MTOM تعیین شود.
پیاده سازی یک مثال
ابتدا کلاس مورد نظر را به صورت زیر تهیه می‌کنیم:
[MessageContract]
public class MyFile
{
    [MessageHeader] 
    public String Filename { get; set; }

    [MessageBodyMember]    
    public Byte[] Contents { get; set; } 
}
چند تذکر
  • به جای DataContract از MessageContract استفاده می‌شود؛
  • تمام خاصیت هایی که نوع آن‌ها غیر از []Byte است باید دارای MessageHeader باشند؛
  • خاصیتی که برای انتقال محتوای باینری تهیه شده است، باید از MessageBodyMember استفاده نماید؛
  • مجاز به تعریف فیلد یا فیلد هایی که نوع آن‌ها Primitive Type است نمی‌باشید.

تنظیمات مربوط به Binding نیز به صورت خواهد بود:

<bindings> 
    <wsHttpBinding>
        <binding name="WsHttpMtomBinding" messageEncoding="Mtom" /> 
    </wsHttpBinding>
 </bindings>
اما یک نکته...
هدف از استفاده از MTOM برای افزایش کارایی انتقال داده‌های باینری در حجم زیاد است. در زیر نتایج مقایسه بررسی انتقال اطلاعات به دو صورت MTOM و Text برای حجم داده‌های متفاوت را مشاهده می‌کنید:


با دقت در نتایج بالا مشخص می‌شود که این روش در حجم داده‌های پایین (مثل 100 بایت یا 1000 بایت) عملکرد مورد انتظار را نخواهد داشت. پس این نکته را نیز در هنگام پیاده سازی به این روش مد نظر داشته باشید.

 
مطالب
امکان تغییر رشته‌ی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه
تغییر پویای رشته‌ی اتصالی به بانک اطلاعاتی در نگارش‌های پیشین EF، مشکل بودند که نمونه‌هایی از آن را پیشتر در مطالب زیر مشاهده کرده‌اید:
- «تنظیم رشته اتصالی Entity Framework به بانک اطلاعاتی به وسیله کد»
- «استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First»

اما EF Core نه تنها این مشکل را پوشش را داده‌است، بلکه امکان تزریق وابستگی‌ها و استفاده‌ی از سرویس‌های مختلف را نیز در این حین، پیش بینی کرده‌است که در ادامه جزئیات آن‌را مرور می‌کنیم.


نیاز به تغییر رشته‌ی اتصالی به بانک اطلاعاتی در زمان اجرا

دلایل نیاز به امکان تغییر رشته‌ی اتصالی در زمان اجرا شامل موارد زیر هستند:
- در برنامه‌هایی کمی پیچیده‌تر و سابقه دار، ممکن است عملیات تجاری یکسال را در بانک اطلاعاتی سال 98 و دیگری را در بانک اطلاعاتی سال 99 ثبت کنید. در این حالت کاربران باید بتوانند در زمان اجرا به هر بانک اطلاعاتی که پیشتر با آن کار کرده‌اند، متصل شده و از آن استفاده کنند.
- یکی از روش‌های پیاده سازی برنامه‌های چند مستاجری، داشتن یک بانک اطلاعاتی مجزا، به ازای هر مستاجر است. در این حالت نیز تک برنامه‌ی ما باید بتواند بر اساس Id مشتری، بانک اطلاعاتی متناظری را در زمان اجرا انتخاب کند.
- نیاز به داشتن چندین context در برنامه و کار با بانک‌های اطلاعاتی متفاوت در زمان اجرا؛ مانند کار با SQL Server، اوراکل و یا SQLite


روش تغییر رشته‌ی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه

اگر به روش ثبت متداول سرویس DbContext برنامه و پروایدر آن دقت کنیم:
services.AddDbContext<ApplicationDbContext>(options =>
      options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")
));
یک action delegate قابل مشاهده‌است. کار این اکشن، تنظیم پروایدر و تمام نیازهای یک رشته‌ی اتصالی به بانک اطلاعاتی، جهت شروع به کار با Context برنامه است. نکته‌ی مهمی که در اینجا وجود دارد، فراخوانی هرباره‌ی این action، به ازای هر اتصال تشکیل شده‌است. یعنی کدهای داخل این action delegate کش نمی‌شوند و همین مساله امکان تغییر پویای آن‌ها را میسر می‌کند.

یک نکته: چون این اطلاعات کش نمی‌شوند، اگر رشته‌ی اتصالی شما ثابت است (و نیازی به تغییر آن در زمان اجرای برنامه نیست)، محل تامین آن‌را به پیش از سطر services.AddDbContext انتقال دهید و فقط نتیجه‌ی محاسبه شده‌ی نهایی را استفاده کنید تا کارآیی برنامه افزایش یابد؛ در غیراینصورت فراخوانی Configuration.GetConnectionString مدام تکرار خواهد شد.


دریافت یک قالب قابل تغییر از تنظیمات برنامه و تغییر آن با هدرهای درخواست رسیده‌ی به آن

فرض کنید قالب رشته‌ی اتصالی برنامه در فایل appsettings.json به صورت زیر است:
"ConnectionStrings": {
    "ConnectionTemplate": "Data Source=.;Initial Catalog={db_Name};Integrated Security=True",
}
و db_Name آن قرار است برای مثال از یک query string، سشن، کوکی و یا فیلد خاصی در هدر HTTP رسیده تامین شود. برای مثال سال مالی انتخابی و یا شماره مستاجر انتخابی به صورت یک فیلد خاص HTTP به سمت برنامه ارسال می‌شوند.
بنابراین اکنون نیاز است به ازای هر درخواست رسیده بتوان به سرویس IHttpContextAccessor و شیء HttpContext.Request جاری دسترسی یافت و سپس از هدرهای رسیده، برای مثال هدر ویژه‌ی tenantId و یا year را پردازش کرد؛ اما در تعریف services.AddDbContext فوق چگونه می‌توان اینکار را انجام داد؟
خوشبختانه متد services.AddDbContext، دارای یک overload دیگر نیز هست که امکان دسترسی به تمام سرویس‌های جاری سیستم را میسر می‌کند:
services.AddDbContext<ApplicationDbContext>((serviceProvider, dbContextBuilder) =>
{
   var connectionStringTemplate = Configuration.GetConnectionString("ConnectionTemplate");
   var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
   var dbName = httpContextAccessor.HttpContext.Request.Headers["tenantId"].First();
   var connectionString = connectionStringTemplate.Replace("{db_Name}", dbName);
   dbContextBuilder.UseSqlServer(connectionString);
});
همانطور که مشاهده می‌کنید، overload دوم متد services.AddDbContext، امکان ارسال serviceProvider را نیز به این action delegate دارد. پس از آن می‌توان توسط متد GetRequiredService آن به هر سرویس مدنظری که در سیستم ثبت شده‌است، دسترسی یافت و برای مثال در اینجا فیلد هدر سفارشی tenantId را از آن استخراج نمود و در قالب رشته‌ی اتصالی به بانک اطلاعاتی، در زمان اجرا به صورت پویایی جایگزین کرد.
همچنین در صورت نیاز می‌توان UseSqlServer آن‌را نیز در این action delegate به هر پروایدر دیگری در زمان اجرا تغییر داد و از این لحاظ محدودیتی وجود ندارد.

یک نکته: البته برنامه نباید هر tenantId ای را پردازش کند و این خودش می‌تواند تبدیل به یک نقیصه‌ی امنیتی شود. به همین جهت برای مثال می‌توان tenantId را در یک JWT قرار داد و در حین تعیین اعتبار آن و کاربر جاری، این مقدار را نیز بررسی کرد.