Install-Package Notificatio.TelegramClient
var api = NotificatioApi.Initialize(" Your Hash_Key"); api.SendMessage("Phone Number", "this is a test Message");
$.ajax({ url: "http://www.api.notificatio.me/v1/user/message", type: "POST", dataType: "json", crossDomaint: true, data: { phoneNumber: "your_phone_number", apiHash: "your_api_hash", message: "your_message" }, cache: false, success: function() { // Your code to handle success message sent }, error: function(error) { // Your code to handle error } });
معرفی کتابخانهی DNTCaptcha.Core
<form asp-controller="Admin" asp-action="CreateBlog" enctype="multipart/form-data" data-ajax="true" data-ajax-method="post" data-ajax-loading="#Progress" data-ajax-complete="onComplete" data-ajax-failure="onFailed" data-ajax-success="onSuccess">
public async Task<IActionResult> CreateBlog(MyViewModel vm, IFormFile attachFile)
اگر در حال تهیه یک سایت چند زبانه هستید و همچنین سری مقالات Globalization در ASP.NET MVC رو دنبال کرده باشید میدانید که با تغییر Culture فایلهای Resource مورد نظر بارگذاری و نوشتههای سایت تغییر میابند ولی با تغییر Culture رفتار اعتبارسنجی در سمت سرور نیز تغییر و اعتبارسنجی بر اساس Culture فعلی سایت انجام میگیرد. بررسی این موضوع را با یک مثال شروع میکنیم.
یک پروژه وب بسازید سپس به پوشه Models یک کلاس با نام ValueModel اضافه کنید. تعریف کلاس به شکل زیر هست:
public class ValueModel { [Required] [Display(Name = "Decimal Value")] public decimal DecimalValue { get; set; } [Required] [Display(Name = "Double Value")] public double DoubleValue { get; set; } [Required] [Display(Name = "Integer Value")] public int IntegerValue { get; set; } [Required] [Display(Name = "Date Value")] public DateTime DateValue { get; set; } }
به سراغ کلاس HomeController بروید و کدهای زیر را اضافه کنید:
[HttpPost] public ActionResult Index(ValueModel valueModel) { if (ModelState.IsValid) { return Redirect("Index"); } return View(valueModel); }
Culture را به fa-IR تغییر میدهیم، برای اینکار در فایل web.config در بخش system.web کد زیر اضافه نمایید:
<globalization culture="fa-IR" uiCulture="fa-IR" />
و در نهایت به سراغ فایل Index.cshtml بروید کدهای زیر رو اضافه کنید:
@using (Html.BeginForm()) { <ol> <li> @Html.LabelFor(m => m.DecimalValue) @Html.TextBoxFor(m => m.DecimalValue) @Html.ValidationMessageFor(m => m.DecimalValue) </li> <li> @Html.LabelFor(m => m.DoubleValue) @Html.TextBoxFor(m => m.DoubleValue) @Html.ValidationMessageFor(m => m.DoubleValue) </li> <li> @Html.LabelFor(m => m.IntegerValue) @Html.TextBoxFor(m => m.IntegerValue) @Html.ValidationMessageFor(m => m.IntegerValue) </li> <li> @Html.LabelFor(m => m.DateValue) @Html.TextBoxFor(m => m.DateValue) @Html.ValidationMessageFor(m => m.DateValue) </li> <li> <input type="submit" value="Submit"/> </li> </ol> }
پرژه را اجرا نمایید و در ٢ تکست باکس اول ٢ عدد اعشاری را و در ٢ تکست باکس آخر یک عدد صحیح و یک تاریخ وارد نمایید و سپس دکمه Submit را بزنید. پس از بازگشت صفحه از سمت سرور در در ٢ تکست باکس اول با این پیامها روبرو میشوید که مقادیر وارد شده نامعتبر میباشند.
اگر پروژه رو در حالت دیباگ اجرا کنیم و نگاهی به داخل ModelState بیاندازیم، میبینیم که کاراکتر جدا کننده قسمت اعشاری برای fa-IR '/' میباشد که در اینجا برای اعداد مورد نظر کاراکتر '.' وارد شده است.
برای فایق شدن بر این مشکل یا باید سمت سرور اقدام کرد یا در سمت کلاینت. در بخش اول راه حل سمت کلاینت را بررسی مینماییم.
در سمت کلاینت برای اینکه کاربر را مجبور به وارد کردن کاراکترهای مربوط به Culture فعلی سایت نماییم باید مقادیر وارد شده را اعتبارسنجی و در صورت معتبر نبودن مقادیر پیام مناسب نشان داده شود. برای اینکار از کتابخانه jQuery Globalize استفاده میکنیم. برای اضافه کردن jQuery Globalize از طریق کنسول nuget فرمان زیر اجرا نمایید:
PM> Install-Package jquery-globalize
پس از نصب کتابخانه اگر به پوشه Scripts نگاهی بیاندازید میبینید که پوشەای با نام jquery.globalize اضافه شده است. درداخل پوشه زیر پوشەی دیگری با نام cultures وجود دارد که در آن Cultureهای مختلف وجود دارد و بسته به نیاز میتوان از آنها استفاده کرد. دوباره به سراغ فایل Index.cshtm بروید و فایلهای جاوا اسکریپتی زیر را به صفحه اضافه کنید:
<script src="~/Scripts/jquery.validate.js"> </script> <script src="~/Scripts/jquery.validate.unobtrusive.js"> </script> <script src="~/Scripts/jquery.globalize/globalize.js"> </script> <script src="~/Scripts/jquery.globalize/cultures/globalize.culture.fa-IR.js"> </script>
در فایل globalize.culture.fa-IR.js کاراکتر جدا کننده اعشاری '.' در نظر گرفته شده است که مجبور به تغییر آن هسیتم. برای اینکار فایل را باز کرده و numberFormat را پیدا کنید و آن را به شکل زیر تغییر دهید:
numberFormat: { pattern: ["n-"], ".": "/", currency: { pattern: ["$n-", "$ n"], ".": "/", symbol: "ریال" } },
و در نهایت کدهای زیر را به فایل Index.cshtml اضافه کنید و برنامه را دوباره اجرا نمایید:
Globalize.culture('fa-IR'); $.validator.methods.number = function(value, element) { if (value.indexOf('.') > 0) { return false; } var splitedValue = value.split('/'); if (splitedValue.length === 1) { return !isNaN(Globalize.parseInt(value)); } else if (splitedValue.length === 2 && $.trim(splitedValue[1]).length === 0) { return false; } return !isNaN(Globalize.parseFloat(value)); }; };
در خط اول Culture را ست مینمایم و در ادامه نحوه اعتبارسنجی را در unobtrusive validation تغییر میدهیم. از آنجایی که برای اعتبارسنجی عدد وارد شده از تابع parseFloat استفاده میشود، کاراکتر جدا کننده قسمت اعشاری قابل قبول برای این تابع '.' است پس در داخل تابع دوباره '/' به '.' تبدیل میشود و سپس اعتبارسنجی انجام میشود از اینرو اگر کاربر '.' را نیز وارد نماید قابل قبول است به همین دلیل با این خط کد if (value.indexOf('.') > 0) وجود نقطه را بررسی میکنیم تا در صورت وجود '.' پیغام خطا نشان داده شود.در خط بعدی بررسی مینماییم که اگر عدد وارد شده اعشاری نباشد از تابع parseInt استفاده نماییم. در خط بعدی این حالت را بررسی مینماییم که اگر کاربر عددی همچون /١٢ وارد کرد پیغام خطا صادر شود.
برای اعتبارسنجی تاریخ شمسی متاسفانه توابع کمکی برای تبدیل تاریخ در فایل globalize.culture.fa-IR.js وجود ندارد ولی اگر نگاهی به فایلهای Culture عربی بیاندازید همه دارای توابع کمکی برای تبدیل تاریج هجری به میلادی هستند به همین دلیل امکان اعتبارسنجی تاریخ شمسی با استفاده از jQuery Globalize میسر نمیباشد. من خودم تعدادی توابع کمکی را به globalize.culture.fa-IR.js اضافه کردەام که از تقویم فارسی آقای علی فرهادی برداشت شده است و با آنها کار اعتبارسنجی را انجام میدهیم. لازم به ذکر است این روش ١٠٠% تست نشده است و شاید راه کاملا اصولی نباشد ولی به هر حال در اینجا توضیح میدهم. در فایل globalize.culture.fa-IR.js قسمت Gregorian_Localized را پیدا کنید و آن را با کدهای زیر جایگزین کنید:
Gregorian_Localized: { firstDay: 6, days: { names: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"], namesAbbr: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"], namesShort: ["ی", "د", "س", "چ", "پ", "ج", "ش"] }, months: { names: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""], namesAbbr: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""] }, AM: ["ق.ظ", "ق.ظ", "ق.ظ"], PM: ["ب.ظ", "ب.ظ", "ب.ظ"], patterns: { d: "yyyy/MM/dd", D: "yyyy/MM/dd", t: "hh:mm tt", T: "hh:mm:ss tt", f: "yyyy/MM/dd hh:mm tt", F: "yyyy/MM/dd hh:mm:ss tt", M: "dd MMMM" }, JalaliDate: { g_days_in_month: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], j_days_in_month: [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] }, gregorianToJalali: function (gY, gM, gD) { gY = parseInt(gY); gM = parseInt(gM); gD = parseInt(gD); var gy = gY - 1600; var gm = gM - 1; var gd = gD - 1; var gDayNo = 365 * gy + parseInt((gy + 3) / 4) - parseInt((gy + 99) / 100) + parseInt((gy + 399) / 400); for (var i = 0; i < gm; ++i) gDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i]; if (gm > 1 && ((gy % 4 == 0 && gy % 100 != 0) || (gy % 400 == 0))) /* leap and after Feb */ ++gDayNo; gDayNo += gd; var jDayNo = gDayNo - 79; var jNp = parseInt(jDayNo / 12053); jDayNo %= 12053; var jy = 979 + 33 * jNp + 4 * parseInt(jDayNo / 1461); jDayNo %= 1461; if (jDayNo >= 366) { jy += parseInt((jDayNo - 1) / 365); jDayNo = (jDayNo - 1) % 365; } for (var i = 0; i < 11 && jDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; ++i) { jDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; } var jm = i + 1; var jd = jDayNo + 1; return [jy, jm, jd]; }, jalaliToGregorian: function (jY, jM, jD) { jY = parseInt(jY); jM = parseInt(jM); jD = parseInt(jD); var jy = jY - 979; var jm = jM - 1; var jd = jD - 1; var jDayNo = 365 * jy + parseInt(jy / 33) * 8 + parseInt((jy % 33 + 3) / 4); for (var i = 0; i < jm; ++i) jDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; jDayNo += jd; var gDayNo = jDayNo + 79; var gy = 1600 + 400 * parseInt(gDayNo / 146097); /* 146097 = 365*400 + 400/4 - 400/100 + 400/400 */ gDayNo = gDayNo % 146097; var leap = true; if (gDayNo >= 36525) /* 36525 = 365*100 + 100/4 */ { gDayNo--; gy += 100 * parseInt(gDayNo / 36524); /* 36524 = 365*100 + 100/4 - 100/100 */ gDayNo = gDayNo % 36524; if (gDayNo >= 365) gDayNo++; else leap = false; } gy += 4 * parseInt(gDayNo / 1461); /* 1461 = 365*4 + 4/4 */ gDayNo %= 1461; if (gDayNo >= 366) { leap = false; gDayNo--; gy += parseInt(gDayNo / 365); gDayNo = gDayNo % 365; } for (var i = 0; gDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap) ; i++) gDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap); var gm = i + 1; var gd = gDayNo + 1; return [gy, gm, gd]; }, checkDate: function (jY, jM, jD) { return !(jY < 0 || jY > 32767 || jM < 1 || jM > 12 || jD < 1 || jD > (Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[jM - 1] + (jM == 12 && !((jY - 979) % 33 % 4)))); }, convert: function (value, format) { var day, month, year; var formatParts = format.split('/'); var dateParts = value.split('/'); if (formatParts.length !== 3 || dateParts.length !== 3) { return false; } for (var j = 0; j < formatParts.length; j++) { var currentFormat = formatParts[j]; var currentDate = dateParts[j]; switch (currentFormat) { case 'dd': if (currentDate.length === 2 || currentDate.length === 1) { day = currentDate; } else { year = currentDate; } break; case 'MM': month = currentDate; break; case 'yyyy': if (currentDate.length === 4) { year = currentDate; } else { day = currentDate; } break; default: return false; } } year = parseInt(year); month = parseInt(month); day = parseInt(day); var isValidDate = Globalize.culture().calendars.Gregorian_Localized.checkDate(year, month, day); if (!isValidDate) { return false; } var grDate = Globalize.culture().calendars.Gregorian_Localized.jalaliToGregorian(year, month, day); var shDate = Globalize.culture().calendars.Gregorian_Localized.gregorianToJalali(grDate[0], grDate[1], grDate[2]); if (year === shDate[0] && month === shDate[1] && day === shDate[2]) { return true; } return false; } },
روال کار در تابع convert به اینصورت است که ابتدا تاریخ وارد شده را بررسی مینماید تا معتبر بودن آن معلوم شود به عنوان مثال اگر تاریخی مثل 1392/12/31 وارد شده باشد و در ادامه برای بررسی بیشتر تاریخ یک بار به میلادی و تاریخ میلادی دوباره به شمسی تبدیل میشود و با تاریخ وارد شده مقایسه میشود و در صورت برابری تاریخ معتبر اعلام میشود. در فایل Index.cshtml کدهای زیر اضافی نمایید:
$.validator.methods.date = function (value, element) { return Globalize.culture().calendars.Gregorian_Localized.convert(value, 'yyyy/MM/dd'); };
برای اعتبارسنجی تاریخ میتوانید از ٢ فرمت استفاده کنید:
١ – yyyy/MM/dd
٢ – dd/MM/yyyy
البته از توابع اعتبارسنجی تاریخ میتوانید به صورت جدا استفاده نمایید و لزومی ندارد آنها را همراه با jQuery Globalize بکار ببرید. در آخر خروجی کار به این شکل است:
در کل استفاده از jQuery Globalize برای اعتبارسنجی در سایتهای چند زبانه به نسبت خوب میباشد و برای هر زبان میتوانید از culture مورد نظر استفاده نمایید. در قسمت دوم این مطلب به بررسی بخش سمت سرور میپردازیم.
تغییرات مورد نیاز جهت فعال سازی ویرایش، حذف و افزودن رکوردهای jqGrid
میخواهیم در بدو نمایش گرید، یک ستون خاص دارای دکمههای ویرایش و حذف ظاهر شوند:
برای اینکار تنها کافی است در انتهای ستونهای تعریف شده، یک ستون خاص را با formatter مساوی actions ایجاد کنیم:
colModel: [ { // سایر ستونها name: 'myac', width: 80, fixed: true, sortable: false, resize: false, formatter: 'actions', formatoptions: { keys: true } } ],
نیاز است تعاریف سایر ستونهایی را که باید قابلیت ویرایش داشته باشند، به نحو ذیل تغییر دهیم:
colModel: [ { name: 'Id', index: 'Id', align: 'right', width: 70, editable: false }, { name: 'Name', index: 'Name', align: 'right', width: 100, editable: true, edittype: 'text', editoptions: { maxlength: 40 }, editrules: { required: true } }, { name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 110, editable: true, edittype: 'select', editoptions: { dataUrl: '@Url.Action("SuppliersSelect","Home")' }, editrules: { required: true } }, { name: 'Category.Id', index: 'Category.Id', align: 'right', width: 110, editable: true, edittype: 'select', editoptions: { dataUrl: '@Url.Action("CategoriesSelect","Home")' }, editrules: { required: true } }, { name: 'Price', index: 'Price', align: 'center', width: 100, formatter: 'currency', formatoptions: { decimalSeparator: '.', thousandsSeparator: ',', decimalPlaces: 2, prefix: '$' }, editable: true, edittype: 'text', editrules: { required: true, number: true, minValue: 0 } }, { name: 'myac', width: 80, fixed: true, sortable: false, resize: false, formatter: 'actions', formatoptions: { keys: true } } ],
- edittype آن بیانگر کنترلی است که باید حین ویرایش آن سلول خاص ظاهر شود. برای مثال اگر text باشد، یک text box و اگر مانند حالت Supplier.Id مساوی select تعریف شود، یک drop down را ظاهر خواهد کرد. برای مقدار دهی این drop down میتوان editoptions و سپس dataUrl آنرا مقدار دهی نمود.
public ActionResult SuppliersSelect() { var list = ProductDataSource.LatestProducts; var suppliers = list.Select(x => new SelectListItem { Text = x.Supplier.CompanyName, Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture) }).ToList(); return PartialView("_SelectPartial", suppliers); }
@model IList<SelectListItem> @Html.DropDownList("srch", Model)
- خاصیت editrules، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شدهاست. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool، خاصیتهای minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است (مثالهای حالت custom را در منابع انتهای بحث میتوانید مطالعه کنید).
- پس از اینکه مشخص شدند کدامیک از ستونها باید قابلیت ویرایش داشته باشند، مسیری که باید اطلاعات نهایی را به سرور ارسال کند، توسط خاصیت editurl مشخص میشود:
$('#list').jqGrid({ caption: "آزمایش چهارم", //url from wich data should be requested url: '@Url.Action("GetProducts","Home")', //url for edit operation editurl: '@Url.Action("EditProduct","Home")',
[HttpPost] public ActionResult EditProduct(Product postData) { //todo: Edit product based on postData return Json(true); }
$('#list').navGrid( '#pager', //enabling buttons { add: true, del: true, edit: false, search: false }, //edit options {}, //add options { width: 'auto', url: '@Url.Action("AddProduct","Home")' }, //delete options { url: '@Url.Action("DeleteProduct","Home")' } );
[HttpPost] public ActionResult DeleteProduct(string id) { //todo: Delete product return Json(true); } [HttpPost] public ActionResult AddProduct(Product postData) { //todo: Add product to repository return Json(true); }
var lastSel; function inlineEdit() { $('input[name=rdEditApproach]').attr('disabled', true); $('#list').navGrid( '#pager', //enabling buttons { add: true, del: true, edit: false, search: false }, //edit options {}, //add options { width: 'auto', url: '@Url.Action("AddProduct","Home")' }, //delete options { url: '@Url.Action("DeleteProduct","Home")' } ); //add onSelectRow event to support inline edit $('#list').setGridParam({ onSelectRow: function (id) { if (id && id != lastSel) { //save changes in row $('#list').saveRow(lastSel, false); lastSel = id; } //trigger inline edit for row $('#list').editRow(id, true); } }); };
- برای فعال سازی خودکار فرمهای افزودن رکوردها و یا ویرایش ردیفهای موجود میتوان از فراخوانی متد formEdit ذیل کمک گرفت:
function formEdit() { $('input[name=rdEditApproach]').attr('disabled', true); $('#list').navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { width: 'auto', checkOnUpdate: true, checkOnSubmit: true, beforeShowForm: function (form) { centerDialog(form, $('#list')); } }, //add options { width: 'auto', url: '@Url.Action("AddProduct","Home")', reloadAfterSubmit: false, checkOnUpdate: true, checkOnSubmit: true, beforeShowForm: function (form) { centerDialog(form, $('#list')); } }, //delete options { url: '@Url.Action("DeleteProduct","Home")', reloadAfterSubmit: false }) .jqGrid('navButtonAdd', "#pager", { caption: "حذف ردیفهای انتخابی", title: "Delete Toolbar", buttonicon: 'ui-icon ui-icon-trash', onClickButton: function () { var idsList = jQuery("#list").jqGrid('getGridParam', 'selarrrow'); alert(idsList); //jQuery("#list").jqGrid('delGridRow',idsList,{reloadAfterSubmit:false}); } }); }; function centerDialog(form, grid) { var dlgDiv = $("#editmod" + grid[0].id); var parentDiv = dlgDiv.parent(); // div#gbox_list var dlgWidth = dlgDiv.width(); var parentWidth = parentDiv.width(); var dlgHeight = dlgDiv.height(); var parentHeight = parentDiv.height(); var parentTop = parentDiv.offset().top; var parentLeft = parentDiv.offset().left; dlgDiv[0].style.top = Math.round( parentTop + (parentHeight-dlgHeight)/2 ) + "px"; dlgDiv[0].style.left = Math.round( parentLeft + (parentWidth-dlgWidth )/2 ) + "px"; }
با کلیک بر روی دکمهی افزودن ردیف جدید، صفحهی ذیل به صورت خودکار تولید میشود:
و با کلیک بر روی دکمهی ویرایش ردیفی انتخاب شده، صفحهی ویرایش آن ردیف به همراه مقادیر سلولهای آن ظاهر خواهند شد:
تنظیمات قسمتهای Add و Delete ویرایش توسط فرمها، با حالت ویرایش داخل ردیفی آنچنان تفاوتی ندارد. فقط در اینجا پیش از نمایش فرم، از متد centerDialog برای نمایش صفحات افزودن و ویرایش رکوردها در وسط صفحه، استفاده شدهاست. توسط checkOnUpdate: true, checkOnSubmit: true سبب خواهیم شد تا اگر کاربر مقادیر موجود فرمی را تغییر دادهاست و سعی در بستن فرم، بدون ذخیره سازی اطلاعات کند، پیغام هشدار دهندهای به او نمایش داده شود که آیا میخواهید تغییرات را ذخیره کنید یا خیر؟
- در انتهای متد formEdit، به کمک متد jqGrid و پارامتر navButtonAdd یک دکمهی سفارشی را نیز اضافه کردهایم. اگر به ستون پس از شمارههای خودکار ردیفها، در سمت راست گرید دقت کنید، یک سری chekbox قابل مشاهده هستند. برای فعال سازی خودکار آنها کافی است خاصیت multiselect گرید به true تنظیم شود. اکنون برای دسترسی به این ستونهای انتخاب شده، میتوان از متد jqGrid به همراه پارامترهای getGridParam و selarrrow استفاده کرد. خروجی آن، لیست idهای ستونها است.
برای مطالعه بیشتر
Common Editing Properties
Inline Editing
Form Editing
Cell Editing
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid04.zip
ارسال به JsonResult
- خروجی باینری قرار گرفته در یک فایل JSON در اصل متنی، هیچ کاربردی ندارد؛ بجز راهی برای انتقال اطلاعات به یک برنامه دیگر (آنهم با سربار بالا).
- اگر کاربر سادهی یک وب سایت قرار است اطلاعاتی را دریافت کند، با استفاده از Ajax نمیتواند فایلی را از سرور دریافت کند. نمونههای مشابه:
- «ایجاد لینک دانلود با استفاده از Handler»
- «مشکل در دریافت خروجی pdf به صورت FlushInBrowser »
- « بازگرداندن Stream فایل از WCF »
راه حل پیشنهادی:
« jquery.fileDownload »