مطالب
استفاده از Kendo UI templates
در مطلب «صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid» در انتهای بحث، ستون IsAvailable به صورت زیر تعریف شد:
columns: [
               {
                   field: "IsAvailable", title: "موجود است",
                   template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>'
                }
]
Templates، جزو یکی از پایه‌های Kendo UI Framework هستند و توسط آن‌ها می‌توان قطعات با استفاده‌ی مجدد HTML ایی را طراحی کرد که قابلیت یکی شدن با اطلاعات جاوا اسکریپتی را دارند.
همانطور که در این مثال نیز مشاهده می‌کنید، قالب‌های Kendo UI از Hash (#) syntax استفاده می‌کنند. در اینجا قسمت‌هایی از قالب که با علامت # محصور می‌شوند، در حین اجرا، با اطلاعات فراهم شده جایگزین خواهند شد.
برای رندر مقادیر ساده می‌توان از # =# استفاده کرد. از # :# برای رندر اطلاعات HTML-encoded کمک گرفته می‌شود و #  # برای رندر کدهای جاوا اسکریپتی کاربرد دارد. از حالت HTML-encoded برای نمایش امن اطلاعات دریافتی از کاربران و جلوگیری از حملات XSS استفاده می‌شود.
اگر در این بین نیاز است # به صورت معمولی رندر شود، در حالت کدهای جاوا اسکریپتی به صورت #\\ و در HTML ساده به صورت #\ باید مشخص گردد.


مثالی از نحوه‌ی تعریف یک قالب Kendo UI

    <!--دریافت اطلاعات از منبع محلی-->
    <script id="javascriptTemplate" type="text/x-kendo-template">
        <ul>
            # for (var i = 0; i < data.length; i++) { #
            <li>#= data[i] #</li>
            # } #
        </ul>
    </script>

    <div id="container1"></div>
    <script type="text/javascript">
        $(function () {
            var data = ['User 1', 'User 2', 'User 3'];
            var template = kendo.template($("#javascriptTemplate").html());
            var result = template(data); //Execute the template
            $("#container1").html(result); //Append the result
        });
    </script>
این قالب ابتدا در تگ script محصور می‌شود و سپس نوع آن مساوی text/x-kendo-template قرار می‌گیرد. در ادامه توسط یک حلقه‌ی جاوا اسکریپتی، عناصر آرایه‌ی فرضی data خوانده شده و با کمک Hash syntax در محل‌های مشخص شده قرار می‌گیرند.
در ادامه باید این قالب را رندر کرد. برای این منظور یک div با id مساوی container1 را جهت تعیین محل رندر نهایی اطلاعات مشخص می‌کنیم. سپس متد kendo.template بر اساس id قالب اسکریپتی تعریف شده، یک شیء قالب را تهیه کرده و سپس با ارسال آرایه‌ای به آن، سبب اجرای آن می‌شود. خروجی نهایی، یک قطعه کد HTML است که در محل container1 درج خواهد شد.
همانطور که ملاحظه می‌کنید، متد kendo.template، نهایتا یک رشته را دریافت می‌کند. بنابراین همینجا و به صورت inline نیز می‌توان یک قالب را تعریف کرد.


کار با منابع داده راه دور

فرض کنید مدل برنامه به صورت ذیل تعریف شده‌است:
namespace KendoUI04.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}
و لیستی از آن توسط یک ASP.NET Web API کنترلر، به سمت کاربر ارسال می‌شود:
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using KendoUI04.Models;

namespace KendoUI04.Controllers
{
    public class ProductsController : ApiController
    {
        public IEnumerable<Product> Get()
        {
            return ProductDataSource.LatestProducts.Take(10);
        }
    }
}
در سمت کاربر و در View برنامه خواهیم داشت:
    <!--دریافت اطلاعات از سرور-->
    <div>
        <div id="container2"><ul></ul></div>
    </div>

    <script id="template1" type="text/x-kendo-template">
        <li> #=Id# - #:Name# - #=kendo.toString(Price, "c")#</li>
    </script>

    <script type="text/javascript">
        $(function () {
            var producatsTemplate1 = kendo.template($("#template1").html());

            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                change: function () {
                    $("#container2 > ul").html(kendo.render(producatsTemplate1, this.view()));
                }
            });
            productsDataSource.read();
        });
    </script>
ابتدا یک div با id مساوی container2 جهت تعیین محل نهایی رندر قالب template1 در صفحه تعریف می‌شود.
هرچند خروجی دریافتی از سرور نهایتا یک آرایه از اشیاء Product است، اما در template1 اثری از حلقه‌ی جاوا اسکریپتی مشاهده نمی‌شود. در اینجا چون از متد kendo.render استفاده می‌شود، نیازی به ذکر حلقه نیست و به صورت خودکار، به تعداد عناصر آرایه دریافتی از سرور، قطعه HTML قالب را تکرار می‌کند.
در ادامه برای کار با سرور از یک Kendo UI DataSource استفاده شده‌است. قسمت transport/read آن، کار تعریف محل دریافت اطلاعات را از سرور مشخص می‌کند. رویدادگران change آن اطلاعات نهایی دریافتی را توسط متد view در اختیار متد kendo.render قرار می‌دهد. در نهایت، قطعه‌ی HTML رندر شده‌ی نهایی حاصل از اجرای قالب، در بین تگ‌های ul مربوط به container2 درج خواهد شد.
رویدادگران change زمانیکه data source، از اطلاعات راه دور و یا یک آرایه‌ی جاوا اسکریپتی پر می‌شود، فراخوانی خواهد شد. همچنین مباحث مرتب سازی اطلاعات، صفحه بندی و تغییر صفحه، افزودن، ویرایش و یا حذف اطلاعات نیز سبب فراخوانی آن می‌گردند. متد view ایی که در این مثال فراخوانی شد، صرفا در روال رویدادگردان change دارای اعتبار است و آخرین تغییرات اطلاعات و آیتم‌های موجود در data source را باز می‌گرداند.


یک نکته‌ی تکمیلی: فعال سازی intellisense کدهای جاوا اسکریپتی Kendo UI

اگر به پوشه‌ی اصلی مجموعه‌ی Kendo UI مراجعه کنید، یکی از آن‌ها vsdoc نام دارد که داخل آن فایل‌های min.intellisense.js و vsdoc.js مشهود هستند.
اگر از ویژوال استودیوهای قبل از 2012 استفاده می‌کنید، نیاز است فایل‌های vsdoc.js متناظری را به پروژه اضافه نمائید؛ دقیقا در کنار فایل‌های اصلی js موجود. اگر از ویژوال استودیوی 2012 و یا بالاتر استفاده می‌کنید باید از فایل‌های intellisense.js متناظر استفاده کنید. برای مثال اگر از kendo.all.min.js کمک می‌گیرید، فایل متناظر با آن kendo.all.min.intellisense.js خواهد بود.
بعد از اینکار نیاز است فایلی به نام references.js_ را به پوشه‌ی اسکریپت‌های خود با این محتوا اضافه کنید (برای VS 2012 به بعد):
/// <reference path="jquery.min.js" />
/// <reference path="kendo.all.min.js" />
نکته‌ی مهم اینجا است که این فایل به صورت پیش فرض از مسیر Scripts/_references.js/~ خوانده می‌شود. برای اضافه کردن مسیر دیگری مانند js/_references.js/~ باید آن‌را به تنظیمات ذیل اضافه کنید:
 Tools menu –> Options -> Text Editor –> JavaScript –> Intellisense –> References
گزینه‌ی Reference Group را به (Implicit (Web تغییر داده و سپس مسیر جدیدی را اضافه نمائید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
KendoUI04.zip
نظرات مطالب
طریقه بررسی صحت کدملی به کمک متدهای الحاقی
اون وقت اگر در این بین به مشکل برخورد چطور؟ استثناء اصلا چیز بدی نیست؛ کرش بسیار پدیده مطلوبی است! چون نشان وجود مشکل در سیستم است.
بجای اینکار بهتر است در همان بدو امر بررسی شود که رشته دریافتی عدد است یا خیر. چون طول رشته زیاد است می‌شود از مثلا Regex استفاده کرد:
public static bool IsItNumber(this string inputvalue)
{
  var isnumber = new Regex("[^0-9]");
  return !isnumber.IsMatch(inputvalue);
}
نظرات مطالب
بررسی تغییرات Blazor 8x - قسمت چهارم - معرفی فرم‌های جدید تعاملی

یک نکته‌ی تکمیلی: روش طراحی binding دو طرفه در Blazor SSR

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

[Parameter] public T? Value { set; get; }

[Parameter] public EventCallback<T?> ValueChanged { get; set; }

[Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;

اگر این خواص را به کامپوننت‌های توکار خود Blazor متصل کنیم (مانند InputBox آن و مابقی آن‌ها)، نیازی به کدنویسی بیشتری ندارند و کار می‌کنند. اما اگر قرار است از یک input ساده‌ی Html ای استفاده کنیم، نیاز است ValueChanged آن‌را اینبار در متد OnInitialized فراخوانی کنیم؛ چون در زمان post-back به سرور است که مقدار آن در اختیار مصرف کننده‌ی کامپوننت قرار می‌گیرد. این مورد، مهم‌ترین تفاوت نحوه‌ی طراحی binding دوطرفه در Blazor SSR با مابقی حالات و نگارش‌های Blazor است.

بررسی وقوع post-back به سرور به دو روش زیر میسر است:

الف) بررسی کنیم که آیا HttpPost ای رخ‌داده‌است؟ سپس در همین لحظه، متد ValueChanged.InvokeAsync را فراخوانی کنیم:

[CascadingParameter] internal HttpContext HttpContext { set; get; } = null!;

protected override void OnInitialized()
{
   base.OnInitialized();

   if (HttpContext.IsPostRequest())
   {
      SetValueCallback(Value);
   }
}

private void SetValueCallback(string value)
{
   if (!ValueChanged.HasDelegate)
   {
      return;
   }

   _ = ValueChanged.InvokeAsync(value);
}

در این مثال نحوه‌ی فعالسازی ارسال اطلاعات از یک کامپوننت سفارشی را به مصرف کننده‌ی آن ملاحظه می‌کنید. اینکار در قسمت OnInitialized و فقط در صورت ارسال اطلاعات به سمت سرور، فعال خواهد شد.

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

protected override void OnInitialized()
{
   base.OnInitialized();

   if (HttpContext.Request.HasFormContentType &&
       HttpContext.Request.Form.TryGetValue(ValueField.HtmlFieldName, out var data))
   {
      SetValueCallback(data.ToString());
   }
}

در اینجا از ValueField.HtmlFieldName که در نکته‌ی قبلی معرفی BlazorHtmlField به آن اشاره شد، جهت یافتن نام واقعی فیلد ورودی، استفاده شده‌است.

نظرات مطالب
شبیه سازی outer Join در entity framework
جهت تکمیل بحث، اگر مدل‌های برنامه به این صورت باشند (محل تولد اجباری است و Id کلید خارجی آن نال پذیر نیست؛ به همراه محل صدور اختیاری، که Id نال پذیر دارد):
    public class Place
    {
        public int Id { set; get; }
        public string Name { set; get; }

        public virtual ICollection<Person> Personnel { set; get; }
    }

    public class Person
    {
        public int Id { set; get; }
        public string FirstName { set; get; }
        public string LastName { set; get; }

        [ForeignKey("BirthPlaceId")]
        public virtual Place BirthPlace { set; get; }
        public int BirthPlaceId { set; get; }

        [ForeignKey("IssuanceLocationId")]
        public virtual Place IssuanceLocation { set; get; }
        public int? IssuanceLocationId { set; get; }
    }
با این Context :
public class MyContext : DbContext
    {
        public DbSet<Place> Places { get; set; }
        public DbSet<Person> Personnel { get; set; }

        public MyContext()
        {
            this.Database.Log = sql => Console.WriteLine(sql);
        }
    }
آنگاه خروجی کوئری ذیل (که یک include دارد روی خاصیت راهبری که مقدار Id کلید خارجی آن ممکن است نال باشد (محل صدور) و نه مورد دومی که Id غیرنال پذیر دارد (محل تولد))
context.Personnel.Include(x => x.IssuanceLocation)
معادل خواهد بود با (left outer join به صورت خودکار تشکیل شده)
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[FirstName] AS [FirstName],
    [Extent1].[LastName] AS [LastName],
    [Extent1].[BirthPlaceId] AS [BirthPlaceId],
    [Extent1].[IssuanceLocationId] AS [IssuanceLocationId],
    [Extent2].[Id] AS [Id1],
    [Extent2].[Name] AS [Name],
    [Extent1].[Place_Id] AS [Place_Id]
    FROM  [dbo].[People] AS [Extent1]
    LEFT OUTER JOIN [dbo].[Places] AS [Extent2] ON [Extent1].[IssuanceLocationId] = [Extent2].[Id]

و خروجی کوئری زیر که DefaultIfEmpty را هم لحاظ کرده و join نویسی صریحی هم دارد (مطابق مقاله فوق):
var query = from personnel in context.Personnel
                            join issuanceLocation in context.Places on
                                  personnel.IssuanceLocationId equals issuanceLocation.Id into aIssuanceLocation
                            from IL in aIssuanceLocation.DefaultIfEmpty()
                            join birthLocation in context.Places on
                                  personnel.BirthPlaceId equals birthLocation.Id into aBirthLocation
                            from BL in aBirthLocation.DefaultIfEmpty()
                            select new
                               {
                                   personnel.Id,
                                   personnel.FirstName,
                                   personnel.LastName,
                                   IssuanceLocation = IL.Name,
                                   BirthLocation = BL.Name
                               };
معادل است با:
SELECT
                        [Extent1].[Id] AS [Id],
                        [Extent1].[FirstName] AS [FirstName],
                        [Extent1].[LastName] AS [LastName],
                        [Extent2].[Name] AS [Name],
                        [Extent3].[Name] AS [Name1]
                        FROM [dbo].[People] AS [Extent1]
                        LEFT OUTER JOIN [dbo].[Places] AS [Extent2] ON [Extent1].[IssuanceLocationId] = [Extent2].[Id]
                        INNER JOIN [dbo].[Places] AS [Extent3] ON [Extent1].[BirthPlaceId] = [Extent3].[Id]
و البته این خروجی دوم فقط در صورتی تشکیل می‌شود که قسمت select new ذکر شود. در غیراینصورت مشکل select n+1 را پیدا می‌کند و اصلا چنین join ایی تشکیل نخواهد شد (در یک حلقه، به ازای هر شخص، یکبار کوئری select به جدول مکان‌ها تشکیل می‌شود). همچنین یک inner join هم علاوه بر left outer join تشکیل شده (برای فیلد غیرنال پذیر).
حتی همین حالت دوم را هم با کوئری ذیل که از خواص راهبری استفاده کرده، می‌توان تولید کرد:
var query = context.Personnel.Select(x => new
             {
              x.Id,
              x.FirstName,
              x.LastName,
              BirthPlaceName = x.BirthPlace.Name,
              IssuanceLocationName = x.IssuanceLocation == null ? "" : x.IssuanceLocation.Name
             });
با این خروجی SQL (به صورت خودکار برای فیلد نال پذیر، left outer join و برای غیر نال پذیر inner join تشکیل داده)
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[FirstName] AS [FirstName],
    [Extent1].[LastName] AS [LastName],
    [Extent2].[Name] AS [Name],
    CASE WHEN ([Extent3].[Id] IS NULL) THEN N'' ELSE [Extent3].[Name] END AS [C1]
    FROM   [dbo].[People] AS [Extent1]
    INNER JOIN [dbo].[Places] AS [Extent2] ON [Extent1].[BirthPlaceId] = [Extent2].[Id]
    LEFT OUTER JOIN [dbo].[Places] AS [Extent3] ON [Extent1].[IssuanceLocationId] = [Extent3].[Id]
نظرات مطالب
اجرای وظایف زمان بندی شده با Quartz.NET - قسمت اول
سلام.
من یه وظیفه نوشتم که می‌یاد هر 1 دقیقه 1 بار یه متن رو در Table درج می‌کنه.
این کد کلاس
using Quartz;
using System.Data;
using System.Data.OleDb;
using System.Configuration;


namespace WebApplication1
{
    
    public class TestQuartzClass:IJob
    {
        
       
        public void Execute(IJobExecutionContext context)
        {
            //Page myPage = (Page)HttpContext.Current.Handler;
            //TextBox MyTextBox=(TextBox)myPage.FindControl("txt");
            
            string sql = "Insert into tbl_Test (Content) values (@Content)";
            ExecuteNoneQuery(System.Data.CommandType.Text, sql, new OleDbParameter[]{
                //new OleDbParameter("@Content", MyTextBox.Text)
                new OleDbParameter("@Content","Hello world!")
            });

        }

        public int ExecuteNoneQuery(CommandType commandType, string commandText, params OleDbParameter[] commandParameters)
        {
            using (OleDbConnection con = new OleDbConnection(ConfigurationManager.ConnectionStrings["ConStr"].ConnectionString))
            {
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = con;
                cmd.CommandType = commandType;
                cmd.CommandText = commandText;
                cmd.Parameters.AddRange(commandParameters);
                con.Open();
                int retVal = cmd.ExecuteNonQuery();
                con.Close();
                return retVal;
            }
        }
    }


}



و این هم کد صفحه ای که با کلیک دکمه وظیفه شروع به کار می‌کنه

using System;
using Quartz;
using Quartz.Impl;

namespace WebApplication1
{
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        public static void ConfigureQuartzJobs()
        {
            // construct a scheduler factory
            ISchedulerFactory schedFact = new StdSchedulerFactory();

            // get a scheduler
            IScheduler sched = schedFact.GetScheduler();
            sched.Start();
            IJobDetail job = JobBuilder.Create<TestQuartzClass>()
                .WithIdentity("SendJob")
                .Build();
            var trigger = TriggerBuilder.Create()
                .WithIdentity("SendTrigger")
                .WithSimpleSchedule(x => x.WithIntervalInMinutes(1).RepeatForever())
                //.StartAt(startTime)
                .StartNow()
                .Build();

            sched.ScheduleJob(job, trigger);
        }

        protected void btn_Click(object sender, EventArgs e)
        {
            ConfigureQuartzJobs();
        }
    }
}

همونطور که می‌بینید متن رو به شکل زیر پاس دادم.
new OleDbParameter("@Content","Hello world!")
ولی من می‌خوام این متن رو از TextBox بگیرم.
و برای اینکه در کلاس بتونم به کنترلهای صفحه دسترسی داشته باشم کدهای زیر رو به متد Exceute اضافه کردم
Page myPage = (Page)HttpContext.Current.Handler;
            TextBox MyTextBox=(TextBox)myPage.FindControl("txt");

ولی به محض اینکه این کدها رو اضافه می‌کنم دیگه برنامه کار نمی‌کنه. 

متن داخل تکست باکس رو هم قصد داشتم به شکل زیر پاس بدم.
new OleDbParameter("@Content", MyTextBox.Text)

لطفاً راهنمایی کنید.
من یک نمونه هم به منظور تست آماده کردم که از لینک زیر می‌تونید دانلود کنید:
http://www.4shared.com/rar/1Fu_jpOOba/WebApplication1.html 
مطالب
کوئری نویسی در EF Core - قسمت چهارم - اعمال تغییرات در داده‌ها
نوع دیگری از کوئری‌های پرکاربرد، کوئری‌های مرتبط با ثبت، حذف و ویرایش اطلاعات هستند که در این قسمت آن‌ها را بررسی می‌کنیم. البته این مثال‌ها از یکسری مثال کوئری‌های مرتبط با PostgreSQL، به EF-Core تبدیل و ترجمه شده‌اند. به همین جهت تطابق یک به یکی در اینجا وجود نداشته و روش شیءگرایی که ORMها برای کار با داده‌ها بکار می‌گیرند، الزاما کوئری‌های یکسانی را تولید نمی‌کنند؛ اما نتیجه‌ی نهایی آن‌ها یکی است.


مثال 1: افزودن ردیفی به یک جدول بانک اطلاعاتی

امکان و ویژگی جدیدی به نام SPA قرار است به مجموعه اضافه شود. اطلاعات آن که شامل موارد ذیل است، نیاز است به جدول facilities اضافه شود:
facid: 9, Name: 'Spa', membercost: 20, guestcost: 30, initialoutlay: 100000, monthlymaintenance: 800.
اگر قرار باشد چنین کاری را توسط دستورات SQL انجام دهیم، عموما به یکی از دو روش زیر عمل می‌شود:
insert into facilities
    (facid, name, membercost, guestcost, initialoutlay, monthlymaintenance)
    values (9, 'Spa', 20, 30, 100000, 800);
-- OR
insert into facilities values (9, 'Spa', 20, 30, 100000, 800);
که معادل آن در EF-Core به صورت زیر است:
context.Facilities.Add(new Facility
                {
                    Name = "Spa",
                    MemberCost = 20,
                    GuestCost = 30,
                    InitialOutlay = 100000,
                    MonthlyMaintenance = 800
                });
context.SaveChanges();
ابتدا وهله‌ای از موجودیت Facility به DbSet مرتبط با آن اضافه می‌شود و در آخر SaveChanges فراخوانی خواهد شد تا کوئری متناظر با آن ساخته شده و به بانک اطلاعاتی اعمال شود.


مثال 2: افزودن چندین ردیف از اطلاعات به یک جدول بانک اطلاعاتی

همان مثال قبلی را درنظر بگیرید. اینبار می‌خواهیم دو ردیف را به آن اضافه کنیم:
facid: 9, Name: 'Spa', membercost: 20, guestcost: 30, initialoutlay: 100000, monthlymaintenance: 800.
facid: 10, Name: 'Squash Court 2', membercost: 3.5, guestcost: 17.5, initialoutlay: 5000, monthlymaintenance: 80.
معادل کدهای SQL چنین عملی، می‌تواند کوئری زیر باشد:
insert into facilities
    (facid, name, membercost, guestcost, initialoutlay, monthlymaintenance)
    values
        (9, 'Spa', 20, 30, 100000, 800),
        (10, 'Squash Court 2', 3.5, 17.5, 5000, 80);
و روش انجام آن در EF-Core تفاوتی با مثال قبلی ندارد:
context.Facilities.Add(new Facility
                {
                    Name = "Spa",
                    MemberCost = 20,
                    GuestCost = 30,
                    InitialOutlay = 100000,
                    MonthlyMaintenance = 800
                });

context.Facilities.Add(new Facility
                {
                    Name = "Squash Court 2",
                    MemberCost = 3.5M,
                    GuestCost = 17.5M,
                    InitialOutlay = 5000,
                    MonthlyMaintenance = 80
                });
context.SaveChanges();
در اینجا می‌توان به هر تعدادی که نیاز است وهله‌های جدیدی از Facility را به context افزودن و سپس SaveChanges را در آخر کار فراخوانی کرد. اینکه EF-Core دستورات insert معادل را به یکباره و یا به صورت مجزایی اجرا می‌کند، به مفهومی به نام batching مرتبط است. اطلاعات بیشتر


مثال 3: افزودن اطلاعات محاسبه شده به یک جدول بانک اطلاعاتی

اطلاعات زیر را درنظر بگیرید:
Name: 'Spa', membercost: 20, guestcost: 30, initialoutlay: 100000, monthlymaintenance: 800.
در مثال اصلی عنوان شده که می‌خواهیم ID آن‌را یکی بیشتر از ردیف قبلی ثبت کنیم. در EF-Core و تنظیمات موجودیت‌هایی که داریم:
namespace EFCorePgExercises.Entities
{
    public class FacilityConfiguration : IEntityTypeConfiguration<Facility>
    {
        public void Configure(EntityTypeBuilder<Facility> builder)
        {
            builder.HasKey(facility => facility.FacId);
            builder.Property(facility => facility.FacId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);
چون ستون ID به صورت خود افزایش یابنده معرفی شده‌است که از صفر شروع می‌شود و به صورت خودکار توسط بانک اطلاعاتی یکی یکی افزایش می‌یابد، نیازی به حل این مساله وجود ندارد. چون ID افزایش یابنده را خود بانک اطلاعاتی محاسبه می‌کند. همچنین به همین علت در مثال‌های قبلی نیز ID را به صورت مستقیمی مقدار دهی نکردیم. اگر نیاز به انجام چنین کاری وجود داشته باشد (ذکر صریح ID خاصی)، با توجه به طراحی بانک اطلاعاتی حاصل از این تنظیمات:
CREATE TABLE [dbo].[Facilities](
[FacId] [int] IDENTITY(0,1) NOT NULL,
--- ...
 CONSTRAINT [PK_Facilities] PRIMARY KEY CLUSTERED 
(
[FacId] ASC
);
باید مانند مثال ثبت اطلاعات اولیه‌ی در بانک اطلاعاتی در قسمت اول این سری، از روش SET IDENTITY_INSERT Facilities ON استفاده کرد تا بتوان مجوز ثبت دستی این ID کنترل شده‌ی توسط بانک اطلاعاتی را پیدا کرد.


مثال 4: به روز رسانی اطلاعاتی از پیش موجود

می‌خواهیم مقدار InitialOutlay دومین زمین تنیس را از 8000 موجود به 10000 تغییر دهیم. با توجه به اینکه ID این زمین شماره 1 است، در حالت متداول SQL نویسی، به کدهای زیر خواهیم رسید:
update facilities
    set initialoutlay = 10000
    where facid = 1;
که معادل EF-Core آن به صورت زیر است:
var facility1 = context.Facilities.Find(1);
facility1.InitialOutlay = 10000;
context.SaveChanges();
این دستورات کوئری مشابهی را تولید نمی‌کنند. ابتدا موجودیت متناظر با ID شماره‌ی 1 از بانک اطلاعاتی واکشی شده و سپس مقدار خاصیتی از آن تغییر کرده‌است. در آخر SaveChanges بر روی آن فراخوانی می‌شود.
EF-Core برای اینکه بتواند تغییرات اعمالی به یک شیء را محاسبه کند، نیاز دارد تا آن شیء را به نحوی در سیستم change tracking خودش موجود داشته باشد. هر نوع کوئری که در EF-Core نوشته می‌شود و به همراه متد AsNoTracking نیست، خروجی تک تک اشیاء حاصل از آن پیش از ارائه‌ی نهایی، وارد سیستم change tracking آن می‌شوند. یعنی اگر مقادیر خواص این اشیاء را تغییر داده و بر روی آن‌ها SaveChanges را فراخوانی کنیم، کوئری‌های متناظر با به روز رسانی تنها این خواص تغییر یافته به صورت خودکار محاسبه شده و به بانک اطلاعاتی اعمال می‌شوند.
فراخوانی متد AsNoTracking بر روی کوئری‌های EF-Core، تولید پروکسی‌های change tracking را غیرفعال می‌کند. یک چنین کوئری‌هایی صرفا کاربردهای گزارشگیری فقط خواندنی را دارند و نسبت به کوئری‌های معمولی، سریعتر و با مصرف حافظه‌ی کمتری هستند. بنابراین نتایج حاصل از کوئری‌های متداول EF-Core، به صورت پیش‌فرض (یعنی بدون داشتن متد AsNoTracking) هم خواندنی و هم نوشتنی با قابلیت اعمال به بانک اطلاعاتی هستند.


مثال 5: به روز رسانی چندین ردیف و چندین جدول در یک زمان

می‌خواهیم مقادیر MemberCost  و GuestCost دو زمین تنیس را به 6 و 30 تغییر دهیم. روش انجام اینکار با SQL نویسی معمولی به صورت زیر است:
update cd.facilities
    set
        membercost = 6,
        guestcost = 30
    where facid in (0,1);
اما همانطور که عنوان شد در EF-Core ابتدا باید اشیاء متناظر با این زمین‌های تنیس را در سیستم change tacking موجود داشت و سپس نسبت به ویرایش آن‌ها اقدام نمود. یکی از روش‌های وارد کردن اشیاء به سیستم change tacking، نوشتن کوئری‌های بدون متد AsNoTracking است و سپس به روز رسانی نتایج حاصل از آن‌ها که اکنون توسط پروکسی‌های change tracking محصور شده‌اند و در آخر فراخوانی SaveChanges بر روی context جاری:
int[] facIds = { 0, 1 };
var tennisCourts = context.Facilities.Where(x => facIds.Contains(x.FacId)).ToList();
foreach (var tennisCourt in tennisCourts)
{
     tennisCourt.MemberCost = 6;
     tennisCourt.GuestCost = 30;
}

context.SaveChanges();


مثال 6: به روز رسانی اطلاعات یک ردیف بر اساس اطلاعات ردیفی دیگر

می‌خواهیم هزینه‌ی دومین زمین تنیس را به نحوی ویرایش کنیم که 10 درصد بیشتر از هزینه‌ی اولین زمین تنیس باشد.
روش پیشنهادی انجام اینکار با SQL نویسی مستقیم به صورت زیر است:
update cd.facilities facs
    set
        membercost = (select membercost * 1.1 from cd.facilities where facid = 0),
        guestcost = (select guestcost * 1.1 from cd.facilities where facid = 0)
    where facs.facid = 1;
در EF-Core می‌توان اشیاء متناظر با این دو زمین تنیس را ابتدا واکشی کرد، سپس تغییر داد و در نهایت ذخیره کرد:
var fac0 = context.Facilities.Where(x => x.FacId == 0).First();
var fac1 = context.Facilities.Where(x => x.FacId == 1).First();
fac1.MemberCost = fac0.MemberCost * 1.1M;
fac1.GuestCost = fac0.GuestCost * 1.1M;

context.SaveChanges();


مثال 7: حذف تمام اطلاعات یک جدول

می‌خواهیم تمام اطلاعات جدول bookings را حذف کنیم.
روش انجام اینکار با SQL نویسی مستقیم به صورت زیر است:
delete from bookings
اما ... این تک کوئری، معادلی را در EF-Core استاندارد ندارد. چون EF-Core نیاز دارد مدام تمام اطلاعات ویرایشی/حذف و به روز رسانی را در context و سیستم change tracking خودش داشته باشد، ابتدا باید توسط یک کوئری لیست اشیاء مدنظر را تهیه کرد و سپس آن‌را به متد RemoveRange معرفی کرد تا حذف تک تک آن‌ها که شامل صدها کوئری خواهد شد، صورت گیرد:
context.Bookings.RemoveRange(context.Bookings.ToList());
context.SaveChanges();
این روش سریع نیست؛ اما کار می‌کند!
البته هستند کتابخانه‌های ثالثی (^ و ^) که انجام به روز رسانی دسته‌ای و یا حذف دسته‌ای از رکوردها را تنها با یک کوئری SQL میسر می‌کنند؛ اما ... هنوز جزئی از EF استاندارد نشده‌اند و مهم‌ترین مشکل احتمالی این روش‌ها، همگام نبودن context و سیستم change tacking، با نتیجه‌ی حاصل از به روز رسانی یکباره‌ی صدها ردیف است.



مثال 8: حذف یک کاربر از جدول کاربران

می‌خواهیم کاربر شماره‌ی 37 را حذف کنیم.
روش انجام اینکار با SQL نویسی به صورت زیر است:
delete from members where memid = 37;
و در EF-Core برای انجام اینکار می‌توان ابتدا شیء متناظر با کاربر 37 را از طریق یک کوئری به سیستم change tracking وارد کرد و سپس آن‌را حذف نمود:
var mem37 = context.Members.Where(x => x.MemId == 37).First();
context.Members.Remove(mem37);

context.SaveChanges();

یک نکته: امکان ساده‌تر حذف یک ردیف با داشتن ID آن

کوئری گرفتن از بانک اطلاعاتی، یک روش وارد کردن شیءای به context و سیستم change tacking آن است. در این حالت عموما فرض بر این است که ID شیء را نمی‌دانیم. اما اگر این ID مانند مثال جاری از پیش مشخص بود، نیازی نیست تا ابتدا از بانک اطلاعاتی کوئری گرفت و کل شیء را در حافظه وارد کرد. در این حالت خاص می‌توان با استفاده از روش زیر، این ID را وارد سیستم tracking کرد و سپس حالت آن‌را به Deleted تغییر داد و در آخر آن‌را ذخیره کرد:
var entry = context.Entry(new Member { MemId = 37 });
entry.State = EntityState.Deleted;
context.SaveChanges();
در کدهای فوق می‌توان سطر entry.State = EntityState.Deleted را با context.Remove(entry) نیز جایگزین کرد و هر دو به یک معنا هستند.
روش فوق چنین کوئری‌هایی را ایجاد می‌کند:
SET NOCOUNT ON;
DELETE FROM [Members]
WHERE [MemId] = @p0;
SELECT @@ROWCOUNT;


مثال 9: حذف بر اساس یک sub-query

می‌خواهیم تمام کاربرانی را که هیچگاه رزروی را انجام نداده‌اند، حذف کنیم.
این مورد نیز با SQL نویسی مستقیم نیز توسط یک کوئری دسته‌ای قابل انجام است:
delete from members where memid not in (select memid from cd.bookings);
اما همانطور که عنوان شد، EF-Core این نوع اعمال ویرایش دسته‌ای را در طی یک تک کوئری پشتیبانی نمی‌کند. به همین جهت ابتدا آن‌ها را توسط یک کوئری به context وارد کرده و سپس حذف می‌کنیم:
var mems = context.Members.Where(x =>
  !context.Bookings.Select(x => x.MemId).Contains(x.MemId)).ToList();
context.Members.RemoveRange(mems);

context.SaveChanges();


کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
نظرات مطالب
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
یک نکته‌ی تکمیلی: همیشه بهتر است در سمت کلاینت هم تاریخ انقضای JWT پیش از استفاده بررسی شود
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;

namespace BlazorWasm.Client.Utils
{
    public class JwtInfo
    {
        public IEnumerable<Claim> Claims { set; get; }

        public DateTime? ExpirationDateUtc { set; get; }

        public bool IsExpired { set; get; }

        public IEnumerable<string> Roles { set; get; }
    }

    /// <summary>
    /// From the Steve Sanderson’s Mission Control project:
    /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
    /// </summary>
    public static class JwtParser
    {
        public static JwtInfo ParseClaimsFromJwt(string jwt)
        {
            var claims = new List<Claim>();
            var payload = jwt.Split('.')[1];

            var jsonBytes = getBase64WithoutPadding(payload);

            foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes))
            {
                if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
                {
                    foreach (var itemValue in element.EnumerateArray())
                    {
                        claims.Add(new Claim(keyValue.Key, itemValue.ToString()));
                    }
                }
                else
                {
                    claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString()));
                }
            }

            var roles = getRoles(claims);
            var expirationDateUtc = getDateUtc(claims, "exp");
            var isExpired = getIsExpired(expirationDateUtc);
            return new JwtInfo
            {
                Claims = claims,
                Roles = roles,
                ExpirationDateUtc = expirationDateUtc,
                IsExpired = isExpired
            };
        }

        private static IList<string> getRoles(IList<Claim> claims) =>
            claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();

        private static byte[] getBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }

        private static bool getIsExpired(DateTime? expirationDateUtc) =>
            !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow);

        private static DateTime? getDateUtc(IList<Claim> claims, string type)
        {
            var exp = claims.SingleOrDefault(claim => claim.Type == type);
            if (exp == null)
            {
                return null;
            }

            var expValue = getTimeValue(exp.Value);
            if (expValue == null)
            {
                return null;
            }

            var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            return dateTimeEpoch.AddSeconds(expValue.Value);
        }

        private static long? getTimeValue(string claimValue)
        {
            if (long.TryParse(claimValue, out long resultLong))
                return resultLong;

            if (float.TryParse(claimValue, out float resultFloat))
                return (long)resultFloat;

            if (double.TryParse(claimValue, out double resultDouble))
                return (long)resultDouble;

            return null;
        }
    }
}
مطالب
صفحه بندی اطلاعات در ASP.NET MVC به روش HashChange
یکی از مواردی که درپروژه‌‌ها زیاد مورد استفاده قرار میگیرد، نمایش داده‌های ذخیره شده‌ی در بانک اطلاعاتی، به صورت صفحه بندی شده به کاربر می‌باشد. قبلا در زمینه بحث Paging، مطلبی تهیه شده بود و در این مقاله قصد داریم کتابخانه‌ای را مورد بررسی قرار دهیم که علاوه بر ارسال داده به صورت Ajax ایی، بتواند همچنین پارامترهای مورد نظر را به صورت Query String نیز در آدرس بار نمایش دهد.
اگر به جستجوی گوگل دقت کرده باشید، به صورت Ajax ایی پیاده سازی شده‌است، با این تفاوت که بعد از هر تغییر درجستجوی مورد نظر، Url صفحه نیز تغییر میکند (برای مثال بعد از جستجوی عبارت dotNetTips  آدرس بار صفحه به شکل https://www.google.com/#q=dotNetTips&* تغییر می‌کند). برای پیاده سازی این ویژگی باید از تکنیکی به نام HashChange استفاده کرد. در نتیجه با این روش مشکل ارسال صفحه‌ای خاص در یک گرید برای دیگران، به صورت Ajax ایی و بدون مشکل انجام می‌شود. از این رو با توجه به داشتن Url‌های منحصر به فرد برای هر صفحه، تا حدی مشکل سئو سایت را نیز برطرف می‌کنیم.

برای استفاده از این ویژگی در ادامه قصد داریم پیاده سازی کتابخانه‌ی MvcAjaxPager را مورد بررسی قرار دهیم. ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به نصب کتابخانه آن می‌نماییم:
 Install-Package MvcAjaxPager

در ادامه نحوه پیاده سازی آن را به همراه مثالی، مورد بررسی قرار می‌دهیم:

ابتدا یک مدل فرضی را همانند زیر تهیه می‌کنیم :
public class Topic
{
   public int Id;
   public string Title;
   public string Text;
}
و کلاسی را همانند زیر برای دریافت یک لیست از مطالب می‌نویسیم:
public class TopicService
{
    public static IEnumerable<Topic> Topics = new List<Topic>() {
       new Topic{Id=1,Title="Title 1",Text= "Text 1"},
       new Topic{Id=2,Title="Title 2",Text="Text 2"},
       new Topic{Id=3,Title="Title 3",Text="Text 3"},
       new Topic{Id=4,Title="Title 4",Text="Text 4"},
       new Topic{Id=5,Title="Title 5",Text="Text 5"},
       new Topic{Id=6,Title="Title 6",Text="Text 6"},
       new Topic{Id=7,Title="Title 7",Text="Text 7"},
       new Topic{Id=8,Title="Title 8",Text="Text 8"},
       new Topic{Id=9,Title="Title 9",Text="Text 9"},
       new Topic{Id=10,Title="Title 10",Text="Text 10"},
       new Topic{Id=11,Title="Title 11",Text="Text 11"},
       new Topic{Id=12,Title="Title 12",Text="Text 12"},
       new Topic{Id=13,Title="Title 13",Text="Text 13"},
       new Topic{Id=14,Title="Title 14",Text="Text 14"},
       new Topic{Id=15,Title="Title 15",Text="Text 15"},
       new Topic{Id=16,Title="Title 16",Text="Text 16"},
       new Topic{Id=17,Title="Title 17",Text="Text 17"},
       new Topic{Id=18,Title="Title 18",Text="Text 18"},
       new Topic{Id=19,Title="Title 19",Text="Text 19"},
       new Topic{Id=20,Title="Title 20",Text="Text 20"},
       new Topic{Id=21,Title="Title 21",Text="Text 21"},
       new Topic{Id=22,Title="Title 22",Text="Text 22"},
      };

    public static IEnumerable<Topic> GetAll()
    {
       return Topics.OrderBy(row => row.Id);
    }
}
همچنین کلاس زیر را اضافه میکنیم:
public class ListViewModel
{
   public IEnumerable<Topic> Topics { get; set; }
   public int PageIndex { get; set; }
   public int TotalItemCount { get; set; }
}
ابتدا یک کنترلر را ایجاد می‌کنیم به همراه اکشن متدی که قصد داریم لیستی از اطلاعات را به کاربر نمایش دهیم:
public ActionResult Index(int page = 1)
{
       var topics = TopicService.GetAll ();
       int totalItemCount = topics.Count();
       var model = new ListViewModel()
       {
              PageIndex = page,
              Topics = topics.OrderBy(p => p.Id).Skip((page - 1) * 10).Take(10).ToList(),
              TotalItemCount = totalItemCount
       };

       if (!Request.IsAjaxRequest())
       {
              return View(model);
       }

       return PartialView("_TopicList", model);
}
در اینجا بعد از واکشی اطلاعات، تعداد 10 رکورد را در هر صفحه نمایش می‌دهیم. 

و در Partial view مربوطه نیز داریم :
@using MvcAjaxPager
@model ListViewModel

@Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions
   {
       ShowDisabledPagerItems = true,
       AlwaysShowFirstLastPageNumber = true,
       HorizontalAlign = "center",
       ShowFirstLast = false,
       CssClass = "NavigationBox",
       AjaxUpdateTargetId = "dvTopics",
       AjaxOnBegin = "AjaxStart",
       AjaxOnComplete = "AjaxStop"
   }, null, null)

<table>
    <tr>
        <th>
            @Html.DisplayName("ID")
        </th>
        <th>
            @Html.DisplayName("Title")
        </th>
        <th>
            @Html.DisplayName("Text")
        </th>
    </tr>

    @foreach (var topic in Model.Topics)
    {
        <tr>
            <td>
                @topic.Id
        </td>
        <td>
            @topic.Title
        </td>
        <td>
            @topic.Text
        </td>
    </tr>
    }
</table>

@Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions
   {
       ShowDisabledPagerItems = true,
       AlwaysShowFirstLastPageNumber = true,
       HorizontalAlign = "center",
       ShowFirstLast = true,
       FirstPageText = "اولین",
       LastPageText = "آخرین",
       MorePageText = "...",
       PrevPageText = "قبلی",
       NextPageText = "بعدی",
       CssClass = "NavigationBox",
       AjaxUpdateTargetId = "dvTopics",
       AjaxOnBegin = "AjaxStart",
       AjaxOnComplete = "AjaxStop"
   }, null, null)

 حال برای استفاده از pager مورد نظر فقط کافیست متد AjaxPager آن را فراخوانی کنیم. این متد شامل 11  OverLoad مختلف هست.
در این قسمت TotalItemCount جمع کل رکورد‌ها، PageSize تعداد رکورد‌های هر صفحه و PageIndex آدرس صفحه جاری می‌باشد.

مهمترین بخش این pager  که قابلیت‌های زیادی را به کاربر می‌دهد، قسمت PagerOptions آن است و تعدادی از پارامتر‌های آن شامل AjaxOnBeginAjaxOnCompelte، AjaxOnSuccess ،  AjaxOnFailure میتوان تعیین کرد تا بعد از شروع، وقوع خطا، موفقیت و یا خاتمه عملیات جاوا اسکریپتی، اجرا شود. 

AlwaysShowFirstLastPageNumber جهت نمایش صفحه اول و آخر
FirstPageText جهت تعیین متن اولین صفحه
LastPageText جهت تعیین متن آخرین صفحه
CssClass ، Id  جهت تعیین Id خاص

و در انتها، در view مربوطه داریم:
@using MvcAjaxPager
@model ListViewModel
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div id="dvTopics">
        @{
            @Html.Partial("_TopicList", Model);
        }
    </div>

    <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/Scripts/path.min.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/Scripts/jquery.pager-1.0.1.min.js")"></script>
    <script type="text/javascript">
        $('.NavigationBox').pager();

        //pagination before start
        function AjaxStart() {
            console.log('Start AJAX call. Loading message can be shown');
        }
        // pagination - after request
        function AjaxStop() {
            console.log('Stop AJAX call. Loading message can be hidden');
        };
    </script>
</body>
</html>
در انتهای صفحه مورد نظر می‌بایست دو فایل جاوااسکریپتی jquerypager و Path را که هنگام نصب Pager، به برنامه اضافه شده اند، فراخوانی کنیم و با استفاده از CssClass  یا Id که قبلا در بخش PagerOption تعیین کردیم، آن را انتخاب و متدpager را فراخوانی کنیم.
مطالب
نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular
در مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها» مشاهده کردیم که Angular در روش فرم‌های مبتنی بر قالب‌ها، تنها از 4 روش بومی اعتبارسنجی مرورگرها مانند ذکر ویژگی required برای فیلدهای اجباری، ویژگی‌های minlength و maxlength برای تعیین حداقل و حداکثر تعداد حروف مجاز قابل ورود در یک فیلد و از pattern برای کار با عبارات با قاعده پشتیبانی می‌کند. برای بهبود این وضعیت در این مطلب قصد داریم روش تهیه اعتبارسنج‌های سفارشی مخصوص حالت فرم‌های مبتنی بر قالب‌ها را بررسی کنیم.


تدارک مقدمات مثال این قسمت

این مثال، در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه می‌کنیم:
 >ng g m CustomValidators -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و CustomValidatorsModule را بجای CustomValidatorsRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت نام یک کاربر را اضافه خواهیم کرد:
 >ng g c CustomValidators/user-register
که اینکار سبب به روز رسانی فایل custom-validators.module.ts و افزوده شدن UserRegisterComponent به قسمت declarations آن می‌شود.
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف می‌کنیم:
 >ng g cl CustomValidators/user
با این محتوا:
export class User {
  constructor(
    public username: string = "",
    public email: string = "", 
    public password: string = "", 
    public confirmPassword: string = "" 
  ) {}
}
در طراحی فرم HTML ایی آن نیاز است این موارد رعایت شوند:
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمه‌ی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمه‌ی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.



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

هرچند می‌توان اعتبارسنجی ایمیل‌ها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوه‌ی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه می‌دهیم:
 >ng g d CustomValidators/EmailValidator -m custom-validators.module
این دستور علاوه بر ایجاد فایل جدید email-validator.directive.ts و تکمیل ساختار ابتدایی آن، کار به روز رسانی custom-validators.module.ts را نیز انجام می‌دهد. در این حالت به صورت خودکار قسمت declarations این ماژول با EmailValidatorDirective مقدار دهی می‌شود.
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده می‌کنید:
import { Directive } from "@angular/core";
import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms";

@Directive({
  selector:
    "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EmailValidatorDirective,
      multi: true
    }
  ]
})
export class EmailValidatorDirective implements Validator {
  validate(element: AbstractControl): { [key: string]: any } {
    const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
    const valid = emailRegex.test(element.value);
    return valid ? null : { appEmailValidator: true };
  }
}
توضیحات تکمیلی:
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آن‌را همانند ویژگی‌های HTML، به فیلد ورودی اضافه کرد:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" 
name="email" [(ngModel)]="model.email">

- روش تعریف selector آن اندکی متفاوت است:
selector:
"[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
در اینجا مطابق https://angular.io/guide/styleguide#style-02-08 توصیه شده‌است که:
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شده‌است:
"apps": [
{
   // ...
   "prefix": "app",
این مساله در جهت مشخص کردن سفارشی بودن این دایرکتیو و همچنین کاهش احتمال تکرار نام‌ها توصیه شده‌است.
ب) در اینجا formControlName، formControl و ngModel قید شده‌ی در کنار نام selector این دایرکتیو را نیز مشاهده می‌کنید. وجود آن‌ها به این معنا است که کلاس این دایرکتیو، به المان‌هایی که به آن‌ها ویژگی appEmailValidator اضافه شده‌است و همچنین آن المان‌ها از یکی از سه نوع ذکر شده هستند، اعمال می‌شود و در سایر موارد بی‌اثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا می‌توان نوشت:
 selector: "[appEmailValidator]"

- پس از آن قسمت providers را مشاهده می‌کنید:
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EmailValidatorDirective,
      multi: true
    }
کار قسمت multi آن این است که EmailValidatorDirective (یا همان کلاس جاری) را به لیست NG_VALIDATORS توکار (اعتبارسنج‌های توکار مبتنی بر قالب‌ها) اضافه می‌کند و سبب بازنویسی هیچ موردی نخواهد شد. بنابراین وجود این قسمت در جهت تکمیل تامین کننده‌های توکار Angular ضروری است.

- سپس پیاده سازی اینترفیس توکار Validator را مشاهده می‌کنید:
 export class EmailValidatorDirective implements Validator {
این اینترفیس جزو مجموعه‌ی فرم‌های مبتنی بر قالب‌ها است و از آن جهت نوشتن اعتبارسنج‌های سفارشی می‌توان استفاده کرد.
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
 validate(element: AbstractControl): { [key: string]: any }
کار این متد این است که المانی را که appEmailValidator به آن اعمال شده‌است، به عنوان پارامتر متد validate در اختیار کلاس جاری قرار می‌دهد. به این ترتیب می‌توان برای مثال به مقدار آن دسترسی یافت و سپس منطق سفارشی را پیاده سازی و یک خروجی key/value را بازگشت داد.
validate(element: AbstractControl): { [key: string]: any } {
  const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
  const valid = emailRegex.test(element.value);
  return valid ? null : { appEmailValidator: true };
}
برای مثال در اینجا مقدار فیلد ایمیل element.value توسط عبارت باقاعده‌ی نوشته شده بررسی می‌شود. اگر با این الگو انطباق داشته باشد، نال بازگشت داده می‌شود (اعلام عدم وجود مشکلی در اعتبارسنجی) و اگر خیر، یک شیء key/value دلخواه را می‌توان بازگشت داد.

- اکنون که این دایرکتیو جدید طراحی و ثبت شده‌است (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آن‌را به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آن‌را به صورت خودکار مدیریت کند:
<input #email="ngModel" required appEmailValidator type="text" class="form-control"
name="email" [(ngModel)]="model.email">


نحوه‌ی طراحی خروجی متد validate

هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشده‌است و همینقدر که این خروجی یک شیء key/value باشد، کفایت می‌کند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شی‌ءایی را بازگشت می‌دهد:
 { required:true }
و یا اگر اعتبارسنج استاندارد minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه می‌دهد:
{ minlength : {
     requiredLength : 3,
     actualLength : 1
   }
}
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد؛ برای نمونه در اینجا appEmailValidator (یک کلید و نام دلخواه است و هیچ الزامی ندارد که با نام selector این دایرکتیو یکی باشد)، به true تنظیم شده‌است:
 { appEmailValidator: true }
بنابراین شرط تامین نوع خروجی، برقرار است. علت true بودن آن نیز مورد ذیل است:
<div class="alert alert-danger"  *ngIf="email.errors.appEmailValidator">
The entered email is not valid.
</div>
در اینجا اگر false را بازگشت دهیم، هرچند email.errors دارای کلید جدید appEmailValidator شده‌است، اما ngIf سبب رندر خطای اعتبارسنجی «ایمیل وارد شده معتبر نیست.» به علت false بودن نتیجه‌ی نهایی، نمی‌شود. یا حتی می‌توان بجای true یک رشته و یا یک شیء با توضیحات بیشتری را نیز تنظیم کرد؛ چون value این key/value به any تنظیم شده‌است و هر چیزی را می‌پذیرد.
از دیدگاه اعتبارسنج فرم‌های مبتنی بر قالب‌ها، همینقدر که آرایه‌ی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیده‌است و اعتبارسنجی المان را شکست خورده ارزیابی می‌کند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه می‌دهد (بر اساس نیازهای نمایشی برنامه).


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

در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمه‌ی عبور و تائید کلمه‌ی عبور را مشاهده می‌کنید:
public password: string = "",
public confirmPassword: string = ""
Angular به همراه اعتبارسنج توکاری برای بررسی یکی بودن این دو نیست. به همین جهت نمونه‌ی سفارشی آن‌را همانند EmailValidatorDirective فوق تهیه می‌کنیم. ابتدا یک دایرکتیو جدید را به نام EqualValidator به ماژول custom-validators اضافه می‌کنیم:
 >ng g d CustomValidators/EqualValidator -m custom-validators.module
که سبب ایجاد فایل جدید equal-validator.directive.ts و به روز رسانی قسمت declarations فایل custom-validators.module.ts با EqualValidatorDirective نیز می‌شود.

در ادامه کدهای کامل آن‌را در ذیل مشاهده می‌کنید:
import { Directive, Attribute } from "@angular/core";
import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms";

@Directive({
  selector:
    "[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: EqualValidatorDirective,
      multi: true
    }
  ]
})
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}

  validate(element: AbstractControl): { [key: string]: any } {
    const selfValue = element.value;
    const otherControl = element.root.get(this.compareToControl);

    console.log("EqualValidatorDirective", {
       thisControlValue: selfValue,
       otherControlValue: otherControl ? otherControl.value : null
    });

    if (otherControl && selfValue !== otherControl.value) {
      return {
        appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
      };
    }

    if (
      otherControl &&
      otherControl.errors &&
      selfValue === otherControl.value
    ) {
      delete otherControl.errors["appValidateEqual"];
      if (!Object.keys(otherControl.errors).length) {
        otherControl.setErrors(null);
      }
    }

    return null;
  }
}
توضیحات تکمیلی:
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شده‌است تا بتوان selector آن‌را به المان‌های HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شده‌است تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شده‌است؛ اما چون در اینجا می‌خواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کننده‌ی مقدار آن‌را نیز افزوده‌ایم:
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}
به این ترتیب زمانیکه قرار است فیلد کلمه‌ی عبور را تعریف کنیم، ابتدا ویژگی appValidateEqual یا همان selector این اعتبارسنج به آن اضافه شده‌است تا کار فعال سازی ابتدایی صورت گیرد:
<input #password="ngModel" required type="password" class="form-control"
appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
سپس Attribute یا ویژگی به نام compare-to نیز تعریف شده‌است. این compare-to همان نامی است که به Attribute@ نسبت داده شده‌است. سپس مقداری که به این ویژگی نسبت داده می‌شود، توسط خاصیت compareToControl دریافت خواهد شد.
در اینجا محدودیتی هم از لحاظ تعداد ویژگی‌ها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند می‌توان ویژگی‌های بیشتری را به سازنده‌ی آن نسبت داد.

یک نکته: می‌توان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج می‌گردد.

- در ابتدای این اعتبارسنج، نحوه‌ی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده می‌کنید:
export class EqualValidatorDirective implements Validator {
  constructor(@Attribute("compare-to") public compareToControl: string) {}

  validate(element: AbstractControl): { [key: string]: any } {
    const selfValue = element.value;
    const otherControl = element.root.get(this.compareToControl);

    console.log("EqualValidatorDirective", {
       thisControlValue: selfValue,
       otherControlValue: otherControl ? otherControl.value : null
    });
در اینجا element.value مقدار المان یا کنترل HTML جاری است که appValidateEqual به آن اعمال شده‌است.
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت می‌شود، می‌توان به کنترل دوم، توسط element.root.get دسترسی یافت.

- در ادامه‌ی کار، مقایسه‌ی ساده‌ای را مشاهده می‌کنید:
    if (otherControl && selfValue !== otherControl.value) {
      return {
        appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject.
      };
    }
اگر کنترل دوم یافت شد و همچنین مقدار آن با مقدار کنترل جاری یکی نبود، همان شیء key/value مورد انتظار متد validate، در جهت اعلام شکست اعتبارسنجی بازگشت داده می‌شود.

- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفته‌است:
    if (otherControl && otherControl.errors && selfValue === otherControl.value) {
      delete otherControl.errors["appValidateEqual"];
      if (!Object.keys(otherControl.errors).length) {
        otherControl.setErrors(null);
      }
    }

    return null;
اعتبارسنج تعریف شده، فقط به کنترلی که هم اکنون در حال کار با آن هستیم اعمال می‌شود. اگر پیشتر کلمه‌ی عبوری را وارد کرده باشیم و سپس به فیلد تائید آن مراجعه کنیم، وضعیت اعتبارسنجی فیلد کلمه‌ی عبور قبلی به حالت غیرمعتبر تنظیم شده‌است. اما پس از تکمیل فیلد تائید کلمه‌ی عبور، هرچند وضعیت فیلد جاری معتبر است، اما هنوز وضعیت فیلد قبلی غیرمعتبر می‌باشد. برای رفع این مشکل، ابتدا کلید دلخواه appValidateEqual را از آن حذف می‌کنیم (همان کلیدی است که پیشتر در صورت مساوی نبودن مقدار فیلدها بازگشت داده شده‌است). حذف این کلید سبب نال شدن آرایه‌ی errors یک شیء نمی‌شود و همانطور که پیشتر عنوان شد، Angular تنها به همین مورد توجه می‌کند. بنابراین در ادامه کار، setErrors یا تنظیم آرایه‌ی errors به نال هم انجام شده‌است. در اینجا است که Angular فیلد دوم را نیز معتبر ارزیابی خواهد کرد.


تکمیل کامپوننت فرم ثبت نام کاربران

اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا می‌کند:
import { NgForm } from "@angular/forms";
import { User } from "./../user";
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-user-register",
  templateUrl: "./user-register.component.html",
  styleUrls: ["./user-register.component.css"]
})
export class UserRegisterComponent implements OnInit {
  model = new User();

  constructor() {}

  ngOnInit() {}

  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);
  }
}
در اینجا تنها کار مهمی که انجام شده‌است، ارائه‌ی خاصیت عمومی مدل، جهت استفاده‌ی از آن در قالب HTML ایی این کامپوننت است. بنابراین به فایل user-register.component.html مراجعه کرده و آن‌را نیز به صورت ذیل تکمیل می‌کنیم:

ابتدای فرم
<div class="container">
  <h3>Registration Form</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
در اینجا novalidate اضافه شده‌است تا اعتبارسنجی توکار مرورگرها با اعتبارسنجی سفارشی فرم جاری تداخل پیدا نکند. همچنین توسط یک template reference variable به وهله‌ای از فرم دسترسی یافته و آن‌را به متد submitForm کامپوننت ارسال کرده‌ایم.

تکمیل قسمت ورود نام کاربری

    <div class="form-group" [class.has-error]="username.invalid && username.touched">
      <label class="control-label">User Name</label>
      <input #username="ngModel" required maxlength="8" minlength="4" type="text"
        class="form-control" name="username" [(ngModel)]="model.username">
      <div *ngIf="username.invalid && username.touched">
        <div class="alert alert-info">
          errors: {{ username.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.required">
          username is required.
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.minlength">
          username should be minimum {{username.errors.minlength.requiredLength}} characters.
        </div>
        <div class="alert alert-danger"  *ngIf="username.errors.maxlength">
          username should be max {{username.errors.maxlength.requiredLength}} characters.
        </div>
      </div>
    </div>
اعتبارسنجی فیلد نام کاربری شامل سه قسمت بررسی errors.required، errors.minlength و errors.maxlength است.


تکمیل قسمت ورود ایمیل

    <div class="form-group" [class.has-error]="email.invalid && email.touched">
      <label class="control-label">Email</label>
      <input #email="ngModel" required appEmailValidator type="text" class="form-control"
        name="email" [(ngModel)]="model.email">
      <div *ngIf="email.invalid && email.touched">
        <div class="alert alert-info">
          errors: {{ email.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="email.errors.required">
          email is required.
        </div>
        <div class="alert alert-danger"  *ngIf="email.errors.appEmailValidator">
          The entered email is not valid.
        </div>
      </div>
    </div>
در اینجا نحوه‌ی استفاده‌ی از دایرکتیو جدید appEmailValidator را ملاحظه می‌کنید. این دایرکتیو ابتدا به المان فوق متصل و سپس نتیجه‌ی آن در قسمت ngIf، برای نمایش خطای متناظری بررسی شده‌است.


تکمیل قسمت‌های ورود کلمه‌ی عبور و تائید آن

    <div class="form-group" [class.has-error]="password.invalid && password.touched">
      <label class="control-label">Password</label>
      <input #password="ngModel" required type="password" class="form-control"
        appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
      <div *ngIf="password.invalid && password.touched">
        <div class="alert alert-info">
          errors: {{ password.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="password.errors.required">
          password is required.
        </div>
        <div class="alert alert-danger"  *ngIf="password.errors.appValidateEqual">
          Password mismatch. Please complete the confirmPassword .
        </div>
      </div>
    </div>

    <div class="form-group" [class.has-error]="confirmPassword.invalid && confirmPassword.touched">
      <label class="control-label">Retype password</label>
      <input #confirmPassword="ngModel" required type="password" class="form-control"
        appValidateEqual compare-to="password" name="confirmPassword" [(ngModel)]="model.confirmPassword">
      <div *ngIf="confirmPassword.invalid && confirmPassword.touched">
        <div class="alert alert-info">
          errors: {{ confirmPassword.errors | json }}
        </div>
        <div class="alert alert-danger"  *ngIf="confirmPassword.errors.required">
          confirmPassword is required.
        </div>
        <div class="alert alert-danger"  *ngIf="confirmPassword.errors.appValidateEqual">
          Password mismatch.
        </div>
      </div>
    </div>
در اینجا نحوه‌ی اعمال دایرکتیو جدید appValidateEqual و همچنین ویژگی compare-to آن‌را به فیلدهای کلمه‌ی عبور و تائید آن مشاهده می‌کنید.
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شده‌است و سبب نمایش خطای اعتبارسنجی متناسبی می‌شود.


تکمیل انتهای فرم

    <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button>
  </form>
</div>
در اینجا بررسی می‌شود که آیا فرم معتبر است یا خیر. اگر خیر، دکمه‌ی submit آن غیرفعال می‌شود و برعکس.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
استفاده از Web API در ASP.NET Web Forms
گرچه ASP.NET Web API بهمراه ASP.NET MVC بسته بندی شده و استفاده می‌شود، اما اضافه کردن آن به اپلیکیشن‌های ASP.NET Web Forms کار ساده ای است. در این مقاله مراحل لازم را بررسی می‌کنیم.

برای استفاده از Web API در یک اپلیکیشن ASP.NET Web Forms دو قدم اصلی باید برداشته شود:

  • اضافه کردن یک کنترلر Web API که از کلاس ApiController مشتق می‌شود.
  • اضافه کردن مسیرهای جدید به متد Application_Start.


یک پروژه Web Forms بسازید

ویژوال استودیو را اجرا کنید و پروژه جدیدی از نوع ASP.NET Web Forms Application ایجاد کنید.


کنترلر و مدل اپلیکیشن را ایجاد کنید

کلاس جدیدی با نام Product بسازید و خواص زیر را به آن اضافه کنید.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}
همانطور که مشاهده می‌کنید مدل مثال جاری نمایانگر یک محصول است. حال یک کنترلر Web API به پروژه اضافه کنید. کنترلر‌های Web API درخواست‌های HTTP را به اکشن متدها نگاشت می‌کنند. در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Add, New Item را انتخاب کنید.

در دیالوگ باز شده گزینه Web را از پانل سمت چپ کلیک کنید و نوع آیتم جدید را Web API Controller Class انتخاب نمایید. نام این کنترلر را به "ProductsController" تغییر دهید و OK کنید.

کنترلر ایجاد شده شامل یک سری متد است که بصورت خودکار برای شما اضافه شده اند، آنها را حذف کنید و کد زیر را به کنترلر خود اضافه کنید.

namespace WebForms
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;

    public class ProductsController : ApiController
    {

        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public Product GetProductById(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return product;
        }

        public IEnumerable<Product> GetProductsByCategory(string category)
        {
            return products.Where(
                (p) => string.Equals(p.Category, category,
                    StringComparison.OrdinalIgnoreCase));
        }
    }
}
کنترلر جاری لیستی از محصولات را بصورت استاتیک در حافظه محلی نگهداری می‌کند. متدهایی هم برای دریافت لیست محصولات تعریف شده اند.


اطلاعات مسیریابی را اضافه کنید

مرحله بعدی اضافه کردن اطلاعات مسیریابی (routing) است. در مثال جاری می‌خواهیم آدرس هایی مانند "api/products/" به کنترلر Web API نگاشت شوند. فایل Global.asax را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.

using System.Web.Http;
حال کد زیر را به متد Application_Start اضافه کنید.
RouteTable.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = System.Web.Http.RouteParameter.Optional }
    );

برای اطلاعات بیشتر درباره مسیریابی در Web API به این لینک مراجعه کنید.


دریافت اطلاعات بصورت آژاکسی در کلاینت

تا اینجا شما یک API دارید که کلاینت‌ها می‌توانند به آن دسترسی داشته باشند. حال یک صفحهHTML خواهیم ساخت که با استفاده از jQuery سرویس را فراخوانی می‌کند. صفحه Default.aspx را باز کنید و کدی که بصورت خودکار در قسمت Content تولید شده است را حذف کرده و کد زیر را به این قسمت اضافه کنید:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" 
    AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebForms._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>Products</h2>
    <table>
    <thead>
        <tr><th>Name</th><th>Price</th></tr>
    </thead>
    <tbody id="products">
    </tbody>
    </table>
</asp:Content>
حال در قسمت HeaderContent کتابخانه jQuery را ارجاع دهید.
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
</asp:Content>

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

نکته: برای ارجاع دادن اسکریپت‌ها می‌توانید بسادگی فایل مورد نظر را با drag & drop به کد خود اضافه کنید.

زیر تگ jQuery اسکریپت زیر را اضافه کنید.

<script type="text/javascript">
    function getProducts() {
        $.getJSON("api/products",
            function (data) {
                $('#products').empty(); // Clear the table body.

                // Loop through the list of products.
                $.each(data, function (key, val) {
                    // Add a table row for the product.
                    var row = '<td>' + val.Name + '</td><td>' + val.Price + '</td>';
                    $('<tr/>', { text: row })  // Append the name.
                        .appendTo($('#products'));
                });
            });
        }

        $(document).ready(getProducts);
</script>

هنگامی که سند جاری (document) بارگذاری شد این اسکریپت یک درخواست آژاکسی به آدرس "api/products/" ارسال می‌کند. سرویس ما لیستی از محصولات را با فرمت JSON بر می‌گرداند، سپس این اسکریپت لیست دریافت شده را به جدول HTML اضافه می‌کند.

اگر اپلیکیشن را اجرا کنید باید با نمایی مانند تصویر زیر مواجه شوید: