مطالب
ارسال چند درخواست به صورت همزمان به ASP.NET Web API 2.x در AngularJS 1.x

 ما در AngularJs آبجکتی را به نام  q$ داریم که برای اجرای توابع به صورت async مفید است و همچنین در استفاده از مقادیر برگشتی از این درخواست‌ها برای پردازش‌های آینده به ما کمک میکند. برای اطلاعات بیشتر در مورد این سرویس به اینجا مراجعه کنید.

در ادامه ما از تابع ()all از q$ برای ترکیب چند شیء promise داخل یک شیء promise، به منظور صدا زدن چند سرویس به صورت یکجا، استفاده میکنیم.


پیاده سازی ASP.NET Web API


قدم اول : Visual Studio  را بازکنید و یک پروژه empty ASP.NET Web API را مطابق شکل زیر ایجاد کنید.


قدم دوم : در داخل پوشه models، کلاسی را با کدهای زیر ایجاد کنید:
using System.Collections.Generic;
 
namespace NG_Combine_Multiple_Promises.Models
{
    public class Courses
    {
        public int CourseId { get; set; }
        public string CourseName { get; set; }
    }
    public class CourseDatabase : List<Courses>
    {
        public CourseDatabase()
        {
            Add(new Courses() { CourseId=1,CourseName="الکترونیک"});
            Add(new Courses() { CourseId = 2, CourseName = "ریاضی 2" });
            Add(new Courses() { CourseId = 3, CourseName = "طراحی نرم افزار" });
        }
    }
 
    public class Student
    {
        public int StudentId { get; set; }
        public string Name { get; set; }
        public string AcadmicYear { get; set; }
    }
 
    public class StudentDatabase : List<Student>
    {
        public StudentDatabase()
        {
            Add(new Student() {StudentId=101,Name="محمد علوی", 
                               AcadmicYear="اول" });
            Add(new Student() { StudentId = 102, Name = "طاهره موسوی", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 103, Name = "علی عباسی", 
                               AcadmicYear = "سوم" });
            Add(new Student() { StudentId = 104, Name = "جواد نوری", 
                               AcadmicYear = "اول" });
            Add(new Student() { StudentId = 105, Name = "محسن خدایی", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 106, Name = "علی کاظمی", 
                               AcadmicYear = "سوم" });
            Add(new Student() { StudentId = 107, Name = "زهرا مقدم", 
                               AcadmicYear = "اول" });
            Add(new Student() { StudentId = 108, Name = "لاله فکور", 
                               AcadmicYear = "دوم" });
            Add(new Student() { StudentId = 109, Name = "علی نوروزی", 
                               AcadmicYear = "چهارم" });
        }
    }
     
}

کد بالا شامل موجودیت‌های Courses و Student است و کلاس‌های CourseDatabase و StudentDatabase به ترتیب برای ذخیره این موجودیت‌ها است.


قدم سوم :  بسته‌ی نیوگت Microsoft.AspNet.WebAPi.Cors را با استفاده از NuGet Package Manager، به منظور فعال سازی امکان صدا زدن این وب سرویس از دامنه‌های مختلف، به پروژه اضافه کرده و کد زیر را در کلاس WebApiCofig در پوشه App_Start قرار دهید.

config.EnableCors();


قدم چهارم : دو کنترل از نوع Web API 2 Empty را با نام‌های CourseAPIController و StudentAPIController ایجاد کرده و کدهای زیر را در آنها قرار دهید.

CourseAPIController.cs 

[EnableCors("*","*","*")]
public class CourseAPIController : ApiController
{
    [Route("Courses")]
    public IEnumerable<Courses> Get()
    {
        return new CourseDatabase();
    }
}

StudentAPIController.cs 

[EnableCors("*", "*", "*")]
public class StudentAPIController : ApiController
{
    [Route("Students")]
    public IEnumerable<Student> Get()
    {
        return new StudentDatabase();
    }
}


استفاده از Angular $q.all  

قدم اول : پروژه‌ی جدیدی را از نوع Empty ASP.NET در همین solution اضافه کرده و ارجاعات jQuery, Bootstrap و AngularJS را با استفاده از NuGet Package manager  مانند زیر اضافه کنید:

Install-Package jQuery
Install-Package bootstrap
Install-Package angularjs


قدم دوم : پوشه‌ایی را به نام MyScripts ایجاد کرده و درون آن فایل javascript  زیر را با نام logic.js اضافه کنید: 

var app = angular.module('mymodule', []);

//سرویسی برای بازگرداندن لیست دروس
app.service('courseService', function ($http) {
    this.get = function () {
        var response = $http.get("http://localhost:11696/Courses");
        return response;
    };
});

//سرویسی برای بازگرداندن لیست دانشجویان
app.service('studentService', function ($http) {
    this.get = function () {
        var response = $http.get("http://localhost:11696/Students");
        return response;
    };
});

//تعریف کنترلر
app.controller('ctrl', function ($scope, $q, courseService, studentService) {

    $scope.Courses = [];
    $scope.Students = [];
    loadData();

    /**/
    function loadData() {
        var promiseCourse = courseService.get();
        var promiseStudent = studentService.get();

        $scope.combineResult = $q.all([
            promiseCourse,
            promiseStudent
        ]).then(function (resp) {
            $scope.Courses = resp[0].data;
            $scope.Students = resp[1].data;
        });
    }
});
توضیحات: تابعq$.all لیستی از promise هایی را که  توسط courseService.get و studentService.get بدست آمده‌اند، گرفته و وقتی تمام promise‌ها تمام شدند، قسمت ()then آن اجرا میشود. 

قدم سوم:  فایل html ایی را با نام DataPage در پوشه‌ی view با کد زیر ایجاد کنید: 
<!DOCTYPE html>
<html ng-app="mymodule">
<head>
    <title></title>
    <meta charset="utf-8" />
    <link href="../Content/bootstrap.min.css" rel="stylesheet" />
    <script src="../Scripts/jquery-3.2.1.min.js"></script>
    <script src="../Scripts/angular.min.js"></script>
    <script src="../MyScripts/logic.js"></script>
</head>
<body ng-controller="ctrl">
       <div class="container">
        <h1 class="h1">دروس</h1>
        <table class="table table-striped table-bordered table-condensed">
            <thead>
                <tr>
                    <td class="text-center">کد درس</td>
                    <td class="text-center">نام درس</td>
                </tr>
            </thead>
            <tbody>
                <tr ng-repeat="Course in Courses">
                    <td class="text-center">{{Course.CourseId}}</td>
                    <td class="text-center">{{Course.CourseName}}</td>
                </tr>
            </tbody>
        </table>
        <hr />
        <h1 class="h1">دانشجویان</h1>

        <table class="table table-striped table-bordered table-condensed">
            <thead>
                <tr>
                    <td class="text-center">کد دانشجویی</td>
                    <td class="text-center">نام و نام خانوادگی</td>
                    <td class="text-center">سال دانشجویی</td>
                </tr>
            </thead>
            <tbody>
                <tr ng-repeat="Student in Students">
                    <td class="text-center">{{Student.StudentId}}</td>
                    <td class="text-center">{{Student.Name}}</td>
                    <td class="text-center">{{Student.AcadmicYear}}</td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

خروجی نهایی پروژه به شکل زیر خواهد بود:

مطالب
ایجاد BootstrapSwitch در MVC
در  مقاله‌ی قبلی ما بخشی از BootstrapDialog را با استفاده از Reflection  پیاده سازی کردیم. دلیل اینکه پیاده سازی کاملی از آن نداشتیم، متغیر بودن مقادیر و پیچیده‌تر شدن و طولانی تر شدن کد نویسی آن بود که برای آن کد ارزش زیادی نداشت تا وقت بیشتری صرف شود. ولی در اینجا بخاطر پیچیدگی کمتر، به طور کامل از Reflection استفاده شده است.
شیء BootstrapSwitch یک چک باکس است که با استفاده از جی کوئری و استایل‌ها به یک سوئیچ انیمیشنی زیبا تبدیل شده است که خودم به شخصه علاقه زیادی به استفاده‌ی از آن در پروژه‌های شخصی پیدا کرده‌ام. غیر از زیبایی، حس خوبی از کارکرد برنامه میدهد.
فایل‌های موردنیاز را دانلود کرده و آن‌ها را در ابتدای صفحه و با رعایت ترتیب صدا بزنید:
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> 
  <link href="~/content/css/bootstrap-switch.min.css" rel=stylesheet"></link>
<script src="~/Scripts/bootstrap-switch.min.js"></script>
نکته مهم: فایل css شامل دو نسخه هست که یکی از آن برای Bootstrap2 و دیگری برای نسخه 3 آن است که نسبت به آن نسخه، استایل مناسب را انتخاب کنید.
پروژه‌ی اصلی را دریافت کنید و آن را به solution خود اضافه کنید. پروژه به دو بخش اصلی Controls و Models تقسیم می‌شود که بخش مدل آن، برای ایجاد ساختارهای آن و در بخش کنترل، برای ترسیم آن به صورت HtmlHelper به کار می‌رود.
ابتدا قبل از هر چیزی یک شیء از کلاس BootstrapSwitchModel ایجاد کنید و مقادیر دلخواه خود را به خصوصیت‌های آن نسبت دهید:
var model=BootstrapSwitchModel();

//وضعیت فعال بودن و غیرفعال بودن سوئیچ
model.Checked=true;

//اندازه آن
model.Size=BootstrapSize..normal;

//یک انیمیشن ساده موقع سوئیچ کردن دارد
model.Animate=true;

//به چک باکس عادی تبدیل میشود
model.Disabled=true;

//غیرفعال شده و به صورت فقط خواندنی قابل دسترس است
model.Readonly=true;

//رنگ قعال بودن
model.OnColor=BootstrapColor.Success;

//رنگ غیرفعال بودن
model.OffColor=BootstrapColor.Danger;

//متن نمایشی در هنگام فعال بودن
model.OnText="On";

//متن نمایشی در حالت عدم انتخاب
model.OffText="Off";

//بین دو حالت روشن و خاموش نمایش داده میشود
model.label="Public Display";

//تعیین میزان اندازه برچسب  بالا
model.LabelWidth=100;

//سوئیچ به صورت آینه ای معکوس میشود
model.Inverse=false;

//کلاسی جهت تغییر استایل سوئیچ
model.BaseClass="myclass";

//تعیین کلاس برای تگ اصلی پدر
model.WrapperClass="wclass";

//فقط یکی از چند سوئیچ میتواند فعال باشد
model.RadioAllOff=false;

//یک سوئیچ در حالت عادی فقط یکی از
//وضعیت‌ها را نمایش میده ولی در این حالت
//سوئیچ در ابتدا بین این دو وضعیت گیر کرده است
model.Indeterminate=true;

//اندازه سمت چپ و راست سوئیچ
model.HandleWidth=25;
برای ترسیم آن در یک ویو هم به صورت زیر عمل کنید:
@{
var model=BootstrapSwitchModel();
....}

@HTML.BootstrapSwitch("id",model);
برای اطلاع از رویدادهای این کنترل، مستندات آن را مطالعه کنید و از id برای ارتباط با آن استفاده می‌کنند.
مطالب دوره‌ها
مثال - نمایش بلادرنگ تعداد کاربران آنلاین توسط SignalR
راه حل‌های زیادی برای محاسبه و نمایش تعداد کاربران آنلاین یک برنامه وب وجود دارند و عموما مبتنی بر کار با متغیرهای سشن یا Application و امثال آن هستند. این روش‌ها عموما دقیق نبوده و خصوصا قسمت قطع اتصال کاربر را نمی‌توانند دقیقا تشخیص دهند. به همین جهت نیاز به یک تایمر دارند که مثلا اگر در 5 دقیقه قبل، کاربری درخواست مشاهده آدرسی را به سرور ارسال نکرده بود، از لیست کاربران آنلاین حذف شود.
در ادامه بجای این روش‌ها، از SignalR برای محاسبه تعداد کاربران آنلاین و همچنین به روز رسانی بلادرنگ این عدد در سمت کاربر، استفاده خواهیم کرد.

تشخیص اتصال و قطع اتصال کاربران در SignalR

زیر ساخت‌های کلاس Hub موجود در SignalR، دارای متدهای ردیابی اتصال (OnConnected)، قطع اتصال (OnDisconnected) و یا برقراری مجدد اتصال کاربران (OnReconnected) هستند. با بازنویسی این متدها می‌توان به تخمین بسیار دقیقی از تعداد کاربران آنلاین یک سایت رسید.


پیشنیازهای بحث
پیشنیازهای این بحث با مطلب «مثال - نمایش درصد پیشرفت عملیات توسط SignalR» یکی است. برای مثال نحوه دریافت وابستگی‌ها، تنظیمات فایل global.asax و افزودن اسکریپت‌ها، تفاوتی با مثال یاد شده ندارند.


تعریف هاب کاربران آنلاین برنامه

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;

namespace SignalR05.Common
{
    public class OnlineUsersHub : Hub
    {
        public static readonly ConcurrentDictionary<string, string> OnlineUsers = new ConcurrentDictionary<string, string>();

        public void UpdateUsersOnlineCount()
        {
            // آی پی معرف یک کاربر است
            // اما کانکشن آی دی معرف یک برگه جدید در مرورگر او است
            // هر کاربر می‌تواند چندین برگه را به یک سایت گشوده یا ببندد
            var ipsCount = OnlineUsers.Select(x => x.Value).Distinct().Count();
            this.Clients.All.updateUsersOnlineCount(ipsCount);
        }

        /// <summary>
        /// اگر کاربران اعتبار سنجی شده‌اند بهتر است از
        /// this.Context.User.Identity.Name
        /// بجای آی پی استفاده شود
        /// </summary>        
        protected string GetUserIpAddress()
        {
            object environment;
            if (!Context.Request.Items.TryGetValue("owin.environment", out environment))
                return null;

            object serverRemoteIpAddress;
            if (!((IDictionary<string, object>)environment).TryGetValue("server.RemoteIpAddress", out serverRemoteIpAddress))
                return null;

            return serverRemoteIpAddress.ToString();
        }

        public override Task OnConnected()
        {
            var ip = GetUserIpAddress();
            OnlineUsers.TryAdd(this.Context.ConnectionId, ip);
            UpdateUsersOnlineCount();

            return base.OnConnected();
        }

        public override Task OnReconnected()
        {
            var ip = GetUserIpAddress();
            OnlineUsers.TryAdd(this.Context.ConnectionId, ip);
            UpdateUsersOnlineCount();

            return base.OnReconnected();
        }

        public override Task OnDisconnected()
        {
            // در این حالت ممکن است مرورگر کاملا بسته شده باشد
            // یا حتی صرفا یک برگه مرورگر از چندین برگه متصل به سایت بسته شده باشند
            string ip;
            OnlineUsers.TryRemove(this.Context.ConnectionId, out ip);
            UpdateUsersOnlineCount();

            return base.OnDisconnected();
        }
    }
}
کدهای کامل هاب شمارش کاربران آنلاین را در اینجا ملاحظه می‌کنید؛ به همراه نکته‌ی نحوه‌ی دریافت IP کاربر متصل شده به سایت، در یک هاب. کار افزودن یا حذف این کاربران به ConcurrentDictionary تعریف شده، در روال‌های بازنویسی شده اتصال، قطع اتصال و اتصال مجدد یک کاربر، انجام شده است.
در اینجا، هم به IP کاربر و هم به ConnectionId او نیاز است. از این جهت که هر ConnectionId، معرف یک برگه جدید باز شده در مرورگر کاربر است. اگر صرفا IPها را پردازش کنیم، با بسته شدن یکی از چندین برگه مرورگر او که اکنون به سایت متصل هستند، آمار او را از دست خواهیم داد. این کاربر هنوز چندین برگه باز دیگر را دارد که با سایت در ارتباط هستند، اما چون IP او را از لیست حذف کرده‌ایم (در نتیجه بسته شدن یکی از برگه‌ها)، آمار کلی شخص را نیز از دست خواهیم داد. بنابراین هر دوی IP و ConnectionIdها باید پردازش شوند.
اگر برنامه شما دارای اعتبارسنجی است (یک صفحه لاگین دارد)، بهتر است بجای IP از this.Context.User.Identity.Name استفاده کنید.


کدهای سمت کلاینت نمایش آمار کاربران

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script>
    <script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>
</head>
<body>
    <form id="form1" runat="server">
    online users count: <span id="usersCount"></span>
    </form>
    <script type="text/javascript">
        $(function () {
            $.connection.hub.logging = true;
            var onlineUsersHub = $.connection.onlineUsersHub;
            onlineUsersHub.client.updateUsersOnlineCount = function (count) {
                $('#usersCount').text(count);
            };
            $.connection.hub.start();
        });
    </script>
</body>
</html>
با توجه به اینکه در هاب تعریف شده، متد پویای updateUsersOnlineCount، آمار تعداد کاربران متصل را (تعداد آی پی‌های منحصربفرد متصل را) به کلاینت‌ها ارسال می‌کند، بنابراین در سمت کلاینت نیز با تعریف callback ایی به همین نام، می‌توان این آمار دریافتی را به کاربران سایت نمایش داد. آماری که به صورت خودکار با کم و زیاد شدن کاربران به روز شده و نیازی نیست کاربر به صورت دستی، صفحه را به روز کند.


کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت کنید:
SignalR05.zip
 
مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت اول
پس از آشنایی مقدماتی با اجزای مهم تشکیل دهنده‌ی Ember.js (^ و ^)، بهتر است این دانسته‌ها را جهت تکمیل یک پروژه‌ی ساده‌ی تک صفحه‌ای بلاگ، بکار بگیریم.
در این بلاگ می‌توان:
- یک مطلب جدید را ارسال کرد.
- مطالب قابل ویرایش و یا حذف هستند.
- مطالب بلاگ قسمت ارسال نظرات دارند.
- امکان گزارشگیری از آخرین نظرات ارسالی وجود دارد.
- سایت صفحات درباره و تماس با ما را نیز دارا است.


ساختار پوشه‌های برنامه

در تصویر ذیل، ساختار پوشه‌های برنامه بلاگ را ملاحظه می‌کنید. چون قسمت سمت کلاینت این برنامه کاملا جاوا اسکریپتی است، پوشه‌های App، Controllers، Libs، Models، Routes و Templates آن در پوشه‌ی Scripts تعریف شده‌اند و به این ترتیب می‌توان تفکیک بهتری را بین اجزای تشکیل دهنده‌ی یک برنامه‌ی تک صفحه‌ای وب Emeber.js پدید آورد.


فایل CSS بوت استرپ نیز به پوشه‌ی Content اضافه شده‌است.


دریافت پیشنیازهای سمت کاربر برنامه

در ساختار پوشه‌های فوق، از پوشه‌ی Libs برای قرار دادن کتابخانه‌های پایه برنامه مانند jQuery و Ember.js استفاده خواهیم کرد. به این ترتیب:
- نیاز به آخرین نگارش‌های Ember.js و همچنین افزونه‌ی Ember-Data آن برای کار ساده‌تر با داده‌ها و سرور وجود دارد. این فایل‌ها را از آدرس ذیل می‌توانید دریافت کنید (نسخه‌‌های نیوگت به دلیل قدیمی بودن و به روز نشدن مداوم آن‌ها توصیه نمی‌شوند):
http://emberjs.com/builds/#/beta
برای حالت آزمایش برنامه، استفاده از فایل‌های دیباگ آن توصیه می‌شوند (فایل‌هایی با نام اصلی و بدون پسوند prod یا min). زیرا این فایل‌ها خطاها و اطلاعات بسیار مفصلی را از اشکالات رخ داده، در کنسول وب مرورگرها، فایرباگ و یا Developer tools آن‌ها نمایش می‌دهند. نسخه‌ی min برای حالت ارائه‌ی نهایی برنامه است. نسخه‌ی prod همان نسخه‌ی دیباگ است با حذف اطلاعات دیباگ (نسخه‌ی production فشرده نشده). نسخه‌ی فشرده شده‌ی prod آن، فایل min نهایی را تشکیل می‌دهد.
- دریافت جی‌کوئری
- آخرین نگارش handlebars.js را از سایت رسمی آن دریافت کنید: http://handlebarsjs.com
- Ember Handlebars Loader: https://github.com/michaelrkn/ember-handlebars-loader
- Ember Data Local Storage Adapter: https://github.com/kurko/ember-localstorage-adapter

ترتیب تعریف و قرارگیری این فایل‌ها را پس از دریافت، در فایل index.html قرار گرفته در ریشه‌ی سایت، در کدهای ذیل مشاهده می‌کنید:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Ember Blog</title>
 
  <link href="Content/bootstrap.css" rel="stylesheet" />
  <link href="Content/bootstrap-theme.css" rel="stylesheet" />
  <link href="Content/styles.css" rel="stylesheet" />
 
  <script src="Scripts/Libs/jquery-2.1.1.min.js" type="text/javascript"></script>
  <script src="Scripts/Libs/bootstrap.min.js" type="text/javascript"></script>
  <script src="Scripts/Libs/handlebars-v2.0.0.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember-handlebars-loader-0.0.1.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember-data.js" type="text/javascript"></script>
  <script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script> 
</head>
<body>
 
</body>
</html>


اصلاح فایل ember-handlebars-loader-0.0.1.js
اگر به فایل ember-handlebars-loader-0.0.1.js مراجعه کنید، مسیر فایل‌های قالب handlebars قسمت‌های مختلف برنامه را از پوشه‌ی templates واقع در ریشه‌ی سایت می‌خواند. با توجه به تصویر ساختار پوشه‌ی پروژه‌ی جاری، پوشه‌ی template به داخل پوشه‌ی Scripts منتقل شده‌است و نیاز به یک چنین اصلاحی دارد:
 url: "Scripts/Templates/" + name + ".hbs",
کار اسکریپت ember-handlebars-loader-0.0.1.js بارگذاری خودکار فایل‌های hbs یا handlebars از پوشه‌ی قالب‌های سفارشی برنامه است. در این حالت اگر برنامه را اجرا کنید، خطای 404 را دریافت خواهید کرد. از این جهت که mime-type پسوند hbs در IIS تعریف نشده‌است. اضافه کردن آن نیز ساده‌است. به فایل web.config برنامه مراجعه کرده و تغییر ذیل را اعمال کنید:
 <system.webServer>
  <staticContent>
        <mimeMap fileExtension=".hbs" mimeType="text/x-handlebars-template" />
  </staticContent>
 </system.webServer>


مزیت استفاده از نسخه‌ی دیباگ ember.js در حین توسعه‌ی برنامه

نسخه‌ی دیباگ ember.js علاوه بر به همراه داشتن خطاهای بسیار جامع و توضیح علل مشکلات، مواردی را که در آینده منسوخ خواهند شد نیز توضیح می‌دهد:


برای مثال در اینجا عنوان شده‌است که دیگر از linkTo استفاده نکنید و آن‌را به link-to تغییر دهید.
همچنین اگر از مرورگر کروم استفاده می‌کنید، افزونه‌ی Ember Inspector را نیز می‌توانید نصب کنید تا اطلاعات بیشتری را از جزئیات مسیریابی‌های تعریف شده و قالب‌های Ember.js بتوان مشاهده کرد. این افزونه به صورت یک برگه‌ی جدید در Developer tools آن ظاهر می‌شود.


ایجاد شیء Application

همانطور که در قسمت‌های پیشین نیز عنوان شد (^ و ^  )، یک برنامه‌ی Ember.js با تعریف وهله‌ای از شیء Application آن آغاز می‌شود. برای این منظور به پوشه‌ی App مراجعه کرده و فایل جدید Scripts\App\blogger.js را اضافه کنید؛ با این محتوا:
 Blogger = Ember.Application.create();
مدخل این فایل را نیز پس از تعاریف وابستگی‌های اصلی برنامه، به فایل index.html اضافه خواهیم کرد:
<script src="Scripts/App/blogger.js" type="text/javascript"></script>
تا اینجا برپایی اولیه‌ی برنامه‌ی تک صفحه‌ای وب مبتنی بر Ember.js ما به پایان می‌رسد.


تعاریف مسیریابی و قالب‌ها

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


کار router در Ember.js، نگاشت آدرس درخواستی توسط کاربر، به یک route یا مسیریابی تعریف شده‌است. به این ترتیب مدل، کنترلر و قالب آن route به صورت خودکار بارگذاری و پردازش خواهند.
با مراجعه به ریشه‌ی سایت، فایل index.html بارگذاری می‌شود.


در اینجا تصویری از صفحه‌ی آغازین بلاگ را مشاهده می‌کنید. در آن صفحه‌ی posts همان ریشه‌ی سایت نیز می‌باشد. بنابراین نیاز است ابتدا مسیریابی آن‌را تعریف کرد. برای این منظور، فایل جدید Scripts\App\router.js را به پوشه‌ی App اضافه کنید؛ با این محتوا:
Blogger.Router.map(function () {
   this.resource('posts', { path: '/' });
});
علت تعریف قسمت path این است که ریشه‌ی سایت (/) نیز به مسیریابی posts ختم شود. در غیر اینصورت کاربر با مراجعه به ریشه‌ی سایت، یک صفحه‌ی خالی را مشاهده خواهد کرد؛ زیرا به صورت پیش فرض، آدرس قابل ترجمه‌ی یک صفحه، با آدرس و نام مسیریابی آن یکی است.

همچنین مدخل آن‌را نیز در فایل index.html تعریف نمائید:
 <script src="Scripts/App/blogger.js" type="text/javascript"></script>
<script src="Scripts/App/router.js" type="text/javascript"></script>
در اینجا Blogger همان شیء Application برنامه است که پیشتر در فایل Scripts\App\blogger.js تعریف کردیم. سپس به کمک متد Blogger.Router.map، اولین مسیریابی برنامه را افزوده‌ایم.


افزودن مسیریابی و قالب posts

در ادامه فایل جدید Scripts\Templates\posts.hbs را اضافه کنید. به این ترتیب قالب خالی مطالب به پوشه‌ی templates اضافه می‌شود. محتوای این فایل را به نحو ذیل تنظیم کنید:
<div class="container">
  <h1>Emeber.js blog</h1>
  <ul>
   <li>Item 1</li>
   <li>Item 2</li>
   <li>Item 3</li>
  </ul>
</div>
در اینجا دیگر نیازی به ذکر تگ script از نوع text/x-handlebars نیست.
برای بارگذاری این قالب نیاز است آن‌را به template loader توضیح داده شده در ابتدای بحث، در فایل index.html اضافه کنیم:
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts'
 ]);
</script>
اکنون برنامه را اجرا کنید. به تصویر ذیل خواهید رسید که در آن افزونه‌ی Ember Inspector نیز نمایش داده شده‌است:



افزودن مسیریابی و قالب about

در ادامه قصد داریم صفحه‌ی about را اضافه کنیم. مجددا با افزودن مسیریابی آن به فایل Scripts\App\router.js شروع می‌کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
});
سپس فایل قالب آن‌را در مسیر Scripts\Templates\about.hbs ایجاد خواهیم کرد؛ برای مثال با این محتوای فرضی:
 <h1>About Ember Blog</h1>
<p>Bla bla bla!</p>
اکنون نام این فایل را به template loader فایل index.html اضافه می‌کنیم.
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about'
 ]);
</script>
اگر از قسمت قبل به خاطر داشته باشید، ساختار کلی قالب‌های ember.js یک چنین شکلی را دارد:
 <script type="text/x-handlebars" data-template-name="about">

</script>
اسکریپت template loader، این تعاریف را به صورت خودکار اضافه می‌کند. مقدار data-template-name را نیز به نام فایل متناظر با آن تنظیم خواهد کرد و چون ember.js بر اساس ایده‌ی convention over configuration کار می‌کند، مسیریابی about با کنترلری به همین نام و قالبی هم نام پردازش خواهد شد. بنابراین نام فایل‌های قالب را باید بر اساس مسیریابی‌های متناظر با آن‌ها تعیین کرد.
برای آزمایش این مسیر و قالب جدید آن، آدرس http://localhost/#/about را بررسی کنید.


اضافه کردن منوی ثابت بالای سایت

روش اول این است که به ابتدای هر دو قالب about.hbs و posts.hbs، تعاریف منو را اضافه کنیم. مشکل این‌کار، تکرار کدها و پایین آمدن قابلیت نگهداری برنامه است. روش بهتر، افزودن کدهای مشترک بین صفحات، در قالب application برنامه است. نمونه‌ی آن‌را در مثال قسمت قبل مشاهده کرده‌اید. در اینجا چون قصد نداریم به صورت دستی قالب‌ها را به صفحه اضافه کنیم و همچنین برای ساده شدن نگهداری برنامه، قالب‌ها را در فایل‌های مجزایی قرار داده‌ایم، تنها کافی است فایل جدید Scripts\Templates\application.hbs را به پوشه‌ی قالب‌های برنامه اضافه کنیم؛ با این محتوا:
<div class='container'>
 <nav class='navbar navbar-default' role='navigation'>
  <ul class='nav navbar-nav'>
  <li>{{#link-to 'posts'}}Posts{{/link-to}}</li>
  <li>{{#link-to 'about'}}About{{/link-to}}</li>
  </ul>
 </nav>

  {{outlet}}
</div>
و سپس همانند قبل، نام فایل قالب اضافه شده را به template loader فایل index.html اضافه می‌کنیم:
<script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application'
 ]);
</script>


افزودن مسیریابی و قالب contact

برای افزودن صفحه‌ی تماس با مای سایت، ابتدا مسیریابی آن‌را در فایل Scripts\App\router.js تعریف می‌کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
  this.resource('contact');
});
سپس قالب متناظر با آن‌را به نام Scripts\Templates\contact.hbs اضافه خواهیم کرد؛ فعلا با این محتوای اولیه:
<h1>Contact</h1>
<ul>
  <li>Phone: ...</li>
  <li>Email: ...</li>
</ul>
و بعد template loader فایل index.html را از وجود آن مطلع خواهیم کرد:
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application', 'contact' ]);
</script>
همچنین لینکی به مسیریابی آن‌را به فایل Scripts\Templates\application.hbs که منوی سایت در آن تعریف شده‌است، اضافه می‌کنیم:
<div class='container'>
 <nav class='navbar navbar-default' role='navigation'>
  <ul class='nav navbar-nav'>
  <li>{{#link-to 'posts'}}Posts{{/link-to}}</li>
  <li>{{#link-to 'about'}}About{{/link-to}}</li>
  <li>{{#link-to 'contact'}}Contact{{/link-to}}</li>
  </ul>
 </nav>

  {{outlet}}
</div>


تعریف مسیریابی تو در تو در صفحه‌ی contact

در حال حاضر صفحه‌ی Contact، ایمیل و شماره تماس را در همان بار اول نمایش می‌دهد. می‌خواهیم این دو را تبدیل به دو لینک جدید کنیم که با کلیک بر روی هر کدام، محتوای مرتبط، در قسمتی از همان صفحه بارگذاری شده و نمایش داده شود.
برای اینکار نیاز است مسیریابی را تو در تو تعریف کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
  this.resource('contact', function () {
   this.resource('email');
   this.resource('phone');
  });
});
اگر مسیریابی‌های email و یا phone را به صورت مستقل مانند about و یا posts تعریف کنیم، با کلیک کاربر بر روی لینک متناظر با هر کدام، یک صفحه‌ی کاملا جدید نمایش داده می‌شود. اما در اینجا قصد داریم تنها قسمت کوچکی از همان صفحه‌ی contact را با محتوای ایمیل و یا شماره تماس جایگزین نمائیم. به همین جهت مسیریابی‌های متناظر را در اینجا به صورت تو در تو و ذیل مسیریابی contact تعریف کرده‌ایم.

پس از آن دو فایل قالب جدید Scripts\Templates\email.hbs را با محتوای:
 <h2>Email</h2>
<p>
<span></span> Email name@site.com.
</p>
و فایل قالب Scripts\Templates\phone.hbs را با محتوای:
 <h2>Phone</h2>
<p>
<span></span> Call 12345678.
</p>
به پوشه‌ی قالب‌ها اضافه نمائید به همراه معرفی نام آن‌ها به template loader برنامه در صفحه‌ی index.html :
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application', 'contact', 'email', 'phone' ]);
</script>
اکنون به قالب contact.hbs مجددا مراجعه کرده و تعاریف آن را به نحو ذیل تغییر دهید:
<h1>Contact</h1>
<div class="row">
  <div class="col-md-6">
   <p>
    Want to get in touch?
    <ul>
      <li>{{#link-to 'phone'}}Phone{{/link-to}}</li>
      <li>{{#link-to 'email'}}Email{{/link-to}}</li>
    </ul>
   </p>
  </div>
  <div class="col-md-6">
   {{outlet}}
  </div>
</div>
در اینجا دو لینک به مسیریابی‌های Phone و Email تعریف شده‌اند. همچنین {{outlet}} نیز قابل مشاهده است. با کلیک بر روی لینک Phone، مسیریابی آن فعال شده و سپس قالب متناظر با آن در قسمت {{outlet}} رندر می‌شود. در مورد لینک Email نیز به همین نحو رفتار خواهد شد.


در اینجا نحوه‌ی پردازش مسیریابی contact را ملاحظه می‌کنید. ابتدا درخواستی جهت مشاهده‌ی آدرس http://localhost/#/contact دریافت می‌شود. سپس router این درخواست را به مسیریابی همنامی منتقل می‌کند. این مسیریابی ابتدا قالب عمومی application را رندر کرده و سپس قالب اصلی و همنام مسیریابی جاری یا همان contact.hbs را رندر می‌کند. در این صفحه چون مسیریابی تو در تویی تعریف شده‌است، اگر درخواستی برای مشاهده‌ی http://localhost/#/contact/phone دریافت شود، محتوای آن‌را در {{outlet}} قالب contact.hbs جاری رندر می‌کند.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_01.zip
مطالب
محدود کردن درخواست های Asp.Net Web Api بر اساس Client IP
در بسیاری از سناریو‌ها این موضوع مطرح می‌شود که سرویس‌های طراحی شده بر اساس Asp.Net Web Api، فقط به یک سری آی پی‌های مشخص سرویس دهند. برای مثال اگر Ip کلاینت در لیست کلاینت‌های دارای لایسنس خریداری شده بود، امکان استفاده از سرویس میسر باشد؛ در غیر این صورت خیر. بسته به نوع پیاده سازی سرویس‌های Web api، پیاده سازی این بخش کمی متفاوت خواهد شد. در طی این پست این موضوع را برای سه حالت IIs Host  و SelfHost و Owin Host بررسی می‌کنیم.
در اینجا قصد داریم حالتی را پیاده سازی نماییم که اگر درخواست جاری از سوی کلاینتی بود که Ip آن در لیست Ip‌های غیر مجاز قرار داشت، ادامه‌ی عملیات متوقف شود.

IIS Hosting:
حالت پیش فرض استفاده از سرویس‌های Web Api همین گزینه است؛ وابستگی مستقیم به System.Web . در مورد مزایا و معایب آن بحث نمی‌کنیم اما اگر این روش را انتخاب کردید تکه کد زیر این کار را برای ما انجام می‌دهد:
if (request.Properties.ContainsKey["MS_HttpContext"]) 
{
    var ctx = request.Properties["MS_HttpContext"] as HttpContextWrapper;
    if (ctx != null)
    {
        var ip = ctx.Request.UserHostAddress;       
    }
}
برای بدست آوردن شی HttpContext می‌توان آن را از لیست Properties‌های درخواست جاری به دست آورد. حال کد بالا را در قالب یک Extension Method در خواهیم آورد؛ به صورت زیر:

public static class HttpRequestMessageExtensions
{
    private const string HttpContext = "MS_HttpContext";

    public static string GetClientIpAddress(this HttpRequestMessage request)
    {
        if (request.Properties.ContainsKey(HttpContext))
        {
            dynamic ctx = request.Properties[HttpContext];
            if (ctx != null)
            {
                return ctx.Request.UserHostAddress;
            }
        }      
        return null;
    }
}

Self Hosting:

در حالت Self Host می‌توان عملیات بالا را با استفاده از خاصیت RemoteEndpointMessageProperty  انجام داد که تقریبا شبیه به حالت Web Host است. مقدار این خاصیت نیز در شی جاری HttpRequestMessage وجود دارد. فقط باید به صورت زیر آن را واکشی نماییم:
if (request.Properties.ContainsKey[RemoteEndpointMessageProperty.Name]) 
{
    var remote = request.Properties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;

    if (remote != null)
    {
        var ip = remote.Address;        
    }
}
خاصیت RemoteEndpointMessageProperty  به تمامی درخواست‌ها وارده در سرویس‌های WCF چه در حالت استفاده از Http و چه در حالت Tcp اضافه می‌شود و در اسمبلی System.ServiceModel نیز می‌باشد. ار آنجا که Web Api از هسته‌ی WCF استفاده می‌کند (WCF Core) در نتیجه می‌توان از این روش استفاده نمود. فقط باید اسمبلی System.ServiceModel را به پروژه‌ی خود اضافه نمایید.

ترکیب حالت‌های قبلی:
اگر می‌خواهید کد‌های نوشته شده شما وابستگی به نوع هاست پروژه نداشته باشد، یا به معنای دیگر، در هر دو حالت به درستی کار کند می‌توانید به روش زیر حالت‌های قبلی را با هم ترکیب کنید. 
»در این صورت دیگر نیازی به اضافه کردن اسمبلی System.ServiceModel  نیست.
public static class HttpRequestMessageExtensions
{
    private const string HttpContext = "MS_HttpContext";
    private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty";

    public static string GetClientIpAddress(this HttpRequestMessage request)
    {
        if (request.Properties.ContainsKey(HttpContext))
        {
            dynamic ctx = request.Properties[HttpContext];
            if (ctx != null)
            {
                return ctx.Request.UserHostAddress;
            }
        }

        if (request.Properties.ContainsKey(RemoteEndpointMessage))
        {
            dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage];
            if (remoteEndpoint != null)
            {
                return remoteEndpoint.Address;
            }
        }

        return null;
    }
}
مرحله بعدی طراحی یک DelegatingHandler جهت استفاده از IP به دست آمده است .
 
public class MyHandler : DelegatingHandler
{
 private readonly HashSet<string> deniedIps;

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {

      if (deniedIps.Contains(request.GetClientIpAddress()))
        {            
            return Task.FromResult( new HttpResponseMessage( HttpStatusCode.Unauthorized ) );
        }

        return base.SendAsync(request, cancellationToken);
    }
}

Owin :

زمانی که از Owin   برای هاست سرویس‌های Web Api خود استفاده می‌کنید کمی روال انجام کار متفاوت خواهد شد. در این مورد نیز می‌توانید از DelegatingHandler‌ها استفاده کنید. معرفی DelegatingHandler طراحی شده به Asp.Net PipeLine به صورت زیر خواهد بود:
public class Startup
    {
        public void Configuration( IAppBuilder appBuilder )
        {     
            var config = new HttpConfiguration();
                 
            var routeHandler = HttpClientFactory.CreatePipeline( new HttpControllerDispatcher( config ), new DelegatingHandler[] 
            {
                new MyHandler(),
            } );
      
            config.Routes.MapHttpRoute(
                name: "Default",
                routeTemplate: "{controller}/{action}",
                defaults: null,
                constraints: null,
                handler: routeHandler
            );            
           
            config.EnsureInitialized();
         
            appBuilder.UseWebApi( config );         
        }
    }
اما نکته ای را که باید به آن دقت داشت، این است که یکی از مزایای استفاده از Owin، یکپارچه سازی عملیات هاستینگ قسمت‌های مختلف برنامه است. برای مثال ممکن است قصد داشته باشید که بخش هایی که با Asp.Net SignalR نیز پیاده سازی شده‌اند، قابلیت استفاده از کد‌های بالا را داشته باشند. در این صورت بهتر است کل عملیات بالا در قالب یک Owin Middleware عمل نماید تا تمام قسمت‌های هاست شده‌ی برنامه از کد‌های بالا استفاده نمایند؛ به صورت زیر:
public class IpMiddleware : OwinMiddleware
{
    private readonly HashSet<string> _deniedIps;

    public IpMiddleware(OwinMiddleware next, HashSet<string> deniedIps) :
        base(next)
    {
        _deniedIps = deniedIps;
    }

    public override async Task Invoke(OwinRequest request, OwinResponse response)
    {
        var ipAddress = (string)request.Environment["server.RemoteIpAddress"];

        if (_deniedIps.Contains(ipAddress))
        {
            response.StatusCode = 403;
            return;
        }

        await Next.Invoke(request, response);
    }
}
برای نوشتن یک Owin Middleware کافیست کلاس مورد نظر از کلاس OwinMiddleware ارث ببرد و متد Invoke را Override کنید.  لیست Ip‌های غیر مجاز، از طریق سازنده در اختیار Middleware قرار می‌گیرد. اگر درخواست مجاز بود از طریق دستور Next.Invoke(request,response) کنترل برنامه به مرحله بعدی منتقل می‌شود در غیر صورت عملیات با کد 403 متوقف می‌شود.
در نهایت برای معرفی این Middleware طراحی شده به Application، مراحل زیر را انجام دهید.
public class Startup
    {
        public void Configuration( IAppBuilder appBuilder )
        {       
           var config = new HttpConfiguration();                      

           var deniedIps = new HashSet<string> {"192.168.0.100", "192.168.0.101"}; 
          
app.Use(typeof(IpMiddleware), deniedIps); appBuilder.UseWebApi( config ); } }
مطالب
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
مطالب
نمایش یک فایل PDF پویا در یک iframe در ASP.NET
عموما در برنامه‌های وب برای نمایش فایل‌های پویای باینری تولید شده، یا ابتدا آن‌ها را بر روی سخت دیسک ذخیره کرده و مسیر نهایی را به نحوی به کاربر نمایش می‌دهند و یا فایل را بدون ذخیره سازی، در مرورگر کاربر اصطلاحا Flush می‌کنند. حالت Flush سبب نمایش صفحه دیالوگ ذخیره سازی فایل گردیده و در همینجا Response خاتمه خواهد یافت.
برای نمونه در اینجا توسط متد inMemoryFile، یک فایل PDF در حافظه تشکیل شده و سپس به صورت یک Byte Array بازگشت داده می‌شود. در ادامه کار، این اطلاعات در مرورگر کاربر Flush خواهد شد:
using System.IO;
using System.Net.Mime;
using System.Web;

namespace WebApplication
{
    public class PdfHandler : IHttpHandler
    {
        private static byte[] inMemoryFile()
        {
            //تولید پویای فایل در حافظه و یا حتی خواندن از یک نمونه موجود
            return File.ReadAllBytes(@"D:\path\DynamicCrosstabSampleRpt.pdf");
        }

        public void ProcessRequest(HttpContext context)
        {
            var pdf = inMemoryFile();

            context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
            context.Response.ContentType = MediaTypeNames.Application.Pdf;
            context.Response.AddHeader("Content-Length", pdf.Length.ToString());
            context.Response.AddHeader("content-disposition", "attachment;filename=test.pdf");
            context.Response.Buffer = true;
            context.Response.Clear();
            context.Response.OutputStream.Write(pdf, 0, pdf.Length);
            context.Response.OutputStream.Flush();
            context.Response.OutputStream.Close();
            context.Response.End();
        }

        public bool IsReusable
        {
            get  {  return false;  }
        }
    }
}
و برای نمایش آن در یک iframe در صفحه:
 <iframe width="100%" src="PdfHandler.ashx" height="200px"></iframe>
نتیجه کار، نمایش صفحه دیالوگ ذخیره سازی فایل به کاربر است:


سؤال: فرض کنید Adobe reader بر روی سیستم نصب است و مرورگر با استفاده از Active-X آن می‌تواند این نوع فایل‌ها را نمایش دهد. آیا راهی وجود دارد تا بجای نمایش save popup dialog، این فایل توسط مرورگر نمایش داده شود؟
پاسخ: بلی. در کدهای فوق تنها کافی است یک سطر آن تغییر کند:
 Response.AddHeader("content-disposition", "inline;filename=test.pdf");
در اینجا تنها نحوه مقدار دهی content-disposition تفاوت کرده است. حالت attachment سبب نمایش save popup dialog می‌شود و مقدار inline، فایل را در مرورگر نمایش خواهد داد.
اینبار اگر برنامه را اجرا کنیم، iframe ایی که به PdfHandler.ashx اشاره می‌کند، فایل PDF را در صفحه نمایش می‌دهد.
مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت چهارم
در قسمت قبل تشخیص تغییرات توسط Web API را بررسی کردیم. در این قسمت نگاهی به پیاده سازی Change-tracking در سمت کلاینت خواهیم داشت.


ردیابی تغییرات در سمت کلاینت توسط Web API

فرض کنید می‌خواهیم از سرویس‌های REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین می‌خواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیت‌ها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی داده‌ها توسط مدل Code-First انجام می‌شود.

در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویس‌های ارائه شده توسط پروژه Web API را فراخوانی می‌کند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی می‌کنیم.

مدل زیر را در نظر بگیرید.

همانطور که می‌بینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه می‌کند. می‌خواهیم مدل‌ها و کد دسترسی به داده‌ها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
  • کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
  • کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیت‌ها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه می‌کند. کلاینت‌ها هنگام ویرایش آبجکت موجودیت‌ها باید این فیلد را مقدار دهی کنند. همانطور که می‌بینید این خاصیت از نوع TrackingState enum مشتق می‌شود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیت‌ها بدین روش، وابستگی‌های EF را برای کلاینت از بین می‌بریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگی‌های بخصوصی معرفی می‌شدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور می‌دهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity
{
    protected BaseEntity()
    {
        TrackingState = TrackingState.Nochange;
    }

    public TrackingState TrackingState { get; set; }
}

public enum TrackingState
{
    Nochange,
    Add,
    Update,
    Remove,
}
  • کلاس‌های موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string Company { get; set; }
    public virtual ICollection<Phone> Phones { get; set; }
}

public class Phone : BaseEntity
{
    public int PhoneId { get; set; }
    public string Number { get; set; }
    public string PhoneType { get; set; }
    public int CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیت‌های جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی می‌کنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext
{
    public Recipe4Context() : base("Recipe4ConnectionString") { }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Phone> Phones { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Do not persist TrackingState property to data store
        // This property is used internally to track state of
        // disconnected entities across service boundaries.
        // Leverage the Custom Code First Conventions features from Entity Framework 6.
        // Define a convention that performs a configuration for every entity
        // that derives from a base entity class.
        modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState));
        modelBuilder.Entity<Customer>().ToTable("Customers");
        modelBuilder.Entity<Phone>().ToTable("Phones");
}
}
  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings>
  <add name="Recipe4ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال می‌کند و به JSON serializer دستور می‌دهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیت‌های Customer و PhoneNumber بوجود می‌آید.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);
    // The bidirectional navigation properties between related entities
    // create a self-referencing loop that breaks Web API's effort to
    // serialize the objects as JSON. By default, Json.NET is configured
    // to error when a reference loop is detected. To resolve problem,
    // simply configure JSON serializer to ignore self-referencing loops.
    GlobalConfiguration.Configuration.Formatters.JsonFormatter
        .SerializerSettings.ReferenceLoopHandling =
            Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    ...
}
  • کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینت‌ها ارائه می‌شود را به مقادیر متناظر کامپوننت‌های ردیابی EF تبدیل می‌کند.
public static EntityState Set(TrackingState trackingState)
{
    switch (trackingState)
    {
        case TrackingState.Add:
            return EntityState.Added;
        case TrackingState.Update:
            return EntityState.Modified;
        case TrackingState.Remove:
            return EntityState.Deleted;
        default:
            return EntityState.Unchanged;
    }
}
  • در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController
{
    // GET api/customer
    public IEnumerable<Customer> Get()
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).ToList();
        }
    }

    // GET api/customer/5
    public Customer Get(int id)
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id);
        }
    }

    [ActionName("Update")]
    public HttpResponseMessage UpdateCustomer(Customer customer)
    {
        using (var context = new Recipe4Context())
        {
            // Add object graph to context setting default state of 'Added'.
            // Adding parent to context automatically attaches entire graph
            // (parent and child entities) to context and sets state to 'Added'
            // for all entities.
            context.Customers.Add(customer);
            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                entry.State = EntityStateFactory.Set(entry.Entity.TrackingState);
                if (entry.State == EntityState.Modified)
                {
                    // For entity updates, we fetch a current copy of the entity
                    // from the database and assign the values to the orginal values
                    // property from the Entry object. OriginalValues wrap a dictionary
                    // that represents the values of the entity before applying changes.
                    // The Entity Framework change tracker will detect
                    // differences between the current and original values and mark
                    // each property and the entity as modified. Start by setting
                    // the state for the entity as 'Unchanged'.
                    entry.State = EntityState.Unchanged;
                    var databaseValues = entry.GetDatabaseValues();
                    entry.OriginalValues.SetValues(databaseValues);
                }
            }

        context.SaveChanges();
    }

    return Request.CreateResponse(HttpStatusCode.OK, customer);
}

    [HttpDelete]
    [ActionName("Cleanup")]
    public HttpResponseMessage Cleanup()
    {
        using (var context = new Recipe4Context())
        {
            context.Database.ExecuteSqlCommand("delete from phones");
            context.Database.ExecuteSqlCommand("delete from customers");
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
حال اپلیکیشن کلاینت (برنامه کنسول) را می‌سازیم که از این سرویس استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
  • فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program
{
    private HttpClient _client;
    private Customer _bush, _obama;
    private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone;
    private HttpResponseMessage _response;

    private static void Main()
    {
        Task t = Run();
        t.Wait();
        Console.WriteLine("\nPress <enter> to continue...");
        Console.ReadLine();
    }

    private static async Task Run()
    {
        var program = new Program();
        program.ServiceSetup();
        // do not proceed until clean-up completes
        await program.CleanupAsync();
        program.CreateFirstCustomer();
        // do not proceed until customer is added
        await program.AddCustomerAsync();
        program.CreateSecondCustomer();
        // do not proceed until customer is added
        await program.AddSecondCustomerAsync();
        // do not proceed until customer is removed
        await program.RemoveFirstCustomerAsync();
        // do not proceed until customers are fetched
        await program.FetchCustomersAsync();
    }

    private void ServiceSetup()
    {
        // set up infrastructure for Web API call
        _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") };
        // add Accept Header to request Web API content negotiation to return resource in JSON format
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue
        ("application/json"));
    }
    private async Task CleanupAsync()
    {
        // call the cleanup method from the service
        _response = await _client.DeleteAsync("api/customer/cleanup/");
    }

    private void CreateFirstCustomer()
    {
        // create customer #1 and two phone numbers
        _bush = new Customer
        {
            Name = "George Bush",
            Company = "Ex President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _whiteHousePhone = new Phone
        {
            Number = "212 222-2222",
            PhoneType = "White House Red Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bushMobilePhone = new Phone
        {
            Number = "212 333-3333",
            PhoneType = "Bush Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bush.Phones.Add(_whiteHousePhone);
        _bush.Phones.Add(_bushMobilePhone);
    }

    private async Task AddCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _bush = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _bush.Name, _bush.Phones.Count);
            foreach (var phoneType in _bush.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private void CreateSecondCustomer()
    {
        // create customer #2 and phone numbers
        _obama = new Customer
        {
            Name = "Barack Obama",
            Company = "President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _obamaMobilePhone = new Phone
        {
            Number = "212 444-4444",
            PhoneType = "Obama Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        // set tracking state to 'Modifed' to generate a SQL Update statement
        _whiteHousePhone.TrackingState = TrackingState.Update;
        _obama.Phones.Add(_obamaMobilePhone);
        _obama.Phones.Add(_whiteHousePhone);
    }

    private async Task AddSecondCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _obama = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _obama.Name, _obama.Phones.Count);
            foreach (var phoneType in _obama.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private async Task RemoveFirstCustomerAsync()
    {
        // remove George Bush from underlying data store.
        // first, fetch George Bush entity, demonstrating a call to the
        // get action method on the service while passing a parameter
        var query = "api/customer/" + _bush.CustomerId;
        _response = _client.GetAsync(query).Result;

        if (_response.IsSuccessStatusCode)
        {
            _bush = await _response.Content.ReadAsAsync<Customer>();
            // set tracking state to 'Remove' to generate a SQL Delete statement
            _bush.TrackingState = TrackingState.Remove;
            // must also remove bush's mobile number -- must delete child before removing parent
            foreach (var phoneType in _bush.Phones)
            {
                // set tracking state to 'Remove' to generate a SQL Delete statement
                phoneType.TrackingState = TrackingState.Remove;
            }
            // construct call to remove Bush from underlying database table
            _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
            if (_response.IsSuccessStatusCode)
            {
                Console.WriteLine("Removed {0} from database", _bush.Name);
                foreach (var phoneType in _bush.Phones)
                {
                    Console.WriteLine("Remove {0} from data store", phoneType.PhoneType);
                }
            }
            else
                Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }

    private async Task FetchCustomersAsync()
    {
        // finally, return remaining customers from underlying data store
        _response = await _client.GetAsync("api/customer/");
        if (_response.IsSuccessStatusCode)
        {
            var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>();
            foreach (var customer in customers)
            {
                Console.WriteLine("Customer {0} has {1} Phone Numbers(s)",
                customer.Name, customer.Phones.Count());
                foreach (var phoneType in customer.Phones)
                {
                    Console.WriteLine("Phone Type: {0}", phoneType.PhoneType);
                }
            }
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }
}

  • در آخر کلاس‌های Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایه‌های مختلف اپلیکیشن به اشتراک گذاشته شوند.

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








شرح مثال جاری

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

سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت می‌کنیم و از سرویس درخواست می‌کنیم که اطلاعات را با فرمت JSON بازگرداند.

سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی می‌کنیم. این فراخوانی تمام داده‌های پیشین را حذف می‌کند.

در قدم بعدی یک مشتری بهمراه دو شماره تماس می‌سازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی می‌کنیم تا کامپوننت‌های Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.

سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی می‌کنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت می‌کند و آن را به context جاری اضافه می‌نماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه می‌شود و EF شروع به ردیابی تغییرات آن می‌کند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.

قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده می‌کنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه می‌کند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه می‌دهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیت‌ها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل می‌کنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام می‌شود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر می‌دهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت می‌کنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت می‌کند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص می‌دهیم. پشت پرده، کامپوننت‌های EF Change-tracking بصورت خودکار تفاوت‌های مقادیر اصلی و مقادیر ارسالی را تشخیص می‌دهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری می‌کنند. فراخوانی‌های بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.

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

متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل می‌کند و آبجکت‌ها را به موتور change-tracking ارسال می‌کند که نهایتا منجر به تولید دستورات لازم SQL می‌شود.

نکته: در اپلیکیشن‌های واقعی بهتر است کد دسترسی داده‌ها و مدل‌های دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت می‌تواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی می‌توانید برای تمام موجودیت‌ها استفاده کنید.

مطالب
پیاده سازی Unobtrusive Ajax در ASP.NET Core 1.0
پیاده سازی Unobtrusive Ajax را در ASP.NET MVC 5.x، می‌توانید در مطلب «ASP.NET MVC #21» مطالعه کنید. HTML Helpers مرتبط با Ajax، به طور کامل از ASP.NET Core 1.0 حذف شده‌اند. اما این مورد به این معنا نیست که نمی‌توان Unobtrusive Ajax را در ASP.NET Core که تمرکزش بیشتر بر روی Tag Helpers جدید هست تا HTML Helpers قدیمی، پیاده سازی کرد.


Unobtrusive Ajax چیست؟

در حالت معمولی، با استفاده از متد ajax جی‌کوئری، کار ارسال غیرهمزمان اطلاعات، به سمت سرور صورت می‌گیرد. چون در این روش کدهای جی‌کوئری داخل صفحات برنامه‌های ما قرار می‌گیرند، به این روش، «روش چسبنده» می‌گویند. اما با استفاده از افزونه‌ی «jquery.unobtrusive-ajax.min.js» مایکروسافت، می‌توان این کدهای چسبنده را تبدیل به کدهای غیرچسنبده یا Unobtrusive کرد. در این حالت، پارامترهای متد ajax، به صورت ویژگی‌ها (attributes) به شکل data-ajax به المان‌های مختلف صفحه اضافه می‌شوند و به این ترتیب، افزونه‌ی یاد شده به صورت خودکار با یافتن مقادیر ویژگی‌های data-ajax، این المان‌ها را تبدیل به المان‌های ای‌جکسی می‌کند. در این حالت به کدهایی تمیزتر و عاری از متدهای چسبنده‌ی ajax قرار گرفته‌ی در داخل صفحات وب خواهیم رسید.
روش طراحی Unobtrusive را در کتابخانه‌های معروفی مانند بوت استرپ هم می‌توان مشاهده کرد.


پیشنیازهای فعال سازی Unobtrusive Ajax در ASP.NET Core 1.0

توزیع افزونه‌ی «jquery.unobtrusive-ajax.min.js» مایکروسافت، از طریق bower صورت می‌گیرد که پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودی‌های کاربران» با آن آشنا شدیم. در اینجا نیز برای دریافت آن، تنها کافی است فایل bower.json را به نحو ذیل تکمیل کرد:
{
  "name": "asp.net",
  "private": true,
  "dependencies": {
   "bootstrap": "3.3.6",
   "jquery": "2.2.0",
   "jquery-validation": "1.14.0",
   "jquery-validation-unobtrusive": "3.2.6",
   "jquery-ajax-unobtrusive": "3.2.4"
  }
}
و پس از آن فایل bundleconfig.json مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 21 - بررسی تغییرات Bundling و Minification» یک چنین شکلی را پیدا می‌کند:
[
  {
   "outputFileName": "wwwroot/css/site.min.css",
   "inputFiles": [
    "bower_components/bootstrap/dist/css/bootstrap.min.css",
    "content/site.css"
   ]
  },
  {
   "outputFileName": "wwwroot/js/site.min.js",
   "inputFiles": [
    "bower_components/jquery/dist/jquery.min.js",
    "bower_components/jquery-validation/dist/jquery.validate.min.js",
    "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",
    "bower_components/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js",
    "bower_components/bootstrap/dist/js/bootstrap.min.js"
   ],
   "minify": {
    "enabled": true,
    "renameLocals": true
   },
   "sourceMap": false
  }
]
در اینجا فایل‌های css و اسکریپت مورد نیاز برنامه، به ترتیب اضافه شده و یکی خواهند شد. خروجی نهایی آن‌ها به شکل زیر در صفحات وب مورد استفاده قرار می‌گیرند:
<!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="~/css/site.min.css" rel="stylesheet" />
</head>
<body>
    <div>
        <div>
                @RenderBody()
        </div>
    </div>

    <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>
در اینجا تنها دو فایل نهایی این عملیات، یعنی css/site.min.css و js/site.min.js به صفحه الحاق شده‌اند که حاوی تمام پیشنیازهای اسکریپتی و شیوه‌نامه‌های برنامه هستند و در این حالت دیگر نیاز به افزودن آن‌ها به دیگر صفحات سایت نیست.


استفاده از معادل‌های واقعی Unobtrusive Ajax در ASP.NET Core 1.0

واقعیت این است که HTML Helper ای‌جکسی حذف شده‌ی از ASP.NET Core 1.0، کاری بجز افزودن ویژگی‌های data-ajax را که توسط افزونه‌ی jquery.unobtrusive-ajax.min.js پردازش می‌شوند، انجام نمی‌دهد و این افزونه مستقل است از مباحث سمت سرور و به نگارش خاصی از ASP.NET گره نخورده است. بنابراین در اینجا تنها کاری را که باید انجام داد، استفاده از همان ویژگی‌های اصلی است که این افزونه قادر به شناسایی آن‌ها است.
خلاصه‌ی آن‌ها را جهت انتقال کدهای قدیمی و یا تهیه‌ی کدهای جدید، در جدول ذیل می‌توانید مشاهده کنید:

 HTML attribute   AjaxOptions 
 data-ajax-confirm   Confirm 
 data-ajax-method   HttpMethod 
 data-ajax-mode   InsertionMode 
 data-ajax-loading-duration   LoadingElementDuration 
 data-ajax-loading   LoadingElementId 
 data-ajax-begin   OnBegin 
 data-ajax-complete   OnComplete 
 data-ajax-failure   OnFailure 
 data-ajax-success   OnSuccess 
 data-ajax-update   UpdateTargetId 
 data-ajax-url   Url 
   
در ASP.NET Core 1.0، به علت حذف متدهای کمکی Ajax دیگر خبری از AjaxOptions نیست. اما اگر علاقمند به انتقال کدهای قدیمی به ASP.NET Core 1.0 هستید، معادل‌های اصلی این پارامترها را می‌توانید در ستون HTML attribute مشاهده کنید.

چند نکته:
- اگر قصد استفاده‌ی از این ویژگی‌ها را دارید، باید ویژگی "data-ajax="true را نیز حتما قید کنید تا سیستم Unobtrusive Ajax فعال شود.
- ویژگی data-ajax-mode تنها با ذکر data-ajax-update (و یا همان UpdateTargetId پیشین) معنا پیدا می‌کند.
- ویژگی data-ajax-loading-duration نیاز به ذکر data-ajax-loading (و یا همان LoadingElementId پیشین) را دارد.
- ویژگی data-ajax-mode مقادیر before، after و replace-with را می‌پذیرد. اگر قید نشود، کل المان با data دریافتی جایگزین می‌شود.
- سه callback قابل تعریف data-ajax-complete، data-ajax-failure و data-ajax-success، یک چنین پارامترهایی را از سمت سرور در اختیار کلاینت قرار می‌دهند:

parameters  
 Callback  
 xhr, status   data-ajax-complete 
 data, status, xhr   data-ajax-success 
 xhr, status, error   data-ajax-failure 

برای مثال می‌توان ویژگی data-ajax-success را به نحو ذیل در سمت کلاینت مقدار دهی کرد:
 data-ajax-success = "myJsMethod"
این متد جاوا اسکریپتی یک چنین امضایی را دارد:
  function myJsMethod(data, status, xhr) {
}
در این حالت در سمت سرور، پارامتر data در یک اکشن متد، به صورت ذیل مقدار دهی می‌شود:
 return Json(new { param1 = 1, param2 = 2, ... });
و در سمت کلاینت در متد myJsMethod این پارامترها را به صورت data.param1 می‌توان دریافت کرد.


مثال‌هایی از افزودن ویژگی‌های data-ajax به المان‌های مختلف

 در حالت استفاده از Form Tag Helpers که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» بررسی شدند، یک فرم ای‌جکسی، چنین تعاریفی را پیدا خواهد کرد:
با این ViewModel فرضی
using System.ComponentModel.DataAnnotations;
 
namespace Core1RtmEmptyTest.ViewModels.Account
{
    public class RegisterViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }
}
که در View متناظر Ajax ایی ذیل استفاده شده‌است:
@using Core1RtmEmptyTest.ViewModels.Account
@model RegisterViewModel
@{
}
 
<form method="post"
      asp-controller="TestAjax"
      asp-action="Index"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      class="form-horizontal"
      role="form"
      data-ajax="true"
      data-ajax-loading="#Progress"
      data-ajax-success="myJsMethod">
 
    <input asp-for="Email" class="form-control" />
    <span asp-validation-for="Email" class="text-danger"></span>
 
    <button type="submit">ارسال</button>
 
    <div id="Progress" style="display: none">
        <img src="images/loading.gif" alt="loading..." />
    </div>
</form>
 
@section scripts{
    <script type="text/javascript">
        function myJsMethod(data, status, xhr) {
            alert(data.param1);
        }
    </script>
}
در اینجا تمام تعاریف مانند قبل است؛ تنها سه ویژگی data-ajax جهت فعال سازی jquery-ajax-unobtrusive به فرم اضافه شده‌اند. همچنین یک callback دریافت پیام موفقیت آمیز بودن عملیات Ajax ایی نیز تعریف شده‌است.

این View از کنترلر ذیل استفاده می‌کند:
using Core1RtmEmptyTest.ViewModels.Account;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class TestAjaxController : Controller
    {
 
        public IActionResult Index()
        {
            return View();
        }
 
        [HttpPost]
        public IActionResult Index([FromForm]RegisterViewModel vm)
        {
            var ajax = isAjax();
            if (ajax)
            {
                // it's an ajax post
            }
 
 
            if (ModelState.IsValid)
            {
                //todo: save data
 
                return Json(new { param1 = 1, param2 = 2 });
            }
            return View();
        }
 
        private bool isAjax()
        {
            return Request?.Headers != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest";
        }
    }
}
به ASP.NET Core 1.0، متد کمکی IsAjax اضافه نشده‌است؛ اما تعریف آن‌را در این کنترلر مشاهده می‌کنید. در مورد قید FromForm در ادامه توضیح داده خواهد شد (هرچند در این مورد خاص، حالت پیش فرض است و الزامی به قید آن نیست).

و یا Action Link ای‌جکسی نیز به صورت خلاصه به این نحو قابل تعریف است:
<div id="EmployeeInfo">
<a 
 asp-controller="MyController" asp-action="MyAction"
 data-ajax="true" 
 data-ajax-loading="#Progress" 
 data-ajax-method="POST" 
 data-ajax-mode="replace" 
 data-ajax-update="#EmployeeInfo">
 Get Employee-1 info
</a>

  <div id="Progress" style="display: none">
    <img src="images/loading.gif" alt="loading..."  />
  </div>
</div>


نکته‌ای در مورد اکشن متدهای ای‌جکسی در ASP.NET Core 1.0

همانطور که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API»، قسمت «تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API» نیز ذکر شد:
public IActionResult Index([FromBody] MyViewModel vm)
{
   return View();
}
ذکر ویژگی FromBody در اینجا الزامی است. از این جهت که اطلاعات با فرمت JSON، از قسمت body درخواست استخراج و به MyViewModel بایند خواهند شد (در حالت dataType: json). و اگر dataType : application/x-www-form-urlencoded; charset=utf-8 بود (مانند حالت پیش فرض Unobtrusive Ajax)، باید از ویژگی FromForm استفاده شود. در غیر اینصورت در سمت سرور نال دریافت خواهیم کرد.
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت اول - نصب پیشنیازها
به روز رسانی: ارتقاء به نگارش «2.0.0rc.0 »

برای ارتقاء به نگارش RC0، این مراحل را باید طی کنید:
1) پیش از هر کاری، پوشه‌ی node_modules قدیمی خود را حذف کنید (با تمام محتوای آن).
2) به روز رسانی فایل package.json به صورت ذیل:
{
    "name": "asp-net-mvc5x-angular2x",
    "version": "1.0.0",
    "author": "DNT",
    "description": "",
    "scripts": {
        "postinstall": "typings install"
    },
    "license": "Apache-2.0",
    "dependencies": {
        "@angular/common": "^2.0.0-rc.0",
        "@angular/compiler": "^2.0.0-rc.0",
        "@angular/core": "^2.0.0-rc.0",
        "@angular/http": "2.0.0-rc.0",
        "@angular/router": "2.0.0-rc.0",
        "@angular/router-deprecated": "^2.0.0-rc.0",
        "@angular/platform-browser": "^2.0.0-rc.0",
        "@angular/platform-browser-dynamic": "^2.0.0-rc.0",
        "bootstrap": "^3.3.6",
        "es6-promise": "^3.1.2",
        "es6-shim": "^0.35.0",
        "jquery": "^2.2.3",
        "reflect-metadata": "^0.1.3",
        "rxjs": "^5.0.0-beta.6",
        "systemjs": "^0.19.27",
        "zone.js": "^0.6.12"
    },
    "devDependencies": {
        "typescript": "^1.8.9",
        "typings": "^0.8.1"
    },
    "repository": { }
}
به روز شده‌ی محتوای فوق، همیشه در آدرس مستندات npm packages موجود است.
3) افزودن فایلی به نام typings.json در ریشه‌ی پروژه؛ با این محتوا:
{
    "ambientDependencies": {
        "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd"
    }
}
این فایل توسط قسمت «postinstall» اسکریپت package.json نصب می‌شود. اما چون مسیر https://raw.githubusercontent.com قابل دسترسی نیست (از این طرف البته!)، موفق به دریافت آن نخواهید شد. بنابراین یک پوشه را به نام typings به ریشه‌ی سایت اضافه کنید و سپس فایل ذیل را به آن اضافه نمائید:
es6-shim.d.ts
بدون این فایل، کامپایلر TypeScript تعاریف ES 6 را مانند Map و Promise و امثال آن‌، نمی‌شناسد و پروژه را کامپایل نخواهد کرد.

اکنون یکبار فایل package.json را ذخیره کنید تا کار به روز رسانی بسته‌ها انجام شود. البته اگر بر روی این فایل کلیک راست کنید، در منوی ظاهر شده، گزینه‌ی restore packages هم موجود است.

4) پس از آن، چند تغییر جزئی ذیل باید در کدهای این سری، اعمال شوند:
هر جایی angular2 تعریف شده، اینبار می‌شود angular@. مثلا:
import { PipeTransform, Pipe } from '@angular/core';
فایل مسیریابی آن قرار است تغییرات کلی داشته باشد. این مورد به صورت ذیل تغییر نام یافته است:
import { RouteParams, Router } from '@angular/router-deprecated';
5) فایل main.ts (قسمت دوم) به صورت ذیل تغییر کرده‌است:
/// <reference path="../typings/es6-shim.d.ts" />
import { bootstrap } from '@angular/platform-browser-dynamic';

// Our main component
import { AppComponent } from "./app.component";

bootstrap(AppComponent);
6) تعاریف اسکریپت‌های Index.html سایت، اینبار به نحو ذیل تغییر کرده‌اند:
یک نکته: اگر می‌خواهید این تعاریف را در یک فایل razor، وارد کنید، چون @ به ابتدای پوشه‌ی angular2 اضافه شده (node_modules\@angular)، مشکل پردازشی razor را ایجاد خواهد کرد و باید escape شود. به همین جهت بجای @ بهتر است معادل آن را یعنی ("@")Html.Raw@   وارد کنید.
سپس ابتدا فایل systemjs.config.js را از اینجا دریافت کنید.
در ادامه مداخل جدید را هم در فایل index.html مثال رسمی آغازین آن بررسی کنید.

بنابراین، فایل systemjs.config.js را  به ریشه‌ی سایت اضافه کنید (از این جهت که مسیر بسته‌های node_modules را از ریشه‌ی سایت می‌خواند). سپس فایل Views\Shared\_Layout.cshtml را به نحو ذیل تغییر دهید:
<!DOCTYPE html>
<html>
<head>
    <base href="/">
    <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="~/node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet"/>
    <link href="~/app/app.component.css" rel="stylesheet"/>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css"/>

    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="~/node_modules/es6-shim/es6-shim.min.js"></script>

    <script src="~/node_modules/zone.js/dist/zone.js"></script>
    <script src="~/node_modules/reflect-metadata/Reflect.js"></script>
    <script src="~/node_modules/systemjs/dist/system.src.js"></script>

    <script src="~/systemjs.config.js"></script>

    <!-- 2. Configure SystemJS -->
    <script>
        System.import('app/main').then(null, console.error.bind(console));
    </script>
</head>
<body>
    <div>
        @RenderBody()
        <pm-app>Loading App...</pm-app>
    </div>

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

خلاصه‌ی سریع این موارد
الف) تغییرات آخرین بسته‌های npm را از مستندات آن پیگیری و اعمال کنید. آخرین نگارش آن همیشه در اینجا قابل دسترسی است.
ب) تغییرات index.html، فایل main.ts و مداخل آغازین آن‌را از مثال آغازین رسمی آن پیگیری و اعمال کنید.