مطالب
Vue.js - نحوه‌ی استفاده از list item ها و تخصیص item - قسمت چهارم
در این قسمت به بحث item list‌‌ها پرداخته می‌شود. روند کلی بدین صورت است که ابتدا یک آرایه را ایجاد کرده (با مقادیر مشخص) و سپس درون تگ مورد نظری از صفحه، آرایه را فراخوانی می‌کنیم.
برای این منظور ابتدا یک vue جدید را ساخته و سپس آرایه مورد نظر را تعریف کرده و آیتم‌های آرایه را مشخص می‌کنیم. به صورت زیر:
     var dotnettips = new Vue({
         el: '#dotnettips',
         data: {
            items: [
                  { message: 'one' },
                  { message: 'two' }
    ]
  }
});
در کد بالا یک ویژگی با نام dotnettips تعریف شده و مقادیری به آن اختصاص داده شده است.
حال زمان آن فرارسیده است تا بدنه اصلی ساختار صفحه را بنویسیم.
<html>
    <body>
    <ul id="dotnettips">
       <li v-for="item in items">
           {{ item.message }}
       </li>
    </ul>
  
    <script src="https://unpkg.com/vue@2.2.6">
    </script>

    <script type="text/javascript">
         var dotnettips = new Vue({
         el: '#dotnettips',
         data: {
            items: [
                  { message: 'one' },
                  { message: 'two' }
            ]
          }
        });
        </script>      
        </body>
</html>
برای نمایش خروجی کار به صورت دلخواه، ابتدا یک تگ ul و درون آن تگ li را ایجاد کردیم و سپس با استفاده از دستور v-for، اقدام به فراخوانی list item مورد نظر کردیم و سپس ویژگی مورد نظر را که همان item.message می‌باشد، درون علامت {{ }} قرار دادیم، تا خروجی نهایی به نمایش درآید.
 .حال با اجرای کد فوق تمامی آیتم‌های مورد نظر روی صفحه به نمایش در می‌آیند 
.دقت نمائید که کدهای فوق و بدنه اسکریپت به دلخواه نوشته شده‌است و شما طبق نیاز خود می‌توانید آن‌ها را تغییر دهید
نظرات مطالب
React 16x - قسمت 4 - کامپوننت‌ها - بخش 1 - کار با عبارات JSX
جهت اطلاع : در آخرین ورژن  (v.17) دیگر نیازی به import وابستگی React نیست. 

 :For example
import React from 'react';

function App() {
  return <h1>Hello World</h1>;
}
will be replaced with 
function App() {
  return <h1>Hello World</h1>;
}



  
مطالب
آپلود فایل‌ها توسط برنامه‌های 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 می‌شود.
مطالب
React 16x - قسمت 16 - مسیریابی - بخش 2 - پارامترهای مسیریابی
در قسمت قبل، نحوه‌ی برپایی و تنظیمات اولیه‌ی کتابخانه‌ی مسیریابی react-router-dom را بررسی کردیم. در ادامه نحوه‌ی دریافت پارامترهای مسیریابی و سایر جزئیات این کتابخانه را مرور می‌کنیم.


دریافت پارامترهای مسیریابی

گاهی از اوقات نیاز به ارسال پارامترهایی به مسیریابی‌های تعریف شده‌است. برای مثال در لیست محصولات تعریف شده، بسته به انتخاب هر کدام، باید id متناظری نیز در URL نهایی ظاهر شود. به این Id، یک Route parameter گفته می‌شود. برای پیاده سازی این نیازمندی به صورت زیر عمل می‌کنیم:
در فایل app.js، یک مسیریابی جدید را برای نمایش جزئیات یک محصول اضافه می‌کنیم:
import ProductDetails from "./components/productDetails";
// ...
class App extends Component {
  render() {
    return (
      <div>
        <NavBar />
        <div className="container">
          <Switch>
            <Route path="/products/:id" component={ProductDetails} />
            <Route
              path="/products"
              render={props => (
                <Products param1="123" param2="456" {...props} />
              )}
            />
            <Route path="/posts/:year/:month" component={Posts} />
            <Route path="/admin" component={Dashboard} />
            <Route path="/" component={Home} />
          </Switch>
        </div>
      </div>
    );
  }
}
در اینجا برای تعریف یک پارامتر مسیریابی، آن‌را با : شروع می‌کنیم؛ مانند id:، در مسیریابی جدید فوق. البته امکان تعریف چندین پارامتر هم در اینجا وجود دارد؛ مانند تعریف پارامترهای سال و ماه برای مسیریابی مطالب. به علاوه چون این نوع مسیریابی‌ها ویژه‌تر هستند، باید در ابتدا قرارگیرند. برای مثال اگر مسیریابی products/ را در اول لیست قرار دهیم، دیگر کار به انتخاب products/:id/ نخواهد رسید.

کامپوننت جدید src\components\productDetails.jsx نیز به صورت زیر تعریف شده‌است:
import React, { Component } from "react";

class ProductDetails extends Component {
  handleSave = () => {
    // Navigate to /products
  };

  render() {
    return (
      <div>
        <h1>Product Details - </h1>
        <button className="btn btn-primary" onClick={this.handleSave}>
      </div>
    );
  }
}

export default ProductDetails;
پس از این تغییرات و ذخیره سازی برنامه، با بارگذاری مجدد برنامه در مرورگر، ابتدا صفحه‌ی products را از منوی راهبری سایت انتخاب کرده و سپس بر روی یکی از محصولات لیست شده کلیک می‌کنیم. سپس در افزونه‌ی react developer tools، کامپوننت نمایش داده شده‌ی ProductDetails را انتخاب می‌کنیم:


در اینجا با گشودن اطلاعات خاصیت match تزریق شده‌ی به کامپوننت ProductDetails، می‌توان اطلاعاتی مانند پارامترهای دریافتی مسیریابی را دقیقا مشاهده کرد. برای مثال در این تصویر id=1 از URL بالای صفحه که به http://localhost:3000/products/1 تنظیم شده‌است، دریافت می‌شود.
بنابراین امکان خواندن اطلاعات پارامترهای مسیریابی، توسط شیء match تزریق شده‌ی به یک کامپوننت وجود دارد. به همین جهت کامپوننت ProductDetails را ویرایش کرده و المان h1 آن‌را جهت نمایش id محصول به صورت زیر تغییر می‌دهیم که در آن شیء match.params، از props کامپوننت تامین می‌شود:
<h1>Product Details - {this.props.match.params.id} </h1>

برای آزمایش آن مجددا از صفحه‌ی products شروع کرده و بر روی لینک یکی از محصولات، کلیک کنید. در اینجا هرچند id محصول به درستی نمایش داده می‌شود، اما ... نمایش جزئیات آن به همراه بارگذاری کامل و مجدد صفحه‌ی آن است که از حالت SPA خارج شده‌است. برای رفع این مشکل به کامپوننت products مراجعه کرده و anchor‌های تعریف شده را همانطور که در قسمت قبل نیز بررسی کردیم، تبدیل به کامپوننت Link می‌کنیم.
از حالت قبلی:
{this.state.products.map(product => (
  <li key={product.id}>
    <a href={`/products/${product.id}`}>{product.name}</a>
  </li>
))}
به حالت جدید:
import { Link } from "react-router-dom";
// ...

<Link to={`/products/${product.id}`}>{product.name}</Link>
با این تغییر دیگر در حین نمایش یک کامپوننت، بارگذاری کامل صفحه رخ نمی‌دهد.


پارامترهای اختیاری مسیریابی

به تعریف مسیریابی زیر، دو پارامتر سال و ماه، اضافه شده‌اند:
<Route path="/posts/:year/:month" component={Posts} />
و برای مثال اگر بر روی لینک posts در منوی راهبری کلیک کنیم، آدرسی مانند http://localhost:3000/posts/2018/06 ایجاد شده و سپس کامپوننت Posts رندر می‌شود. حال اگر پارامتر ماه را حذف کنیم http://localhost:3000/posts/2018 چه اتفاقی رخ می‌دهد؟ در این حالت برنامه کامپوننت Home را نمایش خواهد داد. علت اینجا است که پارامترهای تعریف شده‌ی در مسیریابی، به صورت پیش‌فرض اجباری هستند. به همین جهت URL وارد شده، چون با الگوی تعریفی Route فوق بدلیل نداشتن قسمت ماه، انطباق نیافته و تنها مسیریابی / که کامپوننت Home را نمایش می‌دهد، با آن تطابق یافته‌است.
برای رفع این مشکل می‌توان با اضافه کردن یک ? به هر پارامتر، پارامترهای تعریف شده را اختیاری کرد:
<Route path="/posts/:year?/:month?" component={Posts} />
در regexهای جاوا اسکریپتی زمانیکه یک ? را به یک عبارت باقاعده اضافه می‌کنیم، یعنی آن عبارت اختیاری است.

با این تغییرات اگر مجددا آدرس http://localhost:3000/posts/2018 را درخواست کنیم، کامپوننت Posts بجای کامپوننت Home نمایش داده می‌شود.

اکنون کامپوننت Posts را به صورت زیر تغییر می‌دهیم تا پارامترهای مسیریابی را نیز درج کند:
import React from "react";

const Posts = ({ match }) => {
  return (
    <div>
      <h1>Posts</h1>
      Year: {match.params.year} , Month: {match.params.month}
    </div>
  );
};

export default Posts;
پارامتر ({ match }) در اینجا به این معنا است که شیء props ارسالی به آن، توسط Object Destructuring تجزیه شده و خاصیت match آن در اینجا به صورت یک پارامتر در اختیار کامپوننت بدون حالت تابعی قرار گرفته‌است.

پس از ذخیره سازی این تغییرات و بارگذاری مجدد برنامه در مرورگر، اگر آدرس http://localhost:3000/posts/2018/1 را وارد کنیم، خروجی زیر حاصل می‌شود:



کار با پارامترهای کوئری استرینگ‌های مسیریابی

پارامترهای اختیاری، جزو قابلیت‌هایی هستند که باید تا حد ممکن از بکارگیری آن‌ها اجتناب و آن‌ها را با کوئری استرینگ‌ها تعریف کرد. کوئری استرینگ‌ها با یک ? در انتهای URL شروع می‌شوند و می‌توانند چندین پارامتر را داشته باشند؛ مانند: http://localhost:3000/posts?sortBy=newest&approved=true و یا حتی می‌توان آن‌ها را با پارامترهای اختیاری نیز ترکیب کرد مانند: http://localhost:3000/posts/2018/05?sortBy=newest&approved=true
برای استخراج کوئری استرینگ‌ها در برنامه‌های React باید از شیء location استفاده کرد:


در اینجا مقدار خاصیت search، کل قسمت کوئری استرینگ‌ها را به همراه دارد. البته ما قصد پردازش آن‌را به صورت دستی نداریم. به همین جهت از کتابخانه‌ی زیر برای انجام اینکار استفاده خواهیم کرد:
> npm i query-string --save
پس از نصب کتابخانه‌ی بسیار معروف query-string، به کامپوننت Posts مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
import queryString from "query-string";

const Posts = ({ match, location }) => {
  const result = queryString.parse(location.search);
  console.log(result);
  // ...
- پیشتر ذکر پارامتر ({ match }) را بررسی کردیم. در اینجا خاصیت location نیز به آن اضافه شده‌است تا پس از Object Destructuring شیء props ارسالی به کامپوننت، بتوان به مقدار شیء location نیز دسترسی یافت.
- سپس شیء queryString را از ماژول مرتبط، import می‌کنیم. در ادامه به کمک متد parse آن، می‌توان location.search را آنالیز کرد که خروجی آن، یک شیء جاوا اسکریپتی به صورت زیر است:
{approved: "true", sortBy: "newest"}
بنابراین در اینجا هم می‌توان توسط Object Destructuring، به این خواص دسترسی یافت:
 const { approved, sortBy } = queryString.parse(location.search);

یک نکته: باید دقت داشت که کتابخانه‌ی query-string، همیشه مقادیر خواص را رشته‌ای بازگشت می‌دهد؛ حتی اگر عدد باشند.


مدیریت مسیرهای نامعتبر درخواستی

فرض کنید کاربری آدرس غیرمعتبر http://localhost:3000/xyz را که هیچ نوع مسیریابی را برای آن تعریف نکرده‌ایم، درخواست می‌کند. در این حالت برنامه کامپوننت home را رندر می‌کند که مرتبط است با تعاریف مسیریابی برنامه در فایل app.js. تنها path تعریف شده‌ای که با این آدرس تطابق پیدا می‌کند، / متناظر با کامپوننت home است.
بجای این رفتار پیش‌فرض، مایل هستیم که کاربر به یک صفحه‌ی سفارشی «پیدا نشد» هدایت شود. به همین جهت ابتدا کامپوننت جدید تابعی بدون حالت src\components\notFound.jsx را با محتوای زیر ایجاد می‌کنیم:
import React from "react";

const NotFound = () => {
  return <h1>Not Found</h1>;
};

export default NotFound;
سپس ابتدا به مسیریابی /، ویژگی exact را هم اضافه می‌کنیم تا دیگر بجز ریشه‌ی سایت، به مسیر دیگری پاسخ ندهد:
<Route path="/" exact component={Home} />
اکنون اگر مجددا مسیر xyz را درخواست کنیم، فقط کامپوننت NavBar در صفحه ظاهر می‌شود. برای بهبود این وضعیت و نمایش کامپوننت NotFound، مراحل زیر را طی می‌کنیم:
- ابتدا شیء Redirect را از react-router-dom باید import کنیم.
- همچنین import کامپوننت NotFound نیز باید ذکر شود.
- سپس پیش از مسیریابی کلی /، مسیریابی جدید not-found را که به کامپوننت NotFound اشاره می‌کند، اضافه می‌کنیم.
- اکنون در انتهای Switch تعریف شده (جائی که دیگر هیچ مسیریابی تعریف شده‌ای، با مسیر درخواستی کاربر، تطابق نداشته)، باید کامپوننت Redirect را جهت هدایت به این مسیریابی جدید، تعریف کرد:
import { Redirect, Route, Switch } from "react-router-dom";
//...
import NotFound from "./components/notFound";
//...

class App extends Component {
  render() {
    return (
      <div>
        <NavBar />
        <div className="container">
          <Switch>
            //...
            <Route path="/not-found" component={NotFound} />
            <Route path="/" exact component={Home} />
            <Redirect to="/not-found" />
          </Switch>
        </div>
      </div>
    );
  }
}
پس از این تغییرات، اگر آدرس نامعتبر http://localhost:3000/xyz درخواست شود، بلافاصله به آدرس http://localhost:3000/not-found هدایت می‌شویم.

کاربرد دیگر کامپوننت Redirect، هدایت کاربران از یک آدرس قدیمی، به یک آدرس جدید است که نحوه‌ی تعریف آن به صورت زیر می‌باشد:
<Redirect from="/messages" to="/posts" />
با این تنظیم اگر کاربری مسیر http://localhost:3000/messages را درخواست دهد، به صورت خودکار به http://localhost:3000/posts هدایت خواهد شد.


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

گاهی از اوقات پس از تکمیل فرمی و یا کلیک بر روی دکمه‌ای، می‌خواهیم کاربر را به آدرس خاصی هدایت کنیم. برای مثال در برنامه‌ی جاری می‌خواهیم زمانیکه کاربری صفحه‌ی جزئیات یک محصول را مشاهده و بر روی دکمه‌ی فرضی Save کلیک کرد، دوباره به همان صفحه‌ی لیست محصولات هدایت شود؛ برای این منظور از شیء history استفاده خواهیم کرد:


در اینجا متدها و خواص شیء history را مشاهده می‌کنید. برای نمونه توسط متد push آن می‌توان آدرس جدیدی را به تاریخچه‌ی آدرس‌های مرور شده‌ی توسط کاربر، اضافه کرد و کاربر را با برنامه نویسی، به صفحه‌ی جدیدی هدایت نمود:
class ProductDetails extends Component {
  handleSave = () => {
    // Navigate to /products
    this.props.history.push("/products");
  };

یک نکته: اگر به تصویر دقت کنید، متد replace هم در اینجا قابل استفاده است. متد push با افزودن رکوردی به تاریخچه‌ی آدرس‌های مرور شده‌ی در مرورگر، امکان بازگشت به محل قبلی را با کلیک بر روی دکمه‌ی back مرورگر، فراهم می‌کند؛ اما replace تنها رکورد آدرس جاری را در تاریخچه‌ی مرورگر به روز رسانی می‌کند. یعنی از داشتن تاریخچه محروم خواهیم شد. عمده‌ی کاربرد این متد، در صفحات لاگین است؛ زمانیکه کاربر به سیستم وارد می‌شود و به صفحه‌ی جدیدی مراجعه می‌کند، با کلیک بر روی دکمه‌ی back، دوباره نمی‌خواهیم او را به صفحه‌ی لاگین هدایت کنیم.


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


قصد داریم به صفحه‌ی admin، دو لینک جدید به مطالب و کاربران ادمین را اضافه کنیم، به نحوی که با کلیک بر روی هر کدام، محتوای هر صفحه‌ی متناظر، در همینجا نمایش داده شود (تصویر فوق). به عبارتی در حال حاضر، مسیریابی تعریف شده، جهت مدیریت لینک‌های NavBar بالای صفحه کار می‌کند. اکنون می‌خواهیم مسیریابی دیگری را داخل آن برای پوشش منوی کنار صفحه‌ی ادمین اضافه کنیم. به اینکار، تعریف مسیریابی‌های تو در تو گفته می‌شود و پیاده سازی آن توسط کامپوننت react-router-dom بسیار ساده‌است و شامل این موارد می‌شود:
- ابتدا مسیریابی‌های جدید را همینجا داخل کامپوننت src\components\admin\dashboard.jsx تعریف می‌کنیم:
const Dashboard = ({ match }) => {
  return (
    <div>
      <h1>Admin Dashboard</h1>
      <div className="row">
        <div className="col-3">
          <SideBar />
        </div>
        <div className="col">
          <Route path="/admin/users" component={Users} />
          <Route path="/admin/posts" component={Posts} />
        </div>
      </div>
    </div>
  );
};
در اینجا محتوای کامپوننت بدون حالت تابعی Dashboard را ملاحظه می‌کنید که از یک کامپوننت منوی SideBar و سپس در ستونی دیگر، از 2 کامپوننت Route تشکیل شده‌است که بر اساس URL رسیده، سبب رندر کامپوننت‌های جدید Users و Posts خواهند شد.
تنها نکته‌ی جدید آن، امکان درج کامپوننت Route در قسمت‌های مختلف برنامه، خارج از app.js می‌باشد و این امکان محدود به app.js نیست. در این حالت اگر مسیر /admin/posts توسط کاربر وارد شد، دقیقا در همان محلی که کامپوننت Route درج شده‌است، کامپوننت متناظر با این مسیر یعنی کامپوننت Posts، رندر می‌شود.

در ادامه محتوای این کامپوننت‌های جدید را نیز ملاحظه می‌کنید:
محتوای کامپوننت src\components\admin\sidebar.jsx
import React from "react";
import { Link } from "react-router-dom";

const SideBar = () => {
  return (
    <ul className="list-group">
      <li className="list-group-item">
        <Link to="/admin/posts">Posts</Link>
      </li>
      <li className="list-group-item">
        <Link to="/admin/users">Users</Link>
      </li>
    </ul>
  );
};

export default SideBar;

محتوای کامپوننت src\components\admin\users.jsx
import React from "react";

const Users = () => {
  return <h1>Admin Users</h1>;
};

export default Users;

محتوای کامپوننت src\components\admin\posts.jsx
import React from "react";

const Posts = () => {
  return (
    <div>
      <h1>Admin Posts</h1>
    </div>
  );
};

export default Posts;


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-15-part-02.zip
نظرات مطالب
استفاده از Froala WYSIWYG Editor در ASP.NET
بر اساس این باید از this به جای editor استفاده کنیم.
من به این صورت استفاده کردم 
 insertHTML: {
                                title: 'Insert Code',
                                icon: {
                                    type: 'font',
                                    value: 'fa fa-dollar' // Font Awesome icon class fa fa-*
                                },
                                callback: function (editor) {
                                    this.saveSelection();
                                    var thisEditor = this;
                                    var codeModal = $("<div>").addClass("froala-modal").appendTo("body");
                                    var wrapper = $("<div>").addClass("f-modal-wrapper").appendTo(codeModal);
                                    $("<h4>").append('<span data-text="true">Insert Code</span>')
                                        .append($('<i class="fa fa-times" title="Cancel">')
                                            .click(function () {
                                                codeModal.remove();
                                            }))
                                        .appendTo(wrapper);
 
                                    var dialog = "<textarea id='code_area' style='height: 211px; width: 538px;' /><label>Language:</label><select id='code_lang'><option>CSharp</option><option>VB</option><option>JScript</option><option>Sql</option><option>XML</option><option>CSS</option><option>Java</option><option>Delphi</option></select> <input type='button' name='insert' id='insert_btn' value='Insert' />";
                                    $(dialog).appendTo(wrapper);
 
                                    $("#code_area").text(this.text());
 
                                    if (!this.selectionInEditor()) {
                                        this.$element.focus();
                                    }
 
                                    $('#insert_btn').click(function () {
                                        var lang = $("#code_lang").val();
                                        var code = $("#code_area").val();
                                        code = code.replace(/\s+$/, ""); // rtrim
                                        code = $('<span/>').text(code).html(); // encode   
                                        var htmlCode = "<pre class='brush: " + lang.toLowerCase() + "' language='" + lang + "' name='code'>" + code + "</pre></div>"; // syntaxhighlighter با این کد هماهنگ است
                                        //var htmlCode = "<pre language='" + lang + "' name='code'>" + code + "</pre></div>";
                                        var codeBlock = "<div align='left' dir='ltr'>" + htmlCode + "</div>";
 
                                        thisEditor.restoreSelection();
                                        thisEditor.insertHTML(codeBlock);
                                        thisEditor.saveUndoStep();
 
                                        codeModal.remove();
                                    });
                                }
                            }

مطالب
Angular Material 6x - قسمت چهارم - نمایش پویای اطلاعات تماس‌ها
در قسمت قبل، یک لیست ثابت item 1/item 2/… را در sidenav نمایش دادیم. در این قسمت می‌خواهیم این لیست را با اطلاعات دریافت شده‌ی از سرور، پویا کنیم و همچنین با کلیک بر روی هر کدام، جزئیات آن‌ها را نیز در قسمت main-content نمایش دهیم.



تهیه سرویس اطلاعات پویای برنامه

سرویس Web API ارائه شده‌ی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشت‌های آن‌ها به سمت کلاینت باز می‌گرداند و ساختار موجودیت‌های آن‌ها به صورت زیر است:

موجودیت کاربر که یک رابطه‌ی one-to-many را با UserNotes دارد:
using System;
using System.Collections.Generic;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class User
    {
        public User()
        {
            UserNotes = new HashSet<UserNote>();
        }

        public int Id { set; get; }
        public DateTimeOffset BirthDate { set; get; }
        public string Name { set; get; }
        public string Avatar { set; get; }
        public string Bio { set; get; }

        public ICollection<UserNote> UserNotes { set; get; }
    }
}
و موجودیت یادداشت‌های کاربر که سر دیگر رابطه را تشکیل می‌دهد:
using System;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class UserNote
    {
        public int Id { set; get; }
        public DateTimeOffset Date { set; get; }
        public string Title { set; get; }

        public User User { set; get; }
        public int UserId { set; get; }
    }
}
در نهایت اطلاعات ذخیره شده‌ی در بانک اطلاعاتی توسط سرویس کاربران:
    public interface IUsersService
    {
        Task<List<User>> GetAllUsersIncludeNotesAsync();
        Task<User> GetUserIncludeNotesAsync(int id);
    }
در اختیار کنترلر Web API برنامه، برای ارائه‌ی به سمت کلاینت، قرار می‌گیرد:
namespace MaterialAspNetCoreBackend.WebApp.Controllers
{
    [Route("api/[controller]")]
    public class UsersController : Controller
    {
        private readonly IUsersService _usersService;

        public UsersController(IUsersService usersService)
        {
            _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            return Ok(await _usersService.GetAllUsersIncludeNotesAsync());
        }

        [HttpGet("{id:int}")]
        public async Task<IActionResult> Get(int id)
        {
            return Ok(await _usersService.GetUserIncludeNotesAsync(id));
        }
    }
}
کدهای کامل لایه سرویس، تنظیمات EF Core و تنظیمات ASP.NET Core این قسمت را از پروژه‌ی پیوستی انتهای بحث می‌توانید دریافت کنید.
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
 https://localhost:5001/api/users
یک چنین خروجی قابل مشاهده خواهد بود:


و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت می‌دهد.


تنظیم محل تولید خروجی Angular CLI

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


همانطور که ملاحظه می‌کنید، کلاینت Angular در یک پوشه‌است و برنامه‌ی سمت سرور ASP.NET Core در پوشه‌ای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشه‌ی wwwroot پروژه‌ی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش می‌کنیم:
"build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
تنظیم این outputPath به wwwroot پروژه‌ی وب سبب خواهد شد تا با صدور فرمان زیر:
 ng build --no-delete-output-path --watch
برنامه‌ی Angular در حالت watch (گوش فرا دادان به تغییرات فایل‌ها) کامپایل شده و سپس به صورت خودکار در پوشه‌ی MaterialAspNetCoreBackend.WebApp/wwwroot کپی شود. به این ترتیب پس از اجرای برنامه‌ی ASP.NET Core توسط دستور زیر:
 dotnet watch run
 این برنامه‌ی سمت سرور، در همان لحظه هم API خود را ارائه خواهد داد و هم هاست برنامه‌ی Angular می‌شود.
بنابراین دو صفحه‌ی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشه‌ی MaterialAngularClient) و در دومی dotnet watch run را در پوشه‌ی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا می‌شوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژه‌ها ایجاد کردید، صرفا همان قسمت کامپایل می‌شود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.


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

در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادل‌های کلاس‌های موجودیت‌های سمت سرور خود را به صورت اینترفیس‌هایی تایپ‌اسکریپتی تعریف کنیم:
ng g i contact-manager/models/user
ng g i contact-manager/models/user-note
این دستورات دو اینترفیس خالی کاربر و یادداشت‌های او را در پوشه‌ی جدید models ایجاد می‌کنند. سپس آن‌ها را به صورت زیر و بر اساس تعاریف سمت سرور آن‌ها، تکمیل می‌کنیم:
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote {
  id: number;
  title: string;
  date: Date;
  userId: number;
}
محتویات فایل contact-manager\models\user.ts :
import { UserNote } from "./user-note";

export interface User {
  id: number;
  birthDate: Date;
  name: string;
  avatar: string;
  bio: string;

  userNotes: UserNote[];
}


ایجاد سرویس Angular دریافت اطلاعات از سرور

ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد می‌کنیم:
 ng g s contact-manager/services/user --no-spec
که سبب ایجاد فایل user.service.ts در پوشه‌ی جدید services خواهد شد:
import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor() { }
}
قسمت providedIn آن مخصوص Angular 6x است و هدف از آن کم حجم‌تر کردن خروجی نهایی برنامه‌است؛ اگر از سرویسی که تعریف شده، در برنامه جائی استفاده نشده‌است. به این ترتیب دیگر نیازی نیست تا آن‌را به صورت دستی در قسمت providers ماژول جاری ثبت و معرفی کرد.
کدهای تکمیل شده‌ی UserService را در ذیل مشاهده می‌کنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";

import { User } from "../models/user";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor(private http: HttpClient) { }

  getAllUsersIncludeNotes(): Observable<User[]> {
    return this.http
      .get<User[]>("/api/users").pipe(
        map(response => response || []),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }

  getUserIncludeNotes(id: number): Observable<User> {
    return this.http
      .get<User>(`/api/users/${id}`).pipe(
        map(response => response || {} as User),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }
}
در اینجا از pipe-able operators مخصوص RxJS 6x استفاده شده که در مطلب «ارتقاء به Angular 6: بررسی تغییرات RxJS» بیشتر در مورد آن‌ها بحث شده‌است.
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشت‌های آن‌ها از سرور واکشی می‌کند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشت‌های او از سرور دریافت می‌کند.


بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material

بسته‌ی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوه‌ی «افزودن آیکن‌های متریال به برنامه» را بررسی کردیم که در آنجا آیکن‌های مرتبط، از فایل‌های قلم، دریافت و نمایش داده می‌شوند. این کامپوننت، علاوه بر قلم آیکن‌ها، از فایل‌های svg حاوی آیکن‌ها نیز پشتیبانی می‌کند که یک نمونه از این فایل‌ها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شده‌است (چون برنامه‌ی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
    <svg viewBox="0 0 128 128" height="100%" width="100%" 
             pointer-events="none" display="block" id="user1" >
هر svg تعریف شده‌ی در آن دارای یک id است. از این id به عنوان نام avatar کاربرها استفاده خواهیم کرد. نحوه‌ی فعالسازی آن نیز به صورت زیر است:
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماس‌ها را با صورت زیر تکمیل می‌کنیم:
import { Component } from "@angular/core";
import { MatIconRegistry } from "@angular/material";
import { DomSanitizer } from "@angular/platform-browser";

@Component()
export class ContactManagerAppComponent {

  constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
    iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg"));
  }
  
}
MatIconRegistry جزئی از بسته‌ی angular/material است که در ابتدای کار import شده‌است. متد addSvgIconSet آن، مسیر یک فایل svg حاوی آیکن‌های مختلف را دریافت می‌کند. این مسیر نیز باید توسط سرویس DomSanitizer در اختیار آن قرار گیرد که در کدهای فوق روش انجام آن‌را ملاحظه می‌کنید. در مورد سرویس DomSanitizer در مطلب «نمایش HTML در برنامه‌های Angular» بیشتر بحث شده‌است.
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon(
      "unicorn",
      this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg")
    );
که در نهایت کامپوننت mat-icon، این آیکن را به صورت زیر می‌تواند نمایش دهد:
 <mat-icon svgIcon="unicorn"></mat-icon>

یک نکته: پوشه‌ی node_modules\material-design-icons به همراه تعداد قابل ملاحظه‌ای فایل svg نیز هست.


نمایش لیست کاربران در sidenav

در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق می‌کنیم:
import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component()
export class SidenavComponent implements OnInit {

  users: User[] = [];

  constructor(private userService: UserService) {  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => this.users = data);
  }
}
به این ترتیب با اجرای برنامه و بارگذاری sidenav، در رخ‌داد OnInit آن، کار دریافت اطلاعات کاربران و انتساب آن به خاصیت عمومی users صورت می‌گیرد.

اکنون می‌خواهیم از این اطلاعات جهت نمایش پویای آن‌ها در sidenav استفاده کنیم. در قسمت قبل، جای آن‌ها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
    <mat-list>
      <mat-list-item>Item 1</mat-list-item>
      <mat-list-item>Item 2</mat-list-item>
      <mat-list-item>Item 3</mat-list-item>
    </mat-list>
اگر به مستندات mat-list مراجعه کنیم، در میانه‌ی صفحه، navigation lists نیز ذکر شده‌است که می‌تواند لیستی پویا را به همراه لینک به آیتم‌های آن نمایش دهد و این مورد دقیقا کامپوننتی است که در اینجا به آن نیاز داریم. بنابراین فایل sidenav\sidenav.component.html را گشوده و mat-list فوق را با mat-nav-list تعویض می‌کنیم:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine href="#">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
اکنون اگر برنامه را اجرا کنیم، یک چنین شکلی قابل مشاهده است:


که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آن‌ها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.


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

در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شده‌است. در ادامه می‌خواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine [routerLink]="['/contactmanager', user.id]">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
در اینجا با استفاده از routerLink، هر کاربر را بر اساس Id او، به صفحه‌ی جزئیات آن شخص، متصل کرده‌ایم. البته این مسیریابی برای اینکه کار کند باید به صورت زیر به فایل contact-manager-routing.module.ts اضافه شود:
const routes: Routes = [
  {
    path: "", component: ContactManagerAppComponent,
    children: [
      { path: ":id", component: MainContentComponent },
      { path: "", component: MainContentComponent }
    ]
  },
  { path: "**", redirectTo: "" }
];
البته اگر تا اینجا برنامه را اجرا کنید، با نزدیک کردن اشاره‌گر ماوس به نام هر کاربر، آدرسی مانند https://localhost:5001/contactmanager/1 در status bar مرورگر ظاهر خواهد شد، اما با کلیک بر روی آن، اتفاقی رخ نمی‌دهد.
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کرده‌ایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل می‌کنیم:
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component({
  selector: "app-main-content",
  templateUrl: "./main-content.component.html",
  styleUrls: ["./main-content.component.css"]
})
export class MainContentComponent implements OnInit {

  user: User;
  constructor(private route: ActivatedRoute, private userService: UserService) { }

  ngOnInit() {
    this.route.params.subscribe(params => {
      this.user = null;

      const id = params["id"];
      if (!id) {
        return;
      }

      this.userService.getUserIncludeNotes(id)
        .subscribe(data => this.user = data);
    });
  }
}
در اینجا به کمک سرویس ActivatedRoute و گوش فرادادن به تغییرات params آن در ngOnInit، مقدار id مسیر دریافت می‌شود. سپس بر اساس این id، با کمک سرویس کاربران، اطلاعات این تک کاربر از سرور دریافت و به خاصیت عمومی user نسبت داده خواهد شد.
اکنون می‌توان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user">
  <mat-spinner></mat-spinner>
</div>
<div *ngIf="user">
  <mat-card>
    <mat-card-header>
      <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon>
      <mat-card-title>
        <h2>{{ user.name }}</h2>
      </mat-card-title>
      <mat-card-subtitle>
        Birthday {{ user.birthDate | date:'d LLLL' }}
      </mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <mat-tab-group>
        <mat-tab label="Bio">
          <p>
            {{user.bio}}
          </p>
        </mat-tab>
        <!-- <mat-tab label="Notes"></mat-tab> -->
      </mat-tab-group>
    </mat-card-content>
  </mat-card>
</div>
در اینجا از کامپوننت mat-spinner برای نمایش حالت منتظر بمانید استفاده کرده‌ایم. اگر user نال باشد، این spinner نمایش داده می‌شود و برعکس.


همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کرده‌ایم (اگر دقت کنید، هر کامپوننت آن سه برگه‌ی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره می‌توان با کامپوننت‌های این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا می‌کنیم که مناسب کار ما باشد. سپس سورس آن‌را از همانجا کپی و در برنامه قرار می‌دهیم و در آخر آن‌را بر اساس اطلاعات خود سفارشی سازی می‌کنیم.



نمایش جزئیات اولین کاربر در حین بارگذاری اولیه‌ی برنامه

تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماس‌ها ظاهر می‌شود و همانطور باقی می‌ماند، با اینکه هنوز موردی انتخاب نشده‌است. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظه‌ی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش می‌دهیم:
import { Router } from "@angular/router";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  users: User[] = [];
  
  constructor(private userService: UserService, private router: Router) {
  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => {
        this.users = data;
        if (data && data.length > 0 && !this.router.navigated) {
          this.router.navigate(["/contactmanager", data[0].id]);
        }
      });
  }
}
در اینجا ابتدا سرویس Router به سازنده‌ی کلاس تزریق شده‌است و سپس زمانیکه کار دریافت اطلاعات تماس‌ها پایان یافت و this.router.navigated نبود (یعنی پیشتر هدایت به آدرسی صورت نگرفته بود؛ برای مثال کاربر آدرس id داری را ریفرش نکرده بود)، اولین مورد را توسط متد this.router.navigate فعال می‌کنیم که سبب تغییر آدرس صفحه از https://localhost:5001/contactmanager به https://localhost:5001/contactmanager/1 و باعث نمایش جزئیات آن می‌شود.

البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id'];
if (!id) id = 1;
در اینجا اگر id انتخاب نشده باشد، یعنی اولین بار نمایش برنامه است و خودمان id مساوی 1 را برای آن در نظر می‌گیریم.


بستن خودکار sidenav در حالت نمایش موبایل

اگر اندازه‌ی صفحه‌ی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتم‌های آن‌را انتخاب کنیم، هرچند آن‌ها نمایش داده می‌شوند، اما زیر این sidenav مخفی باقی خواهند ماند:


بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینه‌های آن، این قسمت بسته شده و ناحیه‌ی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شده‌است:
<mat-sidenav #sidenav
برای دسترسی به آن در کدهای کامپوننت خواهیم داشت:
import { MatSidenav } from "@angular/material";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  @ViewChild(MatSidenav) sidenav: MatSidenav;
اکنون که به این ViewChild دسترسی داریم، می‌توانیم در حالت نمایشی موبایل، متد close آن‌را فراخوانی کنیم:
  ngOnInit() {
    this.router.events.subscribe(() => {
      if (this.isScreenSmall) {
        this.sidenav.close();
      }
    });
  }
در اینجا با مشترک this.router.events شدن، متوجه‌ی کلیک کاربر و نمایش صفحه‌ی جزئیات آن می‌شویم. در قسمت سوم این مجموعه نیز خاصیت isScreenSmall را بر اساس ObservableMedia مقدار دهی کردیم. بنابراین اگر کاربر بر روی گزینه‌ای کلیک کرده بود و همچنین اندازه‌ی صفحه در حالت موبایل قرار داشت، sidenav را خواهیم بست تا بتوان محتوای زیر آن‌را مشاهده کرد:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
مطالب
بخش سوم - بررسی امکانات و کدنویسی در کامپایلر Svelte
در بخش‌های قبل تا حدودی با کامپایلر Svelte و ساختار فایل‌های آن آشنا شدیم. در این بخش با چند مثال قصد دارم امکاناتی را که Svelte در اختیارمان قرار میدهد، شرح دهم. 

Dynamic attributes : 
در بخش قبل دیدیم که با استفاده از علامت ( آکولاد {} ) میتوانیم مقادیر موجود در تگ اسکریپت را در html خود رندر کنیم. ولی از این علامت میتوان برای مقدار دهی attribute‌ها هم استفاده کرد. 
<script>
let src = 'https://svelte.dev/tutorial/image.gif';
let name = 'Rick Astley';
</script>

<img src={src} alt="{name} dancing">
اگر این کد را در بدنه کامپوننت خود یعنی همان فایل App.svelte قرار دهیم، به درستی کار خواهد کرد و مقدار‌های متغیر‌های src و name، در تگ img ما قرار خواهند گرفت. چند نکته در اینجا وجود دارد که بهتر است به آنها اشاره کنم.

  • نکته اول : اگر در تگ img مقدار alt را وارد نکنیم و یا alt در این تگ وجود نداشته باشد، یک هشدار توسط کامپایلر svelte برای ما با عنوان <img> element should have an alt attribute> ایجاد میشود. زمان ساخت یک برنامه بسیار مهم است تا قوانین نوشتن یک کد html خوب را رعایت کنیم تا برای تمامی کاربران احتمالی برنامه قابل استفاده باشد. در همین مثال با ایجاد یک هشدار Svelte تلاش میکند که ما را از اشتباه در نوشتن کد html مطلع سازد.

  • نکته دوم : اگر نام یک آبجکت تعریف شده و یک attribute، برابر باشد میتوانیم از نسخه کوتاه شده یا Shorthand attributes در svelte استفاده کنیم. به طور مثال در مثال بالا میتوانیم از کد زیر در خط 6 استفاده کنیم.
<img {src} alt="{name} dancing">


Nested components :
همانطور که قبلا اشاره کرده بودم، همه‌چیز در svelte از کامپوننت‌ها تشکیل میشود. در یک برنامه واقعی درست نیست که تمامی کد برنامه را مستقیما در کامپوننت اولیه برنامه بنویسیم. به جای آن بهتر است هر بخش از وبسایت، کامپوننت مجزایی داشته باشد تا کدها و استایل‌ها و html ما نظم و خوانایی بیشتری پیدا کنند.
یک فایل دیگر را در کنار کامپوننت App.svelte که در بخش قبلی ایجاد کرده بودیم، به نام Nested.svelte ایجاد کنید و در آن کد زیر را قرار دهید.
<script>
  export let siteName = "dotnettips";
</script>

<p>this is a nested component for third tutorial on {siteName}</p>
در این مرحله ما یک کامپوننت جدید را ایجاد کردیم که برای نمایش آن باید به App.svelte اضافه شود. برای انجام اینکار محتوای App.svelte را به کد زیر تغییر دهید.
<script>
  import Nested from "./Nested.svelte";

  export let name;
</script>

<h1>Hello {name}!</h1>

<Nested siteName="dotnettips.info" />
که در نهایت چنین خروجی خواهیم داشت. 
 Hello world!

this is a nested component for third tutorial on dotnettips.info


در مثال بالا ما یک کامپوننت جدید را ایجاد کرده و از طریق دستور import به App.svelte اضافه کردیم. نکته‌ای که در اینجا وجود دارد، نحوه مقدار دهی props در کامپوننت‌ها است. اگر به خط 9 دقت کنیم، کامپوننت ما از طریق تگ جدیدی با نام (Nested) به بدنه html برنامه اضافه شده است که یک attribute به نام siteName دارد. siteName متغیر export شده در کامپوننت Nested.svelte است که در کامپوننت‌ها به این صورت مقدار دهی میشود. قبلا نحوه مقدار دهی این خصیصه‌ها را در فایل‌های جاوا اسکریپت مشاهده کرده بودیم. نکته دیگری که باید به آن دقت داشت این است که خصیصه siteName مقدار پیش فرض dotnettips را در Nested.svelte به خود اختصاص داده بود. به همین جهت اگر ما siteName را هنگام استفاده از کامپوننت مقدار دهی نکنیم، از مقدار پیش فرض خود استفاده خواهد کرد. ولی اینجا ما با مقدار دهی آن، siteName را به dotnettips.info تغییر داده‌ایم.

نکته مهم : دقت داشته باشید کامپوننت‌های شما همیشه باید با حروف بزرگ شروع شوند؛ به طور مثال در صورت نوشتن <nested/> محتوای کامپوننت نمایش داده نخواهد شد. svelte، از طریق زیر نظر گرفتن حروف کوچک و بزرگ در ابتدای تگ‌ها، بین تگ‌های html و کامپوننت‌ها تمایز قائل میشود.



Spread  props :

تا اینجا به صورت خلاصه با props یا خصیصه‌ها آشنا شده‌اید و دیدیم که با export کردن یک متغیر در یک کامپوننت، میتوانیم آن را هنگام استفاده مقدار دهی نماییم. برای اینکه تمرینی هم باشد با توجه به مطالبی که تاکنون گفته شده، پروژه‌ی جدیدی را ایجاد کنید و محتوای App.svelte را مانند کد زیر تغییر دهید.

<script>
import Info from './Info.svelte';

const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>

<Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}/>

همانطور که در خط دوم کد می‌بینید، کامپوننتی به نام Info.svelte به این بخش اضافه شده‌است. این کامپوننت را با محتوای زیر ایجاد نمایید:

<script>
export let name;
export let version;
export let speed;
export let website;
</script>

<p>
The <code>{name}</code> package is {speed} fast.
Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
and <a href={website}>learn more here</a>
</p>

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

 The svelte  package is  blazing  fast. Download version  3  from  npm  and  learn more here
در این مثال در کامپوننت info.svelte حدودا تعداد زیادی متغیر export شده داریم که دقیقا همنام با آنها در App.svelte یک آبجکت به نام pkg مقدار دهی شده‌است (خط 4-9). در این شرایط نیازی به نوشتن تک تک خصیصه‌ها مانند کاری که در خط 12 کردیم نیست. خیلی ساده میتوانیم از امکانات ES6 برای destruct کردن این آبجکت و ست کردن این خصیصه‌ها استفاده کنیم؛ به این صورت : 
<Info {...pkg}/>



مروری کوتاه بر مدیریت Event ها  در svelte:
قدرت اصلی svelte، واکنش پذیر بودن آن است. به این معنی که همیشه DOM را با وضعیت یا state برنامه هماهنگ نگه میدارد. یکی از مواردی که بطور مثال این امکان را به خوبی نمایش میدهد، event‌ها هستند. 
به مثال زیر توجه کنید:
<script>
  let count = 0;

  function handleClick() {
    count += 1;
  }
</script>

<p>Count : {count}</p>
<button on:click={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
در مثال بالا در خط 10، یک button را ایجاد کردیم که به متد click آن یک function اختصاص داده شده که در خط 4 تعریف کرده بودیم. برای bind کردن یک event در svelte از on: استفاده میکنیم. البته دقت کنید که در خط 10، نام function بدون پرانتز نوشته شده‌است؛ چرا که ما قصد اجرای آن را نداریم و صرفا میخواهیم این فانکشن پس از کلیک شدن بر روی button اجرا شود. در خط 5 هم در بدنه فانکشن خود که پس از هر کلیک اجرا خواهد شد، یک واحد به متغیر count اضافه میکنیم. در خطوط 9 و 11 هم مقدار count را نمایش داده‌ایم. 
در بخش‌های بعد به جزئیات کار با event‌ها بیشتر میپردازیم.

واکنش پذیری یا Reactivity در svelte :
svelte به صورت خودکار DOM را پس از اینکه وضعیت کامپوننت یا آبجکت‌های شما تغییر کند، آپدیت میکند؛ ولی در این بین چندین حالت وجود دارند که به صورت خودکار svelte از تغییر وضعیت کامپوننت شما آگاه نمیشود. 
عملگر :$
به مثال زیر توجه کنید. 
<script>
let count = 0;
let doubled = count * 2;

function handleClick() {
count += 1;
}
</script>

<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

<p>{count} doubled is {doubled}</p>
در این مثال در خط سوم متغیر doubled تعریف شده که قرار است دو برابر count را در خود نگه دارد. اگر این کد را اجرا نمایید، متوجه خواهید شد که مقدار double پس از تغییر count، تغییری نخواهد کرد. چرا که بر خلاف سایر فریم ورک‌ها، مانند react برای بهبود performance برنامه، svelte از Virtual-DOM استفاده نمیکند و این امر سبب میشود که کل کدهای کامپوننت ما در هر بار تغییر، در وضعیت بخشی از آن، مجددا اجرا نشود و مستقیما بخش مورد نظر در DOM تغییر کند. این امر سبب شده از نظر performance در کامپوننت‌های پیچیده که به طور مثال از انیمیشن یا visualisation‌ها استفاده میکنند، کامپوننت‌های svelte چندین برابر پرسرعت‌تر از سایر فریم ورک‌های مشابه عمل کند. ولی اگر بخواهیم که مقدار doubled آپدیت شود چه کاری باید کرد؟ 
راهکار اول مقدار دهی doubled در function مرتبط است، چرا که svelte همیشه به عملگر مساوی = در کد جاوا اسکریپت نگاه میکند. ولی این راهکار در مثال بالا منطقی به نظر نمی‌آید چرا که وظیفه متد ما صرفا دو برابر کردن مقدار count است.
راهکار دوم استفاده از عملگر :$  قبل از تعریف متغیر میباشد که به نظر راهکار مناسب‌تری است. پس در کد بالا فقط لازم است خط سوم را به کد زیر تغییر دهیم تا برنامه بدرستی مقدار doubled را پس از تغییر count محاسبه کند. 
$: doubled = count * 2;
به این طریق میتوانیم متغیر خود را وادار کنیم درصورت تغییر مقادیر، در سمت راست مساوی، مقدار doubled را مجددا محاسبه نماید.

استفاده از این عملگر در svelte به تعریف متغیر‌ها محدود نمیشود. ما هر جائیکه نیاز به واکنش پذیری نسبت به کامپوننت‌ها یا متغیر‌های خود داشتیم، میتوانیم از این عملگر استفاده کنیم. به طور مثال برای لاگ کردن اطلاعات، هر بار که مقدار count تغییر کرد میتوانیم از کد زیر استفاده کنیم. 
$: console.log(`the count is ${count}`);
همچنین میتوانیم به آسانی گروهی از فعالیت‌ها را با هم واکنش پذیر کنیم. مثال :
$: {
  console.log(`the count is ${count}`);
  alert(`I SAID THE COUNT IS ${count}`);
}
حتی میتوان از این عملگر قبل از بلاک‌های کد، مانند if یا for استفاده کرد. مثال:
$: if (count >= 10) {
     alert(`count is dangerously high!`);
     count = 9;
}

واکنش پذیری در آرایه‌ها و آبجکت‌ها : 
به مثال زیر توجه کنید:
<script>
  let numbers = [1, 2, 3, 4];

  function addNumber() {
    let newNumber = numbers.length + 1;
    numbers.push(newNumber);
  }

  $: sum = numbers.reduce((t, n) => t + n, 0);
</script>

<p>{numbers.join(' + ')} = {sum}</p>

<button on:click={addNumber}>Add a number</button>

در مثال بالا پس از هر کلیک، یک عدد به آرایه numbers اضافه میشود؛ ولی در خط 11 که این آرایه را نمایش میدهیم، مقدار آن تغییر نخواهد کرد! همانطور که قبلا اشاره کرده بودم svelte برای اینکه متوجه شود تغییری در آبجکت‌های ما صورت گرفته است، به مشاهده عملگر مساوی در کدهای ما میپردازد و چون اینجا در خط 6، ما از متد push آرایه برای افزودن مقداری جدید به آن درحال استفاده هستیم svelte واکنشی به این تغییر ندارد. برای رفع این مشکل دو راهکار وجود دارد. 
راهکار اول استفاده از عملگر مساوی و reassign کردن مقدار آبجکت یا متغیر مورد نظر است به این صورت : 
  function addNumber() {
    let newNumber = numbers.length + 1;
    numbers.push(newNumber);
    numbers = numbers;
  }
راهکار دوم استفاده از امکانات ES6 جاوا اسکریپت است برای افزودن مقدار جدید به آرایه که چون در این روش از مساوی استفاده میکنیم، تغییرات در numbers واکنش پذیر خواهد بود. به این صورت : 
  function addNumber() {
    let newNumber = numbers.length + 1;
    numbers = [...numbers, newNumber];
  }

مروری بر Two way bindings :
فرض کنید قصد داریم که یک input برای دریافت نام در صفحه قرار داده و در متغیری به نام name ذخیره کنیم. سپس این متغیر را در صفحه نشان دهیم. برای انجام اینکار حدودا با توجه به مطالبی که تا الان آموخته‌ایم میتوان کد زیر را تولید کرد : 
<script>
  let name = "";
  function updateName(event) {
    name = event.target.value;
  }
</script>

<h4>My Name Is {name}</h4>
<input value={name} on:input={updateName} />
در خط 9 یک input به کامپوننت خود اضافه کردیم و مقدار value آن را به متغیر name نسبت دادیم. تا اینجا فقط اگر name تغییری کند آن را در input میتوانیم نمایش دهیم ولی پس از تایپ در این input به صورت خودکار مقدار name آپدیت نمیشود چون ما در این حالت از One way binding درحال استفاده هستیم. به همین جهت یک متد برای آپدیت مقدار name توسط input هم باید مینوشتیم که اگر دقت کنید در کد فوق از ایونت input تگ input در خط 11 به همین منظور استفاده شده‌است. در خط سوم اینبار متد ما یک پارامتر هم به نام event دارد. به طور پیش فرض میتوانیم همیشه از طریق این پارامتر به ایونت آبجکت مبداء که فانکشن را صدا زده است، دسترسی داشته باشیم که خیلی ساده از طریق این ایونت در خط 4 مقدار name را پس تغییر در input آپدیت میکنیم. 
شاید کد بالا کاری که خواستیم را انجام داده باشد. ولی پیاده سازی Two way binding به شکلی که نوشتیم ساده نیست و مشکلات خاص خودش را دارد. خبر خوب این است که svelte این امکان را به صورت توکار محیا کرده است. 
برای اتصال یک attribute به یک متغیر یا آبجکت به صورت دو طرفه از کلمه :bind قبل از نام attribute در svelte استفاده میشود. مثال بالا را اگر با استفاده از این توضیح بازنویسی کنیم، به این کد خواهیم رسید:
<script>
  let name = "";
</script>

<h4>My Name Is {name}</h4>
<input bind:value={name} />
همانطور که مشاهده میکنید دیگر نیازی به نوشتن یک فانکشن مجزا و گرفتن اطلاعات از طریق پارامتر ایونت نیست و به سادگی مقدار value به متغیر name به صورت دو طرفه متصل شده است.


expressing logic :
html امکانی برای پیاده سازی منطق برنامه، برخلاف svelte ندارد. در ادامه با syntax پیاده سازی منطق برنامه در کنار کدهای html در svelte آشنا میشویم. 

If blocks
برای نوشتن If در svelte از if# در میان دو آکولاد {} استفاده میشود و برای پایان دادن به آن از if/ در میاد دو آکولاد. به این صورت : 
{#if condition}
    <!-- you html codes ...  -->
{/if}
به مثال زیر توجه کنید : 
در این مثال قصد داریم پس از کلیک بر روی کلید لاگین، وضعیت کاربر را لاگین شده قرار دهیم و کلید لاگین را مخفی کنیم. سپس کلید دیگری برای خروج از برنامه به نام logout را نمایش دهیم که با کلیک بر روی آن کاربر logout شود و مجددا کلید لاگین نمایش داده شود. برای شبیه سازی این سناریو از کد زیر میتوانیم استفاده کنیم :
<script>
let user = { loggedIn: false };

function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>

{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{/if}

{#if !user.loggedIn}
<button on:click={toggle}>
Log in
</button>
{/if}
کدهای داخل اسکریپت که چندان نیازی به توضیح ندارند و صرفا برای toggle کردن وضعیت کاربر هستند. ولی در خطوط 9 - 13 و 15 - 19 طریقه نوشتن if در svelte را با یک مثال واقعی مشاهده میکنید.
در مثال بالا دو شرط با یک مقدار مشابه true/false نوشته شده است که بهتر بود مانند سایر زبان‌های برنامه نویسی از else در اینجا استفاده میشد. در ادامه با نحوه نوشتن else در svelte آشنا خواهیم شد.
Else blocks
برای نوشتن else در svelte از else: در میاد دو آکولاد {} بین یک block مانند if میتوانیم استفاده کنیم. به این صورت :
{#if condition}
    <!-- you html code when condition is true -->
{:else}
    <!-- you html code when condition is false -->
{/if}
باز نویسی دو if block در مثال بالا با استفاده از else : 
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}

همینطور شما میتوانید از else if هم استفاده نمایید. به این صورت : 
{#if condition}
    <!-- you html code when condition is true -->
{:else if condition2}
    <!-- you html code when condition2 is true -->
{:else}
    <!-- you html code when condition and condition2 are false -->
{/if}

نکته تکمیلی : در svelte علامت # همیشه به معنای شروع یک بلاک کد است و علامت / به معنای پایان بلاک. همینطور علامت : نشان دهنده ادامه بلاک کد است. شاید در ابتدا کمی گیج کننده به نظر بیاید ولی در مثال‌های بعدی بیشتر با این علائم آشنا میشویم.

each blocks : 
برای حرکت در میان لیستی از اطلاعات در svelte میتوانیم از each# استفاده نماییم و مانند بلاک if برای خاتمه دادن به آن از each/ استفاده میشود. همینطور از کلمه کلیدی as هم برای دسترسی به هر آیتم در لیست استفاده میشود. به این صورت : 
{#each list as item}
      <!-- you html code per each item in list -->
{/each}
به مثال زیر توجه کنید : 
<script>
let cats = [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>

<h1>The Famous Cats of YouTube</h1>

<ul>
{#each cats as cat}
<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{cat.name}
</a></li>
{/each}
</ul>
در خط 2 ما یک آرایه از گربه‌ها را تعریف کرده ایم و در خط 11 با استفاده از each به ازای هر کدام از آیتم‌های داخل این آرایه یک تگ li ایجاد میکنیم که لینک مرتبط با آن آیتم را نمایش میدهد. البته کد فوق را به شکل دیگری با استفاده از امکانات destructing جاوا اسکریپت هم میتوانستیم بنویسیم. به این صورت : 
<ul>
{#each cats as {id,name}}
<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
{name}
</a></li>
{/each}
</ul>
در این حالت ابتدا id و name به صورت مجزا destruct شده اند و میتوانیم مانند مثال بالا از آنها بدون نوشتن نام آیتم استفاده کنیم . مانند خط 4 کد بالا.
همینطور در each بلاک‌ها امکان دریافت ایندکس جاری آیتم از طریق آرگومان دوم هم وجود دارد. به این صورت : 
<ul>
{#each cats as { id, name }, i}
<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
{i + 1}: {name}
</a></li>
{/each}
</ul>
اگر به کد فوق دقت کنید درخط 2 مقدار i ایندکس آیتم جاری آرایه ما یعنی همان cats است.


نکته : در این بخش من سعی کردم تا حدودی به ترتیب بخش آموزشی خود وبسایت Svelte، موارد را بیان کنم؛ ولی با توجه به اینکه شاید دوستان ترجیح بدهند روش آموزشی خود  آن وبسایت که امکان تغییر و نوشتن کد را هم محیا کرده است، امتحان کنند  لینک آن  را به اشتراک میگذارم. 
مطالب
نمایش فرم‌های مودال Ajax ایی در ASP.NET MVC به کمک Twitter Bootstrap
اصول نمایش اطلاعات مودال به کمک bootstrap در مطلب «استفاده از modal dialogs مجموعه Twitter Bootstrap برای گرفتن تائید از کاربر» بررسی شدند.
در این قسمت قصد داریم یک فرم Ajaxایی را در ASP.NET MVC به همراه تمام مسایل اعتبارسنجی، پردازش اطلاعات و غیره را به کمک Twitter Bootstrap و jQuery Ajax پیاده سازی کنیم.


تهیه افزونه jquery.bootstrap-modal-ajax-form.js

از این جهت که مباحث مرتبط با نمایش و پردازش فرم‌های مودال Ajaxایی به کمک Twitter Bootstrap اندکی نکته دار و طولانی هستند، بهتر است این موارد را به شکل یک افزونه، کپسوله کنیم. کدهای کامل افزونه jquery.bootstrap-modal-ajax-form.js را در ادامه ملاحظه می‌کنید:
// <![CDATA[
(function ($) {
    $.bootstrapModalAjaxForm = function (options) {
        var defaults = {
            renderModalPartialViewUrl: null,
            renderModalPartialViewData: null,
            postUrl: '/',
            loginUrl: '/login',
            beforePostHandler: null,
            completeHandler: null,
            errorHandler: null
        };
        var options = $.extend(defaults, options);

        var validateForm = function (form) {
            //فعال سازی دستی اعتبار سنجی جی‌کوئری
            var val = form.validate();
            val.form();
            return val.valid();
        };

        var enableBootstrapStyleValidation = function () {
            $.validator.setDefaults({
                highlight: function (element, errorClass, validClass) {
                    if (element.type === 'radio') {
                        this.findByName(element.name).addClass(errorClass).removeClass(validClass);
                    } else {
                        $(element).addClass(errorClass).removeClass(validClass);
                        $(element).closest('.control-group').removeClass('success').addClass('error');
                    }
                    $(element).trigger('highlited');
                },
                unhighlight: function (element, errorClass, validClass) {
                    if (element.type === 'radio') {
                        this.findByName(element.name).removeClass(errorClass).addClass(validClass);
                    } else {
                        $(element).removeClass(errorClass).addClass(validClass);
                        $(element).closest('.control-group').removeClass('error').addClass('success');
                    }
                    $(element).trigger('unhighlited');
                }
            });
        }

        var enablePostbackValidation = function () {
            $('form').each(function () {
                $(this).find('div.control-group').each(function () {
                    if ($(this).find('span.field-validation-error').length > 0) {
                        $(this).addClass('error');
                    }
                });
            });
        }

        var processAjaxForm = function (dialog) {
            $('form', dialog).submit(function (e) {
                e.preventDefault();

                if (!validateForm($(this))) {
                    //اگر فرم اعتبار سنجی نشده، اطلاعات آن ارسال نشود
                    return false;
                }

                //در اینجا می‌توان مثلا دکمه‌ای را غیرفعال کرد
                if (options.beforePostHandler)
                    options.beforePostHandler();

                //اطلاعات نباید کش شوند
                $.ajaxSetup({ cache: false });
                $.ajax({
                    url: options.postUrl,
                    type: "POST",
                    data: $(this).serialize(),
                    success: function (result) {
                        if (result.success) {
                            $('#dialogDiv').modal('hide');
                            if (options.completeHandler)
                                options.completeHandler();
                        } else {
                            $('#dialogContent').html(result);
                            if (options.errorHandler)
                                options.errorHandler();
                        }
                    }
                });
                return false;
            });
        };

        var mainContainer = "<div id='dialogDiv' class='modal hide fade in'><div id='dialogContent'></div></div>";
        enableBootstrapStyleValidation(); //اعمال نکات خاص بوت استرپ جهت اعتبارسنجی یکپارچه با آن
        $.ajaxSetup({ cache: false });
        $.ajax({
            type: "POST",
            url: options.renderModalPartialViewUrl,
            data: options.renderModalPartialViewData,
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            complete: function (xhr, status) {
                var data = xhr.responseText;
                var data = xhr.responseText;
                if (xhr.status == 403) {
                    window.location = options.loginUrl; //در حالت لاگین نبودن شخص اجرا می‌شود
                }
                else if (status === 'error' || !data) {
                    if (options.errorHandler)
                        options.errorHandler();
                }
                else {
                    var dialogContainer = "#dialogDiv";
                    $(dialogContainer).remove();
                    $(mainContainer).appendTo('body');

                    $('#dialogContent').html(data); // دریافت پویای اطلاعات مودال دیالوگ
                    $.validator.unobtrusive.parse("#dialogContent"); // فعال سازی اعتبارسنجی فرمی که با ایجکس بارگذاری شده                            
                    enablePostbackValidation();
                    // و سپس نمایش آن به صورت مودال
                    $('#dialogDiv').modal({
                        backdrop: 'static', //با کلیک کاربر روی صفحه، صفحه مودال بسته نمی‌شود
                        keyboard: true
                    }, 'show');
                    // تحت نظر قرار دادن این فرم اضافه شده
                    processAjaxForm('#dialogContent');
                }
            }
        });
    };
})(jQuery);
// ]]>
توضیحات:
- توابع enableBootstrapStyleValidation و enablePostbackValidation در مطلب «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» بررسی شدند.
- این افزونه با توجه به مقدار renderModalPartialViewUrl، یک partial view را از برنامه ASP.NET MVC درخواست می‌کند.
- سپس این partial view را به صورت خودکار به صفحه اضافه کرده و آن‌را به صورت modal نمایش می‌دهد.
- پس از افزودن فرم Ajaxایی دریافتی، مسایل اعتبارسنجی را به آن اعمال کرده و سپس دکمه submit آن‌را تحت کنترل قرار می‌دهد.
- در زمان submit، ابتدا بررسی می‌کند که آیا فرم معتبر است و اعتبارسنجی آن بدون مشکل است؟ اگر اینچنین است، اطلاعات فرم را به آدرس postUrl به صورت Ajaxایی ارسال می‌کند.


کدهای مدل برنامه
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Mvc4TwitterBootStrapTest.Models
{
    public class User
    {
        public int Id { set; get; }

        [DisplayName("نام")]
        [Required(ErrorMessage="لطفا نام را تکمیل کنید")]
        public string Name { set; get; }

        [DisplayName("نام خانوادگی")]
        [Required(ErrorMessage = "لطفا نام خانوادگی را تکمیل کنید")]
        public string LastName { set; get; }
    }
}
در اینجا یک مدل ساده‌را به همراه ویژگی‌های اعتبارسنجی و نام‌های نمایشی خواص ملاحظه می‌کنید.


کدهای کنترلر برنامه

using System.Web.Mvc;
using Mvc4TwitterBootStrapTest.Models;

namespace Mvc4TwitterBootStrapTest.Controllers
{
    public class ModalFormAjaxController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش صفحه اولیه
        }

        [HttpPost] //برای این حالت امن‌تر است
        //[AjaxOnly]
        public ActionResult RenderModalPartialView()
        {
            //رندر پارشال ویوو صفحه مودال به همراه اطلاعات مورد نیاز آن
            return PartialView(viewName: "_ModalPartialView", model: new User { Name = "", LastName = "" });
        }

        [HttpPost]
        //[AjaxOnly]
        public ActionResult Index(User user) //ذخیره سازی اطلاعات
        {
            if (this.ModelState.IsValid)
            {
                //todo: SaveChanges;
                return Json(new { success = true });
            }

            this.ModelState.AddModelError("", "خطایی رخ داده است");
            return PartialView("_ModalPartialView", user);
        }
    }
}
کدهای کنترلر برنامه در این حالت از سه قسمت تشکیل می‌شود:
الف) متد Index حالت HttpGet که صفحه ابتدایی را نمایش خواهد داد.
ب) متد RenderModalPartialView یک partial view اضافه شده به برنامه به نام _ModalPartialView.cshtml را بازگشت می‌دهد. این partial view در حقیقت همان فرمی است که قرار است به صورت مودال نمایش داده شود و پردازش آن نیز Ajaxایی باشد.
ج) متد Index حالت HttpPost که نهایتا اطلاعات فرم مودال را دریافت خواهد کرد. اگر پردازش موفقیت آمیز بود، نیاز است همانند کدهای فوق return Json صورت گیرد. در غیراینصورت مجددا همان partial view را بازگشت دهید.


کدهای Index.cshtml

@{
    ViewBag.Title = "Index";
    var renderModalPartialViewUrl = Url.Action("RenderModalPartialView", "ModalFormAjax");
    var postDataUrl = Url.Action("Index", "ModalFormAjax");
}
<h2>
    Index</h2>
<a href="#" class="btn btn-primary" id="btnCreate">ثبت اطلاعات</a>

@section JavaScript
{
    <script type="text/javascript">
        $(function () {           
            $('#btnCreate').click(function (e) {
                e.preventDefault(); //می‌خواهیم لینک به صورت معمول عمل نکند

                $.bootstrapModalAjaxForm({
                    postUrl: '@postDataUrl',
                    renderModalPartialViewUrl: '@renderModalPartialViewUrl',
                    renderModalPartialViewData: {},
                    loginUrl: '/login',
                    beforePostHandler: function () {                       
                    },
                    completeHandler: function () {
                        // Refresh: برای حالتیکه نیاز به به روز رسانی کامل صفحه زیرین باشد
                        // location.reload();
                    },
                    errorHandler: function () {
                    }
                });
            });
        });             
    </script>
}
این کدها متناظر هستند با کدهای view اکشن متد Index در حالت Get.
- در اینجا یک لینک ساده در صفحه قرار گرفته و به کمک کلاس btn مجموعه bootstrap به شکل یک دکمه مزین شده است.
- در ادامه نحوه استفاده از افزونه‌ای را که در ابتدای بحث طراحی کردیم، ملاحظه می‌کنید. کار با آن بسیار ساده است و تنها باید مسیرهای ارسال اطلاعات نهایی به سرور یا postDataUrl و مسیر دریافت partial view رندر شده یا renderModalPartialViewUrl به آن معرفی شود. سایر مسایل آن خودکار است.


کدهای _ModalPartialView.cshtml یا همان فرم مودال برنامه

@model Mvc4TwitterBootStrapTest.Models.User
<div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">
        &times;</button>
    <h5>
        افزودن کاربر جدید</h5>
</div>
@using (Html.BeginForm("Index", " ModalFormAjax", FormMethod.Post, new { @class = "modal-form" }))
{
    <div class="modal-body">
        @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" })
        <fieldset class="form-horizontal">
            <legend>مشخصات کاربر</legend>
            <div class="control-group">
                @Html.LabelFor(model => model.Name, new { @class = "control-label" })
                <div class="controls">
                    @Html.EditorFor(model => model.Name)
                    @Html.ValidationMessageFor(model => model.Name, null, new { @class = "help-inline" })
                </div>
            </div>
            <div class="control-group">
                @Html.LabelFor(model => model.LastName, new { @class = "control-label" })
                <div class="controls">
                    @Html.EditorFor(model => model.LastName)
                    @Html.ValidationMessageFor(model => model.LastName, null, new { @class = "help-inline" })
                </div>
            </div>
        </fieldset>
    </div>
        
    <div class="modal-footer">
        <button class="btn btn-primary" type="submit">
            ارسال</button>
        <button class="btn" data-dismiss="modal" aria-hidden="true">
            انصراف</button>
    </div>
}
در اینجا اطلاعات فرمی را ملاحظه می‌کنید که قرار است به صورت مودال نمایش داده شود. نحوه طراحی آن بر اساس نکات form-horizontal است. همچنین divهای modal-header، modal-body و modal-footer نیز به این فرم ویژه اضافه شده‌اند تا به خوبی توسط bootstrap پردازش گردد.
حاصل نهایی این مبحث را در دو شکل ذیل ملاحظه می‌کنید. صفحه index نمایش دهنده یک دکمه و در ادامه باز شدن یک فرم مودال، پس از کلیک بر روی دکمه ثبت اطلاعات.


 
اشتراک‌ها
ویژگی Image Tag Helper در MVC 6.0
<img src="~/images/logo.png" 
     alt="company logo" 
     asp-file-version="true" />


 which will generate something like this:
<img src="/images/logo.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk" 
     alt="company logo"/>

The value of the v parameter is calculated based on the contents of the image file. If the contents of the image change, the value of the parameter will change. This forces the browser to download the new version of the file, even if the old version was cached locally. This technique is often called cache busting.
ویژگی Image Tag Helper در MVC 6.0
نظرات مطالب
استفاده از Froala WYSIWYG Editor در ASP.NET
امکان افزونه نویسی هم دارد. اگر همان کدهای افزونه‌ای را که برای redactor نوشتیم، با این ادیتور تطابق دهیم، به دکمه‌ی سفارشی زیر خواهیم رسید:
buttons: [
                 // .... ,
                 "insertHTML" //custom button
         ],


     customButtons: {
                    insertHTML: {
                        title: 'Insert Code',
                        icon: {
                            type: 'font',
                            value: 'fa fa-dollar' // Font Awesome icon class fa fa-*
                        },
                        callback: function (editor) {
                            editor.saveSelection();

                            var codeModal = $("<div>").addClass("froala-modal").appendTo("body");
                            var wrapper = $("<div>").addClass("f-modal-wrapper").appendTo(codeModal);
                            $("<h4>").append('<span data-text="true">Insert Code</span>')
                                .append($('<i class="fa fa-times" title="Cancel">')
                                .click(function () {
                                    codeModal.remove();
                                }))
                                .appendTo(wrapper);

                            var dialog = "<textarea id='code_area' style='height: 211px; width: 538px;' /><br/><label>Language:</label><select id='code_lang'><option>CSharp</option><option>VB</option><option>JScript</option><option>Sql</option><option>XML</option><option>CSS</option><option>Java</option><option>Delphi</option></select> <input type='button' name='insert' id='insert_btn' value='Insert' /><br/>";
                            $(dialog).appendTo(wrapper);

                            $("#code_area").text(editor.text());

                            if (!editor.selectionInEditor()) {
                                editor.$element.focus();
                            }

                            $('#insert_btn').click(function () {
                                var lang = $("#code_lang").val();
                                var code = $("#code_area").val();
                                code = code.replace(/\s+$/, ""); // rtrim
                                code = $('<span/>').text(code).html(); // encode    

                                var htmlCode = "<pre language='" + lang + "' name='code'>" + code + "</pre></div>";
                                var codeBlock = "<div align='left' dir='ltr'>" + htmlCode + "</div><br/>";

                                editor.restoreSelection();
                                editor.insertHTML(codeBlock);
                                editor.saveUndoStep();

                                codeModal.remove();
                            });
                        }
                    }
                }