مطالب
React 16x - قسمت 5 - کامپوننت‌ها - بخش 2 - نمایش لیست‌ها و مدیریت رویدادها و حالات
در قسمت قبل، اولین کامپوننت React خود را ایجاد کردیم و سپس جزئیات بیشتری از عبارات JSX را مانند نحوه‌ی تعریف المان‌های مختلف و تنظیم مقادیر ویژگی‌های آن‌را بررسی کردیم. در ادامه‌ی همان مثال، در این قسمت، نحوه‌ی نمایش لیست‌ها و تعریف و مدیریت رویدادها را در کامپوننت‌های React، بررسی می‌کنیم.


نحوه‌ی رندر لیستی از اشیاء در کامپوننت‌های React

فرض کنید می‌خواهیم لیستی از تگ‌ها را رندر کنیم. برای این منظور ابتدا داده‌های مرتبط را به خاصیت state کامپوننت، اضافه می‌کنیم:
class Counter extends Component {
  state = {
    count: 0,
    tags: ["tag 1", "tag 2", "tag 3"]
  };
اکنون می‌خواهیم tags را توسط المان‌های ul و ui رندر کنیم. اگر با Angular کار کرده باشید، به همراه یک دایرکتیو ngFor است که توسط آن می‌توان یک حلقه را در قالب جاری، پیاده سازی و رندر کرد. اما در React و عبارات JSX، چیزی به نام مفهوم حلقه‌ها وجود خارجی ندارد؛ چون JSX یک templating engine نیست. فقط بیان ساده‌ی المان‌هایی است که قرار است توسط کامپایلر Babel به کدهای جاوا اسکریپتی ترجمه شوند. بنابراین اکنون این سؤال وجود دارد که چگونه می‌توان لیستی از عناصر را در اینجا رندر کرد؟
در مطلب «React 16x - قسمت 3 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 2» در مورد متد Array.map بحث شد. در اینجا می‌توان توسط متد map، هر المان آرایه‌ی تگ‌ها را به یک المان React تبدیل و سپس رندر کرد:
class Counter extends Component {
  state = {
    count: 0,
    tags: ["tag 1", "tag 2", "tag 3"]
  };

  render() {
    return (
      <div>
        <span className={this.getBadgeClasses()}>{this.formatCount()}</span>
        <button className="btn btn-secondary btn-sm">Increment</button>
        <ul>
          {this.state.tags.map(tag => (
            <li>{tag}</li>
          ))}
        </ul>
      </div>
    );
  }
در این مثال، داخل المان ul، با یک {} شروع می‌کنیم تا بتوان به صورت پویا به مقدار آرایه‌ی this.state.tags دسترسی پیدا کرد. سپس متد map را بر روی این آرایه فراخوانی می‌کنیم. متد map، هر عضو آرایه‌ی tags را به callback function آن ارسال کرده و خروجی آن‌را به صورت یک عبارت JSX که در نهایت به یک المان جاوا اسکریپتی خالص ترجمه خواهد شد، تبدیل می‌کند. این فرآیند سبب رندر لیست tags می‌شود:


هرچند اکنون لیستی از تگ‌ها در مرورگر رندر شده‌اند، اما در کنسول توسعه دهندگان مرورگر، یک اخطار نیز درج شده‌است. علت اینجا است که React نیاز دارد تا بتواند هر آیتم رندر شده را به صورت منحصربفردی شناسایی کند. هدف این است که بتواند در صورت تغییر state هر المان در DOM مجازی خودش، خیلی سریع تشخیص دهد که چه چیزی تغییر کرده و فقط کدام قسمت خاص را باید در DOM اصلی، درج و به روز رسانی کند. برای رفع این مشکل، ویژگی key را به هر المان li در کدهای فوق اضافه می‌کنیم:
<li key={tag}>{tag}</li>
البته در مثال ما تگ‌ها منحصربفرد هستند؛ بنابراین استفاده‌ی از آن‌ها به عنوان key، مشکلی را ایجاد نمی‌کند. در یک برنامه‌ی مفصل‌تر، تگ‌ها می‌توانند شیء بوده و هر شیء دارای خاصیت id باشد که در این حالت فرضی می‌توان از tag.id به عنوان key استفاده کرد. همچنین باید دانست که این key فقط نیاز است در لیست ul، منحصربفرد باشد و نیازی نیست تا در کل DOM منحصربفرد باشد.


رندر شرطی عناصر در کامپوننت‌های React

در اینجا می‌خواهیم اگر تگی وجود نداشت، پیام متناسبی ارائه شود؛ در غیراینصورت لیست تگ‌ها همانند قبل نمایش داده شود (رندر شرطی یا conditional rendering). برای انجام اینکار در React، برخلاف Angular، دارای دایرکتیوهای ساختاری if/else نیستیم؛ چون همانطور که عنوان شد، JSX یک templating engine نیست. به همین جهت برای رندر شرطی المان‌ها در React، باید از همان جاوا اسکریپت خالص کمک بگیریم:
  renderTags() {
    if (this.state.tags.length === 0) {
      return <p>There are no tags!</p>;
    }

    return (
      <ul>
        {this.state.tags.map(tag => (
          <li key={tag}>{tag}</li>
        ))}
      </ul>
    );
  }
یک روش حل این مساله، نوشتن متدی است که به همراه یک if/else است. در اینجا اگر آرایه‌ی تگ‌ها، دارای عنصری نبود، یک پاراگراف متناظر نمایش داده می‌شود، در غیراینصورت همان قسمت رندر لیست تگ‌ها را که توسعه دادیم، بازگشت می‌دهیم. بنابراین این متد، دو خروجی JSX را بسته به شرایط مختلف می‌تواند داشته باشد. سپس از این متد به صورت {()this.renderTags} در متد render اصلی استفاده می‌کنیم:
  render() {
    return (
      <div>
        <span className={this.getBadgeClasses()}>{this.formatCount()}</span>
        <button className="btn btn-secondary btn-sm">Increment</button>
        {this.renderTags()}
      </div>
    );
  }
برای آزمایش آن هم یکبار آرایه‌ی tags را به نحو زیر خالی کنید:
  state = {
    count: 0,
    tags: []
  };

روش دوم حل این نوع مساله‌ها، استفاده از روش زیر است؛ در این حالت خاص، فقط یک if را داریم، بدون وجود قسمت else:
{this.state.tags.length === 0 && "Please create a new tag!"}
ابتدا شرط مدنظر نوشته می‌شود، سپس پیامی را که باید در این حالت ارائه شود، پس از && می‌نویسیم. در مثال فوق اگر آرایه‌ی tags خالی باشد، پیامی نمایش داده می‌شود.
اما این روش چگونه کار می‌کند؟! در اینجا && را به دو مقدار مشخص اعمال کرده‌ایم. یکی حاصل یک مقایسه است و دیگری یک مقدار رشته‌ای مشخص. در جاوا اسکریپت برخلاف سایر زبان‌های برنامه نویسی، می‌توان && را بین دو مقدار غیر Boolean نیز اعمال کرد. در جاوا اسکریپت، یک رشته‌ی خالی به false تعبیر می‌شود و اگر تنها دارای یک حرف باشد، true درنظر گرفته می‌شود. برای نمونه در ترکیب 'true && 'Hi، هر دو قسمت به true تفسیر می‌شوند. در این حالت موتور جاوا اسکریپت، دومین عبارت (آخرین عبارت && شده) را بازگشت می‌دهد. همچنین در جاوا اسکریپت عدد صفر به false تفسیر می‌شود. بنابراین ترکیب true && 'Hi' && 1 مقدار 1 را بازگشت می‌دهد؛ چون عدد 1 هم از دیدگاه جاوا اسکریپت به true تفسیر خواهد شد.


مدیریت رخ‌دادها در React


همانطور که در تصویر فوق نیز مشاهده می‌کنید، رخ‌دادهای استاندارد DOM، دارای خواص معادل React ای نیز هستند. برای مثال زمانیکه می‌نویسیم onClick، دقیقا متناظر است با یک خاصیت المان React در عبارات JSX. بنابراین این نام‌ها حساس به کوچکی و بزرگی حروف نیز هستند.
روش تعریف متدهای رخ‌دادگردان در اینجا، با ذکر فعل handle شروع می‌شود:
  handleIncrement() {
    console.log("Increment clicked!");
  }
سپس ارجاعی از این متد را (نه فراخوانی آن‌را)، به خاصیت برای مثال onClick ارسال می‌کنیم:
<button
    onClick={this.handleIncrement}
    className="btn btn-secondary btn-sm"
>
    Increment
</button>
اگر دقت کنید، onClick، ارجاع this.handleIncrement را دریافت کرده‌است (یعنی بدون () ذکر شده‌است) و نه فراخوانی این متد را (با ذکر ()).
اکنون اگر این فایل را ذخیره کرده و خروجی را در مرورگر بررسی کنیم، با هربار کلیک بر روی دکمه‌ی Increment، یک console.log صورت می‌گیرد.

در ادامه می‌خواهیم در این رخ‌دادگردان، مقدار this.state.count را افزایش دهیم. برای این منظور ابتدا مقدار this.state.count را به نحو زیر لاگ می‌کنیم:
  handleIncrement() {
    console.log("Increment clicked!", this.state.count);
  }
پس از ذخیره‌ی فایل و اجرای برنامه، اینبار با کلیک بر روی دکمه‌ی Increment، بلافاصله خطای «Uncaught TypeError: Cannot read property 'state' of undefined» در کنسول توسعه دهنده‌های مرورگر ظاهر می‌شود. عنوان می‌کند که شیء this در این متد، undefined است؛ بنابراین امکان خواندن خاصیت state از آن وجود ندارد.


bind مجدد شیء this در رخ‌دادگردان‌های React

در مورد this و bind مجدد آن در مطلب «React 16x - قسمت 2 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 1» مفصل بحث کردیم و در اینجا می‌خواهیم از نتایج آن استفاده کنیم.
همانطور که مشاهده کردید، در متد رویدادگران handleIncrement، به شیء this دسترسی نداریم. چرا؟ چون this در جاوا اسکریپت نسبت به سایر زبان‌های برنامه نویسی، متفاوت رفتار می‌کند. بسته به اینکه یک متد یا تابع، چگونه فراخوانی می‌شود، this می‌تواند اشیاء متفاوتی را بازگشت دهد. اگر تابعی به عنوان یک متد و جزئی از یک شیء فراخوانی شود، this در این حالت همواره ارجاعی را به آن شیء باز می‌گرداند. اما اگر آن تابع به صورت متکی به خود فراخوانی شد، به صورت پیش‌فرض ارجاعی را به شیء سراسری window مرورگر، بازگشت می‌دهد و اگر strict mode فعال باشد، تنها undefined را بازگشت می‌دهد. به همین جهت است که در اینجا خطای undefined بودن this را دریافت می‌کنیم.
یک روش حل این مشکل که پیشتر نیز در مورد آن توضیح دادیم، استفاده از متد bind است:
  constructor() {
    super();
    console.log("constructor", this);
    this.handleIncrement = this.handleIncrement.bind(this);
  }
زمانیکه شیءای از نوع کلاس جاری ایجاد می‌شود، متد constructor آن نیز فراخوانی خواهد شد. در این مرحله دسترسی کاملی به شیء this وجود دارد که نمونه‌ی آن‌را با console.log نوشته شده می‌توانید آزمایش کنید. در اینجا چون کامپوننت جاری از کلاس Component مشتق شده‌است، پیش از دسترسی به شیء this، نیاز است سازنده‌ی کلاس پایه توسط متد super فراخوانی شود. اکنون که به this دسترسی داریم، می‌توان توسط متد bind، مقدار شیء this شیءای دیگر مانند this.handleIncrement را تنظیم مجدد کنیم (متدها نیز در جاوا اسکریپت شیء هستند). خروجی آن، یک وهله‌ی جدید از شیء handleIncrement است که this آن اینبار به وهله‌ای از شیء جاری اشاره می‌کند. به همین جهت خروجی آن‌را به this.handleIncrement انتساب می‌دهیم تا مشکل تعریف نشده بودن this آن برطرف شود.
اکنون اگر برنامه را اجرا کنید، با کلیک بر روی دکمه‌ی Increment، بجای this.state.count لاگ شده، مقدار آن که صفر است، در کنسول توسعه دهنده‌های مرورگر ظاهر می‌شود.


این یک روش است که کار می‌کند؛ اما کمی طولانی است و به ازای هر روال رویدادگردانی باید دقیقا به همین نحو تکرار شود. روش دیگر، تبدیل متد handleIncrement به یک arrow function است و همانطور که در قسمت دوم این سری نیز بررسی کردیم، arrow functionها، this شیء جاری را بازنویسی نمی‌کنند؛ بلکه آن‌را به ارث می‌برند. بنابراین ابتدا کدهای سازنده‌ی فوق را حذف می‌کنیم (چون دیگر نیازی به آن‌ها نیست) و سپس متد handleIncrement سابق را به صورت زیر، تبدیل به یک arrow function می‌کنیم:
  handleIncrement = () => {
    console.log("Increment clicked!", this.state.count);
  }
به این ترتیب با کلیک بر روی دکمه‌ی Increment، مجددا همان خروجی تصویر قبلی را دریافت می‌کنیم؛ این روش ساده‌تر و تمیزتر است و نیازی به rebind دستی تک تک رویدادگردان‌های کامپوننت جاری در این حالت وجود ندارد.


به روز رسانی state در کامپوننت‌های React

اکنون که در روال رویدادگردان handleIncrement به شیء this و سپس مقدار this.state.count آن دسترسی پیدا کرده‌ایم، می‌خواهیم با هربار کلیک بر روی این دکمه، یک واحد مقدار آن‌را افزایش داده و در UI نمایش دهیم.
در React، خواص شیء state را جهت نمایش آن‌ها در UI، مستقیما تغییر نمی‌دهیم. به عبارت دیگر نوشتن یک چنین کدی در React برای به روز رسانی UI، مرسوم نیست:
  handleIncrement = () => {
    this.state.count++;
  };
اگر تغییر فوق را اعمال و سپس برنامه را اجرا کنید، با کلیک بر روی دکمه‌ی Increment ... اتفاقی رخ نمی‌دهد! رفتار React با Angular متفاوت است و در اینجا هرچند توسط فراخوانی {()this.formatCount} کار نمایش خاصیت count انجام می‌شود، اما به ظاهر، تغییرات مقدار count، به عبارات JSX متصل نیست. در کامپوننت‌های Angular اگر مقدار خاصیتی را تغییر دهید و اگر این خاصیت در قالب آن کامپوننت، به آن خاصیت bind شده باشد، شاهد به روز رسانی آنی UI خواهید بود (Change Detection آنی و به ازای هر تغییری)؛ اما در React خیر. هرچند در همان Angular هم توصیه می‌شود که از حالت changeDetection: ChangeDetectionStrategy.OnPush برای رسیدن به حداکثر کارآیی نمایشی کامپوننت‌ها استفاده شود؛ حالت OnPush در Angular، به روش تشخیص تغییرات React که در ادامه توضیح داده می‌شود، بیشتر شبیه است.

در کدهای فوق هرچند با کلیک بر روی دکمه‌ی Increment، مقدار count افزایش یافته‌است، اما React از وقوع این تغییرات مطلع نیست. به همین جهت است که هیچ تغییری را در UI برنامه مشاهده نمی‌کنید.
با اجرای قطعه کد فوق، یک چنین اخطاری نیز در کنسول توسعه دهندگان مرورگر ظاهر می‌شود:
  Line 33:5:  Do not mutate state directly. Use setState()  react/no-direct-mutation-state

برای رفع این مشکل باید از یکی از متدهای به ارث برده شده‌ی از کلاس پایه‌ی Component، به نام setState استفاده کرد. به این ترتیب به React اعلام می‌کنیم که state تغییر کرده‌است (فعالسازی Change Detection، فقط در صورت نیاز). سپس React شروع به محاسبه‌ی تغییرات کرده و در نتیجه قسمت‌های متناظری از UI را برای هماهنگ سازی DOM مجازی خودش با DOM اصلی، به روز رسانی می‌کند.
زمانیکه از متد setState استفاده می‌کنیم، شیءای را باید به صورت یک پارامتر به آن ارسال کنیم. در این حالت مقادیر آن یا به خاصیت state جاری اضافه می‌شوند و یا در صورت از پیش موجود بودن، همان خواص را بازنویسی می‌کنند:
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
در اینجا به متد this.setState که از قسمت extends Component جاری به ارث رسیده‌است، یک شیء را با خاصیت count و مقدار جدیدی، ارسال می‌کنیم.
در این مرحله، فایل جاری را ذخیره کرده و پس از بارگذاری مجدد برنامه در مرورگر، بر روی دکمه‌ی Increment کلیک کنید. اینبار ... کار می‌کند! چون React از تغییرات مطلع شده‌است:


وقتی state تغییر می‌کند، چه اتفاقاتی رخ می‌دهند؟

با فراخوانی متد this.setState، به React اعلام می‌کنیم که state یک کامپوننت قرار است تغییر کند. سپس React فراخوانی مجدد متد Render را در صف اجرایی خودش قرار می‌دهد تا در زمانی در آینده، اجرا شود؛ این فراخوانی async است. کار متد render، بازگشت یک المان جدید React است. در اینجا DOM مجازی React از چند المان، به صورت یک div و دو فرزند دکمه و span تشکیل شده‌است. در این حالت یک DOM مجازی قدیمی نیز از قبل (پیش از اجرای مجدد متد render) وجود دارد. در این لحظه، React این دو DOM مجازی را کنار هم قرار می‌دهد و محاسبه می‌کند که در اینجا دقیقا کدام المان‌ها نسبت به قبل تغییر کرده‌اند. برای نمونه در اینجا تشخیص می‌دهد که span است که تغییر کرده، چون مقدار count، توسط آن نمایش داده می‌شود. در این حالت از کل DOM اصلی، تنها همان span تغییر کرده را به روز رسانی می‌کند و نه کل DOM را (و نه اعمال مجدد کل المان‌های حاصل از متد render را).
این مورد را می‌توان به نحو زیر آزمایش و مشاهده کرد:
در مرورگر بر روی المان span که شماره‌ها را نمایش می‌دهد، کلیک راست کرده و گزینه‌ی inspect را انتخاب کنید. سپس بر روی دکمه‌ی Increment کلیک نمائید. مرورگر قسمتی را که به روز می‌شود، با رنگی مشخص و متمایز، به صورت لحظه‌ای نمایش می‌دهد:



ارسال پارامترها به متدهای رویدادگردان

تا اینجا متد handleIncrement، بدون پارامتر تعریف شده‌است. فرض کنید در یک برنامه‌ی واقعی قرار است با کلیک بر روی این دکمه، id یک محصول را نیز به handleIncrement، منتقل و ارسال کنیم. اما در onClick={this.handleIncrement} تعریف شده، یک ارجاع را به متد handleIncrement داریم. بنابراین برای حل این مساله نمی‌توان از روشی مانند onClick={this.handleIncrement(1)} استفاده کرد که در آن عدد فرضی 1 به صورت آرگومان متد handleIncrement ذکر شده‌است.
یک روش حل این مساله، تعریف متد دومی است که متد handleIncrement پارامتر دار را فراخوانی می‌کند:
  doHandleIncrement = () => {
    this.handleIncrement({ id: 1, name: "Product 1" });
  };
و در این حالت برای مثال متد handleIncrement یک شیء را پذیرفته‌است:
  handleIncrement = product => {
    console.log(product);
    this.setState({ count: this.state.count + 1 });
  };
سپس بجای تعریف onClick={this.handleIncrement}، از متد doHandleIncrement استفاده خواهیم کرد؛ یعنی onClick={this.doHandleIncrement}

هرچند این روش کار می‌کند، اما بیش از اندازه طولانی شده‌است. راه حل بهتر، استفاده از یک inline function است:
onClick={() => this.handleIncrement({ id: 1, name: "Product 1" })}
یعنی کل arrow function مربوط به doHandleIncrement را داخل onClick قرار می‌دهیم و چون یک سطری است، نیازی به ذکر {} و سمی‌کالن انتهای آن‌را هم ندارد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-04-part02.zip
مطالب
تبدیل یک قالب HTML معمولی به قالب React

با توجه به اینکه React یک سیستم متشکل از کامپوننت‌های کوچک و بزرگ است و از JSX  جهت کدنویسی استفاده میکند و یک قالب HTML، متشکل از تمام عناصر به صورت درهم ریخته می‌باشد و بخش‌های مختلفی دارد، امکان استفاده‌ی مستقیم از قالب HTML در آن وجود ندارد و باید با فرمت React همخوانی داشته باشد. من در اینجا از قالب رایگان و راستچین شده AdminLTE که بر پایه بوت استرپ 4 میباشد استفاده کرده‌ام. 

همانطور که میدانید React پوشه‌ای را به نام public، مهیا کرده‌است که برای استفاده‌ی عمومی از فایل‌های استاتیک ایجاد شده‌است. پس ابتدا فایل‌های js,css، تصاویر و دیگر فایل‌های استاتیک را به پوشه‌ی public  منتقل میکنیم. سپس فایل index قالب را باز کرده و به تگ header فایل مراجعه کنید. تگ‌های لینک‌های معرفی فایل‌های css و script ای را که در آن تعریف شده‌اند، کپی کرده به هدر فایل index.html که در پوشه‌ی public قرار دارد، منتقل کنید. همچنین از فایل‌های اسکرپیت دیگر که در پایین تگ Body قرار گرفته‌اند، غافل نگردید. 

در اینجا باید بخش‌های اساسی قالب، همانند navbar و sidebar را به صورت کامپوننت ایجاد کنیم.

پس ابتدا یک کامپوننت NavBar.jsx را ایجاد کرده  و کدهای همین قسمت را در متد render قرار میدهیم:

 import React, { Component } from "react";  
class NavBar extends Component {
  state = {};
  render() {
    return (
      <React.Fragment>
            <nav>
                <!-- Left navbar links -->
    <ul>
                    <li>
                        <a data-widget="pushmenu" href="#"><i></i></a>
                    </li>
                    <li>
                        <a href="index3.html">خانه</a>
                    </li>
                    <li>
                        <a href="#">تماس</a>
                    </li>
                </ul>

                <!-- SEARCH FORM -->
    <form>
                    <div>
                        <input type="search" placeholder="جستجو" aria-label="Search">
                            <div>
                                <button type="submit">
                                    <i></i>
                                </button>
                            </div>
        </div>
    </form>
                    ...
</nav>
      </React.Fragment>
    );
  }
}

export default NavBar;
در همین جا با خطاهای زیادی روبرو میشویم که  به شما نشان میدهد کدهای کپی شده، هیچ استانداردی از jsx را رعایت نمیکنند به همین جهت شروع به ویرایش کد کپی شده میکنیم: 
  • تمامی کامنت‌های موجود در فایل را حذف کنید.
  • تمام تگ‌ها که شامل خصوصیت class هستند  را با استفاده از ابزار جستجو، یافته و با عبارت className جایگزین کنید.
  • در صورتیکه روی تگ‌ها از خصوصیت style استفاده کرده‌اید، به شکل زیر ویرایش کرده و قالب jsx را روی آن پیاده کنید.
style="opacity:0.8;"
به
style={{ opacity: "0.8" }}

  • در صورتیکه از تگ‌های img و یا input استفاده میکنید، حتما باید انتها تگ‌ها به شکل زیر بسته شده باشند:
<input type="search" placeholder="جستجو" aria-label="Search">
به
 <input
                className="form-control form-control-navbar"
                type="search"
                placeholder="جستجو"
                aria-label="Search"
              />

در نهایت باید کامپوننت بدون هیچ خطایی به شکل زیر ایجاد گردد:
import React, { Component } from "react";

class NavBar extends Component {
  state = {};
  render() {
    return (
      <React.Fragment>
        <nav className="main-header navbar navbar-expand bg-white navbar-light border-bottom">
          <ul className="navbar-nav">
            <li className="nav-item">
              <a className="nav-link" data-widget="pushmenu" href="#">
                <i className="fa fa-bars"></i>
              </a>
            </li>
            <li className="nav-item d-none d-sm-inline-block">
              <a href="index3.html" className="nav-link">
                خانه
              </a>
            </li>
            <li className="nav-item d-none d-sm-inline-block">
              <a href="#" className="nav-link">
                تماس
              </a>
            </li>
          </ul>

          <form className="form-inline ml-3">
            <div className="input-group input-group-sm">
              <input
                className="form-control form-control-navbar"
                type="search"
                placeholder="جستجو"
                aria-label="Search"
              />
              <div className="input-group-append">
                <button className="btn btn-navbar" type="submit">
                  <i className="fa fa-search"></i>
                </button>
              </div>
            </div>
          </form>

     ...
        </nav>
      </React.Fragment>
    );
  }
}

export default NavBar;

کامپوننت‌های دیگر مانند sidebar و footer را به همین شکل ایجاد کرده و در نهایت در فایل App.jsx در متد رندر قرار دهید:
return (
    <React.Fragment>
      <NavBar />
      <SideBar />
      <div className="content-wrapper" style={{ marginTop: "20px" }}>
        <DomainList />
      </div>
      <Footer />
    </React.Fragment>
  )
 در کامپوننت‌های جاری همچون NavBar، نمونه‌هایی از اشیاء دیگر نیز به چشم میخورند که قابلیت تبدیل شدن به کامپوننت‌های مجزایی را دارند؛ همانند پیام‌های اخیر، اعلان‌های سیستم و ... که میتوانید به صورت جزء به جزء، ایجاد کامپوننت‌های آن‌ها را انجام دهید. 
نکته‌ی مهم: در فایل index.html، یک سری تگ را که به فایل‌های css و js ارجاع دارند، در اولین مرحله به این فایل کپی کرده‌اید. تعداد زیادی از این کتابخانه‌ها در مخزن npm موجود بوده و قابلیت import شدن آن‌ها توسط React فراهم است؛ همانند کتابخانه‌های بوت استرپ و یا FontAwesome  که در این مقاله  به همین شکل وارد شده‌اند. بهتر است این موراد اصلاح گردیده و انتقال این فایل‌ها به index.js فراهم گردد. از این رو که به روزرسانی این بسته‌ها از طریق npm ساده‌تر بوده و WebPack نیز مدیریت این بسته‌ها را به عهده می‌گیرد. 
مطالب
Angular Animation – بخش اول
متحرک سازی (Motion)، یکی از مفاهیم مهم در طراحی وب‌اپلیکیشن‌های مدرن محسوب می‌شود. امروزه استفاده از انیمیشن در طراحی رابط کاربری به عنوان جزء جدا ناپذیر UX محسوب می‌شود. برای درک اهمیت انیمیشن در طراحی، « نه فقط به برای زیبایی و چیدمان، بلکه به عنوان جزء جدایی ناپذیر UX » را ببینید. در Angular طراحی انیمیشن برای ساخت رابط کاربری نه تنها کاری سرگرم کننده، بلکه بسیار آسان است.

نصب Angular Animations  
برای شروع کار با Animation در Angular، ابتدا باید کتابخانه Angular Animation را توسط دستور زیر نصب کرد و سپس BrowserAnimationsModule را به ماژول اصلی برنامه اضافه کنید. 
> npm install @angular/animations@latest --save
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
  imports: [
    //…
    BrowserAnimationsModule
  ],
})

در اینجا مثال ساده‌ای را مشاهده خواهید کرد که انتقال (transition) المنت li را از حالت active به inactive، با یک انمیشن ساده همراه خواهد کرد. محل تعریف انیمیشن‌ها در Angular، داخل متادیتای component@ و توسط خصوصیت animations می‌باشد. در ابتدای کار باید توابع مختص انیمیشن را در فایل component مورد نظر خود توسط دستور import به شکل زیر وارد کنیم: 
import { trigger, state, style, transition, animate } from '@angular/animations';
برای پیاده سازی انیمیشن در component، وارد کردن تمامی توابع بالا اجباری هستند. در قدم بعدی با افزودن خصوصیت animations به متادیتای component@، کدهای مربوط به کامپوننت را به شکل زیر تغییر می‌دهیم: 
import { Component } from '@angular/core';
import { trigger,state,style,transition,animate } from '@angular/animations';

@Component({
  selector: 'app-root',
  template: `
    <div style='width:50%; margin:auto'>
      <ul>
        <li [@myState]="currentState"
          style='width:100px; padding:10px; list-style-type: none' 
            (click)="toggleState()">
          {{currentState}}
        </li>
      </ul>
    </div>
  `,
  styleUrls: ['./app.component.css'],
  animations: [
      trigger('myState', [
        state('inactive', style({
          backgroundColor: '#eee',
          transform: 'scale(1)'
        })),
        state('active',   style({
          backgroundColor: '#cfd8dc',
          transform: 'scale(1.1)'
        })),
        transition('inactive => active', animate('100ms ease-in')),
        transition('active => inactive', animate('100ms ease-out'))
      ])
    ]
})
export class AppComponent {
  currentState: string = 'inactive';
  toggleState():void{
    this.currentState = this.currentState === 'inactive' ? 'active' : 'inactive';
  }
}


توضیحات تکمیلی

trigger: جهت تعریف یک انیمیشن، از تابع trigger استفاده می‌کنیم. این تابع استفاده شده‌ی در خصوصیت animations، در پارامتر اول خود، یک نام یکتا را دریافت خواهد کرد و در پارامتر بعدی، لیستی از توابع وارد شده مختص انیمیشن را دریافت می‌کند.

نکته: خصوصیت animations، قابلیت دریافت چندین تابع trigger را برای داشتن چندین انیمیشن مجزا، دارا است.

state: با استفاده از این تابع قادر به تعریف وضعیت‌های مختلف خواهیم بود. انیمشن در Angular بر دو منطق وضعیت (state) و گذار (transition) پیاده سازی می‌شود. به عبارت دیگر انیمیشن در Angular بر روی گذار (transition) بین وضعیت‌ها (states) قابل تعریف هستند. این تابع در پارامتر اول خود یک نام و در پارامتر دوم خود تابع style را دریافت می‌کند.

style: با استفاده از این تابع قادر به تعریف استایلی برای وضعیت تعریف شده خواهیم بود.

transition: برای ساخت انیمیشن واقعی مورد استفاده قرار می‌گیرد. این تابع در پارامتر اول خود، مسیر حرکت بین دو حالت (state) را به صورت یک رشته دریافت کرده و در پارامتر دوم خود، تابع animate را دریافت می‌کند. در این مثال مسیر حرکت به صورت 'inactive => active' تعریف شده است. یعنی هنگام گذار از وضعیت inactive به وضعیت active، تابع animate در پارامتر دوم اجرا خواهد شد.

animate: این تابع دو پارامتر timing و styles را دریافت می‌کند. timing مقداری از جنس رشته (string) است که می‌تواند ترکیبی از مدت زمان (duration) با مقدار اختیاری تاخیر(delay) و مقدار easing باشد. به عنوان مثال مقدار کامل زیر را درنظر بگیرید: 

'1s 100ms ease-out'

در این اینجا مدت زمان، برابر ۱ ثانیه، تاخیر، ۱۰۰ میلی ثانیه و easing، مقدار ease-out خواهد بود و به معنی اجرای انیمیشن با افکت ease-out و در مدت زمان ۱ ثانیه و با تاخیر در شروع به مدت ۱۰۰ میلی ثانیه می‌باشد. در صورتیکه به این پارامتر مقداری عددی را ارسال کنیم، عدد به عنوان مدت زمان (duration) و بر مبنای واحد میلی ثانیه در نظر گرفته خواهد شد. تمامی مقادیر زیر برای ارسال به این پارامتر معتبر می‌باشند: 

500            // duration=500ms 
"1s"            // duration=1s
"100ms 0.5s"        // duration=100ms, delay=0.5s
"5s ease"        // duration=5s, easing=ease
"5s 10ms cubic-bezier(.17,.67,.88,.1)"    // duration=5s, delay=10ms, easing=cubic-bezier(.17,067,.88,.1)

پارامتر styles در تابع animate یکی از توابع style یا keyframes می‌باشد. البته این پارامتر اختیاری است و در قسمت نکات تکمیلی توضیح داده خواهد شد. در مثال بالا از این پارامتر صرف نظر شده است.

برای متصل کردن انیمیشن تعریف شده به المنت‌های موجود در صفحه، از خصوصیت [triggerName@] استفاده کنید. trigger تعریف شده در قطعه کد بالا myState نام دارد. بنابراین برای اینکه المنت li در گذار بین حالت‌ها، این انیمیشن را داشته باشد، باید [myState@] را به المنت خود اضافه کنید. همچنین مقدار حالت جاری را باید برای این خصوصیت تامین کرد. این مقدار می‌تواند یک رشته استاتیک یا یک عبارت محاسبه شده توسط یک تابع یا یک متغیر باشد.

همانطور که در مثال بالا مشاهده می‌کنید، با استفاده از متغیر currentState، المنت li در ابتدا در حالت inactive قرار دارد. با کلیک اول بر روی المنت، تابع toggleState باعث تغییر وضعیت المنت از وضعیت inactive به وضعیت active خواهد شد (inactive=>active) بنابراین انیمیشن زیر اجرا خواهد شد.

 transition('inactive => active', animate('100ms ease-in'))

با کلیک دوباره، المنت از وضعیت active به inactive خواهد رفت (active=>inactive)، بنابراین انیمیشن زیر اجرا می‌شود.

 

نکات تکمیلی

 در صورتیکه در تابع transition، پارامتر دوم برای حالتهای مختلف یکسان باشد، برای مثال رفتن از حالت active به حالت inactive و برعکس، می‌توان از روش زیر استفاده کرد. 
transition('inactive => active, active => inactive', animate('100ms ease-out'))

یا به شکل ساده‌تر: 

transition('inactive <=> active', animate('100ms ease-out'))


کاراکتر * جایگزین تمامی حالتهای موجود در برنامه خواهد بود. برای مثال:

'* <= active': بر روی تمامی گذارهای از حالت active به هر حالت دیگر، اعمال خواهد شد.

'active <= *': بر روی تمامی گذارهای از هر حالت به حالت active، اعمال خواهد شد.

 '* <= *': بر روی تمامی گذارها اعمال خواهد شد. 



همانطور که قبلا ذکر شد، تابع animate در پارامتر دوم خود یک تابع style یا keyframes را دریافت می‌کند. در صورتیکه بخواهیم در مدت زمان اجرای انیمیشن بر روی المنت، استایلی را نیز همراه کنیم، می‌توانیم از تابع style استفاده کنیم. اما این استایل بعد از اتمام انیمیشن بر روی المنت باقی نخواهد ماند؛ چون حالت (state) مقصد برای خود استایل تعریف شده‌است. علاوه بر این، در تابع transition می‌توان به شکل زیر یک استایل خطی را نیز تعریف کرد: 

transition('inactive => active', [
  style({
    backgroundColor: '#cfd8dc',
    transform: 'scale(1.3)'
  }),
  animate('80ms ease-in', style({
    backgroundColor: '#eee',
    transform: 'scale(1)'
  }))
]),

این تعریف استایل در تابع transition در شروع گذار بلافاصله بر روی المنت اعمال خواهد شد و در طول انیمیشن استایل را از دست خواهد داد و به استایل مقصد خواهد رسید.

در اینجا یک مثال بسیار ساده از نحوه پیاده سازی انیمیشن در Angular را مورد برسی قرار دادیم. در بخش بعدی نحوه‌ی پیاده سازی انیمیشن‌های پیشرفته که قابلیت اعمال بر روی المنت‌ها را هنگام اضافه شدن به صفحه و ترک صفحه را خواهند داشت، مورد بررسی قرار می‌دهیم.

نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت هشتم - دریافت اطلاعات از سرور
با سلام. در یک پروژه Angular 5 داده‌های ComboBox‌ها باید در سمت کلاینت کش شوند. بنده در ngOnInit مربوط به app.component عملیات دریافت این داده‌ها را با http.get انجام میدهم و داده‌ها در یک پروپرتی Observable کش می‌شوند. حالا در فرم هایی که به این داده‌ها نیاز دارم در متد ngOnInit اقدام به دریافت داده‌ها از کش میکنم اما هنوز داده‌ها دریافت نشده اند (مشکل همزمانی).
همچنین بنده lifecycle Angular رو بررسی کردم. از ngAfterContentInit هم برای دریافت داده‌ها در فرم‌ها استفاده کردم اما باز هم موقع اجرای این متد(ngAfterContentInit   ) هنور متد subscribe مربوط به http.get اجرا نشده است.
 لطفا در صورت امکان راهنمایی کنید. با تشکر
مطالب
افزودن و اعتبارسنجی خودکار Anti-Forgery Tokens در برنامه‌های Angular مبتنی بر ASP.NET Core
Anti-forgery tokens یک مکانیزم امنیتی، جهت مقابله با حملات CSRF هستند. در برنامه‌های ASP.NET Core، فرم‌های دارای Tag Helper مانند asp-controller و asp-action به صورت خودکار دارای یک فیلد مخفی حاوی این token، به همراه تولید یک کوکی مخصوص جهت تعیین اعتبار آن خواهند بود. البته در برنامه‌های ASP.NET Core 2.0 تمام فرم‌ها، چه حاوی Tag Helpers باشند یا خیر، به همراه درج این توکن تولید می‌شوند.
برای مثال در برنامه‌های ASP.NET Core، یک چنین فرمی:
<form asp-controller="Manage" asp-action="ChangePassword" method="post">   
   <!-- Form details --> 
</form>
به صورت ذیل رندر می‌شود که حاوی قسمتی از Anti-forgery token است و قسمت دیگر آن در کوکی مرتبط درج می‌شود:
<form method="post" action="/Manage/ChangePassword">   
  <!-- Form details --> 
  <input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkSldwD9CpLR...LongValueHere!" /> 
</form>
در این مطلب چگونگی شبیه سازی این عملیات را در برنامه‌های Angular که تمام تبادلات آن‌ها Ajax ایی است، بررسی خواهیم کرد.


تولید خودکار کوکی‌های Anti-forgery tokens برای برنامه‌های Angular

در سمت Angular، مطابق مستندات رسمی آن (^ و ^)، اگر کوکی تولید شده‌ی توسط برنامه، دارای نام مشخص «XSRF-TOKEN» باشد، کتابخانه‌ی HTTP آن به صورت خودکار مقدار آن‌را استخراج کرده و به درخواست بعدی ارسالی آن اضافه می‌کند. بنابراین در سمت ASP.NET Core تنها کافی است کوکی مخصوص فوق را تولید کرده و به Response اضافه کنیم. مابقی آن توسط Angular به صورت خودکار مدیریت می‌شود.
می‌توان اینکار را مستقیما داخل متد Configure کلاس آغازین برنامه انجام داد و یا بهتر است جهت حجیم نشدن این فایل و مدیریت مجزای این مسئولیت، یک میان‌افزار مخصوص آن‌را تهیه کرد:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace AngularTemplateDrivenFormsLab.Utils
{
    public class AntiforgeryTokenMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IAntiforgery _antiforgery;

        public AntiforgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
        {
            _next = next;
            _antiforgery = antiforgery;
        }

        public Task Invoke(HttpContext context)
        {
            var path = context.Request.Path.Value;
            if (path != null && !path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase))
            {
                var tokens = _antiforgery.GetAndStoreTokens(context);
                context.Response.Cookies.Append(
                      key: "XSRF-TOKEN",
                      value: tokens.RequestToken,
                      options: new CookieOptions
                      {
                          HttpOnly = false // Now JavaScript is able to read the cookie
                      });
            }
            return _next(context);
        }
    }

    public static class AntiforgeryTokenMiddlewareExtensions
    {
        public static IApplicationBuilder UseAntiforgeryToken(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<AntiforgeryTokenMiddleware>();
        }
    }
}
توضیحات تکمیلی:
- در اینجا ابتدا سرویس IAntiforgery به سازنده‌ی کلاس میان افزار تزریق شده‌است. به این ترتیب می‌توان به سرویس توکار تولید توکن‌های Antiforgery دسترسی یافت. سپس از این سرویس جهت دسترسی به متد GetAndStoreTokens آن برای دریافت محتوای رشته‌ای نهایی این توکن استفاده می‌شود.
- اکنون که به این توکن دسترسی پیدا کرده‌ایم، تنها کافی است آن‌را با کلید مخصوص XSRF-TOKEN که توسط Angular شناسایی می‌شود، به مجموعه‌ی کوکی‌های Response اضافه کنیم.
- علت تنظیم مقدار خاصیت HttpOnly به false، این است که کدهای جاوا اسکریپتی Angular بتوانند به مقدار این کوکی دسترسی پیدا کنند.

پس از تدارک این مقدمات، کافی است متد الحاقی کمکی UseAntiforgeryToken فوق را به نحو ذیل به متد Configure کلاس آغازین برنامه اضافه کنیم؛ تا کار نصب میان افزار AntiforgeryTokenMiddleware، تکمیل شود:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
   app.UseAntiforgeryToken();


پردازش خودکار درخواست‌های ارسالی از طرف Angular

تا اینجا برنامه‌ی سمت سرور ما کوکی‌های مخصوص Angular را با کلیدی که توسط آن شناسایی می‌شود، تولید کرده‌است. در پاسخ، Angular این کوکی را در هدر مخصوصی به نام «X-XSRF-TOKEN» به سمت سرور ارسال می‌کند (ابتدای آن یک X اضافه‌تر دارد).
به همین جهت به متد ConfigureServices کلاس آغازین برنامه مراجعه کرده و این هدر مخصوص را معرفی می‌کنیم تا دقیقا مشخص گردد، این توکن از چه قسمتی باید جهت پردازش استخراج شود:
public void ConfigureServices(IServiceCollection services)
{
      services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN");
      services.AddMvc();
}

یک نکته: اگر می‌خواهید این کلیدهای هدر پیش فرض Angular را تغییر دهید، باید یک CookieXSRFStrategy سفارشی را برای آن تهیه کنید.


اعتبارسنجی خودکار Anti-forgery tokens در برنامه‌های ASP.NET Core

ارسال کوکی اطلاعات Anti-forgery tokens و سپس دریافت آن توسط برنامه، تنها یک قسمت از کار است. قسمت بعدی، بررسی معتبر بودن آن‌ها در سمت سرور است. روش متداول انجام اینکار‌، افزودن ویژگی [ValidateAntiForgeryToken]  به هر اکشن متد مزین به [HttpPost] است:
  [HttpPost] 
  [ValidateAntiForgeryToken] 
  public IActionResult ChangePassword() 
  { 
    // ... 
    return Json(…); 
  }
هرچند این روش کار می‌کند، اما در ASP.NET Core، فیلتر توکار دیگری به نام AutoValidateAntiForgeryToken نیز وجود دارد. کار آن دقیقا همانند فیلتر ValidateAntiForgeryToken است؛ با این تفاوت که از حالت‌های امنی مانند GET و HEAD صرفنظر می‌کند. بنابراین تنها کاری را که باید انجام داد، معرفی این فیلتر توکار به صورت یک فیلتر سراسری است، تا به تمام اکشن متدهای HttpPost برنامه به صورت خودکار اعمال شود:
public void ConfigureServices(IServiceCollection services)
{
       services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN");
       services.AddMvc(options =>
       {
           options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
       });
}
به این ترتیب دیگر نیازی نیست تا ویژگی ValidateAntiForgeryToken را به تک تک اکشن متدهای از نوع HttpPost برنامه به صورت دستی اعمال کرد.

یک نکته: در این حالت بررسی سراسری، اگر در موارد خاصی نیاز به این اعتبارسنجی خودکار نبود، می‌توان از ویژگی [IgnoreAntiforgeryToken] استفاده کرد.


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

برای آزمایش مواردی را که تا کنون بررسی کردیم، همان مثال «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» را بر اساس نکات متدهای ConfigureServices و Configure مطلب جاری تکمیل می‌کنیم. سپس برنامه را اجرا می‌کنیم:


همانطور که ملاحظه می‌کنید، در اولین بار درخواست برنامه، کوکی مخصوص Angular تولید شده‌است.
در ادامه اگر فرم را تکمیل کرده و ارسال کنیم، وجود هدر ارسالی از طرف Angular مشخص است و همچنین خروجی هم با موفقیت دریافت شده‌است:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-09.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
نحوه‌ی استفاده از ViewComponent درون Controller
در ASP.NET Core یک View Component، در نهایت خلاصه‌ایی از قابلیت‌هایی را ارائه میدهد که قرار است توسط یک کنترلر مدیریت شوند؛ زیرا پارامترهای یک View Component از طریق یک HTTP Request تامین نمی‌شوند. یعنی به صورت مستقیم از طریق درخواست‌های HTTP قابل دسترسی نیستند. فرض کنید در یک برنامه می‌خواهیم لیست کاربران سایت را نمایش دهیم تا با کلیک بر روی نام کاربر، امکان ویرایش کاربر انتخاب شده را داشته باشیم. با کلیک بر روی لینک مورد نظر، اطلاعات درخواست، به کنترلر UserManagerController و سپس اکشن متد Edit ارسال خواهد شد. در حالت عادی باید یک ViewComponent برای لیست کاربران و همچنین یک UserManagerController، برای ویرایش کاربر درون پروژه داشته باشیم:
public class UserListViewComponent : ViewComponent
{
    private readonly UserRepository repository;

    public UserListViewComponent(UserRepository repository)
    {
        this.repository = repository;
    }
    public IViewComponentResult Invoke()
    {
        var users = repository.GetUsers().Take(10).ToList();
        return View(model: users);
    }
}

ویووی کامپوننت فوق نیز به این صورت تعریف شده است:
@model IList<User>
@foreach (var user in Model)
{
    <li>
        <a asp-controller="UserManager" asp-action="Edit" asp-route-id="@user.Id">@user.Name</a>
    </li>
}
<a class="btn btn-info" asp-controller="UserManager" asp-action="Index">More...</a>

کنترلر UserManager نیز یک چنین تعریفی را دارد:
public class UserManagerController : Controller
{
    private readonly UserRepository repository;

    public UserManagerController(UserRepository repository)
    {
        this.repository = repository;
    }

    public ViewResult Index()
    {
        var users = repository.GetUsers().ToList();
        return View(users);
    }

    public ViewResult Edit(int id)
    {
        var user = repository.GetUsers().FirstOrDefault(u => u.Id == id);
        return View(user);
    }

    [HttpPost]
    public IActionResult Edit(User user)
    {
        repository.Edit(user);
        return RedirectToAction("Index", "Home");
    }
}

در ادامه ویووهای تعریف شده‌ی برای کنترلر فوق را نیز مشاهده میکنید:
// Views/UserManager/Edit.cshtml
@model User
<div class="row">
    <div class="col-md-8">
        <form method="post">
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Name"></label>
                <input asp-for="Name" class="form-control"/>
            </div>
            <div class="form-group">
                <label asp-for="LastName"></label>
                <input asp-for="LastName" class="form-control"/>
            </div>
            <div class="form-group">
                <label asp-for="Age"></label>
                <input asp-for="Age" class="form-control"/>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
        </form>
    </div>
</div>

// Views/UserManager/Index.cshtml
@model IList<User>
<table class="table">
    <tr>
        <td>Id</td>
        <td>Name</td>
        <td>LastName</td>
        <td>Age</td>
    </tr>
    @foreach (var user in Model)
    {
         <tr>
            <td>@user.Id</td>
            <td>@user.Name</td>
            <td>@user.LastName</td>
            <td>@user.Age</td>
        </tr>
    }
</table>

همانطور که مشاهده می‌کنید، کنترلر UserManager و کامپوننت UserList، به ترتیب کار مدیریت و نمایش کاربران را انجام میدهند و منطقاً هر دو جزو قابلیت‌های User هستند. برای جلوگیری از تکرار کد، می‌توانیم کنترلر و ویو‌وکامپوننت فوق را با هم ادغام کنیم؛ در واقع می‌توانیم UserListViewComponent را درون UserManagerController تعریف کنیم. برای این کار کافی است فایل UserManagerController را اینگونه تغییر دهیم:
[ViewComponent(Name = "UserList")]
public class UserManagerController : Controller
{
    private readonly UserRepository repository;

    public UserManagerController(UserRepository repository)
    {
        this.repository = repository;
    }

    public ViewResult Index()
    {
        var users = repository.GetUsers().ToList();
        return View(users);
    }

    public ViewResult Edit(int id)
    {
        var user = repository.GetUsers().FirstOrDefault(u => u.Id == id);
        return View(user);
    }

    [HttpPost]
    public IActionResult Edit(User user)
    {
        repository.Edit(user);
        return RedirectToAction("Index", "Home");
    }

    public IViewComponentResult Invoke()
    {
        var users = repository.GetUsers().Take(10).ToList();
        return new ViewViewComponentResult
        {
            ViewData = new ViewDataDictionary<IList<User>>(ViewData, users)
        };
    }
}

  
توضیحات:
همانطور که پیش‌تر نیز بحث شده است، از ویژگی ViewComponent زمانی استفاده خواهد شد که کلاس موردنظر از کلاس پایه ViewComponent ارث‌بری نکرده باشد و همچنین نام کلاس به ViewComponent ختم نشده باشد. با تعیین پراپرتی Name، یک نام برای ViewComponent تعیین کرده‌ایم که در نهایت درون ویوو، توسط Component.Invoke@  قابل فراخوانی باشد. همچنین از آنجائیکه UserManagerController از کلاس پایه ViewComponent ارث‌بری نکرده است، در نتیجه به اشیاء IViewComponentResult دسترسی نداریم، از این جهت به صورت مستقیم ViewViewComponentResult را ایجاد کرده‌ایم و مدلی که قرار است که به ویوو کامپوننت پاس داده شود را مقداردهی کرده‌ایم.

محل تعریف Viewها
Viewهای کنترلر و همچنین ویووکامپوننت مانند روش فوق قابل ترکیب نیستند؛ در نتیجه نیازی به تغییر هیچکدام از ویووها نخواهیم داشت.
UserManagerController:
/Views/UserManager/Edit.cshtml
/Views/UserManager/Index.cshtml

UserListViewComponent:
/Views/Shared/Components/UserList/Default.cshtml

نکته: دقت داشته باشید که ویجت نمایش لیست کاربران که پیشتر به صورت مستقل از عملکرد یک اکشن متد کار می‌کرده، قرار نیست جایگزین لیست کاربران (اکشن متد Index درون کنترلر UserManager) شود؛ یعنی به صورت مستقل از آن عمل میکند. هدف بیشتر قرار دادن View Component موردنظر درون کنترلر UserManager است.
مطالب
بررسی تغییرات Blazor 8x - قسمت پنجم - امکان تعریف جزیره‌های تعاملی Blazor Server
در Blazor 8x می‌توان صفحات SSR ای را به همراه Blazor server islands و یا Blazor WASM islands داشت؛ یعنی یک کامپوننت Blazor Server که داخل یک صفحه‌ی معمولی SSR قرار گرفته و با سرور، ارتباط SiganlR برقرار می‌کند و یا یک کامپوننت Blazor WASM که در قسمتی از صفحه‌ی SSR درج شده و درون مرورگر کاربر اجرا می‌شود. به هر کدام از این‌ها یک «جزیره‌ی تعاملی» گفته می‌شود (interactive island). در این قسمت، نکات مرتبط با جزایر تعاملی Blazor Server را بررسی می‌کنیم.


بررسی یک مثال: تهیه یک برنامه‌ی Blazor 8x برای نمایش لیست محصولات، به همراه جزئیات آن‌ها

به لطف وجود SSR در Blazor 8x، می‌توان HTML نهایی کامپوننت‌ها و صفحات Blazor را همانند صفحات MVC و یا Razor pages، در سمت سرور تهیه و بازگشت داد. این خروجی در نهایت یک static HTML بیشتر نیست و گاهی از اوقات ما به بیش از یک خروجی ساده HTML ای نیاز داریم.
در این مثال که بر اساس قالب dotnet new blazor --interactivity Server تهیه می‌شود، قصد داریم موارد زیر را پیاده سازی کنیم:
- صفحه‌ای که یک لیست محصولات فرضی را نمایش می‌دهد : بر اساس SSR
- صفحه‌ای که جزئیات یک محصول را نمایش می‌دهد: بر اساس SSR
- دکمه‌ای در ذیل قسمت نمایش جزئیات یک محصول، برای دریافت و نمایش لیست محصولات مشابه و مرتبط: بر اساس Blazor server islands

یعنی تا جائیکه ممکن است قصد نداریم تمام صفحات و تمام قسمت‌های برنامه را با فعالسازی سراسری حالت تعاملی Blazor server که در قسمت‌های قبل در مورد آن توضیح داده شد، پیاده سازی کنیم. می‌خواهیم فقط قسمت کوچکی از این سناریو را که واقعا نیاز به یک چنین قابلیتی را دارد، توسط یک جزیره‌ی تعاملی Blazor server واقع شده‌ی در قسمتی از یک صفحه‌ی استاتیک SSR، مدیریت کنیم.


مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول

namespace BlazorDemoApp.Models;

public record Product
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public required string Description { get; set; }
    public decimal Price { get; set; }

    public List<int> Related { get; set; } = new();
}
در اینجا، هدف تعریف لیستی از محصولات فرضی است؛ به همراه خاصیتی که Id محصولات مشابه را نگهداری می‌کند (خاصیت Related).


سرویس برنامه: سرویسی برای بازگشت لیست محصولات

چون Blazor Server و SSR هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی وجود داشته و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت.
در ادامه کدهای کامل سرویس Services\ProductStore.cs را مشاهده می‌کنید:
using BlazorDemoApp.Models;

namespace BlazorDemoApp.Services;

public interface IProductStore
{
    IList<Product> GetAllProducts();
    Product GetProduct(int id);
    IList<Product> GetRelatedProducts(int productId);
}

public class ProductStore : IProductStore
{
    private static readonly List<Product> ProductsDataSource =
        new()
        {
            new Product
            {
                Id = 1, Title = "Smart speaker", Price = 22m,
                Description =
                    "This smart speaker delivers excellent sound quality and comes with built-in voice control, offering an impressive music listening experience.",
                Related = new List<int> { 2, 3 },
            },
            new Product
            {
                Id = 2, Title = "Regular speaker", Price = 89m,
                Description =
                    "Enjoy room-filling sound with this regular speaker. With its slick design, it perfectly fits into any room in your house.",
                Related = new List<int> { 1, 3 },
            },
            new Product
            {
                Id = 3, Title = "Speaker cable", Price = 12m,
                Description =
                    "This high-quality speaker cable ensures a reliable and clear audio connection for your sound system.",
            },
        };

    public IList<Product> GetAllProducts() => ProductsDataSource;

    public Product GetProduct(int id) => ProductsDataSource.Single(p => p.Id == id);

    public IList<Product> GetRelatedProducts(int productId)
    {
        var product = ProductsDataSource.Single(x => x.Id == productId);
        return ProductsDataSource.Where(p => product.Related.Contains(p.Id)).ToList();
    }
}
هدف از این سرویس، ارائه‌ی لیست تمام محصولات، دریافت اطلاعات یک محصول و همچنین یافتن لیست محصولات مشابه یک محصول خاص است.
این سرویس را باید در فایل Program.cs برنامه به صورت زیر معرفی کرد تا در فایل‌های razor برنامه‌ی جاری قابل دسترسی شود:
builder.Services.AddScoped<IProductStore, ProductStore>();


تکمیل صفحه‌ی نمایش لیست محصولات

قصد داریم زمانیکه کاربر برای مثال به آدرس فرضی http://localhost:5136/products مراجعه کرد، با تصویر لیستی از محصولات مواجه شود:


کدهای این صفحه را که در فایل Components\Pages\Store\ProductsList.razor قرار می‌گیرند، در ادامه مشاهده می‌کنید:

@page "/Products"
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services

@inject IProductStore Store

@attribute [StreamRendering]

<h3>Products</h3>

@if (_products == null)
{
    <p>Loading...</p>
}
else
{
    @foreach (var item in _products)
    {
        <a href="/ProductDetails/@item.Id">
            <div>
                <div>
                    <h5>@item.Title</h5>
                </div>
                <div>
                    <h5>@item.Price.ToString("c")</h5>
                </div>
            </div>
        </a>
    }
}

@code {
    private IList<Product>? _products;

    protected override Task OnInitializedAsync() => GetProductsAsync();

    private async Task GetProductsAsync()
    {
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering
        _products = Store.GetAllProducts();
    }

}
توضیحات:
- جهت دسترسی به سرویس لیست محصولات، ابتدا سرویس IProductStore به این صفحه تزریق شده‌است.
- سپس در روال رویدادگردان آغازین OnInitializedAsync، کار دریافت اطلاعات و انتساب آن به لیستی، صورت گرفته‌است.
- در این متد جهت شبیه سازی یک عملیات async از یک Task.Delay استفاده شده‌است.
- چون این صفحه، یک صفحه‌ی SSR عادی است، بدون تعریف ویژگی StreamRendering در آن، پس از اجرای برنامه، هیچگاه قسمت loading که در حالت products == null_ قرار است ظاهر شود، نمایش داده نمی‌شود؛ چون در این حالت (حذف نوع رندر)، صفحه‌ی نهایی که به کاربر ارائه خواهد شد، یک صفحه‌ی استاتیک کاملا رندر شده‌ی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحه‌ی نهایی را دریافت و مشاهده کند. در حالت Streaming rendering، ابتدا می‌توان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آن‌را به محض آماده شدن در طی چند مرحله بازگشت داد.
- لینک‌های نمایش داده شده‌ی در اینجا، به صفحه‌ی ProductDetails اشاره می‌کنند که در آن، جزئیات محصول انتخابی نمایش داده می‌شوند.


تکمیل صفحه‌ی نمایش جزئیات یک محصول


در صفحه‌ی کامپوننت Components\Pages\Store\ProductDetails.razor، کار نمایش جزئیات محصول انتخابی صورت می‌گیرد:

@page "/ProductDetails/{ProductId}"
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services

@inject IProductStore Store

@attribute [StreamRendering]

@if (_product == null)
{
    <p>Loading...</p>
}
else
{
    <div>
        <div>
            <h5>
                @_product.Title (@_product.Price.ToString("C"))
            </h5>
            <p>
                @_product.Description
            </p>
        </div>
        @if (_product.Related.Count > 0)
        {
            <div>
                <RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
            </div>
        }
    </div>
    <NavLink href="/Products">Back</NavLink>
}

@code {
    private Product? _product;

    [Parameter]
    public string? ProductId { get; set; }

    protected override Task OnInitializedAsync() => GetProductAsync();

    private async Task GetProductAsync()
    {
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering
        _product = Store.GetProduct(Convert.ToInt32(ProductId));
    }

}
توضیحات:
- باتوجه به نحوه‌ی تعریف مسیریابی این صفحه، پارامتر ProductId از طریق آدرسی مانند http://localhost:5136/ProductDetails/1 دریافت می‌شود.
- سپس این ProductId را در روال رخ‌دادگردان OnInitializedAsync، برای یافتن جزئیات محصول انتخابی از سرویس تزریقی IProductStore، بکار می‌گیریم.
- در اینجا نیز از Task.Delay برای شبیه سازی یک عملیات طولانی async مانند دریافت اطلاعات از یک بانک اطلاعاتی، کمک گرفته شده‌است.
- همچنین برای نمایش قسمت loading صفحه در حالت SSR، بازهم از StreamRendering استفاده کرده‌ایم.
- اگر دقت کرده باشید، ذیل تصویر اطلاعات محصول، دکمه‌ای نیز جهت بارگذاری اطلاعات محصولات مشابه، قرار دارد که ProductId محصول انتخابی را دریافت می‌کند:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
بنابراین در ادامه کامپوننت RelatedProducts فوق را تکمیل می‌کنیم.


تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط

در فایل Components\Pages\Store\RelatedProducts.razor، کار نمایش یک دکمه و سپس نمایش لیستی از محصولات مشابه، صورت می‌گیرد:
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services
@inject IProductStore Store

<button @onclick="LoadRelatedProducts">Related products</button>

@if (_loadRelatedProducts)
{
    @if (_relatedProducts == null)
    {
        <p>Loading...</p>
    }
    else
    {
        <div>
            @foreach (var item in _relatedProducts)
            {
                <a href="/ProductDetails/@item.Id">
                    <div>
                        <h5>@item.Title (@item.Price.ToString("C"))</h5>
                    </div>
                </a>
            }
        </div>
    }
}

@code{

    private IList<Product>? _relatedProducts;
    private bool _loadRelatedProducts;

    [Parameter]
    public int ProductId { get; set; }

    private async Task LoadRelatedProducts()
    {
        _loadRelatedProducts = true;
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate InteractiveServer mode
        _relatedProducts = Store.GetRelatedProducts(ProductId);
    }

}

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

مشکل! اگر در این حالت برنامه را اجرا کرده و بر روی دکمه‌ی related products کلیک کنیم، هیچ اتفاقی رخ نمی‌دهد! یعنی روال رویدادگران LoadRelatedProducts اصلا اجرا نمی‌شود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیت‌های تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند.
محدودیتی که به همراه صفحات SSR وجود دارد این است: این نوع کامپوننت‌ها و صفحات فقط یکبار رندر می‌شوند و نه بیشتر. بله می‌توان بر روی آن‌ها ده‌ها دکمه، نوارهای لغزان، دراپ‌داون و غیره را قرار داد، اما ... نمی‌توان هیچگونه تعاملی را با آن‌ها داشت. کامپوننت نهایی رندر شده و نمایش داده شده، دیگر در هیچ‌جائی اجرا نمی‌شود. در این حالت است که می‌توان تصمیم گرفت که نیاز است قسمتی از این صفحه، تعاملی شود.
به همین جهت باید نحوه‌ی رندر کامپوننت RelatedProducts را به صورت یک جزیره‌ی تعاملی Blazor server درآورد تا رویداد منتسب به دکمه‌ی related products موجود در آن، پردازش شود. بنابراین به صفحه‌ی ProductDetails.razor مراجعه کرده و rendermode@ این کامپوننت را به صورت زیر به حالت InteractiveServer تغییر می‌دهیم:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@InteractiveServer"/>
اکنون اگر برنامه را مجددا اجرا کرده و بر روی دکمه‌ی نمایش محصولات مشابه قرار گرفته در ذیل جزئیات یک محصول کلیک کنیم، بدون مشکل کار می‌کند:


نحوه‌ی پردازش پشت صحنه‌ی این نوع صفحات هم جالب است. برای اینکار به برگه‌ی network مخصوص developer tools مرورگر مراجعه کرده و مراحل رسیدن به صفحه‌ی نمایش جزئیات محصول را طی می‌کنیم:


- اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمی‌شود (و یا حتی برنامه‌های MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت می‌گیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم به‌روز رسانی نمی‌کند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.
این موارد توسط فایل blazor.web.js درج شده‌ی در کامپوننت آغازین App.razor، به صورت خودکار مدیریت می‌شوند:
<script src="_framework/blazor.web.js"></script>

به علاوه در این حالت ای‌جکسی fetch، کار دریافت مجدد فایل‌های استاتیک مرتبط یک صفحه، مانند فایل‌های js.، css.، تصاویر و غیره، مجددا انجام نمی‌شود که این مورد خود مزیتی است نسبت به حالت متداول برنامه‌های ASP.NET Core MVC و یا Razor pages. در حالت Blazor 8x SSR، فقط یک partial update از نوع Ajax ای انجام می‌شود.
به این قابلیت، enhanced navigation هم گفته می‌شود. برای مثال زمانیکه یک فرم SSR را در Blazor 8x به سمت سرور ارسال می‌کنیم، موقعیت scroll به صورت خودکار ذخیره و بازیابی می‌شود تا کاربر با یک full post back مواجه نشده و موقعیت جاری خود را در صفحه از دست ندهد (چنین ایده‌ای، یک زمانی در برنامه‌های ASP.NET Web forms هم برقرار بود و هست! به نظر مایکروسافت هنوز دلتنگ طراحی قدیمی ASP.NET Web forms است!).

- همچنین به محض نمایش صفحه‌ی جزئیات محصول، پس از پایان کار نمایش آن، یک اتصال وب‌سوکت هم برقرار شده که مرتبط با جزیره‌ی تعاملی Blazor server تعریف شده، یا همان کامپوننت RelatedProducts است.

- یک disconnect را هم در اینجا مشاهده می‌کنید. اگر به یک صفحه‌ی تعاملی مراجعه کنیم، همانطور که مشخص است، یک اتصال SignalR برقرار می‌شود (که به آن در اینجا circuit هم می‌گویند). اما اگر از این صفحه به سمت یک صفحه‌ی SSR حرکت کنیم، پس از نمایش آن صفحه، اتصال SignalR قبلی که دیگر نیازی به آن نیست، بسته خواهد شد تا منابع سمت سرور، رها شوند.


در حین disconnect، شماره ID اتصال SignalR ای که دیگر به آن نیازی نیست، به برنامه ارسال می‌شود تا به صورت خودکار در سمت سرور بسته شود. تمام این موارد توسط blazor.web.js فریم‌ورک، مدیریت می‌شوند.
در این تصویر ابتدا به آدرس http://localhost:5136/ProductDetails/1 مراجعه کرده‌ایم که سبب برقراری اتصال یک وب‌سوکت شده‌است. سپس با کلیک بر روی دکمه‌ی back، به صفحه‌ی SSR مشاهده‌ی لیست محصولات برگشته‌ایم. در این حالت، دستور قطع اتصال SignalR قبلی صادر شده‌است.


نحوه‌ی مدیریت Pre-rendering در جزایر تعاملی Blazor 8x

به صورت پیش‌فرض زمانیکه از حالت رندر InteractiveServer استفاده می‌کنیم، قابلیت pre-rendering آن نیز فعال است. یعنی ابتدا حداقل قالب و قسمت‌های ثابت کامپوننت، در سمت سرور پردازش و رندر شده و سپس به سمت کلاینت ارسال می‌شوند. در این حالت کاربر، تجربه‌ی کاربری روان‌تری را شاهد خواهد بود؛ چون برای مدتی نباید منتظر آماده شدن کل UI مرتبط باشد و حداقل، قسمت‌هایی از صفحه که تعاملی نیستند، قابل دسترسی و مشاهده هستند.
اگر به هر دلیلی نیاز به غیرفعال کردن این قابلیت را دارید، باید به صورت زیر عمل کرد:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@(new InteractiveServerRenderMode(false))"/>
در این حالت اگر برنامه را اجرا کنید، در حین نمایش صفحه‌ی اصلی در برگیرنده‌ی از نوع SSR، فقط جای این کامپوننت در صفحه مشخص می‌شود و پس از برقراری اتصال با سرور از طریق اتصال SignalR، شاهد UI کامپوننت RelatedProducts خواهیم بود، که نسبت به قبل، وقفه‌ای را سبب خواهد شد.

نحوه‌ی تعریف خواص استاتیک InteractiveServer بکار گرفته شده و یا کلاس InteractiveServerRenderMode را در ادامه مشاهده می‌کنید. جهت سهولت تعریف این موارد، سطر زیر که یک using static است، به فایل Imports.razor_ اضافه شده‌است:
@using static Microsoft.AspNetCore.Components.Web.RenderMode

public static class RenderMode
  {
    public static InteractiveServerRenderMode InteractiveServer { get; } = new InteractiveServerRenderMode();

    public static InteractiveWebAssemblyRenderMode InteractiveWebAssembly { get; } = new InteractiveWebAssemblyRenderMode();

    public static InteractiveAutoRenderMode InteractiveAuto { get; } = new InteractiveAutoRenderMode();
  }


public class InteractiveServerRenderMode : IComponentRenderMode
  {
    public InteractiveServerRenderMode()
      : this(true)
    {
    }

    public InteractiveServerRenderMode(bool prerender) => this.Prerender = prerender;

    public bool Prerender { get; }
  }


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-Server-Normal.zip  
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت دوم - بررسی توابع Redux
همانطور که در مقدمه‌ی قسمت قبل نیز عنوان شد، در این سری ابتدا کتابخانه‌ی Redux را به صورت مجزایی از React بررسی می‌کنیم؛ چون در اصل، یک کتابخانه‌ی مدیریت حالت عمومی است و وابستگی خاصی را به React ندارد و در بسیاری از برنامه و فریم‌ورک‌های دیگر نیز قابل استفاده‌است.


ایجاد یک برنامه‌ی خالی React برای آزمایش توابع Redux

در اینجا برای بررسی توابع Redux، یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> npm i -g create-react-app
> create-react-app state-management-redux-mobx
> cd state-management-redux-mobx
> npm start
در ادامه کتابخانه‌ی Redux را نیز نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save redux
البته برای کار با Redux، الزاما نیازی به طی مراحل فوق نیست؛ ولی چون این قالب، یک محیط آماده‌ی کار با ES6 را فراهم می‌کند، به سادگی می‌توان فایل index.js آن‌را خالی کرد و سپس شروع به کدنویسی و آزمایش Redux نمود.


معرفی سریع توابع Redux

Redux، کتابخانه‌ی کوچکی است و تنها از 5 تابع تشکیل شده‌است:
applyMiddleware: function()
bindActionCreators: function()
combineReducers: function()
compose: function()
createStore: function()
و سه مورد از آن‌ها بیشتر کمکی هستند. برای مثال تابع compose مانند متد flow و یا pipe در کتابخانه‌ی lodash است و اصلا به Redux مرتبط نیست. تابع combineReducers، اشیاء موجود در state را با هم ترکیب می‌کند. bindActionCreators نیز یک تابع کمکی است جهت ایجاد ساده‌تر ActionCreators. بنابراین کتابخانه‌ی Redux، آنچنان گسترده نیست.


بررسی تابع compose با یک مثال

پس از ایجاد پروژه‌ی React و افزودن کتابخانه‌ی Redux به آن، به فایل src\index.js این پروژه مراجعه کرده و محتویات آن‌را با قطعه کد ذیل، تعویض می‌کنیم:
import { compose } from "redux";

const makeLouder = text => text.toUpperCase();
const repeatThreeTimes = text => text.repeat(3);
const embolden = text => text.bold();

const makeLouderAndRepeatThreeTimesAndEmbolden = compose(
  embolden,
  repeatThreeTimes,
  makeLouder
);
console.log(makeLouderAndRepeatThreeTimesAndEmbolden("Hello"));
- در ابتدای این ماژول، تابع compose را از کتابخانه‌ی redux دریافت کرده‌ایم.
- سپس سه تابع ساده را برای ضخیم کردن، تکرار و با حروف بزرگ نمایش دادن یک متن ورودی، تعریف کرده‌ایم.
- اکنون با استفاده از متد compose کتابخانه‌ی redux، این سه متد را به صورت ترکیبی، بر روی متن ورودی Hello، اعمال کرده‌ایم.
- در آخر اگر برای مشاهده‌ی نتیجه‌ی اجرای console.log بر روی آن، به کنسول توسعه دهندگان مرورگر مراجعه کنیم، به خروجی زیر خواهیم رسید:
 <b>HELLOHELLOHELLO</b>
همانطور که مشاهده می‌کنید، متن Hello را سه بار با حروف بزرگ تکرار نموده و در نهایت آن‌را با تک b محصور کرده‌است.


بررسی تابع createStore با یک مثال

Store در Redux، جائی است که در آن state برنامه ذخیره می‌شود. تابع createStore که کار ایجاد store را انجام می‌دهد، یک پارامتر را دریافت می‌کند و آن‌هم تابع reducer است که در قسمت قبل معرفی شد و در ساده‌ترین حالت آن، به نحو زیر با یک متد خالی، قابل فراخوانی است:
import { createStore } from "redux";

createStore(() => {});
تابع reducer دو پارامتر را دریافت می‌کند: وضعیت فعلی برنامه (state در اینجا) و رخ‌دادی که واقع شده‌است (action در اینجا). خروجی آن نیز یک state جدید بر این اساس است:
import { createStore } from "redux";

const reducer = (state, action) => {
  return state;
};
const store = createStore(reducer);
console.log(store);
در این مثال، تابع reducer را طوری تعریف کرده‌ایم که بر اساس هر نوع رخ‌دادی که به آن برسد، همان وضعیت قبلی را بازگشت دهد. سپس بر اساس آن یک store را ایجاد کرده‌ایم. اگر به خروجی console.log بررسی محتوای این شیء دقت کنیم، به صورت زیر خواهد بود:


در شیء store، چهار متد dispatch, subscribe, getState, replaceReducer مشخص هستند:
- کارکرد متد replaceReducer مشخص است؛ یک reducer جدید را به آن می‌دهیم و reducer قبلی را جایگزین می‌کند.
- متد dispatch آن مرتبط است به مبحث dispatch actions که در قسمت قبل به آن پرداختیم. هدف آن تغییر state، بر اساس یک action رسیده‌است.
- متد getState، وضعیت فعلی state را باز می‌گرداند.
- متد subscribe با هر تغییری در state، سبب بروز رخ‌دادی می‌شود. یکی از کاربردهای آن می‌تواند به روز رسانی UI، بر اساس تغییرات state باشد. برای مثال کتابخانه‌ی دیگری به نام react redux، دقیقا همین کار را به کمک متد subscribe، انجام می‌دهد. در این قسمت، هدف ما بررسی پشت صحنه‌ی کتابخانه‌هایی مانند react redux است که چه متدهایی را محصور کرده‌اند و دقیقا چگونه کار می‌کنند.

در مثال زیر، مقدار ابتدایی پارامتر state متد reducer را به یک شیء که دارای خاصیت value و مقدار 1 است، تنظیم کرده‌ایم:
import { createStore } from "redux";

const reducer = (state = { value: 1 }, action) => {
  return state;
};
const store = createStore(reducer);
console.log(store.getState());
سپس بر این اساس، store را ایجاد کرده و متد getState آن‌را فراخوانی کرده‌ایم. خروجی آن به صورت زیر است، که معادل وضعیت فعلی state در store می‌باشد:


در کامپوننت‌های React، این نوع خواص به صورت props ارسال می‌شوند. کل state در Redux ذخیره شده و سپس قابل دسترسی و خواندن خواهد شد.


بررسی متد dispatch یک store با مثال

در اینجا مثالی را از کاربرد متد dispatch ملاحظه می‌کنید که یک شیء را می‌پذیرد:
import { compose, createStore } from "redux";

const reducer = (state = { value: 1 }, action) => {
  console.log("Something happened!", action);
  return state;
};
const store = createStore(reducer);
console.log(store.getState());
store.dispatch({ type: "ADD" });
متد reducer با یک state ابتدایی تنظیم شده‌ی به شیء { value: 1 } تعریف شده و سپس با ارسال آن به createStore و ایجاد store، اکنون می‌توانیم رخ‌دادهایی را به آن dispatch کنیم.
فرمت شیءای که به متد dispatch ارسال می‌شود، حتما باید به همراه خاصیت type باشد؛ در غیر اینصورت با صدور استثنائی، این نکته را گوشزد می‌کند. مقدار آن نیز یک رشته‌است که بیانگر وقوع رخدادی در برنامه می‌باشد؛ برای مثال کاربر درخواست دریافت اطلاعاتی را کرده‌است و یا کاربر از سیستم خارج شده‌است و امثال آن.

خروجی قطعه کد فوق، به صورت زیر است:


با اجرای برنامه، یک رخ‌داد درونی از نوع redux/INITo.2.g.b.y.i@@ صادر شده‌است که هدف آن آغاز و مقدار دهی اولیه‌ی store است. پس از آن زمانیکه متد store.dispatch فراخوانی شده‌است، مجددا console.log داخل متد reducer فراخوانی شده‌است که اینبار به همراه type ای است که ما مشخص کرده‌ایم.

در حالت کلی، اینکه شیء ارسالی توسط dispatch را چگونه طراحی می‌کنید، اختیاری است؛ اما الگوی پیشنهادی در این زمینه، redux standard actions نام دارد و به صورت زیر است که هدف از آن، یک‌دست شدن طراحی و پیاده سازی برنامه است:
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
- نوع action باید همواره قید شود و عموما رشته‌ای است.
- سپس به خاصیت payload، تمام داده‌های مرتبط با آن اکشن، مانند نتیجه‌ی بازگشتی از یک API، به صورت یک شیء، انتساب داده می‌شود.
- خاصیت meta، مرتبط با متادیتا و اطلاعات اضافی است که عموما استفاده نمی‌شود.

اکنون که طراحی شیء ارسالی به پارامتر action متد reducer مشخص شد، می‌توان بر اساس خاصیت type آن، یک switch را طراحی کرد و نسبت به نوع‌های مختلف رسیده، واکنش نشان داد:
import { createStore } from "redux";

const reducer = (state = { value: 1 }, action) => {
  console.log("Something happened!", action);

  if (action.type === "ADD") {
    const value = state.value;
    const amount = action.payload.amount;
    state.value = value + amount;
  }

  return state;
};
const store = createStore(reducer);
const firstState = store.getState();
console.log(firstState);
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
const secondState = store.getState();
console.log(secondState);
console.log("secondState === firstState", secondState === firstState);
در اینجا اکشن از نوع ADD، با یک payload با محتوای شیءای که دارای خاصیت amount با مقدار 2 است، به متد reducer مربوط به store، ارسال می‌شود. سپس در متد reducer، بر اساس مقدار action.type، تصمیم‌گیری خواهد شد. اگر این مقدار مساوی اکشن خاص ADD بود، آنگاه مقدار value شیء state را با مقدار amount جمع زده و به state.value انتساب می‌دهیم. اکنون پس از فراخوانی متد store.dispatch، اگر خروجی متد ()store.getState را بررسی کنیم، به صورت {value: 3} در آمده‌است ... و این کاری است که نباید انجام شود! نباید مقدار شیء state را به صورت مستقیم تغییر داد. چون در آخرین بررسی صورت گرفته که کار مقایسه‌ی بین state پیش و پس از فراخوانی store.dispatch است (secondState === firstState)، حاصل true است. یعنی اگر با این روش با React کار کنیم، نمی‌تواند محاسبه کند که چه چیزی در state تغییر کرده‌است، تا بر اساس آن UI را به روز رسانی کند.

یک روش حل این مشکل، حذف سطر state.value = value + amount و جایگزینی آن با یک return است که شیء state جدیدی را بازگشت می‌دهد:
return {
  value: value + amount
};
اکنون نتیجه‌ی مقایسه‌ی secondState === firstState دیگر true نخواهد بود.


بررسی متد subscribe یک store با مثال

در ادامه به store همان مثال فوق، متد subscribe را نیز اضافه می‌کنیم و سپس دوبار، کار store.dispatch را انجام خواهیم داد:
const store = createStore(reducer);

const unsubscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });

unsubscribe();
- هر بار که متد store.dispatch فراخوانی می‌شود، یکبار callback function ارسالی به متد store.subscribe، فراخوانی خواهد شد که در اینجا کار نمایش مقدار شیء state را به عهده دارد. در یک چنین حالتی مشترکین به store، متوجه خواهند شد که احتمالا state جدیدی توسط متد reducer بازگشت داده شده‌است و سپس بر اساس آن برای مثال تصمیم خواهند گرفت تا UI را به روز رسانی کنند.
- خروجی مستقیم متد store.subscribe نیز یک متد است که unsubscribe نام دارد و می‌توان در پایان کار، برای جلوگیری از نشتی‌های حافظه، آن‌را فراخوانی کرد.

خروجی حاصل از console.log منتسب به متد store.subscribe، با دوبار فراخوانی متد store.dispatch پس از آن، به صورت زیر است:
{value: 3}
{value: 5}


بررسی تابع combineReducers با یک مثال

اگر قرار باشد در متد reducer، صدها if action.type را تعریف کرد ... پس از مدتی از کنترل خارج می‌شود و غیرقابل نگهداری خواهد شد. تابع combineReducers به همین جهت طراحی شده‌است تا چندین متد مجزای reducer را با هم ترکیب کند. بنابراین می‌توان در برنامه صدها متد کوچک reducer را که قابلیت توسعه و نگهداری بالایی را دارند، پیاده سازی کرد و سپس با استفاده از تابع combineReducers، آن‌ها را یکی کرده و به متد createStore ارسال کرد. نکته‌ی مهم اینجا است که هرچند اینبار می‌توان تعداد زیادی reducer را طراحی کرد، اما توسط تابع combineReducers، مقدار action ارسالی، از تمام این reducerها عبور داده خواهد شد. به این ترتیب اگر نیاز بود می‌توان به یک action، در چندین متد مختلف reducer گوش فرا داد و بر اساس آن تصمیم گیری کرد. بنابراین بهتر است هر reducer تعریف شده در صورتیکه action.type آن با action رسیده تطابق نداشته باشد، همان state قبلی را بازگشت دهد تا بتوان زنجیره‌ی reducerها را تشکیل داد و بهتر مدیریت کرد.
برای نمونه اگر متد reducer فعلی را به calculatorReducer تغییر نام دهیم، روش معرفی آن توسط متد combineReducers به متد createStore به صورت زیر است:
import { combineReducers, createStore } from "redux";
  // ...

const calculatorReducer = (state = { value: 1 }, action) => {
  // ...
};
const store = createStore(
  combineReducers({
    calculator: calculatorReducer
  })
);
به شیء ارسالی به متد combineReducers، هر reducer موجود به صورت یک خاصیت جدید اضافه می‌شود.


بررسی تابع bindActionCreators با یک مثال

فرض کنید می‌خواهیم متد dispatch را چندین بار فراخوانی کنیم:
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
بجای اینکه شیء ارسالی به آن‌را به این صورت ایجاد کنیم، می‌توان یک تابع را برای آن ایجاد کرد تا مقدار amount را گرفته و شیء action مدنظر را بازگشت دهد:
const createAddAction = amount => {
  return {
    type: "ADD",
    payload: {
      amount // = amount: amount
    },
    meta: {}
  };
};
و سپس می‌توان آن‌را به صورت زیر مورد استفاده قرار داد:
store.dispatch(createAddAction(2));
store.dispatch(createAddAction(2));
و یا توسط تابع bindActionCreators، می‌توان فراخوانی فوق را به صورت زیر نیز انجام داد و نتیجه یکی است:
import { bindActionCreators, combineReducers, compose, createStore } from "redux";

// ...
const dispatchAdd = bindActionCreators(createAddAction, store.dispatch);
dispatchAdd(2);
dispatchAdd(2);
همانطور که ملاحظه می‌کنید، bindActionCreators فقط یک تابع کمکی است تا کار dispatch action را ساده کند.


میان‌افزارها (Middlewares) در Redux

پس از اینکه یک اکشن، به سمت reducer ارسال شد و پیش از رسیدن آن به reducer، می‌توان کدهای دیگری را نیز اجرا کرد. برای مثال چون این توابع خالص هستند، نمی‌توان اعمالی را داخل آن‌ها اجرا کرد که به همراه اثرات جانبی مانند کار با یک API خارجی باشند. با استفاده از میان‌افزارها در این بین می‌توان کدهایی را که با دنیای خارج تعامل دارند نیز اجرا کرد.
یک میان‌افزار در Redux، همانند سایر قسمت‌های آن، تنها یک تابع ساده‌ی جاوا اسکریپتی است:
const logger = ({ getState }) => {
  return next => action => {
    console.log(
      'MIDDLEWARE',
      getState(),
      action
    );
    const value = next(action);
    console.log({value});
    return value;
  }
}
این تابع میان‌افزار، امکان دریافت وضعیت فعلی store را دارد و در نهایت یک تابع دیگر را باز می‌گرداند. داخل این تابع بازگشت داده شده می‌توان اعمال دارای اثرات جانبی را نیز اجرا کرد؛ مانند فراخوانی console.log و یا صدور یک درخواست Ajax ای. یکی دیگر از کاربردهای میان‌افزارها، انجام کارهای آماری بر روی اکشن‌های ارسالی است. بجای اینکه کدهای متناظر را داخل  reducerها قرار داد، می‌توان به رسیدن اکشن‌های خاصی در این بین گوش فرا داد و برای مثال آن‌ها را لاگ کرد و یا آماری را از آن‌ها تهیه نمود.
در پایان کار میان‌افزار باید متد next آن‌را فراخوانی کرد تا بتوان میان‌افزارهای متعددی را زنجیروار اجرا کرد.
در آخر برای معرفی آن به یک store می‌توان از تابع applyMiddleware استفاده کرد:
import { applyMiddleware, bindActionCreators, combineReducers, compose, createStore } from "redux";

// ...

const store = createStore(
  combineReducers({
    calculator: calculatorReducer
  }),
  applyMiddleware(logger)
);

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: state-management-redux-mobx-part02.zip
مطالب
ارسال چند درخواست به صورت همزمان به ASP.NET Web API 2.x در AngularJS 1.x

 ما در AngularJs آبجکتی را به نام  q$ داریم که برای اجرای توابع به صورت async مفید است و همچنین در استفاده از مقادیر برگشتی از این درخواست‌ها برای پردازش‌های آینده به ما کمک میکند. برای اطلاعات بیشتر در مورد این سرویس به اینجا مراجعه کنید.

در ادامه ما از تابع ()all از q$ برای ترکیب چند شیء promise داخل یک شیء promise، به منظور صدا زدن چند سرویس به صورت یکجا، استفاده میکنیم.


پیاده سازی ASP.NET Web API


قدم اول : Visual Studio  را بازکنید و یک پروژه empty ASP.NET Web API را مطابق شکل زیر ایجاد کنید.


قدم دوم : در داخل پوشه models، کلاسی را با کدهای زیر ایجاد کنید:
using System.Collections.Generic;
 
namespace NG_Combine_Multiple_Promises.Models
{
    public class Courses
    {
        public int CourseId { get; set; }
        public string CourseName { get; set; }
    }
    public class CourseDatabase : List<Courses>
    {
        public CourseDatabase()
        {
            Add(new Courses() { CourseId=1,CourseName="الکترونیک"});
            Add(new Courses() { CourseId = 2, CourseName = "ریاضی 2" });
            Add(new Courses() { CourseId = 3, CourseName = "طراحی نرم افزار" });
        }
    }
 
    public class Student
    {
        public int StudentId { get; set; }
        public string Name { get; set; }
        public string AcadmicYear { get; set; }
    }
 
    public class StudentDatabase : List<Student>
    {
        public StudentDatabase()
        {
            Add(new Student() {StudentId=101,Name="محمد علوی", 
                               AcadmicYear="اول" });
            Add(new Student() { StudentId = 102, Name = "طاهره موسوی", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 103, Name = "علی عباسی", 
                               AcadmicYear = "سوم" });
            Add(new Student() { StudentId = 104, Name = "جواد نوری", 
                               AcadmicYear = "اول" });
            Add(new Student() { StudentId = 105, Name = "محسن خدایی", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 106, Name = "علی کاظمی", 
                               AcadmicYear = "سوم" });
            Add(new Student() { StudentId = 107, Name = "زهرا مقدم", 
                               AcadmicYear = "اول" });
            Add(new Student() { StudentId = 108, Name = "لاله فکور", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 109, Name = "علی نوروزی", 
                               AcadmicYear = "چهارم" });
        }
    }
     
}

کد بالا شامل موجودیت‌های Courses و Student است و کلاس‌های CourseDatabase و StudentDatabase به ترتیب برای ذخیره این موجودیت‌ها است.


قدم سوم :  بسته‌ی نیوگت Microsoft.AspNet.WebAPi.Cors را با استفاده از NuGet Package Manager، به منظور فعال سازی امکان صدا زدن این وب سرویس از دامنه‌های مختلف، به پروژه اضافه کرده و کد زیر را در کلاس WebApiCofig در پوشه App_Start قرار دهید.

config.EnableCors();


قدم چهارم : دو کنترل از نوع Web API 2 Empty را با نام‌های CourseAPIController و StudentAPIController ایجاد کرده و کدهای زیر را در آنها قرار دهید.

CourseAPIController.cs 

[EnableCors("*","*","*")]
public class CourseAPIController : ApiController
{
    [Route("Courses")]
    public IEnumerable<Courses> Get()
    {
        return new CourseDatabase();
    }
}

StudentAPIController.cs 

[EnableCors("*", "*", "*")]
public class StudentAPIController : ApiController
{
    [Route("Students")]
    public IEnumerable<Student> Get()
    {
        return new StudentDatabase();
    }
}


استفاده از Angular $q.all  

قدم اول : پروژه‌ی جدیدی را از نوع Empty ASP.NET در همین solution اضافه کرده و ارجاعات jQuery, Bootstrap و AngularJS را با استفاده از NuGet Package manager  مانند زیر اضافه کنید:

Install-Package jQuery
Install-Package bootstrap
Install-Package angularjs


قدم دوم : پوشه‌ایی را به نام MyScripts ایجاد کرده و درون آن فایل javascript  زیر را با نام logic.js اضافه کنید: 

var app = angular.module('mymodule', []);

//سرویسی برای بازگرداندن لیست دروس
app.service('courseService', function ($http) {
    this.get = function () {
        var response = $http.get("http://localhost:11696/Courses");
        return response;
    };
});

//سرویسی برای بازگرداندن لیست دانشجویان
app.service('studentService', function ($http) {
    this.get = function () {
        var response = $http.get("http://localhost:11696/Students");
        return response;
    };
});

//تعریف کنترلر
app.controller('ctrl', function ($scope, $q, courseService, studentService) {

    $scope.Courses = [];
    $scope.Students = [];
    loadData();

    /**/
    function loadData() {
        var promiseCourse = courseService.get();
        var promiseStudent = studentService.get();

        $scope.combineResult = $q.all([
            promiseCourse,
            promiseStudent
        ]).then(function (resp) {
            $scope.Courses = resp[0].data;
            $scope.Students = resp[1].data;
        });
    }
});
توضیحات: تابعq$.all لیستی از promise هایی را که  توسط courseService.get و studentService.get بدست آمده‌اند، گرفته و وقتی تمام promise‌ها تمام شدند، قسمت ()then آن اجرا میشود. 

قدم سوم:  فایل html ایی را با نام DataPage در پوشه‌ی view با کد زیر ایجاد کنید: 
<!DOCTYPE html>
<html ng-app="mymodule">
<head>
    <title></title>
    <meta charset="utf-8" />
    <link href="../Content/bootstrap.min.css" rel="stylesheet" />
    <script src="../Scripts/jquery-3.2.1.min.js"></script>
    <script src="../Scripts/angular.min.js"></script>
    <script src="../MyScripts/logic.js"></script>
</head>
<body ng-controller="ctrl">
       <div class="container">
        <h1 class="h1">دروس</h1>
        <table class="table table-striped table-bordered table-condensed">
            <thead>
                <tr>
                    <td class="text-center">کد درس</td>
                    <td class="text-center">نام درس</td>
                </tr>
            </thead>
            <tbody>
                <tr ng-repeat="Course in Courses">
                    <td class="text-center">{{Course.CourseId}}</td>
                    <td class="text-center">{{Course.CourseName}}</td>
                </tr>
            </tbody>
        </table>
        <hr />
        <h1 class="h1">دانشجویان</h1>

        <table class="table table-striped table-bordered table-condensed">
            <thead>
                <tr>
                    <td class="text-center">کد دانشجویی</td>
                    <td class="text-center">نام و نام خانوادگی</td>
                    <td class="text-center">سال دانشجویی</td>
                </tr>
            </thead>
            <tbody>
                <tr ng-repeat="Student in Students">
                    <td class="text-center">{{Student.StudentId}}</td>
                    <td class="text-center">{{Student.Name}}</td>
                    <td class="text-center">{{Student.AcadmicYear}}</td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

خروجی نهایی پروژه به شکل زیر خواهد بود:

مطالب
پیاده سازی Remote Validation در Blazor
فرم‌های Blazor به همراه پشتیبانی از ویژگی Remote که به همراه ASP.NET Core ارائه می‌شود، نیستند. هرچند می‌توان در حین ارسال فرم به سرور، نتیجه‌ی اعتبارسنجی از راه دور و سمت سرور را به کاربر نمایش داد، اما تجربه‌ی کاربری آن در حد Remote validation نیست. یعنی می‌خواهیم در حین ورود اطلاعات و یا انتقال focus به کنترل دیگری، اعتبارسنجی سمت سرور صورت گیرد و نه فقط در زمان ارسال کل اطلاعات به سرور، در پایان کار. در این مطلب روشی را جهت پیاده سازی این عملیات بررسی خواهیم کرد.


ایجاد یک پروژه‌ی ابتدایی Blazor WASM

پروژه‌ای را که در این مطلب تکمیل خواهیم کرد، از نوع Blazor WASM هاست شده‌است. بنابراین در پوشه‌ی فرضی BlazorAsyncValidation، دستور «dotnet new blazorwasm --hosted» را صادر می‌کنیم تا ساختار ابتدایی پروژه که به همراه یک کلاینت Blazor WASM و یک سرور ASP.NET Core Web API است، تشکیل شود. از قسمت Web API، برای پیاده سازی اعتبارسنجی سمت سرور استفاده خواهیم کرد.


مدل ثبت نام برنامه

مدل ثبت نام برنامه تنها از یک خاصیت نام تشکیل شده و در پروژه‌ی Shared قرار می‌گیرد تا هم در کلاینت و هم در سرور قابل استفاده باشد:
using System.ComponentModel.DataAnnotations;

namespace BlazorAsyncValidation.Shared;

public class UserDto
{
    [Required] public string Name { set; get; } = default!;
}
این نام است که می‌خواهیم عدم تکراری بودن آن‌را حین ثبت نام در سمت سرور، بررسی کنیم. به همین جهت کنترلر API زیر را برای آن تدارک خواهیم دید.


کنترلر API ثبت نام برنامه

کنترلر زیر که در پوشه‌ی BlazorAsyncValidation\Server\Controllers قرار می‌گیرد، منطق بررسی تکراری نبودن نام دریافتی از برنامه‌ی کلاینت را شبیه به منطق remote validation استاندارد MVC، پیاده سازی می‌کند که در نهایت یک true و یا false را باز می‌گرداند.
در اینجا خروجی بازگشت داده کاملا در اختیار شما است و نیازی نیست تا حتما ارتباطی با MVC داشته باشد؛ چون مدیریت سمت کلاینت بررسی آن‌را خودمان انجام خواهیم داد و نه یک کتابخانه‌ی از پیش نوشته شده و مشخص.
using BlazorAsyncValidation.Shared;
using Microsoft.AspNetCore.Mvc;

namespace BlazorAsyncValidation.Server.Controllers;

[ApiController, Route("api/[controller]/[action]")]
public class RegisterController : ControllerBase
{
    [HttpPost]
    public IActionResult IsUserNameUnique([FromBody] UserDto userModel)
    {
        if (string.Equals(userModel?.Name, "Vahid", StringComparison.OrdinalIgnoreCase))
        {
            return Ok(false);
        }

        return Ok(true);
    }
}

غنی سازی فرم استاندارد Blazor جهت انجام Remote validation



اگر بخواهیم از EditForm استاندارد Blazor در حالت متداول آن و بدون هیچ تغییری استفاده کنیم، مانند مثال زیر که InputText متصل به خاصیت Name مربوط به Dto برنامه را نمایش می‌دهد:
@page "/"

<PageTitle>Index</PageTitle>

<h2>Register</h2>

<EditForm EditContext="@EditContext" OnValidSubmit="DoSubmitAsync">
    <DataAnnotationsValidator/>
    <div class="row mb-2">
        <label class="col-form-label col-lg-2">Name:</label>
        <div class="col-lg-10">
            <InputText @bind-Value="Model.Name" class="form-control"/>
            <ValidationMessage For="@(() => Model.Name)"/>
        </div>
    </div>

    <button class="btn btn-secondary" type="submit">Submit</button>
</EditForm>
 تنها رخ‌دادی که در اختیار ما قرار می‌گیرد، رخ‌داد submit (در حالت موفقیت اعتبارسنجی سمت کلاینت و یا تنها submit معمولی) است. بنابراین برای دسترسی به رخ‌دادهای بیشتر EditForm، نیاز است با EditContext آن کار کنیم:
public partial class Index
{
    private const string UserValidationUrl = "/api/Register/IsUserNameUnique";

    private ValidationMessageStore? _messageStore;
    [Inject] private HttpClient HttpClient { set; get; } = default!;
    private EditContext? EditContext { set; get; }

    private UserDto Model { get; } = new();
به همین جهت EditContext را در سطح کامپوننت جاری تعریف کرده و همچنین سرویس HttpClient را جهت ارسال اطلاعات Name و دریافت پاسخ true/false از سرور و یک ValidationMessageStore را برای نگهداری تعاریف خطاهای سفارشی که قرار است در فرم نمایش داده شوند، معرفی می‌کنیم.
ValidationMessageStore به همراه متد Add است و اگر به آن نام فیلد مدنظر را به همراه پیامی، اضافه کنیم، این اطلاعات را به صورت خطای اعتبارسنجی توسط کامپوننت ValidationMessage نمایش می‌دهد.

محل مقدار دهی اولیه‌ی این اشیاء نیز در روال رویدادگردان OnInitialized به صورت زیر است:
    protected override void OnInitialized()
    {
        EditContext = new EditContext(Model);
        _messageStore = new ValidationMessageStore(EditContext);
        EditContext.OnFieldChanged += (sender, eventArgs) =>
        {
            var fieldIdentifier = eventArgs.FieldIdentifier;
            _messageStore?.Clear(fieldIdentifier);
            _ = InvokeAsync(async () =>
            {
                var errors = await OnValidateFieldAsync(fieldIdentifier.FieldName);
                if (errors?.Any() != true)
                {
                    return;
                }

                foreach (var error in errors)
                {
                    _messageStore?.Add(fieldIdentifier, error);
                }

                EditContext.NotifyValidationStateChanged();
            });
            StateHasChanged();
        };
        EditContext.OnValidationStateChanged += (sender, eventArgs) => StateHasChanged();
        EditContext.OnValidationRequested += (sender, eventArgs) => _messageStore?.Clear();
    }
در اینجا ابتدا یک نمونه‌ی جدید از EditContext، بر اساس Model فرم، تشکیل می‌شود. سپس ValidationMessageStore سفارشی که قرار است خطاهای اعتبارسنجی remote ما را نمایش دهد نیز نمونه سازی می‌شود. در ادامه امکان دسترسی به رخ‌دادهای OnFieldChanged ، OnValidationStateChanged و OnValidationRequested را خواهیم داشت که در حالت معمولی کار با EditForm میسر نیستند.
برای مثال اگر فیلدی تغییر کند، رویداد OnFieldChanged صادر می‌شود. در همینجا است که کار فراخوانی متد OnValidateFieldAsync که در ادامه معرفی می‌شود را انجام می‌دهیم تا کار اعتبارسنجی Async سمت سرور را انجام دهد. اگر نتیجه‌ای به همراه آن بود، توسط messageStore به صورت یک خطای اعتبارسنجی نمایش داده خواهد شد و همچنین EditContext نیز با فراخوانی متد NotifyValidationStateChanged، وادار به به‌روز رسانی وضعیت اعتبارسنجی خود می‌گردد.

متد سفارشی OnValidateFieldAsync که کار اعتبارسنجی سمت سرور را انجام می‌دهد، به صورت زیر تعریف شده‌است:
    private async Task<IList<string>?> OnValidateFieldAsync(string fieldName)
    {
        switch (fieldName)
        {
            case nameof(UserDto.Name):
                var response = await HttpClient.PostAsJsonAsync(
                    UserValidationUrl,
                    new UserDto { Name = Model.Name });
                var responseContent = await response.Content.ReadAsStringAsync();
                if (string.Equals(responseContent, "false", StringComparison.OrdinalIgnoreCase))
                {
                    return new List<string>
                    {
                        $"`{Model.Name}` is in use. Please choose another name."
                    };
                }

                // TIP: It's better to use the `DntDebounceInputText` component for this case to reduce the network round-trips.

                break;
        }

        return null;
    }
در اینجا درخواستی به سمت آدرس api/Register/IsUserNameUnique ارسال شده و سپس بر اساس مقدار true و یا false آن، پیامی را بازگشت می‌دهد. اگر نال را بازگشت دهد یعنی مشکلی نبوده.

یک نکته: InputText استاندارد در حالت معمول آن، پس از تغییر focus به یک کنترل دیگر، سبب بروز رویداد OnFieldChanged می‌شود و نه در حالت فشرده شدن کلیدها. به همین جهت اگر برنامه پیوستی را می‌خواهید آزمایش کنید، نیاز است فقط focus را تغییر دهید و یا یک کنترل سفارشی را برای اینکار توسعه دهید.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorAsyncValidation.zip