پاسخ به بازخورد‌های پروژه‌ها
خطا هنگام اتصال
در لاگ ارسالی عنوان شده‌است که:
   Failed to listen on prefix 'http://localhost:14799/' because it conflicts with an existing registration on the machine.
   HTTP could not register URL http://localhost:14799/. Another application has already registered this URL with HTTP.SYS.
یعنی پورتی که انتخاب کردید، هم اکنون در سیستم در حال استفاده‌است. یک پورت آزاد دیگر را انتخاب کنید.
دستور خط فرمان
 netsh http show servicestate
لیست این نوع پورت‌های مورد استفاده‌ی جهت http listener را نمایش می‌دهد.
مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core
پیشنیازها
«بررسی روش آپلود فایل‌ها در ASP.NET Core»
«ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


تدارک مقدمات مثال این قسمت

این مثال در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه می‌کنیم:
 >ng g m UploadFile -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و UploadFileModule را بجای UploadFileRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت یک درخواست پشتیبانی را اضافه خواهیم کرد:
 >ng g c UploadFile/UploadFileSimple
که اینکار سبب به روز رسانی فایل upload-file.module.ts و افزوده شدن UploadFileSimpleComponent به قسمت declarations آن می‌شود.
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف می‌کنیم:
 >ng g cl UploadFile/Ticket
با این محتوا:
export class Ticket {
  constructor(public description: string = "") {}
}
در اینجا Ticket تعریف شده دارای یک خاصیت توضیحات است و این فرم به همراه فیلد ارسال چندین فایل نیز می‌باشد که نیازی به درج آن‌ها در کلاس فوق نیست:



ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن

پس از ایجاد ساختار کلاس Ticket، یک وهله از آن‌را به نام model ایجاد کرده و در اختیار قالب آن قرار می‌دهیم:
import { Ticket } from "./../ticket";

export class UploadFileSimpleComponent implements OnInit {
  model = new Ticket();
سپس قالب این کامپوننت و یا همان فایل upload-file-simple.component.html را به صورت ذیل تکمیل می‌کنیم:
<div class="container">
  <h3>Support Form</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
    <div class="form-group" [class.has-error]="description.invalid && description.touched">
      <label class="control-label">Description</label>
      <input #description="ngModel" required type="text" class="form-control"
        name="description" [(ngModel)]="model.description">
      <div *ngIf="description.invalid && description.touched">
        <div class="alert alert-danger"  *ngIf="description.errors.required">
          description is required.
        </div>
      </div>
    </div>

    <div class="form-group">
      <label class="control-label">Screenshot(s)</label>
      <input #screenshotInput required type="file" multiple (change)="fileChange($event)"
        class="form-control" name="screenshot">
    </div>

    <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button>
  </form>
</div>
در اینجا ابتدا فیلد توضیحات درخواست جدید، ارائه و به خاصیت model.description متصل شده‌است. همچنین این فیلد با ویژگی required مزین، و اجباری بودن آن بررسی گردیده‌است.
سپس در انتها، فیلد آپلود را مشاهده می‌کنید؛ با این ویژگی‌ها:
الف) ngModel ایی به آن متصل نشده‌است؛ چون روش کار با آن متفاوت است.
ب) یک template reference variable به نام screenshotInput# در آن تعریف شده‌است. از این متغیر، در کامپوننت قالب استفاده خواهیم کرد.
ج) به رخ‌داد change این کنترل، متد fileChange متصل شده‌است که رخ‌داد جاری را نیز دریافت می‌کند.
د) ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده می‌کنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.


دسترسی به المان ارسال فایل در کامپوننت متناظر

تا اینجا یک المان ارسال فایل را به فرم، اضافه کرده‌ایم. اما چگونه باید به فایل‌های آن برای ارسال به سرور دسترسی پیدا کنیم؟
برای این منظور در ادامه دو روش را بررسی خواهیم کرد:

1) دسترسی به المان ارسال فایل از طریق رخ‌داد change
در تعریف فیلد ارسال فایل، اتصال به رخ‌داد change تعریف شده‌است:
 (change)="fileChange($event)"
معادل آن در سمت کامپوننت متناظر، به صورت ذیل است:
fileChange(event) {
    const filesList: FileList = event.target.files;
    console.log("fileChange() -> filesList", filesList);
}
همانطور که مشاهده می‌کنید، event.target، امکان دسترسی مستقیم به المان متناظری را در قالب کامپوننت میسر می‌کند. سپس می‌توان به خاصیت files آن دسترسی یافت.


در اینجا ساختار شیء استاندارد FileList و اجزای آن‌را مشاهده می‌کنید. برای مثال چون دو فایل انتخاب شده‌است، این لیست به همراه یک خاصیت طول و دو شیء File است.

تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آن‌ها را شناسایی و intellisense متناظری را مهیا می‌کند:
 C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
همچنین اگر به فایل tsconfig.json پروژه نیز مراجعه کنید، یک چنین تعاریفی در آن قرار دارند:
{
    "lib": [
      "es2016",
      "dom"
    ]
  }
}
وجود و تعریف کتابخانه‌ی dom است که سبب کامپایل شدن کدهای فوق، بدون بروز هیچگونه خطایی می‌شود.


2) دسترسی به المان آپلود فایل از طریق یک template reference variable
در حین تعریف المان فایل در فرم برنامه، متغیر screenshotInput# نیز ذکر شده‌است. می‌توان به یک چنین متغیرهایی در کامپوننت متناظر به روش ذیل دسترسی یافت:
import { Component, OnInit, ViewChild, ElementRef } from "@angular/core";

export class UploadFileSimpleComponent implements OnInit {
  @ViewChild("screenshotInput") screenshotInput: ElementRef;

  submitForm(form: NgForm) {
    const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
    console.log("fileInput.files", fileInput.files);
  }
ابتدا یک خاصیت جدید را به نام screenshotInput از نوع ElementRef که در angular/core@ تعریف شده‌است، اضافه می‌کنیم. سپس برای اتصال آن به template reference variable ایی به نام screenshotInput، از ویژگی به نام ViewChild، با پارامتری مساوی نام همین متغیر، استفاده خواهیم کرد.
اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شده‌است. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه می‌کنید، می‌توان به خاصیت files این کنترل ارسال فایل‌ها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شده‌است.


ارسال فرم درخواست پشتیبانی به سرور

تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون می‌خواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
 >ng g s UploadFile/UploadFileSimple -m upload-file.module
که سبب به روز رسانی خودکار قسمت providers فایل upload-file.module.ts نیز می‌شود.
در ادامه کدهای کامل این سرویس را مشاهده می‌کنید:
import { Http, RequestOptions, Response, Headers } from "@angular/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/map";
import "rxjs/add/observable/of";

import { Ticket } from "./ticket";

@Injectable()
export class UploadFileSimpleService {
  private baseUrl = "api/SimpleUpload";

  constructor(private http: Http) {}

  private extractData(res: Response) {
    const body = res.json();
    return body || {};
  }

  private handleError(error: Response): Observable<any> {
    console.error("observable error: ", error);
    return Observable.throw(error.statusText);
  }

  postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
    if (!filesList || filesList.length === 0) {
      return Observable.throw("Please select a file.");
    }

    const formData: FormData = new FormData();

    for (const key in ticket) {
      if (ticket.hasOwnProperty(key)) {
        formData.append(key, ticket[key]);
      }
    }

    for (let i = 0; i < filesList.length; i++) {
      formData.append(filesList[i].name, filesList[i]);
    }

    const headers = new Headers();
    headers.append("Accept", "application/json");
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(`${this.baseUrl}/SaveTicket`, formData, options)
      .map(this.extractData)
      .catch(this.handleError);
  }
}
توضیحات تکمیلی:
روش کار با فرم‌هایی که فیلدهای ارسال فایل را به همراه دارند، متفاوت است با روش کار با فرم‌های معمولی. در فرم‌های معمولی، اصل شیء Ticket را به متد this.http.post واگذار می‌کنیم. مابقی آن خودکار است. در اینجا باید شیء استاندارد FormData را تشکیل داده و سپس اطلاعات را از طریق آن ارسال کنیم:
الف) افزودن مقادیر خواص شیء Ticket به FormData
  postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
    const formData: FormData = new FormData();

    for (const key in ticket) {
      if (ticket.hasOwnProperty(key)) {
        formData.append(key, ticket[key]);
      }
    }
با استفاده از حلقه‌ی for می‌توان بر روی خواص یک شیء جاوا اسکریپتی حرکت کرد. به این ترتیب می‌توان نام و مقدار آن‌ها را یافت و سپس به formData به صورت key/value افزود.

ب) افزودن فایل‌ها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایل‌های فرم است:
    for (let i = 0; i < filesList.length; i++) {
      formData.append(filesList[i].name, filesList[i]);
    }
این مورد نیز به سادگی تشکیل یک حلقه، بر روی خاصیت files المان آپلود فایل است. به همین جهت بود که به دو روش سعی کردیم، به این خاصیت دسترسی پیدا کنیم.

یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمی‌توان در سمت سرور از روش model binding استفاده کرد. چون این نام دیگر ثابت نیست و هربار می‌تواند متغیر باشد (در حالت model binding دقیقا مشخص است که کلید مشخصی قرار است به سرور ارسال شود و بر همین اساس، نام خاصیت یا پارامتر سمت سرور تعیین می‌گردد). به همین جهت در سمت سرور برای دسترسی به این مجموعه، از روش Request.Form.Files استفاده می‌کنیم.

ج) ارسال اطلاعات نهایی به سرور
اکنون که formData را بر اساس اطلاعات اضافی ticket و فایل‌های متصل به آن تشکیل دادیم، روش ارسال آن به سرور همانند قبل است:
    const headers = new Headers();
    headers.append("Accept", "application/json");
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(`${this.baseUrl}/SaveTicket`, formData, options)
      .map(this.extractData)
      .catch(this.handleError);

یک نکته: در اینجا در روش استفاده از formData نباید Content-Type را به multipart/form-data  تنظیم کرد. در غیراینصورت خطای Missing content-type boundary error را دریافت می‌کنید.


تکمیل کامپوننت ارسال درخواست پشتیبانی

پس از تکمیل سرویس ارسال اطلاعات به سمت سرور، اکنون نوبت به استفاده‌ی از آن در کامپوننت ارسال فرم درخواست پشتیبانی است. بنابراین ابتدا این سرویس جدید را به سازنده‌ی UploadFileSimpleComponent تزریق می‌کنیم:
import { UploadFileSimpleService } from "./../upload-file-simple.service";

export class UploadFileSimpleComponent implements OnInit {
  constructor(private uploadService: UploadFileSimpleService  ) {}
و سپس متد submitForm چنین شکلی را پیدا می‌کند:
  submitForm(form: NgForm) {
    const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
    console.log("fileInput.files", fileInput.files);

    this.uploadService
      .postTicket(this.model, fileInput.files)
      .subscribe(data => {
        console.log("success: ", data);
      });
  }
در اینجا this.model حاوی اطلاعات شیء ticket است (برای مثال اطلاعات توضیحات آن) و fileInput.files امکان دسترسی به اطلاعات فایل‌های انتخابی توسط کاربر را می‌دهد. پس از آن فراخوانی متدهای this.uploadService.postTicket و subscribe، سبب ارسال این اطلاعات به سمت سرور می‌شوند.


دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیره‌ی فایل‌های آن‌

کدهای کامل SimpleUpload که در سرویس فوق مشخص شده‌است، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شده‌است:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Ticket
    {
        public int Id { set; get; }
        public string Description { set; get; }
    }
}
و سپس کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
using System.IO;
using System.Threading.Tasks;
using AngularTemplateDrivenFormsLab.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class SimpleUploadController : Controller
    {
        private readonly IHostingEnvironment _environment;
        public SimpleUploadController(IHostingEnvironment environment)
        {
            _environment = environment;
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> SaveTicket(Ticket ticket)
        {
            //TODO: save the ticket ... get id
            ticket.Id = 1001;

            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            var files = Request.Form.Files;
            foreach (var file in files)
            {
                //TODO: do security checks ...!

                if (file == null || file.Length == 0)
                {
                    continue;
                }

                var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                using (var fileStream = new FileStream(filePath, FileMode.Create))
                {
                    await file.CopyToAsync(fileStream).ConfigureAwait(false);
                }
            }

            return Created("", ticket);
        }
    }
}
توضیحات تکمیلی
- تزریق IHostingEnvironment در سازنده‌ی کلاس کنترلر، سبب می‌شود تا از طریق خاصیت WebRootPath آن، به مسیر wwwroot سایت دسترسی پیدا کنیم و فایل‌های نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه می‌کنید، هنوز هم model binding کار کرده و می‌توان شیء Ticket را به نحو متداولی دریافت کرد:
 SaveTicket(Ticket ticket)
اما همانطور که عنوان شد، چون در حلقه‌ی افزودن فایل‌ها در سمت کلاینت، کلید نام این فایل‌ها هربار متفاوت است:
 formData.append(filesList[i].name, filesList[i]);
مجبور هستیم در سمت سرور بر روی Request.Form.Files یک حلقه را تشکیل داده و تمام فایل‌های رسیده را پردازش کنیم:
var files = Request.Form.Files;
foreach (var file in files)



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 19 - بومی سازی
هدف از زیر ساخت بومی سازی در ASP.NET Core، حذف عبارات و رشته‌های درج شده‌ی در کلاس‌ها و ویووهای مختلف برنامه و انتقال آن‌ها به فایل‌های منبع resx است و سپس استفاده‌ی از آن‌ها توسط تزریق وابستگی‌ها. به این ترتیب می‌توان بر اساس نوع فرهنگ درخواستی کاربر جاری، رشته‌های درج شده را به صورت پویا، در زمان اجرای برنامه، بر اساس ترجمه‌های آن‌ها به کاربر نمایش داد.


نحوه‌ی تعیین فرهنگ ترد جاری در ASP.NET Core

در نگارش‌های پیشین ASP.NET، برای تعیین فرهنگ ترد جاری، از یکی از دو روش ذیل استفاده می‌شود:
الف) افزودن مدخل بومی سازی به فایل web.config
<system.web>
    <globalization uiCulture="fa-IR" culture="fa-IR" />
</system.web>
ب) و یا تعیین فرهنگ ترد با کدنویسی مستقیم در فایل global.asax
protected void Application_BeginRequest()
{
   Thread.CurrentThread.CurrentCulture = new CultureInfo("fa-IR");
   Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR");
}
در ASP.NET Core با حذف شدن System.Web و همچنین فایل global.asax، برای تعیین فرهنگ پیش فرض ترد جاری، به همراه فرهنگ‌هایی که برنامه از آن‌ها پشتیبانی می‌کند، به صورت ذیل عمل می‌شود:
public void Configure(IApplicationBuilder app)
{
    app.UseRequestLocalization(new RequestLocalizationOptions
    {
        DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")),
        SupportedCultures = new[]
        {
            new CultureInfo("en-US"),
            new CultureInfo("fa-IR")
        },
        SupportedUICultures = new[]
        {
            new CultureInfo("en-US"),
            new CultureInfo("fa-IR")
        }
    });
در اینجا با مراجعه به کلاس آغازین برنامه و افزودن تنظیمات میان افزار RequestLocalization، می‌توان فرهنگ پیش فرض درخواست جاری و یا فرهنگ‌های پشتیبانی شده را مشخص کرد.
- تنظیمات SupportedCultures بر روی نمایش تاریخ، ساعت و واحد پولی تاثیر دارند. همچنین می‌توانند بر روی نحوه‌ی مقایسه‌ی حروف و مرتب سازی آن‌ها تاثیر داشته باشند.
- تنظیمات SupportedUICultures مشخص می‌کنند که کدامیک از فایل‌های resx برنامه که مداخل ترجمه‌های آن‌را به زبان‌های مختلف مشخص می‌کنند، باید بارگذاری شوند.
- تنظیم DefaultRequestCulture در صورت مشخص نشدن فرهنگ ترد جاری مورد استفاده قرار می‌گیرد.

یک مثال: هر ترد در دات نت دارای اشیاء CurrentCulture و CurrentUICulture است. اگر فرهنگ ترد جاری به en-US تنظیم شده باشد، متد DateTime.Now.ToLongDateString، خروجی نمونه Thursday, February 18, 2016 را نمایش می‌دهد.


زمانیکه میان افزار RequestLocalization فعال می‌شود، سه تامین کننده‌ی پیش فرض (مقدار‌های پیش فرض خاصیت RequestCultureProviders شیء RequestLocalizationOptions فوق)، جهت مشخص ساختن فرهنگ ترد جاری بکار گرفته خواهند شد:
الف) از طریق کوئری استرینگ با فعال سازی QueryStringRequestCultureProvider
http://localhost:5000/?culture=es-MX&ui-culture=es-MX
http://localhost:5000/?culture=es-MX
برای مثال در اینجا QueryStringRequestCultureProvider به دنبال کوئری استرینگ‌های culture و یا ui-culture گشته و با رسیدن به es-MX، فرهنگ جاری را به اسپانیایی مکزیکی تنظیم می‌کند. در این حالت اگر فقط culture ذکر شود، ui-culture نیز به همان مقدار تنظیم خواهد شد.
ب) از طریق نام کوکی با فعال سازی CookieRequestCultureProvider
CookieRequestCultureProvider کوکی ویژه‌ای را با نام پیش فرض AspNetCore.Culture. ایجاد می‌کند. این کوکی برای ردیابی اطلاعات بومی سازی انتخابی کاربر بکار می‌رود. برای مثال اگر به مقدار ذیل تنظیم شود:
 c='en-UK'|uic='en-US'
c آن به معنای culture و uic آن به معنای ui-culture خواهد بود.
ج) از طریق هدر مخصوص Accept-Language با فعال سازی AcceptLanguageHeaderRequestCultureProvider که می‌تواند به همراه درخواست HTTP ارسال شود.

اگر تمام این حالت‌ها تنظیم نشده بودند، آنگاه از مقدارDefaultRequestCulture  استفاده می‌شود. برای مثال اگر مرورگر به صورت پیش فرض هدر Accept-Language را en-US ارسال می‌کند :


دیگر کار به پردازش مقدارDefaultRequestCulture  نخواهد رسید.

اکنون اگر علاقمند بودید تا به کاربر امکان انتخاب زبانی را بدهید، یک چنین اکشن متدی را طراحی کنید:
public IActionResult SetFaLanguage()
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("fa-IR"))),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );
 
    return RedirectToAction("GetTitle");
}
این اکشن متد بر اساس تامین کننده‌ی کوکی ردیابی زبان انتخاب شده‌ی توسط کاربر و یا CookieRequestCultureProvider کار می‌کند و توسط آن، فرهنگ جاری برنامه به زبان فارسی تنظیم می‌شود. هرگاه که این اکشن متد فراخوانی شود، کوکی AspNetCore.Culture. به مقدار c=fa-IR|uic=fa-IR تنظیم می‌شود:


از اینجا به بعد است که اگر نام کنترلر شما TestLocalController باشد، فایل منبع متناظر با آن یعنی Controllers.TestLocalController.fa.resx، به صورت خودکار بارگذاری و پردازش خواهد شد. در غیر اینصورت فایل نمونه‌ی ختم شده‌ی به en.resx پردازش می‌شود؛ چون این زبان به صورت پیش فرض در هدر Accept-Language قید شده‌است.


آماده سازی برنامه برای کار با فایل‌های منبع زبان‌های مختلف

ابتدا پوشه‌ی جدیدی را به نام Resources به ریشه‌ی پروژه اضافه کنید. سپس به کلاس آغازین برنامه مراجعه کرده و محل یافت شدن این پوشه را معرفی کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization(options => options.ResourcesPath = "Resources");
    services.AddMvc()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
        .AddDataAnnotationsLocalization();
در اینجا سرویس جدید Localization، به لیست سرویس‌های ثبت شده‌ی در IoC Container اضافه می‌شود. همچنین توسط خاصیت  ResourcesPath  آن مشخص شده‌است که فایل‌های resx را باید از کجا دریافت کند.
به علاوه به سرویس ASP.NET MVC، تنظیمات بومی سازی Viewها و DataAnnotations نیز اضافه شده‌اند. تنظیم suffix به معنای  view file suffix و یا مثلا fr در نام فایل Index.fr.cshtml است.


نحوه‌ی تعریف و پوشه بندی فایل‌های منبع زبان‌های مختلف

تا اینجا پوشه‌ی جدید Resources را به پروژه اضافه، معرفی و سرویس‌های مرتبط را نیز فعال کردیم. پس از آن نوبت به افزودن فایل‌های resx است. برای این منظور بر روی پوشه‌ی منابع کلیک راست کرده و گزینه‌ی add->new item را انتخاب کنید.


در اینجا با جستجوی resource، می‌توان فایل resx جدیدی را به پروژه اضافه کرد؛ اما ... انتخاب نام آن باید بر اساس نکات ذیل باشد:
الف) برای کنترلرها یکی از دو مسیر / دار و یا نقطه دار جستجو می‌شوند:
Resources/Controllers.HomeController.fr.resx
Resources/Controllers/HomeController.fr.resx

در اینجا fr ذکر شده، همان LanguageViewLocationExpanderFormat.Suffix است که پیشتر بحث شد. قسمت ابتدایی Controllers همیشه ثابت است (یا به صورت نام یک پوشه و یا به عنوان قسمت اول نام فایل). سپس نام کلاس کنترلر به همراه نام فرهنگ مدنظر باید ذکر شوند. قسمت نام پوشه‌ی Resources را نیز به services.AddLocalization معرفی کرده‌ایم.

ب) برای Viewها نیز همان حالت‌های / دار و یا نقطه دار بررسی می‌شوند:
Resources/Views.Home.About.fr.resx
Resources/Views/Home/About.fr.resx


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

در این نگارش از ASP.NET، در حالت کلی، نام یک فایل منبع، همان نام کامل کلاس آن است؛ منهای فضای نام آن (اگر این فایل منبع در همان اسمبلی قرار گیرد). برای مثال اگر می‌خواهید برای کلاس Startup برنامه، فایل منبعی را درست کنید و نام کامل آن با درنظر گرفتن فضای نام، معادل LocalizationWebsite.Web.Startup است، ابتدای فضای نام آن‌را حذف کنید و سپس آن‌را ختم به fa.resx کنید؛ مثلا Startup.fa.resx
اگر محل واقع شدن فایل‌های resx در همان اسمبلی اصلی پروژه باشند، نیازی به ذکر فضای نام پیش فرض پروژه نیست. برای مثال اگر فضای نام پیش فرض پروژه‌ی وب جاری MyLocalizationWebsite.Web است، بجای نام فایل MyLocalizationWebsite.Web.Controllers.HomeController.fr.resx می‌توانید به صورت خلاصه بنویسید Controllers.HomeController.fr.resx. در غیراینصورت (استفاده از اسمبلی‌های دیگر)، ذکر کامل فضای نام مرتبط هم الزامی است.


چند نکته:
- اگر ResourcesPath را در services.AddLocalization معرفی نکنید، مسیر پیش فرض یافتن فایل‌های resx مربوط به کنترلرها، پوشه‌ی ریشه‌ی پروژه است و برای Viewها، همان پوشه‌ی محل واقع شدن View متناظر خواهد بود.
- اینکه کدام فایل منبع در برنامه بارگذاری می‌شود، دقیقا مرتبط است با فرهنگ ترد جاری و این فرهنگ به صورت پیش فرض en-US است (چون همواره در هدر Accept-Language ارسالی توسط مرورگر وجود دارد). برای تغییر آن، از نکته‌ی اکشن متد public IActionResult SetFaLanguage ابتدای بحث استفاده کنید (در غیراینصورت در آزمایشات خود شاهد بارگذاری فایل‌های منبع دیگری بجز en.resx‌ها نخواهید بود).
- فایل‌های منبع را به صورت کامپایل شده در پوشه‌ی bin برنامه خواهید یافت:



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

فرض کنید کنترلری را به نام TestLocalController ایجاد کرده‌ایم. بنابراین فایل منبع فارسی متناظر با آن Controllers.TestLocalController.fa.resx خواهد بود؛ با این محتوای نمونه:


محتوای این کنترلر نیز به صورت ذیل است:
using System;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Localization;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class TestLocalController : Controller
    {
        private readonly IStringLocalizer<TestLocalController> _stringLocalizer;
        private readonly IHtmlLocalizer<TestLocalController> _htmlLocalizer;
 
        public TestLocalController(
            IStringLocalizer<TestLocalController> stringLocalizer,
            IHtmlLocalizer<TestLocalController> htmlLocalizer)
        {
            _stringLocalizer = stringLocalizer;
            _htmlLocalizer = htmlLocalizer;
        }
 
        public IActionResult Index()
        {
            var name = "DNT";
            var message = _htmlLocalizer["<b>Hello</b><i> {0}</i>", name];
            ViewData["Message"] = message;
            return View();
        }
 
        [HttpGet]
        public string GetTitle()
        {
            var about = _stringLocalizer["About Title"];
            return about;
        }
 
        public IActionResult SetFaLanguage()
        {
            Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("fa-IR"))),
                new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
            );
 
            return RedirectToAction("GetTitle");
        }
    }
}
در اینجا نحوه‌ی دسترسی به فایل‌های منبع را در کنترلرها مشاهده می‌کنید. سرویس IStringLocalizer برای خواندن key/valueهای معمولی طراحی شده‌است و سرویس IHtmlLocalizer برای خواندن key/valueهای تگ دار، بکار می‌رود. علت تنظیم شدن پارامتر جنریک آن‌ها به نام کنترلر جاری این است که این سرویس‌ها بدانند دقیقا چه نوعی را قرار است بارگذاری کنند و دقیقا باید به دنبال کدام فایل بگردند. این سرویس‌ها یک کلید را می‌گیرند و یک خروجی و مقدار را باز می‌گردانند.
اگر برنامه را در حالت معمولی اجرا کنید و سپس آدرس http://localhost:7742/testlocal/gettitle را درخواست کنید، عبارت About Title را مشاهده می‌کنید؛ به دو علت:
الف) هنوز فرهنگ پیش فرض ترد جاری همان en-US است که توسط مرورگر ارسال شده‌است.
ب) چون فایل resx متناظر با فرهنگ پیش فرض ترد جاری یافت نشده‌است، مقدار همان کلید درخواستی بازگشت داده می‌شود؛ یعنی همان About Title.

برای رفع این مشکل آدرس http://localhost:7742/testlocal/SetFaLanguage را درخواست کنید. به این صورت با تنظیم کوکی ردیابی فرهنگ ترد جاری به زبان فارسی، خروجی GetTile این‌بار «درباره» خواهد بود.


خواندن اطلاعات منابع در Viewهای برنامه

فرض کنید فایل Views.TestLocal.Index.fa.resx (فایل منبع کنترلر TestLocal و ویوو Index آن به زبان فارسی) دقیقا همان محتوای فایل Controllers.TestLocalController.fa.resx فوق را دارد (اگر نام پوشه‌ی Views را تغییر داده‌اید، قسمت ابتدایی نام فایل Views را هم باید تغییر دهید). برای دسترسی به اطلاعات آن در یک ویوو، می‌توان از سرویس IViewLocalizer  به نحو ذیل استفاده کرد:
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
 
@{
}
Message @ViewData["Message"]
<br/>
@Localizer["<b>Hello</b><i> {0}</i>", "DNT"]
<br/>
@Localizer["About Title"]
در اینجا ViewData، از همان اطلاعات اکشن متد Index استفاده می‌کند.
Localizer از طریق تزریق سرویس IViewLocalizer  به View برنامه تامین می‌شود. این سرویس در پشت صحنه از همان IHtmlLocalizer استفاده می‌کند و در حین استفاده‌ی از آن، اطلاعات تگ‌ها انکد (encoded) نخواهند شد (به همین جهت برای کار با کلیدها و مقادیر تگ‌دار توصیه می‌شود).


استفاده از اطلاعات منابع در DataAnnotations

قسمت اول فعال سازی بومی سازی DataAnnotations با ذکر AddDataAnnotationsLocalization در متد ConfigureServices، در ابتدای بحث انجام شد و همانطور که پیشتر نیز عنوان گردید، در این نگارش از ASP.NET، برای تمام کلاس‌های برنامه می‌توان فایل منبع ایجاد کرد. برای مثال اگر کلاس RegisterViewModel در فضای نام ViewModels.Account قرار گرفته‌است، نام فایل منبع آن یکی از دو حالت / دار و یا نقطه دار ذیل می‌تواند باشد:
Resources/ViewModels.Account.RegisterViewModel.fr.resx
Resources/ViewModels/Account/RegisterViewModel.fr.resx

محتوای این کلاس را در ذیل مشاهده می‌کنید:
using System.ComponentModel.DataAnnotations;
 
namespace Core1RtmEmptyTest.ViewModels.Account
{
    public class RegisterViewModel
    {
        [Required(ErrorMessage = "EmailReq")]
        [EmailAddress(ErrorMessage = "EmailType")]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }
}
در این حالت مقداری که برای ErrorMessage ذکر می‌شود، کلیدی است که باید در فایل منبع جستجو شود:


یک نکته: هیچ الزامی ندارد که کلیدها را به این شکل وارد کنید. از این جهت که اگر این کلید در فایل منبع یافت نشد و یا فرهنگ ترد جاری با فایل‌های منبع مهیا تطابقی نداشت، عبارتی را که کاربر مشاهده می‌کند، دقیقا معادل «EmailReq» خواهد بود. بنابراین در اینجا می‌توانید کلید را به صورت کامل، مثلا مساوی «The Email field is required» وارد کنید و همین عبارت را به عنوان کلید در فایل منبع ذکر کرده و مقدار آن‌را مساوی ترجمه‌ی آن قرار دهید. این نکته در تمام حالات کار با کنترلرها و ویووها نیز صادق است.


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

اگر می‌خواهید تعدادی از منابع را در همه‌جا در اختیار داشته باشید، روش کار به این صورت است:
الف) یک کلاس خالی را به نام SharedResource دقیقا با فرمت ذیل در پوشه‌ی Resources ایجاد کنید:
// Dummy class to group shared resources
namespace Core1RtmEmptyTest
{
   public class SharedResource
   {
   }
}
ب) اکنون فایل‌های منبع خود را در پوشه‌ی Resources، دقیقا با این نام‌های خاص ایجاد کنید:
SharedResource.fa.resx
SharedResource.en-US.resx
و امثال آن

ج) برای استفاده‌ی از این منبع اشتراکی در کلاس‌های مختلف برنامه تنها کافی است در حین تزریق وابستگی‌ها، نوع آرگومان جنریک IStringLocalizer را به SharedResource تنظیم کنید:
 IStringLocalizer<SharedResource> sharedLocalizer
و یا حتی در ویووهای برنامه نیز می‌توان از آن استفاده کرد:
 @inject IHtmlLocalizer<SharedResource> SharedLocalizer
مطالب
ایجاد چارت سازمانی تحت وب #3
در مطالب قسمت اول و دوم به نحوه ایجاد و تغییر رنگ چارت سازمانی اشاره شد. در این مطلب ، نحوه تغییر فونت‌ها، مکان قرار گرفتن شاخه‌ها و ایجاد لینک در شاخه‌ها ارائه میشود. بدین صورت که در شکل زیر مشاهده مینمائید:

شاخه‌ها ( نودها ) میتوانند فونتهای مختلف داشته باشند.برای تنظیم فونت باید از تابع ()setFont استفاده شود.البته که باید فونت انتخابی بر روی سیستم کاربر موجود باشد در غیر این صورت مرورگر یک فونت دلخواه و پیش فرض خود را جایگزین فونت شما خواهد نمود. در صورت بروز هر گونه خطا در فونت ، متن داخل گره‌ها کوتاه خواهد شد.

با توجه به محدودیت IE در پیاده سازی excanvas  ، در کل کاراکترها متن نود کوتاه میشود. ( اگر کاراکترهای نود ، کاملا پرشونده fit نشوند ، بخشی از کل متن کاراکترهای نود نوشته یا رسم خواهد شد )

پارامترهای تابع ()setFont :

  1. نام فونت . حالت فونت ضخیم bold  یا مورب italic قابل استفاده است.
  2. اندازه فونت در واحد پیکسل
  3. رنگ فونت ( اختیاری )
  4. چیدمان عمودی ( 1 و c یا center برای وسط چین . ( اختیاری )
برای مشاهده از این کدها میتوانید استفاده نمائید:
var o = new orgChart();

o.setColor('#99CC99', '#CCFFCC');
o.setFont('arial', 18);
o.addNode(0, '', '', 'Arial 18', 1);

o.setColor('#CCCC66', '#FFFF99');
o.setFont('arial', 10, '#000000');
o.addNode(11, 0, 'u', 'text will be split if it does not fit. ThisIsAVeryLongWordAndItWillBeClipped. Too many lines will be clipped too.');

o.setFont('arial', 10, '#000000', 0);
o.addNode(12, 0, 'u', 'aligned at top');

o.setFont('arial', 10, '#000000', 1);
o.addNode(13, 0, 'u', 'centered');
o.setColor('#CC4950', '#FF7C80');

o.setFont('times', 16, '#FF0F00');
o.addNode(21, 0, 'l', 'Times 16 red');

o.setFont('times', 16, '#000000');
o.addNode(22, 0, 'l', 'Times 16');

o.setFont('times', 48, '#000000');
o.addNode(23, 0, 'l', 'Times 48 does not fit at all');
o.setColor('#CC9966', '#FFCC99');

o.setFont('jokerman', 12, '#000000');
o.addNode(31, 0, 'r', 'Jokerman (if available)');

o.setFont('bold times', 16, '#000000');
o.addNode(32, 0, 'r', 'bold Times 16');

o.setFont('italic times', 16, '#000000');
o.addNode(33, 0, 'r', 'italic Times 16');

o.setFont('bold italic times', 16, '#000000');
o.addNode(34, 0, 'r', 'bold italic Times 16');

o.setColor('#B5D9EA', '#CFE8EF');
o.setFont('arial', 28, '#000000');
o.addNode(50, '', '', 'Arial 28');
o.setFont('arial', 29);
o.addNode(51, 50, 'u', 'Arial 29');
o.setFont('arial', 30);
o.addNode(52, 51, 'u', 'Arial 30');
o.setFont('arial', 31);
o.addNode(53, 52, 'u', 'Arial 31');
o.setFont('arial', 32);
o.addNode(54, 53, 'u', 'Arial 32');

o.drawChart('c_fonts');

اندازه و مکان :

شما میتوانید اندازه نودها و فضا و offset بین نودها را نیز تنظیم نمائید.این تنظیم بصورت عمومی تاثیر گذار است و تمامی نودها از این تنظیم تبعیت خواهند نمود:


پارامترهای تابع ()setSize:
  1. عرض نودها در واحد پیکسل.
  2. ارتفاع نودها در واحد پیکسل.
  3. فاصله عرضی بین نودهای پدر u-nodes. ( اختیاری ).
  4. فاصله طولی بین نودها ( اختیاری ).
  5. offset عرضی ( فاصله )  از نود چپ و نود راست ( اختیاری ).
var o = new orgChart();

o.setSize(36, 12, 4, 25, 180);

o.setColor('#99CC99', '#CCFFCC');
o.setFont('arial', 18);
o.addNode(0, '', '', 'Root node');
o.setFont('arial', 12);
o.setColor('#CCCC66', '#FFFF99');
o.addNode(11, 0, 'u', 'u-node 1');
o.addNode(12, 0, 'u', 'u-node 2');
o.addNode(13, 0, 'u', 'u-node 3');
o.setColor('#CC4950', '#FF7C80');
o.addNode(21, 0, 'l', 'l-node 1');
o.addNode(22, 0, 'l', 'l-node 2');
o.addNode(23, 0, 'l', 'l-node 3');
o.setColor('#CC9966', '#FFCC99');
o.addNode(31, 0, 'r', 'r-node 1');

o.drawChart('c_size');
پیوندها: LINKS
شما میتوانید به نودها در پارامتر ششم تابع ()addNode آدرس پیوند خود را اضافه نمائید.
در صورت ایجاد پیوند کامل ( مانند : http://www.yourdomain.com ) پیوند در برگه ( tab ) یا یک پنجره جدید ( بسته به تنظیمات مرورگر سمت کاربر ) باز خواهد شد.
اگر نشانگر ماوس ، روی این نوع از نود‌ها قرار بگیرد تغییر شکل به مانند دست ( اشاره گر ) میدهد.



نکته : در این نمونه کد ، هر نود در یک چارت سازمانی جدید دوباره رسم شده اند.در چارت سازمانی قدیمی ، نودها از بین نمی‌روند و همه مسیرهای باقی مانده فعال خواهند ماند.بنابراین اگر reDraw در این نمونه استفاده شود ، چند پیوند در یک نود باز خواهد شد .
اگر بخواهید فقط یک لینک به نودی اختصاص دهید ، یک نود پیوندی بدون پیوند به آن اضافه کنید ( مانند نودها سبز مثال نمونه ).
var o = new orgChart();

o.setColor('#99CC99', '#CCFFCC');
o.setFont('arial', 18);
o.addNode( 0, '', '', 'Searching', 1);
o.addNode(50, '', '', 'Social', 1);
o.addNode(90, '', '', 'Misc.', 1);

o.setColor('#CCCC66', '#FFFF99');
o.setFont('arial', 12);
o.addNode(11, 50, 'u', 'Facebook', 0, 'http://facebook.com');
o.addNode(13, 90, 'u', 'Youtube', 0, 'http://youtube.com');
o.addNode(14, 13, 'l', 'Youtube Music', 0, 'http://youtube.com/music');
o.addNode(15, 13, 'l', 'Youtube Entertainment', 0, 'http://youtube.com/entertainment');

o.setColor('#CC4950', '#FF7C80');
o.addNode(21, 0, 'l', 'Google', 0, 'http://google.com');
o.addNode(22, 0, 'l', 'Bing', 0, 'http://bing.com');

o.addNode('r2', '', '', 'Top of this Page', 0, '#');
o.addNode('', 'r2', 'u', 'Back to the introduction', 0, '/orgchart');

o.drawChart('c_links');

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

مطالب دوره‌ها
ارتباطات بلادرنگ و SignalR
زمانیکه صحبت از برنامه‌های بلادرنگ می‌شود با کاربرانی سر و کار داریم که نیاز دارند تا اطلاعات مورد نیاز خود را همواره و بلافاصله در آخرین وضعیت به روز آن مشاهده کنند. در این بین، کلاینت می‌خواهد یک برنامه وب باشد یا سیلورلایت و یا یک برنامه نوشته شده با WPF. حتی برنامه‌های موبایل را نیز باید به این لیست اضافه کرد.
در اینجا کلمه بلادرنگ به معنای ارسال اطلاعات از طرف سرور به کلاینت‌ها با فاصله زمانی بسیار کوتاهی از به روز رسانی اطلاعات صورت گرفته در سمت سرور است.
نمونه‌ای از این برنامه‌ها شامل موارد ذیل هستند:
- اطلاع رسانی همزمان به گروهی از کاربران
- جستجوهای زنده و به روز رسانی‌هایی از این دست
- نمایش بلادرنگ قیمت‌ها و وضعیت تجاری محصولات و سهام‌ها
- بازی‌های تعاملی
- برنامه‌های گروهی و تعاملی (مانند برنامه‌های Chat)
- برنامه‌های شبکه‌های اجتماعی (برای مثال پیام جدیدی دارید؛ شخص خاصی آنلاین یا آفلاین شد و امثال آن)

بنابراین به صورت خلاصه قصد داریم به ارائه بازخوردها و اطلاع رسانی‌های بلادرنگ یا نسبتا سریع و به روز از سمت سرور به کلاینت‌ها برسیم.
برای مثال یک دیتاگرید را درنظر بگیرید. دو کاربر در شبکه صفحه یکسانی را گشوده‌اند و یکی از آن‌ها مشغول به ویرایش و یا حذف اطلاعات است. در ارتباطات بلادرنگ کاربر یا کاربران دیگر نیز باید (یا بهتر است) در زمانیکه گرید یکسانی را گشوده‌اند، بلافاصله آخرین تغییرات را ملاحظه کنند. یا حتی حالتی را درنظر بگیرید که شخصی SQL Server management studio را گشوده و در آنجا مشغول به تغییر اطلاعات گردیده است. در این حالت نیز بهتر است آخرین تغییرات بلافاصله به اطلاع کاربران رسانده شوند.

معرفی الگوی Push service

البته باید دقت داشت که الگوی push service یک الگوی رسمی ذکر شده در گروه‌های مرسوم الگوهای طراحی نیست، اما مفهوم آن سرویسی است که چندین کار ذیل را انجام می‌دهد:
الف) پذیرش اتصالات از چندین مصرف کننده. مصرف کننده‌ها در اینجا الزاما محدود به کلاینت‌های وب یا دسکتاپ نیستند؛ می‌توانند حتی یک سرور یا سرویس دیگر نیز باشند.
ب) قادر است اطلاعات را به مصرف کننده‌های خود ارسال کند. این سرویس می‌تواند یک برنامه ASP.NET باشد یا حتی یک سرویس متداول ویندوز.
ج) در اینجا چندین منبع خارجی مانند یک بانک اطلاعاتی یا تغییرات رخ داده توسط یک سخت افزار که می‌توانند سبب بروز رخدادهایی در push service گردند نیز می‌تواند وجود داشته باشند. هر زمان که تغییری در این منابع خارجی رخ دهد، مایل هستیم تا مصرف کننده‌ها را مطلع سازیم.


پروتکل HTTP و ارتباطات بلادرنگ

پروتکلی که در ارتباطات بلادرنگ مبتنی بر SignalR مورد استفاده قرار می‌گیرد، HTTP است و از قابلیت‌های Request و Response آن در اینجا بیشترین بهره برده می‌شود. پیاده سازی Push عموما بر مبنای یکی از روش‌های متداول زیر است:
1) Periodic polling
به این معنا که مثلا هر 10 ثانیه یکبار، کاری را انجام بده؛ مانند ارسال متناوب: آیا تغییری رخ داده؟ آیا تغییری رخ داده؟ و .... به همین ترتیب. این روش اصلا بهینه نبوده و منابع زیادی را خصوصا در سمت سرور مصرف خواهد کرد. برای مثال:
function getInfo() {
         $.ajax("url", function ( newInfo){
                  if ( newInfo != null) {
                      // do something with the data
                  }
         });
    // poll again after 20 seconds
    setTimeout(getInfo,20000);
}
// start the polling loop
getInfo();

2) Long polling
به آن HTTP Streaming یا Comet هم گفته می‌شود. این روش نسبتا هوشمند بوده و کلاینت اتصالی را به سرور برقرار خواهد کرد. سرور در این حالت تا زمانیکه اطلاعاتی را در دسترس نداشته باشد، پاسخی نخواهد داد. برای نمونه:
function getNewInfo(){
  $.ajax("url", function (newinfo) {
      // do something with the data
  // start the new request
      getNewINfo();
  });
}
// start the polling loop
getNewInfo();

این روش نسبت به حالت Periodic polling بهینه‌تر است اما نیاز به اتصالات زیادی داشته و همچنین تردهای بسیاری را در سمت سرور به خود مشغول خواهد کرد.

3) Forever frame
فقط در IE پشتیبانی می‌شود. در این روش یک Iframe مخفی توسط مرورگر تشکیل شده و از طریق آن درخواستی به سرور ارسال می‌شود. سپس سرور متناوبا با تزریق اسکریپت‌هایی به این Iframe سبب فراخوانی مجدد وضعیت خود می‌گردد. در این روش نیز به ازای هر درخواست و پاسخ، ارتباطات گشوده و بسته خواهند شد.

4) Server Sent Events یا SSE
این مورد جزو استاندارد HTML5 است. در اینجا اتصالی برقرار شده و داده‌ها از طریق اتصالات HTTP منتقل می‌شوند.
var eventSrc = new EventSource("url");
    // register event handler for the message
    eventSrc.addEventListener( "message",function (evt) {
    //process the data
});
این روش نیز بسیار شبیه به حالت long polling است. سرور تا زمانیکه اطلاعاتی را برای پاسخ دهی فراهم نداشته باشد، اتصال را باز نگه می‌دارد. به این ترتیب از لحاظ مقیاس پذیری گزینه بهتری است (نسبت به حالتیکه مدام اتصال برقرار و قطع می‌شود). اکثر مرورگرها منهای نگارش‌های قدیمی IE از این روش پشتیبانی می‌کنند.
تنها تفاوت آن با حالت long polling در این است که پس از ارائه پاسخ به کلاینت، اتصال را قطع نمی‌کند. Long polling نیز اتصال را باز نگه می‌دارد، اما این اتصال را بلافاصله پس از ارائه پاسخ، می‌بندد.

5) Web sockets
Web sockets در سکوی کاری ویندوز، تنها در ویندوز‌های 8، ویندوز سرور 2012 و دات نت 4 و نیم پشتیبانی می‌شود. هرچند این روش در حال حاضر به عنوان بهترین روش Push مطرح است اما به دلیل محدودیتی که یاد شد، مدتی طول خواهد کشید تا استفاده گسترده‌ای پیدا کند.
var socket = new WebSocket("url");
socket.onmessage = function (msg) {
var newInfo = msg.data;
// do something with the data
}
// client can also send request to server
socket.send(.... )
با این اوصاف آیا راه حل بهتر و میانه‌تری وجود دارد؟
بلی. اگر به وضعیت فعلی سکوی کاری ASP.NET نگاه کنیم:

SignalR را می‌توان مشاهده کرد که در گروه ساخت سرویس‌های آن قرار گرفته است. همانطور که ملاحظه می‌کنید، این سرویس جدید آنچنان وابستگی به سایر اجزای آن نداشته و می‌تواند خارج از ASP.NET نیز مورد استفاده قرار گیرد.

SignalR چیست؟

SignalR راه حلی است سمت سرور برای نوشتن push services. همچنین به همراه کتابخانه‌های سمت کاربری است که ارتباطات push services را در انواع و اقسام سکوهای کاری میسر می‌سازد. SignalR سورس باز بوده و برای اعمال غیرهمزمان (asynchronous) بهینه سازی شده است.
SignalR بر اساس مدل ذهنی اتصالات ماندگار (persistent connections) طراحی شده است. اتصالات ماندگار را باید به عنوان اتصالاتی سریع و غیرطولانی درنظر گرفت. در اینجا Signal یک اتصال است که اطلاعاتی به آن ارسال می‌گردد و هدف، انتقال قطعات کوچکی از اطلاعات است و هدف، ارسال حجم عظیمی از اطلاعات نیست. برای مثال اطلاع رسانی سریعی صورت گیرد که تغییراتی رخ داده است و سپس ادامه کار و دریافت اطلاعات واقعی توسط فرآیندهای متداول مثلا HTTP GET انجام شود. البته باید دقت داشت SignalR نیز نهایتا از یکی از 5 روش push بررسی شده در این قسمت استفاده می‌کند. اما بر اساس توانایی‌های کلاینت و سرور، به صورت هوشمند بهترین و بهینه‌ترین انتخاب را به کاربر ارائه می‌دهد.
اتصالات ماندگار قسمت سطح پایین SignalR را تشکیل می‌دهند. سطح بالاتر آن که این مفاهیم را به شکلی کپسوله شده ارائه می‌دهد، Hubs نام دارد که پایه اصلی دوره جاری را تشکیل خواهد داد.



همانطور که عنوان شد، SignalR سورس باز بوده و دارای مخزن کدی عمومی در GitHub است. همچنین بسته‌های تشکیل دهنده‌ی آن از طریق NuGet نیز قابل دریافت هستند. این بسته‌ها شامل هسته SignalR و کلاینت‌های آن مانند کلاینت‌های WinRT، سیلورلایت، jQuery، ویندوز فون8 و امثال آن هستند.

شروع کار با SignalR

تیم SignalR مثالی مقدماتی از نحوه کار با SignalR را به صورت یک بسته NuGet ارائه داده‌اند که از طریق آدرس و فرمان ذیل قابل دریافت است:
 PM> Install-Package Microsoft.AspNet.SignalR.Sample
قبل از اینکه این مثال را دریافت کنید نیاز است ابتدا یک برنامه ASP.NET جدید را آغاز نمائید (تفاوتی نمی‌کند که MVC باشد یا Web forms). سپس دستور فوق را فراخوانی کنید.

پس از دریافت مثال، یکبار پروژه را کامپایل کرده و سپس بر روی فایل StockTicker.html آن کلیک راست نموده و گزینه مشاهده در مرورگر را انتخاب کنید. همچنین برای اینکه این مثال را بهتر مشاهده کنید، بهتر است دو وهله از مرورگر را باز کرده و آدرس باز شده را در آن بررسی کنید تا اعمال تغییرات همزمان به کلاینت‌های متفاوت را بهتر بتوان بررسی و مشاهده کرد.

مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت ششم - کار با User Claims
از Claims جهت ارائه‌ی اطلاعات مرتبط با هویت هر کاربر و همچنین Authorization استفاده می‌شود. برای مثال درنگارش‌های قبلی ASP.NET، مفاهیمی مانند «نقش‌ها» وجود دارند. در نگارش‌های جدیدتر آن، «نقش‌ها» تنها یکی از انواع «User Claims» هستند. در قسمت‌های قبل روش تعریف این Claims را در IDP و همچنین تنظیمات مورد نیاز جهت دریافت آن‌ها را در سمت برنامه‌ی Mvc Client بررسی کردیم. در اینجا مطالب تکمیلی کار با User Claims را مرور خواهیم کرد.


بررسی Claims Transformations

می‌خواهیم Claims بازگشت داده شده‌ی توسط IDP را به یکسری Claims که کار کردن با آن‌ها در برنامه‌ی MVC Client ساده‌تر است، تبدیل کنیم.
زمانیکه اطلاعات Claim، توسط میان‌افزار oidc دریافت می‌شود، ابتدا بررسی می‌شود که آیا دیکشنری نگاشت‌ها وجود دارد یا خیر؟ اگر بله، کار نگاشت‌ها از یک claim type به claim type دیگر انجام می‌شود.
برای مثال لیست claims اصلی بازگشت داده شده‌ی توسط IDP، پس از تبدیلات و نگاشت‌های آن در برنامه‌ی کلاینت، یک چنین شکلی را پیدا می‌کند:
Claim type: sid - Claim value: f3940d6e58cbb576669ee49c90e22cb1
Claim type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
Claim type: http://schemas.microsoft.com/identity/claims/identityprovider - Claim value: local
Claim type: http://schemas.microsoft.com/claims/authnmethodsreferences - Claim value: pwd
Claim type: given_name - Claim value: Vahid
Claim type: family_name - Claim value: N
در ادامه می‌خواهیم نوع‌های آن‌ها را ساده‌تر کنیم و آن‌ها را دقیقا تبدیل به همان claim typeهایی کنیم که در سمت IDP تنظیم شده‌اند. برای این منظور به فایل src\MvcClient\ImageGallery.MvcClient.WebApp\Startup.cs مراجعه کرده و سازنده‌ی آن‌را به صورت زیر تغییر می‌دهیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        }
در اینجا جدول نگاشت‌های پیش‌فرض Claims بازگشت داده شده‌ی توسط IDP به Claims سمت کلاینت، پاک می‌شود.
در ادامه اگر مجددا لیست claims را پس از logout و login، بررسی کنیم، به این صورت تبدیل شده‌است:
• Claim type: sid - Claim value: 91f5a09da5cdbbe18762526da1b996fb
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
• Claim type: idp - Claim value: local
• Claim type: given_name - Claim value: Vahid
• Claim type: family_name - Claim value: N
اکنون این نوع‌ها دقیقا با آن‌چیزی که IDP ارائه می‌دهد، تطابق دارند.


کار با مجموعه‌ی User Claims

تا اینجا لیست this.User.Claims، به همراه تعدادی Claims است که به آن‌ها نیازی نداریم؛ مانند sid که بیانگر session id در سمت IDP است و یا idp به معنای identity provider می‌باشد. حذف آن‌ها حجم کوکی نگهداری کننده‌ی آن‌ها را کاهش می‌دهد. همچنین می‌خواهیم تعدادی دیگر را نیز به آن‌ها اضافه کنیم.
علاوه بر این‌ها میان‌افزار oidc، یکسری از claims دریافتی را راسا فیلتر و حذف می‌کند؛ مانند زمان منقضی شدن توکن و امثال آن که در عمل واقعا به تعدادی از آن‌ها نیازی نداریم. اما می‌توان این سطح تصمیم گیری فیلتر claims رسیده را نیز کنترل کرد. در تنظیمات متد AddOpenIdConnect، خاصیت options.ClaimActions نیز وجود دارد که توسط آن می‌توان بر روی حذف و یا افزوده شدن Claims، کنترل بیشتری را اعمال کرد:
namespace ImageGallery.MvcClient.WebApp
{
        public void ConfigureServices(IServiceCollection services)
        {
// ...
              .AddOpenIdConnect("oidc", options =>
              {
  // ...
                  options.ClaimActions.Remove("amr");
                  options.ClaimActions.DeleteClaim("sid");
                  options.ClaimActions.DeleteClaim("idp");
              });
        }
در اینجا فراخوانی متد Remove، به معنای حذف فیلتری است که کار حذف کردن خودکار claim ویژه‌ی amr را که بیانگر نوع authentication است، انجام می‌دهد (متد Remove در اینجا یعنی مطمئن شویم که amr در لیست claims باقی می‌ماند). همچنین فراخوانی متد DeleteClaim، به معنای حذف کامل یک claim موجود است.

اکنون اگر پس از logout و login، لیست this.User.Claims را بررسی کنیم، دیگر خبری از sid و idp در آن نیست. همچنین claim از نوع amr نیز به صورت پیش‌فرض حذف نشده‌است:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
• Claim type: amr - Claim value: pwd
• Claim type: given_name - Claim value: Vahid
• Claim type: family_name - Claim value: N

افزودن Claim جدید آدرس کاربر

برای افزودن Claim جدید آدرس کاربر، به کلاس src\IDP\DNT.IDP\Config.cs مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // identity-related resources (scopes)
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Address()
            };
        }
در اینجا در لیست Resources، گزینه‌ی IdentityResources.Address نیز اضافه شده‌است که به Claim آدرس مرتبط است.
همین مورد را به لیست AllowedScopes متد GetClients نیز اضافه می‌کنیم:
AllowedScopes =
{
    IdentityServerConstants.StandardScopes.OpenId,
    IdentityServerConstants.StandardScopes.Profile,
    IdentityServerConstants.StandardScopes.Address
},
در آخر Claim متناظر با Address را نیز به اطلاعات هر کاربر، در متد GetUsers، اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
// ...
                    Claims = new List<Claim>
                    {
// ...
                        new Claim("address", "Main Road 1")
                    }
                },
                new TestUser
                {
// ...
                    Claims = new List<Claim>
                    {
// ...
                        new Claim("address", "Big Street 2")
                    }
                }
            };
        }
تا اینجا تنظیمات IDP برای افزودن Claim جدید آدرس به پایان می‌رسد.
پس از آن به کلاس ImageGallery.MvcClient.WebApp\Startup.cs مراجعه می‌کنیم تا درخواست این claim را به لیست scopes میان‌افزار oidc اضافه کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
// ...
              .AddOpenIdConnect("oidc", options =>
              {
   // ...
                  options.Scope.Add("address");
   // …
   options.ClaimActions.DeleteClaim("address");
              });
        }
همچنین می‌خواهیم مطمئن شویم که این claim درون claims identity قرار نمی‌گیرد. به همین جهت DeleteClaim در اینجا فراخوانی شده‌است تا این Claim به کوکی نهایی اضافه نشود تا بتوانیم همواره آخرین مقدار به روز شده‌ی آن‌را از UserInfo Endpoint دریافت کنیم.

یک نکته: فراخوانی DeleteClaim بر روی address غیر ضروری است و می‌شود این سطر را حذف کرد. از این جهت که اگر به سورس OpenID Connect Options مایکروسافت مراجعه کنیم، مشاهده خواهیم کرد که میان‌افزار اعتبارسنجی استاندارد ASP.NET Core، تنها تعداد معدودی از claims را نگاشت می‌کند. به این معنا که هر claim ای که در token وجود داشته باشد، اما اینجا نگاشت نشده باشد، در claims نهایی حضور نخواهند داشت و address claim یکی از این‌ها نیست. بنابراین در لیست نهایی this.User.Claims حضور نخواهد داشت؛ مگر اینکه مطابق همین سورس، با استفاده از متد options.ClaimActions.MapUniqueJsonKey، یک نگاشت جدید را برای آن تهیه کنیم و البته چون نمی‌خواهیم آدرس در لیست claims وجود داشته باشد، این نگاشت را تعریف نخواهیم کرد.


دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint

همانطور که در قسمت قبل با بررسی «تنظیمات بازگشت Claims کاربر به برنامه‌ی کلاینت» عنوان شد، میان‌افزار oidc با UserInfo Endpoint کار می‌کند که تمام عملیات آن خودکار است. در اینجا امکان کار با آن از طریق برنامه نویسی مستقیم نیز جهت دریافت اطلاعات بیشتری از کاربران، وجود دارد. برای مثال شاید به دلایل امنیتی نخواهیم آدرس کاربر را در لیست Claims او قرار دهیم. این مورد سبب کوچک‌تر شدن کوکی متناظر با این اطلاعات و همچنین دسترسی به اطلاعات به روزتری از کاربر می‌شود.
درخواستی که به سمت UserInfo Endpoint ارسال می‌شود، باید یک چنین فرمتی را داشته باشد:
GET idphostaddress/connect/userinfo
Authorization: Bearer R9aty5OPlk
یک درخواست از نوع GET و یا POST است که به سمت آدرس UserInfo Endpoint ارسال می‌شود. در اینجا ذکر Access token نیز ضروری است. از این جهت که بر اساس scopes ذکر شده‌ی در آن، لیست claims درخواستی مشخص شده و بازگشت داده می‌شوند.

اکنون برای دریافت دستی اطلاعات آدرس از IDP و UserInfo Endpoint آن، ابتدا نیاز است بسته‌ی نیوگت IdentityModel را به پروژه‌ی Mvc Client اضافه کنیم:
dotnet add package IdentityModel
توسط این بسته می‌توان به DiscoveryClient دسترسی یافت و به کمک آن آدرس UserInfo Endpoint را استخراج می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        public async Task<IActionResult> OrderFrame()
        {
            var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]);
            var metaDataResponse = await discoveryClient.GetAsync();

            var userInfoClient = new UserInfoClient(metaDataResponse.UserInfoEndpoint);

            var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            var response = await userInfoClient.GetAsync(accessToken);
            if (response.IsError)
            {
                throw new Exception("Problem accessing the UserInfo endpoint.", response.Exception);
            }

            var address = response.Claims.FirstOrDefault(c => c.Type == "address")?.Value;
            return View(new OrderFrameViewModel(address));
        }
در اینجا یک اکشن متد جدید را جهت سفارش نگارش قاب شده‌ی تصاویر گالری اضافه کرده‌ایم. کار این اکشن متد، دریافت آخرین آدرس شخص از IDP و سپس ارسال آن به View متناظر است. برای این منظور ابتدا یک DiscoveryClient را بر اساس آدرس IDP تشکیل داده‌ایم. حاصل آن متادیتای این IDP است که یکی از مشخصات آن UserInfoEndpoint می‌باشد. سپس بر این اساس، UserInfoClient را تشکیل داده و Access token جاری را به سمت این endpoint ارسال می‌کنیم. حاصل آن، آخرین Claims کاربر است که مستقیما از IDP دریافت شده و از کوکی جاری کاربر خوانده نمی‌شود. سپس از این لیست، مقدار Claim متناظر با address را استخراج کرده و آن‌را به View ارسال می‌کنیم.
OrderFrameViewModel ارسالی به View، یک چنین شکلی را دارد:
namespace ImageGallery.MvcClient.ViewModels
{
    public class OrderFrameViewModel
    {
        public string Address { get; } = string.Empty;

        public OrderFrameViewModel(string address)
        {
            Address = address;
        }
    }
}
و View متناظر با این اکشن متد در آدرس Views\Gallery\OrderFrame.cshtml به صورت زیر تعریف شده‌است که آدرس شخص را نمایش می‌دهد:
@model ImageGallery.MvcClient.ViewModels.OrderFrameViewModel
<div class="container">
    <div class="h3 bottomMarginDefault">Order a framed version of your favorite picture.</div>
    <div class="text bottomMarginSmall">We've got this address on record for you:</div>
    <div class="text text-info bottomMarginSmall">@Model.Address</div>
    <div class="text">If this isn't correct, please contact us.</div>
</div>

سپس به Shared\_Layout.cshtml مراجعه کرده و لینکی را به این اکشن متد و View، اضافه می‌کنیم:
<li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>

اکنون اگر برنامه را اجرا کنیم، پس از login، یک چنین خروجی قابل مشاهده است:


همانطور که ملاحظه می‌کنید، آدرس شخص به صورت مستقیم از UserInfo Endpoint دریافت و نمایش داده شده‌است.


بررسی Authorization مبتنی بر نقش‌ها

تا اینجا مرحله‌ی Authentication را که مشخص می‌کند کاربر وار شده‌ی به سیستم کیست، بررسی کردیم که اطلاعات آن از Identity token دریافتی از IDP استخراج می‌شود. مرحله‌ی پس از ورود به سیستم، مشخص کردن سطوح دسترسی کاربر و تعیین این مورد است که کاربر مجاز به انجام چه کارهایی می‌باشد. به این مرحله Authorization می‌گویند و روش‌های مختلفی برای مدیریت آن وجود دارد:
الف) RBAC و یا Role-based Authorization و یا تعیین سطوح دسترسی بر اساس نقش‌های کاربر
در این حالت claim ویژه‌ی role، از IDP دریافت شده و توسط آن یکسری سطوح دسترسی کاربر مشخص می‌شوند. برای مثال کاربر وارد شده‌ی به سیستم می‌تواند تصویری را اضافه کند و یا آیا مجاز است نگارش قاب شده‌ی تصویری را درخواست دهد؟
ب) ABAC و یا Attribute based access control روش دیگر مدیریت سطوح دسترسی است و عموما آن‌را نسبت به حالت الف ترجیح می‌دهند که آن‌را در قسمت‌های بعدی بررسی خواهیم کرد.

در اینجا روش «تعیین سطوح دسترسی بر اساس نقش‌های کاربر» را بررسی می‌کنیم. برای این منظور به تنظیمات IDP در فایل src\IDP\DNT.IDP\Config.cs مراجعه کرده و claims جدیدی را تعریف می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
//...
                    Claims = new List<Claim>
                    {
//...
                        new Claim("role", "PayingUser")
                    }
                },
                new TestUser
                {
//...
                    Claims = new List<Claim>
                    {
//...
                        new Claim("role", "FreeUser")
                    }
                }
            };
        }
در اینجا دو نقش FreeUser و PayingUser به صورت claimهایی جدید به دو کاربر IDP اضافه شده‌اند.

سپس باید برای این claim جدید یک scope جدید را نیز به قسمت GetIdentityResources اضافه کنیم تا توسط client قابل دریافت شود:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                // ...
                new IdentityResource(
                    name: "roles",
                    displayName: "Your role(s)",
                    claimTypes: new List<string>() { "role" })
            };
        }
چون roles یکی از scopeهای استاندارد نیست، آن‌را به صورت یک IdentityResource سفارشی تعریف کرده‌ایم. زمانیکه scope ای به نام roles درخواست می‌شود، لیستی از نوع claimTypes بازگشت داده خواهد شد.

همچنین باید به کلاینت مجوز درخواست این scope را نیز بدهیم. به همین جهت آن‌را به AllowedScopes مشخصات Client نیز اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
// ...
                    AllowedScopes =
                    {
// ...
                        "roles"
                    }
// ...
                }
             };
        }

در ادامه قرار است تنها کاربری که دارای نقش PayingUser است،  امکان دسترسی به سفارش نگارش قاب شده‌ی تصاویر را داشته باشد. به همین جهت به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی کلاینت مراجعه کرده و درخواست scope نقش‌های کاربر را به متد تنظیمات AddOpenIdConnect اضافه می‌کنیم:
options.Scope.Add("roles");

برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار در صفحه‌ی consent، از کاربر مجوز دسترسی به نقش‌های او نیز سؤال پرسیده می‌شود:


اما اگر به لیست موجود در this.User.Claims در برنامه‌ی کلاینت مراجعه کنیم، نقش او را مشاهده نخواهیم کرد و به این لیست اضافه نشده‌است:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
• Claim type: amr - Claim value: pwd
• Claim type: given_name - Claim value: Vahid
• Claim type: family_name - Claim value: N

همانطور که در نکته‌ای پیشتر نیز ذکر شد، چون role جزو لیست نگاشت‌های OpenID Connect Options مایکروسافت نیست، آن‌را به صورت خودکار به لیست claims اضافه نمی‌کند؛ دقیقا مانند آدرسی که بررسی کردیم. برای رفع این مشکل و افزودن نگاشت آن در متد تنظیمات AddOpenIdConnect، می‌توان از متد MapUniqueJsonKey به صورت زیر استفاده کرد:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار لیست this.User.Claims به صورت زیر تغییر کرده‌است که شامل role نیز می‌باشد:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
• Claim type: amr - Claim value: pwd
• Claim type: given_name - Claim value: Vahid
• Claim type: family_name - Claim value: N
• Claim type: role - Claim value: PayingUser


استفاده از نقش تعریف شده جهت محدود کردن دسترسی به سفارش تصاویر قاب شده

در ادامه قصد داریم لینک درخواست تصاویر قاب شده را فقط برای کاربرانی که دارای نقش PayingUser هستند، نمایش دهیم. به همین جهت به فایل Views\Shared\_Layout.cshtml مراجعه کرده و آن‌را به صورت زیر تغییر می‌دهیم:
@if(User.IsInRole("PayingUser"))
{
   <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
در این حالت از متد استاندارد User.IsInRole برای بررسی نقش کاربر جاری استفاده شده‌است. اما این متد هنوز نمی‌داند که نقش‌ها را باید از کجا دریافت کند و اگر آن‌را آزمایش کنید، کار نمی‌کند. برای تکمیل آن به فایل ImageGallery.MvcClient.WebApp\Startup.cs مراجعه کرده تنظیمات متد AddOpenIdConnect را به صورت زیر تغییر می‌دهیم:
options.TokenValidationParameters = new TokenValidationParameters
{
    NameClaimType = JwtClaimTypes.GivenName,
    RoleClaimType = JwtClaimTypes.Role,
};
TokenValidationParameters نحوه‌ی اعتبارسنجی توکن دریافتی از IDP را مشخص می‌کند. همچنین مشخص می‌کند که چه نوع claim ای از token دریافتی  از IDP به RoleClaimType سمت ASP.NET Core نگاشت شود.
اکنون برای آزمایش آن یکبار از سیستم خارج شده و مجددا به آن وارد شوید. پس از آن لینک درخواست نگارش قاب شده‌ی تصاویر، برای کاربر User 1 نمایان خواهد بود و نه برای User 2 که FreeUser است.

البته هرچند تا این لحظه لینک نمایش View متناظر با اکشن متد OrderFrame را امن کرده‌ایم، اما هنوز خود این اکشن متد به صورت مستقیم با وارد کردن آدرس https://localhost:5001/Gallery/OrderFrame در مرورگر قابل دسترسی است. برای رفع این مشکل به کنترلر گالری مراجعه کرده و دسترسی به اکشن متد OrderFrame را توسط فیلتر Authorize و با مقدار دهی خاصیت Roles آن محدود می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Roles = "PayingUser")]
        public async Task<IActionResult> OrderFrame()
        {
خاصیت Roles فیلتر Authorize، می‌تواند چندین نقش جدا شده‌ی توسط کاما را نیز دریافت کند.
برای آزمایش آن، توسط مشخصات کاربر User 2 به سیستم وارد شده و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. در این حالت یک چنین تصویری نمایان خواهد شد:


همانطور که مشاهده می‌کنید، علاوه بر عدم دسترسی به این اکشن متد، به صفحه‌ی Account/AccessDenied که هنوز در برنامه‌ی کلاینت تعریف نشده‌است، هدایت شده‌ایم. به همین جهت خطای 404 و یا یافت نشد، نمایش داده شده‌است.
برای تغییر مقدار پیش‌فرض صفحه‌ی عدم دسترسی، ابتدا Controllers\AuthorizationController.cs را با این محتوا ایجاد می‌کنیم:
using Microsoft.AspNetCore.Mvc;

public class AuthorizationController : Controller
{
    public IActionResult AccessDenied()
    {
        return View();
    }
}
سپس View آن‌را در فایل Views\Authorization\AccessDenied.cshtml به صورت زیر تکمیل خواهیم کرد که لینک به logout نیز در آن وجود دارد:
<div class="container">
    <div class="h3">Woops, looks like you're not authorized to view this page.</div>
    <div>Would you prefer to <a asp-controller="Gallery" asp-action="Logout">log in as someone else</a>?</div>
</div>

اکنون نیاز است تا این آدرس جدید را به کلاس ImageGallery.MvcClient.WebApp\Startup.cs معرفی کنیم.
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options =>
            {
                // ...
            }).AddCookie("Cookies", options =>
              {
                  options.AccessDeniedPath = "/Authorization/AccessDenied";
              })
// ...
آدرس جدید Authorization/AccessDenied باید به تنظیمات AddCookie اضافه شود تا توسط سیستم شناخته شده و مورد استفاده قرار گیرد. علت اینجا است که role claims، از کوکی رمزنگاری شده‌ی برنامه استخراج می‌شوند. به همین جهت تنظیم آن مرتبط است به AddCookie.
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا با اکانت User 2 به آن وارد شوید و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. اینبار تصویر زیر که همان آدرس جدید تنظیم شده‌است نمایش داده خواهد شد:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب دوره‌ها
مراحل Refactoring یک قطعه کد برای اعمال تزریق وابستگی‌ها
برای رسیدن به الگوی معکوس سازی وابستگی‌ها عموما مراحل زیر طی می‌شوند:

الف) در متدهای لایه جاری خود واژه‌های کلیدی new و همچنین کلیه فراخوانی‌های استاتیک را بیابید.
ب) وهله سازی این‌ها را به یک سطح بالاتر (نقطه آغازین برنامه) منتقل کنید. اینکار باید بر اساس اتکای به Abstraction و برای مثال استفاده از اینترفیس‌ها صورت گیرد.
ج) اینکار را آنقدر تکرار کنید تا دیگر در کدهای لایه جاری خود واژه کلیدی new یا فراخوانی متدهای استاتیک مشاهده نشود.
د) در آخر وهله سازی object graphهای مورد نیاز را به یک IoC Container محول کنید.


یک مثال: ابتدا بررسی یک قطعه کد متداول

using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Mvc;

namespace DI06.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            string result = string.Empty;
            using (var client = new WebClient { Encoding = Encoding.UTF8 })
            {
                result = client.DownloadString("https://www.dntips.ir/");
            }
            var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(result);
            var title = match.Groups[1].Value.Trim();

            ViewBag.PageTitle = title;
            return View();
        }
    }
}
فرض کنید یک برنامه ASP.NET MVC را به نحو فوق تهیه کرده‌ایم. در کدهای کنترلر آن قصد داریم محتویات Html یک صفحه خاص را دریافت و سپس عنوان آن‌را استخراج کرده و نمایش دهیم.
مشکلات کد فوق:
الف) قرار گرفتن منطق تجاری پیاده سازی کدها مستقیما داخل کدهای یک اکشن متد؛ این مساله در دراز مدت به تکرار شدید کدها منجر خواهد شد که نهایتا قابلیت نگهداری آن‌را کاهش می‌دهند.
ب) در این کد حداقل دو بار واژه کلیدی new ذکر شده است. مورد اول یا new WebClient، از همه مهم‌تر است؛ از این جهت که نوشتن آزمون واحد را برای این کنترلر بسیار مشکل می‌کند. آزمون‌های واحد باید سریع و بدون نیاز به منابع خارجی، قابل اجرا باشند. تعویض آن هم مطابق کدهای تدارک دیده شده کار ساده‌ای نیست.


بهبود کیفیت قطعه کد متداول فوق با استفاده از الگوی معکوس سازی وابستگی‌ها

در اصل معکوس سازی وابستگی‌ها عنوان کردیم لایه بالایی سیستم نباید مستقیما به لایه‌های زیرین در حال استفاده از آن، وابسته باشد. این وابستگی باید معکوس شده و همچنین بر اساس Abstraction یا برای مثال استفاده از اینترفیس‌ها صورت گیرد.
به همین منظور یک پروژه دیگر را از نوع Class library، مثلا به نام DI06.Services به Solution جاری اضافه می‌کنیم.
namespace DI06.Services
{
    public interface IWebClientServices
    {
        string FetchUrl(string url);
        string GetWebPageTitle(string url);
    }
}

using System.Net;
using System.Text;
using System.Text.RegularExpressions;

namespace DI06.Services
{
    public class WebClientServices : IWebClientServices
    {
        public string FetchUrl(string url)
        {
            using (var client = new WebClient { Encoding = Encoding.UTF8 })
            {
                return client.DownloadString(url);
            }
        }

        public string GetWebPageTitle(string url)
        {
            var html = FetchUrl(url);
            var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(html);
            return match.Groups[1].Value.Trim();
        }
    }
}
در این لایه، سرویس WebClient ایی را تدارک دیده‌ایم. این سرویس می‌تواند محتوای Html یک آدرس را برگرداند و یا عنوان آن آدرس خاص را استخراج نماید.
هنوز کار معکوس سازی وابستگی‌ها رخ نداده است. صرفا اندکی تمیزکاری و انتقال پیاده سازی منطق تجاری به یک سری کلاس‌هایی با قابلیت استفاده مجدد صورت گرفته است. به این ترتیب اگر باگی در این کدها وجود داشته باشد و همچنین از آن در چندین نقطه برنامه استفاده شده باشد، اصلاح این کلاس مرکزی، به یکباره تمامی قسمت‌های مختلف برنامه را تحت تاثیر مثبت قرار داده و از تکرار کدها و فراموشی احتمالی بهبود قسمت‌های مشابه جلوگیری می‌کند.
کار معکوس سازی وابستگی‌ها در یک لایه بالاتر صورت خواهد گرفت:
using System.Web.Mvc;
using DI06.Services;

namespace DI06.Controllers
{
    public class HomeController : Controller
    {
        readonly IWebClientServices _webClientServices;
        public HomeController(IWebClientServices webClientServices)
        {
            _webClientServices = webClientServices;
        }

        public ActionResult Index()
        {
            ViewBag.PageTitle = _webClientServices.GetWebPageTitle("https://www.dntips.ir/");
            return View();
        }
    }
}
اینبار کنترلر Home را توسط تزریق وابستگی‌ها در سازنده کنترلر، بازنویسی کرده‌ایم. کد نهایی بسیار تمیزتر از حالت قبل است. دیگر پیاده سازی متد GetWebPageTitle مستقیما داخل یک اکشن متد قرار نگرفته است. همچنین این کنترلر اصلا نمی‌داند که قرار است از کدام پیاده سازی اینترفیس IWebClientServices استفاده کند. اگر در تنظیمات اولیه IoC Container مورد استفاده، کلاس WebClientServices ذکر شده باشد، از آن استفاده خواهد کرد؛ یا اگر حتی کلاس WebClientServicesFake نیز معرفی گردد (جهت انجام آزمون غیر وابسته به new WebClient)، باز هم بدون کوچکترین تغییری در کدهای آن قابل استفاده خواهد بود.

در مورد نحوه تنظیمات اولیه یک IoC Container و یا پیشنیازهای ASP.NET MVC جهت آماده شدن برای تزریق خودکار وابستگی‌ها در سازنده کنترلرها، پیشتر مطالبی را در این سری مطالعه کرده‌اید؛ در اینجا نیز اصول مورد استفاده یکی است و تفاوتی نمی‌کند.
مطالب
CoffeeScript #12

بخش‌های بد

جاوااسکریپت یک زبان پیچیده است که شما برای کار با آن، نیاز است قسمت‌هایی را که باید از آن‌ها دوری کنید و قسمت‌های مهمی را که باید استفاده کنید، بشناسید. همانطور که Sun Tzu گفته "دشمن خود را بشناس"، ما نیز در این قسمت می‌خواهیم برای شناخت بیشتر قسمت‌های تاریک و روشن جاوااسکریپت به آن بپردازیم.

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

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

A JavaScript Subset

with یک دستور بسیار زمانبر است و مضر شناخته شده است و نباید از آن استفاده کنید. with با ایجاد یک ساختار خلاصه نویسی، برای جستجو بر روی خصوصیات اشیاء در نظر گرفته شده بود. برای نمونه به جای نوشتن:

dataObj.users.vahid.email = "info@vmt.ir";
می‌توانید به این صورت این کار را انجام دهید:
with(dataObj.users.vahid) {
  email = "info@vmt.ir";
}
مفسر جاوااسکریپت دقیقا نمی‌داند که شما می‌خواهید چه کاری را با with انجام دهید، و به شیء مشخص شده فشار می‌آورد تا اول اسم همه مراجعه شده‌ها را جستجو کند. این عمل واقعا به عملکرد و کارآیی لطمه می‌زند. یعنی مترجم، تمام انواع بهینه سازی‌های JIT را خاموش می‌کند. همچنین پیشنهادهایی مبنی بر حذف کامل آن از نسخه‌های بعدی جاوااسکریپت نیز مطرح شده است.
همه چیز برای عدم استفاده از with در نظر گرفته شده است. CoffeeScript یک قدم جلوتر از همه برداشته و with را از syntax خود حذف کرده است. به عبارت دیگر در صورتیکه شما از آن استفاده کنید، کامپایلر CoffeeScript خطا صادر می‌کند.

Global variables

به طور پیش فرض تمامی برنامه‌های جاوااسکریپت در دامنه global اجرا می‌شوند و تمامی متغیرهایی که ساخته می‌شوند به طور پیش فرض در ناحیه‌ی global قرار می‌گیرند. اگر شما بخواهید متغیری را در ناحیه‌ی local ایجاد کنید، باید از کلمه کلیدی var استفاده کنید.

usersCount = 1;        // Global
var groupsCount = 2;   // Global

(function(){              
  pagesCount = 3;      // Global
  var postsCount = 4;  // Local
})()
اکثر اوقات شما می‌خواهید متغیر local ایی را ایجاد کنید و نه global. توسعه دهندگان باید همیشه به یاد داشته باشند که قبل از مقداردهی اولیه‌ی هر متغیری، کلمه‌ی کلیدی var را قرار دهند یا با انواع و اقسام مشکلات، هنگامی که متغیرها به طور تصادفی با یکدیگر برخورد و یا بازنویسی بر روی یکدیگر انجام می‌دهند، روبرو شوند.
خوشبختانه CoffeeScript به کمک شما می‌آید و به طور کامل انتساب متغیرهای global را به طور ضمنی از بین می‌برد. به عبارت دیگر کلمه کلیدی var در CoffeeScript رزرو شده است و در صورت استفاده خطا صادر می‌شود.
به صورت پیش فرض به طور ضمنی متغیرها local ایجاد می‌شوند و خیلی سخت می‌شود متغیر global ایی را بدون انتساب آن به عنوان خصوصیتی از شیء window ایجاد کرد.
outerScope = true
do ->
  innerScope = true
نتیجه‌ی کامپایل آن می‌شود:
var outerScope;
outerScope = true;
(function() {
  var innerScope;
  return innerScope = true;
})();
همانطور که مشاهده می‌کنید CoffeeScript مقداردهی اولیه متغیر را (با استفاده از var) به صورت خودکار در context ایی که برای اولین بار استفاده شده است انجام می‌دهد. باید مواظب باشید تا از نام متغیر خارجی مجددا استفاده نکنید که این اتفاق ممکن است در کلاس یا تابع با عمق زیاد ایجاد شود. برای مثال، در اینجا به صورت تصادفی متغیر package در یک تابع کلاس بازنویسی شده است:
package = require('./package')

class Test
  build: ->
    # Overwrites outer variable!
    package = @testPackage.compile()

  testPackage: ->
    package.create()
برای ایجاد متغیرهای global باید از انتساب آنها به عنوان خصوصیتی از شیء window استفاده کرد.
  class window.Asset
    constructor: ->
با تضمین متغیرهای global به صورت صریح و روشن به جای به طور ضمنی بودن آنها، CoffeeScript یکی از منابع اصلی ایجاد مشکلات در جاوااسکریپت را حذف کرد‌ه‌است.

Semicolons

جاوااسکریپت اجباری برای نوشتن ";" ندارد، بنابراین ممکن است یک سری از دستورات از قلم بیافتند. با این حال در پشت صحنه‌ی کامپایلر جاوااسکریپت به ";" احتیاج دارد. به طوری که parser جاوااسکریپت به صورت خودکار هر زمانی که نتواند ارزیابی از دستورات داشته باشد، یک بار دیگر با ";" این کار را انجام می‌دهد و درصورت موفقیت، پیام خطایی مبنی بر نبود ";" را صادر می‌کند.
متاسفانه این یک ایده بد است. چرا که ممکن است تغییر رفتاری در کد نوشته شده به وجود آید. به مثال زیر توجه کنید. به نظر کد نوشته شده صحیح است؛ درسته؟
function() {}
(window.options || {}).property
اشتباه است، حداقل با توجه به parser، یک خطای syntax صادر می‌شود. در مورد دوم نیز parser، ";" اضافه نمی‌کند و کد نوشته شده به کد یک خطی تبدیل می‌شوند.
function() {}(window.options || {}).property
حالا شما می‌توانید این موضوع را ببینید که چرا parser خطا داده‌است. وقتی شما در حال نوشتن کد جاوااسکریپتی هستید، باید بعد از هر دستور از ";" استفاده کنید. خوشبختانه در تمام زمانیکه درحال نوشتن کد CoffeeScript هستید، نیازی به نوشتن ";" ندارید. در زمانیکه کد CoffeeScript نوشته شده کامپایل می‌شود، به صورت خودکار ";" را در جای مناسبی قرار می‌دهد.

مطالب
اصول طراحی شی گرا SOLID - #بخش چهارم اصل ISP

اصل 4) I – ISP– Interface Segregation principle
مقایسه با دنیای واقعی:

بیایید فکر کنیم شما یک کامپیوتر دسکتاپ جدید خریداری کرده اید.شما یک زوج پورت USP، چند پورت سریال، یک پورت VGA و ...  را پیدا میکنید. اگر شما بدنه‌ی کیس خود را باز کنید، تعدادی اسلات بر روی مادربرد خود که برای اتصال قطعات مختلف با یکدیگر هستند را مشاهده خواهید کرد که عمدتا مهندسان سخت افزار در زمان منتاژ از آنها استفاده میکنند.
این اسلات‌ها تا زمانی که بدنه کیس را باز نکنید قابل مشاهده نخواهند بود. به طور خلاصه تنها رابطه‌های مورد نیازی که برای شما ساخته شده اند، قابل مشاهده خواهند بود.
و فرض کنید شما به یک توپ فوتبال نیاز دارید. به یک فروشگاه وسایل ورزشی میروید و فروشنده شروع به نشان دادن انواع توپ ها، کفش ها،لباس  و گرم کن‌های ورزشی،لباس شنا و کاراته ،زانوبند و ... کرده است. در نهایت ممکن است شما چیزی را خریداری کنید که اصلا مورد نیازتان نبوده است یا حتی ممکن است فراموش کنیم که ما چرا اینجا هستیم.!
ارائه مشکل:
ما میخواهیم یک سیستم مدیریت گزارشات را توسعه بدهیم. اولین کاری که انجام می‌دهیم ایجاد یک لایه منطقی که توسط سه UI مختلف دیگر مورد استفاده قرار میگیرد.
1- EmployeeUI : نمایش گزارش‌های مربوط با کارمند وارد شده‌ی جاری در سایت.
2- ManagerUI  : نمایش گزارش‌های مربوط به خود و تیمی که به او تعلق دارد.
3- AdminUI : نمایش گزارش‌های مربوط به کارمندان، مربوط به تیم و مربوط به شرکت مانند گزارش سود و زیان و ...
public interface IReportBAL
{
    void GeneratePFReport();
    void GenerateESICReport();

    void GenerateResourcePerformanceReport();
    void GenerateProjectSchedule();

    void GenerateProfitReport();
}
public class ReportBAL : IReportBAL
{    
    public void GeneratePFReport()
    {/*...............*/}

    public void GenerateESICReport()
    {/*...............*/}

    public void GenerateResourcePerformanceReport()
    {/*...............*/}

    public void GenerateProjectSchedule()
    {/*...............*/}

    public void GenerateProfitReport()
    {/*...............*/}
}
public class EmployeeUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
    }
}
public class ManagerUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport ();
        objBal.GenerateProjectSchedule ();
    }
}
public class AdminUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport();
        objBal.GenerateProjectSchedule();
        objBal.GenerateProfitReport();
    }
}
حال زمانی که توسعه دهنده در هر UI عبارت objBal را تایپ کند، در لیست بازشوی آن (intellisense )  به صورت زیر نمایش داده خواهد شد : 

اما مشکل چیست؟ مشکل این است شخصی که روی لایه‌ی EmployeeUI کار میکند به تمام متدها‌ها به خوبی دسترسی دارد و این متدهای غیرضروری ممکن است باعث سردرگمی او شود.
این مشکل به این دلیل اتفاق می‌افتد که وقتی کلاسی یک واسط را پیاده سازی می‌کند، باید همه متدهای آن را نیز پیاده سازی کند. حالا اگر خیلی از این متدها توسط این کلاس استفاده نشود، می‌گوییم که این کلاس از مشکل واسط چاق رنج می‌برد.
اصل ISP  میگوید : " کلاینت‌ها نباید وابسته به متدهایی باشند که آنها را پیاده سازی نمی‌کنند. "
برای رسیدن به این امر در مثال بالا باید آن واسط را به واسط‌های کوچکتر تقسیم کرد. این تقسیم بندی باید بر اساس استفاده کنندگان از واسط‌ها صورت گیرد.
ویرایش مثال بالا با در نظر گرفتن اصل ISP  :
public interface IEmployeeReportBAL
{
    void GeneratePFReport();
    void GenerateESICReport();
}
public interface IManagerReportBAL : IEmployeeReportBAL
{
    void GenerateResourcePerformanceReport();
    void GenerateProjectSchedule();
}
public interface IAdminReportBAL : IManagerReportBAL
{
    void GenerateProfitReport();
}
public class ReportBAL : IAdminReportBAL 
{    
    public void GeneratePFReport()
    {/*...............*/}

    public void GenerateESICReport()
    {/*...............*/}

    public void GenerateResourcePerformanceReport()
    {/*...............*/}

    public void GenerateProjectSchedule()
    {/*...............*/}

    public void GenerateProfitReport()
    {/*...............*/}
}
وضعیت نمایش :
public class EmployeeUI
{
    public void DisplayUI()
    {
        IEmployeeReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
    }
}
  



public class ManagerUI
{
    public void DisplayUI()
    {
        IManagerReportBAL  objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport ();
        objBal.GenerateProjectSchedule ();
    }
}
 




public class AdminUI
{
    public void DisplayUI()
    {
        IAdminReportBAL  objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport();
        objBal.GenerateProjectSchedule();
        objBal.GenerateProfitReport();
    }
}





  DIP   : در قسمت بعدی آخرین اصل را بررسی خواهیم کرد  
مطالب
ایجاد چارت سازمانی تحت وب #1
برای ایجاد چارت سازمانی همواره از راههای دیگر استفاده میکردیم مثلا از تصویر و فایل PDF ، فلش و یا ...
این مورد همیشه باعث اذیت طراحان وب و برنامه نویسان تحت وب بود. با ظهور برخی امکانات در HTML5 که میتوانید توضیحات آن را در مطلب Canvas Reference - قسمت اول سایت جاری مطالعه نمائید ، هم اکنون با استفاده از این امکانات و کتابخانه مربوط به ساخت نمودار سازمانی میتوانید مانند شکل ذیل نمودار در وب ایجاد نمائید.

نمودار سازمانی

برای اینکار ابتدا یک صفحه index.html ایجاد نموده و در قسمت body آن یک المنت از نوع canvas ایجاد نمائید:

<canvas id="canvas1" width="800" height="800" ></canvas>
سپس فایلهای مورد نیاز را در قسمت head آن ارجاع نمائید:
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" >
<!--[if ie]><script type="text/javascript" src="excanvas.js"></script><![endif]-->
<script type="text/javascript" src="orgchart.js"></script>
حال در انتهای متن فایل html این کد را قرار دهید :
var o = new orgChart();

o.addNode(1, '', '', 'Root node');
o.addNode(2, 1, 'u', 'u-node 1');
o.addNode(3, 1, 'u', 'u-node 2');
o.addNode(4, 1, 'u', 'u-node 3');
o.addNode(5, 1, 'l', 'l-node 1');
o.addNode(6, 1, 'l', 'l-node 2');
o.addNode(7, 1, 'r', 'r-node 1');
o.addNode(8, 1, 'r', 'r-node 2');
o.addNode(9, 1, 'r', 'r-node 3');

o.addNode('', '', '', 'Root 2');
o.addNode('', 'Root 2', 'r', 'using');
o.addNode('', 'Root 2', 'r', 'text as\nid');

o.drawChart('canvas1');
انواع نود در نمودار :
u -> نودهایی که در یک سطح افقی (کنار هم ) و در سطح زیرین یک پدر قرار میگیرند.
l -> نودهایی که در یک سطح عمودی ( زیر هم ) و در سمت چپ نود پدر قرار میگیرند.
r -> نوهایی که در یک سطح عمودی ( زیر هم ) و در سمت راست نود پدر قرار میگیرند.

پارامترهای نودها :
  1. شناسه نود است و میتواند عدد یا رشته باشد . در صورتی که خالی بماند متن نود بعنوان شناسه استفاده میشود.
  2. شناسه نود پدر است و در صورتی که خالی ( یا ناشناس ) باشد بعنوان نود اصلی ( پدر ) نمایش داده میشود.
  3. نوع اتصال نودهای r , l , u . برای نود پدر نادیده گرفته میشود.
  4. متن نود . استفاده از "n\"  جلمه را شکسته و به خط جدید منتقل میکند.
  5. نود با نشانه مشخص ( اگر 1 باشد ، حاشیه با ضخامت کشیده میشود. ( اختیاری )
  6. آدرس یک نود - اختصاص آدرس url  جهت اضافه کردن آن به یک نود ( اختیاری )
  7. رنگ خط حاشیه ( اختیاری )
  8. رنگ پس زمینه ( اختیاری )
  9. رنگ متن و فونت ( اختیاری )

پارامترهای تابع ()drowChart :

  1. شناسه المنت canvas
  2. تراز کردن نمودار با استفاده از حرف 'c' یا کلمه 'center' . در صورت حذف ، نمودار در سمت چپ صفحه نمایش داده خواهد شد. تراز چپ ( اختیاری )
  3. تناسب اندازه canvas .  اگر true باشد canvas به اندازه نمودار چارت ، تغییر اندازه میدهد . (اختیاری)
مطمئن باشید که فایل index.html , excanvas.js  ,orgchart.js  در یک فولدر قرار گرفته باشند .
در مطلب بعدی نمایش با رنگهای مختلف ارائه خواهد شد.

مورد نیازهای مطلب جاری :
  1. excanvas.js
  2. orgchart.js
  3. index.html