مطالب
اعمال SEO بر روی AngularJS
در این بخش قصد داریم سئو را بر روی یک برنامه‌ی نوشته شده با آنگلولار و Asp.net Mvc اعمال نماییم. انگولار جی‌اس، صفحات را با  استفاده از جاوااسکریپت رندر میکند، ولی اکثر کرالر‌ها نمیتوانند جاوااسکریپت را اجرا کنند و موقع اجرای صفحات سایت ما  فقط یک div خالی را میبینند.
کاری که سرویس Prerender یا فیلتر سفارشی AjaxCrawlable برای ما انجام میدهد، درخواست‌هایی را که از طرف کرالرها آمده‌است را شناسایی میکند و مانند یک مرورگر، با استفاده از phantomjs آنرا اجرا میکند و نتیجه‌ی کامل صفحات ما را به صورت اچ تی ام ال استاتیک برمی‌گرداند.
فانتوم جی اس، موتور اختصاصی برای شبیه سازی مرورگر مبتنی بر Webkit می‌باشد. فانتوم جی اس را میتوانید بر روی ویندوز، لینوکس و مک نصب نمایید. فانتوم جی اس یک Console در اختیار برنامه نویس قرار می‌دهد که می‌توان توسط آن، برنامه‌های جاوااسکریپت را اجرا نمود. همچنین فانتوم جی اس میتواند اسکرین شاتی را نیز از محتوای وب سایت ما فراهم نماید.
 برای اینکه صفحات انگولار جی اس،ایندکس شوند سه مرحله وجود دارند:
1- به کرالر اطلاع دهیم که رندر کردن سایت، توسط جاوااسکریپت انجام میگردد؛ با اضافه کردن متاتگ زیر در اچ تی ام ال سایت (البته در حالت استفاده HTML5 push state ) :
<meta name="fragment" content="!">
<base href="/">
2- بعد از اضافه کردن متاتگ بالا، کرالر درخواست‌های خود را به صورت زیر به سایت ما ارسال میکند:
http://www.example.com/?_escaped_fragment_=
ما در این مثال از  HTML5 push state  استفاده میکنیم. بنابراین لینکی مانند http://www.example.com/user/123 توسط کرالر به صورت زیر دیده میشود: 
http://www.example.com/user/123?_escaped_fragment_=
3- اچ تی ام ال کاملا رندر شده توسط سایت ما به کرالر ارسال گردد.
برای رندر کردن  اچ تی ام ال صفحات، چندین روش وجود دارد:
روش اول: میتوانیم از سرویس‌های آماده‌ای همچون Prerender.io   استفاده کنیم که سرویسهایی را برای زبانهای مختلف ارائه کرده‌اند. باتوجه به توضیحات نمونه استفاده از آن در Asp.Net Mvc کافیست در سایت Prerender.io  ثبت نام کرده، Token را دریافت کنیم و در کانفیگ برنامه قرار دهیم و در کلاس PreStart قطعه کد زیر را قرار دهیم:
DynamicModuleUtility.RegisterModule(typeof(Prerender.io.PrerenderModule));
مثال استفاده از Prerender.io را میتوانید از این آدرس Simple_Demo_Prerender.zip دانلود نمایید.
 
یکی از ابزارهای مناسب تست کردن اینکه صفحات توسط کرالر ایندکس میشوند یا خیر، برنامه screamingfrog میباشد.
در پنل Ajax آن، صفحات ایندکس شده ما نمایش داده میشوند. لینکی مشابه زیر را در مرورگر اجرا کرده، با ViewPage Source کردن آن میتوانید نتیجه اچ تی ام ال کاملا رندر شده را مشاهده نمایید.
http://www.example.com/user/123?_escaped_fragment_=
نسخه رایگان سرویس Prerender.io تا 250 صفحه را پوشش میدهد.

روش دوم: فیلتر سفارشی AjaxCrawlable. در اولین قدم نیاز به نصب فانتوم جی اس داریم:
<package id="PhantomJS" version="1.9.2" targetFramework="net452" />
<package id="phantomjs.exe" version="1.9.2.1" targetFramework="net452" />
فایل phantomjs.exe را از پوشه packages\PhantomJS.1.9.2\tools\phantomjs\phantomjs.exe یافته و در پوشه bin برنامه قرار دهید. با Attribute زیر هر درخواستی که توسط کرالر ارسال گردد به اکشن returnHTML منتقل میگردد.
برای اینکه خطای معروف A potentially dangerous Request.Form value was detected from the client را دریافت نکنیم، کافیست قسمتهایی از آدرس را که شامل کاراکترهای خاصی مانند :// میباشند، از url حذف کنیم و در اکشن returnHtml قسمتهای حذف شده را  به url  اضافه نماییم.
کرالرها  با مشاهده تگ fragment، تمام لینکها را به همراه کوئری استرینگ _escaped_fragment_  میفرستند، که ما در سرور باید آنرا  با رشته خالی جایگزین نماییم.
 public class AjaxCrawlableAttribute : System.Web.Mvc.ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;
            var url = request.Url.ToString();
            if (request.QueryString[Fragment] != null && !url.Contains("HtmlSnapshot/returnHTML"))
            {
                url = url.Replace("?_escaped_fragment_=", string.Empty).Replace(request.Url.Scheme + "://", string.Empty);
                url = url.Split(':')[1];
                filterContext.Result = new RedirectToRouteResult(
                   new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
Route‌های پیشفرض را با کدهای زیر جایگزین میکنیم:
public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
             name: "HtmlSnapshot",
             url: "HtmlSnapshot/returnHTML/{*url}",
             defaults: new { controller = "HtmlSnapshot", action = "returnHTML", url = UrlParameter.Optional });

            routes.MapRoute(
            name: "SPA",
            url: "{*catchall}",
            defaults: new { controller = "Home", action = "Index" })
        }
 اضافه کردن این فیلتر به فیلترهای Asp.net Mvc 
 public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
ایجاد کنترلر HtmlSnapshot و متد returnHTML :
Url را به عنوان آرگومان به تابع page.open فایل جاوااسکریپتی فانتوم میدهیم و بعد از اجرای کامل، خروجی را درViewData قرار میدهیم 
public ActionResult returnHTML(string url)
        {
            var prefix = HttpContext.Request.Url.Scheme + "://" + HttpContext.Request.Url.Host+":";
            url = prefix+url;
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
            var startInfo = new ProcessStartInfo
            {
                Arguments = string.Format("{0} {1}", Path.Combine(appRoot, "Scripts\\seo.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output1 = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output1.Replace("<!-- ngView:  -->", "").Replace("ng-view=\"\"", "");
            return View();
        }
در فایل renderHtml.cshtml
@{ 
    Layout = null;
}
@Html.Raw(ViewBag.result)
ایجاد فایل seo.js  در پوشه Scripts سایت :
در این بخش webpage  را ایجاد میکنیم و آدرس صفحه را از[system.args[1  دریافت کرده و عملیات کپچر کردن را آغاز میکنیم و بعد از تکمیل اطلاعات در سرور، کد زیر اجرا میشود:
console.log(page.content)

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();;
page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () {

});

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        console.log(page.content);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);
صفحه Layout.Cshtml
<!DOCTYPE html>
<html ng-app="appOne">
<head>
    <meta name="fragment" content="!">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta charset="utf-8" />
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    <base href="/">
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
    <script src="~/Scripts/angular/angular.js"></script>
    <script src="~/Scripts/angular/angular-route.js"></script>
    <script src="~/Scripts/angular/angular-animate.js"></script>
    <script>
        angular.module('appOne', ['ngRoute'], function ($routeProvider, $locationProvider) {
            $routeProvider.when('/one', {
                template: "<div>one</div>", controller: function ($scope) {
                }
            })
            .when('/two', {
                template: "<div>two</div>", controller: function ($scope) {
                }
            }).when('/', {
                template: "<div>home</div>", controller: function ($scope) {
                }
            });
            $locationProvider.html5Mode({
                enabled: true
            });
        });
    </script>
</head>
<body>
    <div id="body">
        <section ng-view></section>
        @RenderBody()
    </div>
    <div id="footer">
        <ul class='xoxo blogroll'>
            <li><a href="one">one</a></li>
            <li><a href="two">two</a></li>
        </ul>
    </div>
</body>
</html>

چند نکته تکمیلی:
* فانتوم جی اس قادر به اجرای لینکهای فارسی (utf-8) نمیباشد.
 * اگر خطای syntax error را دریافت کردید ممکن است پروژه شما در مسیری طولانی در روی هارد دیسک قرار داشته باشد.
مطالب
آزمون واحد در MVVM به کمک تزریق وابستگی
یکی از خوبی‌های استفاده از Presentation Pattern‌ها بالا بردن تست پذیری برنامه و در نتیجه نگهداری کد می‌باشد.
MVVM الگوی محبوب برنامه نویسان WPF و Silverlight می‌باشد.  به صرف استفاده از الگوی MVVM نمی‌توان اطمینان داشت که ViewModel کاملا تست پذیری داریم. به عنوان مثلا اگر در ViewModel خود مستقیما DialogBox کنیم یا ارجاعی از View دیگری داشته باشیم نوشتن آزمون‌های واحد تقریبا غیر ممکن می‌شود. قبلا درباره‌ی این مشکلات و راه حل آن مطلب در سایت منتشر شده است : 
در این مطلب قصد داریم سناریویی را بررسی کنیم که ViewModel از Background Worker جهت انجام عملیات مانند دریافت داده‌ها استفاده می‌کند.
Background Worker کمک می‌کند تا اعمال طولانی در یک Thread دیگر اجرا شود در نتیجه رابط کاربری Freeze نمی‌شود.
به این مثال ساده توجه کنید : 
    public class BackgroundWorkerViewModel : BaseViewModel
    {
        private List<string> _myData;

        public BackgroundWorkerViewModel()
        {
            LoadDataCommand = new RelayCommand(OnLoadData);
        }

        public RelayCommand LoadDataCommand { get; set; }

        public List<string> MyData
        {
            get { return _myData; }
            set
            {
                _myData = value;
                RaisePropertyChanged(() => MyData);
            }
        }

        public bool IsBusy { get; set; }

        private void OnLoadData()
        {
            var backgroundWorker = new BackgroundWorker();
            backgroundWorker.DoWork += (sender, e) =>
                             {
                                 MyData = new List<string> {"Test"};
                                 Thread.Sleep(1000);
                             };
            backgroundWorker.RunWorkerCompleted += (sender, e) => { IsBusy = false; };
            backgroundWorker.RunWorkerAsync();
        }
    }

در این ViewModel با اجرای دستور LoadDataCommand داده‌ها از یک منبع داده دریافت می‌شود. این عمل می‌تواند چند ثانیه طول بکشد ، در نتیجه برای قفل نشدن رابط کاربر این عمل را به کمک Background Worker به صورت Async در پشت صحنه انجام شده است.
آزمون واحد این ViewModel اینگونه خواهد بود : 
    [TestFixture]
    public class BackgroundWorkerViewModelTest
    {
        #region Setup/Teardown

        [SetUp]
        public void SetUp()
        {
            _backgroundWorkerViewModel = new BackgroundWorkerViewModel();
        }

        #endregion

        private BackgroundWorkerViewModel _backgroundWorkerViewModel;

        [Test]
        public void TestGetData()
        {
              
            _backgroundWorkerViewModel.LoadDataCommand.Execute(_backgroundWorkerViewModel);

            Assert.NotNull(_backgroundWorkerViewModel.MyData);
            Assert.IsNotEmpty(_backgroundWorkerViewModel.MyData);
        }
    }

با اجرای این آزمون واحد نتیجه با آن چیزی که در زمان اجرا رخ می‌دهد متفاوت است و با وجود صحیح بودن کدها آزمون واحد شکست می‌خورد.
چون Unit Test به صورت همزمان اجرا می‌شود و برای عملیات‌های پشت صحنه صبر نمی‌کند در نتیحه این آزمون واحد شکست می‌خورد.

آزمون واحد شکست خورده

یک راه حل تزریق BackgroundWorker به صورت وابستگی به ViewModel می‌باشد. همانطور که قبلا اشاره شده یکی از مزایای استفاده از تکنیک‌های تزریق وابستگی  سهولت Unit testing می‌باشد.
در نتیجه یک Interface عمومی و 2  پیاده سازی همزمان و غیر همزمان جهت استفاده در برنامه‌ی واقعی و آزمون واحد تهیه می‌کنیم : 
   public interface IWorker
    {
        void Run(DoWorkEventHandler doWork);
        void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete);
    }
جهت استفاده در برنامه‌ی واقعی : 
    public class AsyncWorker : IWorker
    {
        public void Run(DoWorkEventHandler doWork)
        {
            Run(doWork, null);
        }

        public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
        {
            var backgroundWorker = new BackgroundWorker();
            backgroundWorker.DoWork += doWork;
            if (onComplete != null)
                backgroundWorker.RunWorkerCompleted += onComplete;
            backgroundWorker.RunWorkerAsync();
            

        }
    }
جهت اجرا در آزمون واحد : 
    public class SyncWorker : IWorker
    {
        #region IWorker Members

        public void Run(DoWorkEventHandler doWork)
        {
            Run(doWork, null);
        }

        public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
        {
            Exception error = null;
            var doWorkEventArgs = new DoWorkEventArgs(null);
            try
            {
                doWork(this, doWorkEventArgs);
            }
            catch (Exception ex)
            {
                error = ex;
                throw;
            }
            finally
            {
                onComplete(this, new RunWorkerCompletedEventArgs(doWorkEventArgs.Result, error, doWorkEventArgs.Cancel));
            }
        }

        #endregion
    }
در نتیجه ViewModel اینگونه تغییر خواهد کرد :
    public class BackgroundWorkerViewModel : BaseViewModel
    {
        private readonly IWorker _worker;
        private List<string> _myData;

        public BackgroundWorkerViewModel(IWorker worker)
        {
            _worker = worker;
            LoadDataCommand = new RelayCommand(OnLoadData);
        }

        public RelayCommand LoadDataCommand { get; set; }

        public List<string> MyData
        {
            get { return _myData; }
            set
            {
                _myData = value;
                RaisePropertyChanged(() => MyData);
            }
        }

        public bool IsBusy { get; set; }

        private void OnLoadData()
        {
            IsBusy = true; // view is bound to IsBusy to show 'loading' message.

            _worker.Run(
                (sender, e) =>
                    {
                        MyData = new List<string> {"Test"};
                        Thread.Sleep(1000);
                    },
                (sender, e) => { IsBusy = false; });
        }
    }

کلاس مربوطه به آزمون واحد را مطابق با تغییرات ViewModel :
    [TestFixture]
    public class BackgroundWorkerViewModelTest
    {
        #region Setup/Teardown

        [SetUp]
        public void SetUp()
        {
            _backgroundWorkerViewModel = new BackgroundWorkerViewModel(new SyncWorker());
        }

        #endregion

        private BackgroundWorkerViewModel _backgroundWorkerViewModel;

        [Test]
        public void TestGetData()
        {
              
            _backgroundWorkerViewModel.LoadDataCommand.Execute(_backgroundWorkerViewModel);

            Assert.NotNull(_backgroundWorkerViewModel.MyData);
            Assert.IsNotEmpty(_backgroundWorkerViewModel.MyData);
        }
    }

اکنون اگر Unit Test را اجرا کنیم نتیجه اینگونه خواهد بود :




 
مطالب
ASP.NET MVC #9

مروری بر HTML Helpers استاندارد مهیا در ASP.NET MVC

یکی از اهداف وجودی Server controls در ASP.NET Web forms، رندر خودکار HTML است. برای مثال Menu control، TreeView control، GridView و امثال آن کار تولید تگ‌های table، tr و بسیاری موارد دیگر را در پشت صحنه برای ما انجام می‌دهند. اما در ASP.NET MVC، هدف رسیدن به یک markup ساده و تمیز است که 100 درصد بر روی اجزای آن کنترل داشته باشیم و این مورد به صورت ضمنی به این معنا است که در اینجا تمام این HTMLها را باید خودمان تولید کنیم. البته در عمل خیر. یک نمونه از آن‌را در قسمت قبل مشاهده کردیم که چطور می‌توان منطق تولید تگ‌های HTML را کپسوله سازی کرد و بارها مورد استفاده قرار داد. به علاوه فریم ورک ASP.NET MVC نیز به همراه تعدادی HTML helper توکار ارائه شده است مانند CheckBox، ActionLink، RenderPartial و غیره که کار تولید تگ‌های HTML ضروری و پایه را برای ما ساده می‌کنند.
یک مثال:
@Html.ActionLink("About us", "Index", "About")

در اینجا از متدی به نام ActionLink استفاده شده است. شیء Html هم وهله‌ای از کلاس HtmlHelper است که در تمام Viewها قابل دسترسی می‌باشد.
در این متد،‌ اولین پارامتر، متن نمایش داده شده به کاربر را مشخص می‌کند، پارامتر سوم، نام کنترلری است که مورد استفاده قرار می‌گیرد و پارامتر دوم، نام متد یا اکشنی در آن است که فراخوانی خواهد شد (البته هر کدام از این HtmlHelperها به همراه تعداد قابل توجهی overload هم هستند).
زمانیکه این صفحه را رندر کنیم، به خروجی زیر خواهیم رسید:
<a href="/About">About us</a>

در این لینک نهایی خبری از متد Index ایی که معرفی کردیم، نیست. چرا؟
متد ActionLink بر اساس تعاریف پیش فرض مسیریابی برنامه، سعی می‌کند بهترین خروجی را ارائه دهد. مطابق تعاریف پیش فرض برنامه، متد Index، اکشن پیش فرض کنترلرهای برنامه است. بنابراین ضرورتی به ذکر آن ندیده است.

مثالی دیگر:
همان کلاس‌های Product و Products قسمت هفتم را در نظر بگیرید (قسمت بررسی «ساختار پروژه مثال جاری» در آن مثال). همچنین به اطلاعات «نوشتن HTML Helpers ویژه، به کمک امکانات Razor» قسمت هشتم هم نیاز داریم.
اینبار می‌خواهیم بجای نمایش لیست ساده‌ای از محصولات،‌ ابتدا نام آن‌ها را به صورت لینک‌هایی در صفحه نمایش دهیم. در ادامه پس از کلیک کاربر روی یک نام، توضیحات بیشتری از محصول انتخابی را در صفحه‌ای دیگر ارائه نمائیم. کدهای View ما اینبار به شکل زیر تغییر می‌کنند:

@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
<h2>Index</h2>

@GetProductsList(@Model)

توضیحات:
ابتدا یک helper method را تعریف کرده‌ایم و به کمک Html.ActionLink، از نام و شماره محصول، جهت تولید لینک‌های نمایش جزئیات هر یک از محصولات کمک گرفته‌ایم. بنابراین در کنترلر خود نیاز به متد جدیدی به نام Details خواهیم داشت که پارامتری از نوع ProductNumber را دریافت می‌کند. سپس جزئیات این محصول را یافته و در View متناظر با خودش ارائه خواهد داد. پارامتر سومی که در متد ActionLink بکارگرفته شده در اینجا مشاهده می‌کنید، یک anonymously typed object است و توسط آن خواصی را تعریف خواهیم کرد که توسط تعاریف مسیریابی تعریف شده در فایل Global.asax.cs،‌ قابل تفسیر و تبدیل به لینک‌های مرتبط و صحیحی باشد.
اکنون اگر این مثال را اجرا کنیم، اولین لینک تولیدی آن به این شکل خواهد بود:
http://localhost/Home/Details/D123

در اینجا به یک نکته مهم هم باید دقت داشت؛ نام کنترلر به صورت خودکار به این لینک اضافه شده است. بنابراین بهتر است از ایجاد دستی این نوع لینک‌ها خودداری کرده و کار را به متدهای استاندارد فریم ورک واگذار نمود تا بهترین خروجی را دریافت کنیم.
البته اگر الان بر روی این لینک کلیک نمائیم، با پیغام 404 مواجه خواهیم شد. برای تکمیل این مثال، متد Details را به کنترلر تعریف شده اضافه خواهیم کرد:

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

namespace MvcApplication5.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}

public ActionResult Details(string id)
{
var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
if (product == null)
return View("Error");
return View(product);
}
}
}

در متد Details، ابتدا ProductNumber دریافت شده و سپس شیء محصول متناظر با آن، به View این متد، بازگشت داده می‌شود. اگر بر اساس ورودی دریافتی، محصولی یافت نشد، کاربر را به View ایی به نام Error که در پوشه Views/Shared قرار گرفته است، هدایت می‌کنیم.
برای اضافه کردن این View هم بر روی متد کلیک راست کرده و گزینه Add view را انتخاب کنید. چون یک شیء strongly typed از نوع Product را قرار است به View ارسال کنیم (مانند مثال قسمت پنجم)، می‌توان در صفحه باز شده تیک Create a strongly typed view را گذاشت و سپس Model class را از نوع Product انتخاب کرد و در قسمت Scaffold template هم Details را انتخاب نمود. به این ترتیب Code generator توکار VS.NET قسمتی از کار تولید View را برای ما انجام داده و بدیهی است اکنون سفارشی سازی این View تولیدی که قسمت عمده‌ای از آن تولید شده است، کار ساده‌ای می‌باشد:

@model MvcApplication5.Models.Product

@{
ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
<legend>Product</legend>

<div class="display-label">ProductNumber</div>
<div class="display-field">@Model.ProductNumber</div>

<div class="display-label">Name</div>
<div class="display-field">@Model.Name</div>

<div class="display-label">Price</div>
<div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
@Html.ActionLink("Back to List", "Index")
</p>

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


یک نکته:
اگر سعی کنیم متد @helper GetProductsList فوق را در پوشه App_Code، همانند قسمت قبل قرار دهیم، به متد Html.ActionLink دسترسی نخواهیم داشت. چرا؟
پیغام خطایی که ارائه می‌شود این است:
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink' 

به این معنا که در وهله‌ای از شیء System.Web.WebPages.Html.HtmlHelper، به دنبال متد ActionLink می‌گردد. در حالیکه ActionLink مورد نظر به کلاس System.Web.Mvc.HtmlHelper مرتبط می‌شود.
یک راه حل آن به صورت زیر است. به هر متد helper یک آرگومان WebViewPage page را اضافه می‌کنیم (به همراه دو فضای نامی که به ابتدای فایل اضافه می‌شوند)

@using System.Web.Mvc
@using System.Web.Mvc.Html

@using MvcApplication5.Models

@helper GetProductsList(WebViewPage page, List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
سپس برای استفاده از آن در یک View خواهیم داشت:
@MyHelpers.GetProductsList(this, @Model)


متد ActionLink و عبارات فارسی

متد ActionLink آدرس‌های وبی را که تولید می‌کند، URL encoded هستند. برای نمونه اگر رشته‌ای که قرار است به عنوان پارامتر به اکشن متد ما ارسال شود، مساوی Hello World است، آن‌را به صورت Hello%20World در صفحه درج می‌کند. البته این مورد مشکلی را در سمت متدهای کنترلرها ایجاد نمی‌کند، چون کار URL decoding خودکار است. اما ... اگر مقداری که قرار است ارسال شود مثلا «مقدار یک» باشد، آدرس تولیدی این شکل را خواهد داشت:

http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83

و اگر این URL encoding انجام نشود، فقط اولین قسمت قبل از فاصله به متد ارسال می‌گردد.
مرورگرهایی مثل فایرفاکس و کروم، مشکلی با نمایش این لینک به شکل اصلی فارسی آن ندارند (حین نمایش، URL decoding را اعمال می‌کنند). اما اگر مرورگر مثلا IE8 باشد، کاربر دقیقا به همین شکل آدرس‌ها را در نوار آدرس مرورگر خود مشاهده خواهد کرد که آنچنان زیبا نیستند.
حل این مشکل، یک نکته کوچک را به همراه دارد. اگر href تولیدی به شکل زیر باشد:

<li><a href="/Home/Details/مقدار یک">Super Fast Bike</a></li>

IE حین نمایش نهایی آن، آن‌را فارسی نشان خواهد داد. حتی زمانیکه کاربر بر روی آن کلیک کند، به صورت خودکار کاراکترهایی را که لازم است encode نماید، به نحو صحیحی در URL نهایی قابل مشاهده در نوار آدرس‌ها ظاهر خواهد کرد. برای مثال %20 را به صورت خودکار اضافه می‌کند و نگرانی از این لحاظ وجود نخواهد داشت که الان بین دو کلمه فاصله‌ای وجود دارد یا خیر (مرورگرهای دیگر هم دقیقا همین رفتار را در مورد لینک‌های داخل صفحه دارند).
خلاصه این توضیحات متد کمکی زیر است:

@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
@Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}

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

@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
}
</ul>
}

ضمن اینکه باید درنظر داشت کلا این نوع طراحی مشکل دارد! برای مثال فرض کنید که در این مثال، جزئیات، نمایش دهنده مطلب ارسالی در یک بلاگ است. یعنی یک سری عنوان و جزئیات متناظر با آن‌ها در دیتابیس وجود دارند. اگر آدرس مطالب به این شکل باشد http://site/blog/details/text، به این معنا است که این text مساوی است با primary key جدول بانک اطلاعاتی. یعنی وبلاگ نویس سایت شما فقط یکبار در طول عمر این برنامه می‌تواند بگوید «سال نو مبارک!». دفعه‌ی بعد به علت تکراری بودن، مجاز به ارسال پیام تبریک دیگری نخواهد بود! به همین جهت بهتر است طراحی را به این شکل تغییر دهید http://site/blog/details/id/text. در اینجا id همان primary key خواهد بود. Text هم عنوان مطلب. Id به جهت خوشایند بانک اطلاعاتی و Text هم برای خوشایند موتورهای جستجو در این URL قرار دارند. مطابق تعاریف مسیریابی برنامه، Text فقط حالت تزئینی داشته و پردازش نخواهد شد.
از این نوع ترفندها زیاد به کار برده می‌شوند. برای نمونه به URL مطالب انجمن‌های معروف اینترنتی دقت کنید. عموما یک عدد را به همراه text مشاهده می‌کنید. عدد در برنامه پردازش می‌شود، متن هم برای موتورهای جستجو درنظر گرفته شده است.



مطالب
پیاده سازی عملیات CRUD با استفاده از پروتکل OData
OData  یکی از بهترین روش‌های پیاده سازی RESTful Apis میباشد. Open Data Protocol یا به اصطلاح OData یک data access protocol برای وب میباشد که اجازه‌ی تغییر دادن و نوشتن کوئری درون CRUD مربوطه را میدهد (create - read - update - delete). Asp.Net WebApi از ورژن 3 و 4 این پروتکل بطور کامل پشتیبانی می‌نماید.
در این آموزش ما از WebApi 2.2 , OData V4, Ef 6 استفاده کرده‌ایم.
با استفاده از ویژوال استودیو یک پروژه‌ی Asp.Net را از نوع Empty به نام ProductService میسازیم.

هم چنین در قسمت Add folders and core references تیک گزینه‌ی Web Api را نیز فعال مینماییم.


حال احتیاج به نصب پکیج OData با استفاده از nuget package manager داریم. کافیست دستور زیر را در package manager console وارد نماییم.

Install-Package Microsoft.AspNet.Odata

این دستور آخرین ورژن Odata package را از nuget دانلود مینماید.

بعد از نصب شدن OData نیاز به اضافه کردن یک Model داریم. کلاسی را به نام Product در پوشه‌ی Models میسازیم.

کلاس Product.cs حاوی فیلد‌های زیر است.

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }
    }
}

پراپرتی Id، کلید این entity است و کلاینت میتواند کوئری را بر روی entity، به وسیله‌ی key بزند. برای مثال برای گرفتن Product با Id برابر 2، باید این url را ارسال نمود "(2)Products/"

پرواضح است که Id در Database به عنوان Primary key در نظر گرفته شده است.

حال احتیاج به نصب Entity Framework داریم که با ارسال دستور زیر از طریق nuget نصب خواهد شد

Install-Package EntityFramework

بعد از نصب کردن ef نیاز به اضافه کردن connection string در web config داریم.

<connectionStrings>
    <add name="ProductsContext" connectionString="Data Source=.; 
        Initial Catalog=ProductsContext; Integrated Security=True;MultipleActiveResultSets=True;"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

الان میتوانیم کلاس ProductsContext را درون پوشه‌ی Models ایجاد نماییم. محتویات آن را به صورت زیر وارد مینماییم

using System.Data.Entity;
namespace ProductService.Models
{
    public class ProductsContext : DbContext
    {
        public ProductsContext() 
                : base("name=ProductsContext")
        {
        }
        public DbSet<Product> Products { get; set; }
    }
}

درون Constructor کلاس ProductsContext، داریم name=ProductsContext که باید برابر name درون connection string باشد.

حال نیاز به کانفیگ OData داریم. درون پوشه‌ی App_Start و کلاس WebApiConfig.cs محتویات زیر را جایگزین متد register نمایید:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());
    }
}

این کد دو فرآیند زیر را انجام میدهد

1) ساخت Entity Data Model (EDM)

2) اضافه کردن route

EDM یک مدل انتزاعی از data است. EDM برای تولید سند metadata استفاده میشود. کلاس ODataModelBuilder برای ساخت EDM با استفاده از default naming convention میباشد که باعث کاهش کد‌ها میشود. ضمنا کلاس MapODataServiceRoute برای ساخت OData v4 route میباشد. همانگونه که اطلاع دارید، تعریف route برای مدیریت کردن WebApi و چگونگی مسیریابی درخواست‌های http میباشد.

اگر application شما احتیاج به چند OData endpoint داشته باشد، میتوانید برای هر کدام route‌های جدا و همچنین نام یکتایی را برای routeName و routePrefix آن در نظر بگیرید.


اضافه کردن OData Controller

یک Controller، کلاسی برای مدیریت کردن درخواست‌های http میباشد. شما باید Controllerهای مجزایی را برای هر entity set در OData service خود بسازید. در این مقاله Controller مربوط به موجودیت Product را میسازیم.

در Solution Explorer با کلیک راست بر روی پوشه‌ی Controller، کلاسی به نام ProducsController را میسازیم. دقت کنید نام آن حتما باید به Controller ختم شود.

در OData V3 میتوانیم Controller را با استفاده از Scaffolding بسازیم؛ ولی در V4 این ویژگی وجود ندارد!

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

using ProductService.Models;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.OData;
namespace ProductService.Controllers
{
    public class ProductsController : ODataController
    {
        ProductsContext db = new ProductsContext();
        private bool ProductExists(int key)
        {
            return db.Products.Any(p => p.Id == key);
        } 
        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

این مرحله‌ی ابتدایی از پیاده سازی کنترلر میباشد و در قسمت بعد به پیاده سازی CRUD مربوط به آن میپردازیم.


Querying The Entity Set

این 2 متد را به کنترلر خود اضافه مینماییم

[EnableQuery]
public IQueryable<Product> Get()
{
    return db.Products;
}
[EnableQuery]
public SingleResult<Product> Get([FromODataUri] int key)
{
    IQueryable<Product> result = db.Products.Where(p => p.Id == key);
    return SingleResult.Create(result);
}

ویژگی EnableQuery به معنای امکان Query زدن از سمت کلاینت به آن میباشد. FromODataUri نیز برای امکان پاس دادن پارامتر از طریق Uri است.

متد Get بدون پارامتر، قادر به برگرداندن تمامی Product‌ها میباشد و متد Get با پارامتر، قادر به برگرداندن آن Product خاص با استفاده از unique Id است.

در صورت داشتن EnableQuery با استفاده از Query Option هایی مثل filter$ و sort$ و غیره از سمت کلاینت قادر به تغییر دادن کوئری‌های خود هستیم.


Adding and Entity to Entity Set

برای اجازه دادن به کلاینت، جهت اضافه کردن یک Product به دیتابیس، متد Post زیر را اضافه مینماییم

public async Task<IHttpActionResult> Post(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Created(product);
}


Updation an Entity

OData از دو روش متفاوت برای Update کردن یک موجودیت استفاده مینماید.

1) Patch : امکان partial update برای موجودیت مربوطه را فراهم میسازد.

2) Put : موجودیت جدید را به صورت کامل جایگزین مینماید.

مشکل روش Put این است که کلاینت مجبور به ارسال تمامی فیلد‌های مربوطه میباشد. حتی آن هایی که اساسا تغییری نکرده‌اند. بنابراین روش Patch ترجیح داده میشود.

در هر صورت ما به پیاده سازی هر دو روش می‌پردازیم:

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var entity = await db.Products.FindAsync(key);
    if (entity == null)
    {
        return NotFound();
    }
    product.Patch(entity);
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(entity);
}
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    if (key != update.Id)
    {
        return BadRequest();
    }
    db.Entry(update).State = EntityState.Modified;
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(update);
}

در قسمت Patch کنترلر از <Delta<T استفاده میکند که typeی است برای track کردن تغییرات در مدل مربوطه.


Deleting an Entity

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

public async Task<IHttpActionResult> Delete([FromODataUri] int key)
{
    var product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return StatusCode(HttpStatusCode.NoContent);
}

من چند رکورد تستی را به صورت زیر وارد کرده‌ام:

حال پروژه‌ی خود را run نموده و آدرس زیر را وارد نمایید:

http://localhost:YourPort/Products

پاسخ، مجموعه‌ای از entity‌های زیر خواهد بود:

{
  "@odata.context":"http://localhost:4516/$metadata#Products","value":[
    {
      "Id":1,"Name":"Ali","Price":2.00,"Category":"aaa"
    },{
      "Id":2,"Name":"Reza","Price":1.00,"Category":"bbb"
    },{
      "Id":3,"Name":"Ahmad","Price":0.00,"Category":"ccc"
    }
  ]
}

شما میتوانید از هر کدام از فیلتر‌های زیر برای کوئری زدن از کلاینت به سمت سرور استفاده نمایید. بطور مثال هر کدام از اینها پاسخ متفاوت و مربوط به خود را برگشت میدهد:

/Products(2)

Productی با آی دی 2 را بر میگرداند.

/Products?$filter=Id gt 1

محصولی را با آی دی بزرگتر از 1، بر میگرداند.

Products?$select=Name

روی محصولات select زده و فقط فیلد Name آن‌ها را بر میگرداند.

Products?$select=Name,Price

آرایه‌ای از objectهایی با پراپرتی Name و Price را بر میگرداند.

/Products?$top=3

فقط 3 رکورد اول را بر میگرداند.


همانطور که ملاحظه میفرمایید، استفاده از OData باعث کمتر شدن کد‌های سمت سرور و همچنین امکان کوئری زدن از سمت کلاینت به سمت سرور را مهیا می‌کند.

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

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

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

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

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

                    options.MapInboundClaims = true;

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

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

            app.UseRouting();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

مطالب
استفاده از MVVM زمانیکه امکان Binding وجود ندارد

ساده‌ترین تعریف MVVM، نهایت استفاده از امکانات Binding موجود در WPF و Silverlight است. اما خوب، همیشه همه چیز بر وفق مراد نیست. مثلا کنترل WebBrowser را در WPF در نظر بگیرید. فرض کنید که می‌خواهیم خاصیت Source آن‌را در ViewModel مقدار دهی کنیم تا صفحه‌ای را نمایش دهد. بلافاصله با خطای زیر متوقف خواهیم شد:

A 'Binding' cannot be set on the 'Source' property of type 'WebBrowser'.
A 'Binding' can only be set on a DependencyProperty of a DependencyObject.

بله؛ این خاصیت از نوع DependencyProperty نیست و نمی‌توان چیزی را به آن Bind کرد. بنابراین این نکته مهم را توسعه دهنده‌های کنترل‌های WPF و Silverlight همیشه باید بخاطر داشته باشند که اگر قرار است کنترل‌های شما MVVM friendly باشند باید کمی بیشتر زحمت کشیده و بجای تعریف خواص ساده دات نتی، خواص مورد نظر را از نوع DependencyProperty تعریف کنید.
الان که تعریف نشده چه باید کرد؟
پاسخ متداول آن این است: مهم نیست؛ خودمان می‌توانیم این‌کار را انجام دهیم! یک Attached property یا به عبارتی یک Behavior را تعریف و سپس به کمک آن عملیات Binding را میسر خواهیم ساخت. برای مثال:
در این Attached property قصد داریم یک خاصیت جدید به نام BindableSource را جهت کنترل WebBrowser تعریف کنیم:

using System;
using System.Windows;
using System.Windows.Controls;

namespace WebBrowserSample.Behaviors
{
public static class WebBrowserBehaviors
{
public static readonly DependencyProperty BindableSourceProperty =
DependencyProperty.RegisterAttached("BindableSource",
typeof(object),
typeof(WebBrowserBehaviors),
new UIPropertyMetadata(null, BindableSourcePropertyChanged));

public static object GetBindableSource(DependencyObject obj)
{
return (string)obj.GetValue(BindableSourceProperty);
}

public static void SetBindableSource(DependencyObject obj, object value)
{
obj.SetValue(BindableSourceProperty, value);
}

public static void BindableSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WebBrowser browser = o as WebBrowser;
if (browser == null) return;

Uri uri = null;

if (e.NewValue is string)
{
var uriString = e.NewValue as string;
uri = string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString);
}
else if (e.NewValue is Uri)
{
uri = e.NewValue as Uri;
}

if (uri != null) browser.Source = uri;
}
}
}


یک مثال ساده از استفاده‌ی آن هم به صورت زیر می‌تواند باشد:
ابتدا ViewModel مرتبط با فرم برنامه را تهیه خواهیم کرد. اینجا چون یک خاصیت را قرار است Bind کنیم، همینجا داخل ViewModel آن‌را تعریف کرده‌ایم. اگر تعداد آن‌ها بیشتر بود بهتر است به یک کلاس مجزا مثلا GuiModel منتقل شوند.

using System;
using System.ComponentModel;

namespace WebBrowserSample.ViewModels
{
public class MainWindowViewModel : INotifyPropertyChanged
{
Uri _sourceUri;
public Uri SourceUri
{
get { return _sourceUri; }
set
{
_sourceUri = value;
raisePropertyChanged("SourceUri");
}
}

public MainWindowViewModel()
{
SourceUri = new Uri(@"C:\path\arrow.png");
}

#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

در ادامه بجای استفاده از خاصیت Source که قابلیت Binding ندارد، از Behavior سفارشی تعریف شده استفاده خواهیم کرد. ابتدا باید فضای نام آن تعریف شود، سپس BindableSource مرتبط آن در دسترس خواهد بود:

<Window x:Class="WebBrowserSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:WebBrowserSample.ViewModels"
xmlns:B="clr-namespace:WebBrowserSample.Behaviors"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<VM:MainWindowViewModel x:Key="vmMainWindowViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
<WebBrowser B:WebBrowserBehaviors.BindableSource="{Binding SourceUri}" />
</Grid>
</Window>



نمونه مشابه این مورد را در مثال «استفاده از کنترل‌های Active-X در WPF» پیشتر در این سایت دیده‌اید.

بازخوردهای پروژه‌ها
مشکل با نوشتن تابع تجمعی سفارشی(از طریق پیاده سازی IAggregateFunction)
با سلام؛ ضمن تشکر از اینکه تجربیاتتون رو رایگان در اختیار بقیه قرار می‌دید، به شخصه خیلی استفاده کردم.
سوالی داشتم در رابطه با پیاده سازی اینترفیس IAggregateFunction  
من میخوام یه گزارش بنویسم که تو اون ستون آخرش میخواد مانده تجمعی را حساب کنه.
بنابراین میخواستم با پیاده سازی این اینترفیس و همچنین بازنویسی متد ProcessingBoundary آخرین مقدار رو به عنوان خروجی تابع تجمعی ارسال کنم.
public object ProcessingBoundary(IList<SummaryCellData> columnCellsSummaryData)
        {
            if (columnCellsSummaryData == null || !columnCellsSummaryData.Any()) return 0;

            var list = columnCellsSummaryData;
            var lastItem = list.Last();

            return lastItem.CellData.PropertyValue;

        }
در پروژه‌ی دیگه ای این اینترفیس رو پیاده سازی کردم و مشکلی نبود ولی در پروژه جاری
که پروژه ایست با مشخصات:
نوع پروژه : WPF with MVVM
از Prism و Unity هم برای ماژولار شدن استفاده کردم.
خطای زیر رو میده : 
Method 'set_DisplayFormatFormula' in type 'Hezareh.Modules.Accounting.Reporting.ViewModels.MySampleAggregateFunction' from assembly 'Hezareh.Modules.Accounting, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
در صورتی که اینترفیس IAggregateFunction به صورت کامل توسط کلاس  MySampleAggregateFunction پیاده سازی شده است و این هم کد کامل کلاس که همون کد مثال Sum خودتونه، که فقط تابع  ProcessingBoundary رو تغییر دادم. این هم کد کاملش :
 public class MySampleAggregateFunction : IAggregateFunction
    {
        public MySampleAggregateFunction()
        {

        }

        /// <summary>
        /// Fires before rendering of this cell.
        /// Now you have time to manipulate the received object and apply your custom formatting function.
        /// It can be null.
        /// </summary>
        public Func<object, string> DisplayFormatFormula { set; get; }

        #region Fields (6)

        double _groupAvg;
        long _groupRowNumber;
        double _groupSum;
        double _overallAvg;
        long _overallRowNumber;
        double _overallSum;

        #endregion Fields

        #region Properties (2)

        /// <summary>
        /// Returns current groups' aggregate value.
        /// </summary>
        public object GroupValue
        {
            get { return _groupAvg; }
        }

        /// <summary>
        /// Returns current row's aggregate value without considering the presence of the groups.
        /// </summary>
        public object OverallValue
        {
            get { return _overallAvg; }
        }

        #endregion Properties

        #region Methods (4)

        // Public Methods (1) 

        /// <summary>
        /// Fires after adding a cell to the main table.
        /// </summary>
        /// <param name="cellDataValue">Current cell's data</param>
        /// <param name="isNewGroupStarted">Indicated starting a new group</param>
        public void CellAdded(object cellDataValue, bool isNewGroupStarted)
        {
            checkNewGroupStarted(isNewGroupStarted);

            _overallRowNumber++;
            _groupRowNumber++;

            double cellValue;
            if (double.TryParse(cellDataValue.ToSafeString(), NumberStyles.AllowThousands | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out cellValue))
            {
                groupAvg(cellValue);
                overallAvg(cellValue);
            }
        }
        // Private Methods (3) 

        private void checkNewGroupStarted(bool newGroupStarted)
        {
            if (newGroupStarted)
            {
                _groupRowNumber = 0;
                _groupAvg = 0;
                _groupSum = 0;
            }
        }

        private void groupAvg(double cellValue)
        {
            _groupSum += cellValue;
            _groupAvg = _groupSum / _groupRowNumber;
        }

        private void overallAvg(double cellValue)
        {
            _overallSum += cellValue;
            _overallAvg = _overallSum / _overallRowNumber;
        }

        /// <summary>
        /// A general method which takes a list of data and calculates its corresponding aggregate value.
        /// It will be used to calculate the aggregate value of each pages individually, with considering the previous pages data.
        /// </summary>
        /// <param name="columnCellsSummaryData">List of data</param>
        /// <returns>Aggregate value</returns>
        public object ProcessingBoundary(IList<SummaryCellData> columnCellsSummaryData)
        {
            if (columnCellsSummaryData == null || !columnCellsSummaryData.Any()) return 0;

            var list = columnCellsSummaryData;
            var lastItem = list.Last();

            return lastItem.CellData.PropertyValue;

        }
        #endregion Methods

    }
و همچنین این هم تنظیمات ستونی که از این تابع تجمعی میخوام استفاده کنم.
columns.AddColumn(column =>
                {
                    column.PropertyName<VoucherRowPrintViewModel>(x => x.CaclulatedRemains);
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Right);
                    column.IsVisible(true);
                    column.Order(5);
                    column.Width(1.5f);
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.CustomAggregateFunction(new MySampleAggregateFunction());
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.HeaderCell("مانده");
                });
ممنون میشم در صورت امکان کمکم کنید.
مطالب
ایجاد لیستی از کلاسی جنریک
کلاس جنریک زیر را در نظر بگیرید:
public class Column<T>

{
public string Name { set; get; }
public T Data { set; get; }
}

مشکلی که با این نوع کلاس‌ها وجود دارد این است که نمی‌توان مثلا لیست زیر را در مورد آن‌ها تعریف کرد:

IList<Column<T>> myList = new List<Column<T>>();



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

using System.Collections;


namespace Tests
{
public interface IColumn
{
string Name { set; get; }
object Data { set; get; }
}

public class Column<T> : IColumn
{
public string Name { set; get; }

public T Data { set; get; }

object IColumn.Data
{
get { return this.Data; }
set { this.Data = (T)value; }
}
}
}

ابتدا یک اینترفیس عمومی را همانند اعضای کلاس Column تعریف می‌کنیم که در آن بجای T از object‌ استفاده شده است. سپس یک پیاده سازی جنریک از این اینترفیس را ارائه خواهیم داد؛ با این تفاوت که اینبار خاصیت Data مربوط به اینترفیس، به صورت خصوصی و صریح با استفاده از IColumn.Data تعریف می‌شود و نمونه‌ی جنریک هم نام آن، عمومی خواهد بود.
اکنون می‌توان نوشت:

var myList = new List<IColumn>();


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

var myList = new List<IColumn>

{
new Column<int> { Data = 1, Name = "Col1"},
new Column<double> { Data = 1.2, Name = "Col2"}
};

خوب، تا اینجا یک مرحله پیشرفت است.اکنون اگر بخواهیم در این لیست، Data مثلا عنصری را که نامش Col1 است، دریافت کنیم چه باید کرد؟ آن هم نه به شکل object بلکه از نوع T مشخص:

static T GetColumnData<T>(IList<IColumn> list, string name)

{
var column = (Column<T>)Convert.ChangeType(list.Single(s => s.Name.Equals(name)), typeof(Column<T>), null);
return column.Data;
}

و نمونه‌ای از استفاده آن:

int data = GetColumnData<int>(myList, "Col1");


نظرات مطالب
مدیریت سفارشی سطوح دسترسی کاربران در MVC

سلام؛ من هم مشکل ایشون دارم و رول‌ها کش نمیشه و هر بار متد GetRolesForUser اجرا میشود. اون هم نه یکبار بلکه به تعداد زیاد و بار زیادی به دیتابیس وارد می‌کنه. لینک شما هم پیغام زیر و میدهد:

An update is available for the .NET Framework 4.5 in Windows 7 SP1, Windows Server 2008 R2 SP1, Windows Server 2008 SP2, and Windows Vista SP2: January 2013

اما ویندوز من 8 هست و پیغام سایت برای ویندوزهای 2008و7وvista هستش. با گشتن هم دیدم بعضی‌ها خودشون متد را دستی کش کردن

        public override string[] GetRolesForUser(string username)
        {
            var cacheKey = string.Format("{0}:{1}", username, ApplicationName);
            var cache = HttpContext.Current.Cache;
            var roles = cache[cacheKey] as string[];
            if (null == roles)
            {              
                using (var db = new Uas3Context())
                {
                    var u = new EfStudentService(db);
                    roles=u.GetUserRoles(username);
                    cache.Insert(cacheKey, roles, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration);                    
                }
            }
            return roles;
        }

اما این متد و من کش کردم باقی متدها چی؟ کل متدها را همین طوری کش کنم؟ این کد هم به نظرم ناقصه . چون با تغییر دیتابیس به روز نمیشه و صرفا درصورت وجود کش اطلاعات می‌خونه. اصلا چرا در دات نت 4.5 این مشکل هست و چرا بر روی ویندوز 8 این پیغام داده میشود؟

مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
Ajax.BeginForm در ASP.NET MVC از jQuery Ajax برای ارسال مقادیر فرم، به سرور استفاده می‌کند. در این بین اگر یکی از عناصر فرم، المان ارسال فایل به سرور باشد، مقدار دریافتی در سمت سرور نال خواهد بود. مشکل اینجا است که نمی‌توان به کمک Ajax معمولی (یا به عبارتی XMLHttpRequest) فایلی را به سرور ارسال کرد. یا باید از سیلورلایت یا فلش استفاده نمود و یا از مرورگرهایی که XMLHttpRequest Level 2 را پشتیبانی می‌کنند (از IE 10 به بعد مثلا) که امکان Ajax upload توکار به همراه گزارش درصد آپلود را بدون نیاز به فلش یا سیلورلایت، دارند.
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل می‌شود، مقادیر معمولی فرم (تمام المان‌ها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback می‌شود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر می‌رسد. برای انجام اینکار تنها کافی است از افزونه‌ی AjaxFileUpload استفاده کنیم که در ادامه نحوه‌ی استفاده از آن‌را بررسی خواهیم کرد.


پیشنیازها

در ادامه فرض بر این است که افزونه‌ی AjaxFileUpload را دریافت کرده و به فایل 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/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.11.1.min.js"></script>
    <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
    <script src="~/Scripts/ajaxfileupload.js"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>


مدل، کنترلر و View برنامه

مدل برنامه مشخصات یک محصول است:
namespace MVCAjaxFormUpload.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

کنترلر آن از سه متد تشکیل شده‌است:
using System.Threading;
using System.Web;
using System.Web.Mvc;
using MVCAjaxFormUpload.Models;

namespace MVCAjaxFormUpload.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Product product)
        {
            var isAjax = this.Request.IsAjaxRequest();

            return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public ActionResult UploadFiles(HttpPostedFileBase image1, int id)
        {
            var isAjax = this.Request.IsAjaxRequest();
            Thread.Sleep(3000); //شبیه سازی عملیات طولانی
            return Json(new { FileName = "/Uploads/filename.ext" }, "text/html", JsonRequestBehavior.AllowGet);
        }
    }
}
Index اول، کار نمایش صفحه‌ی ارسال اطلاعات را انجام خواهد داد.
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام می‌دهد. HttpPost آن معمولی است.

و کدهای View این مثال نیز به شرح زیر است:
@model MVCAjaxFormUpload.Models.Product
@{
    ViewBag.Title = "Index";
}

<h2>Ajax Form Upload</h2>

@using (Ajax.BeginForm(actionName: "Index",
                       controllerName: "Home",
                       ajaxOptions: new AjaxOptions { HttpMethod = "POST" },
                       routeValues: null,
                       htmlAttributes: new { id = "uploadForm" }))
{
    <label>Name:</label>
    @Html.TextBoxFor(model => model.Name)
    <br />
    <label>Image:</label>
    <br />
    <input type="file" name="Image1" id="Image1" />
    <br />
    <input type="submit" value="Submit" />
    <img id="loading" src="~/Content/Images/loading.gif" style="display:none;">
}

@section Scripts
{
    <script type="text/javascript">
        $(function () {
            $('#uploadForm').submit(function () {
                $("#loading").show();
                $.ajaxFileUpload({
                    url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود
                    secureuri: false,
                    fileElementId: 'Image1', // آی دی المان ورودی فایل
                    dataType: 'json',
                    data: { id: 1, data: 'test' }, // اطلاعات اضافی در صورت نیاز
                    success: function (data, status) {
                        $("#loading").hide();
                        if (typeof (data.FileName) != 'undefined') {
                            alert(data.FileName);
                        }
                    },
                    error: function (data, status, e) {
                        $("#loading").hide();
                        alert(e);
                    }
                });
            });
        });
    </script>
}
فرمی که توسط Ajax.BeginForm تشکیل شده‌است، یک فرم معمولی Ajax ایی است و نکته‌ی جدیدی ندارد. تنها در آن یک المان ارسال فایل قرار گرفته‌است و همچنین Id آن‌را نیز جهت استفاده توسط jQuery مشخص کرده‌ایم.
در ادامه نحوه‌ی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده می‌کنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره می‌کند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحه‌است. از قسمت data جهت ارسال اطلاعات اضافه‌تری به اکشن متد UploadFiles استفاده می‌شود. سایر قسمت‌های آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا می‌شوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمه‌ی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمه‌ی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام می‌شوند.

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