مطالب
آپلود فایل‌ها توسط برنامه‌های React به یک سرور ASP.NET Core به همراه نمایش درصد پیشرفت
قصد داریم اطلاعات یک فرم React را به همراه دو فایل الصاقی به آن، به سمت یک سرور ASP.NET Core ارسال کنیم؛ بطوریکه درصد پیشرفت ارسال فایل‌ها، زمان سپری شده، زمان باقی مانده و سرعت آپلود نیز گزارش داده شوند:



پیشنیازها
«بررسی روش آپلود فایل‌ها در ASP.NET Core»
«ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax »

- در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


برپایی پروژه‌های مورد نیاز

ابتدا یک پوشه‌ی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا می‌کنیم:
 dotnet new react
در مورد این قالب که امکان تجربه‌ی توسعه‌ی یکپارچه‌ی ASP.NET Core و React را میسر می‌کند، در مطلب «روش یکی کردن پروژه‌های React و ASP.NET Core» بیشتر بحث کرده‌ایم.
سپس در این پوشه، پوشه‌ی ClientApp پیش‌فرض آن‌را حذف می‌کنیم؛ چون کمی قدیمی است. همچنین فایل‌های کنترلر و سرویس آب و هوای پیش‌فرض آن‌را به همراه پوشه‌ی صفحات Razor آن، حذف و پوشه‌ی خالی wwwroot را نیز به آن اضافه می‌کنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
از تنظیم زیر استفاده کرده‌ایم تا با هر بار تغییری در کدهای پروژه‌ی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
بدیهی است در این حالت باید از طریق خط فرمان به پوشه‌ی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود. البته ما برنامه را به صورت یکپارچه بر روی پورت 5001 وب سرور ASP.NET Core، مرور می‌کنیم.

اکنون در ریشه‌ی پروژه‌ی ASP.NET Core ایجاد شده، دستور زیر را صادر می‌کنیم تا پروژه‌ی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
سپس وارد این پوشه‌ی جدید شده و بسته‌های زیر را نصب می‌کنیم:
> cd clientapp
> npm install --save bootstrap axios react-toastify
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود که برای افزودن فایل bootstrap.css آن به پروژه‌ی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.
- برای نمایش پیام‌های برنامه از کامپوننت react-toastify استفاده می‌کنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آن‌را اضافه کنیم:
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
همچنین نیاز است ToastContainer را به ابتدای متد render آن نیز اضافه کرد:
  render() {
    return (
      <React.Fragment>
        <ToastContainer />
- برای ارسال فایل‌ها به سمت سرور از کتابخانه‌ی معروف axios استفاده خواهیم کرد.


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

پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
import React, { useState } from "react";
import axios from "axios";
import { toast } from "react-toastify";

export default function UploadFileSimple() {

  const [description, setDescription] = useState("");
  const [selectedFile1, setSelectedFile1] = useState();
  const [selectedFile2, setSelectedFile2] = useState();


  return (
    <form>
      <fieldset className="form-group">
        <legend>Support Form</legend>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="description">
            Description
          </label>
          <input
            type="text"
            className="form-control"
            name="description"
            onChange={event => setDescription(event.target.value)}
            value={description}
          />
        </div>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="file1">
            File 1
          </label>
          <input
            type="file"
            className="form-control"
            name="file1"
            onChange={event => setSelectedFile1(event.target.files[0])}
          />
        </div>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="file2">
            File 2
          </label>
          <input
            type="file"
            className="form-control"
            name="file2"
            onChange={event => setSelectedFile2(event.target.files[0])}
          />
        </div>

        <div className="form-group row">
          <button
            className="btn btn-primary"
            type="submit"
          >
            Submit
          </button>
        </div>
      </fieldset>
    </form>
  );
}
کاری که تا این مرحله انجام شده، بازگشت UI فرم برنامه توسط یک functional component است.
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شده‌اند.
- مرحله‌ی بعد، دسترسی به فایل‌های انتخابی کاربر و همچنین مقدار توضیحات وارد شده‌است. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کرده‌ایم:
  const [description, setDescription] = useState("");
  const [selectedFile1, setSelectedFile1] = useState();
  const [selectedFile2, setSelectedFile2] = useState();
با React Hooks، بجای تعریف یک state، به صورت خاصیت، آن‌را صرفا use می‌کنیم و یا همان useState، که یک تابع است و باید در ابتدای کامپوننت، مورد استفاده قرار گیرد. این متد برای شروع به کار، نیاز به یک state آغازین را دارد؛ مانند انتساب یک رشته‌ی خالی به description. سپس اولین خروجی متد useState که داخل یک آرایه مشخص شده‌است، همان متغیر description است که توسط state ردیابی خواهد شد. اینبار بجای متد this.setState قبلی که یک متد عمومی بود، متدی اختصاصی را صرفا جهت تغییر مقدار همین متغیر description به نام setDescription به عنوان دومین خروجی متد useState، تعریف می‌کنیم. بنابراین متد useState، یک initialState را دریافت می‌کند و سپس یک مقدار را به همراه یک متد، جهت تغییر state آن، بازگشت می‌دهد. همین کار را برای دو فیلد دیگر نیز تکرار کرده‌ایم. بنابراین selectedFile1، فایلی است که توسط متد setSelectedFile1 تنظیم خواهد شد و این تنظیم، سبب رندر مجدد UI نیز خواهد گردید.
- پس از طراحی state این فرم، مرحله‌ی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، می‌توان آن‌را به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخ‌داد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
          <input
            type="text"
            className="form-control"
            name="description"
            onChange={event => setDescription(event.target.value)}
            value={description}
          />
در مورد فیلدهای دریافت فایل‌ها، روش انجام اینکار به صورت زیر است:
          <input
            type="file"
            className="form-control"
            name="file1"
            onChange={event => setSelectedFile1(event.target.files[0])}
          />
چون المان‌های دریافت فایل می‌توانند بیش از یک فایل را نیز دریافت کنند (اگر ویژگی multiple، به تعریف تگ آن‌ها اضافه شود)، به همین جهت خاصیت files بر روی آن‌ها قابل دسترسی شده‌است. اما چون در اینجا ویژگی multiple ذکر نشده‌است، بنابراین تنها یک فایل توسط آن‌ها قابل دریافت است و به همین جهت دسترسی به اولین فایل و یا files[0] را در اینجا مشاهده می‌کنید. بنابراین با فراخوانی متد setSelectedFile1، اکنون متغیر حالت selectedFile1، مقدار دهی شده و قابل استفاده است.


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

در فرم‌های معمولی، عموما داده‌ها به صورت یک شیء JSON به سمت سرور ارسال می‌شوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرم‌هایی که به همراه المان‌های دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه می‌شود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا می‌کند:
<form enctype="multipart/form-data" action="/upload" method="post">
   <input id="file-input" type="file" />
</form>
اما اگر قرار باشد همین فرم را توسط جاوا اسکریپت به سمت سرور ارسال کنیم، روش کار به صورت زیر است:
let file = document.getElementById("file-input").files[0];
let formData = new FormData();
 
formData.append("file", file);
fetch('/upload/image', {method: "POST", body: formData});
ابتدا به خاصیت files و اولین فایل آن دسترسی پیدا کرده و سپس شیء استاندارد FormData را بر اساس آن و تمام فیلدهای فرم تشکیل می‌دهیم. FormData ساختاری شبیه به یک دیکشنری را دارد و از کلیدهایی که متناظر با Id المان‌های فرم و مقادیری متناظر با مقادیر آن المان‌ها هستند، تشکیل می‌شود که توسط متد append آن، به این دیکشنری اضافه خواهند شد. در آخر هم شیء formData را به سمت سرور ارسال می‌کنیم.
در یک برنامه‌ی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام داده‌ایم. در مرحله‌ی بعد، شیء FormData را تشکیل خواهیم داد:
  // ...

export default function UploadFileSimple() {
  // ...

  const handleSubmit = async event => {
    event.preventDefault();

    const formData = new FormData();
    formData.append("description", description);
    formData.append("file1", selectedFile1);
    formData.append("file2", selectedFile2);


      toast.success("Form has been submitted successfully!");

      setDescription("");
  };

  return (
    <form onSubmit={handleSubmit}>
    </form>
  );
}
به همین جهت، ابتدا کار مدیریت رخ‌داد onSubmit فرم را انجام داده و توسط آن با استفاده از متد preventDefault، از post-back متداول فرم به سمت سرور جلوگیری می‌کنیم. سپس شیء FormData را بر اساس مقادیر حالت متناظر با المان‌های فرم، تشکیل می‌دهیم. کلیدهایی که در اینجا ذکر می‌شوند، نام خواص مدل متناظر سمت سرور را نیز تشکیل خواهند داد.


ارسال مدل داده‌های فرم React به سمت سرور

پس از تشکیل شیء FormData در متد مدیریت کننده‌ی handleSubmit، اکنون با استفاده از کتابخانه‌ی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
  // ...

export default function UploadFileSimple() {
  const apiUrl = "https://localhost:5001/api/SimpleUpload/SaveTicket";

  // ...
  const [isUploading, setIsUploading] = useState(false);

  const handleSubmit = async event => {
    event.preventDefault();

    const formData = new FormData();
    formData.append("description", description);
    formData.append("file1", selectedFile1);
    formData.append("file2", selectedFile2);

    try {
      setIsUploading(true);

      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        }}
      });

      toast.success("Form has been submitted successfully!");

      console.log("uploadResult", data);

      setIsUploading(false);
      setDescription("");
    } catch (error) {
      setIsUploading(false);
      toast.error(error);
    }
  };


  return (
  // ...
  );
}
در اینجا نحوه‌ی ارسال شیء FormData را توسط کتابخانه‌ی axios به سمت سرور مشاهده می‌کنید. با استفاده از متد post آن، به سمت مسیر api/SimpleUpload/SaveTicket که آن‌را در ادامه تکمیل خواهیم کرد، شیء formData متناظر با اطلاعات فرم، به صورت async، ارسال شده‌است. همچنین headers آن نیز به همان «"enctype="multipart/form-data» که پیشتر توضیح داده شد، تنظیم شده‌است.
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده می‌کنید. از آن می‌توان برای فعال و غیرفعال کردن دکمه‌ی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
<button
   disabled={ isUploading }
   className="btn btn-primary"
   type="submit"
>
  Submit
</button>
به این ترتیب اگر فراخوانی await axios.post هنوز به پایان نرسیده باشد، مقدار isUploading مساوی true بوده و سبب غیرفعال شدن دکمه‌ی submit می‌شود.


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

در اینجا شاید نیاز باشد نوع و یا اندازه‌ی فایل‌های انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه می‌کنیم:
  const isFileValid = selectedFile => {
    if (!selectedFile) {
      // toast.error("Please select a file.");
      return false;
    }

    const allowedMimeTypes = [
      "image/png",
      "image/jpeg",
      "image/gif",
      "image/svg+xml"
    ];
    if (!allowedMimeTypes.includes(selectedFile.type)) {
      toast.error(`Invalid file type: ${selectedFile.type}`);
      return false;
    }

    const maxFileSize = 1024 * 500;
    const fileSize = selectedFile.size;
    if (fileSize > maxFileSize) {
      toast.error(
        `File size ${(fileSize / 1024).toFixed(
          2
        )} KB must be less than ${maxFileSize / 1024} KB`
      );
      return false;
    }

    return true;
  };
در اینجا ابتدا بررسی می‌شود که آیا فایلی انتخاب شده‌است یا خیر؟ سپس فایل انتخاب شده، باید دارای یکی از MimeTypeهای تعریف شده باشد. همچنین اندازه‌ی آن نیز نباید بیشتر از 500 کیلوبایت باشد. در هر کدام از این موارد، یک خطا توسط react-toastify به کاربر نمایش داده خواهد شد.

اکنون برای استفاده‌ی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کننده‌ی submit اطلاعات:
  const handleSubmit = async event => {
    event.preventDefault();

    if (!isFileValid(selectedFile1) || !isFileValid(selectedFile2)) {
      return;
    }
در ابتدای متد مدیریت کننده‌ی handleSubmit، متد isFileValid را بر روی دو متغیر حالتی که حاوی اطلاعات فایل‌های انتخابی توسط کاربر هستند، فراخوانی می‌کنیم.

ب) استفاده‌ی از آن جهت غیرفعال کردن دکمه‌ی submit:
<button
            disabled={
              isUploading ||
              !isFileValid(selectedFile1) ||
              !isFileValid(selectedFile2)
            }
            className="btn btn-primary"
            type="submit"
>
   Submit
</button>
می‌توان دقیقا در همان زمانیکه کاربر فایلی را انتخاب می‌کند نیز به انتخاب او واکنش نشان داد. چون مقدار دهی‌های متغیرهای حالت، همواره سبب رندر مجدد فرم می‌شوند و در این حالت مقدار ویژگی disabled نیز محاسبه‌ی مجدد خواهد شد، بنابراین در همان زمانیکه کاربر فایلی را انتخاب می‌کند، متد isFileValid نیز بر روی آن فراخوانی شده و در صورت نیاز، خطایی به او نمایش داده می‌شود.


نمایش درصد پیشرفت آپلود فایل‌ها

کتابخانه‌ی axios، امکان دسترسی به میزان اطلاعات آپلود شده‌ی به سمت سرور را به صورت یک رخ‌داد فراهم کرده‌است که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایل‌ها استفاده می‌کنیم:
      const startTime = Date.now();

      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        },
        onUploadProgress: progressEvent => {
          const { loaded, total } = progressEvent;

          const timeElapsed = Date.now() - startTime;
          const uploadSpeed = loaded / (timeElapsed / 1000);

          setUploadProgress({
            queueProgress: Math.round((loaded / total) * 100),
            uploadTimeRemaining: Math.ceil((total - loaded) / uploadSpeed),
            uploadTimeElapsed: Math.ceil(timeElapsed / 1000),
            uploadSpeed: (uploadSpeed / 1024).toFixed(2)
          });
        }
      });
هر بار که متد رویدادگردان onUploadProgress فراخوانی می‌شود، به همراه اطلاعات شیء progressEvent است که خواص loaded آن به معنای میزان اطلاعات آپلود شده و total هم جمع کل اندازه‌ی اطلاعات در حال ارسال است. بر این اساس و همچنین زمان شروع عملیات، می‌توان اطلاعاتی مانند درصد پیشرفت عملیات، مدت زمان باقیمانده، مدت زمان سپری شده و سرعت آپلود اطلاعات را محاسبه کرد و سپس توسط آن، شیء state ویژه‌ای را به روز رسانی کرد که به صورت زیر تعریف می‌شود:
  const [uploadProgress, setUploadProgress] = useState({
    queueProgress: 0,
    uploadTimeRemaining: 0,
    uploadTimeElapsed: 0,
    uploadSpeed: 0
  });
هر بار به روز رسانی state، سبب رندر مجدد UI می‌شود. به همین جهت متدی را برای رندر جدولی که اطلاعات شیء state فوق را نمایش می‌دهد، به صورت زیر تهیه می‌کنیم:
  const showUploadProgress = () => {
    const {
      queueProgress,
      uploadTimeRemaining,
      uploadTimeElapsed,
      uploadSpeed
    } = uploadProgress;

    if (queueProgress <= 0) {
      return <></>;
    }

    return (
      <table className="table">
        <thead>
          <tr>
            <th width="15%">Event</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>
              <strong>Elapsed time</strong>
            </td>
            <td>{uploadTimeElapsed} second(s)</td>
          </tr>
          <tr>
            <td>
              <strong>Remaining time</strong>
            </td>
            <td>{uploadTimeRemaining} second(s)</td>
          </tr>
          <tr>
            <td>
              <strong>Upload speed</strong>
            </td>
            <td>{uploadSpeed} KB/s</td>
          </tr>
          <tr>
            <td>
              <strong>Queue progress</strong>
            </td>
            <td>
              <div
                className="progress-bar progress-bar-info progress-bar-striped"
                role="progressbar"
                aria-valuemin="0"
                aria-valuemax="100"
                aria-valuenow={queueProgress}
                style={{ width: queueProgress + "%" }}
              >
                {queueProgress}%
              </div>
            </td>
          </tr>
        </tbody>
      </table>
    );
  };
و این متد را به این شکل در ذیل المان fieldset فرم، اضافه می‌کنیم تا کار رندر نهایی را انجام دهد:
{showUploadProgress()}
هربار که state به روز می‌شود، مقدار شیء uploadProgress دریافت شده و بر اساس آن، 4 سطر جدول نمایش پیشرفت آپلود، تکمیل می‌شوند.
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایل‌ها استفاده شده‌است. اگر style آن‌را هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.

یک نکته: اگر می‌خواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگه‌ی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایل‌ها، 100 درصد را مشاهده خواهید کرد.


دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیره‌ی فایل‌های آن‌

بر اساس نحوه‌ی تشکیل FormData سمت کلاینت:
const formData = new FormData();
formData.append("description", description);
formData.append("file1", selectedFile1);
formData.append("file2", selectedFile2);
مدل سمت سرور معادل با آن به صورت زیر خواهد بود:
using Microsoft.AspNetCore.Http;

namespace UploadFilesSample.Models
{
    public class Ticket
    {
        public int Id { set; get; }

        public string Description { set; get; }

        public IFormFile File1 { set; get; }

        public IFormFile File2 { set; get; }
    }
}
که در اینجا هر selectedFile سمت کلاینت، به یک IFormFile سمت سرور نگاشت می‌شود. نام این خواص نیز باید با نام کلیدهای اضافه شده‌ی به دیکشنری FormData، یکی باشند.
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using UploadFilesSample.Models;

namespace UploadFilesSample.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SimpleUploadController : Controller
    {
        private readonly IWebHostEnvironment _environment;

        public SimpleUploadController(IWebHostEnvironment environment)
        {
            _environment = environment;
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
        {
            var file1Path = await saveFileAsync(ticket.File1);
            var file2Path = await saveFileAsync(ticket.File2);

            //TODO: save the ticket ... get id

            return Created("", new { id = 1001 });
        }

        private async Task<string> saveFileAsync(IFormFile file)
        {
            const string uploadsFolder = "uploads";
            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            //TODO: Do security checks ...!

            if (file == null || file.Length == 0)
            {
                return string.Empty;
            }

            var filePath = Path.Combine(uploadsRootFolder, file.FileName);
            using (var fileStream = new FileStream(filePath, FileMode.Create))
            {
                await file.CopyToAsync(fileStream);
            }

            return $"/{uploadsFolder}/{file.Name}";
        }
    }
}
توضیحات تکمیلی:
- تزریق IWebHostEnvironment در سازنده‌ی کلاس کنترلر، سبب می‌شود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایل‌های نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه می‌کنید، هنوز هم model binding کار کرده و می‌توان شیء Ticket را به نحو متداولی دریافت کرد:
public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
ویژگی FromForm نیز مرتبط است به هدر multipart/form-data ارسالی از سمت کلاینت:
      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        }}
      });


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگی‌های سمت کلاینت نیز می‌شود، ابتدا به پوشه‌ی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار می‌کند. سپس به پوشه‌ی اصلی برنامه‌ی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائه‌ی برنامه‌ی React بر روی پورت 5001 می‌شود.
مطالب
طراحی گزارش در Stimulsoft Reports.Net – بخش 2
در این بخش هم به معرفی ابزار و امکانات این گزارش‌ساز خواهم پرداخت، که شامل بند Group , فیلد محاسباتی و کامپوننتهای Panel,Clone و همچنین نحوه ایجاد یک گزارش به صورت Master-Detail خواهد بود.
ابتدا برای شروع به شیوه‌ای که در بخش 1 بیان شد یک دیکشنری ایجاد کنید. بر روی صفحه طراحی رایت کلیک کنید و گزینه Design را انتخاب نمایید. فرم Page Setup ظاهر می‌شود، در پنل سمت چپ این فرم گزینه Columns را انتخاب نمایید. سپس مقادیر 2 و 0.1 را به ترتیب برای "Number of Columns" و "Column Gaps" ست نمایید. سپس بندهای Header , Group و Data را به ترتیب بر روی صفحه طراحی قرار دهید. بر روی بند Group دابل کلیک کنید و همانند تصویر زیر فیلد EmployeeID را از جدول Orders انتخاب نمایید.

حال بر روی بند Data دابل کلیک کنید و در بخش Data Source جدول Orders را انتخاب نمایید. از پنل ابزار یک کامپوننت Text بر روی بند Group قرار دهید و بر روی آن دابل کلیک کنید و در بخش Expression عبارت زیر را وارد نمایید.
Employee ID: {Orders.EmployeeID}  --- GLine: {GroupLine}
*GroupLine یک متغیر سیستمی است که شماره سطر جاری گروه را برمی‌گرداند.
از پنل Dictionary همانند تصویر زیر فیلدهای ShipName و ShipAddress را از جدول Orders بر روی بندهای Header و Data قرار دهید.

حال با مشاهده خروجی گزارش از سربرگ Preview شما شاهد یک صفحه دو ستونه خواهید بود که بر اساس فیلد EployeeID گروه بندی شده است. 
فیلد محاسباتی:
این نوع فیلد زمانی استفاده می‌شود که شما بخواهید در هر سطر از اطلاعات با توجه به مقادیر رکورد جاری محاسباتی را انجام داده و در گزارش نمایش دهید. برای اضافه کردن فیلد محاسباتی در پنل Dictionary بر روی جدول Order Details رایت کلیک کنید و گزینه New Calculated Column را انتخاب نمایید، همانند تصویر زیر

در فرم ظاهر شده مقادیر را به صورت زیر وارد نمایید:

Name: TotalPrice 
Alias: TotalPrice 
Type: decimal
Value: Order_Details.UnitPrice * Order_Details.Quantity
حال یک فیلد به نام TotalPrice به فیلدهای جدول Order Details اضافه شده است.

ایجاد گزارش به صورت Master-Detail
:
برای ایجاد چنین گزارشی نیاز به ارتباط بین جدولها می‌باشد. با توجه به نحوه ایجاد Connection برای این مثال، روابط بین جدولها انتقال داده نشده است ولی شما میتوانید رابطه بین جدولها را اضافه نمایید حتی اگر این رابطه در منبع اطلاعات وجود نداشته باشد. برای این مثال نیاز به دو رابطه بین جدول Orders Detail و جدولهای Orders, Products می‌باشد. برای انجام این کار کافیست در پنل Dictionary بر روی جدول Orders Detail رایت کلیک کنید و گزینه New Relation را انتخاب نماید. همانند تصاویر زیر مقادیر را ست نمایید.

حال بر روی صفحه طراحی بعد از بند DataBand1 به ترتیب بندهای Header و Data و Footer را اضافه نمایید. در بند HeaderBand2 چهار کامپوننت Text قرار دهید و به ترتیب از سمت چپ مقادیر زیر را در خصوصیت Text کامپوننتها قرار دهید.

ProductName
UnitPrice 
Quantity
TotalPrice
سپس بر روی بند DataBand2 دابل کلیک کنید در فرم Data Setup ابتدا در بخش Data Source جدول Orders Detail را انتخاب نمایید؛ در بخش Relation رابطه Orders را انتخاب نمایید و در نهایت در بخش Master Component بند DataBand1 را انتخاب نمایید. حال در بند DataBand2 چهار کامپوننت Text قرار دهید و به ترتیب از سمت چپ مقادیر زیر را در خصوصیت Text کامپوننتها قرار دهید.
{Order_Details.Products.ProductName}
{Order_Details.UnitPrice}
{Order_Details.Quantity}
{Order_Details.TotalPrice}
در بند FooterBand1 یک کامپوننت Text در زیر ستون TotalPrice قرار دهید و مقدار زیر را در خصوصیت Text آن قرار دهید.
{Sum(DataBand2,Order_Details.TotalPrice)}
نتیجه کار طراحی گزارش به شکل زیر خواهد بود.

حال میتوانید خروجی گزارش Master-Detail را از سربرگ Preview مشاهده نمایید. در صورتی که همانند تصویر بالا گزارش را طراحی کرده باشید در خروجی گزارش فاصله‌ای بین سطرها ایجاد شده است که علت آن ارتفاع کمتر کامپوننتهای Text نسبت به بندهای خود می‌باشد. برای رفع این مشکل، شما سه راه حل دارید.

الف: یکسان سازی ارتفاع کاپوننتها با بند دربرگیرنده آنها

ب: ست کردن خصوصیت Can Shrink بند دربرگیرنده کامپوننتها به مقدار true

ج: ست کردن خصوصیت Grow to Height کامپوننتهای Text به مقدار true

در این مثال ما از روش دوم استفاده میکنیم و خصوصیت Can Shrink بندهای HeaderBand2 و DataBand2 را به مقدار true ست میکنیم.

کامپوننتهای Panel و Clone :

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

Clone: به شما امکان کپی کردن یک بخش از گزارش را میدهد که البته آن بخش فقط میتواند از نوع Panel باشد.

این دو کامپوننت یکی از عوامل قدرت این گزارش‌ساز می‌باشد. برای شروع یک Page (صفحه طراحی) دیگر به گزارش اضافه کنید. می‌توانید با رایت کلیک بر روی نوار سربرگهای محیط طراحی گزینه New Page را انتخاب نمایید. بر روی Page2 یک کامپوننت Panel قرار دهید، سپس از پنل Dictionary جدول Countries را دراگ کرده و در Panel1 رها کنید. در فرم Data تمامی فیلدها و بند Header را انتخاب نمایید، سپس یک کامپوننت Clone به صفحه طراحی اضافه کنید بلافاصله فرم Select Container ظاهر می‌شود، Panel1 را انتخاب کنید. حال شما می‌توانید خروجی گزارش را مشاهده کنید، خروجی Page2 بعد از خروجی Page1 ظاهر خواهد شد.

Report2-1.mrt 

مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت اول - معرفی و تعیین نوع props کامپوننت‌ها
React به صورت پیش‌فرض از ES6 برای توسعه‌ی برنامه‌‌های خودش استفاده می‌کند؛ اما استفاده‌ی از TypeScript با پروژه‌های React، مزایای قابل توجهی را مانند type checking در زمان کامپایل برنامه، دسترسی به intellisense آنی، امکان refactoring بهتر را در اختیار توسعه دهنده قرار می‌دهد که نه فقط سرعت و سهولت توسعه را افزایش می‌دهند، بلکه از بروز بسیاری از خطاهای زمان اجرای برنامه نیز جلوگیری می‌کند.


ایجاد پروژه‌های React مبتنی بر TypeScript

برای ایجاد ساختار ابتدایی پروژه‌های React که جهت استفاده‌ی از TypeScript تنظیم شده‌اند، دستور زیر را در خط فرمان اجرا می‌کنیم:
npx create-react-app tssample --template typescript
مزیت کار با npx، عدم نیاز به نصب محلی برنامه‌ی create-react-app است. به این ترتیب هربار که این دستور را به این نحو اجرا می‌کنیم، مطمئن خواهیم بود که از آخرین نگارش برنامه‌ی create-react-app استفاده خواهد شد؛ و نه نگارش محلی که پیشتر نصب کرده‌ایم که ممکن است هم اکنون تاریخ مصرف گذشته باشد.
بنابراین npx create-react-app کار اجرای آخرین نسخه‌ی برنامه‌ی ایجاد ساختار پروژه‌های React را انجام می‌دهد. پس از آن یک نام دلخواه ذکر شده‌است و در آخر توسط سوئیچ template typescript-- سبب خواهیم شد تا این ساختار بجای استفاده‌ی از ES6 پیش‌فرض، بر اساس TypeScript ایجاد و تنظیم شود.


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


در تصویر فوق، نمونه‌ای از این ساختار ابتدایی ایجاد شده‌ی مبتنی بر TypeScript را مشاهده می‌کنید. اولین تفاوت مهم این ساختار، با ساختار پیش‌فرض پروژه‌های React مبتنی بر ES6، وجود فایل جدید tsconfig.json است. کار آن تنظیم پارامترهای کامپایلر TypeScript است. همچنین اینبار بجای پسوندهای js و jsx، پسوندهای ts و tsx قابل مشاهده هستند؛ مانند فایل‌های serviceWorker.ts ، index.tsx و App.tsx. البته اگر به ساختار این فایل‌ها دقت کنید، آنچنان تفاوت مهمی را با نمونه‌های قبلی ES6 خود ندارند و تقریبا یکی هستند. روش اجرای آن‌ها نیز مانند قبل است و با همان دستور npm start صورت می‌گیرد:



قابلیت استفاده‌ی از کدهای جاوا اسکریپتی موجود، در پروژه‌های تایپ اسکریپتی جدید

داخل پوشه‌ی src، پوشه‌ی جدید components را ایجاد کرده و داخل آن فایل جدید Head.js را اضافه می‌کنیم. سپس داخل آن rfac را نوشته و دکمه‌ی tab را فشار می‌دهیم تا ساختار ابتدایی یک react arrow functional component جدید ایجاد شود:
import React from "react";

export const Head = () => {
  return (
    <div>
      <h1>Hello</h1>
    </div>
  );
};
در ادامه جهت نمایش آن، آن‌را به فایل src\App.tsx به شکل متداولی، ابتدا با import تابع آن و سپس درج المان متناظر با آن در تابع App، اضافه می‌کنیم:
import { Head } from "./components/Head";
// ...

function App() {
  return (
    <div className="App">
      <Head />
      // ... 
    </div>
  );
}

export default App;
اگر دقت کرده باشید، پسوند این فایل را js درنظر گرفته‌ایم (src\components\Head.js) و نه ts و بدون مشکل می‌توان از آن در داخل یک فایل tsx استفاده کرد. علت آن به وجود تنظیم allowJs در فایل tsconfig.json برنامه بر می‌گردد. مزیت وجود یک چنین تنظیمی، امکان مهاجرت ساده‌تر کدهای ES6 موجود، به یک پروژه‌ی تایپ اسکریپتی جدید است. به این ترتیب می‌توان از تمام این کدها بدون مشکل در برنامه‌ی جدید خود استفاده کرد و سر فرصت، یکی یکی آن‌ها را به tsx تبدیل نمود.
به همین جهت پس از مشاهده‌ی این قابلیت، پسوند فایل کامپوننت جدید js ایجاد شده را به tsx تغییر می‌دهیم (src\components\Head.tsx). البته در یک چنین حالتی اگر هنوز دستور npm start در حال اجرا است، نیاز خواهید داشت یکبار آن‌را بسته و مجددا اجرا کنید. پس از آن، باز هم برنامه بدون مشکل کامپایل می‌شود و نشان دهنده‌ی این است که کدهای نوشته شده‌ی در کامپوننت Head، کدهای کاملا معتبر تایپ اسکریپتی نیز هستند. علت اینجا است که TypeScript، در حقیقت Superset جاوا اسکریپت به‌شمار می‌رود و قابلیت‌های جدیدی را به TypeScript استفاده می‌کند. بنابراین کدهای جاوااسکریپتی موجود، کدهای معتبر تایپ اسکریپتی نیز به‌شمار می‌روند.


مشخص کردن نوع props کامپوننت‌ها توسط TypeScript

اولین استفاده‌ی ما از TypeScript در اینجا، مشخص کردن نوع props یک کامپوننت است:
import React from "react";

export const Head = ({ title, isActive }) => {
  return (
    <div>
      <h1>{title}</h1>
      {isActive && <h3>Active</h3>}
    </div>
  );
};
برای این منظور، دو خاصیت جدید را از طریق شیء props به این کامپوننت ارسال کرده و از آن‌ها جهت نمایش یک عنوان و تعیین نمایش یک برچسب استفاده می‌کنیم. اولین موردی را که پس از این تغییر متداول مشاهده می‌کنیم، خط قرمز کشیده شدن زیر متغیرهای حاصل از Object Destructuring مربوط به شیء props است:


علت اینجا است که این فایل، tsx است و نه js. به همین جهت نوع این متغیرها را، همان حالت پیش‌فرض جاوا اسکریپت که any است، درنظر گرفته‌است و ... این مورد بر اساس تنظیمات فایل tsconfig.json برنامه، ممنوع است. البته اگر به این فایل دقت کنید، شاید چنین گزینه‌ای را به صورت صریح نتوانید در آن پیدا کنید. علت اینجا است که تعداد گزینه‌های قابل تنظیم در فایل tsconfig روز به روز بیشتر می‌شوند. به همین جهت برای ساده سازی فعالسازی آن‌ها، از TypeScript 2.3 به بعد، پرچم strict نیز به این تنظیمات اضافه شده‌است. کار آن فعالسازی یکجای تمام بررسی‌های strict است؛ مانند noImplicitAny، strictNullChecks و غیره.
{ 
    "compilerOptions": { 
        "strict": true  /* Enable all strict type-checking options. */ 
    } 
}
در این حالت اگر نیاز به لغو یکی از گزینه‌ها بود، می‌توان به صورت ذیل عمل کرد:
{ 
    "compilerOptions": { 
        "strict": true, 
        "noImplicitAny": false 
    } 
}
گزینه‌ی strict تمام بررسی‌های متداول را فعال می‌کند؛ اما ذکر و تنظیم صریح noImplicitAny به false، تنها این یک مورد را لغو خواهد کرد.
بنابراین چون در فایل tsconfig.json برنامه‌ی React ما گزینه‌ی strict به true تنظیم شده‌است، گزینه‌ی فعال noImplicitAny نیز جزئی از آن است و دیگر نمی‌توان متغیر یا خاصیتی را بدون ذکر صریح نوع آن، رها کرد.

برای رفع خطای noImplicitAny موجود، به ابتدای فایل src\components\Head.tsx، نوع جدید Props را اضافه می‌کنیم (نام آن کاملا دلخواه است):
type Props = {
  title: string;
  isActive: boolean;
};
و سپس از آن جهت مشخص سازی نوع شیء props رسیده، به نحو زیر استفاده خواهیم کرد:
export const Head = ({ title, isActive }: Props) => {
پس از این تغییر، خطای noImplicitAny پیشین، برطرف می‌شود و دیگر خطوط قرمز ذیل دو متغیر حاصل از Object Destructuring، مشاهده نمی‌شوند.

یک نکته: البته اگر از سری React 16x بخاطر داشته باشید، می‌توان یک چنین قابلیتی را توسط propTypes خود React نیز پیاده سازی کرد:
Head.propTypes = {
  title: PropTypes.string,
  isActive: PropTypes.bool
}
 اما روش کار با TypeScript، نسبت به آن بسیار پیشرفته‌تر است. برای کار با TypeScript، نیازی به import یک بسته‌ی جدید، مانند PropTypes نیست و همچنین بررسی PropTypes توسط خود React، در زمان اجرای برنامه صورت می‌گیرد؛ اما با TypeScript، بررسی زمان کامپایل برنامه را خواهیم داشت و همچنین نمایش آنی خطاهای مرتبط با عدم رعایت آن‌ها، در ادیتورهایی مانند VSCode. به علاوه روش تعریف type ذکر شده‌ی توسط TypeScript، نسبت به نمونه‌ی پیشنهاد شده‌ی توسط React با propTypes، بسیار تمیزتر و خواناتر است.

پس از این تغییر، اگر به فایل src\App.tsx مراجعه کنیم، مشاهده می‌کنیم که ذیل تعریف المان کامپوننت Head، مجددا خط قرمزی کشیده شده‌است:


عنوان می‌کند که بر اساس نوع جدید Props ای که تعریف کرده‌اید، نیاز است دو خاصیت اجباری title و isActive را نیز در اینجا ذکر کنید؛ وگرنه تعریف این المان، بدون آن‌ها ناقص است.
امکان جالب دیگری که با تعریف نوع props توسط تایپ‌اسکریپت رخ می‌دهد، فعال شدن intellisense متناظر با تعریف این خواص و ویژگی‌ها است:


در ادامه با تعریف این دو ویژگی جدید، خط قرمز رنگ ذیل کامپوننت Head برطرف می‌شود:
<Head title="Hello" isActive={true} />
و اگر در حین تعریف این ویژگی‌ها، نوع‌های مقادیر آن‌ها را به درستی وارد نکنیم، بازهم شاهد تذکر آنی خطاهای مرتبط با آن‌ها خواهیم بود:


همچنین در این حالت، کد برنامه نیز کامپایل نمی‌شود و ذکر این خطاها صرفا منحصر به ادیتور مورد استفاده نیست.

بنابراین به صورت خلاصه مزیت‌های کار با TypeScript برای تعاریف نوع props به شرح زیر است:
- auto-complete و داشتن intellisense خودکار.
- اگر نام المان کامپوننتی و یا نام یکی از props را به اشتباه وارد کنیم، بلافاصله خطای یافت نشدن آن‌ها را نمایش می‌دهد.
- اگر ذکر یک prop اجباری را فراموش کنیم، بلافاصله خطای متناظری را دریافت می‌کنیم.
- اگر نوع مقدار یکی از props را به اشتباه وارد کنیم، باز هم خطایی را جهت گوشزد کردن آن مشاهده خواهیم کرد.
- فعال بودن TypeScript، امکان refactoring بسیار قوی‌تری را میسر می‌کند. برای مثال با فشردن دکمه‌ی F2 می‌توان نام یک کامپوننت را در کل برنامه به سادگی تغییر داد. همچنین یک چنین قابلیتی برای تغییر نام props نیز میسر است و به صورت خودکار تمام کاربردهای آن‌را نیز به روز می‌کند.
- اگر نوع prop ای را در تعریف آن تغییر دادیم، اما مقدار منتسب به آن‌را خیر، باز هم بلافاصله متوجه این مشکل خواهیم شد.

به این ترتیب با دسترسی به بررسی‌های دقیق زمان کامپایل برنامه، می‌توان مشکلات بسیار کمتری را در زمان اجرای آن شاهد بود.
مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت دوم - تعیین نوع‌های پیشرفته‌تر props
در قسمت قبل با معرفی نوع props توسط TypeScript، مجبور به تکمیل اجباری تک تک آن‌ها شدیم؛ اما در React می‌توان props را به صورت اختیاری و یا با مقادیری پیش‌فرض نیز تعریف کرد.


روش تعیین props پیش‌فرض توسط TypeScript

اگر بخواهیم توسط روش‌های خود React، مقادیر پیش‌فرض props را تعیین کنیم، می‌توان از defaultProps به صورت زیر با تعریف یک شیء جاوا اسکریپتی از پیش مقدار دهی شده، استفاده کرد:
Head.defaultProps = {
   title: "Hello",
   isActive: true
};
اما در حالت استفاده‌ی از TypeScript و یا حتی نگارش ES6 آن (React در حالت پیش‌فرض آن)، می‌توان مقادیر پیش‌فرض props را با مقدار دهی مستقیم متغیرهای حاصل از Object Destructuring آن، تعیین کرد:
type Props = {
  title: string;
  isActive: boolean;
};

export const Head = ({ title = "Hello", isActive = true }: Props) => {
در اینجا هر متغیری که با مقداری پیش‌فرض، مقدار دهی شده باشد، اختیاری در نظر گرفته شده و اگر دارای مقدار پیش‌فرضی نباشد، باید به صورت اجباری در حین تعریف المان این کامپوننت، ذکر شود.
در این حالت انتظار داریم که در حین استفاده و تعریف المان کامپوننت Head، اگر برای مثال ویژگی isActive را ذکر نکردیم، کامپایلر TypeScript خطایی را گزارش نکند؛ که اینطور نیست:


هنوز هم در اینجا می‌توان خطای عدم تعریف خاصیت isActive را مشاهده کرد. برای رفع این مشکل، به صورت زیر عمل می‌کنیم:
type Props = {
  title: string;
  isActive?: boolean;
};

export const Head = ({ title, isActive = true }: Props) => {
در حین تعریف یک type، اگر خاصیتی با علامت ? ذکر شود، به معنای اختیاری بودن آن است. همچنین در اینجا مقدار پیش‌فرض title را هم حذف کرده‌ایم تا تعریف آن اجباری شود. بنابراین در typeها، تمام خواص اجباری هستند؛ مگر اینکه توسط ? به صورت اختیاری تعریف شوند. این مورد هم مزیتی است که در ابتدای طراحی props یک کامپوننت، باید در مورد اختیاری و یا اجباری بودن آن‌ها بیشتر فکر کرد. همچنین نیازی به استفاده از روش‌های غیراستانداردی مانند Head.defaultProps خود React نیست. ذکر مقدار پیش‌فرض متغیرهای حاصل از Object Destructuring، جزئی از جاوااسکریپت استاندارد است و یا مشخص سازی خواص اختیاری در TypeScript، فقط مختص به پروژه‌های React نیست و در همه جا به همین شکل کاربرد دارد.
اکنون با تعریف isActive?: boolean، دیگر شاهد نمایش خطایی در حین تعریف المان Head، بدون ذکر خاصیت isActive، نخواهیم بود.


تعریف انواع و اقسام نوع‌های props

تا اینجا نوع‌های ساده‌ای مانند string و boolean و همچنین نحوه‌ی تعریف اجباری و اختیاری آن‌ها را بررسی کردیم. در ادامه یک نمونه‌ی کامل‌تر را مشاهده می‌کنید:
type User = {
  name: string;
};

type Props = {
  title: string;
  isActive?: boolean;
  count: number;
  options: string[];
  status: "loading" | "loaded";
  thing: {};
  thing2: {
    name: string;
  };
  user: User;
  func: () => void;
};
- در ابتدا نوع‌های متداولی مانند number و string ذکر شده‌اند.
- سپس نحوه‌ی تعریف آرایه‌ای از رشته‌ها را مشاهده می‌کنید.
- یا می‌توان مقدار یک خاصیت را تنها به مقادیری خاص محدود کرد؛ مانند خاصیت status در اینجا و اگر در حین مقدار دهی این خاصیت، از مقدار دیگری استفاده شود، تایپ‌اسکریپت، خطایی را صادر می‌کند.
- در ادامه سه روش تعریف اشیاء تو در تو را مشاهده می‌کنید؛ خاصیت thing از نوع یک شیء خالی تعریف شده‌است (بجای آن می‌توان از نوع object هم استفاده کرد). خاصیت thing2 از نوع یک شیء که دارای خاصیت رشته‌ای name است، تعریف شده و یا بهتر است این نوع تعاریف را به یک type مستقل دیگر مانند User منتقل کرد و سپس از آن جهت تعیین نوع خاصیتی مانند user استفاده نمود.
- در اینجا حتی می‌توان یک خاصیت را که از نوع یک تابع است، تعریف کرد. در این تعریف، void نوع خروجی آن است.


روش تعریف props تابعی در TypeScript

برای بررسی روش تعریف نوع توابع ارسالی از طریق props، ابتدا کامپوننت جدید src\components\Button.tsx را ایجاد می‌کنیم. سپس آن‌را به صورت زیر تکمیل خواهیم کرد:
import React from "react";

type Props = {
  // onClick(): string;  method returns string
  // onClick(): void  method returns nothing;
  // onClick(text: string): void; method with params
  // onClick: () => void; function returns nothing
  onClick: (text: string) => void; // function with params
};

export const Button = ({ onClick }: Props) => {
  return <button onClick={() => onClick("hi")}>Click Me</button>;
};
در این کامپوننت، متغیر onClick حاصل از Object Destructuring شیء props دریافتی، یک تابع است که قرار است با کلیک بر روی دکمه‌ای که در این کامپوننت قرار دارد، پیامی را به کامپوننت والد ارسال کند.
با توجه به تعریف { onClick }، در همان لحظه، خطای any بودن نوع آن از طرف TypeScript گزارش داده می‌شود. بنابراین نوع جدید Props را ایجاد کرده و برای onClick، نوع متناسبی را تعریف می‌کنیم. در اینجا 4 روش مختلف تعریف نوع function را در TypeScript مشاهده می‌کنید؛ دو حالت آن با ذکر پرانتزها و درج امضای متد انجام شده و دو حالت دیگر به کمک arrow functions پیاده سازی شده‌اند.
برای نمونه آخرین حالت تعریف شده از روش arrow functions استفاده می‌کند که متداول‌ترین روش تعریف نوع توابع است (چون عنوان می‌کند که نوع onClick، یک تابع است و آن‌را شبیه به یک متد معمولی نمایش نمی‌دهد) که در آن در ابتدا امضای پارامترهای این تابع مشخص شده‌اند و در ادامه پس از <=، نوع خروجی این تابع تعریف شده‌است که void می‌باشد (این تابع چیزی را بر نمی‌گرداند).

در آخر، تعریف المان آن‌را به صورت زیر به فایل src\App.tsx اضافه می‌کنیم که onClick آن یک مقدار را دریافت کرده و سپس آن‌را در کنسول نمایش می‌دهد.
البته خروجی از نوع void، در اینجا بسیار معمول است؛ چون هدف از این نوع توابع بیشتر ارسال مقادیری به کامپوننت در برگیرنده‌ی آن‌ها است (مانند value در اینجا) و اگر برای مثال خروجی رشته‌ای را داشته باشند، باید در حین درج و تعریف المان آن‌ها، برای نمونه یک "return "value1 را هم در انتهای کار قرار داد که عملا استفاده‌ای ندارد و بی‌معنا است:
import { Button } from "./components/Button";
import { Head } from "./components/Head";
// ...

function App() {
  return (
    <div className="App">
      <Head title="Hello" />
      <Button
        onClick={(value) => {
          console.log(value);
        }}
      />
  // ...
مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت سوم - قالب پذیر ساختن گرید
در قسمت دوم، قالب نمایش ردیف‌های جدول، ثابت است و درون جدول به صورت مستقیمی درج و تعریف شده‌است. در ادامه می‌خواهیم این گرید را به نحوی تغییر دهیم که به ازای حالت‌های مختلفی مانند نمایش اطلاعات و یا ویرایش اطلاعات هر ردیف، از قالب‌های خاص آن‌ها استفاده شود.
قابلیتی که در ادامه از آن برای «قالب پذیر ساختن گرید» استفاده خواهیم کرد، همان نکته‌ی «امکان تعویض پویای قالب‌های یک دربرگیرنده» است که در مطلب «امکان تعریف قالب‌ها در Angular با دایرکتیو ng-template» به آن پرداختیم.


تعریف قالب‌های نمایش و ویرایش اطلاعات یک ردیف در گرید طراحی شده

پس از آشنایی با دایرکتیوهای تعریف و کار با قالب‌ها در Angular، اکنون تبدیل بدنه‌ی ثابت جدول، به دو قالب نمایش و ویرایش، ساده‌است.
در قسمت دوم این سری، کار رندر بدنه‌ی اصلی گرید توسط همین چند سطر، در قالب آن مدیریت می‌شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
        <td class="text-center">{{ item.productId }}</td>
        <td class="text-center">{{ item.productName }}</td>
        <td class="text-center">{{ item.price | number:'.0' }}</td>
        <td class="text-center">
          <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
            disabled="disabled" />
        </td>
      </tr>
    </tbody>
  </table>

در ادامه قسمت داخلی ngFor را تبدیل به یک ng-container می‌کنیم تا قالب پذیر شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <ng-container [ngTemplateOutlet]="loadTemplate(item)"
                 [ngOutletContext]="{ $implicit: item, idx: i }"></ng-container>
      </tr>
    </tbody>
کار دایرکتیو ngOutletContext، تنظیم شیء context هر قالب است. به این ترتیب شیء متناظر با هر ردیف و همچنین ایندکس آن‌را به هر قالب ارجاع می‌دهیم. خاصیت implicit$ به این معنا است که اگر منبع داده‌ی متغیر ورودی مشخص نشد، از مقدار item استفاده شود.
در اینجا ngTemplateOutlet این امکان را می‌دهد تا بتوان توسط کدهای برنامه، قالب هر ردیف را مشخص کرد. متد loadTemplate در کدهای کامپوننت متناظر فراخوانی شده و بر اساس وضعیت هر ردیف، یکی از دو قالب ذیل را بازگشت می‌دهد:

الف) قالب نمایش معمولی و فقط خواندنی رکوردها


<!--The Html Template for Read-Only Rows-->
<ng-template #readOnlyTemplate let-item let-i="idx">
  <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
  <td class="text-center">{{ item.productId }}</td>
  <td class="text-center">{{ item.productName }}</td>
  <td class="text-center">{{ item.price | number:'.0' }}</td>
  <td class="text-center">
    <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
      disabled="disabled" />
  </td>
  <td>
    <input type="button" value="Edit" class="btn btn-default btn-xs" (click)="editItem(item)"
    />
  </td>
  <td>
    <input type="button" value="Delete" (click)="deleteItem(item)" class="btn btn-danger btn-xs"
    />
  </td>
</ng-template>
همانطور که ملاحظه می‌کنید، در اینجا بدنه‌ی ngFor را به یک ng-template مشخص شده‌ی با readOnlyTemplate# انتقال داده‌ایم. همچنین دو متغیر ورودی item و i را توسط -let تعریف کرده‌ایم. چون عبارت منبع داده item مشخص نشده‌است، از همان خاصیت implicit$ شیء context استفاده می‌کند.
این قالب در کدهای کامپوننت آن به صورت ذیل قابل دسترسی و انتخاب شده‌است:
 @ViewChild("readOnlyTemplate") readOnlyTemplate: TemplateRef<any>;

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



شبیه به همان کاری را که برای نمایش ردیف‌های فقط خواندنی انجام دادیم، در مورد قالب ویرایش هر ردیف نیز تکرار می‌کنیم. در اینجا فقط امکان ویرایش نام محصول، قیمت آن و موجود بودن آن‌را توسط یک‌سری input box مهیا کرده‌ایم:
<!--The Html Template for Editable Rows-->
<ng-template #editTemplate let-item let-i="idx">
  <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
  <td class="text-center">{{ item.productId }}</td>
  <td class="text-center">
    <input type="text" [(ngModel)]="selectedItem.productName" class="form-control" />
  </td>
  <td class="text-center">
    <input type="text" [(ngModel)]="selectedItem.price" class="form-control" />
  </td>
  <td class="text-center">
    <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
      [(ngModel)]="selectedItem.isAvailable" />
  </td>
  <td>
    <input type="button" value="Save" (click)="saveItem()" class="btn btn-success btn-xs"
    />
  </td>
  <td>
    <input type="button" value="Cancel" (click)="cancel()" class="btn btn-warning btn-xs"
    />
  </td>
</ng-template>
به این قالب نیز با توجه به template reference variable آن که editTemplate# نام دارد، به صورت ذیل در کامپوننت متناظر دسترسی خواهیم یافت.
 @ViewChild("editTemplate") editTemplate: TemplateRef<any>;

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


خواص عمومی مورد نیاز جهت کار با قالب‌ها و ویرایش‌های درون ردیفی

@ViewChild("readOnlyTemplate") readOnlyTemplate: TemplateRef<any>;
@ViewChild("editTemplate") editTemplate: TemplateRef<any>;
selectedItem: AppProduct;
isNewRecord: boolean;
برای اینکه بتوانیم قالب‌ها را به صورت پویا تعویض کنیم، نیاز است در کدهای کامپوننت، به آن‌ها دسترسی داشت. اینکار را توسط تعریف ViewChildهایی با همان نام template reference variable قالب‌ها انجام داده‌ایم.
به علاوه اگر به قالب editTemplate دقت کنید، مقدار ویرایش شده به [(ngModel)]="selectedItem.productName" انتساب داده می‌شود. به همین جهت شیء selectedItem نیز تعریف شده‌است.
همچنین نیاز است بدانیم اکنون در حال ویرایش یک ردیف هستیم یا این ردیف، کاملا ردیف جدیدی است. به همین جهت پرچم isNewRecord نیز تعریف شده‌است.


فعالسازی قالب ویرایش هر ردیف

در انتهای هر ردیف، دکمه‌ی ویرایش نیز قرار دارد که به (click) آن، رخداد editItem متصل است:
editItem(item: AppProduct) {
  this.selectedItem = item;
}
در اینجا Item انتخابی را به selectedItem انتساب می‌دهیم. همین مساله سبب محاسبه‌ی مجدد ردیف می‌شود. یعنی متد loadTemplate داخل حلقه‌ی ngFor مجددا فراخوانی می‌شود:
  loadTemplate(item: AppProduct) {
    if (this.selectedItem && this.selectedItem.productId === item.productId) {
      return this.editTemplate;
    } else {
      return this.readOnlyTemplate;
    }
  }
در اینجا بررسی می‌کنیم که آیا در حال ویرایش اطلاعات هستیم؟ آیا selectedItem  مقدار دهی شده‌است و نال نیست؟ اگر بله، قالب editTemplate را بازگشت می‌دهیم. اگر خیر، قالب نمایش ردیف‌های فقط خواندنی بازگشت داده می‌شود. به این ترتیب می‌توان در کدهای برنامه به صورت پویا، در مورد نمایش قالبی خاص تصمیم‌گیری کرد.


مدیریت افزودن یک ردیف جدید

دکمه‌ی افزودن یک ردیف جدید به صورت ذیل به قالب اضافه شده‌است:
<div class="panel">
  <input type="button" value="Add new product" class="btn btn-primary" (click)="addItem()"
  />
</div>
بنابراین نیاز است رخ‌داد addItem آن‌را به صورت ذیل تعریف کرد:
  addItem() {
    this.selectedItem = new AppProduct(0, "", 0, false);
    this.isNewRecord = true;

    this.queryResult.items.push(this.selectedItem);
    this.queryResult.totalItems++;
  }
در اینجا برخلاف حالت ویرایش که selectedItem را به item انتخابی ردیف جاری تنظیم کردیم، آن‌را به یک شیء جدید و تازه تنظیم می‌کنیم. همچنین پرچم isNewRecord  را نیز true خواهیم کرد. سپس این آیتم را به لیست رکوردهای موجود گرید نیز اضافه می‌کنیم. همینقدر تغییر، سبب محاسبه‌ی مجدد loadTemplate و بارگذاری قالب ویرایشی آن می‌شود.


مدیریت لغو ویرایش هر ردیف

برای اینکه ویرایش هر ردیف را لغو کنیم و قالب آن‌‌را به حالت فقط خواندنی بازگشت دهیم، فقط کافی است selectedItem را به نال تنظیم کنیم:
cancel() {
  this.selectedItem = null;
}
با این تنظیم و محاسبه‌ی خودکار و مجدد متد loadTemplate، قسمت return this.readOnlyTemplate فعال می‌شود که سبب نمایش عادی یک ردیف خواهد شد.


مدیریت حذف هر ردیف

در اینجا با پیاده سازی متد رخ‌دادگردان deleteItem و ارسال id هر ردیف به سرور، کار حذف هر ردیف را انجام خواهیم داد:
  deleteItem(item: AppProduct) {
    this.productsService
      .deleteAppProduct(item.productId)
      .subscribe((resp: Response) => {
        this.getPagedProductsList();
      });
  }


مدیریت ثبت و یا به روز رسانی هر ردیف

آخرین عملیاتی که باید مدیریت شود، بررسی پرچم isNewRecord است. اگر true بود، کار افزودن یک ردیف جدید صورت گرفته و سپس این پرچم false می‌شود. اگر false بود، به معنای درخواست به روز رسانی ردیفی مشخص است. در پایان هر دو عملیات selectedItem را نیز true می‌کنیم و این پایان عملیات باید داخل قسمت دریافت پاسخ از سرور مدیریت شود و نه پس از فراخوانی این متدها؛ چون متدهای subscribe غیرهمزمان بوده و ردیف‌های پس از آن‌ها بلافاصله اجرا می‌شوند.
  saveItem() {
    if (this.isNewRecord) {
      this.productsService
        .addAppProduct(this.selectedItem)
        .subscribe((resp: AppProduct) => {
          this.selectedItem.productId = resp.productId;
          this.isNewRecord = false;
          this.selectedItem = null;
        });
    } else {
      this.productsService
        .updateAppProduct(this.selectedItem.productId, this.selectedItem)
        .subscribe((resp: AppProduct) => {
          this.selectedItem = null;
        });
    }
  }


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت هفتم - تعیین نوع هوک useContext
پیشتر در مطلب «React 16x - قسمت 33 - React Hooks - بخش 4 - useContext Hook» با هوک useContext آشنا شدیم. در این قسمت می‌خواهیم نکات تایپ‌اسکریپتی آن‌را بررسی کنیم.


ایجاد UserContext

فرض کنید برای انتقال داده‌های کاربر وارد شده‌ی به سیستم، از روش انتقال props از بالاترین کامپوننت موجود در component tree به پایین‌ترین کامپوننت آن، نیاز است از Context استفاده کنیم. به همین جهت فایل جدید src\contexts\userContext.tsx را با محتوای زیر ایجاد می‌کنیم:
import React from "react";

export type User = {
  name: string;
  isActive: boolean;
  logOut: () => void;
};

export const UserContext = React.createContext<User>({});
export const useUser = () => React.useContext(UserContext);
متد createContext در اصل یک متد جنریک است که بر اساس نوع آرگومان جنریک آن، مقدار شیء تامین کننده‌ی مقدار آن مشخص می‌شود. برای مثال اگر نوع User را تعریف کرده و به آن انتساب دهیم، یک چنین امضایی را پیدا می‌کند:
function React.createContext<User>(defaultValue: User,
 calculateChangedBits?: ((prev: User, next: User) => number) | undefined): React.Context<User>
اما ... قطعه کد فوق با خطای تایپ‌اسکریپتی زیر کامپایل نمی‌شود:
Argument of type '{}' is not assignable to parameter of type 'User'.
Type '{}' is missing the following properties from type 'User': name, isActive, logOutts(2345)
عنوان می‌کند که شیء خالی که به createContext ارسال کرده‌ایم، از نوع User نیست. برای رفع این مشکل می‌توان از مفهومی به نام Partial استفاده کرد:
export const UserContext = React.createContext<Partial<User>>({});
به این ترتیب تمام خواص نوع User به صورت اختیاری معرفی می‌شوند و در این حالت انتساب یک شیء خالی اولیه به آن، مشکلی را ایجاد نخواهد کرد.

نکته 1: البته می‌توان آرگومان جنریک آن‌را ذکر نکرد و createContext را با یک شیء آغازین از نوع User مقدار دهی کرد. در این حالت با استفاده از Type Inference، نوع آن، همان User درنظر گرفته می‌شود و دیگر نیازی به ذکر صریح این نوع نخواهد بود.
نکته 2: اگر از شیء مقدار دهی شده‌ی آغازین استفاده کنید، دیگر حتی نیازی به ذکر export type User هم نیست. نوع createContext بر اساس نوع خواص شیء آغازین ارسالی به آن در سراسر برنامه مشخص شده و قابل دسترسی می‌شود؛ با intellisense کامل و type checking دقیق.
نکته 3: useUser تعریف شده، در حقیقت یک هوک سفارشی است که می‌توان بجای React.useContext از آن در سایر قسمت‌های برنامه استفاده کرد.
نکته 4: اگر علاقمند به استفاده‌ی از نوع Partial نیستید، می‌توان از union types هم در اینجا استفاده کرد. در این حالت می‌توان مقدار اولیه را undefined تعریف کرد:
export const UserContext = React.createContext<User | undefined>(undefined);


معرفی UserContext به بالاترین کامپوننت موجود در component tree

برای این منظور به فایل src\App.tsx مراجعه کرده و آن‌را با UserContext.Provider محصور می‌کنیم:
import { User, UserContext } from "./contexts/userContext";
// ...

function App() {
  const user: User = {
    name: "User 1",
    isActive: true,
    logOut: () => {
      console.log("LogOut.");
    },
  };

  return (
    <UserContext.Provider value={user}>
    // ... 
    </UserContext.Provider>
  );
}

export default App;
ابتدا نوع User و سپس UserContext  را import کرده‌ایم و سپس کل محتوای موجود در کامپوننت App را با UserContext.Provider که دارای مقدار user ابتدایی تعریف شده‌است، محصور می‌کنیم.


خواندن شیء Context در کامپوننتی دیگر

برای این منظور به کامپوننت src\components\Head.tsx که در قسمت‌های قبل ایجاد کردیم، مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
import { UserContext } from "../contexts/userContext";

// ...

export const Head = ({ title, isActive = true }: Props) => {
  const context = React.useContext(UserContext); // or useUser();
// ...
};
در اینجا با استفاده از هوک useContext به UserContext دریافتی دسترسی یافته و سپس می‌توان با اطلاعات شیء User کار کرد.
اولین مزیت کار با TypeScript در اینجا، وجود intellisense کامل به همراه context است:


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



یک نکته: اگر UserContext را با استفاده از union types تعریف کنیم:
export const UserContext = React.createContext<User | undefined>(undefined);
هنگام استفاده‌ی از آن، خطای عدم وجود خاصیت‌های آن‌را در حین Object Destructuring دریافت می‌کنیم:


علت اینجا است که با فعال بودن بررسی نوع‌های نال‌نپذیر، چون user context اکنون می‌تواند undefined هم باشد، یا باید از روش context?.name استفاده کرد و یا یک علامت تعجب را پس از useContext قرار داد:
const context = React.useContext(UserContext)!; // or useUser()!;
const { name } = context;
ذکر ! به این معنا است که می‌دانیم خروجی این متد، نال و یا undefined نیست (راهنمایی کردن کامپایلر TypeScript). چون پیشتر آن‌را در کامپوننت App، در حین تعریف Provider، مقدار دهی اولیه کرده‌ایم.
نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
برای دستیابی به متدهای غیر استاتیکی سی شارپ در کامپوننت‌ها از طریق کدهای جاوااسکریپتی و بدون فراخوانی آنها توسط IJSRuntime میتوان از روش زیر استفاده کرد.
var object;

window.JsFunctionHelper = {MapSuccessResponse: function (instance, address) {
       object = instance;
       return instance.invokeMethodAsync("GetAddress", valuesFromApi);
       }
 };

 function getValuesFromApi() {
      .
      .
      .

      object.invokeMethodAsync("GetAddress", valuesFromApi);
 }
در این مثال API گوگل مپ جزییات یک آدرس درخواست شده را به یک فایل js برمیگرداند، و میخواهم این مقادیر را به کامپوننت برای پرازش ارسال کنم. ابتدا یک متغیر با دسترسی global تعریف میکنیم و آن را برابر با  ارجاعی که از وهله‌ی کامپوننت جاری ساخته شده قرار میدهیم. در نهایت شی  invokeMethodAsync از این روش برای فراخوانی متد به همراه پارامتر در کامپوننت در دسترس است.
نحوه امکان فراخوانی کدهای #C از طریق کدهای جاوااسکریپت در برنامه‌های Blazor به عنوان یک نکته تکمیلی در بخش نظرات همین پست آمده.

مطالب
Blazor 5x - قسمت ششم - مبانی Blazor - بخش 3 - چرخه‌های حیات کامپوننت‌ها
در این قسمت می‌خواهیم انواع رویدادهای چرخه‌ی حیات یک کامپوننت را بررسی کنیم. به همین جهت ابتدا دو کامپوننت جدید Lifecycle.razor و Lifecycle‍Child.razor را به مثالی که تا این قسمت تکمیل کرده‌ایم، اضافه کرده و آن‌ها‌را به صورت زیر جهت نمایش رویدادهای چرخه‌ی حیات، تغییر می‌دهیم:

کدهای کامل کامپوننت Pages\LearnBlazor\Lifecycle.razor
@page "/lifecycle"
@using System.Threading

<div class="border">
    <h3>Lifecycles Parent Component</h3>

    <div class="border">
        <LifecycleChild CountValue="CurrentCount"></LifecycleChild>
    </div>

    <p>Current count: @CurrentCount</p>

    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    <br /><br />
    <button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount
</div>

@code
{
    int CurrentCount = 0;
    int MaxCount = 5;

    private void IncrementCount()
    {
        CurrentCount++;
        Console.WriteLine("Parnet - IncrementCount is called");
    }

    protected override void OnInitialized()
    {
        Console.WriteLine("Parnet - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnInitializedAsync is called");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("Parnet - OnParameterSet is called");
    }

    protected override async Task OnParametersSetAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnParametersSetAsync is called");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
            CurrentCount = 111;
        }
        else
        {
            CurrentCount = 999;
            Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnAfterRenderAsync is called");
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("Parnet - ShouldRender is called");
        return true;
    }

    void StartCountdown()
    {
        Console.WriteLine("Parnet - StartCountdown()");
        var timer = new Timer(TimeCallBack, null, 1000, 1000);
    }

    void TimeCallBack(object state)
    {
        if (MaxCount > 0)
        {
            MaxCount--;
            Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
            InvokeAsync(StateHasChanged);
        }
    }
}

و کدهای کامل کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\Lifecycle‍Child.razor
<h3 class="ml-3 mr-3">Lifecycles Child Componenet</h3>

@code
{
    [Parameter]
    public int CountValue { get; set; }

    protected override void OnInitialized()
    {
        Console.WriteLine("  Child - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnInitializedAsync is called");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("  Child - OnParameterSet is called");
    }

    protected override async Task OnParametersSetAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnParametersSetAsync is called");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("  Child - OnAfterRender(firstRender == true) is called");
        }
        else
        {
            Console.WriteLine("  Child - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnAfterRenderAsync is called");
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("  Child - ShouldRender is called");
        return true;
    }
}
و همچنین برای دسترسی به آن‌ها، مدخل منوی زیر را به کامپوننت Shared\NavMenu.razor اضافه می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="lifecycle">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Lifecycles
    </NavLink>
</li>
با توجه به اینکه برنامه‌ی جاری از نوع Blazor Server است، Console.WriteLine‌های آن، در صفحه‌ی کنسول اجرای برنامه ظاهر می‌شوند و نه در developer tools مرورگر:





رویدادهای OnInitialized و OnInitializedAsync

@code
{
    protected override void OnInitialized()
    {
        Console.WriteLine("Parnet - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnInitializedAsync is called");
    }
همانطور که در تصویر فوق نیز ملاحظه می‌کنید، اولین رویدادی که فراخوانی می‌شود، OnInitialized نام دارد و پس از آن نمونه‌ی async آن به نام OnInitializedAsync. این رویدادها زمانیکه یک کامپوننت و اجزای UI آن کاملا بارگذاری شده‌اند، فراخوانی می‌شوند. مهم‌ترین کاربرد آن‌ها، دریافت اطلاعات از سرویس‌های برنامه‌است.
در کامپوننت Lifecycle.razor، یک کامپوننت دیگر نیز به نام Lifecycle‍Child.razor فراخوانی شده‌است. در این حالت ابتدا OnInitialized کامپوننت والد فراخوانی شده‌است و پس از آن بلافاصله فراخوانی OnInitialized کامپوننت فرزند را مشاهده می‌کنیم.


رویدادهای OnParametersSet و OnParametersSetAsync

این رویدادها یکبار در زمان بارگذاری اولیه‌ی کامپوننت و بار دیگر هر زمانیکه کامپوننت فرزند، پارامتر جدیدی را از طریق کامپوننت والد دریافت می‌کند، فراخوانی می‌شوند. برای نمونه کامپوننت LifecycleChild، پارامتر CurrentCount را از والد خود دریافت می‌کند:
<LifecycleChild CountValue="CurrentCount"></LifecycleChild>
هرچند این پارامتر در UI کامپوننت فرزند مثال تهیه شده استفاده نمی‌شود، اما مقدار دهی آن از طرف والد، سبب بروز رویدادهای OnParametersSet و OnParametersSetAsync خواهد شد. برای آزمایش آن اگر بر روی دکمه‌ی click me در کامپوننت والد کلیک کنیم، این رویدادهای جدید را مشاهده خواهیم کرد:
Parnet - IncrementCount is called
Parnet - ShouldRender is called
  Child - OnParameterSet is called
  Child - ShouldRender is called
Parnet - OnAfterRender(firstRender == false) is called
  Child - OnAfterRender(firstRender == false) is called
  Child - OnParametersSetAsync is called
  Child - ShouldRender is called
  Child - OnAfterRender(firstRender == false) is called
  Child - OnAfterRenderAsync is called
Parnet - OnAfterRenderAsync is called
  Child - OnAfterRenderAsync is called
ابتدا متد IncrementCount کامپوننت والد فراخوانی شده‌است که سبب تغییر مقدار پارامتر CurrentCount ارسالی به کامپوننت Lifecycle‍Child می‌شود و پس از آن، رویداد OnParameterSet کامپوننت فرزند را مشاهده می‌کنید که عکس العملی است به این تغییر مقدار. یکی از کاربردهای آن، دریافت مقدار جدید پارامترهای کامپوننت و سپس به روز رسانی قسمت خاصی از UI بر اساس آن‌ها است.



رویدادهای OnAfterRender و OnAfterRenderAsync

پس از هر بار رندر کامپوننت، این متدها فراخوانی می‌شوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آن‌ها به پایان رسیده‌است. یکی از کاربردهای آن، آغاز کامپوننت‌های جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.

یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمی‌شوند؛ چون در مرحله‌ی آخر رندر UI قرار دارند.

@code
{
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
            CurrentCount = 111;
        }
        else
        {
            CurrentCount = 999;
            Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnAfterRenderAsync is called");
    }
}
در مثال‌های فوق، پارامتر firstRender را نیز مشاهده می‌کنید. یک کامپوننت چندین بار می‌تواند رندر شود. برای مثال هربار که توسط رویدادگردانی مقدار فیلدی را که در UI استفاده می‌شود، تغییر دهیم، کامپوننت مجددا رندر می‌شود. برای نمونه با کلیک بر روی دکمه‌ی click me، سبب تغییر مقدار فیلد CurrentCount می‌شویم. این تغییر و فراخوانی ضمنی StateHasChanged در پایان کار متد و در پشت صحنه، سبب رندر مجدد UI شده و در نتیجه‌ی آن، مقدار جدیدی را در صفحه مشاهده می‌کنیم. در اینجا اگر خواستیم بدانیم که رندر انجام شده برای بار اول است که صورت می‌گیرد یا خیر، می‌توان از پارامتر firstRender استفاده کرد.

سؤال: با توجه به مقدار دهی‌های 111 و 999 صورت گرفته‌ی در متد OnAfterRender، در اولین بار نمایش کامپوننت، چه عددی به عنوان CurrentCount نمایش داده می‌شود؟
در اولین بار نمایش صفحه، لحظه‌ای عدد 111 و سپس عدد 999 نمایش داده می‌شود. عدد 111 را در بار اول رندر و عدد 999 را در بار دوم رندر که پس از مقدار دهی پارامتر کامپوننت فرزند است، می‌توان مشاهده کرد.
اما ... اگر پس از نمایش اولیه‌ی صفحه، چندین بار بر روی دکمه‌ی click me کلیک کنیم، همواره عدد 1000 مشاهده می‌شود. علت اینجا است که تغییرات مقادیر فیلدها در متد OnAfterRender، به UI اعمال نمی‌شوند؛ چون در این مرحله، رندر UI به پایان رسیده‌است. در اینجا فقط مقدار فیلد CurrentCount به 999 تغییر می‌کند و به همین صورت باقی می‌ماند. دفعه‌ی بعدی که بر روی دکمه‌ی click me کلیک می‌کنیم، یک واحد به آن اضافه شده و اکنون است که کار رندر UI، مجددا شروع خواهد شد (در واکشن به یک رخ‌داد و فراخوانی ضمنی StateHasChanged در پشت صحنه) و اینبار حاصل 999+1 را در UI مشاهده می‌کنیم و باز هم در پایان کار رندر، مجددا مقدار CurrentCount به 999 تغییر می‌کند که ... دیگر به UI منعکس نمی‌شود تا زمان کلیک بعدی و همینطور الی آخر.


رویدادهای StateHasChanged و ShouldRender

- اگر خروجی رویداد ShouldRender مساوی true باشد، اجازه‌ی اعمال تغییرات به UI داده خواهد شد و برعکس. بنابراین اگر حالت UI تغییر کند و خروجی این متد false باشد، این تغییرات نمایش داده نخواهند شد.
- اگر رویداد StateHasChanged فراخوانی شود، به معنای درخواست رندر مجدد UI است. کاربرد آن در مکان‌هایی است که نیاز به اطلاع رسانی دستی تغییرات UI وجود دارد؛ درست پس از زمانیکه رندر UI به پایان رسیده‌است. برای آزمایش این مورد و فراخوانی دستی StateHasChanged، کدهای تایمر زیر تهیه شده‌اند:
@page "/lifecycle"
@using System.Threading

button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount

@code
{
    int MaxCount = 5;

    void StartCountdown()
    {
        Console.WriteLine("Parnet - StartCountdown()");
        var timer = new Timer(TimeCallBack, null, 1000, 1000);
    }

    void TimeCallBack(object state)
    {
        if (MaxCount > 0)
        {
            MaxCount--;
            Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
            InvokeAsync(StateHasChanged);
        }
    }
}
تایمر تعریف شده، یک thread timer است. یعنی callback آن بر روی یک ترد جدید و مجزای از ترد UI اجرا می‌شود. در این حالت اگر StateHasChanged را جهت اطلاع رسانی تغییر حالت UI فراخوانی نکنیم، در حین کار تایمر، هیچ تغییری را در UI مشاهده نخواهیم کرد.


یک نکته: متدهای رویدادگردان در Blazor، می‌توانند sync و یا async باشند؛ مانند متدهای OnClick و OnClickAsync زیر که هر دو پس از پایان متدها، سبب فراخوانی ضمنی StateHasChanged نیز می‌شوند (به این دلیل است که با کلیک بر روی دکمه‌ای، UI هم به روز رسانی می‌شود). البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged می‌شوند؛ یکبار زمانیکه قسمت sync متد به پایان می‌رسد و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید:
<button @onclick="OnClick">Synchronous</button>
<button @onclick="OnClickAsync">Asynchronous</button>
@code{
    void OnClick()
    {
    } // StateHasChanged is called after the method

    async Task OnClickAsync()
    {
        text = "click1";
        // StateHasChanged is called here as the synchronous part of the method ends

        await Task.Delay(1000);
        await Task.Delay(2000);
        text = "click2";
    } // StateHasChanged is called after the method
}
بنابراین یکی دیگر از دلایل نیاز به فراخوانی صریح InvokeAsync(StateHasChanged) در callback تایمر تعریف شده، عدم فراخوانی خودکار آن، در پایان کار رویداد callback تایمر است.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-06.zip
مطالب دوره‌ها
طراحی روابط و ارجاعات در RavenDB
در قسمت‌های قبل، با پیش زمینه‌ی ذهنی طراحی مدل‌های RavenDB به همراه اصول مقدماتی کوئری نویسی آن آشنا شدیم. در این قسمت قصد داریم معادل‌های روابط موجود در بانک‌های اطلاعاتی رابطه‌ای را در RavenDB و مطابق ذهنیت غیر رابطه‌ای آن، مدلسازی کنیم و مثال‌های بیشتری را بررسی نمائیم.

مدیریت روابط در RavenDB

یکی از اصول طراحی مدل‌ها در RavenDB، مستقل بودن اسناد یا documents است. به این ترتیب کلیه اطلاعاتی که یک سند نیاز دارد، داخل همان سند ذخیره می‌شوند (به این نوع شیء،  Root Aggregate هم گفته می‌شود). اما این اصل سبب نخواهد شد تا نتوان یا نباید ارتباطی را بین اسناد تعریف کرد. بنابراین سؤال مهم اینجا است که چه اطلاعات مرتبطی باید داخل یک سند ذخیره شوند و چه اطلاعاتی باید به سند دیگری ارجاع داده شوند. برای پاسخ به این سؤال سه روش ذیل را باید مدنظر داشت:

الف) Denormalized references
فرض کنید در دنیای رابطه‌ای دو جدول سفارش و مشتری را دارید. در این حالت، جدول سفارش تنها شماره آی دی اطلاعات مشتری را از جدول مشتری یا کاربران سیستم، در خود ذخیره خواهد کرد. به این ترتیب از تکرار اطلاعات مشتری در جدول سفارشات جلوگیری می‌گردد. اما اگر اطلاعات پرکاربرد مشتری را در داخل جدول سفارش قرار دهیم به آن denormalized reference گفته می‌شود.
ایجاد denormalized reference یکی از روش‌های مرسوم در دنیای NoSQL و RavenDB است؛ خصوصا جهت سهولت نمایش اطلاعات. به این ترتیب ارجاع به سندهای دیگر کمتر شده و ترافیک شبکه نیز کاهش می‌یابد. برای مثال در اینجا نام و آدرس مشتری را داخل سند ثبت شده قرار می‌دهیم و از سایر اطلاعات او (که اهمیت نمایشی ندارند) مانند کلمه عبور و امثال آن صرفنظر خواهیم کرد.
اینجا است که یک سری از سؤالات مطرح خواهند شد مانند : «اگر آدرس مشتری تغییر کرد، چطور؟»
بنابراین بهترین حالت استفاده از روش denormalized references محدود خواهد شد به موارد ذیل:
الف) قید اطلاعاتی که به ندرت تغییر می‌کنند. برای مثال نام یک شخص یا نام یک کشور، استان یا شهر.
ب) ثبت اطلاعات تکراری که در طول زمان تغییر می‌کنند، اما باید تاریخچه‌ی آن‌ها حفظ شوند. برای مثال اگر آدرس مشتری تغییر کرده است، واقعا اجناس سندهای قبلی او، صرفنظر از آدرس جدیدی که اعلام کرده است، به آدرس قبلی او ارسال شده‌اند و این تاریخچه باید در سیستم حفظ شوند.
ج) اطلاعاتی که ممکن است بعدها حذف شوند؛ اما نیاز است سابقه اسناد قبلی تخریب نشوند. برای مثال کارخانه‌ای را درنظر بگیرید که امسال یک سری چینی خاص را تولید می‌کند و می‌فروشد. سال بعد خط تولید خود را عوض کرده و سری اجناس دیگری را شروع به تولید و فروش خواهد کرد. در بانک‌های اطلاعاتی رابطه‌ای نمی‌توان اجناسی را که در جداول دیگر ارجاع دارند، به این سادگی‌ها حذف کرد. در اینجا باید از روش‌هایی مانند تعریف فیلد بیتی IsDeleted برای مخفی کردن ظاهری رکوردهای موجود کمک گرفت. اما در دنیای رابطه‌ای، اطلاعات مهم محصول را در سند اصلی ثبت کنید. بعد هر زمانیکه نیازی به محصول نبود، کلا تعریف آن‌را حذف نمائید.


ب) Includes
Includes در RavenDB برای پوشش مشکلات denormalization ارائه شده است. در اینجا بجای اینکه یک شیء کپی اطلاعات پرکاربرد شیء‌ایی دیگر را در خود ذخیره کند، تنها ارجاعی (یک Id رشته‌ای) از آن شیء را در سند مرتبط ذخیره خواهد کرد.
public class Order
{
    public string CustomerId { get; set; }
    public LineItem[] LineItems { get; set; }
    public double TotalPrice { get; set; }
}
 
public class Customer
{
    public string Name { get; set; }
    public string Address { get; set; }
    public short Age { get; set; }
    public string HashedPassword { get; set; }
}
برای نمونه در کلاس Order شاهد یک Id رشته‌ای ارجاع دهنده به کلاس Customer هستیم. هرگاه که نیاز به بارگذاری اطلاعات شیء Order به همراه کل اطلاعات مشتری او تنها در یک رفت و برگشت به بانک اطلاعاتی باشد، می‌توان از متد الحاقی Include مختص RavenDB استفاده کرد:
var order = session.Include<Order>(x => x.CustomerId)
                   .Load("orders/1234");
 
// این کوئری از کش سشن خوانده می‌شود و کاری به سرور ندارد
var cust = session.Load<Customer>(order.CustomerId);
همانطور که مشاهده می‌کنید، با ذکر متد Include، اعلام کرده‌ایم که مایل هستیم تا اطلاعات سند مشتری متناظر را نیز داشته باشیم. در این حالت در Load بعدی که بر اساس Id مشتری انجام شده، دیگر رفت و برگشتی به سرور انجام نشده و اطلاعات مشتری از کش سشن جاری که پیشتر با فراخوانی Include مقدار دهی شده است، دریافت می‌گردد.
حتی می‌توان چند سند مرتبط را با هم بارگذاری کرد؛ با حداقل رفت و برگشت به سرور:
var orders = session.Include<Order>(x => x.CustomerId)
    .Load("orders/1234", "orders/4321");
 
foreach (var order in orders)
{
    // این کوئری‌ها سمت کلاینت هستند و به سرور ارسال نمی‌شوند
    var cust = session.Load<Customer>(order.CustomerId);
}
همچنین امکان استفاده از متد Include در LINQ API نیز پیش بینی شده است. برای این منظور باید از متد Customize استفاده کرد:
var orders = session.Query<Order>()
    .Customize(x => x.Include<Order>(o => o.CustomerId))
    .Where(x => x.TotalPrice > 100)
    .ToList();
 
foreach (var order in orders)
{
    // این کوئری‌ها سمت کلاینت اجرا می‌شوند
    var cust = session.Load<Customer>(order.CustomerId);
}


Includeهای یک به چند

اکنون فرض کنید به کلاس سفارش، آرایه تامین کننده‌ها نیز افزوده شده است (رابطه یک به چند):
public class Order
{
    public string CustomerId { get; set; }
    public string[] SupplierIds { get; set; }
    public LineItem[] LineItems { get; set; }
    public double TotalPrice { get; set; }
}
بارگذاری یکباره روابط یک به چند نیز با Include میسر است:
var orders = session.Include<Order>(x => x.SupplierIds)
    .Load("orders/1234", "orders/4321");
 
foreach (var order in orders)
{
    foreach (var supplierId in order.SupplierIds)
    {
        // از کش سشن خوانده می‌شود
        var supp = session.Load<Supplier>(supplierId);
    }
}



Includeهای چند سطحی

در اینجا کلاس سفارشی را در نظر بگیرید که دارای خاصیت ارجاع دهنده نیز هست. این خاصیت به شکل یک کلاس تعریف شده است و نه به شکل  یک آی دی رشته‌ای:
public class Order
{
    public string CustomerId { get; set; }
    public string[] SupplierIds { get; set; }
    public Referral Refferal { get; set; }
    public LineItem[] LineItems { get; set; }
    public double TotalPrice { get; set; }
}

public class Referral
{
    public string CustomerId { get; set; }
    public double CommissionPercentage { get; set; }
}
متد Include امکان ارجاع به خواص تو در تو را نیز دارد:
var order = session.Include<Order>(x => x.Refferal.CustomerId)
    .Load("orders/1234");
 
// از کش سشن خوانده می‌شود
var referrer = session.Load<Customer>(order.Refferal.CustomerId);
همچنین این متد با مجموعه‌ها نیز کار می‌کند. برای مثال اگر تعریف متد LineItem به صورت زیر باشد:
public class LineItem
{
    public string ProductId { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}
برای بارگذاری یکباره اسناد مرتبط می‌توان به روش ذیل عمل کرد:
var order = session.Include<Order>(x => x.LineItems.Select(li => li.ProductId))
    .Load("orders/1234");
 
foreach (var lineItem in order.LineItems)
{
    // از کش سمت کلاینت خوانده می‌شود
    var product = session.Load<Product>(lineItem.ProductId);
}

و به صورت خلاصه برای باگذاری اسناد مرتبط، دیگر از دو کوئری پشت سر هم ذیل استفاده نکنید:
var order = session.Load<Order>("orders/1");
var customer = session.Load<Customer>(order.CustomerId);
این دو کوئری یعنی دوبار رفت و برگشت به سرور. با استفاده از Include می‌توان تعداد رفت و برگشت‌ها و همچنین ترافیک شبکه را کاهش داد. به علاوه سرعت کار نیز افزایش خواهد یافت.


ج) تفاوت بین Reference و Relationship

برای درک اینکه آیا اطلاعات یک شیء مرتبط را بهتر است داخل شیء اصلی (Aggregate rooe) ذخیره کرد یا خیر، باید مفاهیم ارجاع و ارتباط را بررسی کنیم.
اگر به مثال سفارش و مشتری دقت کنیم، یک سفارش را بدون مشتری نیز می‌توان تکمیل کرد. برای مثال بسیاری از فروشگاه‌ها به همین نحو عمل می‌کنند و اگر شماره Id مشتری را به سندی اضافه می‌کنیم، صرفا جهت این است که بدانیم این سند متعلق به شخص دیگری نیست. بنابراین «ارجاعی» به کاربر در جدول سفارش می‌تواند وجود داشته باشد.
اکنون اقلام سفارش را درنظر بگیرید. هر آیتم سفارش تنها با بودن آن سفارش خاص است که معنا پیدا می‌کنند و نه بدون آن. این آیتم می‌تواند ارجاعی به محصول مرتبط داشته باشد. اینجا است که می‌گوییم اقلام سند با سفارش «در ارتباط» هستند؛ اما یک سند ارجاعی دارد به مشتری.
از این دو مفهوم برای تشخیص تشکیل Root Aggregate استفاده می‌شود. به این ترتیب تشخیص داده‌ایم اقلام سند، Root Aggregate را تشکیل می‌دهند؛ بنابراین ذخیره سازی تمام آن‌ها داخل یک سند RavenDB معنا پیدا می‌کند.


چند مثال برای درک بهتر نحوه طراحی اسناد در RavenDB

الف) Stackoverflow
صفحه نمایش یک سؤال و پاسخ‌های آن و همچنین رای‌های هر آیتم را درنظر بگیرید. در اینجا کاربران همزمانی ممکن است به یک سؤال رای بدهند، پاسخ‌هایی را ارائه دهند و یا کاربر اصلی، سؤال خویش را ویرایش کند. به این ترتیب با قرار دادن کلیه آیتم‌های این سند داخل آن، به مشکلات همزمانی برخواهیم خورد. برای مثال واقعا نمی‌خواهیم که به علت افزوده شدن یک پاسخ، کل سند قفل شود.
بنابراین ذخیره سازی سؤال در یک سند و ذخیره سازی لیست پاسخ‌ها در سندی دیگر، طراحی بهتری خواهد بود.

ب) سبد خرید و آیتم‌های آن
زمانیکه کاربری مشغول به خرید آنلاین از سایتی می‌شود، لیست اقلام انتخابی او یک سفارش را تشکیل داده و به تنهایی معنا پیدا نمی‌کنند. به همین جهت ذخیره سازی اقلام سفارش به صورت یک Root aggregate در اینجا مفهوم داشته و متداول است.

ج) یک بلاگ و کامنت‌های آن
در اینجا نیز کاربران، مجزای از مطلب اصلی ارسال شده ممکن است نظرات خود را ویرایش کنند یا اینکه بخواهیم نظرات را جداگانه لیست کنیم. بنابراین این دو (مطالب و نظرات) موضوعاتی جداگانه بوده و نیازی نیست به صورت یک Root aggregate تعریف شوند.

بنابراین در حین طراحی اسناد NoSQL باید به اعمال و «محدوده‌های تراکنشی» انجام شده دقت داشت تا اینکه صرفا عنوان شود این یک رابطه یک به چند یا چند به چند است.