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

دستورات بدون خروجی:
یک سری از دستورات هستند که خروجی ندارند و رکوردی را باز نمی‌گردانند و برای اجرای دستوراتی چون افزودن، به روزرسانی و حذف بسیار مناسبند. اجرای  این دستورات را ما به متدی به نام run می‌سپاریم. در دفعه قبل که از این دستور استفاده کردیم، پارامتری برای تعیین کردن نداشت؛ ولی در این مقاله، دستور با پارامتر آن را اجرا می‌کنیم:
ابتدا کدهای زیر را به فایل html، برای درج رکورد جدید اضافه می‌کنیم:
First Name:<br/>
<input type="text" id="txtfname" /><br/>
Last Name:<br/>
<input type="text" id=txtlname /><br/>
Number:<br/>
<input type="tel" id="txttel" /><br/>
<button id="btnsubmit">Save</button><br/>
برای خواندن مقادیر هم از این تابع کمک می‌گیریم:
function GetValues()
{
  let fname=$("#txtfname").val();
  let lname=$("#txtlname").val()
  let tel=$("#txttel").val();
  let row=
  {
    fname:fname,
    lname:lname,
    number:tel
  };
  return row;
}
سپس تکه کد زیر را  با کمک  جی کوئری اضافه می‌کنیم:
$("#btnsubmit").click((e)=>{
  e.preventDefault();
  let row=GetValues();

  //save in db
  //get last id
  let statement==db.prepare("select id from numbers order by id desc limit 1");
  let lastRecord=statement.getAsObject({}).id;
  row.id=lastRecord++;
  let count=db.prepare("select count(*) as count from numbers order by id desc").getAsObject({}).count;
  statement.free();
  let insertCommand="insert into numbers values(?,?,?,?)";
  db.run(insertCommand,[row.id,row.fname,row.lname,String(row.number)])
  let newcount=db.prepare("select count(*) as count from numbers order by id desc").getAsObject({}).count;
  SaveChanges();

//show in the table
if(count<newcount)
{
  AddToTable(row);
}
});
});
متد GetValues  مقادیر را از input‌ها دریافت و در متغیر row نگهداری می‌کند. سپس برای درج رکورد، از آنجاکه ما فیلد id را افزایشی تعیین نکرده‌ایم، باید آخرین رکورد موجود در جدول را واکشی کنیم که برای این منظور از متد prepare کمک می‌گیریم. این متد در عوض کوئری داده شده، یک شیء statement را بر می‌گرداند که حاوی نتایج کوئری داده شده است؛ ولی هنوز به مرحله اجرا نرسیده است. برای اجرای دستور کوئری، دو متد وجود دارند که یکی step است و دیگری getAsObject می‌باشد. متد getAsObject برای زمانی خوب است که شما در حد یک رکورد خروجی دارید و می‌خواهید همان یک رکورد را به صورت شیء برگردانید؛ یعنی با "نام شی.نام خصوصیت" به آن دسترسی پیدا کنید. همانطور که می‌بینید ما درخواست فیلد id را کرده‌ایم و تنها یک رکورد را درخواست کرده‌ایم که باعث می‌شود به راحتی به فیلد id دسترسی داشته باشیم. وجود علامت {} داخل این متد به این معنی است که پارامتری برای ارسال وجود ندارد. ولی اگر قرار بود پارامتری را داشته باشیم، به این شکل می‌نوشتیم:
var statement= db.prepare("SELECT * FROM NUMBERS WHERE fname=@fname AND lname=@lname");

var result = statement .getAsObject({'@fname' :'ali', '@lname' : 'yeganeh'});
اگر دوست داشتید دوباره از این دستور کوئری بگیرید ولی با مقادیر متفاوت، می‌توانید از متد bind کمک بگیرید:
statement.bind(['hossein','yeganeh']);
متد دیگری هم برای پاکسازی پارامترها به نام reset، وجود دارد که فضای اختصاصی و گرفته شده توسط پارامترها را باز می‌گرداند.

متد step همانند متدهای next در cursor یا read در datareader عمل میکند و با هر بار صدا زدن، یک رکورد، به سمت جلو حرکت می‌کند. دریافت هر رکورد جاری توسط متد get و نوع خروجی آرایه انجام می‌شود:
while(statement.step())
{
     var rec=statement.get();
}
بعد از اینکه کارمان با آن تمام شد، برای پاکسازی حافظه از متد free استفاده می‌کنیم. در دستورات بعد، شیء statement را مستقیما مورد استفاده قرار داده‌ایم و توسط آن تعداد رکوردها را دریافت کرده‌ایم. سپس با استفاده از متد run دستور درج را داده‌ایم. اینبار این متد را به شکل متفاوتی استفاده کردیم و به آن پارامتری هم دادیم. نحوه ارائه پارامتر به این متد، باید به صورت آرایه و به ترتیب علامت‌های ؟ باشد. نهایتا با دریافت تعداد رکوردهای جاری و مقایسه با تعداد رکوردهای سابق متوجه می‌شویم که آیا رکوردی اضافه شده است یا خیر؟ در صورتی که اضافه شده است، باید رکورد جدید در جدول، توسط جی کوئری نمایش داده شود و تغییرات دیتابیس هم روی دیسک سخت ذخیره شوند. چون دیتابیس مورد استفاده به صورت in-memory یعنی مقیم در حافظه مورد استفاده قرار میگیرد، باید کل دیتابیس، بر روی دیسک سخت رونویسی شود. متد SaveChanges شامل کد زیر است که حاوی کد ارسال پیام به Main Thread یا Main Process می‌باشد تا در دیسک سخت بنویسد:
  const {ipcRenderer} = require('electron');
function SaveChanges()
{
    ipcRenderer.send("SaveToDb");
}
و برای ترد اصلی:
const {ipcMain} = require('electron');
ipcMain.on("SaveToDb", (event, arg) => {
  SaveToDb();
});

function SaveToDb()
{
  var data=db.export();
  var buffer=new Buffer(data);
  fs.writeFileSync(dbPath,buffer);
}

پی نوشت :یکی از بهترین روش‌ها این است که از همین تعداد سطرها برای id رکورد استفاده کنیم. چرا که فیلد id به عنوان کلید اصلی است و چینش فیزیکی دارد و از لحاظ کارایی بهتر است ولی به علت آموزشی بودن مطلب و آشنایی با دیگر دستورات، این شیوه را انتخاب کردیم.

ویرایش رکورد
ابتدا template string سطر جدول را به شکل زیر تغییر می‌دهیم:
function AddToTable(row)
{
  let tableBody=$("#people");
  let rowTemplate=`<tr><td>${row.fname}</td><td>${row.lname}</td><td>${row.number}</td><td><button class=
  "btn btn-success btnupdate" data-id="${row.id}" >Edit</button></td></tr>`;
  tableBody.append(rowTemplate);
}
تغییری که رخ داده است این است که یک دکمه ویرایش، به صفحه اضافه شده‌است که خصوصیت data-id آن با id رکورد پر خواهد شد.
سپس در تگ اسکریپت، در رویداد ready جی کوئری، این دستورات را اضافه می‌کنیم:
$("#people").on('click','.btnupdate',function(e)
{
  e.preventDefault();

  row.id=$(this).data("id");
  let row=GetValues();
  db.run("UPDATE NUMBERS SET FNAME=?,LNAME=?,NUMBER=? WHERE ID=?",[row.fname,row.lname,row.number,row.id]);
  SaveChanges();
  
  tr=$(this).closest("tr");
  let column=0;
  tr.find("td").each(function(index)
  {
    oldRow=$(this);
    switch(column)
    {
      case 0: //fname
        oldRow.text(row.fname);
        break;
      case 1: //lname
        oldRow.text(row.lname);
        break;
      case 2: //number
        oldRow.text(row.number);
        break;
    }
    column++;
  });
});
ابتدای id ذخیره شده در المان و مقادیر جدید را دریافت می‌کنیم. با استفاده از متد run کوئری به روزرسانی را به همراه پارامترها ارسال میکنیم و نتیجه را بر روی دیسک سخت ذخیره می‌کنیم. از اینجا به بعد نقش جی کوئری پر رنگ‌تر می‌شود و به خوبی می‌توانیم اهمیت آن را درک کنیم. سطر دکمه جاری را پیدا می‌کنیم و مقادیر جدید را ستون به ستون تغییر می‌دهیم.

خواندن و بازگردانی رکوردها

در مقاله قبلی با دستور each آشنا شدیم که یک متد غیرهمزمان بود و نتیجه هر رکورد را با یک callback به ما بازگشت میداد. در اصل این متد شامل 4 پارامتر است: پارامتر اول آن، کوئری ارسالی است. پارامتر دوم آن، پارامتر کوئری‌ها ، پارامتر سوم، تابع callback که به ازای هر رکورد اجرا می‌شود و پارامتر چهارم، تابع done می باشد. یعنی زمانی که کلیه رکوردها بازگشت داده شدند. شکل کامل آن به این صورت است:
db.each("SELECT name,age FROM users WHERE age >= $majority",
  {$majority:18},
  function(row){console.log(row.name)},
                                function(){console.log("done");}
  );


در این مقاله با متد دیگری به نام exec نیز آشنا می‌شویم که بازگردانی مقادیر در آن به صورت همزمان صورت می‌گیرد و توانایی آن را دارد که چندین دستور select را بازگردانی کند. به عنوان مثال دستور زیر را در نظر بگیرید:
SELECT ID FROM NUMBERS;SELECT FNAME,LNAME FROM NUMBERS
شکل خروجی آن به این صورت خواهد بود:
 [
         {columns: ['id'], values:[[1],[2],[3]]},
         {columns: ['fname','lname'], values:[['ali','yeganeh'],['hossein','yeganeh'],['mohammad','yeganeh']]}
    ]
پس اگر بخواهیم کد بازیابی رکورهای جدول numbers را در این پروژه با این متد بازنویسی کنیم، از کد زیر استفاده می‌کنیم:
var records=db.exec("select * from numbers");
let values=records[0].values;
let length=values.length;

for(let i=0;i<length;i++)
{
  let object=values[i];
  let row={
    id:object[0],
    fname:object[1],
    lname:object[2],
    number:object[3]
  };
  AddToTable(row);
}
در کد بالا از آنجا که تنها یک data result یا query result داریم، فقط اندیس 0 آن را می‌خوانیم و سپس تعداد آرایه‌ها را که برابر تعداد رکوردهاست، دریافت می‌کنیم و در یک حلقه، رکورد به رکورد، در جدول اضافه می‌کنیم.
مطالب
React 16x - قسمت 19 - کار با فرم‌ها - بخش 2 - اعتبارسنجی ورودی‌های کاربران
تمام فرم‌های تعریف شده، نیاز به اعتبارسنجی اطلاعات وارد شده‌ی توسط کاربران خود را دارند. ابتدا اعتبارسنجی اطلاعات را در حین ارسال فرم و سپس آن‌را همزمان با ورود اطلاعات، بررسی می‌کنیم.


اصول کلی طراحی یک اعتبارسنج ساده

در قسمت قبل، تمام اطلاعات فرم لاگین را درون شیء account خاصیت state قرار دادیم. در اینجا نیز شبیه به چنین شیءای را برای ذخیره سازی خطاهای اعتبارسنجی فیلدهای فرم، تعریف می‌کنیم:
class LoginForm extends Component {
  state = {
    account: { username: "", password: "" },
    errors: {
      username: "Username is required"
    }
  };
خاصیت errors تعریف شده، یک شیء را باز می‌گرداند که حاوی اطلاعات و خطاهای مرتبط با اعتبارسنجی فیلدهای مشکل دار است. بنابراین نام خواص این شیء، با نام فیلدهای فرم تطابق دارند. کار کردن با یک شیء هم جهت یافتن خطاهای یک فیلد مشخص، ساده‌تر است از کار کردن با یک آرایه؛ از این جهت که نیازی به جستجوی خاصی در این شیء نبوده و با استفاده از روش دسترسی پویای به خواص یک شیء جاوا اسکریپتی مانند errors["username"]، می‌توان خطاهای مرتبط با هر فیلد را به سادگی نمایش داد.
البته در ابتدای کار، خاصیت errors را با یک شیء خالی ({}) مقدار دهی می‌کنیم و سپس در متد مدیریت ارسال فرم به سرور:
  validate = () => {
    return { username: "Username is required." };
  };

  handleSubmit = e => {
    e.preventDefault();

    const errors = this.validate();
    console.log("Validation errors", errors);
    this.setState({ errors });
    if (errors) {
      return;
    }

    // call the server
    console.log("Submitted!");
  };
- ابتدا خروجی متد validate سفارشی را بررسی می‌کنیم که خروجی آن، خطاهای ممکن است.
- اگر خطایی وجود داشت، به مرحله‌ی بعد که ارسال فرم به سمت سرور می‌باشد، نخواهیم رسید و کار را با یک return، خاتمه می‌دهیم.
- علت فراخوانی متد setState در اینجا، درخواست رندر مجدد فرم، با توجه به خطاهای اعتبارسنجی ممکنی است که به خاصیت errors، اضافه یا به روز رسانی کرده‌ایم.
- نمونه‌ای از خروجی متد validate را نیز در اینجا مشاهده می‌کنید که تشکیل شده‌است از یک شیء، که هر خاصیت آن، به نام یک فیلد موجود در فرم، اشاره می‌کند.


پیاده سازی یک اعتبارسنج ساده

در اینجا یک نمونه پیاده سازی ساده و ابتدایی منطق اعتبارسنجی فیلدهای فرم را ملاحظه می‌کنید:
  validate = () => {
    const { account } = this.state;

    const errors = {};
    if (account.username.trim() === "") {
      errors.username = "Username is required.";
    }

    if (account.password.trim() === "") {
      errors.password = "Password is required.";
    }

    return Object.keys(errors).length === 0 ? null : errors;
  };
- ابتدا توسط Object Destructuring، خاصیت account شیء منتسب به خاصیت state کامپوننت را دریافت می‌کنیم، تا مدام نیاز به نوشتن this.state.account نباشد.
- سپس یک شیء خالی error را تعریف کرده‌ایم.
- در ادامه با توجه به اینکه مقادیر المان‌های فرم در state وجود دارند، خالی بودن آن‌ها را بررسی می‌کنیم. اگر خالی بودند، یک خاصیت جدید را با همان نام المان مورد بررسی، به شیء errors اضافه کرده و پیام خطایی را درج می‌کنیم.
- در نهایت این شیء errors و یا نال را (در صورت عدم وجود خطایی) بازگشت می‌دهیم.

برای آزمایش آن، پس از اجرای برنامه، یکبار بدون وارد کردن اطلاعاتی، بر روی دکمه‌ی Login کلیک کنید؛ یکبار هم با وارد کردن اطلاعاتی در فیلدهای مختلف. در این بین کنسول توسعه دهندگان مرورگر را نیز جهت مشاهده‌ی شیء‌های error لاگ شده، بررسی نمائید.



نمایش خطاهای اعتبارسنجی فیلدهای فرم

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

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

export default Input;
- در اینجا ابتدا خاصیت error را به لیست خواص مورد انتظار از شیء props، اضافه کرده‌ایم.
- سپس با توجه به نکته‌ی «رندر شرطی عناصر در کامپوننت‌های React» در قسمت 5، اگر error مقداری داشته باشد و به true تفسیر شود، آنگاه به صورت خودکار، div ای که دارای کلاس‌های بوت استرپی اخطار است به همراه متن خطا، رندر خواهد شد؛ در غیراینصورت هیچ div ای به صفحه اضافه نمی‌شود.
- اکنون متد رندر کامپوننت فرم لاگین را به صورت زیر تکمیل می‌کنیم:
  render() {
    const { account, errors } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <Input
          name="username"
          label="Username"
          value={account.username}
          onChange={this.handleChange}
          error={errors.username}
        />
        <Input
          name="password"
          label="Password"
          value={account.password}
          onChange={this.handleChange}
          error={errors.password}
        />
        <button className="btn btn-primary">Login</button>
      </form>
    );
  }
در ابتدای متد رندر، با استفاده از Object Destructuring، خاصیت errors شیء منتسب به خاصیت state کامپوننت را دریافت کرده‌ایم. سپس با استفاده از آن، ویژگی جدید error را که به تعریف کامپوننت Input اضافه کردیم، در دو فیلد username و password، مقدار دهی می‌کنیم.

تا اینجا اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، با کلیک بر روی دکمه‌ی Login، خطاهای اعتبارسنجی به صورت زیر ظاهر می‌شوند:


در این حالت اگر هر دو فیلد را تکمیل کرده و بر روی دکمه‌ی لاگین کلیک کنیم، به خطای زیر در کنسول توسعه دهندگان مرورگر می‌رسیم:
loginForm.jsx:55 Uncaught TypeError: Cannot read property 'username' of null
at LoginForm.render (loginForm.jsx:55)
علت اینجاست که چون فرم اعتبارسنجی شده و مشکلی وجود نداشته‌است، خروجی متد validate در این حالت، null است. بنابراین دیگر نمی‌توان به خاصیت برای مثال username آن دسترسی یافت. برای رفع این مشکل در متد handleSubmit، جائیکه errors را در خاصیت state به روز رسانی می‌کنیم، اگر errors نال باشد، بجای آن یک شیء خالی را بازگشت می‌دهیم:
this.setState({ errors: errors || {} });
این قطعه کد، به این معنا است که اگر errors مقدار دهی شده بود، از آن استفاده کن، در غیراینصورت {} (یک شیء خالی جاوا اسکریپتی) را بازگشت بده.


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

تا اینجا نحوه‌ی اعتبارسنجی فیلدهای ورودی را در حین submit بررسی کردیم. شبیه به همین روش را به حالت onChange و متد handleChange فرم لاگین که در قسمت قبل تکمیل کردیم نیز می‌توان اعمال کرد:
  handleChange = ({ currentTarget: input }) => {
    const errors = { ...this.state.errors }; //cloning an object
    const errorMessage = this.validateProperty(input);
    if (errorMessage) {
      errors[input.name] = errorMessage;
    } else {
      delete errors[input.name];
    }

    const account = { ...this.state.account }; //cloning an object
    account[input.name] = input.value;

    this.setState({ account, errors });
  };
- ابتدا شیء errors را clone می‌کنیم؛ چون می‌خواهیم خواصی را به آن کم و زیاد کرده و سپس بر اساس آن مجددا state را به روز رسانی کنیم.
- سپس اینبار فقط نیاز داریم اعتبار اطلاعات ورودی یک فیلد را بررسی کنیم و متد validate فعلی، فیلدهای کل فرم را با هم تعیین اعتبار می‌کند. به همین جهت متد جدید validateProperty را به صورت زیر تعریف می‌کنیم. اگر این متد خروجی داشت، خاصیت متناظر با آن‌را در شیء errors به روز رسانی می‌کنیم؛ در غیراینصورت این خاصیت را از شیء errors حذف می‌کنیم تا پیام اشتباهی را نمایش ندهد. در نهایت توسط متد setState، مقدار خاصیت errors را با شیء errors جاری به روز رسانی می‌کنیم:
  validateProperty = ({ name, value }) => {
    if (name === "username") {
      if (value.trim() === "") {
        return "Username is required.";
      }
      // ...
    }

    if (name === "password") {
      if (value.trim() === "") {
        return "Password is required.";
      }
      // ...
    }
  };
در متد validateProperty، خواص name و value از شیء input ارسالی به آن استخراج شده‌اند و سپس بر اساس آن‌ها کار اعتبارسنجی صورت می‌گیرد.

پس از ذخیره سازی این تغییرات، برای آزمایش آن، یکبار حرف a را بجای username وارد کنید و سپس آن‌را حذف کنید. بلافاصله پیام خطای مرتبطی نمایش داده خواهد شد و اگر مجددا عبارتی را وارد کنیم، این پیام محو می‌شود که معادل قسمت delete در کدهای فوق است.


معرفی Joi


تا اینجا، هدف نمایش ساختار یک اعتبارسنج ساده بود. این روش مقیاس پذیر نیست و در ادامه آن‌را با یک کتابخانه‌ی اعتبارسنجی بسیار پیشرفته به نام Joi، جایگزین خواهیم کرد که نمونه مثال‌های آن‌را در اینجا می‌توانید مشاهده کنید:
const Joi = require('@hapi/joi');
const schema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required(),
    password: Joi.string().pattern(/^[a-zA-Z0-9]{3,30}$/),
    email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
});
schema.validate({ username: 'abc', birth_year: 1994 });
ایده‌ی اصلی Joi، تعریف یک اسکیما برای object جاوا اسکریپتی خود است. در این اسکیما، تمام خواص شیء مدنظر ذکر شده و سپس توسط fluent api آن، نیازمندی‌های اعتبارسنجی هرکدام ذکر می‌شوند. برای مثال username باید رشته‌ای بوده، تنها از حروف و اعداد تشکیل شود. حداقل طول آن، 3 و حداکثر طول آن، 30 باشد و همچنین ورود آن نیز اجباری است. با استفاده از pattern آن می‌توان عبارات باقاعده را ذکر کرد و یا با متدهایی مانند email، از قالب خاص مقدار یک خاصیت، اطمینان حاصل کرد.

برای نصب آن، پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install @hapi/joi --save
> npm i --save-dev @types/hapi__joi
که در نهایت سبب نصب کتابخانه‌ی node_modules\@hapi\joi\dist\joi-browser.min.js خواهند شد و همچنین TypeScript definitions آن‌را نیز نصب می‌کنند که بلافاصله سبب فعالسازی intellisense مخصوص آن در VSCode خواهد شد. بدون نصب types آن، پس از تایپ Joi.، از مزایای تکمیل خودکار fluent api آن توسط VSCode، برخوردار نخواهیم بود.

سپس به کامپوننت فرم لاگین مراجعه کرده و در ابتدای آن، Joi را import می‌کنیم:
import Joi from "@hapi/joi";
پس از آن، اسکیمای شیء account را تعریف خواهیم کرد. اسکیما نیازی نیست جزئی از state باشد؛ چون قرار نیست تغییر کند. به همین جهت آن‌را به صورت یک خاصیت جدید در سطح کلاس کامپوننت تعریف می‌کنیم:
  schema = {
    username: Joi.string()
      .required()
      .label("Username"),
    password: Joi.string()
      .required()
      .label("Password")
  };
خاصیت اسکیما را با یک شیء با ساختار از نوع Joi.object، که خواص آن، با خواص شیء account مرتبط با فیلدهای فرم لاگین، تطابق دارد، تکمیل می‌کنیم. مقدار هر خاصیت نیز با Joi. شروع شده و سپس نوع و محدودیت‌های مدنظر اعتبارسنجی را می‌توان تعریف کرد که در اینجا هر دو مورد باید رشته‌ای بوده و به صورت اجباری وارد شوند. توسط متد label، برچسب نام خاصیت درج شده‌ی در پیام خطای نهایی را می‌توان تنظیم کرد. اگر از این متد استفاده نشود، از همان نام خاصیت ذکر شده استفاده می‌کند.
سپس ابتدای متد validate قبلی را به صورت زیر بازنویسی می‌کنیم:
  validate = () => {
    const { account } = this.state;
    const validationResult = Joi.object(this.schema).validate(account, {
      abortEarly: false
    });
    console.log("validationResult", validationResult);
ابتدا مقدار خاصیت account، از شیء state استخراج شده‌است که حاوی شیءای با اطلاعات نام کاربری و کلمه‌ی عبور است. سپس این شیء را به متد validate خاصیت اسکیمایی که تعریف کردیم، ارسال می‌کنیم. Joi، اعتبارسنجی را به محض یافتن خطایی، متوقف می‌کند. به همین جهت تنظیم abortEarly آن به false صورت گرفته‌است تا تمام خطاهای اعتبارسنجی را نمایش دهد.
اکنون اگر برنامه را اجرا کرده و بدون ورود اطلاعاتی، بر روی دکمه‌ی لاگین کلیک کنیم، خروجی زیر در کنسول توسعه دهندگان مرورگر ظاهر می‌شود:


همانطور که مشاهده می‌کنید، خروجی Joi، یک شیء است که اگر دارای خاصیت error بود، یعنی خطای اعتبارسنجی رخ‌داده‌است. سپس باید خاصیت آرایه‌ای details این شیء error را جهت یافتن خواص مشکل دار بررسی کرد. هر خاصیت در اینجا با path مشخص می‌شود. بنابراین قدم بعدی، تبدیل این ساختار، به ساختار شیء errors موجود در state کامپوننت جاری است تا مابقی برنامه بتواند بدون تغییری از آن استفاده کند.


نگاشت شیء دریافتی از Joi، به شیء errors موجود در state کامپوننت لاگین

خاصیت error شیء دریافتی از متد validate کتابخانه‌ی Joi، تنها زمانی ظاهر می‌شود که خطایی وجود داشته باشد. همچنین خاصیت details آن نیز آرایه‌ا‌ی از اشیاء با خواص message و path است. این path نیز یک آرایه است که اولین المان آن، نام خاصیت در حال بررسی است. اکنون می‌خواهیم این آرایه را تبدیل به یک شیء قابل درک برای برنامه کنیم:
  validate = () => {
    const { account } = this.state;
    const validationResult = Joi.object(this.schema).validate(account, {
      abortEarly: false
    });
    console.log("validationResult", validationResult);

    if (!validationResult.error) {
      return null;
    }

    const errors = {};
    for (let item of validationResult.error.details) {
      errors[item.path[0]] = item.message;
    }
    return errors;
  };
ابتدا بررسی می‌کنیم که آیا خاصیت error، تنظیم شده‌است یا خیر؟ اگر خیر، کار این متد به پایان می‌رسد. سپس حلقه‌ای را بر روی آرایه‌ی details، تشکیل می‌دهیم تا شیء errors مدنظر ما را با خاصیت دریافتی از path و پیام دریافتی متناظری تکمیل کند. در آخر این شیء errors با ساختار مدنظر خود را بازگشت می‌دهیم.

اکنون اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، همانند قبل می‌توان خطاهای اعتبارسنجی در حین submit را مشاهده کرد:



بازنویسی متد validateProperty توسط Joi

تا اینجا متد validate ساده و ابتدایی خود را با استفاده از امکانات کتابخانه‌ی Joi، بازنویسی کردیم. اکنون نوبت بازنویسی متد اعتبارسنجی در حین تایپ اطلاعات است:
  validateProperty = ({ name, value }) => {
    const userInputObject = { [name]: value };
    const propertySchema = Joi.object({ [name]: this.schema[name] });
    const { error } = propertySchema.validate(userInputObject, {
      abortEarly: true
    });
    return error ? error.details[0].message : null;
  };
تفاوت این متد با متد validate، در اعتبارسنجی تنها یک خاصیت از شیء account موجود در state است؛ به همین جهت نمی‌توان کل this.state.account را به متد validate کتابخانه‌ی Joi ارسال کرد. بنابراین نیاز است بر اساس name و value رسیده‌ی از کاربر، یک شیء جدید را به صورت پویا تولید کرد. در اینجا روش تعریف { [name]: value } به computed properties معرفی شده‌ی در ES6 اشاره می‌کند. اگر در حین تعریف یک شیء، برای مثال بنویسیم {"username:"value}، این username به صورت "username" ثابتی تفسیر می‌شود و پویا نیست. اما در ES6 می‌توان با استفاده از [] ها، تعریف نام یک خاصیت را پویا کرد که نمونه‌ای از آن‌را در userInputObject و همچنین propertySchema مشاهده می‌کنید.
تا اینجا بجای this.state.account که به کل فرم اشاره می‌کند، شیء اختصاصی‌تر userInputObject را ایجاد کرده‌ایم که معادل اطلاعات فیلد ورودی کاربر است. یک چنین نکته‌ای را در مورد schema نیز باید رعایت کرد. در اینجا نمی‌توان اعتبارسنجی را با this.schema شروع کرد؛ چون این شیء نیز به اطلاعات کل فرم اشاره می‌کند و نه تک فیلدی که هم اکنون در حال کار با آن هستیم. بنابراین نیاز است یک Joi.object جدید را بر اساس name رسیده، از this.schema کلی، استخراج و تولید کرد؛ مانند propertySchema که مشاهده می‌کنید.
اکنون می‌توان متد validate این شیء اسکیمای جدید را فراخوانی کرد و خاصیت error شیء حاصل از آن‌را توسط Object Destructuring، استخراج نمود. در اینجا abortEarly به true تنظیم شده‌است (البته حالت پیش‌فرض آن است و نیازی به ذکر صریح آن نیست). علت اینجا است که نمایش خطاهای بیش از اندازه به کاربر، برای او گیج کننده خواهند بود؛ به همین جهت هربار، اولین خطای حاصل را به او نمایش می‌دهیم.


غیرفعالسازی دکمه‌ی submit در صورت شکست اعتبارسنجی‌های فرم

در ادامه می‌خواهیم دکمه‌ی submit فرم لاگین، تا زمانیکه اعتبارسنجی آن با موفقیت به پایان برسد، غیرفعال باشد:
 <button disabled={this.validate()} className="btn btn-primary">
 Login
</button>
ویژگی disabled را اگر به true تنظیم کنیم، این دکمه را غیرفعال می‌کند. در اینجا متد validate تعریف شده، نال و یا یک شیء را بازگشت می‌دهد. اگر نال را بازگشت دهد، در جاوااسکریپت به false تفسیر شده و دکمه را فعال می‌کند و برعکس؛ مانند تصویر حاصل زیر:


فراخوانی setState‌های تعریف شده‌ی در متدهای رویدادگردان این فرم هستند که سبب رندر مجدد کامپوننت شده و در این بین در متد رندر، کار بررسی مجدد مقدار نهایی متد validate صورت می‌گیرد تا بر اساس آن، فعال یا غیرفعال بودن دکمه‌ی Login، مشخص شود.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-19.zip
مطالب
جلوگیری از ارسال Spam در ASP.NET MVC
در هر وب‌سایتی که فرمی برای ارسال اطلاعات به سرور موجود باشد، آن وب سایت مستعد ارسال اسپم و بمباران درخواست‌های متعدد خواهد بود. در برخی موارد استفاده از کپچا می‌تواند راه خوبی برای جلوگیری از ارسال‌های مکرر و مخرب باشد، ولی گاهی اوقات سناریوی ما به شکلی است که امکان استفاده از کپچا، به عنوان یک مکانیزم امنیتی مقدور نیست.
اگر شما یک فرم تماس با ما داشته باشید استفاده از کپچا یک مکانیزم امنیتی معقول می‌باشد و همچنین اگر فرمی جهت ارسال پست داشته باشید. اما در برخی مواقع مانند فرمهای ارسال کامنت، پاسخ، چت و ... امکان استفاده از این روش وجود ندارد و باید به فکر راه حلی مناسب برای مقابل با درخواست‌های مخرب باشیم.
اگر شما هم به دنبال تامین امنیت سایت خود هستید و دوست ندارید که وب سایت شما (به دلیل کمبود پهنای باند یا ارسال مطالب نامربوط که گاهی اوقات به صدها هزار مورد می‌رسد) از دسترس خارج شود این آموزش را دنبال کنید.
برای این منظور ما از یک ActionFilter برای امضای ActionMethodهایی استفاده می‌کنیم که باید با ارسالهای متعدد از سوی یک کاربر مقابله کنند. این ActionFilter  باید قابلیت تنظیم حداقل زمان بین درخواستها را داشته باشد و اگر درخواستی در زمانی کمتر از مدت مجاز تعیین شده برسد، به نحوی مطلوبی به آن رسیدگی کند.
پس از آن ما نیازمند مکانیزمی هستیم تا درخواست‌های رسیده‌ی از سوی هرکاربر را به شکلی کاملا خاص و یکتا شناسایی کند. راه حلی که قرار است در این ActionFilter  از آن استفاده کنم به شرح زیر است:
ما به دنبال آن هستیم که یک شناسه‌ی منحصر به فرد را برای هر درخواست ایجاد کنیم. لذا از اطلاعات شیئ Request جاری برای این منظور استفاده می‌کنیم.
1) IP درخواست جاری (قابل بازیابی از هدر HTTP_X_FORWARDED_FOR یا REMOTE_ADDR)
2) مشخصات مرورگر کاربر (قابل بازیابی از هدر USER_AGENT)
3) آدرس درخواست جاری (برای اینکه شناسه‌ی تولیدی کاملا یکتا باشد، هرچند می‌توانید آن را حذف کنید)

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

مرحله بعد پیاده سازی مکانیزمی برای نگهداری این اطلاعات و بازیابی آن‌ها در هر درخواست است. ما برای این منظور از سیستم Cache استفاده می‌کنیم؛ هرچند راه حل‌های بهتری هم وجود دارند.
بنابراین پس از ایجاد شناسه یکتای درخواست، آن را در Cache قرار می‌دهیم و زمان انقضای آن را هم پارامتری که ابتدای کار گفتم قرار می‌دهیم. سپس در هر درخواست Cache را برای این مقدار یکتا جستجو می‌کنیم. اگر شناسه پیدا شود، یعنی در کمتر از زمان تعیین شده، درخواست مجددی از سوی کاربر صورت گرفته است و اگر شناسه در Cache موجود نباشد، یعنی درخواست رسیده در زمان معقولی صادر شده است.
باید توجه داشته باشید که تعیین زمان بین هر درخواست به ازای هر ActionMethod خواهد بود و نباید آنقدر زیاد باشد که عملا کاربر را محدود کنیم. برای مثال در یک سیستم چت، زمان معقول بین هر درخواست 5 ثانیه است و در یک سیستم ارسال نظر یا پاسخ، 10 ثانیه. در هر حال بسته به نظر شما این زمان می‌تواند قابل تغییر باشد. حتی می‌توانید کاربر را مجبور کنید که در روز فقط یک دیدگاه ارسال کند!

قبل از پیاده سازی سناریوی فوق، در مورد نقش گزینه‌ی سوم در شناسه‌ی درخواست، لازم است توضیحاتی بدهم. با استفاده از این خصوصیت (یعنی آدرس درخواست جاری) شدت سختگیری ما کمتر می‌شود. زیرا به ازای هر آدرس، شناسه‌ی تولیدی متفاوت خواهد بود. اگر فرد مهاجم، برنامه‌ای را که با آن اسپم می‌کند، طوری طراحی کرده باشد که مرتبا درخواست‌ها را به آدرس‌های متفاوتی ارسال کند، مکانیزم ما کمتر با آن مقابله خواهد کرد.
برای مثال فرد مهاجم می‌تواند در یک حلقه، ابتدا درخواستی را به AddComment بدهد، بعد AddReply و بعد SendMessage. پس همانطور که می‌بینید اگر از پارامتر سوم استفاده کنید، عملا قدرت مکانیزم ما به یک سوم کاهش می‌یابد.
نکته‌ی دیگری که قابل ذکر است اینست که این روش راهی برای تشخیص زمان بین درخواست‌های صورت گرفته از کاربر است و به تنهایی نمی‌تواند امنیت کامل را برای مقابله با اسپم‌ها، مهیا کند و باید به فکر مکانیزم دیگری برای مقابله با کاربری که درخواست‌های نامعقولی در مدت زمان کمی می‌فرستد پیاده کنیم (پیاده سازی مکانیزم تکمیلی را در آینده شرح خواهم داد).
اکنون نوبت پیاده سازی سناریوی ماست. ابتدا یک کلاس ایجاد کنید و آن را از ActionFilterAttribute مشتق کنید و کدهای زیر را وارد کنید:
using System;
using System.Linq;
using System.Web.Mvc;
using System.Security.Cryptography;
using System.Text;
using System.Web.Caching;

namespace Parsnet.Core
{
    public class StopSpamAttribute : ActionFilterAttribute
    {
        // حداقل زمان مجاز بین درخواست‌ها برحسب ثانیه
        public int DelayRequest = 10;

        // پیام خطایی که در صورت رسیدن درخواست غیرمجاز باید صادر کنیم
        public string ErrorMessage = "درخواست‌های شما در مدت زمان معقولی صورت نگرفته است.";

        //خصوصیتی برای تعیین اینکه آدرس درخواست هم به شناسه یکتا افزوده شود یا خیر
        public bool AddAddress = true;


        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // درسترسی به شئی درخواست
            var request = filterContext.HttpContext.Request;

            // دسترسی به شیئ کش
            var cache = filterContext.HttpContext.Cache;

            // کاربر IP بدست آوردن
            var IP = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;

            // مشخصات مرورگر
            var browser = request.UserAgent;

            // در اینجا آدرس درخواست جاری را تعیین می‌کنیم
            var targetInfo = (this.AddAddress) ? (request.RawUrl + request.QueryString) : "";

            // شناسه یکتای درخواست
            var Uniquely = String.Concat(IP, browser, targetInfo);


            //در اینجا با کمک هش یک امضا از شناسه‌ی درخواست ایجاد می‌کنیم
            var hashValue = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(Uniquely)).Select(s => s.ToString("x2")));

            // ابتدا چک می‌کنیم که آیا شناسه‌ی یکتای درخواست در کش موجود نباشد
            if (cache[hashValue] != null)
            {
                // یک خطا اضافه می‌کنیم ModelState اگر موجود بود یعنی کمتر از زمان موردنظر درخواست مجددی صورت گرفته و به
                filterContext.Controller.ViewData.ModelState.AddModelError("ExcessiveRequests", ErrorMessage);
            }
            else
            {
                // اگر موجود نبود یعنی درخواست با زمانی بیشتر از مقداری که تعیین کرده‌ایم انجام شده
                // پس شناسه درخواست جدید را با پارامتر زمانی که تعیین کرده بودیم به شیئ کش اضافه می‌کنیم
                cache.Add(hashValue, true, null, DateTime.Now.AddSeconds(DelayRequest), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
            }

            base.OnActionExecuting(filterContext);
        }
    }
}
و حال برای استفاده از این مکانیزم امنیتی ActionMethod مورد نظر را با آن امضا می‌کنیم:
[HttpPost]
        [StopSpam(DelayRequest = 5)]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> SendFile(HttpPostedFileBase file, int userid = 0)
        { 
        
        }

[HttpPost]
        [StopSpam(DelayRequest = 30, ErrorMessage = "زمان لازم بین ارسال هر مطلب 30 ثانیه است")]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> InsertPost(NewPostModel model)
        {
     
        }

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

به توجه به دیدگاه‌های مطرح شده اصلاحاتی در کلاس صورت گرفت و قابلیتی به آن اضافه گردید که بتوان مکانیزم اعتبارسنجی را کنترل کرد.
برای این منظور خصوصیتی به این ActionFilter افزوده شد تا هنگامیکه داده‌های فرم معتبر نباشند و در واقع هنوز چیزی ثبت نشده است این مکانیزم را بتوان کنترل کرد. خصوصیت CheckResult باعث میشود تا اگر داده‌های مدل ما در اعتبارسنجی، معتبر نبودند کلید افزوده شده به کش را حذف تا کاربر بتواند مجدد فرم را ارسال کند. مقدار آن به طور پیش فرض true است و اگر برابر false قرار بگیرد تا اتمام زمان تعیین شده در مکانیزم ما، کاربر امکان ارسال مجدد فرم را ندارد.
همچنین باید بعد از اتمام عملیات در صورت عدم موفقیت آمیز بودن آن به ViewBag یک خصوصیت به نام ExecuteResult اضافه کنید و مقدار آن را برابر false قرار دهید. تا کلید از کش حذف گردد.
نحوه استفاده آن هم به شکل زیر می‌باشد:
        [HttpPost]
        [StopSpam(AddAddress = true, DelayRequest = 20)]
        [ValidateAntiForgeryToken]
        public Task<ActionResult> InsertPost(NewPostModel model)
        {
            if (ModelState.IsValid)
            {
                var newPost = dbContext.InsertPost(model);
                if (newPost != null)
                {
                    ViewBag.ExecuteResult = true;
                }
            }

            if (ModelState.IsValidField("ExcessiveRequests") == true)
{
ViewBag.ExecuteResult = false;
}
return View(); }

فایل ضمیمه را می‌توانید از زیر دانلود کنید:
StopSpamAttribute.rar
نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
میخوام یه پروژه MVC رو به تمپلیت Skoruba به عنوان یک کلاینت جدید اضافه کنم. تو تنظیمات Sturtup پروژه Mvc اینگونه عمل کردم:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            IdentityModelEventSource.ShowPII = true;
            services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
       options.DefaultSignOutScheme = "oidc";
                })
                .AddCookie("Cookies", options =>
                {
                    options.AccessDeniedPath = "/Authorization/AccessDenied";
                    // set session lifetime
                    options.ExpireTimeSpan = TimeSpan.FromHours(8);
                    // sliding or absolute
                    options.SlidingExpiration = false;
                    // host prefixed cookie name
                    options.Cookie.Name = "MVC";
                    // strict SameSite handling
                    options.Cookie.SameSite = SameSiteMode.Strict;
                })
                .AddOpenIdConnect("oidc", options =>
                {
                    options.SignInScheme = "Cookies";
                    options.Authority = Configuration["IDPBaseAddress"]; 
                    options.ClientId = Configuration["ClientId"];
                    options.ClientSecret = Configuration["ClientSecret"];                      
                    options.ResponseType = "code id_token";
                    options.ResponseMode = "query";

                    options.RequireHttpsMetadata = false;
                    options.CallbackPath = new PathString("/Home/");
                    options.SignedOutCallbackPath = new PathString("/Home/");

                    options.MapInboundClaims = true;

                    options.Scope.Clear();
                    options.Scope.Add("openid");
                    options.Scope.Add("profile");
                    options.Scope.Add("roles");
                    options.Scope.Add("PS.WebApi.Read");
                    options.Scope.Add("offline_access");
                    
                    options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;
                    //options.UsePkce = true;
                    //options.ClaimActions.MapJsonKey(claimType: "role", jsonKey: "role"); // for having 2 or more roles
                    
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        NameClaimType = JwtClaimTypes.GivenName,
                        RoleClaimType = JwtClaimTypes.Role
                    };
                });
            //ServicePointManager.Expect100Continue = true;
            //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
            //                                       | SecurityProtocolType.Tls11
            //                                       | SecurityProtocolType.Tls12
            //                                       | SecurityProtocolType.Ssl3;
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "areas",
                    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
                );
                //.RequireAuthorization();

                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}"
                );
                //.RequireAuthorization();
            });

            //[HttpPost]
            //public IActionResult Logout()
            //{
            //    return SignOut("Cookies", "oidc");
            //}
        }

پروژه‌های بنده به ترتیب Endpoint هاشون اینگونه هست:
Skoruba.IdentityServer4.Admin  = https://localhost:44303
Skoruba.IdentityServer4.STS.Identity   = https://localhost:44310
Skoruba.IdentityServer4.Admin.Api    = https://localhost:44356 
Mvc_Client_Project = https://localhost:44332
تنظیمات کانفیک پروژه MVC: (appsettings.json)
  "WebApiBaseAddress": "https://localhost:44356",
  "IDPBaseAddress": "https://localhost:44310",
  "ClientId": "Mvc_ClientId",
  "ClientSecret": "WebMvc"
محتویات فایل Identityserverdata.json:

{
    "IdentityServerData": {
        "IdentityResources": [
            {
                "Name": "roles",
                "Enabled": true,
                "DisplayName": "Roles",
                "UserClaims": [
                    "role"
                ]
            },
            {
                "Name": "openid",
                "Enabled": true,
                "Required": true,
                "DisplayName": "Your user identifier",
                "UserClaims": [
                    "sub"
                ]
            },
            {
                "Name": "profile",
                "Enabled": true,
                "DisplayName": "User profile",
                "Description": "Your user profile information (first name, last name, etc.)",
                "Emphasize": true,
                "UserClaims": [
                    "name",
                    "family_name",
                    "given_name",
                    "middle_name",
                    "nickname",
                    "preferred_username",
                    "profile",
                    "picture",
                    "website",
                    "gender",
                    "birthdate",
                    "zoneinfo",
                    "locale",
                    "updated_at"
                ]
            },
            {
                "Name": "email",
                "Enabled": true,
                "DisplayName": "Your email address",
                "Emphasize": true,
                "UserClaims": [
                    "email",
                    "email_verified"
                ]
            },
            {
                "Name": "address",
                "Enabled": true,
                "DisplayName": "Your address",
                "Emphasize": true,
                "UserClaims": [
                    "address"
                ]
            }
        ],
        "ApiScopes": [
          {
            "Name": "Idp_Admin_ClientId_api",
            "DisplayName": "Idp_Admin_ClientId_api",
            "Required": true,
            "UserClaims": [
              "role",
              "name"
            ]
          },
          {
            "Name": "WebApi.Read",
            "DisplayName": "WebApi Read",
            "Required": true,
            "UserClaims": [
              "role",
              "WebApi.Read"
            ]
          },
          {
            "Name": "WebApi.Write",
            "DisplayName": "WebApi Write",
            "Required": true,
            "UserClaims": [
              "role",
              "WebApi.Write"
            ]
          }
        ],
        "ApiResources": [
          {
            "Name": "Idp_Admin_ClientId_api",
            "Scopes": [
              "Idp_Admin_ClientId_api"
            ]
          },
          {
            "Name": "WebApi",
            "Scopes": [
              "WebApi.Read",
              "WebApi.Write"
            ]
          }
        ],
      "Clients": [
        {
          "ClientId": "Idp_Admin_ClientId",
          "ClientName": "Idp_Admin_ClientId",
          "ClientUri": "https://localhost:44303",
          "AllowedGrantTypes": [
            "authorization_code"
          ],
          "RequirePkce": true,
          "ClientSecrets": [
            {
              "Value": "Idp_Admin_ClientSecret"
            }
          ],
          "RedirectUris": [
            "https://localhost:44303/signin-oidc"
          ],
          "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc",
          "PostLogoutRedirectUris": [
            "https://localhost:44303/signout-callback-oidc"
          ],
          "AllowedCorsOrigins": [
            "https://localhost:44303"
          ],
          "AllowedScopes": [
            "openid",
            "email",
            "profile",
            "roles"
          ]
        },
        {
          "ClientId": "Idp_Admin_ClientId_api_swaggerui",
          "ClientName": "Idp_Admin_ClientId_api_swaggerui",
          "AllowedGrantTypes": [
            "authorization_code"
          ],
          "RequireClientSecret": false,
          "RequirePkce": true,
          "RedirectUris": [
            "https://localhost:44302/swagger/oauth2-redirect.html"
          ],
          "AllowedScopes": [
            "Idp_Admin_ClientId_api"
          ],
          "AllowedCorsOrigins": [
            "https://localhost:44302"
          ]
        },
        //WebApi
        {
          "ClientId": "WebApi_ClientId",
          "ClientName": "WebApi_ClientId",
          "ClientUri": "https://localhost:44365",
          "AllowedGrantTypes": [
            "authorization_code"
          ],
          "RequirePkce": true,
          "ClientSecrets": [
            {
              "Value": "WebApi"
            }
          ],
          "RedirectUris": [
            "https://localhost:44303/signin-oidc"
          ],
          "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc",
          "PostLogoutRedirectUris": [
            "https://localhost:44303/signout-callback-oidc"
          ],
          "AllowedCorsOrigins": [
            "https://localhost:44303",
            "https://localhost:44310"
          ],
          "AllowedScopes": [
            "openid",
            "email",
            "profile",
            "roles"
          ]
        },
        //Mvc
        {
          "ClientId": "Mvc_ClientId",
          "ClientName": "Mvc_ClientId",
          "ClientUri": "https://localhost:44332",
          "AllowedGrantTypes": [
            "hybrid"
          ],
          //"RequirePkce": true,
          "AllowPlainTextPkce": false,
          "ClientSecrets": [
            {
              "Value": "WebMvc"
            }
          ],

          "RedirectUris": [
            "https://localhost:44332/signin-oidc"
          ],
          "FrontChannelLogoutUri": "https://localhost:44332/signout-oidc",
          "PostLogoutRedirectUris": [
            "https://localhost:44332/signout-callback-oidc"
          ],
          "AllowedCorsOrigins": [
            "https://localhost:44332",
            "https://localhost:44310"
          ],
          "AllowedScopes": [
            "openid",
            "email",
            "profile",
            "roles",
            "address",
            "PS.webApi"
          ],
          "AllowAccessTokensViaBrowser": true,
          "RequireConsent": false,
          "AllowOfflineAccess": true
          //"UpdateAccessTokenClaimsOnRefresh": true
        }
      ]
    }
}

کنترلر Home در پروژه MVC:
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult Default()
        {
            return View();
        }
        public IActionResult Index()
        {
            return View();
        }

        [Authorize]
        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }

اما در نهایت بعد از اجرا و مراجعه به آدرس https://localhost:44332/home/privacy که مزین به اتریبیوت
[Authorize]  
می‌باشد با خطای زیر مواجه می‌شوم:

لازم به توضیح هست که پروپرتی RequireHttpsMetadata = false می‌باشد.

مطالب
Performance در AngularJS قدم دوم
در مقاله‌ی قبل روش درست استفاده کردن از Binding را برای بهبود Performance، توضیح دادم. در این مقاله می‌خواهم در مورد ng-if و فرق آن با ng-show صحبت کنم و اینکه کدامیک Performance بهتری را برای AngularJS فراهم می‌کنند.

سول اول، کار ng-show چیست؟
ng-show یکی از پر کاربردترین Directiveهای AngularJS است که وظیفه‌ی Show و Hide قسمتی از Vew را به عهده دارد. به کد زیر توجه کنید:
<div ng-show="has">
     <div ng-repeat="item in items">
          <span>{{item.title}}</span>
     </div>
</div>
در این کد ما از ng-show استفاده کرده‌ایم. اگر has مقدار true داشته باشد div نمایش داده می‌شود و اگر false باشد، div نمایش داده نمی‌شود. در داخل div، لیستی وجود دارد که توسط ng-repeat تکرار شده و یک لیست ساده را درست می‌کند.

سول دوم، کار ng-if چیست؟  
کد بالا را دوباره تکرار می‌کنیم. ولی با این تفاوت که اینبار بجای ng-show از ng-if استفاده خواهیم کرد:
<div ng-if="has">
     <div ng-repeat="item in items">
          <span>{{item.title}}</span>
     </div>
</div>
خوب؟ آیا عملکرد این دو کد با هم تفاوت دارد؟ جواب سؤال در ظاهر خیر هست. یعنی مانند کد بالایی، اگر has مقدار true داشته باشد، div نمایش داده می‌شود؛ در غیر اینصورت، خیر.

سوال سوم، پس اگر عملکرد یکسانی دارند، تفاوت آن‌ها در چیست؟
تفاوت این دو Directive در Performance هست. اجازه دهید بیشتر توضیح دهم. ng-show اگر مقدار false دریافت کند، tag مورد نظر را نمایش نمی‌دهد؛ ولی تگ‌های داخلی آن توسط AngularJS پردازش می‌شوند. یعنی چه ng-show مقدار true بگیرید و یا false، لیست داخل tag، توسط AngularJS پردازش و Render می‌شود. ولی در ظاهر ما در View چیزی را نمی‌بینیم. ng-if بر حسب مقادیری که دریافت می‌کند، می‌تواند به بالا رفتن Performance AngularJS کمک کند. فرض کنید ng-if مقدار false گرفته است؛ یعنی has مقدار false دارد. علاوه بر اینکه tag div نمایش داده نمی‌شود، بلکه داخل tag نیز پردازش نمی‌شود. یعنی لیستی که ما در کد نوشته‌ایم، به هیچ عنوان توسط AngularJS پردازش نخواهد شد که باعث می‌شود Watcher، کار کمتری انجام دهد. پس در نتیجه بهبودی را در کارآیی Rendering و Binding خواهیم داشت.
مطالب
آشنایی با Directive ها در AngularJs
اگر مطالعه ای اجمالی درباره مزیت‌ها و قدرت‌های فریم ورک Angular داشته باشید یکی از مواردی که بسیار جلب توجه می‌کند مبحث Directive‌ها است. به کمک Directive‌ها در Angular می‌توانید کد‌های HTML خود را توسعه دهید. این توسعه علاوه بر تعریف تگ‌های جدید، شامل توسعه کلاس‌ها و همچنین ویژگی‌های تگ‌های HTML نیز خواهد بود. کد‌های HTML شما بسیار خواناتر و از طرفی با قابلیت استفاده مجدد می‌شود. البته این پست فقط شروع به کار در این مقوله است زیرا مبحث Directive‌ها بسیار گسترده‌تر از آن است که بتوان مطالب آن را در  یک مقاله گنجاند.
برای شروع یک فایل جاوا اسکریپ ایجاد کرده و در ابتدای آن یک ماژول تعریف کنید:
var app = angular.module('myApp', []);
در این جا نام ماژول را myApp انتخاب کردم. حال یک Directive به نام angry (نام دیرکتیو‌ها را با حروف کوچک آغاز کنید )به صورت زیر ایجاد می‌کنیم:
app.directive('angry', function () {
    return {
        restrict: 'E',
        template: '<div style="color:red"> I am angry!</div>'
    }
})
 تابع سازنده Directive مورد نظر که یک آبجکت را برگشت می‌دهد شامل خواص زیر می‌باشد:
restrict: که چهار مقدار E و A و C و M را می‌پذیرد که به EACM نیز معروف هستند.
E: زمانی که قصد داشته باشیم یک المان جدید بسازیم از E به معنای element در restrict استفاده می‌کنیم(<my-directive></my-directive> 
 A: زمانی که قصد داشته باشیم Directive مورد نظر به عنوان Attribute در تگ‌ها استفاده شود از A به معنای Attribute در restrict استفاده می‌شود(<div my-directive="exp"></div> 
C: از C نیز برای تعریف Directive به عنوان مقادیر ویژگی کلاس استفاده می‌کنیم(<div class="my-directive: exp "></div> 
M: حالت M  نیز برای استفاده Directive در کامنت‌ها است(<!-- directive: my-directive exp -->  ).
در ادامه یک Directive  دیگر به نام happy می‌سازیم:
app.directive('happy', function () {
    return {
        restrict: 'A',
        template: '<div style="color:blue"> I am happy!</div>'
    };
})
تفاوت اصلی بین این دو Directive در نوع restrict آن‌ها می‌باشد. برای استفاده از این Directive‌ها در View می‌توان به صورت زیر عمل نمود:
<script type="text/javascript" src="~/scripts/Modules/module1.js"></script>

<div ng-app="myApp">
    <angry></angry>
    <div happy></div>
</div>
همان طور که می‌بینید دستور Directive اول که را با restrict از نوع E است می‌توان به عنوان یک تگ جدید استفاده کرد. دستور happy به عنوان ویژگی تگ div مورد استفاده قرار می‌گیرد(به دلیل اینکه restrict آن از نوع A است) که در نهایت خروجی ساده مثال بالا به صورت زیر خواهد بود:



ادامه دارد...
بازخوردهای پروژه‌ها
استفاده از pdfreport برای اولین بار
سلام
آقای نصیری از پروژه تون خیلی خیلی تشکر می‌کنم. واقعا عالیه. تو فکر بودم که چه جوری تحت وب گزارش گیری کنم بدون crystal report.
راستش خیلی از مقاله‌ها رو خوندم اما واقعا گیج شدم.
توی مقاله معرفی کتابخانه PdfReport گفتید کلاسی به اسم مثلا User ایجاد کنیم توی یه ClassLibrary. سوالم اینه که من که از ef استفاده می‌کنم و کلاس هایی هم توی DomainClass دارم دوباره باید برای گزارش گیری این کلاس‌ها رو ایجاد کنم؟
درکل چه جوری تحت وب (Web Form) از این کتابخانه استفاده کنم؟ اگه راهنمایی کلی می‌خوام که به چه نحوی باید گزارش ایجاد کنیم. انقد گشتم سرم گیج میره.
مطالب
بیلد سیستم گریدل Gradle Build System
امروزه در بسیاری از محیط‌های برنامه نویسی جاوا و اندروید، استفاده از این سیستم رایج است. ولی هنوز دیده می‌شود عده‌ای نسبت به آن دید روشنی ندارند و برای آن‌ها ناشناخته است و در حد یک سیستم کانفیگ آن را می‌شناسند. در این مقاله قصد داریم که مفهوم روشن‌تری از این سیستم را داشته باشم و بدانیم هدف آن چیست و چگونه کار می‌کند تا از این به بعد دیگر آن را به چشم یک کانفیگ کننده‌ی ساده نگاه نکنیم.  قبل از هر چیزی بهتر است که با تعدادی از اصطلاحات آن آشنا شویم تا در متن مقاله به مشکل برنخوریم:

DSL یا Domain Specific languages به معنی زبان‌هایی با دامنه محدود است که برای اهداف خاصی نوشته می‌شوند و تنها بر روی یک جنبه از هدف تمرکز دارند. این زبان‌ها به شما اجازه نمی‌دهند که یک برنامه را به طور کامل با آن بنویسید. بلکه به شما اجازه می‌دهند به هدفی که برای آن نوشته شده‌اند، برسید. یکی از این زبان‌ها همان  css هست که با آن کار میکنید. این زبان به صورت محدود تنها بر روی یک جنبه و آن، تزئین سازی المان‌های وب، تمرکز دارد. در وقع مثل زبان سی شارپ همه منظوره نیست و محدوده‌ای مشخص برای خود دارد. به این نوع از زبان‌های DSL، نوع اکسترنال هم می‌گویند. چون زبانی مستقل برای خود است و به زبان دیگری وابستگی ندارد. ولی در یک زبان اینترنال، وابستگی به زبان دیگری وجود دارد. مثل Fluent Interface‌ها که به ما شیوه آسانی از دسترسی به جنبه‌های یک شیء را می‌دهد. برای آشنایی هر چه بیشتر با این زبان‌ها و ساختار آن، کتاب Domain Specific languages نوشته آقای مارتین فاولر توصیه می‌شود.

Groovy یک زبان شیء گرای DSL هست که برای پلتفرم جاوا ساخته شده است. برای اطلاعات بیشتر در مورد این زبان، صفحه ویکی ، میتواند مفید واقع شود.

از دیرباز سیستم‌های Ant و Maven وجود داشتند و کار آن‌ها اتوماسیون بعضی اعمال بود. ولی بعد از مدتی سیستم Gradle یا جمع کردن نقاط قوت آن‌ها و افزودن ویژگی‌های قدرتمندتری به خود، پا به میدان گذاشت تا راحتی بیشتری را برای برنامه نویس فراهم کند. از ویژگی‌های گریدل می‌توان داشتن زبان گرووی اشاره کرده که قدرت بیشتری را نسبت به سایر سیستم‌ها داشت و مزیت مهم دیگر این بود که انعطاف بالایی را جهت افزودن پلاگین‌ها داشت و گوگل با استفاده از این قابلیت،  پشتیبانی از گریدل را در اندروید استادیو نیز گنجاند تا راحتی بیشتری را در اتوماسیون وظایف سیستمی ایجاد کند. در واقع آنچه شما در سیستم گریدل کار میکنید و اطلاعات خود را با آن کانفیگ میکنید، پلاگینی است که از سمت گوگل در اختیار شما قرار گرفته است و در مواقع خاص این وظایف توسط پلاگین‌ها اجرا می‌شوند.
گریدل به راحتی از سایت رسمی آن قابل دریافت است و می‌توان آن را در پروژه‌های جاوایی که مدنظر شماست، دریافت کنید و با استفاده از خط فرمان، با آن تعامل کنید. هر چند امروزه اکثر ویراستارهای جاوا از آن پشتیبانی می‌کنند.

گریدل یک ماهیت توصیفی دارد که شما تنها لازم است اعمالی را برای آن توصیف کنید تا بقیه کارها را انجام دهد. گریدل در پشت صحنه از یک "گراف جهت دار بدون دور" Directed Acycllic Graph یا به اختصار DAG  استفاده می‌کند و طبق آن ترتیب وظایف یا task‌ها را دانسته و آن‌ها را اجرا می‌کند. گریدل با این DAG، سه فاز آماده سازی، پیکربندی و اجرا را انجام می‌دهد.
  • در مرحله آماده سازی ما به گریدل می‌گوییم چه پروژه یا پروژه‌هایی نیاز به بیلد شدن دارند. در اندروید استادیو،  این مرحله در فایل settings.gradle انجام می‌شود؛ شما در این فایل مشخص می‌کنید چه پروژه‌های نیاز به بیلد شدن توسط گریدل دارند. ساختار این فایل به این شکل است:
include ':ActiveAndroid-master', ':app', ':dbutilities'
در این فایل سه پروژه برای گریدل مشخص شده‌اند. البته از نگاه Intellij سه ماژول معرفی شده‌اند و این فایل برای یک پروژه اختیاری است. گریدل برای پیدا کردن این فایل، از الگوریتم‌های متفاوتی استفاده می‌کند.
  1. در اولین مرحله انتظار دارد که فایل settings در دایرکتوری جاری باشد و اگر آن را پیدا کرد آن را مورد استفاده قرار می‌دهد؛ در غیر اینصورت مرحله بعدی را آغاز می‌کند.
  2. در مرحله دوم،  در این دایرکتوری به دنبال دایرکتوری به نام master میگردد و اگر در آن هم یافت نکرد مرحله سوم را آغاز می‌کند.
  3. در مرحله سوم، جست و جو در دایرکتوری والد انجام می‌شود
  4. چنانچه این فایل را در هیچ یک از احتمالات بالا نیابد، همین پروژه جاری را تشخیص خواهد داد.
اسامی پروژه‌های بالا با این تفکر هستند که در دایرکتوری یا فضای کاری جاری قرار گرفته‌اند که به آن IncludeFlat می‌گویند. ولی چنانچه آدرس پروژه‌ای در وضعیت خاص قرار بگیرد، می‌توان اینگونه مسیر آن را تغییر داد:
include ':ActiveAndroid-master', ':app', ':dbutilities'
project('dbutilities').projectDir=new File(settingsDir,'../dir1/dir2');
الان پروژه dbutiilities در سطح بالاتری از دایرکتوری جاری قرار گرفته است.

  • در مرحله پیکربندی، وظایف یا task‌ها را معرفی می‌کنیم. این عمل پیکربندی توسط فایل build.gradle که برای پروژه اصلی و هر زیر پروژه‌ای که مشخص شده‌اند، صورت می‌گیرد. در این فایل شما می‌توانید خواص و متدهایی را تعریف و و ظایفی را مشخص کنید.
    در پروژه اصلی، فایل BuildGradle شامل خطوط زیر است:
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
    }
}

allprojects {
    repositories {
        jcenter()

    }
}
در کلوژر buildscript مخازنی را که کتابخانه‌های نامبرده (وابستگی ها) در این کلوژ قرار میگیرند، معرفی میکنیم. در کلوژر بعدی تنظمیاتی را که برای همه پروژه‌ها اعمال می‌شوند، انجام می‌دهیم که در آن به معرفی مخزن jcenter پرداختیم. کتابخانه‌ای که در کلوژر buildscript صدا زدیم،  همان پلاگینی است که گوگل برای گریدل منتشر کرده که ما به آن ابزار بیلد می‌گوییم. حال به سراغ دیگر فایل‌های منحصر به فرد هر پروژه بروید. در این فایل‌ها،  شما تنظیمات پیش‌فرضی را می‌بینید که یکی از آن‌ها نسخه بندی پروژه است. پروژه‌های وابسته‌ای را که از مخازن باید دریافت شوند، شامل می‌شود و بسیاری از گزینه‌های دیگری که برای شما مهیا شده است تا در فاز پیکربندی، وظایف را بسازید.

در مرحله اجرا هم این وظایف را اجرا میکنیم.  تمامی این سه عملیات توسط فایل و دستوری به نام gradlew که برگرفته از gradleWrapper می‌باشد انجام می‌شود. اگر در ترمینال اندروید استادیو این عبارت را تایپ کنید، می‌توانید در ادامه دستور پیام‌های مربوط به این عملیات را ببینید و ترتیب اجرای فازها را مشاهده کنید.
بیایید یک task را تعریف کنیم
task mytask <<{
    println ".net tips task in config phase"
}
در ابتدا عبارت task را به عنوان معرفی task می‌آوریم و سپس نام آن را وارد می‌کنیم. بعد از آن ما از عبارت‌های شیفت چپ>> استفاده کردیم. این عبارت شیفت چپ به این معناست که تسک مربوطه را آخر از همه وظایف اجرا کن که این عمل از طریق اجرای متدی به نام doLast صورت میگیرد. اگر در ترمینال اندروید عبارت زیر را تایپ کنید، متن مورد نظر باید نمایش یابد:
gradlew mytask
برای نمایش اطلاعات بیشتر می‌توانید از فلگ info هم استفاده کنید:
gradlew --info mytask
حال شاید بگویید من در بعضی از سایت‌ها یا مستندات و یا پروژه‌های دیگر دیده‌ام که عبارت >> را قرار نمی‌دهند. در این مورد باید گفت که آن‌ها در واقع تسک‌های اجرایی نیستند و برای پیکربندی به کار می‌روند و در فاز پیکربندی هم اجرا می‌شوند که در ادامه نمونه آن را خواهیم دید.
اگر بخواهید خودتان دستی یک تسک پیکربندی را به یک تسک اجرایی تبدیل کنید، می‌توانید متد doLast را صدا بزنید. کد زیر را توسط gradlew اجرا کنید؛ به همراه اطلاعات verbose تا ببینید که هر کدام از پیام‌ها در کدام بخش چاپ می‌شوند. پیام اول در فاز پیکربندی و پیام دوم در فاز اجرایی چاپ می‌شوند.
task mytask {
    println ".net tips task in config phase"

    doLast{
        println ".net tips task in exe phase"
    }
}

یکی از کارهایی که در یک تسک میتوانید انجام دهید این است که آن را به یک تسک دیگر وابسته کنید. به عنوان مثال ما قصد داریم بعد از تسک mytask1،  تسک my task2 اجرا شود و زمان پایان تسک mytask1 را در خروجی نمایش دهیم. برای اینکار باید بین تسک‌ها  یک وابستگی ایجاد شود و سپس با متد doLast کد خودمان را اجرایی نماییم. البته توجه داشته باشید که این وابستگی‌ها تنها به تسک‌های داخل فایل گریدل انجام می‌شود و نه تسک‌های پلاگین‌ها یا وابستگی هایی که تعریف می‌کنیم.
task mytask1 << {
    println ".net tips is the best"
}

task mytask2() {
    dependsOn mytask1

    doLast{
        Date time=Calendar.getInstance().getTime();
        SimpleDateFormat formatter=new SimpleDateFormat("HH:mm:ss , YYYY/MM/dd");
        println "mytask1 is done at " + formatter.format(time);

    }
}
سپس تسک شماره دو را صدا می‌زنیم. در اینجا جون تسک شماره دو به تسک شماره یک وابسته است، ابتدا تسک شماره یک اجرا شده و سپس نوبت تسک شماره دو می‌شود.
gradlew --info mytask2
خروجی کار:
Executing task ':app:mytask1' (up-to-date check took 0.003 secs) due to:
  Task has not declared any outputs.
خروجی تسک شماره یک
.net tips is the best       
:app:mytask1 (Thread[main,5,main]) completed. Took 0.046 secs.
:app:mytask2 (Thread[main,5,main]) started.
:app:mytask2                 
Executing task ':app:mytask2' (up-to-date check took 0.0 secs) due to:
  Task has not declared any outputs.
خروجی تسک شماره دو
mytask1 is done at 04:03:09 , 2016/07/07
:app:mytask2 (Thread[main,5,main]) completed. Took 0.075 secs.
               
BUILD SUCCESSFUL

در گریدل مخالف doLast یعنی doFirst را نیز داریم ولی عملگر جایگزینی برای آن وجود ندارد و مستقیما باید آن را پیاده سازی کنید. خود گریدل به طور پیش فرض نیز تسک‌های آماده ای نیز دارد که می‌توانید در مستندات آن بیابید. به عنوان مثال یکی از تسک‌های مفید و کاربردی آن تسک کپی کردن هست که از طریق آن می‌توانید فایلی یا فایل‌هایی را از یک مسیر به مسیر دیگر کپی کنید. برای استفاده از چنین تسکهایی، باید تسک‌های خود را به شکل زیر به شیوه اکشن بنویسید:
task mytask(type:Copy) {
    dependsOn mytask1

    doLast{
        from('build/apk')
                {
                    include '**/*.apk'
                }
        into '.'
    }

}
در تسک بالا بعد از اجرای تسک شماره یک، آخرین کاری که انجام می‌شود این است که فایل‌های apk موجود در زیر دایرکتوری‌های مسیر from به ریشه اصلی کپی خواهند شد. پس همانطور که می‌بینید گریدل به راحتی عملیات اتوماسیون را انجام می‌دهد.
برای نمایش تسک‌های موجود می‌توانید از گریدل درخواست کنید که لیست تمامی تسک‌های موجود را به شما نشان دهد. برای اینکار می‌توانید دستور زیر را صدا کنید:
gradlew --info tasks
بعد از مدتی تمامی تسک‌های موجود به صورت گروه بندی نمایش داده شده و تسک‌هایی که شما جدیدا اضافه کردید را با عنوان  other tasks نمایش خواهد داد:
Other tasks           
-----------           
clean                 
jarDebugClasses       
jarReleaseClasses     
mytask                
mytask2               
transformResourcesWithMergeJavaResForDebugUnitTest
transformResourcesWithMergeJavaResForReleaseUnitTest
اگر به تسک‌های خود گریدل نگاه کنید برای هر کدام توضیحی هم وجود دارد؛ اگر شما هم قصد دارید توضیحی اضافه کنید از خصوصیت description استفاده کنید:
task mytask(type:Copy) {

    description "copy apk files to root directory"

    dependsOn mytask1

    doLast{
        from('build/apk')
                {
                    include '**/*.apk'
                }
        into '.'
    }

}
یکبار دیگر دستور نمایش تسک‌ها را صدا بزنید تا اینبار توضیح اضافه شده نمایش داده شود.
یکی دیگر از نکات جالب در مورد گریدل این است که می‌تواند برای شما callback ارسال کند. بدین صورت که اگر اتفاقی خاصی افتاد، تسک خاصی را اجرا کند. به عنوان مثال ما در کد پایین تسکی را ایجاد کرده‌ایم که به ما این اجازه را می‌دهد، هر موقع تسکی در مرحله پیکربندی به بیلد اضافه می‌شود، تسک ما هم اجرا شود و نام تسک اضافه شده به بیلد را چاپ می‌کند.
tasks.whenTaskAdded{
    task-> println "task is added $task.name"
}

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

Gradle Wrapper
گریدل در حال حاضر مرتبا در حال تغییر و به روز رسانی است و اگر بخواهیم مستقیما با گریدل کار کنیم ممکن است که به مشکلاتی که در نسخه بندی است برخورد کنیم. از آنجا که هر پروژه‌ای که روی سیستم شما قرار بگیرد از نسخه‌ای متفاوتی از گریدل استفاده کند، باعث می‌شود که نتوانید نسخه مناسبی از گریدل را برای سیستم خود دانلود کنید. بدین جهت wrapper ایجاد شد تا دیگر نیازی به نصب گریدل پیدا نکنید. wrapper در هر پروژه میداند که که به چه نسخه‌ای از گریدل نیاز است. پس موقعی که شما دستور gradlew را صدا می‌زنید در ویندوز فایل gradlew.bat صدا زده شده و یا در لینوکس و مک فایل شِل اسکریپت gradlew صدا زده می‌شود و wrapper به خوبی میداند که به چه نسخه‌ای از گریدل برای اجرا نیاز دارد و آن را از طریق دانلود فراهم می‌کند. اگر همینک دایرکتوری والد پروژه اندرویدی خود را نگاه کنید می‌توانید این دو فایل را ببینید.

از آنجا که خود اندروید استادیو به ساخت wrapper اقدام میکند، شما راحت هستید. ولی اگر دوست دارید خودتان برای پروژه‌ای wrapper تولید کنید، مراحل زیر را دنبال کنید:
برای ایجاد wrapper توسط خودتان باید گریدل را دانلود و روی سیستم نصب کنید و سپس دستور زیر را صادر کنید:
gradle wrapper --gradle-version 2.4
دستور بالا یک wrapper برای نسخه 2.4 ایجاد می‌کند.
اگر میخواهید ببینید wrapper که اندروید استادیو شما دارد چه نسخه از گردیل را صدا میزند مسیر را از دایرکتوری پروژه دنبال کنید و فایل زیر را بگشایید:
\gradle\wrapper\gradle-wrapper.properties
هنگامی که گوگل قصد آپدیت نسخه گریدل شما را بکند این فایل را باز کرده و نسخه داخل آن را ویرایش میکند.

این‌ها فقط مختصراتی از آشنایی با نحوه عملکر گریدل برای داشتن دیدی روشن‌تر نسبت به آن بود. برای آشنایی بیشتر با گریدل، باید مستندات رسمی آن را دنبال کنید.
مطالب
گروه بندی اطلاعات در jqGrid
فرض کنید لیستی از مطالب را به فرمت ذیل در اختیار داریم:
namespace jqGrid10.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string CategoryName { set; get; }
        public int NumberOfViews { set; get; }
    }
}
می‌خواهیم آن‌ها را با شرایط ذیل گروه بندی کنیم:
- گروه بندی بر روی ستون CategoryName انجام شود.
- ستونی که بر روی آن گروه بندی انجام می‌شود، نمایش داده نشود.
- در ابتدای نمایش گروه‌ها، تمام آن‌ها به صورت جمع شده و Collapsed نمایش داده شوند.
- پس از نمایش گروه‌ها، اولین گروه به صورت خودکار باز شود.
- تعداد ردیف هر گروه به عنوان گروه اضافه شود.
- جمع کل ستون تعداد بار مشاهدات هر گروه قابل محاسبه شود.
- جمع کل هر گروه در زمانیکه هر گروه نیز بسته‌است نمایش داده شود.
- رنگ ردیف جمع کل قابل تنظیم باشد.



فعال سازی گروه بندی در jqGrid

فعال سازی گروه بندی در jqGrid به سادگی افزودن تعاریف ذیل است:
            $('#list').jqGrid({
                caption: "آزمایش دهم",
                //...
                grouping: true,
                groupingView: {
                    groupField: ['CategoryName'],
                    groupOrder: ['asc'],
                    groupText : ['<b>{0} - {1} ردیف</b>'],
                    groupDataSorted: true,
                    groupColumnShow: false,
                    groupCollapse: true,
                    groupSummary: [true],
                    showSummaryOnHide: true
                }
            });
grouping: true سبب می‌شود تا گروه بندی فعال شود. سپس نیاز است ستون یا ستون‌هایی را که قرار است بر روی آن‌ها گروه بندی صورت گیرد، معرفی کنیم. اینکار توسط خواص شیء groupingView انجام می‌شوند.
groupField بیانگر آرایه‌ای از ستون‌هایی است که قرار است بر روی آن‌ها گروه بندی صورت گیرد.
groupOrder آرایه‌ای اختیاری از مقادیر asc یا desc است که متناظر هستند با نحوه‌ی مرتب سازی پیش فرض ستون‌های معرفی شده در آرایه groupField.
groupText آرایه‌ای اختیاری از عناوین گروه بندی‌های انجام شده‌است. اگر ذکر شود، {0} آن با نام گروه و {1} آن با تعداد عناصر گروه جایگزین می‌شود.
تنظیم groupDataSorted سبب خواهد شد تا نام ستونی که بر روی آن گروه بندی صورت می‌گیرد، به سرور ارسال شود (توسط پارامتر sidx). به این ترتیب در سمت سرور می‌توان اطلاعات را به صورت پویا مرتب سازی کرده و بازگشت داد.
با تنظیم groupColumnShow به false سبب خواهیم شد تا ستون‌های معرفی شده در قسمت groupField نمایش داده نشوند.
با تنظیم groupCollapse به true، در ابتدای نمایش گروه‌ها، ردیف‌های آن‌ها نمایش داده نخواهند شد و در حالت جمع شده قرار می‌گیرند.
groupSummary به معنای فعال سازی نمایش ردیف محاسبه‌ی summary مانند sum، min، max و امثال آن بر روی یک گروه است.
اگر مقدار showSummaryOnHide مساوی true باشد، ردیف محاسبه‌ی summary حتی در حالت groupCollapse: true نمایش داده خواهد شد.


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

برای فعال سازی نهایی محاسبه‌ی جمع ستون تعداد بار مشاهدات، علاوه بر تنظیم groupSummary به true، نیاز است در همان ستون مشخص کنیم که این محاسبات چگونه باید انجام شوند:
                colModel: [
// ........
                    {
                        name: '@(StronglyTyped.PropertyName<Post>(x => x.Title))',
                        index: '@(StronglyTyped.PropertyName<Post>(x => x.Title))',
                        align: 'right',
                        width: 150,
                        summaryTpl: '<div style="text-align: left;">خلاصه </div>',
                        summaryType: function (val, name, record) {
                            return "";
                        }
                    },
// ........
                    {
                        name: '@(StronglyTyped.PropertyName<Post>(x => x.NumberOfViews))',
                        index: '@(StronglyTyped.PropertyName<Post>(x => x.NumberOfViews))',
                        align: 'center',
                        width: 70,
                        summaryType: 'sum', summaryTpl: '<b>جمع مشاهدات: {0}</b>'
                    }
                ],
خاصیت summaryType نوع متد محاسبه‌ی summary مانند sum را مشخص می‌کند. خاصیت summaryTpl اختیاری است. اگر ذکر شود سبب فرمت مقدار محاسبه شده‌ی نهایی می‌گردد. در اینجا {0} به مقدار نهایی که قرار است در این سلول درج شود، اشاره می‌کند.
summaryType مانند ستون عنوان، سفارشی شده نیز می‌توان باشد. در ردیف summary و ستون عنوان تنها می‌خواهیم یک مقدار ثابت را نمایش دهیم، به همین جهت summaryType آن به یک مقدار خالی تنظیم شده‌است.


تغییر رنگ ردیف خلاصه عملیات هر گروه به همراه گشودن خودکار اولین گروه

گروه بندی به همراه یک سری متد توکار نیز هست. برای مثال اگر متد groupingToggle را بر روی Id هر گروه فراخوانی کنیم، می‌توان سبب باز یا بسته شده آن گروه شد. متدهای دیگری مانند groupingGroupBy برای گروه بندی پویا و groupingRemove برای حذف گروه بندی نیز وجود دارند:
            $('#list').jqGrid({
                caption: "آزمایش دهم",
                //.........
                loadComplete: function() {
                    //.........
                    $('#list').jqGrid('groupingToggle', 'list' + 'ghead_0_0');
                    $("tr.jqfoot td").css({
                        "background": "#2f4f4f",
                        "color": "#FFF"
                    });
                },
            });
بهترین محل اعمال رنگ به ردیف‌های summary، در روال رویدادگردان loadComplete گرید است.


مثال کامل این قسمت را از اینجا می‌توانید دریافت کنید:
jqGrid10.zip
مطالب
صدور رخدادها از سرویس‌ها به کامپوننت‌ها در برنامه‌های Angular
در طراحی برنامه‌های Angular توصیه شده‌است تا هرگونه منطقی که مستقیما به View یک کامپوننت مرتبط نیست، به یک کلاس سرویس منتقل شود. در این بین ممکن است نیاز به صدور رخدادی از یک سرویس به خارج از آن باشد؛ چیزی مانند EventEmitter. اما EventEmitter برای سرویس‌ها طراحی نشده‌است و کاربرد صحیح آن صرفا محدود به کامپوننت‌ها است. برای حل این مساله، API سرویس ما باید یک Observable را در معرض دید استفاده کننده قرار دهد تا توسط آن بتوان رخ‌دادهایی را به کامپوننت‌های مشترک شده‌ی به آن، صادر کرد.


چگونه می‌توان رخ‌دادهایی از نوع Observable را ایجاد کرد؟

کلاس Subject پاسخی است به این پرسش. Subjectها Observableهایی هستند که می‌توانند چندین مشترک داشته باشند و رخ‌دادهایی را به مشترکین خود صادر کنند. برای کار با آن‌ها باید یک private Subject را در سرویس خود ایجاد کرد و سپس جریان منتقل شده‌ی توسط آن‌را توسط یک public Observable در اختیار مصرف کنندگان قرار داد. با فراخوانی متد next یک Subject، رخ‌دادی به مشترکین آن منتقل می‌شود.
import { Subject } from “rxjs/Subject”;

public countdown: number = 0;

private countdownEndSource = new Subject<void>();
public countdownEnd$ = this.countdownEndSource.asObservable();
مرسوم است نام Observableهایی را که قرار است رخ‌دادی را صادر کنند به $ ختم می‌کنند.
استفاده کنندگان نیز مشترک این $countdownEnd شده و هر بار که در طرف سرویس، متد next آن فراخوانی می‌شود، از به روز رسانی آن مطلع خواهند شد.


چرا مستقیما از مقدار countdown استفاده نکنیم؟

در قسمتی از سرویس فوق که ملاحظه می‌کنید، می‌توان مقدار countdown را مستقیما نیز در کامپوننت‌ها مورد استفاده قرار داد. اما این روش بهینه نیست. از این جهت که Angular باید مدام تغییرات این خاصیت را رصد کند و به آن واکنش نشان دهد. آیا بهتر نیست ما به Angular اعلام کنیم که مقدار آن تغییر کرده‌است و اکنون بهتر است View را به روز رسانی کنی؟ با ارائه‌ی مقادیر جدیدی توسط یک Observable، اکنون Angular صرفا به تغییرات آن واکنش نشان خواهد داد و دیگری نیاز به بررسی مداوم تغییرات مقدار countdown ندارد.


یک مشکل! Subject تعریف شده، مقادیر را تنها در زمان فراخوانی متد next ارائه می‌دهد و نه به صورت دیگری.

پیشتر با دسترسی مستقیم به خاصیت countdown، همواره به مقادیر آن هم دسترسی داشتیم. اما با استفاده از یک Subject، تنها زمانیکه متد next آن فراخوانی شود می‌توان به این مقدار دسترسی یافت. برای رفع این مشکل یک Subject ویژه به نام BehaviorSubject طراحی شده‌است که به محض مشترک شدن به آن، اولین و یا آخرین مقدار آن‌را می‌توان دریافت کرد.


تفاوت Subject با BehaviorSubject

BehaviorSubject مانند یک Subject است؛ با این تفاوت که همواره از وضعیت خود آگاه می‌باشد. یک BehaviorSubject:
- همواره دارای مقداری است. حتی در زمان وهله سازی، باید مقدار اولیه‌ای را برای آن مشخص کرد.
- در زمان اشتراک به آن، می‌توان آخرین مقدار موجود در آن را که ممکن است اولین مقدار آن نیز باشد، دریافت کرد.
- همواره می‌توان مقدار آن‌را توسط متد getValue بدست آورد.

و مهم‌ترین مزیت آن نسبت به Subject، همان مورد دوم است. اگر مشترک یک Subject شویم، تا متد next آن فراخوانی نشود، مقداری را دریافت نمی‌کنیم. اما همان لحظه که مشترک BehaviorSubject می‌شویم، آخرین مقدار موجود در آن‌را دریافت خواهیم کرد.
برای مثال فرض کنید کامپوننتی را دارید که به خاصیت isLoggedIn از نوع Observable یک Subject گوش فرا می‌دهد. اما اشتراک آن پس از فراخوانی متد next در این سرویس بوده‌است. از این رو این کامپوننت هیچگاه متوجه تغییر و یا مقدار نهایی isLoggedIn نخواهد شد. به همین جهت است که به BehaviorSubject نیاز داریم. در این بین مهم نیست که چه زمانی مشترک آن می‌شویم؛ همواره در زمان اشتراک، آخرین و یا اولین مقدار موجود در آن‌را دریافت خواهیم کرد.


یک مثال: بررسی عملکرد BehaviorSubject

در ادامه یک ماژول را به همراه 4 کامپوننت و یک سرویس سفارشی ایجاد می‌کنیم:
ng g m ServiceComponentCommunication -m app.module --routing
ng g c ServiceComponentCommunication/First
ng g c ServiceComponentCommunication/Second
ng g c ServiceComponentCommunication/Third
ng g c ServiceComponentCommunication/Final
ng g s ServiceComponentCommunication/Sample


هدف این است که سه کامپوننت اول، دوم و سوم را در کامپوننت final، همانند تصویر فوق نمایش دهیم.
در این بین یک سرویس انتشار اطلاعات نیز طراحی شده‌است:
import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs/BehaviorSubject";

@Injectable()
export class SampleService {

  private msgSource = new BehaviorSubject<string>("default service value");

  telecast$ = this.msgSource.asObservable();

  constructor() { }

  editMsg(newMsg: string) {
    this.msgSource.next(newMsg);
  }

}
کار این سرویس ارائه یک پیام از نوع BehaviorSubject از طریق خاصیت عمومی $telecast آن است که به صورت Observable در معرض دید کامپوننت‌های مشترک به آن قرار خواهد گرفت. هدف این است که کامپوننت‌ها مدام تغییرات msg را بررسی نکنند و فقط به آخرین تغییر صادر شده‌ی توسط کامپوننت که از طریق فراخوانی متد next در متد editMsg صورت می‌گیرد، واکنش نشان دهند.

در کامپوننت اول، نحوه‌ی اشتراک به این سرویس را مشاهده می‌کنید:
import { SampleService } from "./../sample.service";
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

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

  editedMsg: string;
  sampleSubscription: Subscription;

  constructor(private sampleService: SampleService) { }

  ngOnInit() {
    this.sampleSubscription = this.sampleService.telecast$.subscribe(message => {
      this.editedMsg = message;
    });
  }

  editMsg() {
    this.sampleService.editMsg(this.editedMsg);
  }

  ngOnDestroy() {
    this.sampleSubscription.unsubscribe();
  }
}
کار اشتراک در این کامپوننت در متد ngOnInit انجام شده‌است. بسیار مهم است جهت عدم بروز نشتی حافظه، در متد ngOnDestroy کار unsubscribe بر روی این اشتراک نیز صورت گیرد.
در اینجا هر زمانیکه متد next در سرویس فراخوانی شود، this.editedMsg مقدار جدیدی را دریافت می‌کند.
با این قالب:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">First Component</h2>
  </div>
  <div class="panel-body">
    <p> {{editedMsg}}</p>
    <input class="form-control" type="text" [(ngModel)]="editedMsg">
    <button (click)="editMsg()" class="btn btn-primary">Change</button>
  </div>
</div>


اما اگر به تصویر دقت کنید، this.editedMsg هم اکنون دارای مقدار است (در اولین بار اجرای این کامپوننت). علت آن به داشتن مقدار اولیه‌ای در BehaviorSubject تعریف شده بر می‌گردد که در اولین بار اشتراک به آن، در اختیار مشترک قرار خواهد گرفت. این مورد، مهم‌ترین تفاوت BehaviorSubject با Subject است.
در این کامپوننت اگر کاربر مقداری را در textbox وارد کند و سپس بر روی دکمه‌ی Change کلیک نماید، این تغییر از طریق سرویس، به تمام مشترکین آن صادر خواهد شد.

کامپوننت دوم نیز مانند کامپوننت اول است، فقط یک textbox ورود اطلاعات را به همراه ندارد.


همانطور که ملاحظه می‌کنید، این کامپوننت نیز دارای مقدار اولیه‌ی BehaviorSubject است.

کامپوننت سوم، اندکی متفاوت است:
import { SampleService } from "./../sample.service";
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

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

  message: string;
  sampleSubscription: Subscription;

  constructor(private sampleService: SampleService) { }

  ngOnInit() {
  }

  subscribe() {
    this.sampleSubscription = this.sampleService.telecast$.subscribe(message => {
      this.message = message;
    });
  }

  ngOnDestroy() {
    if (this.sampleSubscription) {
      this.sampleSubscription.unsubscribe();
    }
  }
}
در اینجا کار اشتراک در متد subscribe فراخوانی شده‌ی توسط قالب آن صورت می‌گیرد:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">Third Component</h2>
  </div>
  <div class="panel-body">
    <p>{{message}}</p>
    <button (click)="subscribe()" class="btn btn-success">Subscribe</button>
  </div>
</div>


و چون این متد پس از ngOnInit قرار است توسط کاربر فراخوانی شود، مقدار message این کامپوننت هنوز خالی است.
اکنون اگر بر روی دکمه‌ی Subscribe آن کلیک کنیم، بلافاصله در لحظه‌ی اشتراک، اولین/آخرین مقدار موجود در BehaviorSubject را دریافت خواهیم کرد:


کامپوننت Final نیز تمام کامپوننت‌ها را در صفحه نمایش می‌دهد:
<div class="row">
  <div class="col-md-4">
    <app-first></app-first>
  </div>
  <div class="col-md-4">
    <app-second></app-second>
  </div>
  <div class="col-md-4">
    <app-third></app-third>
  </div>
</div>

و اگر در textbox کامپوننت اول، مقدار Test را وارد کنیم و سپس بر روی دکمه‌ی Change آن کلیک نمائیم، این مقدار به تمام کامپوننت‌های مشترک به BehaviorSubject سرویس برنامه، منتشر خواهد شد:


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.