نظرات مطالب
مبانی TypeScript؛ ماژول‌ها
- با هربار تغییر فایل tsconfig.json، کامپایل دوباره‌ی پروژه را فراموش نکنید (مهم). از منوی build گزینه‌ی rebuild solution را انتخاب کنید. این rebuild، کار کامپایل مجدد فایل‌های ts. را هم انجام می‌دهد.
- commonjs بیشتر برای برنامه‌های nodejs استفاده می‌شود. اگر علاقمند باشید که با سیستمی شبیه به AngularJS 2.0 کار کنید، از یک module loader ویژه، به نام SystemJS استفاده کنید (که قابلیت بارگذاری خودکار ES6 modules, AMD, CommonJS را دارد). بنابراین فایل tsconfig.json را به این صورت تغییر دهید:
{
    "compileOnSave": true,
    "compilerOptions": {
        "target": "es5",
        "module": "system",
        "sourceMap": true
    }
}
بعد فایل index.html شما چنین شکلی را پیدا می‌کند:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css"/>

    <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.22/system.js"></script>
    <script type="text/javascript">
        System.defaultJSExtensions = true;
        System.import('app');
    </script>
</head>
<body>
    <h1>TypeScript HTML App</h1>

    <div id="content"></div>
</body>
</html>
در اینجا System.JS کار بارگذاری اولین ماژول برنامه یا همان app.js را به صورت خودکار انجام می‌دهد (و همچنین تمام ماژول‌های مرتبط با آن‌را). بنابراین دیگر نیازی به ذکر اسکریپت‌های برنامه در اینجا نیست (هیچکدام از آن‌ها، منهای موارد عمومی مثل خود system.js).
بعد فایل app.ts را هم به این صورت تغییر دهید، چون این کدها پس از onload اجرا می‌شوند:
import {Book} from "./testmd";

let book: Book = new Book();
console.log(book.bookName);
document.getElementById("content").innerText = book.GetbookNmae;
مطالب
تهیه قالب برای ایمیل‌های ارسالی یک برنامه ASP.Net

فرض کنید ایمیل اطلاع رسانی برنامه ASP.Net شما قرار است ایمیل زیر را پس از تکمیل یک فرم ارسال کند.


برای ارسال این قالب که مطابق تصویر هر بار باید سه برچسب آن تغییر کند چه راهی را پیشنهاد می‌دهید؟

راه اول: (راه متداول)
این فرم را در یک html editor درست کرده و جای سه برچسب را خالی می‌گذاریم. سپس html مورد نظر را در تابع ارسال ایمیل خود به صورت یک رشته تعریف نموده و جاهای خالی را پر خواهیم کرد. مثلا:
            string Name = "علی";
string Desc = "منابع مورد نیاز";
int Number = 10;
string content =
"<div dir=\"rtl\" style=\"text-align: right; font-family:Tahoma; font-size:9pt\">" +
"با سلام<br />" +
"احتراما آقای/خانم" +
Name +
"&nbsp;درخواست چاپ" +
Desc +
"&nbsp;دارای" +
Number +
"&nbsp;صفحه را داده‌اند. لطفا جهت تائید درخواست ایشان به برنامه مراجعه بفرمائید.<br />" +
"<br />" +
"با تشکر</div>";

ایرادات:
  • الف) امکان مشاهده شکل نهایی تا زمانیکه ایمیل مورد نظر را دریافت نکرده باشیم، وجود ندارد.
  • ب) اعمال تغییرات جدید به این فرمت رشته‌ای مشکل است. همیشه استفاده از ابزارهای بصری برای بهبود کار کمک بزرگی هستند که در این حالت از آن‌ها محروم خواهیم شد.
  • ج)اگر تغییر رسیده جدید، درخواست اضافه کردن لیست پرینت‌های قبلی این شخص بود چه باید کرد؟ آیا جدول مورد نظر را باید به صورت دستی ایجاد و باز هم به صورت یک رشته به این مجموعه اضافه کرد؟ در این حالت از کنترل‌های استانداردی مانند GridView و امثال آن محروم خواهیم شد.
  • د) هر بار تغییر، نیاز به recompile برنامه دارد.
بنابراین همانطور که مشاهده می‌کنید، نگهداری این روش مشکل است.

راه دوم: استفاده از قالب‌ها

خوشبختانه در ASP.Net امکان رندر کردن کنترل‌ها به صورت یک string‌ نیز موجود است. در مثال ما نیاز است تا چندین کنترل در کنار هم قرار گیرند تا شکل نهایی را ایجاد کنند. بنابراین می‌توان تمام آنها را در یک یوزر کنترل قرار داد. سپس باید کل یوزر کنترل را به صورت یک رشته، رندر کرد که در ادامه به آن خواهیم پرداخت.

اگر قالب فوق را بخواهیم در یک یوزر کنترل طراحی کنیم، سورس صفحه html یوزر کنترل به صورت زیر خواهد بود (فایل WebUserControl1.ascx) :
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="WebUserControl1.ascx.cs"
Inherits="testWebForms87.WebUserControl1" %>
<div dir="rtl" style="text-align: right; font-family:Tahoma; font-size:9pt">
با سلام<br />
احتراما آقای/خانم
<asp:Label ID="lblName" runat="server"></asp:Label>
&nbsp;درخواست چاپ
<asp:Label ID="lblDesc" runat="server"></asp:Label>
&nbsp;دارای
<asp:Label ID="lblNumber" runat="server"></asp:Label>
&nbsp;صفحه را داده‌اند. لطفا جهت تائید درخواست ایشان به برنامه مراجعه بفرمائید.<br />
<br />
با تشکر</div>

همچنین می‌توان مقادیر برچسب‌ها را از طریق خواصی که برای یوزر کنترل تعریف خواهیم کرد، در تابع ارسال ایمیل خود مقدار دهی نمائیم. در این حالت در سورس صفحه یوزر کنترل داریم (فایل WebUserControl1.ascx.cs) :
        public string Name { get; set; }
public int Number { get; set; }
public string Desc { get; set; }

protected void Page_Load(object sender, EventArgs e)
{
lblNumber.Text = Number.ToString();
lblName.Text = Name;
lblDesc.Text = Desc;
}

تا اینجا طراحی اولیه محتوای ایمیل به پایان می‌رسد. دیگر از آن رشته کذایی خبری نیست و همچنین می‌توان از designer ویژوال استودیو برای طراحی بصری قالب مورد نظر استفاده کرد که این مزیت بزرگی است و در آینده اگر نیاز به تغییر متن ایمیل ارسالی وجود داشت، تنها کافی است فایل ascx ما ویرایش شود (بدون نیاز به کامپایل مجدد پروژه). یا در اینجا به سادگی برای مثال می‌توان یک GridView را تعریف، طراحی و bind کرد.

مرحله بعد، رندر کردن خودکار این یوزر کنترل و سپس تبدیل محتوای حاصل به یک رشته است. برای این منظور از تابع زیر می‌توان کمک گرفت (برای مثال تعریف شده در کلاس دلخواه CLoadUC) :
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Web;
using System.Web.UI;

/// <summary>
/// تبدیل یک یوزر کنترل به معادل اچ تی ام ال آن
/// </summary>
/// <param name="path">مسیر یوزر کنترل</param>
/// <param name="properties">لیست خواص به همراه مقادیر مورد نظر</param>
/// <returns></returns>
/// <exception cref="NotImplementedException"><c>NotImplementedException</c>.</exception>
public static string RenderUserControl(string path,
List<KeyValuePair<string,object>> properties)
{
Page pageHolder = new Page();

UserControl viewControl =
(UserControl)pageHolder.LoadControl(path);

Type viewControlType = viewControl.GetType();

foreach (var pair in properties)
{
PropertyInfo property =
viewControlType.GetProperty(pair.Key);

if (property != null)
{
property.SetValue(viewControl, pair.Value, null);
}
else
{
throw new NotImplementedException(string.Format(
"UserControl: {0} does not have a public {1} property.",
path, pair.Key));
}
}

pageHolder.Controls.Add(viewControl);
StringWriter output = new StringWriter();
HttpContext.Current.Server.Execute(pageHolder, output, false);
return output.ToString();
}

ماخذ اصلی تابع فوق این آدرس است.
که البته تابع نهایی آنرا کمی اصلاح کردم تا بتوان لیستی از خواص پابلیک یک یوزر کنترل را به آن پاس کرد و محدود به یک خاصیت نبود.
اکنون استفاده از یوزر کنترلی که تاکنون طراحی کرده‌ایم به سادگی زیر است:
            List<KeyValuePair<string, object>> lst =
new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("Name", "علی"),
new KeyValuePair<string, object>("Number", 10),
new KeyValuePair<string, object>("Desc", "منابع مورد نیاز")
};

string content = CLoadUC.RenderUserControl("WebUserControl1.ascx", lst);

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

مطالب
ASP.NET Web API - قسمت پنجم
مدیریت کدهای وضعیت در Web API
تمامی پاسخ‌های دریافتی از Web API توسط Client، باید در قالب کدهای وضعیت HTTP باشند. دو کلاس جدید با نام‌های HttpResponseMessage و HttpResponseException همراه با ASP.NET MVC 4 معرفی شده اند که ارسال کدهای وضعیت پردازش درخواست به Client را آسان می‌سازند. به عنوان مثال، ارسال وضعیت برای چهار عمل اصلی بازیابی، ایجاد، آپدیت و حذف رکورد را بررسی می‌کنیم.

بازیابی رکورد
بر اساس مستندات پروتوکل HTTP، در صورتی که منبع درخواستی Client پیدا نشد، باید کد وضعیت 404 برگشت داده شود. این حالت را در متد ذیل پیاده سازی کرده ایم.
public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)); 
    }
    return item;
}
در صورتی که رکوردی با مشخصه‌ی درخواستی پیدا نشد، با استفاده از کلاس HttpResponseException، خطایی به Client ارسال خواهد شد. پارامتر سازنده‌ی این کلاس، شی ای از نوع کلاس HttpResponseMessage است. سازنده‌ی کلاس HttpResponseMessage، مقداری از یک enum با نام HttpStatusCode را می‌پذیرد. مقدار NotFound، نشان از خطای 404 است و زمانی به کار می‌رود که منبع درخواستی وجود نداشته باشد. اگر محصول درخواست شده یافت شد، در قالب JSON برگشت داده می‌شود. در شکل ذیل، پاسخ دریافتی در زمان درخواست محصولی که وجود ندارد را ملاحظه می‌کنید.

ایجاد رکورد
برای ایجاد رکورد، Client درخواستی از نوع POST را همراه با داده‌های رکورد در بدنه‌ی درخواست به Server ارسال می‌کند. در ذیل، پیاده سازی ساده ای از این حالت را مشاهده می‌کنید.
public Product PostProduct(Product item)
{
    item = repository.Add(item);
    return item;
}
 این پیاده سازی کار می‌کند اما کمبودهایی دارد:
  • کد وضعیت پردازش درخواست: به طور پیش فرض، Web API، کد 200 را در پاسخ ارسال می‌کند، اما بر اساس مستندات پروتوکل HTTP، زمانی که یک درخواست از نوع POST منجر به تولید منبعی می-شود، Server باید کد وضعیت 201 را به Client برگشت بدهد.
  • آدرس منبع جدید ایجاد شده: بر اساس مستندات پروتوکل HTTP، زمانی که منبعی بر روی Server ایجاد می‌شود، باید آدرس منبع جدید ایجاد شده از طریق هدر Location به Client ارسال شود.
با توجه به این توضیحات، متد قبل به صورت ذیل در خواهد آمد.
public HttpResponseMessage PostProduct(Product item)
{
    item = repository.Add(item);
    var response = Request.CreateResponse(HttpStatusCode.Created, item);

    string uri = Url.Link("DefaultApi", new { id = item.Id });
    response.Headers.Location = new Uri(uri);
    return response;
}
همان طور که ملاحظه می‌کنید، خروجی متد از نوع کلاس HttpResponseMessage است، چون با استفاده از این نوع می‌توانیم جزئیات مورد نیاز را در مورد نتیجه‌ی پردازش درخواست به مرورگر ارسال کنیم. همچنین، داده‌های رکورد جدید نیز در بدنه‌ی پاسخ، با یک فرمت مناسب مانند XML یا JSON برگشت داده می‌شوند. با استفاده از متد CreateResponse کلاس Request و پاس دادن کد وضعیت و شی ای که قصد داریم به Client ارسال شود به این متد، شی ای از نوع کلاس HttpResponseMessage ایجاد می‌کنیم. آدرس منبع جدید نیز با استفاده از response.Headers.Location مشخص شده است. نمونه ای از پاسخ دریافت شده در سمت Client به صورت ذیل است.


آپدیت رکورد
آپدیت با استفاده از درخواست‌های از نوع PUT انجام می‌گیرد. یک مثال ساده در این مورد.

public void PutProduct(int id, Product product)
{
    product.Id = id;
    if (!repository.Update(product))
    {
        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));
    }
}
نام متد با عبارت Put آغاز شده است. بنابراین توسط Web API برای پردازش درخواست‌های از نوع PUT در نظر گرفته می‌شود. متد قبل، دو پارامتر ورودی دارد. id برای مشخصه‌ی محصول، و محصول آپدیت شده که در پارامتر دوم قرار می‌گیرد. مقدار پارامتر id از آدرس دریافت می‌شود و مقدار پارامتر product از بدنه‌ی درخواست. به طور پیش فرض، Web API، مقدار داده هایی با نوع ساده مانند int، string و bool را از طریق route، و مقدار نوع‌های پیچیده‌تر مانند داده‌های یک کلاس را از بدنه‌ی درخواست می‌خواند.

حذف یک رکورد
حذف یک رکورد، با استفاده از درخواست‌های از نوع DELETE انجام می‌گیرد. یک مثال ساده در این مورد.
public HttpResponseMessage DeleteProduct(int id)
{
    repository.Remove(id);
    return new HttpResponseMessage(HttpStatusCode.NoContent);
}
بر اساس مستندات پروتکل HTTP، اگر منبعی که Client قصد حذف آن را دارد از پیش حذف شده است، نباید خطایی به وی گزارش شود. معمولاً در متدهایی که وظیفه‌ی حذف منبع را بر عهده دارند، کد 204 مبنی بر پردازش کامل درخواست و پاسخ خالی برگشت داده می‌شود. این کد با استفاد از مقدار NoContent برای HttpStatusCode مشخص می‌شود.

فراخوانی متدها و مدیریت کدهای وضعیت HTTP در سمت Client
حال ببینیم چگونه می‌توان از متدهای قبل در سمت Client استفاده و خطاهای احتمالی آنها را مدیریت کرد.
بهتر است مثال را برای حالتی که در آن رکوردی آپدیت می‌شود بررسی کنیم. کدهای مورد نیاز برای فراخوانی متد PutProduct در سمت Client به صورت ذیل است.
var id = $("#myTextBox").val();

$.ajax({
  url: "/api/Test/" + id,
   type: 'PUT',
   data: { Id: "1", Name: "Tomato Soup", Category: "Groceries", Price: "1.39M" },
   cache: false,
   statusCode: {
       200: function (data) {
           alert("آپدیت انجام شد");
        },
       404:
            function () {
                alert("خطا در آپدیت");
            }
      }
});
از متدهای get، getJson یا post در jQuery نمی‌توان برای عمل آپدیت استفاده نمود، چون Web API انتظار دارد تا نام فعل درخواستی، PUT باشد. اما با استفاده از متد ajax و ذکر نام فعل در پارامتر type آن می‌توان نوع درخواست را PUT تعریف کرد. خط 5 بدین منظور است. از طریق خصیصه‌ی statusCode نیز می‌توان کدهای وضعیت مختلف HTTP را بررسی کرد. دو کد 200 و 404 که به ترتیب نشان از موفقیت و عدم موفقیت در آپدیت رکورد هستند تعریف شده و پیغام مناسب به کاربر نمایش داده می‌شود.
در حالتی که آپدیت با موفقیت همراه باشد، بدنه‌ی پاسخ به شکل ذیل است.


  و در صورتی که خطایی رخ دهد، بدنه‌ی پاسخ دریافتی به صورت ذیل خواهد بود.


مطالب
نقش compile$ در Angular چیست؟

در مواقعی نیاز است تا محتوایی را به صورت داینامیک به کدهای موجود در یک app Angular  تزریق کنیم و یا محتوای موجود را تغییر دهیم. اگر این محتوای لود شده دارای دایرکتیوهای  Angularباشد بعد از لود محتوا متوجه می‌شویم که کدهای Angular ما کار نمی‌کنند. چرا؟  اینجاست که نقش  compile$  مشخص می‌شود. از طریق  compile$ محتوای لود شده مجددا در محدوده تحت مدیریت Angular قرار گرفته و مشکل حل می‌شود. مثال زیر را بررسی می‌کنیم :

app.directive("otcDynamic", function(){
    return {
        link: function(scope, element){
            element.html("<button ng-click='doSomething()'>{{label}}</button>");
        }
    };
});

در کد فوق  ما یک دکمه را که در کلیک آن قرار است متدی توسط Angular فراخوانی شود، به صورت داینامیک ایجاد کرده و به المنت مورد نظرمان اضافه کرده‌ایم. دقت داشته باشید که این محتوا ممکن است مثلا توسط درخواست‌های Ajax از جایی فراخوانی شود. با کلیک بر روی دکمه‌ی ایجاد شده، متد doSomething اجرا نخواهد شد؛ چرا که Angular قادر به تشخیص HTML تزریق شده نمی‌باشد و در واقع اصلا از وجود کد ما خبری ندارد. برای حل این مشکل از compile $ استفاده می‌کنیم:

app.directive("otcDynamic", function($compile){
    return{
        link: function(scope, element){
            var template = "<button ng-click='doSomething()'>{{label}}</button>";
            var linkFn = $compile(template);
            var content = linkFn(scope);
            element.append(content);
        }
    }
});

با استفاده از compile $ در واقع ما کدهای خود را مجددا تحت مدیریت Angular قرار می‌دهیم. مثال فوق یک مثال ساده از این مبحث بود. در واقع کدهای تزریق شده می‌توانند شامل کل HTML تحت مدیریت Angular باشد که مثلا از طریق درخواست‌های Ajax به‌روز رسانی می‌شوند.

مطالب
ASP.NET MVC #18

اعتبار سنجی کاربران در ASP.NET MVC

دو مکانیزم اعتبارسنجی کاربران به صورت توکار در ASP.NET MVC در دسترس هستند: Forms authentication و Windows authentication.
در حالت Forms authentication، برنامه موظف به نمایش فرم لاگین به کاربر‌ها و سپس بررسی اطلاعات وارده توسط آن‌ها است. برخلاف آن، Windows authentication حالت یکپارچه با اعتبار سنجی ویندوز است. برای مثال زمانیکه کاربری به یک دومین ویندوزی وارد می‌شود، از همان اطلاعات ورود او به شبکه داخلی، به صورت خودکار و یکپارچه جهت استفاده از برنامه کمک گرفته خواهد شد و بیشترین کاربرد آن در برنامه‌های نوشته شده برای اینترانت‌های داخلی شرکت‌ها است. به این ترتیب کاربران یک بار به دومین وارد شده و سپس برای استفاده از برنامه‌های مختلف ASP.NET، نیازی به ارائه نام کاربری و کلمه عبور نخواهند داشت. Forms authentication بیشتر برای برنامه‌هایی که از طریق اینترنت به صورت عمومی و از طریق انواع و اقسام سیستم عامل‌ها قابل دسترسی هستند، توصیه می‌شود (و البته منعی هم برای استفاده در حالت اینترانت ندارد).
ضمنا باید به معنای این دو کلمه هم دقت داشت: هدف از Authentication این است که مشخص گردد هم اکنون چه کاربری به سایت وارد شده است. Authorization، سطح دسترسی کاربر وارد شده به سیستم و اعمالی را که مجاز است انجام دهد، مشخص می‌کند.


فیلتر Authorize در ASP.NET MVC

یکی دیگر از فیلترهای امنیتی ASP.NET MVC به نام Authorize، کار محدود ساختن دسترسی به متدهای کنترلرها را انجام می‌دهد. زمانیکه اکشن متدی به این فیلتر یا ویژگی مزین می‌شود، به این معنا است که کاربران اعتبارسنجی نشده، امکان دسترسی به آن‌را نخواهند داشت. فیلتر Authorize همواره قبل از تمامی فیلترهای تعریف شده دیگر اجرا می‌شود.
فیلتر Authorize با پیاده سازی اینترفیس System.Web.Mvc.IAuthorizationFilter توسط کلاس System.Web.Mvc.AuthorizeAttribute در دسترس می‌باشد. این کلاس علاوه بر پیاده سازی اینترفیس یاد شده، دارای دو خاصیت مهم زیر نیز می‌باشد:

public string Roles { get; set; } // comma-separated list of role names
public string Users { get; set; } // comma-separated list of usernames

زمانیکه فیلتر Authorize به تنهایی بکارگرفته می‌شود، هر کاربر اعتبار سنجی شده‌ای در سیستم قادر خواهد بود به اکشن متد مورد نظر دسترسی پیدا کند. اما اگر همانند مثال زیر، از خواص Roles و یا Users نیز استفاده گردد، تنها کاربران اعتبار سنجی شده مشخصی قادر به دسترسی به یک کنترلر یا متدی در آن خواهند شد:

[Authorize(Roles="Admins")]
public class AdminController : Controller
{
  [Authorize(Users="Vahid")]
  public ActionResult DoSomethingSecure()
   {
  }
}

در این مثال، تنها کاربرانی با نقش Admins قادر به دسترسی به کنترلر جاری Admin خواهند بود. همچنین در بین این کاربران ویژه، تنها کاربری به نام Vahid قادر است متد DoSomethingSecure را فراخوانی و اجرا کند.

اکنون سؤال اینجا است که فیلتر Authorize چگونه از دو مکانیزم اعتبار سنجی یاد شده استفاده می‌کند؟ برای پاسخ به این سؤال، فایل web.config برنامه را باز نموده و به قسمت authentication آن دقت کنید:

<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

به صورت پیش فرض، برنامه‌های ایجاد شده توسط VS.NET جهت استفاده از حالت Forms یا همان Forms authentication تنظیم شده‌اند. در اینجا کلیه کاربران اعتبار سنجی نشده، به کنترلری به نام Account و متد LogOn در آن هدایت می‌شوند.
برای تغییر آن به حالت اعتبار سنجی یکپارچه با ویندوز، فقط کافی است مقدار mode را به Windows تغییر داد و تنظیمات forms آن‌را نیز حذف کرد.


یک نکته: اعمال تنظیمات اعتبار سنجی اجباری به تمام صفحات سایت
تنظیم زیر نیز در فایل وب کانفیگ برنامه، همان کار افزودن ویژگی Authorize را انجام می‌دهد با این تفاوت که تمام صفحات سایت را به صورت خودکار تحت پوشش قرار خواهد داد (البته منهای loginUrl ایی که در تنظیمات فوق مشاهده نمودید):

<authorization>
<deny users="?" />
</authorization>

در این حالت دسترسی به تمام آدرس‌های سایت تحت تاثیر قرار می‌گیرند، منجمله دسترسی به تصاویر و فایل‌های CSS و غیره. برای اینکه این موارد را برای مثال در حین نمایش صفحه لاگین نیز نمایش دهیم، باید تنظیم زیر را پیش از تگ system.web به فایل وب کانفیگ برنامه اضافه کرد:

<!-- we don't want to stop anyone seeing the css and images -->
<location path="Content">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>

در اینجا پوشه Content از سیستم اعتبارسنجی اجباری خارج می‌شود و تمام کاربران به آن دسترسی خواهند داشت.
به علاوه امکان امن ساختن تنها قسمتی از سایت نیز میسر است؛ برای مثال:

<location path="secure">
  <system.web>
    <authorization>
      <allow roles="Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
</location>

در اینجا مسیری به نام secure، نیاز به اعتبارسنجی اجباری دارد. به علاوه تنها کاربرانی در نقش Administrators به آن دسترسی خواهند داشت.


نکته: به تنظیمات انجام شده در فایل Web.Config دقت داشته باشید
همانطور که می‌شود دسترسی به یک مسیر را توسط تگ location بازگذاشت، امکان بستن آن هم فراهم است (بجای allow از deny استفاده شود). همچنین در ASP.NET MVC به سادگی می‌توان تنظیمات مسیریابی را در فایل global.asax.cs تغییر داد. برای مثال اینبار مسیر دسترسی به صفحات امن سایت، Admin خواهد بود نه Secure. در این حالت چون از فیلتر Authorize استفاده نشده و همچنین فایل web.config نیز تغییر نکرده، این صفحات بدون محافظت رها خواهند شد.
بنابراین اگر از تگ location برای امن سازی قسمتی از سایت استفاده می‌کنید، حتما باید پس از تغییرات مسیریابی، فایل web.config را هم به روز کرد تا به مسیر جدید اشاره کند.
به همین جهت در ASP.NET MVC بهتر است که صریحا از فیلتر Authorize بر روی کنترلرها (جهت اعمال به تمام متدهای آن) یا بر روی متدهای خاصی از کنترلرها استفاده کرد.
امکان تعریف AuthorizeAttribute در فایل global.asax.cs و متد RegisterGlobalFilters آن به صورت سراسری نیز وجود دارد. اما در این حالت حتی صفحه لاگین سایت هم دیگر در دسترس نخواهد بود. برای رفع این مشکل در ASP.NET MVC 4 فیلتر دیگری به نام AllowAnonymousAttribute معرفی شده است تا بتوان قسمت‌هایی از سایت را مانند صفحه لاگین، از سیستم اعتبارسنجی اجباری خارج کرد تا حداقل کاربر بتواند نام کاربری و کلمه عبور خودش را وارد نماید:

[System.Web.Mvc.AllowAnonymous]
public ActionResult Login()
{
return View();
}

بنابراین در ASP.NET MVC 4.0، فیلتر AuthorizeAttribute را سراسری تعریف کنید. سپس در کنترلر لاگین برنامه از فیلتر AllowAnonymous استفاده نمائید.
البته نوشتن فیلتر سفارشی AllowAnonymousAttribute در ASP.NET MVC 3.0 نیز میسر است. برای مثال:

public class LogonAuthorize : AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext) {
if (!(filterContext.Controller is AccountController))
base.OnAuthorization(filterContext);
}
}

در این فیلتر سفارشی، اگر کنترلر جاری از نوع AccountController باشد، از سیستم اعتبار سنجی اجباری خارج خواهد شد. مابقی کنترلرها همانند سابق پردازش می‌شوند. به این معنا که اکنون می‌توان LogonAuthorize را به صورت یک فیلتر سراسری در فایل global.asax.cs معرفی کرد تا به تمام کنترلرها، منهای کنترلر Account اعمال شود.



مثالی جهت بررسی حالت Windows Authentication

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس یک کنترلر جدید را به نام Home نیز به آن اضافه کنید. در ادامه متد Index آن‌را با ویژگی Authorize، مزین نمائید. همچنین بر روی نام این متد کلیک راست کرده و یک View خالی را برای آن ایجاد کنید:

using System.Web.Mvc;

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

محتوای View متناظر با متد Index را هم به شکل زیر تغییر دهید تا نام کاربر وارد شده به سیستم را نمایش دهد:

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>
Current user: @User.Identity.Name

به علاوه در فایل Web.config برنامه، حالت اعتبار سنجی را به ویندوز تغییر دهید:

<authentication mode="Windows" />

اکنون اگر برنامه را اجرا کنید و وب سرور آزمایشی انتخابی هم IIS Express باشد، پیغام HTTP Error 401.0 - Unauthorized نمایش داده می‌شود. علت هم اینجا است که Windows Authentication به صورت پیش فرض در این وب سرور غیرفعال است. برای فعال سازی آن به مسیر My Documents\IISExpress\config مراجعه کرده و فایل applicationhost.config را باز نمائید. تگ windowsAuthentication را یافته و ویژگی enabled آن‌را که false است به true تنظیم نمائید. اکنون اگر برنامه را مجددا اجرا کنیم، در محل نمایش User.Identity.Name، نام کاربر وارد شده به سیستم نمایش داده خواهد شد.
همانطور که مشاهده می‌کنید در اینجا همه چیز یکپارچه است و حتی نیازی نیست صفحه لاگین خاصی را به کاربر نمایش داد. همینقدر که کاربر توانسته به سیستم ویندوزی وارد شود، بر این اساس هم می‌تواند از برنامه‌های وب موجود در شبکه استفاده کند.



بررسی حالت Forms Authentication

برای کار با Forms Authentication نیاز به محلی برای ذخیره سازی اطلاعات کاربران است. اکثر مقالات را که مطالعه کنید شما را به مباحث membership مطرح شده در زمان ASP.NET 2.0 ارجاع می‌دهند. این روش در ASP.NET MVC هم کار می‌کند؛ اما الزامی به استفاده از آن نیست.

برای بررسی حالت اعتبار سنجی مبتنی بر فرم‌ها، یک برنامه خالی ASP.NET MVC جدید را آغاز کنید. یک کنترلر Home ساده را نیز به آن اضافه نمائید.
سپس نیاز است نکته «تنظیمات اعتبار سنجی اجباری تمام صفحات سایت» را به فایل وب کانفیگ برنامه اعمال نمائید تا نیازی نباشد فیلتر Authorize را در همه جا معرفی کرد. سپس نحوه معرفی پیش فرض Forms authentication تعریف شده در فایل web.config نیز نیاز به اندکی اصلاح دارد:

<authentication mode="Forms">
<!--one month ticket-->
<forms name=".403MyApp"
cookieless="UseCookies"
loginUrl="~/Account/LogOn"
defaultUrl="~/Home"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200"/>
</authentication>

در اینجا استفاده از کوکی‌ها اجباری شده است. loginUrl به کنترلر و متد لاگین برنامه اشاره می‌کند. defaultUrl مسیری است که کاربر پس از لاگین به صورت خودکار به آن هدایت خواهد شد. همچنین نکته‌ی مهم دیگری را که باید رعایت کرد، name ایی است که در این فایل config عنوان می‌‌کنید. اگر بر روی یک وب سرور، چندین برنامه وب ASP.Net را در حال اجرا دارید، باید برای هر کدام از این‌ها نامی جداگانه و منحصربفرد انتخاب کنید، در غیراینصورت تداخل رخ داده و گزینه مرا به خاطر بسپار شما کار نخواهد کرد.
کار slidingExpiration که در اینجا تنظیم شده است نیز به صورت زیر می‌باشد:
اگر لاگین موفقیت آمیزی ساعت 5 عصر صورت گیرد و timeout شما به عدد 10 تنظیم شده باشد، این لاگین به صورت خودکار در 5:10‌ منقضی خواهد شد. اما اگر در این حین در ساعت 5:05 ، کاربر، یکی از صفحات سایت شما را مرور کند، زمان منقضی شدن کوکی ذکر شده به 5:15 تنظیم خواهد شد(مفهوم تنظیم slidingExpiration). لازم به ذکر است که اگر کاربر پیش از نصف زمان منقضی شدن کوکی (مثلا در 5:04)، یکی از صفحات را مرور کند، تغییری در این زمان نهایی منقضی شدن رخ نخواهد داد.
اگر timeout ذکر نشود، زمان منقضی شدن کوکی ماندگار (persistent) مساوی زمان جاری + زمان منقضی شدن سشن کاربر که پیش فرض آن 30 دقیقه است، خواهد بود.

سپس یک مدل را به نام Account به پوشه مدل‌های برنامه با محتوای زیر اضافه نمائید:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication15.Models
{
public class Account
{
[Required(ErrorMessage = "Username is required to login.")]
[StringLength(20)]
public string Username { get; set; }

[Required(ErrorMessage = "Password is required to login.")]
[DataType(DataType.Password)]
public string Password { get; set; }

public bool RememberMe { get; set; }
}
}

همچنین مطابق تنظیمات اعتبار سنجی مبتنی بر فرم‌های فایل وب کانفیگ، نیاز به یک AccountController نیز هست:

using System.Web.Mvc;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn()
{
return View();
}

[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
return View();
}
}
}

در اینجا در حالت HttpGet فرم لاگین نمایش داده خواهد شد. بنابراین بر روی این متد کلیک راست کرده و گزینه Add view را انتخاب کنید. سپس در صفحه باز شده گزینه Create a strongly typed view را انتخاب کرده و مدل را هم بر روی کلاس Account قرار دهید. قالب scaffolding را هم Create انتخاب کنید. به این ترتیب فرم لاگین برنامه ساخته خواهد شد.
اگر به متد HttpPost فوق دقت کرده باشید، علاوه بر دریافت وهله‌ای از شیء Account، یک رشته را به نام returnUrl نیز تعریف کرده است. علت هم اینجا است که سیستم Forms authentication، صفحه بازگشت را به صورت خودکار به شکل یک کوئری استرینگ به انتهای Url جاری اضافه می‌کند. مثلا:

http://localhost/Account/LogOn?ReturnUrl=something

بنابراین اگر یکی از پارامترهای متد تعریف شده به نام returnUrl باشد، به صورت خودکار مقدار دهی خواهد شد.

تا اینجا زمانیکه برنامه را اجرا کنیم، ابتدا بر اساس تعاریف مسیریابی پیش فرض برنامه، آدرس کنترلر Home و متد Index آن فراخوانی می‌گردد. اما چون در وب کانفیگ برنامه authorization را فعال کرده‌ایم، برنامه به صورت خودکار به آدرس مشخص شده در loginUrl قسمت تعاریف اعتبارسنجی مبتنی بر فرم‌ها هدایت خواهد شد. یعنی آدرس کنترلر Account و متد LogOn آن درخواست می‌گردد. در این حالت صفحه لاگین نمایان خواهد شد.

مرحله بعد، اعتبار سنجی اطلاعات وارد شده کاربر است. بنابراین نیاز است کنترلر Account را به نحو زیر بازنویسی کرد:

using System.Web.Mvc;
using System.Web.Security;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn(string returnUrl)
{
if (User.Identity.IsAuthenticated) //remember me
{
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect(FormsAuthentication.DefaultUrl);
}

return View(); // show the login page
}

[HttpGet]
public void LogOut()
{
FormsAuthentication.SignOut();
}

private bool shouldRedirect(string returnUrl)
{
// it's a security check
return !string.IsNullOrWhiteSpace(returnUrl) &&
Url.IsLocalUrl(returnUrl) &&
returnUrl.Length > 1 &&
returnUrl.StartsWith("/") &&
!returnUrl.StartsWith("//") &&
!returnUrl.StartsWith("/\\");
}

[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
if (this.ModelState.IsValid)
{
if (loginInfo.Username == "Vahid" && loginInfo.Password == "123")
{
FormsAuthentication.SetAuthCookie(loginInfo.Username, loginInfo.RememberMe);
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
FormsAuthentication.RedirectFromLoginPage(loginInfo.Username, loginInfo.RememberMe);
}
}
this.ModelState.AddModelError("", "The user name or password provided is incorrect.");
ViewBag.Error = "Login faild! Make sure you have entered the right user name and password!";
return View(loginInfo);
}
}
}

در اینجا با توجه به گزینه «مرا به خاطر بسپار»، اگر کاربری پیشتر لاگین کرده و کوکی خودکار حاصل از اعتبار سنجی مبتنی بر فرم‌های او نیز معتبر باشد، مقدار User.Identity.IsAuthenticated مساوی true خواهد بود. بنابراین نیاز است در متد LogOn از نوع HttpGet به این مساله دقت داشت و کاربر اعتبار سنجی شده را به صفحه پیش‌فرض تعیین شده در فایل web.config برنامه یا returnUrl هدایت کرد.
در متد LogOn از نوع HttpPost، کار اعتبارسنجی اطلاعات ارسالی به سرور انجام می‌شود. در اینجا فرصت خواهد بود تا اطلاعات دریافتی، با بانک اطلاعاتی مقایسه شوند. اگر اطلاعات مطابقت داشتند، ابتدا کوکی خودکار FormsAuthentication تنظیم شده و سپس به کمک متد RedirectFromLoginPage کاربر را به صفحه پیش فرض سیستم هدایت می‌کنیم. یا اگر returnUrl ایی وجود داشت، آن‌را پردازش خواهیم کرد.
برای پیاده سازی خروج از سیستم هم تنها کافی است متد FormsAuthentication.SignOut فراخوانی شود تا تمام اطلاعات سشن و کوکی‌های مرتبط، به صورت خودکار حذف گردند.

تا اینجا فیلتر Authorize بدون پارامتر و همچنین در حالت مشخص سازی صریح کاربران به نحو زیر را پوشش دادیم:

[Authorize(Users="Vahid")]

اما هنوز حالت استفاده از Roles در فیلتر Authorize باقی مانده است. برای فعال سازی خودکار بررسی نقش‌های کاربران نیاز است یک Role provider سفارشی را با پیاده سازی کلاس RoleProvider، طراحی کنیم. برای مثال:

using System;
using System.Web.Security;

namespace MvcApplication15.Helper
{
public class CustomRoleProvider : RoleProvider
{
public override bool IsUserInRole(string username, string roleName)
{
if (username.ToLowerInvariant() == "ali" && roleName.ToLowerInvariant() == "User")
return true;
// blabla ...
return false;
}

public override string[] GetRolesForUser(string username)
{
if (username.ToLowerInvariant() == "ali")
{
return new[] { "User", "Helpdesk" };
}

if(username.ToLowerInvariant()=="vahid")
{
return new [] { "Admin" };
}

return new string[] { };
}

public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}

public override string ApplicationName
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}

public override void CreateRole(string roleName)
{
throw new NotImplementedException();
}

public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
{
throw new NotImplementedException();
}

public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
throw new NotImplementedException();
}

public override string[] GetAllRoles()
{
throw new NotImplementedException();
}

public override string[] GetUsersInRole(string roleName)
{
throw new NotImplementedException();
}

public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}

public override bool RoleExists(string roleName)
{
throw new NotImplementedException();
}
}
}

در اینجا حداقل دو متد IsUserInRole و GetRolesForUser باید پیاده سازی شوند و مابقی اختیاری هستند.
بدیهی است در یک برنامه واقعی این اطلاعات باید از یک بانک اطلاعاتی خوانده شوند؛ برای نمونه به ازای هر کاربر تعدادی نقش وجود دارد. به ازای هر نقش نیز تعدادی کاربر تعریف شده است (یک رابطه many-to-many باید تعریف شود).
در مرحله بعد باید این Role provider سفارشی را در فایل وب کانفیگ برنامه در قسمت system.web آن تعریف و ثبت کنیم:

<roleManager>
<providers>
<clear />
<add name="CustomRoleProvider" type="MvcApplication15.Helper.CustomRoleProvider"/>
</providers>
</roleManager>


همین مقدار برای راه اندازی بررسی نقش‌ها در ASP.NET MVC کفایت می‌کند. اکنون امکان تعریف نقش‌ها، حین بکارگیری فیلتر Authorize میسر است:

[Authorize(Roles = "Admin")]
public class HomeController : Controller



مطالب
ساختار داده‌های خطی Linear Data Structure قسمت اول
بعضی از داده‌ها ساختارهای ساده‌ای دارند و به صورت یک صف یا یک نوار ضبط به ترتیب پشت سر هم قرار می‌گیرند؛ مثل ساختاری که صفحات یک کتاب را نگهداری می‌کند. یکی از نمونه‌های این ساختارها، List، صف، پشته و مشتقات آن‌ها می‌باشند.

ساختار داده‌ها چیست؟
در اغلب اوقات، موقعی‌که ما برنامه‌ای را می‌نویسیم با اشیاء یا داده‌های زیادی سر و کار داریم که گاهی اوقات اجزایی را به آن‌ها اضافه یا حذف می‌کنیم و در بعضی اوقات هم آن‌ها را مرتب سازی کرده یا اینکه پردازش دیگری را روی آن‌ها انجام میدهیم. به همین دلیل بر اساس کاری که قرار است انجام دهیم، باید داده‌ها را به روش‌های مختلفی ذخیره و نگه داری کنیم و در اکثر این روش‌ها داده‌ها به صورت منظم و پشت سر هم در یک ساختار قرار می‌گیرند.
ما در این مقاله، مجموعه‌ای از داده‌ها را در قالب ساختارهای متفاوتی بر اساس منطق و قوانین ریاضیات مدیریت می‌کنیم و بدیهی است که انتخاب یک ساختار مناسب برای هرکاری موجب افزایش کارآیی و کارآمدی برنامه خواهد گشت. می‌توانیم در مقدار حافظه‌ی مصرفی و زمان، صرفه جویی کنیم و حتی گاهی تعداد خطوط کدنویسی را کاهش دهیم.

نوع داده انتزاعی Abstraction Data Type -ADT
به زبان خیلی ساده لایه انتزاعی به ما تنها یک تعریف از ساختار مشخص شده‌ای را می‌دهد و هیچگونه پیاده سازی در آن وجود ندارد. برای مثال در لایه انتزاعی، تنها خصوصیت و عملگر‌ها و ... مشخص می‌شوند. ولی کد آن‌ها را پیاده سازی نمی‌کنیم و این باعث می‌شود که از روی این لایه بتوانیم پیاده سازی‌های متفاوت و کارآیی‌های مختلفی را ایجاد کنیم.
ساختار داده‌های مختلف در برنامه نویسی:
  • خطی یا Linear: شامل ساختارهایی چون لیست و صف و پشته است: List ,Queue,Stack
  • درختی یا Tree-Like: درخت باینری ، درخت متوازن و B-Trees
  • Dictionary : شامل یک جفت کلید و مقدار است در جدول هش
  • بقیه: گراف‌ها، صف الویت، bags, Multi bags, multi sets
در این مقاله تنها ساختارهای خطی را دنبال می‌کنیم و در آینده ساختارهای پیچیده‌تری را نیز بررسی خواهیم کرد و نیاز است بررسی کنیم کی و چگونه باید از آن‌ها استفاده کنیم.
ساختارهای لیستی از محبوبترین و پراستفاده‌ترین ساختارها هستند که با اشیاء زیادی در دنیای واقعی سازگاری دارند. مثال زیر را در نظر بگیرید:
قرار است که ما از فروشگاهی خرید کنیم و هر کدام از اجناس (المان‌ها) فروشگاه را که در سبد قرار دهیم، نام آن‌ها در یک لیست ثبت خواهد شد و اگر دیگر المان یا جنسی را از سبد بیرون بگذاریم، از لیست خط خواهد خورد.
همان که گفتیم یک ADT میتواند ساختارهای متفاوتی را پیاده سازی کند. یکی از این ساختارها اینترفیس system.collection.IList است که پیاده سازی آن منجر به ایجاد یک کلاس جدید در سیستم دات نت خواهد شد. پیاده سازی اینترفیس‌ها در سی شارپ، قوانین و قرادادهای خاص خودش را دارد و این قوانین شامل مجموعه‌ای از متد‌ها و خصوصیت‌هاست. برای پیاده سازی هر کلاسی از این اینترفیس‌ها باید این متدها و خصوصیت‌ها را هم در آن پیاده کرد.
با ارث بری از اینترفیس system.collection.IList باید رابط‌های زیر در آن پیاده سازی گردد:
(void Add(object    افزودن المان به آخر لیست 
(void Remove(object   حذف یک المان خاص از لیست  
 ()void Clear    حذف کلیه المان‌ها
( bool Contains(object   شامل این داده میشود یا خیر؟
( void RemoveAt(int  حذف یک المان بر اساس  جایگاه یا اندیسش 
(void Insert(int, object
 افزودن یک المان در جایگاهی (اندیس) خاص بر اساس مقدار position 
(int IndexOf(object اندیس یا جایگاه یک عنصر را بر می‌گرداند
 [this[int ایندکسر ، برای دستریس به عنصر در اندیس مورد نظر

لیست‌های ایستا static Lists
آرایه‌ها می‌توانند بسیاری از خصوصیات ADT را پیاده کنند ولی تفاوت بسیار مهم و بزرگی با آن‌ها دارند و آن این است که لیست به شما اجازه می‌دهد به هر تعدادی که خواستید، المان‌های جدیدی را به آن اضافه کنید؛ ولی یک آرایه دارای اندازه‌ی ثابت Fix است. البته این نکته قابل تامل است که پیاده سازی لیست با آرایه‌ها نیز ممکن است و باید به طور خودکار طول آرایه را افزایش دهید. دقیقا همان اتفاقی که برای stringbuilder در این مقاله توضیح دادیم رخ می‌دهد. به این نوع لیست‌ها، لیست‌های ایستایی که به صورت آرایه ای توسعه پذیر پیاده سازی میشوند می‌گویند. کد زیر پیاده سازی چنین لیستی است:
public class CustomArrayList<T>
{
    private T[] arr;
    private int count;
 
    public int Count
    {
        get
        {
            return this.count;
        }
    }
 
    private const int INITIAL_CAPACITY = 4;
 
    public CustomArrayList(int capacity = INITIAL_CAPACITY)
    {
        this.arr = new T[capacity];
        this.count = 0;
    }
در کد بالا یک آرایه با طول متغیر INITIAL_CAPACITY که پیش فرض آن را 4 گذاشته ایم می‌سازیم و از متغیر count برای حفظ تعداد عناصر آرایه استفاده می‌کنیم و اگر حین افزودن المان جدید باشیم و count بزرگتر از INITIAL_CAPACITY رسیده باشد، باید طول آرایه افزایش پیدا کند که کد زیر نحوه‌ی افزودن المان جدید را نشان می‌دهد. استفاده از حرف T بزرگ مربوط به مباحث Generic هست. به این معنی که المان ورودی می‌تواند هر نوع داده‌ای باشد و در آرایه ذخیره شود.
public void Add(T item)
{
    GrowIfArrIsFull();
    this.arr[this.count] = item;
    this.count++;
} 

public void Insert(int index, T item)
{
    if (index > this.count || index < 0)
    {
        throw new IndexOutOfRangeException(
            "Invalid index: " + index);
    }
    GrowIfArrIsFull();
    Array.Copy(this.arr, index,
        this.arr, index + 1, this.count - index);
    this.arr[index] = item;
    this.count++;
} 

private void GrowIfArrIsFull()
{
    if (this.count + 1 > this.arr.Length)
    {
        T[] extendedArr = new T[this.arr.Length * 2];
        Array.Copy(this.arr, extendedArr, this.count);
        this.arr = extendedArr;
    }
}
 
public void Clear()
{
    this.arr = new T[INITIAL_CAPACITY];
    this.count = 0;
}
در متد Add خط اول با تابع GrowIfArrIsFull بررسی می‌کند آیا خانه‌های آرایه کم آمده است یا خیر؟ اگر جواب مثبت باشد، طول آرایه را دو برابر طول فعلی‌اش افزایش می‌دهد و خط دوم المان جدیدی را در اولین خانه‌ی جدید اضافه شده قرار می‌دهد. همانطور که می‌دانید مقدار count همیشه یکی بیشتر از آخرین اندیس است. پس به این ترتیب مقدار count همیشه به  خانه‌ی بعدی اشاره می‌کند و سپس مقدار count به روز میشود. متد دیگری که در کد بالا وجود دارد insert است که المان جدیدی را در اندیس داده شده قرار می‌دهد. جهت این کار از سومین سازنده‌ی array.copy استفاده می‌کنیم. برای این کار آرایه مبدا و مقصد را یکی در نظر می‌گیریم و از اندیس داده شده به بعد در آرایه فعلی، یک کپی تهیه کرده و در خانه‌ی بعد اندیس داده شده به بعد قرار می‌دهیم. با این کار آرایه ما یک واحد از اندیس داده شده یک خانه، به سمت جلو حرکت می‌کند و الان خانه index و index+1 دارای یک مقدار هستند که در خط بعدی مقدار جدید را داخل آن قرار می‌دهیم و متغیر count را به روز می‌کنیم. باقی موارد را چون پردازش‌های جست و جو، پیدا کردن اندیس یک المان و گزینه‌های حذف، به خودتان واگذار می‌کنم.

لیست‌های پیوندی Linked List - پیاده سازی پویا
همانطور که دیدید لیست‌های ایستا دارای مشکل بزرگی هستند و آن هم این است که با انجام هر عملی بر روی آرایه‌ها مانند افزودن، درج در مکانی خاص و همچنین حذف (خانه ای در آرایه خالی خواهد شد و خانه‌های جلوترش باید یک گام به عقب برگردند) نیاز است که خانه‌های آرایه دوباره مرتب شوند که هر چقدر میزان داده‌ها بیشتر باشد این مشکل بزرگتر شده و ناکارآمدی برنامه را افزایش خواهد داد.
این مشکل با لیست‌های پیوندی حل می‌گردد. در این ساختار هر المان حاوی اطلاعاتی از المان بعدی است و در لیست‌های پیوندی دوطرفه حاوی المان قبلی است. شکل زیر نمایش یک لیست پیوندی در حافظه است:

برای پیاده سازی آن به دو کلاس نیاز داریم. کلاس ListNode برای نگهداری هر المان و اطلاعات المان بعدی به کار می‌رود که از این به بعد به آن Node یا گره می‌گوییم و دیگری کلاس <DynamicList<T برای نگهداری دنباله ای از گره‌ها و متدهای پردازشی آن.

public class DynamicList<T>
{
    private class ListNode
    {
        public T Element { get; set; }
        public ListNode NextNode { get; set; }
 
        public ListNode(T element)
        {
            this.Element = element;
            NextNode = null;
        }
 
        public ListNode(T element, ListNode prevNode)
        {
            this.Element = element;
            prevNode.NextNode = this;
        }
    }
 
    private ListNode head;
    private ListNode tail;
    private int count;
 
    // …
}

از آن جا که نیازی نیست کاربر با کلاس ListNode آشنایی داشته باشد و با آن سر و کله بزند، آن را داخل همان کلاس اصلی به صورت خصوصی استفاده می‌کنیم. این کلاس دو خاصیت دارد؛ یکی برای المان اصلی و دیگر گره بعدی. این کلاس دارای دو سازنده است که اولی تنها برای عنصر اول به کار می‌رود. چون اولین بار است که یک گره ایجاد می‌شود، پس باید خاصیت NextNode یعنی گره بعدی در آن Null باشد و سازنده‌ی دوم برای گره‌های شماره 2 به بعد به کار می‌رود که همراه المان داده شده، گره قبلی را هم ارسال می‌کنیم تا خاصیت NextNode آن را به گره جدیدی که می‌سازیم مرتبط سازد. سه خاصیت کلاس اصلی به نام‌های Count,Tail,Head به ترتیب برای اشاره به اولین گره، آخرین گره و تعداد گره‌ها، به کار می‌روند که در ادامه کد آن‌را در زیر می‌بینیم:

public DynamicList()
{
    this.head = null;
    this.tail = null;
    this.count = 0;
}

public void Add(T item)
{
    if (this.head == null)
    {
        this.head = new ListNode(item);
        this.tail = this.head;
    }
    else
    {
        ListNode newNode = new ListNode(item, this.tail);
        this.tail = newNode;
    }
    this.count++;
}

سازنده مقدار دهی پیش فرض را انجام می‌دهد. در متد Add المان جدیدی باید افزوده شود؛ پس چک می‌کند این المان ارسالی قرار است اولین گره باشد یا خیر؟ اگر head که به اولین گره اشاره دارد Null باشد، به این معنی است که این اولین گره است. پس اولین سازنده‌ی کلاس ListNode را صدا می‌زنیم و آن را در متغیر Head قرار می‌دهیم و چون فقط همین گره را داریم، پس آخرین گره هم شناخته می‌شود که در tail نیز قرار می‌گیرد. حال اگر فرض کنیم المان بعدی را به آن بدهیم، اینبار دیگر Head برابر Null نخواهد بود. پس دومین سازنده‌ی ListNode صدا زده می‌شود که به غیر از المان جدید، باید آخرین گره قبلی هم با آن ارسال شود و گره جدیدی که ایجاد می‌شود در خاصیت NextNode آن نیز قرار بگیرد و در نهایت گره ایجاد شده به عنوان آخرین گره لیست در متغیر Tail نیز قرار می‌گیرد. در خط پایانی هم به هر مدلی که المان جدید به لیست اضافه شده باشد متغیر Count به روز می‌شود.

public T RemoveAt(int index)
{
    if (index >= count || index < 0)
    {
        throw new ArgumentOutOfRangeException(
            "Invalid index: " + index);
    }
 
    int currentIndex = 0;
    ListNode currentNode = this.head;
    ListNode prevNode = null;
    while (currentIndex < index)
    {
        prevNode = currentNode;
        currentNode = currentNode.NextNode;
        currentIndex++;
    }
 

    RemoveListNode(currentNode, prevNode);
 
    return currentNode.Element;
}

private void RemoveListNode(ListNode node, ListNode prevNode)
{
    count--;
    if (count == 0)
    {
        this.head = null;
        this.tail = null;
    }
    else if (prevNode == null)
    {
        this.head = node.NextNode;
    }
    else
    {
        prevNode.NextNode = node.NextNode;
    }

    if (object.ReferenceEquals(this.tail, node))
    {
        this.tail = prevNode;
    }
}

برای حذف یک گره شماره اندیس آن گره را دریافت می‌کنیم و از Head، گره را بیرون کشیده و با خاصیت nextNode آنقدر به سمت جلو حرکت می‌کنیم تا متغیر currentIndex یا اندیس داده شده برابر شود و سپس گره دریافتی و گره قبلی آن را به سمت تابع RemoveListNode ارسال می‌کنیم. کاری که این تابع انجام می‌دهد این است که مقدار NextNode گره فعلی که قصد حذفش را داریم به خاصیت Next Node گره قبلی انتساب می‌دهد. پس به این ترتیب پیوند این گره از لیست از دست می‌رود و گره قبلی به جای اشاره به این گره، به گره بعد از آن اشاره می‌کند. مابقی کد از قبیل جست و برگردان اندیس یک عنصر و ... را به خودتان وگذار می‌کنم.

در روش‌های بالا ما خودمان 2 عدد ADT را پیاده سازی کردیم و متوجه شدیم برای دخیره داده‌ها در حافظه روش‌های متفاوتی وجود دارند که بیشتر تفاوت آن در مورد استفاده از حافظه و کارآیی این روش هاست.


لیست‌های پیوندی دو طرفه Doubly Linked_List

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

این مبحث را در اینجا می‌بندیم و در قسمت بعدی آن را ادامه می‌دهیم.

مطالب
پیاده سازی CQRS توسط MediatR - قسمت چهارم
در این قسمت قصد داریم به بررسی Behavior‌ ها در فریمورک MediatR بپردازیم. کدهای این قسمت به‌روزرسانی و از این ریپازیتوری قابل دسترسی است.

با استفاده از Behavior‌ها امکان پیاده سازی AOP را براحتی خواهید داشت. Behavior‌ها، مانند Filter‌‌ ها در ASP.NET MVC هستند. همانطور که با استفاده از متدهای OnActionExecuting و OnActionExecuted میتوانستیم اعمالی را قبل و بعد از اجرای یک اکشن‌متد انجام دهیم، چنین قابلیتی را با Behavior‌ها در MediatR نیز خواهیم داشت. مزیت اینکار این است که شما میتوانید کدهای Cross-Cutting-Concern خود را یکبار نوشته و چندین بار بدون تکرار مجدد، از آن استفاده کنید.


Performance Counter Behavior

فرض کنید میخواهید زمان انجام کار یک متد را اندازه گیری کرده و در صورت طولانی بودن زمان انجام آن، لاگی را مبنی بر کند بودن بیش از حد مجاز این متد، ثبت کنید. شاید اولین راهی که برای انجام اینکار به ذهنتان بیاید این باشد که داخل تمام متدهایی که میخواهیم زمان انجام آنها را  محاسبه کنیم، چنین کدی را تکرار کنیم:
public class SomeClass
{
    private readonly ILogger _logger;

    public SomeClass(ILogger logger)
    {
        _logger = logger;
    }

    public void SomeMethod()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        // TODO: Do some work here

        stopwatch.Stop();

        if (stopwatch.ElapsedMilliseconds > TimeSpan.FromSeconds(5).Milliseconds)
        {
            // This method has taken a long time, So we log that to check it later.
            _logger.LogWarning($"SomeClass.SomeMethod has taken {stopwatch.ElapsedMilliseconds} to run completely !");
        }
    }
}
در این صورت تمام متدهایی که نیاز به محاسبه زمان پردازش را دارند، باید به کلاسشان Logger تزریق شود. Stopwatch باید ایجاد، Start و Stop شود و در نهایت، بررسی کنیم که آیا زمان انجام این متد از حداکثری که برای آن مشخص کرده‌ایم گذشته است یا خیر.

علاوه بر این تصور کنید روزی تصمیم بگیرید که حداکثر زمان برای Log کردن را از 5 ثانیه به 10 ثانیه تغییر دهید. در این صورت بدلیل اینکه در همه متدها این قطعه کد تکرار شده‌است، مجبور به تغییر تمام کدهای برنامه برای اصلاح این بخش خواهید شد. در اینجا اصل DRY نقض شده‌است.

 

برای حل این مشکل از Behavior‌ها استفاده میکنیم. برای پیاده سازی Behavior‌ها داخل MediatR، کافیست از interface ای بنام IPipelineBehavior ارث بری کنیم:
public class RequestPerformanceBehavior<TRequest, TResponse> :
    IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<RequestPerformanceBehavior<TRequest, TResponse>> _logger;

    public RequestPerformanceBehavior(ILogger<RequestPerformanceBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();

        TResponse response = await next();

        stopwatch.Stop();

        if (stopwatch.ElapsedMilliseconds > TimeSpan.FromSeconds(5).Milliseconds)
        {
            // This method has taken a long time, So we log that to check it later.
            _logger.LogWarning($"{request} has taken {stopwatch.ElapsedMilliseconds} to run completely !");
        }

        return response;
    }
}
همانطور که میبینید منطق کد ما تغییری نکرده‌است. از IPipelineBehavior ارث بری کرده و متد Handle آن را پیاده سازی کرده‌ایم. همانند Middleware‌ ها در ASP.NET Core، در اینجا نیز یک RequestHandlerDelegate بنام next داریم که با اجرا و return آن، روند اجرای بقیه Command/Query‌ها ادامه پیدا خواهد کرد.

سپس باید Behavior‌های خود را از طریق DI به MediatR معرفی کنیم. داخل Startup.cs به این صورت RequestPerformanceBehavior خود را Register میکنیم:
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceBehavior<,>));

در نهایت برای تست کارکرد این Behavior، در کوئری GetCustomerByIdQueryHandler خود 5 ثانیه Delay ایجاد میکنیم تا طول اجرای آن، از Maximum زمان مشخص شده بیشتر و Log انجام شود:
public class GetCustomerByIdQueryHandler : IRequestHandler<GetCustomerByIdQuery, CustomerDto>
{
    private readonly ApplicationDbContext _context;
    private readonly IMapper _mapper;

    public GetCustomerByIdQueryHandler(ApplicationDbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    public async Task<CustomerDto> Handle(GetCustomerByIdQuery request, CancellationToken cancellationToken)
    {
        Customer customer = await _context.Customers
            .FindAsync(request.CustomerId);

        if (customer == null)
        {
            throw new RestException(HttpStatusCode.NotFound, "Customer with given ID is not found.");
        }

        // For testing PerformanceBehavior
        await Task.Delay(5000, cancellationToken);

        return _mapper.Map<CustomerDto>(customer);
    }
}

پس از اجرای برنامه و فراخوانی GetCustomerById ، داخل Console این پیغام را خواهید دید:



Transaction Behavior


یکی دیگر از استفاده‌های Behavior‌ها میتواند پیاده سازی Transaction و Rollback باشد. فرض کنید میخواهیم افزودن یک مشتری به دیتابیس فقط زمانی صورت گیرد که تمام کارهای داخل Command با موفقیت و بدون رخ دادن Exception انجام شود. برای انجام اینکار میتوان یک TransactionBehavior نوشت تا بدنه Command‌ها را داخل یک TransactionScope قرار دهد و در صورت وقوع Exception ، عمل Rollback صورت گیرد :
public class TransactionBehavior<TRequest, TResponse> :
    IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var transactionOptions = new TransactionOptions
        {
            IsolationLevel = IsolationLevel.ReadCommitted,
            Timeout = TransactionManager.MaximumTimeout
        };

        using (var transaction = new TransactionScope(TransactionScopeOption.Required, transactionOptions,
            TransactionScopeAsyncFlowOption.Enabled))
        {
            TResponse response = await next();

            transaction.Complete();

            return response;
        }
    }
}

سپس این Behavior را داخل DI Container خود Register میکنیم :
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));

در نهایت متد Handle در CreateCustomerCommandHandler را که در قسمت‌های قبل ایجاد کردیم، تغییر داده و بعد از SaveChanges مربوط به Entity Framework، یک Exception را صادر میکنیم:
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto>
{
    readonly ApplicationDbContext _context;
    readonly IMapper _mapper;
    readonly IMediator _mediator;

    public CreateCustomerCommandHandler(ApplicationDbContext context,
        IMapper mapper,
        IMediator mediator)
    {
        _context = context;
        _mapper = mapper;
        _mediator = mediator;
    }

    public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken)
    {
        Domain.Customer customer = _mapper.Map<Domain.Customer>(createCustomerCommand);

        await _context.Customers.AddAsync(customer, cancellationToken);
        await _context.SaveChangesAsync(cancellationToken);

        throw new Exception("======= MY CUSTOM EXCEPTION =======");

        // Raising Event ...
        await _mediator.Publish(new CustomerCreatedEvent(customer.FirstName, customer.LastName, customer.RegistrationDate), cancellationToken);

        return _mapper.Map<CustomerDto>(customer);
    }
}

اگر برنامه را اجرا کنید خواهید دید با اینکه Exception ما بعد از SaveChanges رخ داده است، اما بدلیل استفاده از Transaction Behavior ای که نوشتیم، عملیات Rollback صورت گرفته و داخل دیتابیس رکوردی ثبت نشده‌است.

* نکته : قبل از استفاده از Transaction‌ها حتما این مطالب ( 1 , 2 ) را مطالعه کنید.

MediatR دارای 2 اینترفیس IRequestPreProcessor و IRequestPostProcessor نیز هست که اگر نیاز داشته باشید یک عمل فقط قبل یا بعد از انجام یک Command/Query صورت گیرد، میتوانید از آنها استفاده کنید.

همچنین پیاده سازی‌های پیشفرضی از این 2 اینترفیس با نام‌های RequestPreProcessorBehavior و RequestPostProcessorBehavior داخل فریمورک، بطور پیشفرض وجود دارد که قبل و بعد از تمامی Handler‌ها اجرا خواهند شد.
مطالب
چندین Submit در یک Html Form و انتساب Action های مجزا به هر یک از Submit ها در MVC
تا به حال به این نکته برخورد کردید که برای یک فرم Html نیاز به چندین Submit داشته باشید که هر کدوم یک Action مجزا داشته باشن و یک کار متفاوت انجام بدن ؟
برای مثال فرمی داریم که داده‌های وارد شده در ان باید به دو صورت برای یک کاربر ارسال بشن یا از طریق پیامک یا از طریق ایمیل (این فقط یک مثال پیش فرض هست) و .... در حالت عادی ما در یک فرم نمیتونیم دو عدد Submit  داشته باشیم که هر کدوم به یک Action جدا بسط داده بشه خب راه حل چیه ؟ شاید با خودتون بگید خب دو input از نوع radio قرار میدیم و در یک اکشن کنترل میکنیم که کدوم یکی انتخاب شده و عملیات رو با اون معیار انجام میدیم ... به نظرتون زیباتر نیست برای هر عملیات که ممکن باشه هر کدوم کاملا روال کاری متفاوتی داشته باشه یک Action وجود داشته باشه ؟ در این صورت خوانایی کد خیلی بالاتر میره و Unit Test هر Action کاملا مشخص هست که قراره چه فرایندی رو مورد تست قرار بده و مجبور نیستیم چندین حالت رو با عبارات شرطی از هم جدا کنیم و همه چی قاطی بشه با هم ... من در کل با امکاناتی که C# و MVC در اختیارم قرار میده حاظر نیستم تن به کد نویسی به صورت کلاسیک و قاطی پاتی بدم سعی میکنم با مطالعه‌ی سورس MVC بهترین حالت رو انتخاب کنم شما چطور ؟ معلومه که همه همینو میخوان پس بریم سر اصل مطلب .
قطعه کد Html و Razor ساده‌ی زیر رو در نظر بگیرید برای View :
@model Models.MyModel
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <title>ViewPage1</title>
</head>
<body>
    <div>
        @using (Html.BeginForm("SendMessage", "Home", FormMethod.Post))
        {

            @Html.LabelFor(x => x.Name); 
            @Html.TextBoxFor(x => x.Name);

            <input type="submit" value="ارسال توسط پیامک" name="Send_sms" />
            <input type="submit" value="ارسال توسط ایمیل" name="Send_email" />
        }
    </div>
</body>
</html>

خب ما دو تا Submit داریم . یکم اگه شیطنت کنید و مقادیر ارسال شده بعد از submit این فرم رو توسط ابزارهای مانیتورینگ بررسی کنید میبینید که روی هر کدوم از Submit‌ها که کلیک میشه داده ای با نام اون که در خاصیت name اون و مقدار موجود در value اون همراه اون فرم به سرور ارسال میشه و اون یکی Submit از این اتفاق بی نصیب میمونه ... خب ما هم استفاده‌ی لازم رو از این موضوع شیرین میبریم و با یک تکنیک تهاجمی از این موضوع برای رسیدن به هدفمون استفاده میکنیم .

این هم کلاس Model ماست :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;

namespace Models
{
    public class MyModel
    {
        [DisplayName("نام خود را وارد کنید :")]
        public string Name { get; set; }
    }
}
 و اما یک نکته‌ی دیگه . توجه داشته باشید که ما در قسمت View نام Action رو در فرم, SendMessage مشخص کردیم . ولی ... اصلا در واقع همچین اکشنی وجود نداره ! پس چرا ما همچین نامی رو واسه اکشن فرم گذاشتیم !؟
دلیل اینه که ما قصد داریم با یک ActionNameSelectorAttribute درخواست کاربر رو شکار کنیم و اون رو به اکشن دلخواه ارجاع بدیم ... جالبه نه ؟ ولی چه جوری ... کلاس زیر رو بهش دقت مضاعف کنید و در پروژتون ایجادش کنید :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Reflection;

namespace ActionHandlers
{
    public class SendMessageHandlerAttribute : ActionNameSelectorAttribute
    {
        public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
        {
            if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
                return true;

            if (!actionName.Equals("SendMessage", StringComparison.InvariantCultureIgnoreCase))
                return false;

            var request = controllerContext.RequestContext.HttpContext.Request;
            return request[methodInfo.Name] != null;
        }
    }
}
 خب حالا بخش Controller رو بهش دقت کنید که ما در اون دو اکشن رو با نام هایی که برای هر Submit مشخص کردیم مینویسیم و ActionNameSelectorAttribute نوشته شده رو به اونها بسط میدیم. 
    [SendMessageHandler ]
        [HttpPost]
        public ContentResult Send_sms(MyModel mdl)
        {
            /// Do something ...
           return string.Empty ;
        }

        [SendMessageHandler ]
        [HttpPost]
        public ContentResult Send_email(MyModel mdl)
        {
           /// Do something ...
          return  string.Empty; 
        }
 خب حالا بعد از کلیک بر روی هر Submit اکشن متناظر با اون اجرا میشه . بعد از ارسال درخواست به سرور MVC در بین اکشن‌های موجود در Controller مشخص شده به دنبال اکشن معین شده میگرده و وقتی به اکشن‌های ما میرسه میبینه عجب ! اون دوتا ActionNameSelectorAttribute سفارشی دارن پس میره ببینه چه خبره اونجا که ما با یک حرکت تهاجمی بررسی میکنیم که اگه  نام اکشن مشخص شده در فرم با نام اکشن در حال بررسی مساوی بود که همینو اجرا کن ( یعنی ما میتونی اکشنی با نام SendMessage هم داشته باشیم ) . اگه نام اکشن مشخص شده در فرم اون نامی نبود که ما میخوایم که کلا بیخیال هندل کردن اکشن میشیم میزاریم خود MVC تصمیم بگیره . و در اخر بررسی میکنیم که ایا در درخواست جاری مقداری با نام اکشن در حال بررسی وجود داره !؟ اگه داشت یعنی همون Submit که ما میخوایم وصل بشه به این اکشن کلیک شده پس اکشن در حال بررسی رو بسط میدیم به درخواست ارسال شده ... به همین سادگی ...

پیروز و موفق باشید . 
مطالب
breeze js به همراه ایجاد سایت آگهی قسمت اول
با قدرت گرفتن جاوا اسکریپت، نیازهایی مانند کوئری گرفتن در سمت کلاینت، کش کردن داده‌ها در سمت کلاینت، ردیابی تغییرات، اعتبارسنجی مدلها، ذخیره کردن گروهی از عملیات‌ها (Save Batch)، تعامل با Web Api .Net یا Node Js، قابلیت کار کردن با No Sql و... افزایش یافته است و تمام این کارها توسط breeze  امکان پذیر میباشد. breeze  با هر سرویس دهنده‌ای که بتواند از  طریق http و با فرمت json عملیات خود را انجام دهد، میتواند ارتباط برقرار کند.
breeze مطابق با استاندارد ECMAScript 5 نوشته شده‌است؛ از این‌رو بر روی اکثر مرورگرهای جدید به خوبی اجرا میشود.

  برای مرورگرهای قدیمی می‌توان کتابخانه es5-shim.js and es5-sham.js را قبل از تگ اسکریپت breeze js قرار داد.
در این قسمت به برخی از  توانایی های  breeze  اشاره کوتاهی میکنیم و در مقالات بعدی پیاده سازی آن‌را در قالب angular js /Web Api .net خواهیم داشت:
1- کوئری‌های breeze اکثر امکانات  linq را دارا می‌باشد؛ مانند شرطهای ساده، شرط‌های پیچیده، Sorting ،Paging،Projections  و Filter کردن برخی از پراپرتی‌ها که بسیار مشابه  linq میباشد:
var query = breeze.EntityQuery
           .from("Customers")
           .where("CompanyName", "startsWith", "A")
           .orderBy("CompanyName");
2- کوئری‌های breeze با promises عملیات خود را انجام میدهند:
var promise = manager.executeQuery(query)
              .then(querySucceeded)
              .fail(queryFailed);
* breeze مانند  entity framework  قابلیت ردیابی تغییرات را دارا میباشد:
if (manager.hasChanges()) {
    manager.saveChanges().then(saveSucceeded).fail(saveFailed);
}
// listen for any change to a customer 
customer.entityAspect.propertyChanged.subscribe(somethingHappened);
3- انقیاد داده‌ها با Angular / Knockout  / Backbone را دارا میباشد:
<!-- Angular template -->
<li data-ng-repeat="emp in employees">
    <label>{{emp.FirstName}}</label>
    <label>{{emp.LastName}}</label>
</li>
// bound to employees from query
manager.executeQuery(breeze.EntityQuery.from("Employees"))
       .then(function(data) { $scope.employees = data.results; });
4- درbreeze امکان  کوئری گرفتن به همراه entity‌های مرتبط وجود دارد:
/* Query with related entities using expand */
// query for orders of customers whose name begins "Alfreds"
// include their customers & child details & their detail products
breeze.EntityQuery.from("Orders")
   .where("Customer.CompanyName", "startsWith", "Alfreds")
   .expand("Customer, OrderDetails.Product")
   .using(manager)
   .execute().then(querySucceeded).fail(queryFailed);
5- در breeze  کوئری‌ها میتوانند از سرور و یا کش درخواست شوند:
// execute query asynchronously on the server
manager.executeQuery(query).then(querySuccess).fail(queryFail);
 
// execute query synchronously on local cache
var customers = manager.executeQueryLocally(query)
مطالب
سفارشی کردن قابلیت طراحی ریپورت توسط END USER - بخش اول
در قسمتی از پروژه تجاری، برای طراحی ریپورت توسط کاربر نیاز به موضوع مطرح شده این پست داشتم و به نتایج مطلوبی در این زمینه توسط کامپوننت Stimulsoft Report.Net دست یافتم.
پروژه بنده به صورتی هست که در آن قرار است یک سری اطلاعات توسط DataTable به Report ارسال و ریپورت هم Field‌ها را که از قبل طراحی شده، روی فرم داشته باشد و کاربر فقط این امکان را داشته باشد که مکان فیلد‌ها ( یعنی مختصات ) روی صفحه A4 را با توجه به سلیقه خودش تنظیم و ذخیره نماید.

اهداف :
1.غیر فعال کردن بعضی از امکانات صفحه Design
2.اعمال یکسری تنظیمات از طریق کدنویسی (Code Behind) بر روی ریپورت
مانند : اینکه ShowGrid فعال باشه یا نه -- یا Toolbox.visible=false باشه
3.فارسی سازی محیط طراحی برای کاربر
4.ذخیره و ...

اسکرین شات : ( حالت پیشفرض بدون اعمال تغییرات Runtime )


رفرنس‌های مورد نیاز:
using Stimulsoft.Base.Services;
using Stimulsoft.Report;
using Stimulsoft.Report.Components;
using Stimulsoft.Report.Design;
using Stimulsoft.Report.Design.Toolbars;
using Stimulsoft.Report.Render;
using Stimulsoft.Report.Units;

//ایجاد یک شی از ریپورت            
StiReport report = new StiReport();
//ریست کردن تنظیمات به حالت پیشفرض
 report.Reset();
//ریست کردن تنظیمات سرویس‌های StiConfig.Reset();
//ریست کردن تنظیمات چاپ
 StiSettings.Clear();

            

            //تنظیم عنوان ریپورت به صورت دلخواه
            StiOptions.Designer.DesignerTitle = title + " طراحی فرم ";
            StiOptions.Designer.DesignerTitleText = title + " طراحی فرم ";
            //غیرفعال شدن نمایش تب کدنویسی
            StiOptions.Designer.CodeTabVisible = false;
            //فعال کردن امکان RightToLeft
            StiOptions.Designer.UseRightToLeftGlobalizationEditor = true;
            //غیرفعال شدن قابلیت تغییر نام ریپورت توسط کاربر
            StiOptions.Designer.CanDesignerChangeReportFileName = false;
            //غیر فعال کردن تشخیص اتوماتیک زبان پیش فرض UI طراحی
            StiOptions.Designer.UseSimpleGlobalizationEditor = false;

            //تنظیم تم ریپورت از حالت استاندارد به ریبون
            StiOptions.Windows.GlobalGuiStyle = StiGlobalGuiStyle.Office2010Blue;
            //فعال سازی تم ریبون
            StiOptions.Designer.IsRibbonGuiEnabled = true;
برای دسترسی به بخش Dictionary که به اطلاعات DataSoruce‌ها دسترسی می‌دهد البته برای قابلیت Design :
یک شی از StiOptions ایجاد کنید. سپس Designer و فعال و غیرفعال کردن نمایش هر بخش را و حتی تغییر آیتم‌های موجود در منوی راست کلیک هر شی، قابل تغییر خواهند بود.

پنل Dictionary از سه شاخه اصلی تشکیل می‌شود : که با دستورات زیر می‌توان نمایش این بخش‌ها را در صورت خالی بودن از داده غیر فعال نمود
BusinessObjectsCategory - DataSourcesCategory -VariablesCategory
StiOptions.Designer.Panels.Dictionary.ShowEmptyBusinessObjectsCategory = false;
StiOptions.Designer.Panels.Dictionary.ShowEmptyDataSourcesCategory = false;
StiOptions.Designer.Panels.Dictionary.ShowEmptyVariablesCategory = false;
در صفحه طراحی، 3 پنل وجود دارد :
1- Dictionary
2- Properties
3- Report Tree

که Properties نسبت به هر شیء ایی که از صفحه ریپورت انتخاب می‌کنید، تنظیمات مربوط به آن را برای ویرایش در اختیار کاربر قرار می‌دهد.
پنل Report Tree نیز از داده‌های موجود از دیتاسورس بخش Dictionary که به صورت شیء در صفحه ریپورت قرار داده شده‌اند نمایش درخت واره‌ای را در اختیار کاربر قرار می‌دهد و می‌تواند از اشیاء این درخت واره در ریپورت به صورت متعدد استفاده نماید.

می‌توان هر کدام از این پنل‌ها را ( که به صورت سرویس در Stimulsoft تعریف شده) از دید کاربر مخفی نمود یا به صورت محدود یکسری از قابلیت‌ها را در اختیار کاربر قرار داد:
//غیر فعال کردن سرویس‌های پنل
            Stimulsoft.Report.Design.Panels.StiPropertiesPanelService propPanel = Stimulsoft.Report.Design.Panels.StiPropertiesPanelService.GetService();
            propPanel.ServiceEnabled = false;

            Stimulsoft.Report.Design.Panels.StiDictionaryPanelService dictPanel = Stimulsoft.Report.Design.Panels.StiDictionaryPanelService.GetService();
            dictPanel.ServiceEnabled = true;
            
            Stimulsoft.Report.Design.Panels.StiReportTreePanelService treePanel = Stimulsoft.Report.Design.Panels.StiReportTreePanelService.GetService();
            treePanel.ServiceEnabled = false;

            Stimulsoft.Report.Design.Toolbars.StiToolsToolbarService cpanel = Stimulsoft.Report.Design.Toolbars.StiToolsToolbarService.GetService();
            cpanel.ServiceEnabled = false;
            StiOptions.Dictionary.BusinessObjects.AddBusinessObjectAssemblyToReferencedAssembliesAutomatically = false;
            StiOptions.Dictionary.BusinessObjects.AllowProcessNullItemsInEnumerables = false;
            StiOptions.Dictionary.BusinessObjects.AllowUseDataColumn = false;
            StiOptions.Dictionary.BusinessObjects.AllowUseFields = false;
            StiOptions.Dictionary.BusinessObjects.AllowUseProperties = false;
            StiOptions.Dictionary.BusinessObjects.CheckTableDuplication = false;
           
            

            StiOptions.Dictionary.ShowOnlyAliasForDataSource = true;
            StiOptions.Dictionary.ShowOnlyAliasForDataColumn = true;
            StiOptions.Dictionary.ShowOnlyAliasForTotal = true;
            dictPanel.ShowNewButton = false;
            dictPanel.ShowActionsButton = false;
            dictPanel.ShowBusinessObjectNewMenuItem = false;
            dictPanel.ShowCalcColumnNewMenuItem = false;
            dictPanel.ShowCategoryNewMenuItem = false;
            dictPanel.ShowCollapseAllMenuItem = true;
            dictPanel.ShowColumnNewMenuItem = false;
            dictPanel.ShowConnectionNewMenuItem = false;
            dictPanel.ShowContextMenu = false;
            dictPanel.ShowCreateFieldOnDoubleClick = false;
            dictPanel.ShowCreateLabel = false;
            dictPanel.ShowDataParameterNewMenuItem = false;
            dictPanel.ShowDataSourceNewMenuItem = false;
            dictPanel.ShowDataSourcesNewMenuItem = false;
            dictPanel.ShowDeleteButton = false;
            dictPanel.ShowDeleteForBusinessObject = false;
            dictPanel.ShowDeleteForDataColumn = false;
            dictPanel.ShowDeleteForDataConnection = false;
            dictPanel.ShowDeleteForDataParameter = false;
            dictPanel.ShowDeleteForDataRelation = false;
            dictPanel.ShowDeleteForDataSource = false;
            dictPanel.ShowDeleteForVariable = false;
            dictPanel.ShowDeleteMenuItem = false;
            dictPanel.ShowDictMergeMenuItem = false;
            dictPanel.ShowDictNewMenuItem = false;
            dictPanel.ShowDictOpenMenuItem = false;
            dictPanel.ShowDictSaveMenuItem = false;
            dictPanel.ShowDictXmlExportMenuItem = false;
            dictPanel.ShowDictXmlImportMenuItem = false;
            dictPanel.ShowDictXmlMergeMenuItem = false;
            dictPanel.ShowDownButton = false;
            dictPanel.ShowEditButton = false;
            dictPanel.ShowEditForBusinessObject = false;
            dictPanel.ShowEditForDataColumn = false;
            dictPanel.ShowEditForDataConnection = false;
            dictPanel.ShowEditForDataParameter = false;
            dictPanel.ShowEditForDataRelation = false;
            dictPanel.ShowEditForDataSource = false;
            dictPanel.ShowEditForVariable = false;
            dictPanel.ShowEditMenuItem = false;
            
            dictPanel.ShowExpandAllMenuItem = true;
            dictPanel.ShowMarkUsedMenuItem = false;
            dictPanel.ShowNewButton = false;
            dictPanel.ShowPropertiesForBusinessObject = false;
            dictPanel.ShowPropertiesForDataColumn = false;
            dictPanel.ShowPropertiesForDataConnection = false;
            dictPanel.ShowPropertiesForDataParameter = false;
            dictPanel.ShowPropertiesForDataRelation = false;
            dictPanel.ShowPropertiesForDataSource = false;
            dictPanel.ShowPropertiesForVariable = false;
            dictPanel.ShowPropertiesMenuItem = false;
            dictPanel.ShowRelationNewMenuItem = false;
            dictPanel.ShowRelationsImportMenuItem = false;
            dictPanel.ShowRemoveUnusedMenuItem = false;
            dictPanel.ShowSortItemsButton = false;
            dictPanel.ShowSynchronizeMenuItem = false;
            dictPanel.ShowUpButton = false;
            dictPanel.ShowUseAliases = true;
            dictPanel.ShowVariableNewMenuItem = false;
            dictPanel.ShowViewDataMenuItem = false;
یکی از قابلیت‌های خوب و کاربردی این کامپوننت پشتیبانی کامل حتی منوها از زبان فارسی می‌باشد که استفاده از این کامپوننت را در نرم افزار‌های تجاری در کشور قابل انعطاف پذیر‌تر می‌کند. برای فارسی سازی به یک فایل XML که در مسیر نصب کامپوننت قرار دارد، نیاز است.
نام فایل fa.xml می‌باشد. آن‌را در مسیر نرم افزار قرار دهید و سپس کد زیر را اضافه نمایید:
//تنظیم زبان به فارسی
StiConfig.LoadLocalization("fa.xml");