مطالب
فعال‌سازی HSTS در ASP.NET Core
برای فعال‌سازی SSL در ASP.NET Core می‌توانیم از ویژگی RequireHttps برای کنترلرها و همچنین اکشن‌متدها استفاده کنیم:
[RequireHttps]
public class AccountController : Controller
{
    public IActionResult Login()
    {
        return Content("Login Page");
    }
}
اکنون اگر پروژه را اجرا کنید، خروجی به صورت زیر خواهد بود و اگر به آدرس Account/Login مراجعه کنید، چیزی در خروجی نمایش داده نخواهد شد:

علت آن نیز این است که اگر درخواست HTTPS نباشد، یک Permanent Redirect به همان URL خواهیم داشت؛ زیرا بر روی پورتی که پروژه توسط آن اجرا شده‌است، هیچ certificateی نصب نشده‌است. در ادامه می‌خواهیم یک self-signed certificate تستی را برای اجرای پروژه ایجاد کنیم. 
توسط دستورات زیر می‌توانید یک certificate را برای localhost ایجاد کنید:
$ openssl genrsa -out key.pem 2048
$ openssl req -new -sha256 -key key.pem -out csr.csr
$ openssl req -x509 -sha256 -days 365 -key key.pem -in csr.csr -out certificate.pem
openssl pkcs12 -export -out localhost.pfx -inkey key.pem -in certificate.pem
با اجرای دستورات فوق، باید یک فایل جدید با نام localhost.pfx و تعدادی فایل دیگر، درون پروژه ایجاد شده باشند:

اکنون باید Kestrel را از وجود این certificate مطلع کنیم. برای انجام اینکار ابتدا باید پکیج زیر را نصب کنیم:
$ dotnet add package Microsoft.AspNetCore.Server.Kestrel.Https
سپس فایل Program.cs را به صورت زیر تغییر دهیم:
namespace testingSSL
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseKestrel(options =>
                {
                    options.Listen(IPAddress.Any, 8080);
                    options.Listen(IPAddress.Any, 443,
                        listenOptions => listenOptions.UseHttps("localhost.pfx", "qwe123456"));
                })
                .UseStartup<Startup>()
                .Build();
    }
}
در این حالت با مراجعه به کنترلری که با ویژگی RequireHttps مزین شده‌است، به صورت خودکار، درخواست به نسخه HTTPS هدایت خواهد شد:

البته تا اینجا، هدف بررسی ویژگی RequireHttps بود؛ طبیعتاً به SSL در حین Development نیازی نخواهید داشت. در نتیجه می‌توانیم به صورت Global تمامی کنترلرها را در زمان Production به ویژگی گفته شده مزین کنیم:

private readonly IHostingEnvironment _env;
public Startup(IConfiguration configuration,
IHostingEnvironment env)
{
    Configuration = configuration;
    _env = env;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    if (!_env.IsDevelopment())
        services.Configure<MvcOptions>(o => o.Filters.Add(new RequireHttpsAttribute()));
}


(Http Strict Transport Security (HSTS

هدایت کردن خودکار درخواست از حالت HTTP به HTTPS، توسط خیلی از سایت‌ها انجام می‌شود:

البته این روش بهتر از استفاده نکردن از SSL است؛ اما در نظر داشته باشید که همیشه اولین درخواست به صورت رمزنگاری نشده ارسال خواهد شد. فرض کنید در یک محیط پابلیک از طریق WiFi به اینترنت متصل شده‌ایم. شخصی (هکر) که بر روی مودم کنترل دارد، طوری WiFi را پیکربندی کرده‌است که به جای آدرس اصلی که در تصویر مشاهده می‌کنید، یک نسخه جعلی از سایت باز شود؛ به طوریکه URL همانند URL اصلی باشد. در این‌حالت کاربر به جای اینکه نام‌کاربری و کلمه‌عبور را وارد سایت اصلی کند، آن را درون سایت جعلی وارد خواهد کرد. برای حل این مشکل می‌توانیم وب‌سایت‌مان را طوری تنظیم کنیم که هدر Strict-Transport-Security را به هدر اولین responseی که توسط مرورگر دریافت می‌شود اضافه کند:

Strict-Transport-Security: max-age=31536000

بنابراین مرورگر وب‌سایت را درون یک لیست internal به مدت یکسال (مقدار max-age) نگهداری خواهد کرد؛ در طول این زمان به هیچ درخواست ناامنی اجازه داده نخواهد شد. به این قابلیت HSTS گفته می‌شود. البته ASP.NET Core به صورت توکار روشی را جهت اضافه کردن این هدر ارائه نداده است؛ اما می‌توانیم خودمان یک Middleware سفارشی را به pipeline اضافه کنیم تا اینکار را برایمان انجام دهد:

namespace testingSSL.Middleware
{
    public class HstsMiddleware
    {
        private readonly RequestDelegate _next;
        public HstsMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            if (!context.Request.IsHttps)
                return _next(context);

            if (IsLocalhost(context))
                return _next(context);

            context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000");

            return _next(context);
        }
        private bool IsLocalhost(HttpContext context)
        {
            return string.Equals(context.Request.Host.Host, "localhost", StringComparison.OrdinalIgnoreCase);
        }
    }
}


یا اینکه می‌توانیم از کتابخانه NWebSec استفاده کنیم:

$ dotnet add package NWebsec.AspNetCore.Middleware

برای استفاده از آن نیز خواهیم داشت:

app.UseHsts(h => h.MaxAge(days: 365));


اما هنوز یک مشکل وجود دارد؛ هنوز مشکل اولین درخواست را برطرف نکرده‌ایم. زیرا مرورگر برای دریافت این هدر نیاز به مراجعه به سایت دارد. برای حل این مشکل می‌توانید آدرس وب‌سایت خود را در سایت hstspreload ثبت کنید، سپس متد PreLoad را به کد فوق اضافه کنید:

app.UseHsts(h => h.MaxAge(days: 365).Preload());

در اینحالت حتی اگر کسی به وب‌سایت شما مراجعه نکند، مرورگر می‌داند که باید از HTTPS استفاده کند. زیرا این لیست به صورت توکار درون مرورگر تعبیه شده‌است. بنابراین در این‌حالت مطمئن خواهیم شد که تمامی connectionها به سایت‌مان امن می‌باشند.


دریافت کدهای مطلب جاری (+)

مطالب
دریافت و نمایش فایل‌های PDF در برنامه‌های Blazor WASM
زمانیکه قرار است با فایل‌های باینری واقع در سمت سرور کار کنیم، اگر اکشن متدهای ارائه دهنده‌ی آن‌ها محافظت شده نباشند، برای نمایش و یا دریافت آن‌ها تنها کافی است از آدرس مستقیم این منابع استفاده کرد و در این حالت نیازی به رعایت هیچ نکته‌ی خاصی نیست. اما اگر اکشن متدی در سمت سرور توسط فیلتر Authorize محافظت شده باشد و روش محافظت نیز مبتنی بر کوکی‌ها نباشد، یعنی این کوکی‌ها در طی درخواست‌های مختلف، به صورت خودکار توسط مرورگر به سمت سرور ارسال نشوند، آنگاه نیاز است با استفاده از HttpClient برنامه‌های Blazor WASM، درخواست دسترسی به منبعی را به همراه برای مثال JSON Web Tokens کاربر به سمت سرور ارسال کرد و سپس فایل باینری نهایی را به صورت آرایه‌ای از بایت‌ها دریافت نمود. در این حالت با توجه به ماهیت Ajax ای این این عملیات، برای نمایش و یا دریافت این فایل‌های محافظت شده در مرورگر، نیاز به دانستن نکات ویژه‌ای است که در این مطلب به آن‌ها خواهیم پرداخت.



کدهای سمت سرور دریافت فایل PDF

در اینجا کدهای سمت سرور برنامه، نکته‌ی خاصی را به همراه نداشته و صرفا یک فایل PDF ساده (محتوای باینری) را بازگشت می‌دهد:
using Microsoft.AspNetCore.Mvc;

namespace BlazorWasmShowBinaryFiles.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ReportsController : ControllerBase
    {
        [HttpGet("[action]")]
        public IActionResult GetPdfReport()
        {
            //TODO: create the `sample.pdf` report file on the server

            return File(virtualPath: "~/app_data/sample.pdf",
                        contentType: "application/pdf",
                        fileDownloadName: "sample.pdf");
        }
    }
}
که در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.

یک نکته: استفاده مستقیم از کتابخانه‌های تولید PDF در برنامه‌های سمت کاربر Blazor منطقی نیست؛ چون به ازای هر کاربر، گاهی از اوقات مجبور به ارسال بیش از 8 مگابایت اضافی مختص به فایل‌های dll. آن کتابخانه‌ی تولید PDF خواهیم شد. بنابراین بهتر است تولید PDF را در سمت سرور و در اکشن متدهای Web API انجام داد و سپس فایل نهایی تولیدی را در برنامه‌ی سمت کلاینت، دریافت و یا نمایش داد. به همین جهت در این مثال خروجی نهایی یک چنین عملیات فرضی را توسط یک اکشن متد Web API ارائه داده‌ایم که در بسیاری از موارد حتی می‌تواند توسط فیلتر Authorize نیز محافظت شده باشد.


ساخت URL برای دسترسی به اطلاعات باینری

تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی می‌کنند. این متد، شیء URL را از شیء window جاری دریافت می‌کند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید می‌کند. حاصل آن، یک URL ویژه‌است مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در برنامه‌های Blazor نیاز است اینکار را توسط JS Interop آن انجام داد؛ از این جهت که API تولید یک Blob URL، صرفا توسط کدهای جاوا اسکریپتی قابل دسترسی است. به همین جهت فایل جدید Client\wwwroot\site.js را با محتوای زیر ایجاد کرده و همچنین مدخل آن‌را در به انتهای فایل Client\wwwroot\index.html، پیش از بسته شدن تگ body، اضافه می‌کنیم:
window.JsBinaryFilesUtils = {
  createBlobUrl: function (byteArray, contentType) {
    // The byte array in .NET is encoded to base64 string when it passes to JavaScript.
    const numArray = atob(byteArray)
      .split("")
      .map((c) => c.charCodeAt(0));
    const uint8Array = new Uint8Array(numArray);
    const blob = new Blob([uint8Array], { type: contentType });
    return URL.createObjectURL(blob);
  },
  downloadFromUrl: function (fileName, url) {
    const anchor = document.createElement("a");
    anchor.style.display = "none";
    anchor.href = url;
    anchor.download = fileName;
    document.body.appendChild(anchor);
    anchor.click();
    document.body.removeChild(anchor);
  },
  downloadBlazorByteArray: function (fileName, byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.downloadFromUrl(fileName, blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
  printFromUrl: function (url) {
    const iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = url;
    document.body.appendChild(iframe);
    if (iframe.contentWindow) {
      iframe.contentWindow.print();
    }
  },
  printBlazorByteArray: function (byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.printFromUrl(blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
  showUrlInNewTab: function (url) {
    window.open(url);
  },
  showBlazorByteArrayInNewTab: function (byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.showUrlInNewTab(blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
};
توضیحات:
- زمانیکه در برنامه‌های Blazor با استفاده از متد ()HttpClient.GetByteArrayAsync آرایه‌ای از بایت‌های یک فایل باینری را دریافت می‌کنیم، ارسال آن به کدهای جاوااسکریپتی به صورت یک رشته‌ی base64 شده صورت می‌گیرد (JS Interop اینکار را به صورت خودکار انجام می‌دهد). به همین جهت در متد createBlobUrl روش تبدیل این رشته‌ی base64 دریافتی را به آرایه‌ای از بایت‌ها، سپس به یک Blob و در آخر به یک Blob URL، مشاهده می‌کنید.  این Blob Url اکنون آدرس موقتی دسترسی به آرایه‌ای از بایت‌های دریافتی توسط مرورگر است. به همین جهت می‌توان از آن به عنوان src بسیاری از اشیاء HTML استفاده کرد.
- متد downloadFromUrl، کار دریافت یک Url و سپس دانلود خودکار آن‌را انجام می‌دهد. اگر به یک anchor استاندارد HTML، ویژگی download را نیز اضافه کنیم، با کلیک بر روی آن، بجای گشوده شدن این Url، مرورگر آن‌را دریافت خواهد کرد. متد downloadFromUrl کار ساخت لینک و تنظیم ویژگی‌های آن و سپس کلیک بر روی آن‌را به صورت خودکار انجام می‌دهد. از متد downloadFromUrl زمانی استفاده کنید که منبع مدنظر، محافظت شده نباشد و Url آن به سادگی در مرورگر قابل گشودن باشد.
- متد downloadBlazorByteArray همان کار متد downloadFromUrl را انجام می‌دهد؛ با این تفاوت که Url مورد نیاز توسط متد downloadFromUrl را از طریق یک Blob Url تامین می‌کند.
- متد printFromUrl که جهت دسترسی به منابع محافظت نشده طراحی شده‌است، Url یک منبع را دریافت کرده، آن‌را به یک iframe اضافه می‌کند و سپس متد print را بر روی این iframe به صورت خودکار فراخوانی خواهد کرد تا سبب ظاهر شدن صفحه‌ی پیش‌نمایش چاپ شود.
- printBlazorByteArray همان کار متد printFromUrl را انجام می‌دهد؛  با این تفاوت که Url مورد نیاز توسط متد printFromUrl را از طریق یک Blob Url تامین می‌کند.


تهیه‌ی متدهایی الحاقی جهت کار ساده‌تر با JsBinaryFilesUtils

پس از تهیه‌ی JsBinaryFilesUtils فوق، می‌توان با استفاده از کلاس زیر که به همراه متدهایی الحاقی جهت دسترسی به امکانات آن است، کار با متدهای دریافت، نمایش و چاپ فایل‌های باینری را ساده‌تر کرد و از تکرار کدها جلوگیری نمود:
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace BlazorWasmShowBinaryFiles.Client.Utils
{
    public static class JsBinaryFilesUtils
    {
        public static ValueTask<string> CreateBlobUrlAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeAsync<string>("JsBinaryFilesUtils.createBlobUrl", byteArray, contentType);
        }

        public static ValueTask DownloadFromUrlAsync(this IJSRuntime JSRuntime, string fileName, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadFromUrl", fileName, url);
        }

        public static ValueTask DownloadBlazorByteArrayAsync(
            this IJSRuntime JSRuntime,
            string fileName, byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadBlazorByteArray",
                    fileName, byteArray, contentType);
        }

        public static ValueTask PrintFromUrlAsync(this IJSRuntime JSRuntime, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printFromUrl", url);
        }

        public static ValueTask PrintBlazorByteArrayAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printBlazorByteArray", byteArray, contentType);
        }

        public static ValueTask ShowUrlInNewTabAsync(this IJSRuntime JSRuntime, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showUrlInNewTab", url);
        }

        public static ValueTask ShowBlazorByteArrayInNewTabAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showBlazorByteArrayInNewTab", byteArray, contentType);
        }
    }
}


اصلاح Content Security Policy سمت سرور جهت ارائه‌ی محتوای blob

پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL می‌توان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرس‌هایی به صورت blob:http تولید می‌شوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob:
default-src 'self' blob:
object-src 'self' blob:
در غیراینصورت مرورگر، نمایش یک چنین تصاویر و یا اشیایی را سد خواهد کرد.


نمایش فایل PDF دریافتی از سرور، به همراه دکمه‌های دریافت، چاپ و نمایش آن در صفحه‌ی جاری

در ادامه کدهای کامل مرتبط با تصویری را که در ابتدای بحث مشاهده کردید، ملاحظه می‌کنید:
@page "/"

@using BlazorWasmShowBinaryFiles.Client.Utils

@inject IJSRuntime JSRuntime
@inject HttpClient HttpClient

<h1>Display PDF Files</h1>
<button class="btn btn-info" @onclick="handlePrintPdf">Print PDF</button>
<button class="btn btn-primary ml-2" @onclick="handleShowPdf">Show PDF</button>
<button class="btn btn-success ml-2" @onclick="handleDownloadPdf">Download PDF</button>

@if(!string.IsNullOrWhiteSpace(PdfBlobUrl))
{
    <section class="card mb-5 mt-3">
        <div class="card-header">
            <h4>using iframe</h4>
        </div>
        <div class="card-body">
            <iframe title="PDF Report" width="100%" height="600" src="@PdfBlobUrl" type="@PdfContentType"></iframe>
        </div>
    </section>

    <section class="card mb-5">
        <div class="card-header">
            <h4>using object</h4>
        </div>
        <div class="card-body">
            <object data="@PdfBlobUrl" aria-label="PDF Report" type="@PdfContentType" width="100%" height="100%"></object>
        </div>
    </section>

    <section class="card mb-5">
        <div class="card-header">
            <h4>using embed</h4>
        </div>
        <div class="card-body">
            <embed aria-label="PDF Report" src="@PdfBlobUrl" type="@PdfContentType" width="100%" height="100%">
        </div>
    </section>
}

@code
{
    private const string ReportUrl = "/api/Reports/GetPdfReport";
    private const string PdfContentType = "application/pdf";

    private string PdfBlobUrl;

    private async Task handlePrintPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `PrintFromUrlAsync` method.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        await JSRuntime.PrintBlazorByteArrayAsync(byteArray, PdfContentType);
    }

    private async Task handleDownloadPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `DownloadFromUrlAsync` method.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        await JSRuntime.DownloadBlazorByteArrayAsync("report.pdf", byteArray, PdfContentType);
    }

    private async Task handleShowPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `ReportUrl` as the `src` of the `iframe` directly.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        PdfBlobUrl = await JSRuntime.CreateBlobUrlAsync(byteArray, PdfContentType);
    }

    // Tips:
    // 1- How do I enable/disable the built-in pdf viewer of FireFox
    // https://support.mozilla.org/en-US/kb/disable-built-pdf-viewer-and-use-another-viewer
    // 2- How to configure browsers to use the Adobe PDF plug-in to open PDF files
    // https://helpx.adobe.com/acrobat/kb/pdf-browser-plugin-configuration.html
    // https://helpx.adobe.com/acrobat/using/display-pdf-in-browser.html
    // 3- Microsoft Edge is gaining new PDF reader features within the Windows 10 Fall Creator’s Update (version 1709).
}
توضیحات:
- پس از تهیه‌ی JsBinaryFilesUtils و متدهای الحاقی متناظر با آن، اکنون تنها کافی است با  استفاده از متد ()HttpClient.GetByteArrayAsync، فایل PDF ارائه شده‌ی توسط یک اکشن متد را به صورت آرایه‌ای از بایت‌ها دریافت و سپس به متدهای چاپ (PrintBlazorByteArrayAsync) و دریافت (DownloadBlazorByteArrayAsync) آن ارسال کنیم.
- در مورد نمایش آرایه‌ای از بایت‌های دریافتی، وضعیت کمی متفاوت است. ابتدا باید توسط متد CreateBlobUrlAsync، آدرس موقتی این آرایه را در مرورگر تولید کرد و سپس این آدرس را برای مثال به src یک iframe انتساب دهیم تا PDF را با استفاده از امکانات توکار مرورگر، نمایش دهد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmShowBinaryFiles.zip
مطالب
استفاده از Razor در فایل‌های JavaScript و CSS
یکی از مشکلات سینتکس Razor سمت سرور، این است که در فایل‌های JavaScript و CSS سمت کاربر نمی‌توانیم از آن استفاده کنیم، به عنوان مثال فرض کنید در یک فایل JavaScript نیاز به مشخص سازی آدرس یک اکشن متد دارید؛ مثلاً انجام یک عملیات ای‌جکسی. در این حالت به عنوان یک Best Practice بهتر است از Url.Action استفاده کنید. اما همانطور که عنوان شد، این امکان یعنی استفاده از سینتکس Razor در فایل‌های JS و CSS مهیا نیست.
ساده‌ترین راه‌حل، تولید ویوهای سمت سرور JavaScript ایی است. برای اینکار تنها کاری که باید انجام دهیم، تغییر مقدار Content-Type صفحه به مقدار موردنظر می‌باشد؛ مثلاً text/javascript برای فایل‌های JS و text/css برای فایل‌های CSS. به عنوان مثال برای فایل‌های CSS به این صورت عمل خواهیم کرد:
public ActionResult Style()
{
            Response.ContentType = "text/css";
            var model = new Style
            {
                Color = "red",
                Background = "blue"
            };
            return View(model);
}
برای ویوی آن نیز خواهیم داشت:
@model ExternalJavaScript.Models.Style
@{
    Layout = null;
}
body {
    color : @Model.Color;
    background-color : @Model.Background;
}
در نهایت ویوی فوق را به عنوان فایل CSS در فایل Layout استفاده خواهیم کرد:
<link rel="stylesheet" href="@Url.Action("Style","Home")" />
برای حالت فوق می‌توانیم یک اکشن فیلتر به صورت زیر تهیه کنیم:
public class ContentType : ActionFilterAttribute
    {
        private string _contentType;
        public ContentType(string ct)
        {
            this._contentType = ct;
        }

        public override void OnActionExecuted(ActionExecutedContext context) { /* nada */ }
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            context.HttpContext.Response.ContentType = this._contentType;
        }
    }
و برای استفاده از آن خواهیم داشت:
[ContentType("text/css")]
public ActionResult Style()
 {
     var model = new Style
      {
                Color = "red",
                Background = "blue"
      };
     return View(model);
}

برای فایل‌های JS نیز می‌توانیم از یک View به عنوان محل قرارگیری کدهای جاوا اسکریپت استفاده کنیم:
public class JavaScriptSettingsController : Controller
{
        public ActionResult Index()
        {
            return PartialView();
        }
}
در این حالت در داخل فایل Index.cshtml کدهای جاوا اسکریپت را همراه با سینتکس Razor می‌توانیم بنویسیم:
$(function(){
    $.post('@Url.Action("GetData", "Home")', function (data) {
        $('.notificationList').html(data);
            if ($(data).filter("li").length != 0) {
                $('#notificationCounter').html($(data).filter("li").length);
            }
    });
});
سپس در داخل فایل Layout.cshtml_ می‌توانیم به ویوی فوق ارجاعی داشته باشیم:
<script src="/JavaScriptSettings"></script>
این روش به خوبی برای ویوهای JS و CSS کار خواهد کرد؛ اما از آنجائیکه ویوی ما توسط ویژوال استودیو به عنوان یک فایل JS و یا CSS معتبر شناخته نمی‌شود، Intellisense برای آن مهیا نیست. برای فعال سازی Intellisense و همچنین معتبر شناخته شدن ویوی فوق، بهترین راه‌حل قرار دادن کدهای JS درون بلاک script است (برای فایل‌های CSS نیز همینطور):
<script>
    $(function () {
        $.post('@Url.Action("Index", "Home")', function (data) {
            $('.notificationList').html(data);
            if ($(data).filter("li").length != 0) {
                $('#notificationCounter').html($(data).filter("li").length);
            }
        });
    });
</script>
اما با اجرای برنامه، در کنسول مرورگر بلافاصله خطای Uncaught SyntaxError: Unexpected token < را دریافت خواهید کرد. در این حالت به روشی نیاز داریم که در زمان اجرا بلاک script را حذف نمائید. بنابراین از یک اکشن فیلتر سفارشی برای اینکار استفاده خواهیم کرد. کار این اکشن فیلتر، تغییر مقدار Content-Type و همچنین حذف بلاک مورد نظر می‌باشد:
public class ExternalFileAttribute : ActionFilterAttribute
    {
        private readonly string _contentType;
        private readonly string _tag;
        public ExternalFileAttribute(string ct, string tag)
        {
            this._contentType = ct;
            _tag = tag;
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var response = filterContext.HttpContext.Response;
            response.Filter = new StripEnclosingTagsFilter(response.Filter, _tag);
            response.ContentType = _contentType;
        }

        private class StripEnclosingTagsFilter : MemoryStream
        {
            private static Regex _leadingOpeningScriptTag;
            private static Regex _trailingClosingScriptTag;

            //private static string Tag;

            private readonly StringBuilder _output;
            private readonly Stream _responseStream;

            /*static StripEnclosingTagsFilter()
            {
                LeadingOpeningScriptTag = new Regex(string.Format(@"^\s*<{0}[^>]*>", Tag), RegexOptions.Compiled);
                TrailingClosingScriptTag = new Regex(string.Format(@"</{0}>\s*$", Tag), RegexOptions.Compiled);
            }*/

            public StripEnclosingTagsFilter(Stream responseStream, string tag)
            {
                _leadingOpeningScriptTag = new Regex(string.Format(@"^\s*<{0}[^>]*>", tag), RegexOptions.Compiled);
                _trailingClosingScriptTag = new Regex(string.Format(@"</{0}>\s*$", tag), RegexOptions.Compiled);

                _responseStream = responseStream;
                _output = new StringBuilder();
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                string response = GetStringResponse(buffer, offset, count);
                _output.Append(response);
            }

            public override void Flush()
            {
                string response = _output.ToString();

                if (_leadingOpeningScriptTag.IsMatch(response) && _trailingClosingScriptTag.IsMatch(response))
                {
                    response = _leadingOpeningScriptTag.Replace(response, string.Empty);
                    response = _trailingClosingScriptTag.Replace(response, string.Empty);
                }

                WriteStringResponse(response);
                _output.Clear();
            }

            private static string GetStringResponse(byte[] buffer, int offset, int count)
            {
                byte[] responseData = new byte[count];
                Buffer.BlockCopy(buffer, offset, responseData, 0, count);

                return Encoding.Default.GetString(responseData);
            }

            private void WriteStringResponse(string response)
            {
                byte[] outdata = Encoding.Default.GetBytes(response);
                _responseStream.Write(outdata, 0, outdata.GetLength(0));
            }
        }
    }
 در نهایت می‌توانیم اکشن‌متد موردنظرمان را با فیلتر سفارشی مزین کنیم:
[ExternalFile("text/javascript", "script")]
public ActionResult Index()
{
      return PartialView();
}
برای تولید ویوهای CSS نیز کافی است مقادیر فیلتر را تغییر دهیم:
[ExternalFile("text/css", "style")]
public ActionResult Style()
{
            var model = new Style
            {
                Color = "red",
                Background = "blue"
            };
            return View(model);
}
اشتراک‌ها
ملاحظات امنیتی جهت کار با JavaScript در سال 2024

5 JavaScript Security Best Practices for 2024

Any JavaScript web application needs to have a Content Security Policy (CSP), a browser security standard that dictates what the browser is allowed to load — whether that be a domain, subdomain, or resource. Without a CSP, hackers can exploit cross-site scripting vulnerabilities, potentially resulting in a data breach.

ملاحظات امنیتی جهت کار با JavaScript  در سال 2024
مطالب
تنظیمات CORS در ASP.NET Core
برنامه‌های امروزی، ممکن است به چندین Web API مستقل، تبدیل شده و سپس برنامه‌هایی (Front-ends) جدای از آن‌ها برای کار با آن‌ها ایجاد شوند. بنابراین این وظیفه‌ی برنامه‌‌های Web API است که مطمئن شوند کلاینت‌ها قادر به تعامل با آن‌ها هستند. CORS استانداردی است که یک چنین امکانی را مهیا می‌کند.



CORS چیست؟

CORS و یا cross origin resource sharing، یک مکانیزم امنیتی است که در تمام مرورگرهای جدید جهت جلوگیری از ارسال اطلاعات یک وب سایت، به وب سایتی دیگر، درنظر گرفته شده‌است. درخواست دسترسی به یک منبع (Resource) مانند تصویر، داده و غیره، خارج از سرآغاز آن، یک درخواست cross-origin نامیده می‌شود. برای مدیریت یک چنین درخواست‌هایی، استانداردی به نام CORS طراحی شده‌است. می‌توان به آن مانند نگهبانی یک ساختمان نگاه کرد که تا مجوز خاصی به آن‌ها ارائه نشود، امکان دسترسی به منابع ساختمان را صادر نخواهند کرد.


Origin چیست؟

سرآغاز یک درخواست از سه قسمت تشکیل می‌شود:
- Protocol/Scheme مانند HTTP/HTTPS
- Host مانند نام دومین
- شماره پورت مانند 443 برای پروتکل HTTPS  یا پورت 80 برای HTTP که عموما هر دو مورد به علت پیش‌فرض بودن، ذکر نمی‌شوند

بنابراین URLای مانند https://www.dntips.ir یک Origin را مشخص می‌کند. در اینجا به تمام منابعی که از این سرآغاز شروع می‌شوند و سه قسمت یاد شده‌ی آن‌ها یکی است، same-origin گفته می‌شود.
در این حالت اگر منابعی به URLهایی مانند https://www.dntips.ir (پروتکل متفاوت) و یا https://github.com (با host متفاوت) اشاره کنند، به آن‌ها Different-Origin گفته خواهد شد.


سیاست امنیتی Same-Origin چیست؟

سیاست امنیتی Same-Origin که به صورت توکار در تمام مرورگرهای امروزی تعبیه شده‌است، مانع تعامل سرآغازهایی متفاوت، جهت جلوگیری از حملات امنیتی مانند Cross-Site Request Forgery یا CSRF می‌شود. این سیاست امنیتی بسیار محدود کننده‌است و برای مثال مانع این می‌شود که بتوان توسط کدهای JavaScript ای، درخواستی را به سرآغاز دیگری ارسال کرد؛ حتی اگر بدانیم درخواست رسیده از منبعی مورد اطمینان صادر شده‌است. برای مثال اگر سایت جاری یک درخواست Ajax ای را به https://anotherwebsite.com/api/users ارسال کند، چون قسمت host مربوط به origin آن‌ها یکی نیست، این عملیات توسط مرورگر غیرمجاز شمرده شده و مسدود می‌شود.


چگونه می‌توان از تنظیمات CORS، برای رفع محدودیت‌های سیاست‌های دسترسی Same-Origin استفاده کرد؟

استاندارد CORS تعدادی header ویژه را جهت تبادل اطلاعات بین سرور و مرورگر مشخص کرده‌است تا توسط آن‌ها بتوان منابع را بین سرآغازهای متفاوتی به اشتراک گذاشت. در این حالت ابتدا کلاینت درخواستی را به سمت سرور ارسال می‌کند. سپس سرور پاسخی را به همراه هدرهای ویژه‌ای که مشخص می‌کنند به چه منابعی و چگونه می‌توان دسترسی یافت، به سمت کلاینت ارسال خواهد کرد. اکثر این هدرها با Access-Control-Allow شروع می‌شوند:
• Access-Control-Allow-Origin
در آن لیست سرآغازهایی را که سرور مایل است منابع خود را با آن‌ها به اشتراک بگذارد، مشخص می‌شوند. در حالت توسعه‌ی برنامه می‌توان از مقدار * نیز برای آن استفاده کرد تا هر سرآغازی بتواند به منابع سرور دسترسی پیدا کند. اما از استفاده‌ی این مقدار سراسری و کلی، در حالت انتشار برنامه خودداری کنید.
• Access-Control-Allow-Headers
• Access-Control-Allow-Methods
• Access-Control-Allow-Credentials


درخواست‌های ویژه‌ی Pre-Flight

در بسیاری از موارد، مرورگر زمانیکه تشخیص می‌دهد درخواست صادر شده‌ی از طرف برنامه، قرار است به یک Origin دیگر ارسال شود، خودش یک درخواست ویژه را به نام Pre-Flight و از نوع OPTIONS به سمت سرور ارسال می‌کند. علت آن این است که سرورهای قدیمی، مفهوم CORS را درک نمی‌کنند و در برابر درخواست‌های ویژه‌ای مانند Delete که از سرور جاری صادر نشده باشند، مقاومت می‌کنند (صرفا درخواست‌های Same-Origin را پردازش می‌کنند). به همین جهت است که اگر به برگه‌ی network ابزارهای توسعه دهندگان مرورگر خود دقت کنید، درخواست‌های cross-origin به نظر دوبار ارسال شده‌اند. بار اول درخواستی که method آن، options است و در خواست دوم که درخواست اصلی است و برای مثال method آن put است. این درخواست اول، Pre-Flight Request نام دارد. هدف آن این است که بررسی کند آیا سرور مقصد، استاندارد CORS را متوجه می‌شود یا خیر و آیا اینقدر قدیمی است که صرفا درخواست‌های Same-Origin را پردازش می‌کند و مابقی را مسدود خواهد کرد. اگر سرور درخواست Pre-Flight را درک نکند، مرورگر درخواست اصلی را صادر نخواهد کرد.



البته درخواست‌های pre-flight بیشتر در حالت‌های زیر توسط مرورگر صادر می‌شوند:
- اگر متد اجرایی مدنظر، متدی بجز GET/POST/HEAD باشد.
- اگر content type درخواست‌های از نوع POST، چیزی بجز application/x-www-form-urlencoded, multipart/form-data و یا text/plain باشند.
- اگر درخواست، به همراه یک هدر سفارشی باشد نیز سبب صدور یک pre-flight request خواهد شد.

بنابراین در برنامه‌های SPA خود که اطلاعات را با فرمت JSON به سمت یک Web API ارسال می‌کنند (و Origin آن‌ها یکی نیست)، حتما شاهد درخواست‌های Pre-Flight نیز خواهید بود.


تنظیمات CORS در ASP.NET Core

اکنون که با مفهوم CORS آشنا شدیم، در ادامه به نحوه‌ی انجام تنظیمات آن در برنامه‌های ASP.NET Core خواهیم پرداخت.
تنظیمات ابتدایی CORS در فایل Startup.cs و متد ConfigureServices آن انجام می‌شوند:
public void ConfigureServices(IServiceCollection services) 
{ 
    // Add service and create Policy with options 
    services.AddCors(options => 
    { 
        options.AddPolicy("CorsPolicy", 
            builder => builder.AllowAnyOrigin() 
            .AllowAnyMethod() 
            .AllowAnyHeader() 
            .AllowCredentials() ); 
    }); 
     
    // Make sure MVC is added AFTER CORS 
    services.AddMvc(); 
}
ابتدا توسط متد AddCors، سرویس‌های مرتبط، به سیستم تزریق وابستگی‌های ASP.NET Core اضافه خواهند شد و سپس در قسمت تنظیمات آن می‌توان همان هدرهای Access-Control-Allow را که پیشتر در مورد آن‌ها بحث کردیم، تعریف و مقدار دهی کرد. برای مثال تنظیم AllowAnyOrigin، همان استفاده‌ی از مقدار * در هدر Access-Control-Allow-Origin تولیدی است. باید دقت داشت که تنظیمات این سرویس‌ها باید پیش از افزودن سرویس‌های MVC انجام شوند.
در اینجا CorsPolicy که به عنوان پارامتر مشخص شده‌است، نام این سیاست دسترسی سفارشی است و در قسمت‌های مختلف برنامه می‌توان ارجاعاتی را به این نام تعریف کرد.

و برای تعریف سرآغازی خاص و یا متدی مشخص، می‌توان به صورت زیر عمل کرد (ذکر صریح WithOrigins برای حالت توزیع برنامه مناسب است و از دیدگاه امنیتی، استفاده‌ی از AllowAnyOrigin کار صحیحی نیست ):
services.AddCors(options =>
{
    options.AddPolicy("AllowOrigin",
            builder => builder.WithOrigins("http://localhost:55294")
                              .WithMethods("GET", "POST", "HEAD"));
});

پس از تنظیم سیاست دسترسی مدنظر، اکنون نوبت به اعمال آن است. اینکار در متد Configure فایل Startup.cs انجام می‌شود:
public void Configure(IApplicationBuilder app) 
{ 
    // ... 
 
    app.UseCors("CorsPolicy"); 
     
    // ... 
     
    app.UseMvc(routes => 
    { 
        routes.MapRoute( 
            name: "default", 
            template: "{controller=Home}/{action=Index}/{id?}"); 
    }); 
}
در اینجا متد UseCors که میان‌افزار CORS را فعال می‌کند، باید پیش‌از میان‌افزار Mvc تعریف شود تا بتواند هدرها را اعمال کند. همچنین نامی که در اینجا به عنوان پارامتر آن استفاده شده، دقیقا به همان نامی که برای تنظیمات سیاست دسترسی درنظر گرفته شد، اشاره می‌کند.
متد app.UseCors، یک متد سراسری است و کل سیستم را داخل این سیاست دسترسی CORS قرار می‌دهد. اگر می‌خواهید صرفا به کنترلر خاصی این تنظیمات اعمال شوند، می‌توان از فیلتر EnableCors با ذکر نام سیاست دسترسی تعریف شده، استفاده کرد (در این حالت ذکر services.AddCors ضروری است و از ذکر app.UseCors صرفنظر می‌شود):
[EnableCors("CorsPolicy")]
public class MyApiController : Controller

و یا اگر می‌خواهید از app.UseCors سراسری استفاده کنید، اما آن‌را برای کنترلر و یا حتی اکشن متد خاصی غیرفعال نمائید، می‌توان از فیلتر [DisableCors] استفاده کرد.

چند نکته:
- به نام رشته‌ای سیاست دسترسی تعریف شده، دقت داشته باشید. اگر این نام را درست ذکر نکنید، هدری تولید نخواهد شد.
- میان‌افزار CORS، برای درخواست‌های same-origin نیز هدری را تولید نمی‌کند.


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

در هر دو روش فوق که یکی بر اساس app.UseCors سراسری و دیگری بر اساس فیلتر EnableCors اختصاصی کار می‌کنند، ذکر نام سیاست دسترسی options.AddPolicy ضروری است. اگر علاقه‌ای به ذکر این نام ندارید، می‌توان به طور کامل از تنظیم services.AddCors صرفنظر کرد (قسمت پارامتر و تنظیمات آن‌را ذکر نکرد) و متد app.UseCors را به صورت زیر تنظیم نمود که قابلیت داشتن تنظیمات خاص خود را نیز دارا است:
public void ConfigureServices(IServiceCollection services)
{
      services.AddCors();
      services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
 
    app.UseCors(policyBuilder =>
    {
        string[] origins = new string[] { "http://localhost:2000", "http://localhost:2001" };
 
        policyBuilder
            .AllowAnyHeader()
            .AllowAnyMethod()
            .WithOrigins(origins);
    });
 
    app.UseMvc();
}

روش دیگر انجام اینکار، تعریف یک DefaultPolicy است:
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddDefaultPolicy(
            builder =>
            {
            builder
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader();
            });
          options.AddPolicy("MyCORSPolicy",
            builder =>
            {
                builder.WithOrigins("http://localhost:49373")
                                    .AllowAnyHeader()
                                    .AllowAnyMethod();
            });
     });
    services.AddMvc().AddNewtonsoftJson();
}
در اینجا در متد AddCors می‌توان چندین AddPolicy نامدار را داشت، به همراه یک AddDefaultPolicy بدون نام. استفاده‌ی از سیاست نام‌دار تعریف شده، یا توسط متد سراسری app.UseCors("MyCORSPolicy") انجام می‌شود و یا توسط فیلتر [EnableCors("MyCORSPolicy")]. اما اگر فیلتر [EnableCors] را بدون ذکر نامی قید کنیم، از همان تنظیمات AddDefaultPolicy استفاده خواهد کرد.


خطاهای متداول حین کار با CORS

خطای کار با SignalR و ارسال اطلاعات اعتبارسنجی کاربر

Access to XMLHttpRequest at 'http://localhost:22135/chat/negotiate?jwt=<removed the jwt key>' 
from origin 'http://localhost:53150' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: 
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. 
The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
برای رفع این مشکل کافی است متد AllowCredentials نیز قید شود:
services.AddCors(options =>
{
     options.AddPolicy("AllowAllOrigins",
         builder =>
          {
              builder
                 .AllowAnyOrigin()
                 .AllowAnyHeader()
                 .AllowAnyMethod()
                 .AllowCredentials();
          });
    });

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

CORS به صورت پیش‌فرض اطلاعات کوکی‌ها را به دومینی دیگر ارسال نمی‌کند. در این حالت اگر حتما نیاز به انجام اینکار است، باید در درخواست Ajax ای ارسالی، خاصیت withCredentials به true تنظیم شود. همچنین سمت سرور نیز هدر Access-Control-Allow-Credentials را تنظیم کند (همان متد AllowCredentials فوق). در اینجا Credentials به کوکی‌ها، هدرهای اعتبارسنجی (مانند هدرهای JWT) کاربران و یا مجوزهای TLS کلاینت‌ها اشاره می‌کند.
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://www.example.com/api/test');
xhr.withCredentials = true;

//In jQuery:
$.ajax({
  type: 'get',
  url: 'http://www.example.com/home',
  xhrFields: {
    withCredentials: true
}

//ES6 fetch API:
fetch(
   'http://remote-url.com/users',
   {
     'PUT',
     headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
     },
     mode: 'cors',
     credentials: 'include',
     body: JSON.stringify(body)
   }
)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((error) => console.error(error));

CORS حساس به حروف کوچک و بزرگ است

روش دیگر تنظیم فیلتر EnableCors را در اینجا مشاهده می‌کنید:
[EnableCors(origins: "http://MyWebsite.com", headers: "*", methods: "*")]
//is not the same as this:
[EnableCors(origins:"http://mywebsite.com", headers: "*", methods: "*")]
اگر بجای * دقیقا origins را مشخص کردید، بخاطر داشته باشید که این تنظیم به کوچکی و بزرگی حروف حساس است و در صورت اشتباهی (مانند ذکر MyWebSite بجای mywebsite متداول با حروف کوچک)، درخواست‌های رسیده مسدود خواهند شد. همچنین همانطور که ذکر شد، بین HTTP و HTTPS نیز تفاوت وجود دارد و origin این‌ها یکی درنظر گرفته نمی‌شود و حتی اگر تمام حروف را هم کوچک وارد کرده باشید، باز هم تنظیمات جداگانه‌ای را نیاز خواهند داشت.


در حالت بروز خطای مدیریت نشده‌ای در سمت سرور، ASP.NET Core هدرهای CORS را ارسال نمی‌کند
اطلاعات بیشتر 
مطالب
نحوه اضافه کردن Auto-Complete به جستجوی لوسین در ASP.NET MVC و Web forms
پیشنیازها:
چگونه با استفاده از لوسین مطالب را ایندکس کنیم؟
چگونه از افزونه jQuery Auto-Complete استفاده کنیم؟
نحوه استفاده صحیح از لوسین در ASP.NET


اگر به جستجوی سایت دقت کرده باشید، قابلیت ارائه پیشنهاداتی به کاربر توسط یک Auto-Complete به آن اضافه شده‌است. در مطلب جاری به بررسی این مورد به همراه دو مثال Web forms و MVC پرداخته خواهد شد.


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

الف) دریافت لوسین
از طریق NuGet آخرین نگارش را دریافت و به پروژه خود اضافه کنید. همچنین Lucene.NET Contrib را نیز به همین نحو دریافت نمائید.

ب) ایجاد ایندکس
کدهای این قسمت با مطلب برجسته سازی قسمت‌های جستجو شده، یکی است:
using System.Collections.Generic;
using System.IO;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Store;
using LuceneSearch.Core.Model;
using LuceneSearch.Core.Utils;

namespace LuceneSearch.Core
{
    public static class CreateIndex
    {
        static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_30;

        public static Document MapPostToDocument(Post post)
        {
            var postDocument = new Document();
            postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            var titleField = new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
            titleField.Boost = 3;
            postDocument.Add(titleField);
            postDocument.Add(new Field("Body", post.Body.RemoveHtmlTags(), Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
            return postDocument;
        }

        public static void CreateFullTextIndex(IEnumerable<Post> dataList, string path)
        {
            var directory = FSDirectory.Open(new DirectoryInfo(path));
            var analyzer = new StandardAnalyzer(_version);
            using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
            {
                foreach (var post in dataList)
                {
                    writer.AddDocument(MapPostToDocument(post));
                }

                writer.Optimize();
                writer.Commit();
                writer.Close();
                directory.Close();
            }
        }
    }
}
تنها تفاوت آن اضافه شدن titleField.Boost = 3 می‌باشد. توسط Boost به لوسین خواهیم گفت که اهمیت عبارات ذکر شده در عناوین مطالب، بیشتر است از اهمیت متون آن‌ها.


ج) تهیه قسمت منبع داده Auto-Complete

namespace LuceneSearch.Core.Model
{
    public class SearchResult
    {
        public int Id { set; get; }
        public string Title { set; get; }
    }
}

using System.Collections.Generic;
using System.IO;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using LuceneSearch.Core.Model;
using LuceneSearch.Core.Utils;

namespace LuceneSearch.Core
{
    public static class AutoComplete
    {
        private static IndexSearcher _searcher;

        /// <summary>
        /// Get terms starting with the given prefix
        /// </summary>
        /// <param name="prefix"></param>
        /// <param name="maxItems"></param>
        /// <returns></returns>
        public static IList<SearchResult> GetTermsScored(string indexPath, string prefix, int maxItems = 10)
        {
            if (_searcher == null)
                _searcher = new IndexSearcher(FSDirectory.Open(new DirectoryInfo(indexPath)), true);

            var resultsList = new List<SearchResult>();
            if (string.IsNullOrWhiteSpace(prefix))
                return resultsList;

            prefix = prefix.ApplyCorrectYeKe();

            var results = _searcher.Search(new PrefixQuery(new Term("Title", prefix)), null, maxItems);
            if (results.TotalHits == 0)
            {
                results = _searcher.Search(new PrefixQuery(new Term("Body", prefix)), null, maxItems);
            }

            foreach (var doc in results.ScoreDocs)
            {
                resultsList.Add(new SearchResult
                {
                    Title = _searcher.Doc(doc.Doc).Get("Title"),
                    Id = int.Parse(_searcher.Doc(doc.Doc).Get("Id"))
                });
            }

            return resultsList;
        }
    }
}
توضیحات:
برای نمایش Auto-Complete نیاز به منبع داده داریم که نحوه ایجاد آن‌را در کدهای فوق ملاحظه می‌کنید. در اینجا توسط جستجوی سریع لوسین و امکانات PrefixQuery آن، به تعدادی مشخص (maxItems)، رکوردهای یافت شده را بازگشت خواهیم داد. خروجی حاصل لیستی است از SearchResultها شامل عنوان مطلب و Id آن. عنوان را به کاربر نمایش خواهیم داد؛ از Id برای هدایت او به مطلبی مشخص استفاده خواهیم کرد.


د) نمایش Auto-Complete در ASP.NET MVC

using System.Text;
using System.Web.Mvc;
using LuceneSearch.Core;
using System.Web;

namespace LuceneSearch.Controllers
{
    public class HomeController : Controller
    {
        static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx";

        public ActionResult Index(int? id)
        {
            if (id.HasValue)
            {
                //todo: do something
            }
            return View(); //Show the page
        }

        public virtual ActionResult ScoredTerms(string q)
        {
            if (string.IsNullOrWhiteSpace(q))
                return Content(string.Empty);

            var result = new StringBuilder();
            var items = AutoComplete.GetTermsScored(_indexPath, q);
            foreach (var item in items)
            {
                var postUrl = this.Url.Action(actionName: "Index", controllerName: "Home", routeValues: new { id = item.Id }, protocol: "http");
                result.AppendLine(item.Title + "|" + postUrl);
            }

            return Content(result.ToString());
        }
    }
}

@{
    ViewBag.Title = "جستجو";
    var scoredTermsUrl = Url.Action(actionName: "ScoredTerms", controllerName: "Home");
    var bulletImage = Url.Content("~/Content/Images/bullet_shape.png");
}
<h2>
    جستجو</h2>

<div align="center">
    @Html.TextBox("term", "", htmlAttributes: new { dir = "ltr" })
    <br />
    جهت آزمایش lu را وارد نمائید
</div>

@section scripts
{
    <script type="text/javascript">
        EnableSearchAutocomplete('@scoredTermsUrl', '@bulletImage');
    </script>
}

function EnableSearchAutocomplete(url, img) {
    var formatItem = function (row) {
        if (!row) return "";
        return "<img src='" + img + "' /> " + row[0];
    }

    $(document).ready(function () {
        $("#term").autocomplete(url, {
            dir: 'rtl', minChars: 2, delay: 5,
            mustMatch: false, max: 20, autoFill: false,
            matchContains: false, scroll: false, width: 300,
            formatItem: formatItem
        }).result(function (evt, row, formatted) {
            if (!row) return;
            window.location = row[1];
        });
    });
}
توضیحات:
- ابتدا ارجاعاتی را به jQuery، افزونه Auto-Complete و اسکریپت سفارشی تهیه شده، در فایل layout پروژه تعریف خواهیم کرد.
در اینجا سه قسمت را مشاهده می‌کنید: کدهای کنترلر، View متناظر و اسکریپتی که Auto-Complete را فعال خواهد ساخت.
- قسمت مهم کدهای کنترلر، دو سطر زیر هستند:
result.AppendLine(item.Title + "|" + postUrl);
return Content(result.ToString());
مطابق نیاز افزونه انتخاب شده در مثال جاری، فرمت خروجی مدنظر باید شامل سطرهایی حاوی متن قابل نمایش به همراه یک Id (یا در اینجا یک آدرس مشخص) باشد. البته ذکر این Id اختیاری بوده و در اینجا جهت تکمیل بحث ارائه شده است.
return Content هم سبب بازگشت این اطلاعات به افزونه خواهد شد.
- کدهای View متناظر بسیار ساده هستند. تنها نام TextBox تعریف شده مهم می‌باشد که در متد جاوا اسکریپتی EnableSearchAutocomplete استفاده شده است. به علاوه، نحوه مقدار دهی آدرس دسترسی به اکشن متد ScoredTerms نیز مهم می‌باشد.
- در متد EnableSearchAutocomplete نحوه فراخوانی افزونه autocomplete را ملاحظه می‌کنید.
جهت آن، به راست به چپ تنظیم شده است. با 2 کاراکتر ورودی فعال خواهد شد با وقفه‌ای کوتاه. نیازی نیست تا انتخاب کاربر از لیست ظاهر شده حتما با عبارت جستجو شده صد در صد یکی باشد. حداکثر 20 آیتم در لیست ظاهر خواهند شد. اسکرول بار لیست را حذف کرده‌ایم. عرض آن به 300 تنظیم شده است و نحوه فرمت دهی نمایشی آن‌را نیز ملاحظه می‌کنید. برای این منظور از متد formatItem استفاده شده است. آرایه row در اینجا در برگیرنده اعضای Title و Id ارسالی به افزونه است. اندیس صفر آن به عنوان دریافتی اشاره می‌کند.
همچنین نحوه نشان دادن عکس العمل به عنصر انتخابی را هم ملاحظه می‌کنید (در متد result مقدار دهی شده).  window.location را به عنصر دوم آرایه row هدایت خواهیم کرد. این عنصر دوم مطابق کدهای اکشن متد تهیه شده، به آدرس یک صفحه اشاره می‌کند.


ه) نمایش Auto-Complete در ASP.NET WebForms

قسمت عمده مطالب فوق با وب فرم‌ها نیز یکی است. خصوصا توضیحات مرتبط با متد EnableSearchAutocomplete ذکر شده.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LuceneSearch.WebForms.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>جستجو</title>
    <link href="Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.autocomplete.js" type="text/javascript"></script>
    <script src="Scripts/custom.js" type="text/javascript"></script>
</head>
<body dir="rtl">
    <h2>
        جستجو</h2>
    <form id="form1" runat="server">
    <div align="center">
        <asp:TextBox runat="server" dir="ltr" ID="term"></asp:TextBox>
        <br />
        جهت آزمایش lu را وارد نمائید
    </div>
    </form>
    <script type="text/javascript">
        EnableSearchAutocomplete('Search.ashx', 'Content/Images/bullet_shape.png');
    </script>
</body>
</html>

using System.Text;
using System.Web;
using LuceneSearch.Core;

namespace LuceneSearch.WebForms
{
    public class Search : IHttpHandler
    {
        static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx";

        public void ProcessRequest(HttpContext context)
        {
            string q = context.Request.QueryString["q"];
            if (string.IsNullOrWhiteSpace(q))
            {
                context.Response.Write(string.Empty);
                context.Response.End();
            }

            var result = new StringBuilder();
            var items = AutoComplete.GetTermsScored(_indexPath, q);
            foreach (var item in items)
            {
                var postUrl = "Default.aspx?id=" + item.Id;
                result.AppendLine(item.Title + "|" + postUrl);
            }

            context.Response.ContentType = "text/plain";
            context.Response.Write(result.ToString());
            context.Response.End();
        }

        public bool IsReusable
        { get { return false; } }
    }
}

در اینجا بجای Controller از یک Generic handler استفاده شده است (Search.ashx).
result.AppendLine(item.Title + "|" + postUrl);
context.Response.Write(result.ToString());
در آن، عنوان مطالب یافت شده به همراه یک آدرس مشخص، تهیه و در Response نوشته خواهند شد.


کدهای کامل مثال فوق را از اینجا می‌توانید دریافت کنید:
همچنین باید دقت داشت که پروژه MVC آن از نوع MVC4 است (VS2010) و فرض براین می‌باشد که IIS Express 7.5 را نیز پیشتر نصب کرده‌اید.
کلمه عبور فایل: dotnettips91
 
مطالب
سفارشی سازی عناصر صفحات پویای افزودن و ویرایش رکوردهای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطالب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» و «فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال‌ها اکتفاء خواهد شد.


صورت مساله

    public class Product
    {
        public int Id { set; get; }
        public DateTime AddDate { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
    }
در اینجا تعریف محصول، شامل خاصیت‌های تاریخ ثبت، نام و قیمت آن است.
می‌خواهیم زمانیکه فرم‌های پویای ویرایش یا افزودن رکوردها ظاهر شدند، در حین تکمیل نام، یک auto complete ظاهر شود:


در حین ورود تاریخ، یک date picker شمسی جهت سهولت ورود اطلاعات نمایش داده شود:


همچنین در قسمت ورود مبلغ و قیمت، به صورت خودکار حرف سه رقم جدا کننده هزارها، نمایش داده شوند تا کاربران در حین ورود مبالغ بالا دچار اشتباه نشوند.



پیشنیازها

- برای نمایش auto complete از همان امکانات توکار jQuery UI که به همراه jqGrid عرضه می‌شوند، استفاده خواهیم کرد.
- برای نمایش date picker شمسی از مطلب «PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند» کمک خواهیم گرفت.
- جهت اعمال خودکار حرف سه رقم جدا کننده هزارها از افزونه‌ی Price Format جی‌کوئری استفاده می‌کنیم.

تعریف و الحاق این پیشنیازها، فایل layout برنامه را به شکل زیر تغییر خواهد داد:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>

    <link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" />
    <link href="~/Content/jquery.jqGrid/ui.jqgrid.css" rel="stylesheet" />
    <link href="~/Content/PersianDatePicker.css" rel="stylesheet" />
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.7.2.min.js"></script>
    <script src="~/Scripts/jquery-ui-1.8.11.min.js"></script>
    <script src="~/Scripts/i18n/grid.locale-fa.js"></script>
    <script src="~/Scripts/jquery.jqGrid.min.js"></script>
    <script src="~/Scripts/PersianDatePicker.js"></script>
    <script src="~/Scripts/jquery.price_format.2.0.js"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>


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

الف) نمایش auto complete در حین ورود نام محصولات
                colModel: [
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 100,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40,
                            dataInit: function (elem) {
                                // http://jqueryui.com/autocomplete/
                                $(elem).autocomplete({
                                    source: '@Url.Action("GetProductNames","Home")',
                                    minLength: 2,
                                    select: function (event, ui) {
                                        $(elem).val(ui.item.value);
                                        $(elem).trigger('change');
                                    }
                                });
                            }
                        },
                        editrules: {
                            required: true
                        }
                    }           
     ],
برای اعمال هر نوع افزونه‌ی جی‌کوئری به عناصر فرم‌های خودکار ورود اطلاعات در jqGrid، تنها کافی است که رویداد dataInit یک ستون را بازنویسی کنیم. در اینجا توسط elem، المان جاری را در اختیار خواهیم داشت. سپس از این المان جهت اعمال افزونه‌ای دلخواه استفاده می‌کنیم. برای مثال در اینجا از متد autocomplete استفاده شده‌است که جزئی از jQuery UI استاندارد است.
برای پردازش سمت سرور آن و مقدار دهی url آن، یک چنین اکشن متدی را می‌توان تدارک دید:
        public ActionResult GetProductNames(string term)
        {
            var list = ProductDataSource.LatestProducts
                .Where(x => x.Name.StartsWith(term))
                .Select(x => x.Name)
                .Take(10)
                .ToArray();
            return Json(list, JsonRequestBehavior.AllowGet);
        }
مقدار term، عبارتی است که کاربر وارد کرده است. توسط متد StartsWith، کلیه نام‌هایی را که با این عبارت شروع می‌شوند (البته 10 مورد از آن‌ها را) بازگشت می‌دهیم.

ب) نمایش date picker شمسی در حین ورود تاریخ
                colModel: [
                    {
                        name: 'AddDate', index: 'AddDate', align: 'center', width: 100,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 10,
                            // https://www.dntips.ir/post/1382
                            onclick: "PersianDatePicker.Show(this,'@today');"
                        },
                        editrules: {
                            required: true
                        }
                    }
                ],
Date picker مورد استفاده، وابستگی خاصی به jQuery ندارد. مطابق مستندات آن باید در رویدادگردان onclick، این تقویم شمسی را فعال کرد. بنابراین در قسمت onclick دقیقا این مورد را اعمال می‌کنیم.

 @{
ViewBag.Title = "Index";
var today = DateTime.Now.ToPersianDate();
}
مقدار today آن در ابتدای View به نحو فوق تعریف شده‌است. کدهای کامل متد کمکی ToPersianDate در پروژه‌ی پیوست موجود است.

ج) اعمال حروف سه رقم جدا کننده هزارها در حین ورود قیمت
                colModel: [
                    {
                        name: 'Price', index: 'Price', align: 'center', width: 100,
                        formatter: 'currency',
                        formatoptions:
                        {
                            decimalSeparator: '.',
                            thousandsSeparator: ',',
                            decimalPlaces: 2,
                            prefix: '$'
                        },
                        editable: true, edittype: 'text',
                        editoptions: {
                            dir: 'ltr',
                            dataInit: function (elem) {
                                // http://jquerypriceformat.com/
                                $(elem).priceFormat({
                                    prefix: '',
                                    thousandsSeparator: ',',
                                    clearPrefix: true,
                                    centsSeparator: '',
                                    centsLimit: 0
                                });
                            }
                        },
                        editrules: {
                            required: true,
                            minValue: 0
                        }
                    }
                ],
افزونه‌ی price format نیز یک افزونه‌ی جی‌کوئری است. بنابراین دقیقا مانند حالت auto complete آن‌را در dataInit فعال سازی می‌کنیم و همچنین یک سری تنظیم ابتدایی مانند مشخص سازی  thousandsSeparator آن‌را مقدار دهی خواهیم کرد.


یک نکته

همین تعاریف را دقیقا به فرم‌های جستجو نیز می‌توان اعمال کرد. در اینجا برای حالات ویرایش و افزودن رکوردها، editoptions مقدار دهی شده‌است؛ در مورد فرم‌های جستجو باید searchoptions و برای مثال dataInit آن‌را مقدار دهی کرد.



مشکل مهم!

با تنظیمات فوق، قسمت UI بدون مشکل کار می‌کند. اما اگر در سمت سرور، مقادیر دریافتی را بررسی کنیم، نه تاریخ و نه قیمت، قابل دریافت نیستند. زیرا تاریخ ارسالی به سرور شمسی است و مدل برنامه DateTime میلادی می‌باشد. همچنین به دلیل وجود حروف سه رقم جدا کننده هزارها، عبارت دریافتی قابل تبدیل به عدد نیستند و مقدار دریافتی صفر خواهد بود.
برای رفع این مشکلات، نیاز به تغییر model binder توکار ASP.NET MVC است. برای تاریخ‌ها از کلاس PersianDateModelBinder می‌توان استفاده کرد. برای اعداد decimal از کلاس ذیل:
using System;
using System.Globalization;
using System.Threading;
using System.Web.Mvc;

namespace jqGrid05.CustomModelBinders
{
    /// <summary>
    /// How to register it in the Application_Start method of Global.asax.cs
    /// ModelBinders.Binders.Add(typeof(decimal), new DecimalBinder());
    /// </summary>
    public class DecimalBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType == typeof(decimal) || bindingContext.ModelType == typeof(decimal?))
            {
                return bindDecimal(bindingContext);
            }
            return base.BindModel(controllerContext, bindingContext);
        }

        private static object bindDecimal(ModelBindingContext bindingContext)
        {
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult == null)
                return null;
            
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
            decimal value;
            var valueAsString = valueProviderResult.AttemptedValue == null ?
                                        null : valueProviderResult.AttemptedValue.Trim();
            if (string.IsNullOrEmpty(valueAsString))
                return null;
            
            if (!decimal.TryParse(valueAsString, NumberStyles.Any, Thread.CurrentThread.CurrentCulture, out value))
            {
                const string error ="عدد وارد شده معتبر نیست";
                var ex = new InvalidOperationException(error, new Exception(error, new FormatException(error)));
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
                return null;
            }
            return value;
        }
    }
}
در اینجا عبارت ارسالی به سرور به صورت یک رشته دریافت شده و سپس تبدیل به یک عدد decaimal می‌شود. در آخر به سیستم model binding بازگشت داده خواهد شد. به این ترتیب دیگر مشکلی با پردازش حروف سه رقم جدا کننده هزارها نخواهد بود.

برای ثبت و معرفی این کلاس‌ها باید به نحو ذیل در فایل global.asax.cs برنامه عمل کرد:
using System;
using System.Web.Mvc;
using System.Web.Routing;
using jqGrid05.CustomModelBinders;

namespace jqGrid05
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());
            ModelBinders.Binders.Add(typeof(decimal), new DecimalBinder());
        }
    }
}


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid05.zip
 
مطالب
جایگزین کردن jQuery با JavaScript خالص - قسمت پنجم - درخواست‌های Ajax
AJAX و یا «Asynchronous JavaScript and XML» قابلیتی است که توسط web API جاوا اسکریپتی مرورگرها برای دریافت و یا به روز رسانی اطلاعات، بدون بارگذاری مجدد و کامل صفحه، ارائه می‌شود. این قابلیت اولین بار در سال 1999 توسط مایکروسافت با ارائه‌ی مرورگر IE 5.0 و معرفی شیء XMLHTTP که توسط یک ActiveX control ارائه می‌شد، میسر گردید و این روزها توسط استاندارد XMLHttpRequest در تمام مرورگرها قابل استفاده است. این استاندارد نیز مدتی است که توسط Fetch API مرورگرهای جدید جایگزین شده‌است (پس از 15 سال از ارائه‌ی استاندارد XMLHttpRequest) و در این لحظه در تمام مرورگرهای غالب وب پشتیبانی می‌شود.
در ادامه روش‌های مختلف ارسال درخواست‌های Ajax را توسط jQuery و همچنین معادل‌های XMLHttpRequest و Fetch API آن‌ها بررسی می‌کنیم.


ارسال درخواست‌های GET

توسط استاندارد جدید Fetch API 
  توسط XMLHttpRequest استاندارد  توسط jQuery 
 fetch('/my/name').then(function(response) {
  if (response.ok) {
   return response.text();
  }
  else {
   throw new Error();
  }
 }).then(
  function success(name) {
  console.log('my name is ' + name);
  },
  function failure() {
  console.error('Name request failed!');
  }
 );
 var xhr = new XMLHttpRequest();
  xhr.open('GET', '/my/name');
 xhr.onload = function() {
  if (xhr.status >= 400) {
   console.error('Name request failed!');
  }
  else {
   console.log('my name is ' + xhr.responseText);
  }
 };
 xhr.onerror = function() {
  console.error('Name request failed!');
 };
 xhr.send();
 $.get('/my/name').then(
  function success(name) {
  console.log('my name is ' + name);
  },
  function failure() {
  console.error('Name request failed!');
  }
 );

jQuery برای پشتیبانی از درخواست‌های Ajax، متد ویژه‌ای را به نام ()ajax ارائه می‌کند که برای ارسال درخواست‌هایی از هر نوع، مانندGET، POST و غیره کاربرد دارد. همچنین برای بعضی از نوع‌ها، متدهای کوتاه‌تری را مانند ()get و ()post نیز در اختیار برنامه نویس قرار می‌دهد. جاوا اسکریپت خالص و Web API مرورگرها نیز دو شیء XMLHttpRequest و fetch را برای ارسال درخواست‌های غیرهمزمان، ارائه می‌کند. XMLHttpRequest در تمام مرورگرهای قدیمی و جدید پشتیبانی می‌شود، اما fetch API مدتی است که در غالب مرورگرهای امروزی در دسترس است. در جدول فوق روش ارسال درخواست‌های Ajax از نوع GET را توسط این سه روش مشاهده می‌کنید.
در این مثال‌ها درخواستی به آدرس my/name سمت سرور ارسال شده و انتظار می‌رود که یک plaintext حاوی متن کاربر بازگشت داده شود که در نهایت در console لاگ می‌شود.
- در حالت استفاده‌ی از jQuery در صورت بازگشت موفقیت آمیز پاسخی از طرف سرور، متد success و در غیراینصورت متد failure اجرا می‌شود. باید درنظر داشت که متد ajax جی‌کوئری، چیزی بیشتر از یک محصور کننده‌ی اشیاء XMLHttpRequest نیست.
- در حالت کار با XMLHttpRequest باید اندکی بیشتر تایپ کرد؛ اما اصول کار یکی است. در اینجا onload زمانی فراخوانی می‌شود که پاسخی از سرور دریافت شده و عملیات خاتمه یافته‌است؛ هرچند این پاسخ می‌تواند یک خطا نیز باشد. به همین جهت باید status آن‌را بررسی کرد. اگر رخ‌داد onerror فراخوانی شد، یعنی درخواست، در سطوح بسیار پایین آن مانند بروز یک خطای CORS با شکست مواجه شده‌است.
همانطور که مشاهده می‌کنید در حالت کار با XMLHttpRequest جاوا اسکریپت از اشیاء Promise پشتیبانی نمی‌کند که این کمبود با معرفی fetch API برطرف شده‌است که نمونه‌ای از آن‌را با متد then متصل به fetch مشاهده می‌کنید؛ دقیقا مشابه متد ajax جی‌کوئری که آن نیز یک Promise را بازگشت می‌دهد.
تفاوت Promise جی‌کوئری با fetch API در این است که جی‌کوئری در صورتیکه یک status code بیانگر خطا را دریافت کند، قسمت failure را اجرا می‌کند؛ اما fetch API مانند اشیاء XMLHttpRequest تنها در صورت بروز خطاهای سطح پایین درخواست، این متد را فراخوانی خواهد کرد. هرچند اگر در اینجا response.ok نبود، می‌توان با صدور یک استثناء به رفتاری شبیه به jQuery رسید و قسمت then failure را به صورت خودکار اجرا کرد.


ارسال درخواست‌های Ajax از نوع POST ، PUT و DELETE

در اینجا اطلاعاتی با MIME type از نوع plaintext به سمت سرور ارسال می‌شود. جهت سهولت توضیح و تمرکز بر روی قسمت‌های مهم آن، بخش مدیریت پاسخ آن حذف شده‌است و این مورد دقیقا با مثال قبلی که در مورد درخواست‌های از نوع GET بود، یکی است.
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
توسط jQuery 
 fetch('/user/name', {
  method: 'POST',
  body: 'some data'
 });
 var xhr = new XMLHttpRequest();
 xhr.open('POST', '/user/name');
 xhr.send('some data');
 $.ajax({
  method: 'POST',
  url: '/user/name',
  contentType: 'text/plain',
  data: 'some data'
 });
در حالت ارسال اطلاعات به سرور با متد POST، نیاز است contentType متد ajax جی‌کوئری حتما ذکر شود. در غیراینصورت آن‌را به application/x-www-form-urlencoded تنظیم می‌کند که ممکن است الزاما مقداری نباشد که مدنظر ما است. در اینجا بدنه‌ی درخواست به خاصیت data انتساب داده می‌شود.
اگر از شیء XMLHttpRequest استفاده شود، Content-Type آن به صورت پیش‌فرض به text/plain تنظیم شده‌است. در اینجا بدنه‌ی درخواست به متد send ارسال می‌شود.
متد fetch نیز همانند شیء XMLHttpRequest دارای Content-Type پیش‌فرض از نوع text/plain است. در اینجا بدنه‌ی درخواست به خاصیت body انتساب داده می‌شود.

درخواست‌های از نوع POST عموما برای ایجاد رکوردی جدید در سمت سرور مورد استفاده قرار می‌گیرند و از درخواست‌های PUT بیشتر برای به روز رسانی مقادیر موجود یک رکورد کمک گرفته می‌شود. درخواست‌های از نوع PUT نیز دقیقا مانند درخواست‌های از نوع POST در اینجا مدیریت می‌شوند و در هر سه حالت، متد ارسال اطلاعات، به مقدار PUT تنظیم خواهد شد:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 توسط jQuery 
 fetch('/user/1', {
  method: 'PUT',
  body: //record including new mobile number 
 });
 var xhr = new XMLHttpRequest();
 xhr.open('PUT', '/user/1');
 xhr.send(/* record including new data */);
 $.ajax({
  method: 'PUT',
  url: '/user/1',
  contentType: 'text/plain',
  data: //record including new data
 });
درخواست‌های از نوع DELETE نیز مانند قبل بوده و تنها تفاوت آن، نداشتن بدنه‌ی درخواست است:
 توسط استاندارد جدید Fetch API   توسط XMLHttpRequest استاندارد  توسط jQuery 
 fetch('/user/1', {method: 'DELETE'});
 var xhr = new XMLHttpRequest();
 xhr.open('DELETE', '/user/1');
 xhr.send();
 $.ajax('/user/1', {method: 'DELETE'});


مدیریت Encoding درخواست‌های Ajax

در مثال‌های قبل، اطلاعاتی از نوع text/plain را به سمت سرور ارسال کردیم که به آن encoding type نیز گفته می‌شود. برای تکمیل بحث می‌توان حالت‌های دیگری مانند application/x-www-form-urlencoded، application/json و یا multipart/form-data را که در برنامه‌های کاربردی زیاد مورد استفاده قرار می‌گیرند، بررسی کرد.

کار با URL Encoding

عموما URL encoding در دو قسمت آدرس درخواستی به سرور و یا حتی بدنه‌ی درخواست ارسالی تنظیم می‌شود. MIME type آن نیز application/x-www-form-urlencoded است و اطلاعات آن شامل یکسری جفت کلید/مقدار است. برای متدهای ارسال از نوع GET و DELETE، اطلاعات آن در انتهای آدرس درخواستی و برای سایر حالات در بدنه‌ی درخواست ذکر می‌شوند.
در جی‌کوئری با استفاده از متد param آن می‌توان یک شیء جاوا اسکریپتی را به معادل URL-encoded string آن‌ها تبدیل کرد:
$.param({
  key1: 'some value',
  'key 2': 'another value'
});
با این خروجی encode شده:
 key1=some+value&key+2=another+value
البته باید دقت داشت زمانیکه از متد ajax جی‌کوئری استفاده می‌شود، دیگر نیازی به استفاده‌ی مستقیم از متد param نیست:
 $.ajax({
  method: 'POST',
  url: '/user',
  data: {
  name: 'VahidN',
  address: 'Address 1',
  phone: '555-555-5555'
  }
 });
در اینجا یک شیء جاوا اسکریپتی معمولی به خاصیت data آن نسبت داده شده‌است که در پشت صحنه در حین ارسال به سرور، چون Content-Type پیش‌فرض (و ذکر نشده‌ی در اینجا) دقیقا همان application/x-www-form-urlencoded است، به صورت خودکار تبدیل به یک URL-encoded string می‌شود.

برخلاف جی‌کوئری در حین کار با روش‌های جاوا اسکریپتی خالص این encoding باید به صورت دستی و صریحی انجام شود. برای این منظور دو متد استاندارد encodeURI و encodeURIComponent در جاوا اسکریپت مورد استفاده قرار می‌گیرند. هدف متد encodeURI اعمال آن بر روی یک URL کامل است و یا کلید/مقدارهای جدا شده‌ی توسط & مانند first=Vahid&last=N. اما متد encodeURIComponent صرفا جهت اعمال بر روی یک تک مقدار طراحی شده‌است.
به این ترتیب معادل جاوا اسکریپتی قطعه کد جی‌کوئری فوق به صورت زیر است:
 var xhr = new XMLHttpRequest();
 var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555');
 xhr.open('POST', '/user');
 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
 xhr.send(data);
که در آن data توسط encodeURI تبدیل به یک رشته‌ی URL-encoded شده و سپس با ذکر صریح Content-Type به سمت سرور ارسال می‌شود.
روش انجام اینکار توسط fetch API به صورت زیر است:
   var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555');
  fetch('/user', {
  method: 'POST',
  headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  body: data
 });
معادل متد param جی‌کوئری با جاوا اسکریپت خالص به صورت زیر است؛ برای تبدیل یک شیء جاوا اسکریپتی به معادل URL-encoded string آن:
function param(object) {
    var encodedString = '';
    for (var prop in object) {
        if (object.hasOwnProperty(prop)) {
            if (encodedString.length > 0) {
                encodedString += '&';
            }
            encodedString += encodeURI(prop + '=' + object[prop]);
        }
    }
    return encodedString;
}


کار با JSON Encoding

در عمل JSON نمایش رشته‌ای یک شیء جاوا اسکریپتی است و هدف آن سهولت نقل و انتقالات این اشیاء به سرور و برعکس است. برخلاف حالت application/x-www-form-urlencoded که اطلاعات آن مسطح است، حالت application/json امکان ارسال اطلاعات سلسله مراتبی را نیز میسر می‌کند (مانند مثال زیر که phone آن دیگر مسطح نیست و خود آن نیز یک شیء جاوا اسکریپتی است).
 در جی‌کوئری برای ارسال اشیاء جاوا اسکریپتی JSON Encoded به سمت سرور از روش زیر استفاده می‌شود:
 $.ajax({
  method: 'POST',
  url: '/user',
  contentType: 'application/json',
  data: JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
   home: '555-555-5555',
   mobile: '444-444-4444'
  }
  });
 });
در اینجا نسبت به مثال قبلی، ذکر Content-Type ضروری بوده و همچنین data نیز باید به صورت دستی encode شود. برای این منظور می‌توان از متد استاندارد JSON.stringify استفاده کرد که از زمان IE 8.0 به بعد در تمام مرورگرها پشتیبانی می‌شود.
پیاده سازی همین مثال با جاوا اسکریپت خالص و XMLHttpRequest استاندارد به صورت زیر است:
  var xhr = new XMLHttpRequest();
 var data = JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
    home: '555-555-5555',
    mobile: '444-444-4444'
   }
   });
 xhr.open('POST', '/user');
 xhr.setRequestHeader('Content-Type', 'application/json');
 xhr.send(data);
که در اینجا نیز Content-Type به صورت صریحی ذکر و از متد JSON.stringify برای encode دستی اطلاعات کمک گرفته شده‌است.
در این حالت اگر خروجی سرور نیز JSON باشد، روش دریافت و پردازش آن به صورت زیر است:
 var xhr = new XMLHttpRequest();
 xhr.open('GET', '/user/1');
 xhr.onload = function() {
  var user = JSON.parse(xhr.responseText);
  // do something with this user JavaScript object 
 };
 xhr.send();
رخ‌داد onload، پس از پایان درخواست فراخوانی می‌شود. در اینجا برای دسترسی به response body می‌توان از خاصیت responseText استفاده کرد و سپس توسط متد JSON.parse این رشته را تبدیل به یک شیء جاوا اسکریپتی نمود.
اگر از مرورگر IE صرفنظر کنیم، تمام مرورگرهای دیگر دارای خاصیتی به نام xhr.response نیز هستند که نیاز به تبدیل و Parse دستی رشته‌ی دریافتی را حذف می‌کند؛ از این جهت که این خاصیت حاوی شیء جاوا اسکریپتی معادل بدنه‌ی response دریافتی از سمت سرور است. البته با این شرط که سرور، Content-Type مساوی application/json را برای response تنظیم کرده باشد.
و روش انجام این عملیات توسط fetch API به صورت زیر است:
 fetch('/user', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
   home: '555-555-5555',
   mobile: '444-444-4444'
  }
  });
 });
که در اینجا نیز هدر Content-Type تنظیم و همچنین از متد JSON.stringify برای تبدیل شیء جاوا اسکریپتی به رشته‌ی معادل، استفاده شده‌است.
و یا اگر بخواهیم اطلاعات JSON دریافتی از سمت سرور را در اینجا پردازش کنیم، روش کار به صورت زیر است:
 fetch('/user/1').then(function(response) {
  return response.json();
  }).then(function(userRecord) {
   // do something with this user JavaScript object 
 });
Fetch API به صورت یک Promise، امکان دسترسی به شیء response را مهیا می‌کند. چون می‌دانیم خروجی آن json است، از متد ()json آن که یک Promise را بازگشت می‌دهد استفاده خواهیم کرد. پس از پایان موفقیت آمیز درخواست، then دوم تعریف شده، اجرا و userRecord ارسالی به آن، همان شیء جاوا اسکریپتی دریافتی از سمت سرور است.
همین مثال را اگر بخواهیم توسط ECMAScript 2016 و Arrow functions آن بازنویسی کنیم، به قطعه کد زیر می‌رسیم:
 fetch('/user/1')
  .then(response => response.json()) // Transform the data into json 
  .then(userRecord => {
  // do something with this userRecord object
  });


کار با Multipart Encoding

نوع دیگری از encoding که بیشتر با فرم‌های HTML بکار می‌رود، multipart/form-data نام دارد:
 <form action="my/server" method="POST" enctype="multipart/form-data">
  <label>First Name:
   <input name="first">
  </label>
 
  <label>Last Name:
   <input name="last">
  </label>
 
  <button>Submit</button>
 </form>
با فعال بودن این نوع encoding، اطلاعات نمونه‌ی فرم فوق به شکل زیر به سمت سرور ارسال می‌شوند:
 -----------------------------1686536745986416462127721994
 Content-Disposition: form-data; name="first"

 Vahid
 -----------------------------1686536745986416462127721994
 Content-Disposition: form-data; name="last"

 N
 -----------------------------1686536745986416462127721994--
از این روش نه فقط برای ارسال اطلاعات کلید/مقدارها و اشیاء جاوا اسکریپتی استفاده می‌شود، بلکه از آن برای ارسال اطلاعات فایل‌های باینری نیز کمک گرفته می‌شود.
روش ارسال اطلاعات با این نوع encoding خاص به سمت سرور توسط متد ajax جی‌کوئری به صورت زیر است:
  var formData = new FormData();
 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');
 
 $.ajax({
  method: 'POST',
  url: '/user',
  contentType: false,
  processData: false,
  data: formData
 });
همانطور که ملاحظه می‌کنید jQuery روش توکاری را برای انجام اینکار نداشته و باید از FormData جاوا اسکریپت یا همان web API مرورگرها به همراه متد ajax آن استفاده کرد. در این حالت اطلاعات به صورت کلید/مقدارها به شیء استاندارد FormData اضافه شده و سپس به سمت سرور ارسال می‌شوند. باید دقت داشت FormData از نگارش‌های پس از IE 9.0 در دسترس است.
در اینجا ذکر processData: false ضروری است. در غیراینصورت jQuery این اطلاعات را به یک URL-encoded string تبدیل می‌کند. همچنین با اعلام contentType: false، جی‌کوئری در کار مرورگر دخالت نمی‌کند. از این جهت که هدر ویژه‌ی این نوع درخواست‌ها توسط خود مرورگر تنظیم می‌شود و برای مثال یک چنین شکلی را دارد:
 multipart/form-data; boundary=—————————1686536745986416462127721994
این عدد انتهای هدر یک unique ID است که جزئی از اطلاعات multipart بوده و توسط مرورگر به انتهای هدر اصلی multipart/form-data اضافه می‌شود.

روش انجام اینکار با XMLHttpRequest و همچنین fetch API به صورت زیر است:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 var formData = new FormData();
 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');

 fetch('/user', {
  method: 'POST',
  body: formData
 });
 var formData = new FormData(),
  xhr = new XMLHttpRequest();

 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');

 xhr.open('POST', '/user');
 xhr.send(formData);
همانطور که مشاهده می‌کنید قسمت استفاده‌ی از FormData استاندارد در اینجا یکسان است و همچنین نیازی به ذکر هدر و یا اطلاعات اضافه‌تری نیست.


آپلود فایل‌ها توسط درخواست‌های Ajax ایی

تنها راه آپلود فایل‌ها در مرورگرهای قدیمی که شامل IE 9.0 هم می‌شود، تعریف المان <"input type="file> در داخل المان <form> و سپس submit مستقیم آن فرم است. برای رفع این مشکل در مرورگرهای پس از IE 9.0 و پشتیبانی از Ajax، جهت آپلود فایل‌ها، استاندارد XMLHttpRequest Level 2 معرفی شده‌است. در این حالت اگر المان <input type=file> در صفحه وجود داشته باشد، روش ارسال Ajax ایی آن به سمت سرور به صورت زیر است:
توسط استاندارد جدید Fetch API 
 توسط XMLHttpRequest استاندارد  توسط jQuery 
var file = document.querySelector(
      'INPUT[type="file"]').files[0];
fetch('/uploads', {
  method: 'POST',
  body: file
  });
 }
var file = document.querySelector(
    'INPUT[type="file"]').files[0],
xhr = new XMLHttpRequest();
xhr.open('POST', '/uploads');
xhr.send(file);

var file = $('INPUT[type="file"]')[0].files[0]; 
$.ajax({
   method: 'POST',
   url: '/uploads',
   contentType: false,
   processData: false,
   data: file
  });
در اینجا روش آپلود یک تک فایل را به سرور، توسط درخواست‌های Ajax ایی مشاهده می‌کنید و توضیحات contentType: false و processData: false آن مانند قبل است تا jQuery این اطلاعات multipart را پیش از ارسال به سرور دستکاری نکند.
یک نکته: اگر نیاز به آپلود بیش از یک فایل را داشتید و همچنین در اینجا نیاز به اطلاعات دیگری مانند سایر فیلدهای فرم نیز وجود داشت، از همان روش تعریف  new FormData و افزودن اطلاعات مورد نیاز به آن استفاده کنید. امکان افزودن شیء file نیز به FormData پیش بینی شده‌است.


دانلود فایل‌ها توسط درخواست‌های Ajax ایی

پیشتر در حین بررسی JSON encoding توسط fetch API از متد ()json برای تبدیل اطلاعات دریافتی از سرور به json و بازگشت آن به صورت یک Promise استفاده کردیم:
  fetch(url) 
  .then((resp) => resp.json()) // Transform the data into json 
  .then(function(data) { 
    // use data object
    }) 
  })
در اینجا علاوه بر ()json، متدهای استاندارد دیگری نیز پیش بینی شده‌اند که همگی یک Promise را بازگشت می‌دهند:
- ()clone یک کپی از response را تهیه می‌کند.
- ()redirect یک response جدید را با URL دیگری ایجاد می‌کند.
- ()arrayBuffer یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء ArrayBuffer تبدیل می‌کند.
- ()formData یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء FormData تبدیل می‌کند.
- ()blob یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء Blob تبدیل می‌کند.
- ()text یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به string تبدیل می‌کند.
- ()json یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء جاوا اسکریپتی تبدیل می‌کند.

در اینجا متدی که می‌تواند برای تبدیل یک byte array بازگشتی از سرور به فایل قابل دریافت در سمت کلاینت مورد استفاده قرار گیرد، متد blob است:
function downloadBlob()
{
       fetch('/Home/InMemoryReport')
   .then(function(response) {
         return response.blob();
   })
   .then(function(xlsxBlob) {
             var a = document.createElement("a");
             document.body.appendChild(a);
             a.style = "display: none";
             let url = window.URL.createObjectURL(xlsxBlob);
             a.href = url;
             a.download = "report.xlsx";
             a.click();
             window.URL.revokeObjectURL(url);
   });
}
فرض کنید مسیر Home/InMemoryReport یک گزارش PDF و یا اکسل را به صورت byte array بازگشت می‌دهد. اولین then نوشته شده، درخواست تبدیل این byte array را پس از پایان response به یک شیء Blob می‌دهد. پس از پایان درخواست، این Promise اجرا شده و نتیجه‌ی آن به صورت خودکار در اختیار then دوم قرار می‌گیرد. در اینجا همانطور که در قسمت قبل نیز بررسی کردیم، یک المان جدید anchor مخفی را ایجاد کرده و به صفحه اضافه می‌کنیم. سپس url آن‌را به این شیء Blob، توسط متد استاندارد window.URL.createObjectURL تنظیم می‌کنیم. با استفاده از متد URL.createObjectURL می‌توان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرس‌هایی به صورت blob:http تولید می‌شوند. نام این فایل را هم توسط ویژگی download این شیء می‌توان تنظیم نمود. در نهایت بر روی آن، متد click را فراخوانی می‌کنیم. با اینکار مرورگر این فایل را به صورت یک فایل دریافت شده‌ی متداول در لیست فایل‌های آن قرار می‌دهد. این روش در مورد تدارک دکمه‌ی دریافت تمام blobهای دریافتی از سرور کاربرد دارد.


ارسال درخواست‌های Ajax به دومین‌های دیگر (CORS)

گاهی از اوقات نیاز است اطلاعاتی را توسط درخواست‌های Ajax، به سروری دیگر در دومینی دیگر ارسال و یا دریافت کرد. هرچند انجام اینکار به صورت مستقیم و خارج از مرورگر بدون مشکل قابل انجام است، اما مرورگرها برای درخواست‌های جاوا اسکریپتی محدودیت «same-origin policy» را اعمال می‌کنند. به این معنا که XMLHttpRequest بین دومین‌ها به صورت پیش‌فرض ممنوع است. برای ارسال درخواست‌های مجاز و از پیش مشخص شده‌ی Ajax بین دومین‌ها، تاکنون دو روش پیش بینی شده‌است:
الف) روش JSONP
«same-origin policy» از شروع ارسال درخواستی به خارج از دومین جاری، جلوگیری می‌کند. هرچند این مورد به درخواست‌های XMLHttpRequest اعمال می‌شود، اما در مورد المان‌هایی از نوع <a>، <img>  و  <script> صادق نیست و آن‌ها محدود به این سیاست امنیتی نیستند. روش «JavaScript Object Notation with Padding» و یا به اختصار JSONP از یکی از همین استثناءها جهت ارسال درخواست‌هایی به سایر دومین‌ها استفاده می‌کند. البته نام این روش کمی غلط انداز است؛ از این جهت که در این فرآیند اصلا JSON ایی مورد استفاده قرار نمی‌گیرد؛ خروجی سرور در این حالت یک تابع جاوا اسکریپتی است و نه JSON.
روش انجام این نوع درخواست‌ها را توسط جی‌کوئری در ذیل مشاهده می‌کنید:
 $.ajax('http://jsonp-aware-endpoint.com/user/1', {
  jsonp: 'callback',
  dataType: 'jsonp'
 }).then(function(response) {
  // handle user info from server 
 });
در این حالت jQuery پس از اجرای تابع دریافتی از سرور، نتیجه‌ی آن‌را در قسمت then، در اختیار مصرف کننده قرار می‌دهد.
انجام اینکار بدون jQuery و در حقیقت کاری که jQuery در پشت صحنه برای ایجاد تگ script انجام می‌دهد، چنین چیزی است:
 window.myJsonpCallback = function(data) {
  // handle user info from server 
 };

 var scriptEl = document.createElement('script');
 scriptEl.setAttribute('src',
  'http://jsonp-aware-endpoint.com/user/1?callback=myJsonpCallback');
 document.body.appendChild(scriptEl);
هرچند این روش هنوز هم در بعضی از سایت‌ها مورد استفاده‌است، در کل بهتر است از آن دوری کنید؛ چون ممکن است به مشکلات امنیتی دیگری ختم شود. این روش بیشتر در روزهای آغازین معرفی محدودیت «same-origin policy» بکار گرفته می‌شد (در زمان IE 7.0).


ب) روش CORS

CORS  و یا Cross Origin Resource Sharing روش مدرن و پذیرفته شده‌ی ارسال درخواست‌های Ajax در بین دومین‌ها است و دارای دو نوع ساده و غیرساده است. نوع ساده‌ی آن به همراه هدر مخصوص Origin است که جهت بیان دومین ارسال کننده‌ی درخواست بکار می‌رود و تنها از encodingهای “text/plain”  و  “application/x-www-form-urlencoded”  پشتیبانی می‌کند. نوع غیرساده‌ی آن که این روزها بیشتر بکار می‌رود، از نوع «preflight» است. Preflight در اینجا به این معنا است که زمانیکه درخواست Ajax ایی را به دومین دیگری ارسال کردید، پیش از ارسال، مرورگر یک درخواست از نوع OPTIONS را به سمت سرور مقصد ارسال می‌کند. در این حالت اگر سرور مجوز مناسبی را صادر کرد، آنگاه مرورگر اصل درخواست را به سمت آن سرور ارسال می‌کند. به همین جهت در این حالت به ازای هر درخواستی که در برنامه ارسال می‌شود، در برگه‌ی network مرورگر، دو درخواست را مشاهده خواهید کرد. درخواست preflight از نوع OPTIONS به صورت خودکار توسط مرورگر مدیریت می‌شود و نیازی به کدنویسی خاصی ندارد.


مدیریت کوکی‌ها در درخواست‌های Ajax

اگر درخواست Ajax ایی را به دومین دیگری ارسال کنید، به صورت پیش‌فرض به همراه کوکی‌های مرتبط نخواهد بود. برای رفع این مشکل نیاز است خاصیت withCredentials را به true تنظیم کنید:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 توسط jQuery 
 fetch('http://someotherdomain.com', {
  method: 'POST',
  headers: {
  'Content-Type': 'text/plain'
  },
  credentials: 'include'
 });
 var xhr = new XMLHttpRequest();
 xhr.open('POST', 'http://someotherdomain.com');
 xhr.withCredentials = true;
 xhr.setRequestHeader('Content-Type', 'text/plain');
 xhr.send('sometext');
 $.ajax('http://someotherdomain.com', {
  method: 'POST',
  contentType: 'text/plain',
  data: 'sometext',
  beforeSend: function(xmlHttpRequest) {
  xmlHttpRequest.withCredentials = true;
  }
 });
 
یک نکته‌ی مهم: در fetch API حتی برای درخواست‌های ساده نیز کوکی‌ها ارسال نمی‌شوند. در این حالت برای کار با دومین جاری و ارسال کوکی‌های کاربر به سمت سرور، باید از تنظیم 'credentials: 'same-origin استفاده کرد؛ زیرا مقدار پیش‌فرض آن omit است.
نظرات مطالب
بهبود امنیت CSP با استفاده از معرفی هش‌های اسکریپت‌های Inline
با سلام،
برای style‌های inline هم باید از WithHash256   استفاده کرد؟
در یکی از پروژه‌ها متاسفانه inline Style زیاد استفاده شده و بنابراین پس از اجرای پروژه تمامی صفحه به هم می‌ریزد. با استفاده از developer tools هش‌های متعددی مانند پیغام خطایی که شما مثال زدید نمایش می‌دهد. بر آن شدم که تمامی هش‌ها را توسط  WithHash256   وارد کنم اما بازهم نتیجه منفی بود و خطای صادر شده تمامی هش هایی که اضافه شده بودند را نیز در لیست می‌آورد. 
بنابراین فعلا برای قسمت style از  UnsafeInline استفاده کردم. آیا برای style‌ها می‌شود کار دیگری انجام داد؟
Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self' <URL> <URL>". Either the 'unsafe-inline' keyword, a hash ('sha256-e89EFOm4894OkHmgoH52lEUIFeaK8fITnql0='), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present.

مطالب
ASP.NET MVC #21

آشنایی با تکنیک‌های Ajax در ASP.NET MVC

اهمیت آشنایی با Ajax، ارائه تجربه‌ کاربری بهتری از برنامه‌های وب، به مصرف کنندگان نهایی آن می‌باشد. به این ترتیب می‌توان درخواست‌های غیرهمزمانی (asynchronous) را با فرمت XML یا Json به سرور ارسال کرد و سپس نتیجه نهایی را که حجم آن نسبت به یک صفحه کامل بسیار کمتر است، به کاربر ارائه داد. غیرهمزمان بودن درخواست‌ها سبب می‌شود تا ترد اصلی رابط کاربری برنامه قفل نشده و کاربر در این بین می‌تواند به سایر امور خود بپردازد. به این ترتیب می‌توان برنامه‌های وبی را که شبیه به برنامه‌های دسکتاپ هستند تولید نمود؛ کل صفحه مرتبا به سرور ارسال نمی‌شود، flickering و چشمک زدن صفحه کاهش خواهد یافت (چون نیازی به ترسیم مجدد کل صفحه نخواهد بود و عموما قسمتی جزئی از یک صفحه به روز می‌شود) یا بدون نیاز به ارسال کل صفحه به سرور، به کاربری خواهیم گفت که آیا اطلاعاتی که وارد کرده است معتبر می‌باشد یا نه (نمونه‌ای از آن‌ را در قسمت Remote validation اعتبار سنجی اطلاعات ملاحظه نمودید).


مروری بر محتویات پوشه Scripts یک پروژه جدید ASP.NET MVC در ویژوال استودیو

با ایجاد هر پروژه ASP.NET MVC‌ جدیدی در ویژوال استودیو، یک سری اسکریپت‌ هم به صورت خودکار در پوشه Scripts آن اضافه می‌شوند. تعدادی از این فایل‌ها توسط مایکروسافت پیاده سازی شده‌اند. برای مثال:
MicrosoftAjax.debug.js
MicrosoftAjax.js
MicrosoftMvcAjax.debug.js
MicrosoftMvcAjax.js
MicrosoftMvcValidation.debug.js
MicrosoftMvcValidation.js

این فایل‌ها از ASP.NET MVC 3 به بعد، صرفا جهت سازگاری با نگارش‌های قبلی قرار دارند و استفاده از آن‌ها اختیاری است. بنابراین با خیال راحت آن‌ها را delete کنید! روش توصیه شده جهت پیاده سازی ویژگی‌های Ajax ایی، استفاده از کتابخانه‌های مرتبط با jQuery می‌باشد؛ از این جهت که 100ها افزونه برای کار با آن توسط گروه وسیعی از برنامه نویس‌ها در سراسر دنیا تاکنون تهیه شده است. به علاوه فریم ورک jQuery تنها منحصر به اعمال Ajax ایی نیست و از آن جهت دستکاری DOM (document object model) و CSS صفحه نیز می‌توان استفاده کرد. همچنین حجم کمی نیز داشته،‌ با انواع و اقسام مرورگرها سازگار است و مرتبا هم به روز می‌شود.

در این پوشه سه فایل دیگر پایه کتابخانه jQuery نیز قرار دارند:
jquery-xyz-vsdoc.js
jquery-xyz.js
jquery-xyz.min.js

فایل vsdoc برای ارائه نهایی برنامه طراحی نشده است. هدف از آن ارائه Intellisense بهتری از jQuery در VS.NET می‌باشد. فایلی که باید به کلاینت ارائه شود، فایل min یا فشرده شده آن است. اگر به آن نگاهی بیندازیم به نظر obfuscated مشاهده می‌شود. علت آن هم حذف فواصل، توضیحات و همچنین کاهش طول متغیرها است تا اندازه فایل نهایی به حداقل خود کاهش پیدا کند. البته این فایل از دیدگاه مفسر جاوا اسکریپت یک مرورگر، فایل بی‌نقصی است!
اگر علاقمند هستید که سورس اصلی jQuery را مطالعه کنید، به فایل jquery-xyz.js مراجعه نمائید.
محل الحاق اسکریپت‌های عمومی مورد نیاز برنامه نیز بهتر است در فایل master page یا layout برنامه باشد که به صورت پیش فرض اینکار انجام شده است.
سایر فایل‌های اسکریپتی که در این پوشه مشاهده می‌شوند، یک سری افزونه عمومی یا نوشته شده توسط تیم ASP.NET MVC برفراز jQuery هستند.

به چهار نکته نیز حین استفاده از اسکریپت‌های موجود باید دقت داشت:
الف) همیشه از متد Url.Content همانند تعاریفی که در فایل Views\Shared\_Layout.cshtml مشاهده می‌کنید،‌ برای مشخص سازی مسیر ریشه سایت، استفاده نمائید. به این ترتیب صرفنظر از آدرس جاری صفحه، همواره آدرس صحیح قرارگیری پوشه اسکریپت‌ها در صفحه ذکر خواهد شد.
ب) ترتیب فایل‌های js مهم هستند. ابتدا باید کتابخانه اصلی jQuery ذکر شود و سپس افزونه‌های آن‌ها.
ج) اگر اسکریپت‌های jQuery در فایل layout سایت تعریف شده‌اند؛ نیازی به تعریف مجدد آن‌ها در View‌های سایت نیست.
د) اگر View ایی به اسکریپت ویژه‌ای جهت اجرا نیاز دارد، بهتر است آن‌را به شکل یک section داخل view تعریف کرد و سپس به کمک متد RenderSection این قسمت را در layout سایت مقدار دهی نمود. مثالی از آن‌را در قسمت 20 این سری مشاهده نمودید (افزودن نمایش جمع هر ستون گزارش).


یک نکته
اگر آخرین به روز رسانی‌های ASP.NET MVC را نیز نصب کرده باشید، فایلی به نام packages.config به صورت پیش فرض به هر پروژه جدید ASP.NET MVC اضافه می‌شود. به این ترتیب VS.NET به کمک NuGet این امکان را خواهد یافت تا شما را از آخرین به روز رسانی‌های این کتابخانه‌ها مطلع کند.


آشنایی با Ajax Helpers توکار ASP.NET MVC

اگر به تعاریف خواص و متدهای کلاس WebViewPage دقت کنیم:

using System;

namespace System.Web.Mvc
{
public abstract class WebViewPage<TModel> : WebViewPage
{
protected WebViewPage();
public AjaxHelper<TModel> Ajax { get; set; }
public HtmlHelper<TModel> Html { get; set; }
public TModel Model { get; }
public ViewDataDictionary<TModel> ViewData { get; set; }
public override void InitHelpers();
protected override void SetViewData(ViewDataDictionary viewData);
}
}

علاوه بر خاصیت Html که وهله‌ای از آن امکان دسترسی به Html helpers توکار ASP.NET MVC را در یک View فراهم می‌کند، خاصیتی به نام Ajax نیز وجود دارد که توسط آن می‌توان به تعدادی متد AjaxHelper توکار دسترسی داشت. برای مثال توسط متد Ajax.ActionLink می‌توان قسمتی از صفحه را به کمک ویژگی‌های Ajax، به روز رسانی کرد.


مثالی در مورد به روز رسانی قسمتی از صفحه به کمک متد Ajax.ActionLink

ابتدا نیاز است فایل Views\Shared\_Layout.cshtml را اندکی ویرایش کرد. برای این منظور سطر الحاق jquery.unobtrusive-ajax.min.js را به فایل layout برنامه اضافه نمائید (اگر این سطر اضافه نشود، متد Ajax.ActionLink همانند یک لینک معمولی رفتار خواهد کرد):

<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
</head>

سپس مدل ساده و منبع داده زیر را نیز به پروژه اضافه کنید:

namespace MvcApplication18.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}

using System.Collections.Generic;

namespace MvcApplication18.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
for (int i = 0; i < 1000; i++)
{
list.Add(new Employee { Id = i + 1, Name = "name " + i });
}
return list;
}
}
}

در ادامه کنترلر جدیدی را به برنامه با محتوای زیر اضافه کنید:

using System.Linq;
using System.Web.Mvc;
using MvcApplication18.Models;

namespace MvcApplication18.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}

[HttpPost] //for IE-8
public ActionResult EmployeeInfo(int? id)
{
if (!Request.IsAjaxRequest())
return View("Error");

if (!id.HasValue)
return View("Error");

var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return View("Error");

return PartialView(viewName: "_EmployeeInfo", model: data);
}
}
}

بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. یک View خالی را به آن اضافه نمائید. همچنین بر روی متد EmployeeInfo کلیک راست کرده و با انتخاب گزینه Add view در صفحه ظاهر شده یک partial view را اضافه نمائید. جهت تمایز بین partial view و view هم بهتر است نام partial view با یک underline شروع شود.
کدهای partial view مورد نظر را به نحو زیر تغییر دهید:

@model MvcApplication18.Models.Employee

<strong>Name:</strong> @Model.Name

سپس کدهای View متناظر با متد Index را نیز به صورت زیر اعمال کنید:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>

<div id="EmployeeInfo">
@Ajax.ActionLink(
linkText: "Get Employee-1 info",
actionName: "EmployeeInfo",
controllerName: "Home",
routeValues: new { id = 1 },
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
})
</div>

<div id="Progress" style="display: none">
<img src="@Url.Content("~/Content/images/loading.gif")" alt="loading..." />
</div>

توضیحات جزئیات کدهای فوق

متد Ajax.ActionLink لینکی را تولید می‌کند که با کلیک کاربر بر روی آن، اطلاعات اکشن متد واقع در کنترلری مشخص، به کمک ویژگی‌های jQuery Ajax دریافت شده و سپس در مقصدی که توسط UpdateTargetId مشخص می‌گردد، بر اساس مقدار InsertionMode،‌ درج خواهد شد (می‌تواند قبل از آن درج شود یا پس از آن و یا اینکه کل محتوای مقصد را بازنویسی کند). HttpMethod آن هم به POST تنظیم شده تا با IE‌ مشکلی نباشد. از این جهت که IE پیغام‌های GET را کش می‌کند و مساله ساز خواهد شد. توسط پارامتر routeValues، آرگومان مورد نظر به متد EmployeeInfo ارسال خواهد شد.
به علاوه یکی دیگر از خواص کلاس AjaxOptions، برای معرفی حالت بروز خطایی در سمت سرور به نام OnFailure در نظر گرفته شده است. در اینجا می‌توان نام یک متد JavaScript ایی را مشخص کرده و پیغام خطای عمومی را در صورت فراخوانی آن به کاربر نمایش داد. یا توسط خاصیت Confirm آن می‌توان یک پیغام را پیش از ارسال اطلاعات به سرور به کاربر نمایش داد.
به این ترتیب در مثال فوق، id=1 به متد EmployeeInfo به صورت غیرهمزمان ارسال می‌گردد. سپس کارمندی بر این اساس یافت شده و در ادامه partial view مورد نظر بر اساس اطلاعات کاربر مذکور، رندر خواهد شد. نتیجه کار، در یک div با id مساوی EmployeeInfo درج می‌گردد (InsertionMode.Replace). متد Ajax.ActionLink از این جهت داخل div تعریف شده‌است که پس از کلیک کاربر و جایگزینی محتوا، محو شود. اگر نیازی به محو آن نبود، آن‌را خارج از div تعریف کنید.
عملیات دریافت اطلاعات از سرور ممکن است مدتی طول بکشد (برای مثال دریافت اطلاعات از بانک اطلاعاتی). به همین جهت بهتر است در این بین از تصاویری که نمایش دهنده انجام عملیات است، استفاده شود. برای این منظور یک div با id مساوی Progress تعریف شده و id آن به LoadingElementId انتساب داده شده است. این div با توجه به display: none آن، در ابتدای امر به کاربر نمایش داده نخواهد شد؛ در آغاز کار دریافت اطلاعات از سرور توسط متد Ajax.ActionLink نمایان شده و پس از خاتمه کار مجددا مخفی خواهد شد.
به علاوه اگر به کدهای فوق دقت کرده باشید، از متد Request.IsAjaxRequest نیز استفاده شده است. به این ترتیب می‌توان تشخیص داد که آیا درخواست رسیده از طرف jQuery Ajax صادر شده است یا خیر. البته آنچنان روش قابل ملاحظه‌ای نیست؛ چون امکان دستکاری Http Headers همیشه وجود دارد؛ اما بررسی آن ضرری ندارد. البته این نوع بررسی‌ها را در ASP.NET MVC بهتر است تبدیل به یک فیلتر سفارشی نمود؛ به این ترتیب حجم if و else نویسی در متدهای کنترلرها به حداقل خواهد رسید. برای مثال:

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    if (filterContext.HttpContext.Request.IsAjaxRequest())
    {
      base.OnActionExecuting(filterContext);
    }
    else
    {
      throw new InvalidOperationException("This operation can only be accessed via Ajax requests");
    }
  }
}

و برای استفاده از آن خواهیم داشت:

[AjaxOnly]
public ActionResult SomeAjaxAction()
{
    return Content("Hello!");
}


در مورد کلمه unobtrusive در قسمت بررسی نحوه اعتبار سنجی اطلاعات، توضیحاتی را ملاحظه نموده‌اید. در اینجا نیز از ویژگی‌های data-* برای معرفی پارامترهای مورد نیاز حین ارسال اطلاعات به سرور، استفاده می‌گردد. برای مثال خروجی متد Ajax.ActionLink به شکل زیر است. به این ترتیب امکان حذف کدهای جاوا اسکریپت از صفحه فراهم می‌شود و توسط یک فایل jquery.unobtrusive-ajax.min.js که توسط تیم ASP.NET MVC تهیه شده، اطلاعات مورد نیاز به سرور ارسال خواهد گردید:
<a data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST" 
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
href="/Home/EmployeeInfo/1">Get Employee-1 info</a>

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


به روز رسانی اطلاعات قسمتی از صفحه بدون استفاده از متد Ajax.ActionLink

الزامی به استفاده از متد Ajax.ActionLink و فایل jquery.unobtrusive-ajax.min.js وجود ندارد. اینکار را مستقیما به کمک jQuery نیز می‌توان به نحو زیر انجام داد:

<a href="#" onclick="LoadEmployeeInfo()">Get Employee-1 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfo() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfo",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning a simple text, not json
complete: function (xhr, status) {
var data = xhr.responseText;
if (status === 'error' || !data) {
//handleError
}
else {
$('#EmployeeInfo').html(data);
}
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}

توضیحات:
توسط متد jQuery.Ajax نیز می‌توان درخواست‌های Ajax ایی خود را به سرور ارسال کرد. در اینجا type نوع http verb مورد نظر را مشخص می‌کند که به POST تنظیم شده است. Url آدرس کنترلر را دریافت می‌کند. البته حین استفاده از متد توکار Ajax.ActionLink،‌ این لینک به صورت خودکار بر اساس تعاریف مسیریابی برنامه تنظیم می‌شود. اما در صورت استفاده مستقیم از jQuery.Ajax باید دقت داشت که با تغییر تعاریف مسیریابی برنامه نیاز است تا این Url نیز به روز شود.
سه سطر بعدی نوع اطلاعاتی را که باید به سرور POST شوند مشخص می‌کند. نوع json است و همچنین contentType آن برای ارسال اطلاعات یونیکد ضروری است. از متد JSON.stringify برای تبدیل اشیاء به رشته کمک گرفته‌ایم. این متد در تمام مرورگرهای امروزی به صورت توکار پشتیبانی می‌شود و استفاده از آن سبب خواهد شد تا اطلاعات به نحو صحیحی encode شده و به سرور ارسال شوند. بنابراین این رشته ارسالی اطلاعات را به صورت دستی تهیه نکنید؛ چون کاراکترهای زیادی هستند که ممکن است مشکل ساز شده و باید پیش از ارسال به سرور اصطلاحا escape یا encode شوند.
متداول است از پارامتر success برای دریافت نتیجه عملیات متد jQuery.Ajax استفاده شود. اما در اینجا از پارامتر complete آن استفاده شده است. علت هم اینجا است که return PartialView یک رشته را بر می‌گرداند. پارامتر success انتظار دریافت خروجی از نوع json را دارد. به همین جهت در این مثال خاص باید از پارامتر complete استفاده کرد تا بتوان به رشته بدون فرمت خروجی بدون مشکل دسترسی پیدا کرد.
به علاوه چون از یک section برای تعریف اسکریپت‌های مورد نیاز استفاده کرده‌ایم، برای درج خودکار آن در هدر صفحه باید قسمت هدر فایل layout برنامه را به صورت زیر مقدار دهی کرد:

@RenderSection("javascript", required: false)



دسترسی به اطلاعات یک مدل در View، به کمک jQuery Ajax

اگر جزئی از صفحه که قرار است به روز شود، پیچیده است، روش استفاده از partial viewها توصیه می‌شود؛ برای مثال می‌توان اطلاعات یک مدل را به همراه یک گرید کامل از اطلاعات، رندر کرد و سپس در صفحه درج نمود. اما اگر تنها به اطلاعات چند خاصیت از مدلی نیاز داشتیم، می‌توان از روش‌هایی با سربار کمتر نیز استفاده کرد. برای مثال متد جدید زیر را به کنترلر Home اضافه کنید:

[HttpPost] //for IE-8        
public ActionResult EmployeeInfoData(int? id)
{
if (!Request.IsAjaxRequest())
return Json(false);

if (!id.HasValue)
return Json(false);

var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return Json(false);

return Json(data);
}

سپس View برنامه را نیز به نحو زیر تغییر دهید:

<a href="#" onclick="LoadEmployeeInfoData()">Get Employee-2 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfoData() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfoData",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning the json data
success: function (result) {
if (result) {
alert(result.Id + ' - ' + result.Name);
}
hideProgress();
},
error: function (result) {
alert(result.status + ' ' + result.statusText);
hideProgress();
}
});
}

function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}

در این مثال، کنترلر برنامه، اطلاعات مدل را تبدیل به Json کرده و بازگشت خواهد داد. سپس می‌توان به اطلاعات این مدل و خواص آن در View برنامه، در پارامتر success متد jQuery.Ajax، مطابق کدهای فوق دسترسی یافت. اینبار چون خروجی کنترلر تعریف شده از نوع Json است، امکان استفاده از پارامتر success فراهم شده است. همه چیز هم در اینجا خودکار است؛ تبدیل یک شیء به Json و برعکس.
یک نکته: اگر نوع متد کنترلر، HttpGet باشد، نیاز خواهد بود تا پارامتر دوم متد بازگشت Json، مساوی JsonRequestBehavior.AllowGet قرار داده شود.


ارسال اطلاعات فرم‌ها به سرور، به کمک ویژگی‌های Ajax

متد کمکی توکار دیگری به نام Ajax.BeginForm در ASP.NET MVC وجود دارد که کار ارسال غیرهمزمان اطلاعات یک فرم را به سرور انجام داده و سپس اطلاعاتی را از سرور دریافت و قسمتی از صفحه را به روز خواهد کرد. مکانیزم کاری کلی آن بسیار شبیه به متد Ajax.ActionLink می‌باشد. در ادامه با تکمیل مثال قسمت جاری، به بررسی این ویژگی خواهیم پرداخت.
ابتدا متد جستجوی زیر را به کنترلر برنامه اضافه کنید:

[HttpPost] //for IE-8        
public ActionResult SearchEmployeeInfo(string data)
{
if (!Request.IsAjaxRequest())
return Content(string.Empty);

if (string.IsNullOrWhiteSpace(data))
return Content(string.Empty);

var employeesList = EmployeeDataSource.CreateEmployees();
var list = employeesList.Where(x => x.Name.Contains(data)).ToList();
if (list == null || !list.Any())
return Content(string.Empty);

return PartialView(viewName: "_SearchEmployeeInfo", model: list);
}

سپس بر روی نام متد کلیک راست کرده و گزینه add view را انتخاب کنید. در صفحه باز شده، گزینه create a stronlgly typed view را انتخاب کرده و قالب scaffolding را هم بر روی list قرار دهید. سپس گزینه ایجاد partial view را نیز انتخاب کنید. نام آن‌را هم _SearchEmployeeInfo وارد نمائید. برای نمونه خروجی حاصل به نحو زیر خواهد بود:

@model IEnumerable<MvcApplication18.Models.Employee>

<table>
<tr>
<th>
Name
</th>
</tr>

@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}

</table>

تا اینجا یک متد جستجو را ایجاد کرده‌ایم که می‌تواند لیستی از رکوردهای کارمندان را بر اساس قسمتی از نام آن‌ها که توسط کاربری جستجو شده است، بازگشت دهد. سپس این اطلاعات را به partial view مورد نظر ارسال کرده و یک جدول را بر اساس آن تولید خواهیم نمود.
اکنون به فایل Index.cshtml مراجعه کرده و فرم Ajax ایی زیر را اضافه نمائید:

@using (Ajax.BeginForm(actionName: "SearchEmployeeInfo",
controllerName: "Home",
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
}))
{
@Html.TextBox("data")
<input type="submit" value="Search" />
}

اینبار بجای استفاده از متد Html.BeginForm از متد Ajax.BeginForm استفاده شده است. به کمک آن اطلاعات Html.TextBox تعریف شده، به کنترلر Home و متد SearchEmployeeInfo آن، بر اساس HttpMethod تعریف شده، ارسال گردیده و نتیجه آن در یک div با id مساوی EmployeeInfo درج می‌گردد. همچنین اگر اطلاعاتی یافت نشد، به کمک متد return Content یک رشته خالی بازگشت داده می‌شود.
متد Ajax.BeginForm نیز از ویژگی‌های data-* برای تعریف اطلاعات مورد نیاز ارسالی به سرور استفاده می‌کند. بنابراین نیاز به سطر الحاق jquery.unobtrusive-ajax.min.js در فایل layout برنامه جهت وفق دادن این اطلاعات unobtrusive به اطلاعات مورد نیاز متد jQuery.Ajax وجود دارد.

<form action="/Home/SearchEmployeeInfo" data-ajax="true" 
data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
id="form0" method="post">
<input id="data" name="data" type="text" value="" />
<input type="submit" value="Search" />
</form>


کتابخانه کمکی «ASP.net MVC Awesome - jQuery Ajax Helpers»
علاوه بر متدهای توکار Ajax همراه با ASP.NET MVC، سایر علاقمندان نیز یک سری Ajax helper را بر اساس افزونه‌های jQuery تدارک دیده‌اند که از آدرس زیر قابل دریافت هستند:
http://awesome.codeplex.com/


افزودن فرم‌ها به کمک jQuery.Ajax و فعال سازی اعتبار سنجی سمت کلاینت

در ASP.NET MVC چون ViewState حذف شده است، امکان تزریق فرم‌های جدید به صفحه یا به روز رسانی قسمتی از صفحه توسط jQuery Ajax به سهولت و بدون دریافت پیغام «viewstate is corrupted» در حین ارسال اطلاعات به سرور، میسر است.
در این حالت باید به یک نکته مهم نیز دقت داشت: «اعتبار سنجی سمت کلاینت دیگر کار نمی‌کند». علت اینجا است که در حین بارگذاری متداول یک صفحه، متد زیر به صورت خودکار فراخوانی می‌گردد:
$.validator.unobtrusive.parse("#{form-id}");

اما با به روز رسانی قسمتی از صفحه، دیگر اینچنین نخواهد بود و نیاز است این فراخوانی را دستی انجام دهیم. برای مثال:

$.ajax
({
url: "/{controller}/{action}/{id}",
type: "get",
success: function(data)
{
$.validator.unobtrusive.parse("#{form-id}");
}
});

//or
$.get('/{controller}/{action}/{id}', function (data) { $.validator.unobtrusive.parse("#{form-id}"); });

شبیه به همین مساله را حین استفاده از Ajax.BeginForm نیز باید مد نظر داشت:

@using (Ajax.BeginForm(
    "Action1",
    "Controller",
    null,
    new AjaxOptions {
        OnSuccess = "onSuccess",
        UpdateTargetId = "result"
    },
    null)
)
{
    <input type="submit" value="Save" />
}

var onSuccess = function(result) {
    // enable unobtrusive validation for the contents
    // that was injected into the <div id="result"></div> node
    $.validator.unobtrusive.parse("#result");
};

در این مثال در پارامتر UpdateTargetId، مجددا یک فرم رندر می‌شود. بنابراین اعتبار سنجی سمت کلاینت آن دیگر کار نخواهد کرد مگر اینکه با مقدار دهی خاصیت OnSuccess، مجددا متد unobtrusive.parse را فراخوانی کنیم.