مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت سوم - تعریف نوع رویدادها و children props
در قسمت قبل، نوع توابع ارسالی از طریق props را تعیین کردیم. فرض کنید در همان مثال می‌خواهیم بجای ارسال یک رشته به فراخوان کامپوننت تعریف شده، اصل رخ‌داد واقع شده را ارسال کنیم. به همین جهت onClick دریافتی را مستقیما به رخ‌داد onClick، نسبت می‌دهیم:
export const Button = ({ onClick }: Props) => {
  return <button onClick={onClick}>Click Me</button>;
};
در این حالت بلافاصله با خطای زیر مواجه خواهیم شد که عنوان می‌کند امضای تابع onClick، با امضای رویداد مدنظر یکی نیست:


برای رفع این مشکل، می‌توان رخ‌داد کلیک ماوس بر روی یک دکمه را از نوع بسیار عمومی React.MouseEvent تعریف کرد:
import React from "react";

type Props = {
  onClick: (e: React.MouseEvent) => void;
};

export const Button = ({ onClick }: Props) => {
  return <button onClick={onClick}>Click Me</button>;
};
پس از آن می‌توان تعریف المان کامپوننت Button را در فایل src\App.tsx به صورت زیر تغییر داد؛ چون از دیدگاه تایپ‌اسکریپت، اکنون نوع e به صورت <e: React.MouseEvent<Element, MouseEvent تعریف شده‌است و به تمام خواص و متدهای MouseEvent، با کمک intellisense کامل مرتبط با آن‌ها، دسترسی وجود دارد:
<Button
  onClick={(e) => {
    e.preventDefault();
    console.log(e);
  }}
/>


محدود کردن دامنه‌ی رویدادها


زمانیکه نوع یک رویداد به صورت کلی e: React.MouseEvent تعریف می‌شود، اگر به تصویر فوق دقت کنیم، تایپ‌اسکریپت آن‌را قابل اعمال به هر نوع Element ای می‌داند. اما اگر بخواهیم دامنه‌ی دید آن‌را صرفا به المان استاندارد button تعریف شده محدود کنیم، اشاره‌گر ماوس را در فایل src\components\Button.tsx بر روی رویداد onClick المان button تعریف شده قرار می‌دهیم:


همانطور که مشخص است، نوع این المان را به صورت HTMLButtonElement ذکر کرده‌است. بنابراین می‌توان تعریف کلی قبلی را به صورت زیر محدود کرد:
import React from "react";

type Props = {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
};

export const Button = ({ onClick }: Props) => {
  return <button onClick={onClick}>Click Me</button>;
};
در اینجا توسط generics، نوع React.MouseEvent به HTMLButtonElement محدود شده‌است و دیگر اینبار بازه‌ی دید آن یک Element کلی نیست. این تعریف عنوان می‌کند که رخ‌داد واقع شده، فقط از یک button صادر شده‌است و نه از المان نوع دیگری. به این ترتیب در حین استفاده‌ی از رخ‌داد رسیده در کامپوننت App در برگیرنده‌ی آن، به متدها و رخ‌دادهای ویژه‌تری نیز دسترسی خواهیم یافت.


تعیین نوع رخ‌داد onChange

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


import React from "react";

type Props = {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onChange?: (e: React.FormEvent<HTMLInputElement>) => void;
};

export const Button = ({ onClick, onChange }: Props) => {
  return (
    <>
      <input onChange={onChange} />
      <button onClick={onClick}>Click Me</button>
    </>
  );
};
در اینجا یا می‌توان از رویداد اختصاصی React.ChangeEvent از نوع HTMLInputElement، استفاده کرد و یا از حالت عمومی‌تر React.FormEvent.


سؤال: اطلاعات نمایش داده شده‌ی نوع‌های متناظر با رویدادهای مختلف در تصاویر فوق، از کجا تامین می‌شوند؟

اگر به فایل package.json پروژه‌ی تایپ‌اسکریپتی ایجاد شده مراجعه کنید، می‌توانید حداقل دو مدخل جدید زیر را نیز در آن مشاهده کنید:
{ 
  "dependencies": {
    "@types/react": "^16.9.35",
    "@types/react-dom": "^16.9.8",
  },
این بسته‌های جدید، حاوی تعاریف تمام نوع‌های تایپ‌اسکریپتی مرتبط با اشیاء یک پروژه‌ی React هستند. برای مثال در فایل src\components\Button.tsx، اشاره‌گر ماوس را بر روی MouseEvent تعریف شده قرار دهید و سپس دکمه‌ی ctrl را نگه دارید. مشاهده خواهید کرد که تعریف این نوع، تبدیل به یک لینک قابل کلیک خواهد شد. اگر بر روی آن کلیک کنید، به فایل node_modules\@types\react\index.d.ts پروژه‌ی خود هدایت می‌شوید که دقیقا محل تعریف این رویداد و موارد مشابه است.


سؤال: آیا بسته‌های @types دار موجود در فایل package.json، حجم فایل‌های نهایی برنامه را افزایش می‌دهند؟ آیا برنامه‌های React مبتنی بر TypeScript، حجم بیشتری را نسبت به نمونه‌ها‌ی ES6 آن دارند؟

اکثر این تعاریف @types به صورت اینترفیس‌های تایپ‌اسکریپتی تعریف شده‌اند که صرفا در زمان کامپایل برنامه، کمک حال کامپایلر آن خواهند بود و پس از کامپایل، تبخیر شده و در کدهای نهایی تولیدی، حضور نخواهند داشت. به عبارتی استفاده‌ی از تایپ‌اسکریپت، حجم نهایی پروژه‌ها را افزایش نمی‌دهد؛ چون اینترفیس‌های تایپ‌اسکریپت، معادلی را در جاوااسکریپت استاندارد ندارند و فقط به عنوان راهنمای کامپایلر تایپ‌اسکریپت عمل می‌کنند.


یک نکته: تعریف رویدادهای مدنظر را می‌توان در importها نیز قرار داد:
import React, { FormEvent } from "react";

type Props = {
  onChange?: (e: FormEvent<HTMLInputElement>) => void;
};
که در این حالت دیگر نیازی به ذکر کامل React.FormEvent نیست. اما در کل، روش تعریف React.xyz، مرسوم‌تر است.


تعریف نوع Children Props

زمانیکه داخل تعریف المان یک کامپوننت، کامپوننت دیگری ذکر می‌شود، به عنوان فرزند او در React درنظر گرفته می‌شود. برای نمونه تعریف کامپوننت Button را در فایل src\App.tsx به صورت زیر تغییر می‌دهیم تا فرزندی را به صورت «Hello world» بپذیرد:
<Button
  onClick={(e) => {
    e.preventDefault();
    console.log(e);
  }}
>
  Hello world!
</Button>
 اکنون سؤال اینجا است که چگونه می‌توان نوع آن‌را توسط تایپ‌اسکریپت مشخص کرد؟
با توجه به اینکه این فرزند، از نوع رشته‌ای است، فقط کافی است خاصیت children را با همین نوع، به Props اضافه کرده و از آن استفاده کنیم:
import React, { FormEvent } from "react";

type Props = {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onChange?: (e: FormEvent<HTMLInputElement>) => void;
  children: string;
};

export const Button = ({ onClick, onChange, children }: Props) => {
  return (
    <>
      <input onChange={onChange} />
      <button onClick={onClick}>{children}</button>
    </>
  );
};
در ادامه، مثال فوق را اندکی مشکل‌تر می‌کنیم. فرض کنید اینبار بجای یک رشته، یک المان image، به عنوان فرزند کامپوننت دکمه معرفی شود:
<Button
  onClick={(e) => {
    e.preventDefault();
    console.log(e);
  }}
>
  <img src={logo} className="App-logo" alt="logo" />
</Button>
که سبب بروز خطای زیر می‌شود:


عنوان می‌کند که با این تغییر، نوع children به Element تغییر یافته‌است؛ اما در تعریف Props آن، به صورت رشته‌ای معرفی شده‌است. در این حالت اگر به تعریف Props مراجعه کنیم و نوع string را به Element تغییر دهیم، باز هم این خطا برطرف نمی‌شود. حتی اگر children: HTMLImageElement را نیز اضافه کنیم، باز هم این خطا تغییری نمی‌کند.
روش صحیح حل این مشکل را در ادامه مشاهده می‌کنید:
import React, { FormEvent } from "react";

type Props = {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onChange?: (e: FormEvent<HTMLInputElement>) => void;
  // children: HTMLImageElement;
};

export const Button: React.FC<Props> = ({ onClick, onChange, children }) => {
  return (
    <>
      <input onChange={onChange} />
      <button onClick={onClick}>{children}</button>
    </>
  );
};
- ابتدا خاصیت children را از تعریف نوع Props حذف می‌کنیم.
- سپس ذکر نوع Props را هم از Object Destructuring صورت گرفته، حذف می‌کنیم.
- در آخر، نوع کامپوننت جاری را به صورت <React.FC<Props معرفی می‌کنیم. در اینجا FC یعنی Functional Component و با تعریف آن، می‌توان نوع props را به صورت آرگومان جنریک آن مشخص کرد. پس از آن دیگر نیازی به ذکر خاصیت children، در Props نیست؛ چون این خاصیت جزئی از React.FC می‌باشد و به صورت خودکار شناسایی می‌شود:



بنابراین در حالت کلی اگر از خاصیت children استفاده نمی‌کنید، از همان syntax قبلی که به صورت export const Button = ({ onClick, onChange }: Props) است، استفاده کنید؛ در غیراینصورت استفاده‌ی از نوع <React.FC<T، ضروری خواهد بود.
مطالب
فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطلب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال اول اکتفاء خواهد شد.


تغییرات مورد نیاز سمت کلاینت جهت فعال سازی جستجو در jqGrid

در سمت کلاینت، در حین تعریف ستون‌ها، ابتدا باید توسط مقدار دهی خاصیت search، ستون‌های مشارکت کننده‌ی در حین جستجو را مشخص کرد:
                colModel: [
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 200,
                        search: true, stype: 'text', searchoptions: { sopt: searchOptions }
                    },
                    {
                        name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 200,
                        search: true, stype: 'select', edittype: 'select', searchOperators: true,
                        searchoptions:
                        {
                            sopt: searchOptions, dataUrl: '@Url.Action("SuppliersSelect","Home")'
                        }
                    }
                ],
- برای نمونه در اینجا search: true، جهت دو ستون نام محصول و نام تولید کننده، تنظیم شده‌اند.
- stype، روش مقایسه‌ی مقادیر را مشخص می‌کند. مقدار پیش فرض آن text است و مقادیری مانند int، integer، float، number، numeric، date و datetime را می‌پذیرد.
- searchoptions برای تنظیم جزئیات نحو‌ه‌ی جستجوی بر روی فیلدها بکار می‌رود. توسط sopt می‌توان آرایه‌ای با مقادیر ذیل را مقدار دهی کرد:
 var searchOptions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge', 'bw', 'bn', 'in', 'ni', 'ew', 'en', 'cn', 'nc'];
eq به معنای مساوی است، ne، مساوی نیست، lt کمتر و به همین ترتیب.
البته باید دقت داشت که آرایه فوق کاملترین حالت ممکن است و ضرورتی ندارد تمام حالات را برای یک فیلد تعریف کرد. چون برای مثال جستجو cn یا contains برای مقادیر رشته‌ای معنا دارد و نه سایر حالات.
- searchoptions گزینه‌های دیگری را نیز می‌تواند شامل شود. برای مثال در حین نمایش جستجوی داخل ردیفی یا صفحه‌ی دیالوگ مخصوص آن، قصد داریم فیلد نام تولید کننده را توسط یک drop down نمایش دهیم و نه یک text box پیش فرض. برای این منظور dataUrl مقدار دهی شده‌است.
SuppliersSelect آن به اکشن متد ذیل اشاره می‌کند که لیست تولید کنندگان را با فرمت لیستی از SelectListItemها به یک partial view تولید کننده‌ی دراپ داون ارسال می‌کند:
        public ActionResult SuppliersSelect()
        {
            var list = ProductDataSource.LatestProducts;
            var suppliers = list.Select(x => new SelectListItem
            {
                Text = x.Supplier.CompanyName,
                Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture)
            }).ToList();
            return PartialView("_SelectPartial", suppliers);
        }
و محتوای فایل _SelectPartial نیز به صورت ذیل است:
 @model IList<SelectListItem>

@Html.DropDownList("srch", Model)
در این حالت با نمایش صفحه‌ی جستجو و انتخاب فیلد نام تولید کننده، به صورت خودکار یک dorp down پویا نمایش داده خواهد شد.

- اگر دقت کرده باشید، نام فیلد تولید کننده با Supplier.Id مقدار دهی شده‌است. علت اینجا است که در زمان استفاده از drop down، مقدار Id آیتم انتخابی، به سرور ارسال می‌شود. به همین جهت کار کردن پویا با Supplier.Id ساده‌تر خواهد بود.




- برای نمایش دیالوگ و یا نوار ابزار توکار جستجو، می‌توان دکمه‌هایی را به فوتر گرید اضافه کرد:


        $(document).ready(function () {
            $('#list').jqGrid({
              // ... مانند قبل و توضیحات فوق
            })
            .jqGrid('navGrid', '#pager',
                { add: false, edit: false, del: false },
                {},  // default settings for edit
                {},  // default settings for add
                {},  // delete instead that del:false we need this
                {
                    // search options
                    multipleSearch: true,
                    closeOnEscape: true,
                    closeAfterSearch: true,
                    ignoreCase: true
                })
            .jqGrid('navButtonAdd', "#pager", {
                caption: "نوار ابزار جستجو", title: "Search Toolbar", buttonicon: 'ui-icon-search',
                onClickButton: function () {
                    toolbarSearching();
                }
            });
        });
        

        function toolbarSearching() {
            $('#list').filterToolbar({
                groupOp: 'OR',
                defaultSearch: "cn",
                autosearch: true,
                searchOnEnter: true,
                searchOperators: true, // فعال سازی منوی اپراتورها
                stringResult : true // وجود این سطر سبب می‌شود تا اپراتورها به سرور ارسال شوند
            });
        };

        function singleSearching() {
            $('#list').searchGrid({
                closeAfterSearch: true
            });
        };

        function advancedSearching() {
            $('#list').searchGrid({
                multipleSearch: true,
                closeAfterSearch: true
            });
        };
در اینجا نکات ذیل قابل توجه هستند:
- با استفاده از متد jqGrid و پارامتر navGrid، در ناحیه‌ی pager گرید، تنظیمات جستجو را فعال کرده‌ایم.
multipleSearch به معنای امکان جستجوی همزمان بر روی بیش از یک فیلد است. closeOnEscape سبب بسته شدن صفحه‌ی دیالوگ جستجو با فشردن دکمه‌ی ESC می‌شود. اگر closeAfterSearch به true تنظیم نشود، صفحه‌ی دیالوگ جستجو پس از جستجو، در صفحه باقی مانده و بسته نخواهد شد.
- این دکمه‌ی جستجو، جزو موارد توکار jqGrid است. اگر قصد داشته باشیم یک دکمه‌ی سفارشی دیگر را نیز اضافه کنیم، مجددا از متد jqGrid با پارامتر navButtonAdd در ناحیه‌ی pager استفاده خواهیم کرد. کلیک بر روی آن سبب اجرای متد toolbarSearching می‌شود.

در اینجا حداقل سه نوع جستجو را می‌توان فعال کرد:
- filterToolbar که سبب نمایش نوار ابزار جستجو، دقیقا بالای ستون‌های جدول می‌شود.
- searchGrid که صفحه‌ی دیالوگ مستقلی را جهت جستجو به صورت پویا تولید می‌کند. اگر خاصیت multipleSearch آن به true تنظیم نشود، این جستجو هربار تنها بر روی یک فیلد قابل انجام خواهد بود و برعکس.
- در حالت جستجوی نوار ابزاری، اگر خواص searchOperators و stringResult به true تنظیم شوند، مانند تصویر ذیل، به ازای هر ستون می‌توان از عملگرهای مختلفی استفاده کرد. در غیراینصورت جستجوی انجام شده بر اساس groupOp و defaultSearch پیش فرض انجام می‌شود. یعنی And یا Or تمام موارد تنها در حالت مثلا contains یا تساوی و امثال آن و نه حالت پیشرفته‌ی انتخاب عملگرها توسط کاربر.


یک نکته
اگر می‌خواهید صفحه‌ی جستجو در وسط صفحه ظاهر شود، می‌توانید از تنظیمات CSS ذیل استفاده کنید:
/* align center search popup in jqgrid */
.ui-jqdialog {
    display: none;
    width: 300px;
    position: absolute;
    padding: .2em;
    font-size: 11px;
    overflow: visible;
    left: 30% !important;
    top: 40% !important;
}


پردازش سمت سرور جستجوی پویای jqGrid

کدهای سمت سرور، با کدهای استفاده از dynamic LINQ مایکروسافت یکی است. با این تفاوت که اینبار قسمت where این کوئری نیز پویا می‌باشد. پیشتر قسمت order by را پویا پردازش کرده بودیم. برای ساخت where پویا که در dynamic LINQ به خوبی پشتیبانی می‌شود، باید ابتدا ساختار اطلاعات ارسالی به سرور را آنالیز کنیم:
 //single field search
//_search=true&nd=1403935889318&rows=10&page=1&sidx=Id&sord=asc&searchField=Id&searchString=4444&searchOper=eq&filters=

//multi-field search
//_search=true&nd=1403935941367&rows=10&page=1&sidx=Id&sord=asc&filters=%7B%22groupOp%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22field%22%3A%22Id%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%2244%22%7D%2C%7B%22field%22%3A%22SupplierID%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%221%22%7D%5D%7D&searchField=&searchString=&searchOper=
// filters -> {"groupOp":"AND","rules":[{"field":"All","op":"cn","data":"fffff"},{"field":"Price","op":"bn","data":"ffff"}]}

//toolbar search
//_search=true&nd=1403935593036&rows=10&page=1&sidx=Id&sord=asc&Id=2&Name=333&SupplierID=1&CategoryID=1&Price=44
در اینجا ساختار ارسالی به سرور را در سه حالت مختلف جستجوی پویای jqGrid، ملاحظه می‌کنید:
- در تمام این حالات پارامتر _search مساوی true است (تفاوت آن با درخواست اطلاعات معمولی).
- در حالت جستجوی نوار ابزاری، اگر گزینه‌های searchOperators و stringResult به true تنظیم نشوند، حالت toolbar search فوق را شاهد خواهیم بود. در غیراینصورت به حالت multi-field search سوئیچ می‌شود.
- در حالت جستجوی تک فیلدی توسط صفحه دیالوگ جستجوی jqGrid، فیلد در حال جستجو توسط searchField و مقدار آن توسط searchString به سرور ارسال شده‌اند. مابقی پارامترها نال هستند.
- در حالت جستجوی چند فیلدی توسط صفحه دیالوگ جستجوی jqGrid، اینبار filters مقدار دهی شده‌است و سایر پارامترها نال هستند. مقدار filters ارسالی، در حقیقت یک شیء JSON است با ساختار کلی ذیل:
        { "groupOp": "AND",
              "groups" : [ 
                { "groupOp": "OR",
                    "rules": [
                        { "field": "name", "op": "eq", "data": "England" }, 
                        { "field": "id", "op": "le", "data": "5"}
                     ]
                } 
              ],
              "rules": [
                { "field": "name", "op": "eq", "data": "Romania" }, 
                { "field": "id", "op": "le", "data": "1"}
              ]
        }
که می‌توان چنین ساختاری را برای آن متصور شد:
    public class SearchFilter
    {
        public string groupOp { set; get; }
        public List<SearchGroup> groups { set; get; }
        public List<SearchRule> rules { set; get; }
    }

    public class SearchRule
    {
        public string field { set; get; }
        public string op { set; get; }
        public string data { set; get; }

        public override string ToString()
        {
            return string.Format("'{0}' {1} '{2}'", field, op, data);
        }
    }

    public class SearchGroup
    {
        public string groupOp { set; get; }
        public List<SearchRule> rules { set; get; }
    }
در اینجا AND و Or کلی مشخص می‌شود، به همراه فیلدهای ارسالی به سرور، عملگرهای اعمالی بر روی آن‌ها و مقادیر مرتبط.
اگر این موارد را کنار هم قرار دهیم، به متدی عمومی ApplyFilter با امضای ذیل خواهیم رسید.
   public IQueryable<T> ApplyFilter<T>(IQueryable<T> query, bool _search, string searchField, string searchString,
string searchOper, string filters, NameValueCollection form)
کدهای کامل آن‌را به علت طولانی بودن پردازش سه حالت ذکر شده‌ی فوق، از پروژه‌ی پیوست می‌توانید دریافت کنید.
پس از آن، تغییراتی که در کدهای متد GetProducts باید اعمال شوند به صورت ذیل است:
        [HttpPost]
        public ActionResult GetProducts(string sidx, string sord, int page, int rows,
                                        bool _search, string searchField, string searchString,
                                        string searchOper, string filters)
        {
            var list = ProductDataSource.LatestProducts;            

            var pageIndex = page - 1;
            var pageSize = rows;
            var totalRecords = list.Count;
            var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize);

            var productsQuery = list.AsQueryable();

            productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString,
                                                           searchOper, filters, this.Request.Form);
            var productsList = productsQuery.OrderBy(sidx + " " + sord)
                                            .Skip(pageIndex * pageSize)
                                            .Take(pageSize)
                                            .ToList();

            var productsData = new JqGridData
            {
                Total = totalPages,
                Page = page,
                Records = totalRecords,
                Rows = (productsList.Select(product => new JqGridRowData
                {
                    Id = product.Id,
                    RowCells = new List<string>
                               {
                                     product.Id.ToString(CultureInfo.InvariantCulture),
                                     product.Name,
                                     product.Supplier.CompanyName,
                                     product.Category.Name,
                                     product.Price.ToString(CultureInfo.InvariantCulture)
                                }
                })).ToArray()
            };
            return Json(productsData, JsonRequestBehavior.AllowGet);
        }
- ابتدا چند پارامتر اضافه‌تر به امضای متد اضافه شده‌اند، تا فیلدهای جستجو را نیز دریافت کنند.
- نوع متد به HttpPost تغییر کرده‌است. این مورد برای ارسال اطلاعات حجیم جستجوها به سرور ضروری است و بهتر است از حالت Get استفاده نشود.
این حالت در سمت کلاینت نیز باید تنظیم شود:
 $('#list').jqGrid({
//url access method type
mtype: 'POST',
- سایر سطرها مانند قبل است؛ فقط یک سطر ذیل جهت اعمال where پویا به عبارت LINQ ساخته شده، اضافه شده‌است:
 productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString,
  searchOper, filters, this.Request.Form);

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


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid03.zip
 
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 13 - معرفی View Components
نکته تکمیلی :
ایجاد منوهای چند سطحی با استفاده از ViewComponent تا N سطح
کلاس Entity :
public class Navbar
    {
        public int Id { get; set; }
        public string Title { get; set; }

        public int? ParentId { get; set; }
        public virtual Navbar Parent { get; set; }
        public bool IsActive { get; set; }
        public bool HasChiled { get; set; }
        public bool IsMegaMenu { get; set; }
        public PageGroup PageGroup { get; set; }
        public string Url { get; set; }
        public bool OpenNewPage { get; set; }

        public virtual ICollection<Navbar> Children { get; set; }
    }
کلاس ViewComponent :
    public class TopNavbar : ViewComponent
    {
        private readonly DbSet<Navbar> _navbars;
        private readonly AppDbContext _dbContext;

        public TopNavbar(AppDbContext dbContext)
        {
            _dbContext = dbContext;
            _navbars = _dbContext.Set<Navbar>();
        }
        public async Task<IViewComponentResult> InvokeAsync()
        {
            var navbars = await _navbars.Include(p=>p.Parent).Include(x=>x.Children).OrderBy(x=>x.ParentId).ToListAsync();
            return View(viewName: "~/Views/Shared/Components/NavbarViewComponent/_Menu.cshtml", navbars);
        }
    }
فراخوانی viewcomponent در Layout.cshtml
 <ul class="menu">
                <li>
                    <a href="Index_demo6.html"><i class="menu_icon_wrapper fal fa-home-lg-alt"></i>صفحه اصلی</a>
                </li>
                @await Component.InvokeAsync("TopNavbar");                
 </ul>
Menu.cshtml_ :
@using TR.Context.Entities
@using Microsoft.AspNetCore.Html
@model IEnumerable<TR.Context.Entities.Navbar>


@foreach (var menu in Model.Where(x => x.Parent == null))
{

    <li class="@(menu.HasChiled ? "has_sub narrow" : "")">
        <a href="#">@menu.Title</a>
        @if (menu.HasChiled)
        {
            <div class="second">
                <div class="inner">
                    <ul>
                        @foreach (var menuChild in menu.Children)
                        {
                        <partial name="~/Views/Shared/Components/NavbarViewComponent/_SubMenu.cshtml" model="menuChild" />
                        }
                    </ul>
                </div>
            </div>
        }
    </li>
}
پارشیال ویو SubMenu.cshtml_
@model TR_.Context.Entities.Navbar

<li class="@(Model.HasChiled ? "sub":"")">
    <a href="#">
        @if (Model.Children.Any())
        {<i class="q_menu_arrow fal fa-angle-left"></i>}
        @Model.Title
    </a>
    @if (Model.Children.Any())
    {
        <ul>
            @foreach (var menuChild in Model.Children)
            {
                <partial name="~/Views/Shared/Components/NavbarViewComponent/_SubMenu.cshtml" model="menuChild" />
            }
        </ul>
    }
</li>
با این خروجی :

مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت سوم - Data binding
در قسمت قبل، ساختار فرم ثبت اطلاعات کارمندان را تکمیل کردیم. در این قسمت قصد داریم این اطلاعات را در کامپوننت آن توسط data binding دریافت کنیم.


نقش ngModel در data binding

ngModel دایرکتیوی است که وجود آن سبب می‌شود تا Angular آن المان ورودی خاص را تحت نظر قرار دهد:
<!--no binding -->
<input name="firstname" ngModel>
در حالت تعریفی فوق، هیچگونه عملیات data binding ایی صورت نمی‌گیرد؛ اما Angular به علت وجود ngModel، از وجود این فیلد مطلع شده‌است. اما کامپوننت برنامه اطلاعات خاصی را دریافت نخواهد کرد.
برای رفع این مشکل می‌توان با data binding یک طرفه شروع کرد:
<!--one way binding -->
<input name="firstname" [ngModel]="firstName">
در اینجا از syntax ویژه‌ی property binding استفاده شده و ngModel داخل [] قرار گرفته‌است و به firstName تنظیم شده‌است. در این حالت Angular در کامپوننت متناظر با این قالب HTML ایی، به دنبال یک خاصیت عمومی به نام firstName می‌گردد و مقدار اولیه‌ی این فیلد را از آن دریافت می‌کند.
در حالت data binding یک طرفه، اگر کاربر اطلاعات فیلد firstname را در فرم برنامه تغییر دهد، این اطلاعات به خاصیت عمومی firstName منعکس نخواهد شد.
برای رفع این مشکل (در صورت نیاز)، می‌توان از data binding دو طرفه استفاده کرد:
<!--two way binding -->
<input name="firstname" [ngModel]="firstName"
(ngModelChange)="firstName=$event">
این حالت شبیه به حالت data binding یک طرفه است؛ با این تفاوت که رویدادگردانی ngModelChange نیز به آن اضافه شده‌است. در اینجا event$ به مقدار فیلد تغییر یافته اشاره می‌کند و آن‌را به firstName انتساب می‌دهد.
البته این حالت دو طرفه، syntax ساده شده‌ی زیر را که به banana in the box نیز معروف شده‌است (موز همان () است و جعبه به [] اشاره می‌کند)، نیز می‌تواند داشته باشد که بیشتر مورد استفاده قرار می‌گیرد:
<!--two way binding -->
<input name="firstname" [(ngModel)]="firstName">


تعریف مدل فرم ثبت اطلاعات کارمندان

برای نگهداری اطلاعات فرم کارمندان، کلاس Employee را به ماژول Employee اضافه می‌کنیم:
 > ng g cl Employee/Employee
با این خروجی:
 installing class
  create src\app\Employee\employee.ts
سپس ساختار این کلاس را به نحو ذیل تکمیل خواهیم کرد که هر کدام از خواص آن، معادل یکی از المان‌های فرم است:
export class Employee {
  constructor(
    public firstName: string,
    public lastName: string,
    public isFullTime: boolean,
    public paymentType: string,
    public primaryLanguage: string
  ) {}
}
TypeScript این امکان را می‌دهد تا بتوان خواص عمومی را مستقیما در سازنده‌ی کلاس تعریف کرد. بنابراین در اینجا برای نمونه firstName هم یکی از آرگومان‌های سازنده‌ی کلاس کارمند است و هم یک خاصیت عمومی تعریف شده‌ی در آن. به علاوه در اینجا می‌توان به این خواص، مقادیر پیش فرضی را نیز انتساب داد تا در حین وهله سازی آن بتوان از تعریف اجباری یک سری از پارامترها صرفنظر کرد.

پس از آن، به فایل employee-register.component.ts مراجعه کرده و وهله‌ای از کلاس را به صورت یک خاصیت عمومی در اختیار قالب HTML ایی آن که فرم جاری را تشکیل می‌دهد، قرار می‌دهیم:
import { Employee } from "app/employee/employee";

export class EmployeeRegisterComponent implements OnInit {
  languages = ["Persian", "English", "Spanish", "Other"];
  model = new Employee("Vahid", "N", true, "FullTime", "Persian");
ابتدا کلاس کارمند import شده و سپس وهله‌ای از آن به نام model، به صورت یک خاصیت عمومی در اختیار قالب آن قرار گرفته‌است.


تغییر قالب فرم ثبت اطلاعات کارمندان برای اتصال به model

در ادامه، مرحله به مرحله قالب فرم جاری را جهت اتصال به شیء model فوق تغییر خواهیم داد:

اتصال به Text boxes

  <form #form="ngForm" novalidate>
    <div class="form-group">
      <label>First Name</label>
      <input type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
    </div>

    <div class="form-group">
      <label>Last Name</label>
      <input type="text" class="form-control" name="lastName" [(ngModel)]="model.lastName">
    </div>
همانطور که مشاهده می‌کنید، اینبار ngModel خالی قسمت قبل را توسط syntax تکمیلی banana in the box به data binding دو طرفه تغییر داده‌ایم. به این ترتیب در ابتدای نمایش فرم، این دو فیلد، مقادیر اولیه نام و نام خانوادگی را از شیء model دریافت کرده و نمایش می‌دهند. به علاوه اگر فرم نیز تغییر کند، این اطلاعات به شیء model و خواص آن نیز منعکس می‌شوند.

برای بررسی این مورد، در پایان فرم جهت دیباگ data binding، اطلاعاتی را که در مدل داریم و همچنین اطلاعاتی را که Angular در حال نظارت بر آن‌ها است، به صورت json در صفحه درج می‌کنیم:
    <button class="btn btn-primary" type="submit">Ok</button>
  </form>
  Model: {{ model | json }}
  <br> Angular: {{ form.value | json }}
  <br> form.pristine: {{ form.pristine }}
برای مثال یکبار [()] را به [] تبدیل کنید و سپس سعی در تغییر مقادیر فرم نمائید. مشاهده می‌کنید هرچند این اطلاعات تحت نظارت Angular هستند، اما چون data binding به حالت یک طرفه تغییر کرده‌است، دیگر انعکاس آن‌ها، در Model مشاهده نمی‌شوند.


اتصال به Check boxes

    <div class="checkbox">
      <label>
            <input type="checkbox" name="is-full-time"
                   [(ngModel)]="model.isFullTime"> Full Time Employee
            </label>
    </div>
روش کار در اینجا نیز همانند قبل است. با استفاده از data binding دو طرفه، مقدار checkbox را به یک خاصیت عمومی boolean انتساب داده‌ایم و برعکس (زمانیکه فرم برای بار اول نمایش داده می‌شود، مقدار اولیه‌ی خود را از شیء model دریافت می‌کند).

اتصال به Radio buttons

    <label>Payment Type</label>
    <div class="radio">
      <label>
            <input type="radio" name="paymentType" value="FullTime" checked
                   [(ngModel)]="model.paymentType">
                Full Time
            </label>
    </div>
    <div class="radio">
      <label>
            <input type="radio" name="paymentType" value="PartTime"
                   [(ngModel)]="model.paymentType">
                Part Time
            </label>
    </div>
روش اتصال به radio buttons نیز بر اساس data binding دو طرفه‌است. فقط در اینجا دقیقا یک خاصیت مشخص، به چندین radio button متصل شده‌است و در نهایت در این گروه که بر اساس name هایی یکسان تشکیل شده‌است، یک مقدار انتخاب می‌شود و مقدار آن از ویژگی value المان متناظر دریافت می‌گردد.

اتصال به Drop downs

    <div class="form-group">
      <label>Primary Language</label>
      <select class="form-control" name="primaryLanguage" [(ngModel)]="model.primaryLanguage">
          <option *ngFor="let lang of languages">
             {{ lang }}
          </option>
      </select>
    </div>
در اینجا نیز ابتدا نامی به این المان انتساب داده شده‌است و سپس توسط data binding دو طرفه، خاصیت متناظری از مدل را به این المان متصل کرده‌ایم یا برعکس؛ زمانیکه این فرم برای اولین بار نمایش داده می‌شود، مقدار اولیه‌ی این فیلد بر اساس مقدار آن در شیء model تعیین می‌شود:



نحوه‌ی فراخوانی یک متد در حین data binding دو طرفه

همانطور که در ابتدای بحث نیز عنوان شد، data binding دو طرفه را به نحو دیگری نیز می‌توان تعریف کرد:

<div class="form-group">
            <label>First Name</label>
            <input type="text" class="form-control" name="firstName" 
                [ngModel]="model.firstName" 
                (ngModelChange)="firstNameToUpperCase($event)">
</div>
در اینجا بجای استفاده‌ی از syntax معروف banana in the box، از روش اتصال یک طرفه و سپس دریافت تغییرات از طریق یک رخدادگردان استفاده شده‌است. مزیت این روش امکان دسترسی همزمان به مقدار وارد شده‌ی توسط کاربر، در کامپوننت متناظر می‌باشد:
  firstNameToUpperCase(value: string) {
    if (value.length > 0)
      this.model.firstName = value.charAt(0).toUpperCase() + value.slice(1);
    else
      this.model.firstName = value;
  }
برای مثال در اینجا اگر کاربر حرف اول یک نام را با حروف کوچک وارد کند، توسط این متد به حرف بزرگ تبدیل شده و جایگزین می‌شود. این جایگزینی نیز بلافاصله در فرم منعکس خواهد شد.

در قسمت بعد مباحث اعتبارسنجی فرم‌های مبتنی بر قالب‌ها را بررسی می‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نظرات مطالب
استفاده از خواص راهبری در Entity framework بجای Join نویسی
معادل سؤال شما با توجه به مدل‌های فوق به صورت زیر است:
می‌خواهیم لیست افرادی را بدست بیاوریم که در شهر x متولد شده‌اند.
روش اول: اگر شماره شهر را داریم:
var cityId = 1;
var list = context.People.Where(x => x.BornInCityId == cityId).ToList();
روش دوم: اگر نام شهر را داریم:
var cityName = "city-1";
var list2 = context.People.Where(x => x.BornInCity.Name == cityName).ToList();
در روش اول از نکته تعریف کلید خارجی استفاده شده.
در روش دوم از نکته استفاده از خواص راهبری، استفاده شده.

نظرات مطالب
محدود کردن کاربر‌ها به آپلود فایل‌هایی خاص در ASP.NET MVC
یک نکته‌ی تکمیلی
نحوه‌ی فقط خواندنی کردنی دسترسی به فایل‌های یک پوشه (گرفتن دسترسی اجرا)
<location path="upload">
  <system.webServer>
    <handlers accessPolicy="Read" />
  </system.webServer>
</location>
مطالب
UnitTest برای کلاس های Abstract با استفاده از Rhino Mocks
در این پست قصد دارم کلاس زیر رو براتون آزمایش کنم:
public abstract class myabstractclass
{
    public abstract string dosomething( string input );
 
    public double round( double number , int decimals )
    {
        return math.round( number , decimals );
    }
}
در کلاس بالا که abstract هستش، متدی دارم که abstract است و بدنه‌ای نداره و از متد بعدی به اسم round برای گرد کردن اعداد استفاده می‌شه. برای تست کلاس بالا و اطمینان از درست بودن متد‌ها باید به روش زیر عمل نمود:

روش اول:
در این روش ابتدا باید کلاسی نوشت تا کلاس abstract بالا رو پیاده سازی کنه:
   public class mynewclass : myabstractclass
   {
       public override string dosomething( string input )
       {
           return input;
       }
   }
بعد می‌شه خیلی راحت برای کلاس دوم متد‌های تست رو نوشت. به روش زیر:
    [testclass]
    public class mytest
    {
        [testmethod]
        public void testround()
        {
            mynewclass mynewclass = new mynewclass();
 
            var result = mynewclass.round( 5.55 , 1 );
 
            assert.areequal( 5.6 , result );
        }
    }
که بعد از اجرا نتیجه زیر رو خواهید دید


البته روش بالا خیلی مورد پسند من نیست. 

در روش دوم که من خیلی بیشتر بهش علاقه دارم دیگه نیازی به استفاده از یک کلاس دوم برای پیاده سازی کلاس abstract نیست. بلکه در این روش از ابزار rhinomocks  برای این کار استفاده می‌کنیم . استفاده از rhino mocks به چندین روش امکان پذیره که امروز 2 روش اونو براتون توضیح میدم:
در روش اول از mockrepository استفاده می‌کنیم  و در روش دوم از روش aaa یا arrange-act-assert

استفاده از mockrepository :
ابتدا کد‌های مربوطه رو می‌نویسم:
       [testmethod]
       public void testwithmockrepository()
       {
           var mockrepository = new rhino.mocks.mockrepository();
           var mock = mockrepository.partialmock<myabstractclass>();
 
           using ( mockrepository.record() )
           {
               expect.call( mock.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();
           }
           using ( mockrepository.playback() )
           {
               assert.areequal( "hi..." , mock.dosomething( "salam" ) );             
           }
       }
همانطور که در کد‌های نوشته شده بالا می‌بینید ابتدا یک mockrepository ساخته شده، سپس از نمونه اون کلاس برای ساخت partialmock کلاس myabstractclass استفاده کردم. در این روش متد‌های expect حتما باید بین بلاک record نوشته شوند تا بتونیم از اون‌ها در بلاک playback استفاده کنیم. نتیجه اجرای این متد تست هم مثل متد تست قبلی درست است.
در مورد expect.call باید بگم که از این کلاس برای شبیه سازی رفتار یک متد استفاده میشه (مثلا در مواقعی که یک متد برای انجام عملیات باید به دیتا بیس وصل شده و یک query را اجرا کنه) برای اینکه در تست، از این عملیات صرف نظر بشه از mock‌ها استفاده کرده و رفتار متد رو به روش بالا شبیه سازی می‌کنیم. البته کار کردن با rhino mocks به صورت بالا دیگه از مد افتاده و جدیدا از روش aaa استفاده میشه که اونو در پایین توضیح می‌دم:
      [testmethod]
      public void testwithaaa()
      {
          var mock = mockrepository.generatepartialmock<myabstractclass>();
 
          mock.expect( x => x.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();//arange
 
          var result = mock.dosomething( "salam" );//act
 
          assert.areequal( "hi..." , result );//assert
      }
توی این روش دیگه خبری از record و playback نیست و همانطور که مشخصه از سه مرحله arrange-act-assert تشکیل شده که هر مرحله رو براتون مشخص کردم. مزیت استفاده از این روش اینه که اولا تعداد خطوط کمتری برای کد نویسی نیاز داره و دوما سرعت اجراش از روش قبلی خیلی بیشتره. در مورد repeat.once هم بگم که این دستور نشون می‌ده فقط یک بار اجازه انجام عملیات act رو دارید. یعنی اگر کد هارو به صورت زیر تغییر بدیم با خطا روبرو می‌شیم:
       [testmethod]
       public void testwithaaa()
       {
           var mock = mockrepository.generatepartialmock<myabstractclass>();
 
           mock.expect( x => x.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();//arange
 
           var result = mock.dosomething( "salam" );//act
           var result2 = mock.dosomething( "bye" );//act
           assert.areequal( "hi..." , result );//assert
       }



خطای آن هم واضح داره میگه که expected#1 هستش در حالی که actual#2 (تعداد دفعات حقیقی از دفعات مورد انتظار بیشتره)
توی پست‌های بعدی (اگه وقت بشه) حتما در مورد rhino mocks بیشتر توضیح میدم 
نظرات مطالب
دستکاری mime types در IIS
فعلا خیر؛ اگر mime type بروز داده شده application/x-shockwave-flash نباشه، پسوند می‌تونه همان swf هم باشد. البته اگر پسوند را عوض کردید، خودبخود mime type هم چیز دیگری خواهد شد (بر اساس نگاشت‌های موجود در وب سرور).
و اگر لازم باشه بر اساس پسوند هم می‌تونند ...
مطالب
فارسی کردن اعداد در صفحات blazor
برای فارسی کردن اعداد در صفحات  HTML قبلا از  کتابخانه‌های  jquery  یا javascript استفاده می‌کردیم. در این مقاله قصد دارم فارسی کردن اعداد را به کمک کامپوننت‌های  blazor انجام دهم. البته بهتر است از این روش برای وقتی استفاده کنیم که قرار است متن ما فقط شامل اعداد باشد؛ مثلا فیلدهای عددی یک جدول.

یک کامپوننت جدید را به نام PersianNumber به صورت زیر ایجاد می‌کنیم. در این کامپوننت یک پارامتر را به نام Number داریم که کاراکتر به کاراکتر آن را پیمایش کرده و اعداد انگلیسی را با اعداد فارسی جایگزین می‌کنیم:
@Number

@code {
    [Parameter]
    public string Number { get; set; }

    protected override Task OnInitializedAsync()
    {
        var persianDic = new Dictionary<char, char>
        {
            {'0','۰'},
            {'1','۱'},
            {'2','۲'},
            {'3','۳'},
            {'4','۴'},
            {'5','۵'},
            {'6','۶'},
            {'7','۷'},
            {'8','۸'},
            {'9','۹'},

        };
        var number = Number.ToString();
        var ech = number.ToCharArray();
        for (int i = 0; i < ech.Length; i++)
        {
            persianDic.TryGetValue(ech[i], out char pch);
            if (pch == null)
                continue;
            ech[i] = pch;
        }
        Number = new string(ech);
        return base.OnInitializedAsync();
    }
}
حالا از این کامپوننت در هر جای صفحه که مثلا عددی را از دیتابیس (api) دریافت کرده و می‌خواهیم نمایش دهیم، استفاده می‌کنیم:
... 
@foreach (var item in _items)
                {
                    <tr>
                        <td class="h6 text-color-1">@item.Title</td>
                        <td> <PersianNumber Number="@item.Price.ToString()"/> ریال</td>
                    </tr>
                }
...

اشتراک‌ها
معرفی Kretes

Programming Environment for TypeScript & Node.js
Kretes is a programming environment for TypeScript applications built on top of Visual Studio Code. It combines a web framework with an editor into an integrated, battery-included solution. 

معرفی Kretes