مطالب
آشنایی اولیه با gRPC
در مقاله‌ی قبلی بطور کلی با Protocol ‌Buffers آشنا شدیم. در این قسمت با gRPC  آشنا شده و همچنین به پیاده سازی یک سرور و کلاینت، با استفاده از gRPC پرداخته که توسط آن به تبادل اطلاعات با یکدیگر میپردازند. 
gRPC یک فریم ورک مدرن و متن باز با کارآیی بالاست. توسط گوگل پیاده سازی شده و جزء انجمن CNCF میباشد (مثل Docker & Kubernetes) که بر روی سیستم عامل‌های متعددی اجرا میشود. به صورت خیلی کارا میتواند سرویس‌های متعددی را به یکدیگر متصل کرده و همچنین از امکاناتی همچون load balancing, monitoring, tracing, health checking, authentication به صورت خیلی ساده پشتیبانی میکند. بسایر سریع و همچنین Low Latency است. مستقل از یک زبان برنامه نویسی خاص هست و برای streaming بسیار مناسب است و همچنین برای سیستم‌های توزیع شده پیشنهاد میشود و به راحتی قابل توسعه و نگهداری است.
راجع به مزایای gRPC بسیار صحبت کردیم. برای طراحی سرویس‌های متعددی که با یکدیگر در ارتباط هستند، مناسب میباشد. از HTTP/2 به صورت پیشفرض استفاده میکند (راجع به تفاوت HTTP/2 و HTTP1 اینجا  را مطالعه بفرمایید).
شاید بزرگترین مشکلی که در حال حاضر دارد این است که REST را پشتیبانی نمیکند. بدین معنا که شما از طریق browser نمیتوانید یک در خواست را به یک سرور پیاده سازی شده توسط gRPC بصورت مستقیم ارسال کنید. راه حل اول برای حل این مشکل، پیاده سازی یک restful gateway با ابزار دلخواه خود و بقیه سرویس‌ها بعد از آن به هم از طریق gRPC ارتباط برقرا میکنند، یا راه حل بهتر اینکه از grpc-gateway  استفاده شود. ابزاری است که به کمک آن میتوانید سیستم خود را با REST یکپارچه سازی نمایید (هر چند راه‌های دیگری برای وصل شدن از مرورگر به یک سرور gRPC با استفاده از کتابخانه‌های third party میسر شده، اما خارج از موضوع بحث است و مطالعه‌ی بیشتر را به خواننده واگذار میکنم)
قدم اول در پیاده سازی یک سرور/کلاینت با استفاده از gRPC، آشنایی با protocol buffers هست. برای آشنایی، به مقاله‌ی قبلی رجوع فرمایید. تمامی پیاده سازی‌های ما از روی کد‌های تولید شده از تعاریف protocol bufferهایی هست که نوشته‌ایم.
حال فرض کنید میخواهیم یک سرور gRPC را با استفاده از #C نوشته و پیاده سازی نماییم:
۱) قدم اول قطعا نوشتن protobuf می‌باش‍د‍‍. همانطور که در مقاله‌ی قبلی ذکر شده است، به صورت زیر، مدل و همچنین متد‌های لازم را معرفی مینماییم و نام آن را helloworld.proto قرار میدهیم.
syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
مدل و سرویس‌ها بصورت واضحی نوشته شده‌اند؛ SayHello با ورودی HelloRequest و خروجی HelloReply تعیین شده‌است.
۲) حالا کافی است یک پروژه‌ی Console را ساخته و ابتدا پکیج‌های زیر را نصب نماییم.
Google.Protobuf
grpc
Grpc.Tools
از طریق Grpc.Tools میتوانیم protobuf‌های خود را بصورت خودکار بعد از build تولید نماییم. در csproj آیتم زیر را اضافه کرده و آدرس protobuf را تعیین مینماییم.
<ItemGroup>
      <Protobuf Include="helloworld.proto" />
  </ItemGroup>
حال کافی است کد‌های زیر را جایگزین نماییم:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Helloworld;
using Grpc.Core;

namespace ServerGrpc
{
    class GreeterImpl : Greeter.GreeterBase
    {
        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            System.Console.WriteLine("request made!");
            return Task.FromResult(new HelloReply { Message = "Hello " + request.Name });
        }
    }

    class Program
    {
       const int Port = 50051;

        public static void Main(string[] args)
        {
            Server server = new Server
            {
                Services = { Greeter.BindService(new GreeterImpl()) },
                Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) }
            };
            server.Start();

            Console.WriteLine("Greeter server listening on port " + Port);
            Console.WriteLine("Press any key to stop the server...");
            Console.ReadKey();

            server.ShutdownAsync().Wait();
        }
    }
}
همانطور که مشاهده میکنید، مدل‌ها و سریس‌ها بصورت خودکار تولید شده‌اند (ضمن اینکه میتوانستیم بصورت دستی نیز protobuf را برای زبان دلخواه خود تولید نماییم).
سرور را بر روی پورت مشخصی ایجاد کرده و همچنین سرویس مورد نظرمان را پیاده سازی کرده‌ایم؛ به صورت فوق همه چیز به ساده‌ترین صورت در نظر گرفته شده است.
gRPC به صورت خودکار از پروتکل امن ssl استفاده میکند؛ اما برای راحتی کار ما از آن استفاده نکرده‌ایم.
نکته: فایل‌های generate شده را از طریق آدرس زیر میتوانید پیدا کنید:
obj/Debug/netcoreapp2.2(یا نسخه‌ی دیگری که استفاده میکنید)

حالا بنا داریم یک کلاینت را با یک زبان برنامه نویسی کاملا مجزا نوشته و به سرور grpc متصل شویم. این کلاینت را با زبان Go خواهیم نوشت (بدیهی است می‌توان جای زبان‌های برنامه نویسی کلاینت و سرور را تغییر داد).
نکته: خیلی وارد جزیات زبان Go نمی‌شویم و فقط اشاره‌ای به موارد کلی خواهیم کرد.
ابتدا باید از روی protobuf کد مربوط به Go را تولید نماییم؛ به صورت زیر:
protoc helloworld.proto --go_out=plugins=grpc:.
فرض کنید فایل generate شده در پوشه‌ی proto قرار گرفته به نام "helloworld.pb.go"
یک فایل به نام main.go ساخته و کد‌های زیر را وارد مینماییم.
package main
import (
        "fmt"
        "golang.org/x/net/context"
        "google.golang.org/grpc"
        "gosample/proto"
)
func main() {
    initial()
}

func initial(){
    conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
    defer conn.Close()
    client := helloworld.NewGreeterClient(conn)
    data, _ := client.SayHello(context.Background(), &helloworld.HelloRequest{Name : "Ali"})

    fmt.Println(data)
}

 به سرور به صورت insecure متصل شده ایم؛ آخر برنامه connection را می‌بندیم و SayHello را فراخوانی کرده و جواب را بر روی خروجی نمایش میدهیم.
نکته: gosample اسم پروژه‌ای است که من ساخته‌ام و proto آدرس پوشه‌ای است که فایل تولید شده‌ی grpc داخل آن قرار گرفته‌است؛ بقیه نیز کتابخانه‌های لازم برای کار با grpc میباشد.
نکته: gRPC برای streaming دیتا بسیار مناسب است (هم یکطرفه و همینطور دو طرفه).
نکته: به دلیل سادگی کار با ابزار‌های مختلف، انتخاب خیلی خوبی برای سیستم‌های توزیع شده‌است؛ همانطور که مشاهده کردید به راحتی قابلیت تعامل بین زبان‌های برنامه نویسی متعددی برقرار است.
نکته‌ی آخر: از وارد شدن به موارد ریز اجتناب کرده‌ام و صرفا این مقاله جهت آشنایی و دید کلی نسبت به این موضوع در نظر گرفته شده‌است.
نظرات مطالب
برش تصاویر قبل از آپلود (Crop)
پیرو مطلب بالا، در صورتی که تصویر شما ابعادی بزرگتر از اندازه صفحه داشته باشد باعث میشود که عکس به درستی در صفحه جا نشود، به همین دلیل ممکن است تگ img را با تگ Div با مشخصات زیر بپوشانید:
 
  <div style="overflow: auto">
            <img  id="preview" />
        </div>
این کد باعث میشود که عکس به صورت تمام در صفحه نمایش داده شود و قسمت‌های نهان آن با اسکرول جابجا شوند. در اینجا چند مشکل ایجاد می‌شود اول اینکه زیبایی صفحه زیاد نیست، دوم اینکه در حین کراپ چندان خوشاید نیست و سوم اینکه در صفحه موبایل عکس بزرگ و اسکرول چندان کارایی ندارد.

در این بین من با استفاده از فریمورک بوت استراپ کلاس img-responsive را به تگ image می‌دهم تا تصویر در هر صفحه نمایشی متناسب با اندازه آن نمایش داده شود. مشکل زیبایی تصویر در صفحه و نحوه کراپ کردن آن حل میشود ولی مشکل جدیدتر این است که تصویری که کراپ می‌شود آن ناحیه ای نیست که شما قبلا انتخاب کرده بودید؛ دلیل آن هم این است که تصویری که responsive شده است اندازه جدیدی برای خود دارد و برش ناحیه در سمت کلاینت و مختصاتی که به شما داده میشود بر اساس اندازه تغییر یافته است ولی در سمت سرور شما با اندازه واقعی تصویر سر و کار دارید و به همین دلیل مختصات ناحیه برش داده شده اشتباه بوده و قسمت‌های دیگری از تصویر برش میخورند.
برای حل این مشکل ابتدا دو المان مخفی زیر را به صفحه اضافه میکنیم:
<input type="hidden" id="RealW" name="RealW" />
<input type="hidden" id="RealH" name="RealH" />
این دو المان برای نگهداری اندازه تگ img است که ممکن است در هر صفحه نمایشی متفاوت باشد و کراپ کردن بر اساس آن انجام میشود.
سپس کدهای زیر را به فایل js به شکل زیر ویرایش می‌کنیم:
$('#preview').Jcrop({

                    aspectRatio: 2, 
                    bgFade: true, 
                    bgOpacity: .3,
                    onChange: updateInfo,
                    onSelect: updateInfo
                }, function () {
                    // use the Jcrop API to get the real image size

//============== خطوط جدید
                    $("#RealW").val($('#preview').css("width").replace("px", ""));
                    $("#RealH").val($('#preview').css("height").replace("px", ""));
//==============

                    jcrop_api = this;
                });
دو خط جدید اضافه شده به این کد اندازه تگ img را درخواست کرده و داخل المان‌های مخفی جای میدهد تا در هنگام پست شدن صفحه ارسال شوند و از آنجا که اندازه‌های دریافتی به همراه نام واحد  میباشند (که در اینجا px است) آن‌ها را از انتهای رشته حذف کرده ام.
سپس در سمت کلاینت با محاسبه فرمول‌های تناسب این مشکل را رفع میکنیم تا مختصات‌های دریافت شده را به مختصات‌های واقعی تبدیل کنیم:
 public static byte[] Resize(this byte[] byteImageIn, int x1,int y1,int x2,int y2,int realW,int realH)
        {

            //convert to full size image
            ImageConverter ic = new ImageConverter();
            Image src = (Image)(ic.ConvertFrom(byteImageIn)); //original size

            if (src == null)
                return null;

// چهار خط زیر فرمول تناسب را اجرا می‌کند
            x1 = src.Width * x1 / realW;
            x2 = src.Width * x2 / realW;

            y1 = src.Height * y1 / realH;
            y2 = src.Height * y2 / realH;

            Bitmap target = new Bitmap(x2 - x1, y2 - y1);
            using (Graphics graphics = Graphics.FromImage(target))
                graphics.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height),
                    new Rectangle(x1,y1,x2-x1,y2-y1), 
                    GraphicsUnit.Pixel);
            src = target;
            using (var ms = new MemoryStream())
            {
                src.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
                return ms.ToArray();
            }

        }
مطالب
نکاتی درباره پرس و جو با استفاده از پردازش موازی

برای انجام عملیات  پرس و جوی LINQ با استفاده از روش پردازش موازی به راحتی میتوان الحاقیه AsParallel را به هر داده‌ای از نوع IEnumerable<T> افزود: 

var data = 
new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// پرس و جوی عادی
var q1 = from i in data select i;
// پرس و جو به شیوه موازی
var q2 = from i in data.AsParallel() select i;

الحاقیه .AsParallel() در پرس و جویq2  نسخه موازی LINQ  را بر روی متغیر data  اجرا میکند و اگر همه چیز به صورت صحیح انجام شود هر دو پرس و جو باید نتایج یکسانی داشته باشند، اما نتایج عبارتند از :

//نتیجه اجرای q1
0 1 2 3 4 5 6 7 8 9 10
//نتیجه اجرای q2
0 6 1 7 2 8 3 9 4 10 5

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

حال اگر چگونگی ترتیب داده‌ها، برایمان مهم است به دو روش می‌توانیم آن را انجام دهیم:

1- افزودن عبارت orderby به پرس و جو

2- استفاده از الحاقیه AsOrdered 

var q3 = from i in data.AsParallel() orderby i  select i;

var q4 = from i in data.AsParallel().AsOrdered() select i;

که نتیجه انجام هر دو پرس و جوی بالا یکی خواهد بود. حال مسأله دیگر این است که آیا همیشه استفاده از پردازش موازی مفید خواهد بود یا خیر؟پاسخ این سؤال وابسته است به نوع مسأله و حجم داده مورد نظر و مشخصات سیستمی که قرار است از آن کد استفاده کند. چگونگی اندازه سرعت و مقدار مصرف حافظه در اجرای چهار پرس و جوی فوق  در کامپیوتر من با پردازنده Intel Q9550 به شکل زیر است:

نتیجه تست پرس و جوی موازی

مطالب
ارتقاء به 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
مطالب
نمایش، ذخیره و چاپ فایل‌های PDF در برنامه‌های Angular
با توجه به اینکه فایل‌های PDF نیز فایل باینری هستند، کلیات نکات مطلب «دریافت و نمایش تصاویر از سرور در برنامه‌های Angular» در مورد آن‌ها هم صادق است. در اینجا به تکمیل این نکات پرداخته و مواردی را مانند ذخیره، چاپ و استفاده از اشیاء نمایشی <object>، <embed> و <iframe> نیز بررسی می‌کنیم. نمایش PDF در اینجا بر اساس امکانات توکار مرورگرها صورت می‌گیرد و نیاز به افزونه‌ی اضافه‌تری ندارد.


کدهای سمت سرور دریافت فایل PDF

در اینجا کدهای سمت سرور برنامه، نکته‌ی خاصی را به همراه نداشته و صرفا یک فایل PDF ساده (محتوای باینری) را بازگشت می‌دهد:
using Microsoft.AspNetCore.Mvc;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class ReportsController : Controller
    {
        [HttpGet("[action]")]
        public IActionResult GetPdfReport()
        {
            return File(virtualPath: "~/assets/sample.pdf",
                        contentType: "application/pdf",
                        fileDownloadName: "sample.pdf");
        }
    }
}
که در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.


سرویس دریافت محتوای باینری در برنامه‌های Angular

برای اینکه HttpClient برنامه‌های Angular بتواند محتوای باینری را بجای محتوای JSON پیش‌فرض آن دریافت کند، نیاز است نوع خروجی سمت سرور آن‌را به blob تنظیم کرد:
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { HttpClient } from "@angular/common/http";

@Injectable()
export class DownloadPdfDataService {

  constructor(private httpClient: HttpClient) { }

  public getReport(): Observable<Blob> {
    return this.httpClient.get("/api/Reports/GetPdfReport", { responseType: "blob" });
  }
}
به این ترتیب پس از اشتراک به متد getReport این سرویس، اطلاعات باینری این فایل PDF را دریافت خواهیم کرد.


اصلاح Content Security Policy سمت سرور جهت ارائه‌ی محتوای blob

پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL می‌توان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرس‌هایی به صورت blob:http تولید می‌شوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob:
default-src 'self' blob:
object-src 'self' blob:
در غیراینصورت مرورگر نمایش یک چنین تصاویر و یا اشیایی را سد خواهد کرد.


دریافت فایل‌های PDF از سرور و نمایش آن‌ها در یک برنامه‌ی Angular

پس از این مقدمات، کامپوننتی که یک فایل PDF را از سمت سرور دریافت کرده و نمایش می‌دهد، چنین کدی را خواهد داشت:
import { DownloadPdfDataService } from "./../download-pdf-data.service";
import { WindowRefService } from "./../../core/window.service";
import { Component, OnInit } from "@angular/core";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";

@Component({
  templateUrl: "./view-pdf.component.html",
  styleUrls: ["./view-pdf.component.css"]
})
export class ViewPdfComponent implements OnInit {
  private nativeWindow: Window;
  private pdfBlobUrl: string;
  sanitizedPdfBlobResourceUrl: SafeResourceUrl;

  constructor(private downloadService: DownloadPdfDataService,
    private windowRefService: WindowRefService, private sanitizer: DomSanitizer) { }

  ngOnInit() {
    this.nativeWindow = this.windowRefService.nativeWindow;
    this.downloadService.getReport().subscribe(pdfDataBlob => {
      console.log("pdfDataBlob", pdfDataBlob);
      const urlCreator = this.nativeWindow.URL;
      this.pdfBlobUrl = urlCreator.createObjectURL(pdfDataBlob);
      console.log("pdfBlobUrl", this.pdfBlobUrl);
      this.sanitizedPdfBlobResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.pdfBlobUrl);
    });
  }
}
با این قالب:
<h1>Display PDF Files</h1>

<div *ngIf="sanitizedPdfBlobResourceUrl">
  <h4>using iframe</h4>
  <iframe width="100%" height="600" [attr.src]="sanitizedPdfBlobResourceUrl" type="application/pdf"></iframe>
  <h4>using object</h4>
  <object [attr.data]="sanitizedPdfBlobResourceUrl" type="application/pdf" width="100%"
    height="100%"></object>
  <h4>usin embed</h4>
  <embed [attr.src]="sanitizedPdfBlobResourceUrl" type="application/pdf" width="100%"
    height="100%">
</div>
- در اینجا در ngOnInit، به سرویس پنجره دسترسی یافته و وهله‌ای از آن‌را جهت کار با متد createObjectURL شیء URL آن دریافت می‌کنیم.
- سپس مشترک متد getReport دریافت فایل PDF شده و اطلاعات نهایی آن‌را به صورت pdfDataBlob دریافت می‌کنیم.
- این اطلاعات باینری را به متد createObjectURL ارسال کرده و آدرس موقتی این تصویر را در مرورگر بدست می‌آوریم.
- چون در این حالت Angular این URL را امن سازی می‌کند، یک چنین خروجی unsafe:blob بجای blob تولید خواهد شد که نمایش این مورد نیز توسط مرورگر سد می‌شود. برای رفع این مشکل، می‌توان از سرویس DomSanitizer آن که به سازنده‌ی کلاس تزریق شده‌است استفاده کرد:
this.sanitizedPdfBlobResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.pdfBlobUrl);
تفاوت این مورد با حالت نمایش تصویر، استفاده از متد bypassSecurityTrustResourceUrl بجای متد bypassSecurityTrustUrl است. از این جهت که اشیاء یاد شده نیاز به SafeResourceUrl دارند و نه SafeUrl.
اینبار یک چنین انتسابی به صورت مستقیم کار می‌کند که سه نمونه‌ی این انتساب را به اشیاء iframe ،object و embed، در قالب فوق مشاهده می‌کنید.



افزودن دکمه‌ی چاپ PDF به برنامه

پس از اینکه به this.pdfBlobUrl دسترسی یافتیم، اکنون می‌توان یک iframe مخفی را ایجاد کرد، سپس src آن‌را به این آدرس ویژه تنظیم نمود و در آخر متد print آن‌را فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر می‌شود:
  printPdf() {
    const iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = this.pdfBlobUrl;
    document.body.appendChild(iframe);
    iframe.contentWindow.print();
  }


نمایش فایل PDF در یک برگه‌ی جدید

اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگه‌ای جدید نمایش دهید، می‌توان از متد window.open استفاده کرد:
  showPdf() {
    this.nativeWindow.open(this.pdfBlobUrl);
  }


دریافت فایل PDF

بجای نمایش فایل PDF می‌توان دکمه‌ای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
  downloadPdf() {
    const fileName = "test.pdf";
    const anchor = document.createElement("a");
    anchor.style.display = "none";
    anchor.href = this.pdfBlobUrl;
    anchor.download = fileName;
    document.body.appendChild(anchor);
    anchor.click();
  }
در اینجا یک anchor جدید به صورت مخفی به صفحه اضافه می‌شود که href آن به this.pdfBlobUrl تنظیم شده‌است. سپس متد click آن فراخوانی خواهد شد. نام این فایل را هم توسط ویژگی download این شیء می‌توان تنظیم نمود.
این روش در مورد تدارک دکمه‌ی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایل‌های PDF نیست.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
مسیریابی در Angular - قسمت دوم - مسیریابی ماژول‌ها
اغلب برنامه‌های بزرگ Angular، ویژگی‌های مختلف خود را به ماژول‌های مجزایی تبدیل می‌کنند. این ماژول‌ها شبیه به مفهوم Area در ASP.NET MVC هستند و هدف آن‌ها نظم بخشیدن به کامپوننت‌های ویژه‌ی یک قسمت خاص از برنامه، در ناحیه‌‌ای مختص به آن می‌باشد. به علاوه ایجاد ماژول‌ها، قابلیت lazy loading مسیریابی‌ها را نیز مسیر می‌کند. هر برنامه‌ی Angular حداقل به همراه یک ماژول است که بر اساس قراردادی، AppModule نام گرفته‌است و در فایل src\app\app.module.ts قرار دارد. با توسعه‌ی برنامه و بیشتر شدن قابلیت‌های آن، استفاده‌ی از این تک ماژول پیش فرض، مشکل تداخل مسئولیت‌ها را به همراه خواهد داشت. برای رفع این مشکل توصیه شده‌است که کامپوننت‌های مرتبط به یک ویژگی خاص را درون ماژولی مختص به خود قرار دهید؛ برای مثال مانند ماژول محصولات، برای نظم دهی به کامپوننت‌های لیست محصولات، جزئیات محصولات، ویرایش محصولات و غیره، ماژول کاربران برای تعریف کامپوننت‌های لاگین، تغییر کلمه‌ی عبور و امثال آن. در این قسمت قصد داریم نحوه‌ی تنظیمات مسیریابی و هدایت کاربران را به ماژول‌های مختلف برنامه، بررسی کنیم.


تنظیم مسیریابی ماژول‌ها

در اینجا نیازی به تنظیم base path نیست و این تنظیم تنها یکبار به ازای کل برنامه انجام می‌شود. همانطور که در قسمت قبل نیز عنوان شد، ماژول مسیریابی Angular و یا همان RouterModule، به همراه سرویسی برای دسترسی به امکانات آن، تنظیمات مسیریابی و یک سری دایرکتیو مانند routerLink، جهت تعامل با آن است. از آنجائیکه سرویس ماژول مسیریابی در فایل src\app\app-routing.module.ts تعریف و تنظیم شده‌است، باید اطمینان حاصل کرد که این سرویس تنها یکبار در طول عمر برنامه وهله سازی می‌شود و از آنجائیکه هر ماژول تنظیمات مجزای مسیریابی خود را خواهد داشت، دیگر نمی‌توان از متد RouterModule.forRoot سراسری استفاده کرد و در اینجا باید از متد forChild این ماژول، جهت تعریف تنظیمات مسیریابی‌های ماژول‌های مختلف کمک گرفت. متد forChild نیز شبیه به همان آرایه‌ی تنظیمات مسیریابی متد forRoot را دریافت می‌کند.

یک مثال: در ادامه‌ی مثالی که در قسمت قبل به کمک Angular CLI ایجاد کردیم، ماژول جدید محصولات را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
 >ng g m product --routing
پس از اجرای این دستور، ماژول جدید محصولات در فایل src\app\product\product.module.ts و تنظیمات ابتدایی آن در فایل src\app\product\product-routing.module.ts تشکیل می‌شوند:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ProductRoutingModule { }
همانطور که مشاهده می‌کنید، در حین تشکیل تنظیمات ابتدایی مسیریابی این ماژول جدید، اینبار از متد forChild استفاده شده‌است و نه متد forRoot که مختص به ماژول اصلی و سراسری برنامه‌است.
سپس ProductRoutingModule به قسمت imports ماژول محصولات به صورت خودکار اضافه شده‌است:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ProductRoutingModule } from './product-routing.module';

@NgModule({
  imports: [
    CommonModule,
    ProductRoutingModule
  ],
  declarations: []
})
export class ProductModule { }

در ادامه کامپوننت جدید لیست محصولات را به این ماژول اضافه می‌کنیم:
 >ng g c product/ProductList
به این ترتیب داخل پوشه‌ای به نام produc-list، کامپوننت product-list.component.ts تشکیل خواهد شد. در حقیقت این دستور اعمال ذیل را انجام می‌دهد:
 installing component
  create src\app\product\product-list\product-list.component.css
  create src\app\product\product-list\product-list.component.html
  create src\app\product\product-list\product-list.component.spec.ts
  create src\app\product\product-list\product-list.component.ts
  update src\app\product\product.module.ts
اگر به سطر آخر آن دقت کنید، کار به روز رسانی فایل ماژول محصولات، جهت درج این کامپوننت جدید، در قسمت declarations فایل product.module.ts نیز به صورت خودکار انجام شده‌است:
import { ProductListComponent } from './product-list/product-list.component';

@NgModule({
  imports: [
  ],
  declarations: [ProductListComponent]
})
export class ProductModule { }

اکنون که این ماژول جدید را به همراه یک کامپوننت نمونه در آن تعریف کردیم، برای افزودن مسیریابی به آن، به فایل src\app\product\product-routing.module.ts مراجعه کرده و آرایه‌ی  Routes آن‌را تکمیل می‌کنیم:
import { ProductListComponent } from './product-list/product-list.component';

const routes: Routes = [
  { path: 'products', component: ProductListComponent }
];
ابتدا کامپوننت لیست محصولات import شده و سپس آرایه‌ی Routes مسیری را به این کامپوننت تعریف کرده‌ایم.

در ادامه می‌خواهیم لینکی را به این مسیریابی جدید اضافه کنیم. در قسمت قبل منویی را به برنامه اضافه کردیم. به همین جهت به فایل src\app\app.component.html مراجعه کرده و routerLink جدیدی را به آن اضافه می‌کنیم:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <a class="navbar-brand">{{title}}</a>
    <ul class="nav navbar-nav">
      <li>
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li>
        <a [routerLink]="['/products']">Product List</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
پیشتر لینکی را به کامپوننت welcome در قسمت قبل اضافه کرده بودیم. در اینجا لینک جدیدی را به کامپوننت لیست محصولات، در ذیل آن تعریف کرده‌ایم.
در اینجا نیز نحوه‌ی تعریف لینک‌ها مانند قبل است و آرایه‌ی تنظیمات پارامترهای لینک باید به مقدار خاصیت path تعریف شده اشاره کند.

اکنون دستور ng serve -o را صادر کنید تا برنامه در حافظه ساخته شده و در مرورگر نمایش داده شود. در ادامه اگر بر روی لینک لیست محصولات کلیک کنید، صفحه‌ی ذیل را مشاهده خواهید کرد:


به این معنا که برنامه اطلاعی از این مسیریابی جدید نداشته و صفحه‌ی یافت نشدن مسیریابی را که در قسمت قبل تنظیم کردیم، نمایش داده‌است. برای رفع این مشکل باید به فایل src\app\app.module.ts مراجعه کرده و این ماژول جدید را به آن معرفی کنیم:
import { ProductModule } from './product/product.module';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,

    ProductModule,

    AppRoutingModule
  ],
در اینجا در ابتدا ماژول محصولات import شده و سپس به قسمت لیست imports ماژول App اضافه گردیده‌است. اکنون مسیریابی به این کامپوننت جدید واقع شده‌ی در قسمت ماژول محصولات، کار می‌کند:


نکته 1: علت اینکه ProductModule را پیش از AppRoutingModule تعریف کردیم این است که AppRoutingModule دارای تعریف مسیریابی ** یا catch all است که در قسمت قبل آن‌را جهت مدیریت مسیرهای یافت نشده به برنامه افزودیم. اگر ابتدا AppRoutingModule تعریف می‌شد و سپس ProductModule، هیچگاه فرصت به پردازش مسیریابی‌های ماژول محصولات نمی‌رسید؛ چون مسیر ** پیشتر برنده شده بود.
نکته 2: می‌توان در قسمت import متد RouterModule.forRoot را نیز مستقیما قرار داد (بجای AppRoutingModule). اگر این کار صورت گیرد، ابتدا مسیریابی‌های موجود در ماژول‌ها پردازش می‌شوند و در آخر مسیرهای موجود در RouterModule.forRoot صرفنظر از محل قرارگیری آن در این لیست بررسی خواهد شد (حتی اگر در ابتدای لیست قرار گیرد). هرچند جهت مدیریت بهتر برنامه، این متد به AppRoutingModule منتقل شده‌است. بنابراین اکنون «نکته‌ی 1» برقرار است.


انتخاب استراتژی مناسب نامگذاری مسیرها

هنگام کار کردن با تعدادی ویژگی مرتبط به هم قرار گرفته‌ی داخل یک ماژول، بهتر است روش نامگذاری مناسبی را برای تنظیمات مسیریابی آن درنظر گرفت تا مسیرهای تعیین شده علاوه بر زیبایی، وضوح بیشتری را نیز پیدا کنند. به علاوه این نامگذاری مناسب، گروه بندی مسیریابی‌ها و lazy loading آن‌ها را نیز ساده‌تر می‌کند.
استراتژی ابتدایی که به ذهن می‌رسد، نامگذاری هر مسیر بر اساس عملکرد آن‌ها است مانند products برای نمایش لیست محصولات، product/:id برای نمایش جزئیات محصولی خاص که در اینجا id پارامتر مسیریابی است و productEdit/:id برای ویرایش جزئیات یک محصول مشخص. همانطور که مشاهده می‌کنید، هرچند این مسیرها متعلق به یک ماژول هستند، اما مسیرهای تعیین شده‌ی برای آن‌ها اینگونه به نظر نمی‌رسد. بنابراین بهتر است تمام ویژگی‌های قرار گرفته‌ی درون یک ماژول را با مسیر ریشه‌ی یکسانی شروع کنیم. به این ترتیب نمایش لیست محصولات همان products باقی خواهد ماند اما برای نمایش جزئیات محصولی خاص از مسیر products/:id استفاده می‌کنیم (همان اسم جمع ریشه‌ی مسیر؛ بجای اسم مفرد). اینبار مسیر ویرایش جزئیات یک محصول به صورت products/:id/edit تنظیم خواهد شد:
products
products/:id
products/:id/edit
در اینجا نام ریشه‌ی یکسانی را برای عناصر مختلف قرارگرفته‌ی درون یک ماژول انتخاب کرده‌ایم؛ تا ارتباط بین آن‌ها بهتر مشخص شود و همچنین در آینده بتوان مباحث گروه بندی و lazy loading را نیز بر این اساس پیاده سازی کرد.


فعالسازی یک مسیر با کدنویسی

تا اینجا نحوه‌ی فعالسازی یک مسیر را با استفاده از دایرکتیو routerLink بررسی کردیم. اما گاهی از اوقات نیاز است تا بتوان با کدنویسی نیز کاربران را به مسیری خاص هدایت کرد. برای مثال پس از عملیات logout می‌خواهیم مجددا صفحه‌ی اول سایت نمایش داده شود. برای اینکار از سرویس Router مسیریاب Angular کمک گرفته می‌شود. ابتدا آن‌را در سازنده‌ی یک کامپوننت تزریق کرده و سپس می‌توان به قابلیت‌های آن مانند استفاده‌ی از متد navigate آن، در کدهای برنامه دسترسی یافت.
باید درنظر داشت که دایرکتیو routerLink نیز در پشت صحنه از همین متد navigate سرویس Router استفاده می‌کند. بنابراین تمام پارامترهای آن در متد navigate نیز قابل استفاده هستند. برای مثال زمانیکه تعداد پارامترهای routerLink یک مورد است، می‌توان آرایه‌ی آن‌را به یک رشته خلاصه کرد. یک چنین قابلیتی با متد navigate نیز میسر است.
متد navigate تنها قسمت‌هایی از URL جاری را تغییر می‌دهد. اگر نیاز باشد تا کل آدرس تعویض شود، می‌توان از متد دیگر سرویس Router به نام navigateByUrl استفاده کرد. این متد تمام URL segments موجود را با مسیر جدیدی جایگزین می‌کند. به علاوه برخلاف متد navigate، تنها یک رشته را به عنوان پارامتر می‌پذیرد.

در ادامه مثال جاری می‌خواهیم پیاده سازی ابتدایی login و logout را به برنامه اضافه کنیم. به همین منظور ابتدا ماژول جدید user را به همراه تنظیمات ابتدایی مسیریابی آن اضافه می‌کنیم:
 >ng g m user --routing
به این ترتیب دو فایل src\app\user\user-routing.module.ts و src\app\user\user.module.ts به برنامه اضافه می‌شوند.
همانند ماژول قبلی، نیاز است UserModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم:
import { UserModule } from './user/user.module';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,

    ProductModule,
    UserModule,

    AppRoutingModule
  ],

سپس کامپوننت جدید لاگین را به ماژول user برنامه اضافه می‌کنیم:
 >ng g c user/login
که اینکار سبب به روز رسانی فایل user.module.ts جهت تکمیل قسمت declarations آن با LoginComponent نیز می‌شود.

در ادامه به فایل src\app\user\user-routing.module.ts مراجعه کرده و مسیریابی جدیدی را به کامپوننت لاگین تعریف می‌کنیم:
import { LoginComponent } from './login/login.component';

const routes: Routes = [
  { path: 'login', component: LoginComponent}
];
ابتدا این کامپوننت import شده و سپس یک path جدید را به آن انتساب می‌دهیم.

مرحله‌ی بعد، فعالسازی این مسیریابی است، با تعریف لینکی به آن. به همین جهت به فایل src\app\app.component.html مراجعه کرده و منوی برنامه را تکمیل می‌کنیم:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <a class="navbar-brand">{{title}}</a>
    <ul class="nav navbar-nav">
      <li>
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li>
        <a [routerLink]="['/products']">Product List</a>
      </li>
    </ul>
    <ul class="nav navbar-nav navbar-right">
      <li>
        <a [routerLink]="['/login']">Log In</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
اکنون دستور ng serve -o را صادر کنید تا برنامه در حافظه ساخته شده و در مرورگر نمایش داده شود و سپس بر روی لینک login کلیک کنید تا قالب ابتدایی آن نمایش داده شود:



تکمیل کامپوننت login و افزودن لینک logout

در ادامه می‌خواهیم یک فرم لاگین مقدماتی را پس از کلیک بر روی لینک لاگین نمایش دهیم و هدایت به صفحه‌ی لیست محصولات را پس از لاگین و مخفی کردن لینک لاگین و نمایش لینک خروج را در این حالت پیاده سازی کنیم. برای این منظور ابتدا اینترفیس خالی کاربر را ایجاد می‌کنیم:
 >ng g i user/user
که سبب ایجاد فایل src\app\user\user.ts خواهد شد. این اینترفیس را به صورت زیر تکمیل می‌کنیم:
export interface IUser {
    id: number;
    userName: string;
    isAdmin: boolean;
}

پس از آن یک سرویس ابتدایی اعتبارسنجی کاربران را نیز اضافه خواهیم کرد:
 >ng g s user/auth -m user/user.module
که سبب افزوده شدن سرویس auth.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول user.module.ts نیز می‌شود:
 installing service
  create src\app\user\auth.service.spec.ts
  create src\app\user\auth.service.ts
  update src\app\user\user.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمی‌شود.

پس از ایجاد قالب ابتدایی فایل auth.service.ts آن‌را به نحو ذیل تکمیل کنید:
import { IUser } from './user';
import { Injectable } from '@angular/core';

@Injectable()
export class AuthService {
  currentUser: IUser;

  constructor() { }

  isLoggedIn(): boolean {
    return !this.currentUser;
  }

  login(userName: string, password: string): boolean {
    if (!userName || !password) {
      return false;
    }

    if (userName === 'admin') {
      this.currentUser = {
        id: 1,
        userName: userName,
        isAdmin: true
      };
      return true;
    }

    this.currentUser = {
      id: 2,
      userName: userName,
      isAdmin: false
    };
    return true;
  }

  logout(): void {
    this.currentUser = null;
  }
}
در اینجا اگر کاربر هر نوع کلمه‌ی عبور و یا نام کاربری را وارد کند، به سیستم وارد خواهد شد. اگر نامش admin باشد، دسترسی admin پیدا می‌کند و همچنین متدهای logout با null کردن یوزر وارد شده‌ی به سیستم و isLoggedIn برای مشخص بودن نال نبودن شیء کاربر جاری، به این سرویس اضافه شده‌اند.

سپس کامپوننت لاگین واقع در فایل src\app\user\login\login.component.ts را به نحو ذیل تکمیل کنید:
import { Router } from '@angular/router';
import { AuthService } from './../auth.service';

import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  errorMessage: string;
  pageTitle = 'Log In';

  constructor(private authService: AuthService,
    private router: Router) { }

  ngOnInit() {
  }

  login(loginForm: NgForm) {
    if (loginForm && loginForm.valid) {
      let userName = loginForm.form.value.userName;
      let password = loginForm.form.value.password;
      if (this.authService.login(userName, password)) {
        this.router.navigate(['/products']);
      }
    } else {
      this.errorMessage = 'Please enter a user name and password.';
    };
  }
}
در این کامپوننت دو سرویس AuthService، که پیشتر ایجاد کردیم و سرویس Router، به سازنده‌ی کلاس تزریق شده‌اند.
از AuthService برای اعتبارسنجی کاربر و لاگین او به سیستم استفاده می‌کنیم و از سرویس مسیریاب Angular جهت فراخوانی متد navigate آن به صفحه‌ی مشاهده‌ی محصولات، پس از لاگین کاربر استفاده شده‌است.

اکنون می‌خواهیم قالب این کامپوننت را نیز تکمیل کنیم. پیش از آن به فایل src\app\user\user.module.ts مراجعه کرده و در قسمت imports آن FormsModule را نیز اضافه کنید:
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    UserRoutingModule
  ],

سپس فایل src\app\user\login\login.component.html را به نحو ذیل تغییر دهید:
<div class="panel panel-default">
  <div class="panel-heading">
    {{pageTitle}}
  </div>

  <div class="panel-body">
    <form class="form-horizontal" novalidate (ngSubmit)="login(loginForm)" #loginForm="ngForm" autocomplete="off">
      <fieldset>
        <div class="form-group" [ngClass]="{'has-error': (userNameVar.touched || 
                                               userNameVar.dirty) && 
                                               !userNameVar.valid }">
          <label class="col-md-2 control-label" for="userNameId">User Name</label>
          <div class="col-md-8">
            <input class="form-control" id="userNameId" type="text" 
                   placeholder="User Name (required)" required 
                   (ngModel)="userName" name="userName" #userNameVar="ngModel" />
            <span class="help-block" *ngIf="(userNameVar.touched ||
                                             userNameVar.dirty) &&
                                             userNameVar.errors">
                            <span *ngIf="userNameVar.errors.required">
                                User name is required.
                            </span>
            </span>
          </div>
        </div>

        <div class="form-group" [ngClass]="{'has-error': (passwordVar.touched || 
                                               passwordVar.dirty) && 
                                               !passwordVar.valid }">
          <label class="col-md-2 control-label" for="passwordId">Password</label>

          <div class="col-md-8">
            <input class="form-control" id="passwordId" type="password" 
                   placeholder="Password (required)" required 
                  (ngModel)="password" name="password" #passwordVar="ngModel" />
            <span class="help-block" *ngIf="(passwordVar.touched ||
                                             passwordVar.dirty) &&
                                              passwordVar.errors">
                            <span *ngIf="passwordVar.errors.required">
                                Password is required.
                            </span>
            </span>
          </div>
        </div>

        <div class="form-group">
          <div class="col-md-4 col-md-offset-2">
            <span>
               <button class="btn btn-primary"
                       type="submit"
                       style="width:80px;margin-right:10px"
                       [disabled]="!loginForm.valid">
                               Log In
               </button>
            </span>
            <span>
                <a class="btn btn-default"
                   [routerLink]="['/welcome']">
                     Cancel
                </a>
            </span>
          </div>
        </div>
      </fieldset>
    </form>
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
  </div>
</div>
تا اینجا صفحه‌ی لاگین نمایش داده شده و کاربر می‌تواند به سیستم وارد شود. تا زمانیکه کلمه‌ی عبور و نام کاربری وارد نشده باشند، دکمه‌ی login این فرم غیرفعال باقی می‌ماند. پس از آن کاربر با هر نوع ترکیبی از کلمه‌ی عبور و نام کاربری می‌تواند به سیستم وارد شده و بلافاصله به صفحه‌ی لیست محصولات هدایت می‌شود.

اکنون می‌خواهیم پس از ورود او، نام او را نمایش داده و همچنین دکمه‌ی logout را بجای login در منوی بالای سایت نمایش دهیم. به همین جهت در قالب کامپوننت App که منوی برنامه در آن تنظیم شده‌است، نیاز است بتوانیم به سرویس Auth سفارشی دسترسی یافته و خروجی متد isLoggedIn آن‌را بررسی کنیم. به همین منظور به فایل src\app\app.component.ts مراجعه کرده و آن‌را به صورت ذیل تکمیل کنید:
import { Router } from '@angular/router';
import { AuthService } from './user/auth.service';
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  pageTitle: string = 'Routing Lab';

  constructor(private authService: AuthService,
    private router: Router) { }

  logOut(): void {
    this.authService.logout();
    this.router.navigateByUrl('/welcome');
  }
}
در اینجا دو سرویس Auth و Router به سازنده‌ی کامپوننت App تزریق شده‌اند. به این ترتیب می‌توان از شیء authService در قالب این کامپوننت برای دسترسی به متد isLoggedIn و سایر خواص این سرویس استفاده کرد. همچنین از سرویس مسیریاب Angular برای پیاده سازی logout و هدایت کاربر به صفحه‌ی welcome کمک گرفته‌ایم. در اینجا از متد navigateByUrl استفاده شده‌است؛ از این جهت که پس از Logout دیگر حفظ URL Segments موجود بی‌مفهوم است و تمام آن‌ها باید پاک شده و با آدرس جدید جایگزین شوند.
پس از این تغییرات، اکنون می‌توان قالب src\app\app.component.html را به نحو ذیل تکمیل کرد:
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <a class="navbar-brand">{{title}}</a>
    <ul class="nav navbar-nav">
      <li>
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li>
        <a [routerLink]="['/products']">Product List</a>
      </li>
    </ul>
    <ul class="nav navbar-nav navbar-right">
      <li *ngIf="authService.isLoggedIn()">
        <a>Welcome {{ authService.currentUser.userName }}</a>
      </li>
      <li *ngIf="!authService.isLoggedIn()">
        <a [routerLink]="['/login']">Log In</a>
      </li>
      <li *ngIf="authService.isLoggedIn()">
        <a (click)="logOut()">Log Out</a>
      </li>
    </ul>
  </div>
</nav>
<div class="container">
  <router-outlet></router-outlet>
</div>
همانطور که مشاهده می‌کنید، دوبار لاگین بودن کاربر جاری توسط متد authService.isLoggedIn بررسی شده‌است. اگر خروجی این متد true باشد، نام کاربری شخص به همراه لینک Logout نمایش داده می‌شود. اگر خروجی این متد false باشد، تنها لینک login نمایان شده و مابقی گزینه‌ها (لینک لاگین و نمایش نام شخص) از صفحه حذف می‌شوند.

اکنون اگر برنامه را توسط دستور ng serve -o اجرا کنید، صفحه‌ی لاگین و منوی بالای صفحه چنین شکلی را خواهد داشت:


پس از لاگین، لینک لاگین از منو حذف شده و سپس نام کاربری و لینک به logout نمایان می‌شوند.


اینبار اگر بر روی logout کلیک کنید، نام کاربری و لینک logout از صفحه حذف و مجددا لینک لاگین نمایش داده می‌شود.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-01.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
ساخت منوهای چند سطحی در ASP.NET MVC
پیش نیاز مطلب جاری مطالب زیر می‌باشند:
1- EF Code First #8
2- مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code First
3- نگاهی به اجزای تعاملی Twitter Bootstrap 

هدف از مطلب جاری نحوه نمایش منوی‌های چند سطحی می‌باشد، ابتدا مثال کامل زیر را در نظر بگیرید :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Menu.Models.Entities
{
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int? ParentId { get; set; }
        public virtual Category Parent { get; set; }
        public virtual ICollection<Category> Children { get; set; }
    }
}

public class MyContext : DbContext
{
        public DbSet<Category> Category { get; set; }
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Self Referencing Entity
            modelBuilder.Entity<Category>()
                        .HasOptional(x => x.Parent)
                        .WithMany(x => x.Children)
                        .HasForeignKey(x => x.ParentId)
                        .WillCascadeOnDelete(false);
 
            base.OnModelCreating(modelBuilder);
        }
}

همانطور که ملاحظه می‌کنید، مدل ما شامل مشخصات گروه محصولات می‌باشد که به صورت خود ارجاع دهنده (خاصیت Parent به همین کلاس اشاره میکند) تعریف شده است. در مورد خواص مدل‌های خود ارجاع دهنده، مطالبی را در سایت مطالعه کردید (خواص مربوط در مطالب گفته شده دقیقاً به همان صورت می‌باشد و نیازی به توضیح اضافه‌تری نیست).
هدف از این بحث، نحوه نمایش گروه محصولات داخل منو به صورت چند سطحی می‌باشد، جهت نمایش می‌بایست از تکنیک recursive function استفاده کنید، ابتدا در نظر داشته باشید که ساختار منوی تشکیل شده می‌بایست بدین صورت باشد :
 

این حالت می‌تواند تا n سطح پیش برود، حال نحوه نمایش در View مربوطه باید به صورت زیر باشد :

@using Menu.Helper
@model IEnumerable<.Models.Entities.Category>
@ShowTree(Model)
 
@helper ShowTree(IEnumerable<Menu.Models.Entities.Category> categories)
{
    foreach (var item in categories)
    {
    <li class="@(item.Children.Any() ? "dropdown-submenu" : "")">
 
        @Html.ActionLink(item.Name, actionName: "Category", controllerName: "Product", routeValues: new { Id = item.Id, productName = item.Name.ToSeoUrl() }, htmlAttributes: null)
        @if (item.Children.Any())
        {
            <ul>
                @ShowTree(item.Children)
            </ul>
                }
    </li>
 
        }
}
توجه داشته باشید که رندر نهایی توسط Bootstrap انجام شده است. ساختار منو همانطور که ملاحظه می‌کنید با استفاده از کلاس‌های drop-down که از کلاس‌های پیش فرض بوت استرپ می‌باشد تشکیل شده است همچنین کلاس dropdown-submenu که از نسخه 2 به بعد بوت استرپ موجود می‌باشد، استفاده شده است.

یک نکته :
در خط 9 این مورد را که آیا آیتم جاری فرزندی دارد چک کرده ایم اگر داشته باشد کلاس dropdown-submenu  را به li جاری اضافه میکند.
اشتراک‌ها
نکته‌هایی درباره‌ی Redirect یا تغییر مسیر صفحات وب

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

تغییر مسیر یا Redirect از روش‌های مختلفی امکان‌پذیر است. در این نوشته به تغییر مسیر از طریق سرآمد پاسخ( header ) می‌پردازیم و از روش‌های موجود در HTML و JavaScript چشم‌پوشی می‌کنیم. 
نکته‌هایی درباره‌ی Redirect یا تغییر مسیر صفحات وب
مطالب
آشنایی با مفاهیم شیء گرایی در جاوا اسکریپت #2
از آنجا که برای کار با جاوا اسکریپت نیاز به درک کاملی درباره‌ی مفهوم حوزه کارکرد متغیرها (Scope) می‌باشد و نحوه فراخوانی توابع نیز نقش اساسی در این مورد بازی می‌کند، در این قسمت با این موارد آشنا خواهیم شد:
جاوا اسکریپت از مفهومی به نام functional scope برای تعیین حوزه متغیرها استفاده می‌کند و به این معنی است که با تعریف توابع، حوزه عملکرد متغیر مشخص می‌شود. در واقع هر متغیری که در یک تابع تعریف می‌شود در کلیه قسمتهای آن تابع، از قبیل If statement – for loops و حتی nested function نیز در دسترس میباشد.
اجازه دهید با مثالی این موضوع را بررسی نماییم.
function testScope() {
var myTest = true;
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
testScope(); // will alert "I am changed!"
همانگونه که میبینیم با اینکه در داخل بلاک if یک متغیر جدید تعریف شده، ولی در خارج از این بلاک نیز این متغیر قابل دسترسی میباشد. البته در مثال بالا اگر بخواهیم به متغیر myTest در خارج از function دسترسی داشته باشیم، با خطای undefined مواجه خواهیم شد. یعنی برای مثال در کد زیر:
function testScope() {
var myTest = true;
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
testScope(); // will alert "I am changed!"
alert(myTest); // will throw a reference error, because it doesn't exist outside of the function
 برای حل این مشکل دو راه وجود دارد: 
1 – متغیر myTest را در بیرون بلاک testScope() تعریف کنیم
2 – هنگام تعریف متغیر myTest، کلمه کلیدی var را حذف کنیم که این موضوع باعث میشود این متغیر در کل window قابل دسترس باشد و یا به عبارتی متغیر global میشود.
قبل از پرداختن به ادامه بحث خواندن مقاله مربوط به Closure در جاوااسکریپت توصیه میگردد .
در پایان بحث Scope‌ها با یک مثال نسبتا جامع اکثر این حالات به همراه خروجی را نشان میدهیم :
<script type="text/javascript">
          // a globally-scoped variable
        var a = 1;
        // global scope
        function one()
        {
            alert(a);
        }
        // local scope
        function two(a)
        {
            alert(a);
        }
        // local scope again
        function three()
        {
            var a = 3;
            alert(a);
        }
        // Intermediate: no such thing as block scope in javascript
        function four()
        {
            if (true)
            {
                var a = 4;
            }
            alert(a); // alerts '4', not the global value of '1'
        }
        // Intermediate: object properties
        function Five()
        {
            this.a = 5;
        }
        // Advanced: closure
        var six = function ()
        {
            var foo = 6;
            return function ()
            {
                // javascript "closure" means I have access to foo in here, 
                // because it is defined in the function in which I was defined.
                alert(foo);
            }
        }()
        // Advanced: prototype-based scope resolution
        function Seven()
        {
            this.a = 7;
        }
        // [object].prototype.property loses to [object].property in the lookup chain
        Seven.prototype.a = -1; // won't get reached, because 'a' is set in the constructor above.
        Seven.prototype.b = 8; // Will get reached, even though 'b' is NOT set in the constructor.
        // These will print 1-8
        one();
        two(2);
        three();
        four();
        alert(new Five().a);
        six();
        alert(new Seven().a);
        alert(new Seven().b);
</Script>
برای مطالعه بیشتر به اینجا  مراجعه نمایید.

Function Invocation Patterns In JavaScript :
از آنجا که توابع در جاوااسکریپت به منظور 1 – ساخت اشیاء  و 2 – حوزه دسترسی متغیرها(Scope)  نقش اساسی ایفا می‌کنند بهتر است کمی درباره استفاده و نحوه فراخوانی آنها  (Function Invocation Patterns) در جاوااسکریپت بحث نماییم.
در جاوااسکریپت 4 مدل فراخوانی تابع داریم که به نامهای زیر مطرح هستند:
1. Method Invocation
2. Function Invocation
3. Constructor Invocation
4. Apply And Call Invocation
 در فراخوانی توابع به هر یک از روشهای بالا باید به این نکته توجه داشت که حوزه دسترسی متغیرها در جاوااسکریپت ابتدا و انتهای توابع هستند و اگر به عنوان مثال از توابع تو در تو استفاده کردیم ،حوزه شی this برای توابع داخلی تغییر خواهد کرد .این موضوع را در طی مثالهایی نشان خواهیم داد.
Method Invocation :
وقتی یک تابع قسمتی از یک شی باشد به آن متد میگوییم به عنوان مثال :
var obj = {
    value: 0,
    increment: function() {
        this.value+=1;
    }
};
obj.increment(); //Method invocation
در اینحالت this به شی (Object) اشاره میکند که متد در آن فراخوانی شده است و در زمان اجرا نیز به عناصر شی Bind میشود ،در مثال بالا حوزه  this شی obj خواهد شد و به همین منظور به متغیر value دسترسی داریم.
Function Invocation:
در اینحالت که از () برای فراخوانی تابع استفاده میگردد ،This به شی سراسری (global object ) اشاره می‌کند؛ منظور اینکه this به اجزای تابعی که فراخوانی آن انجام شده اشاره نمی‌کند. اجازه دهید با مثالی این موضوع را روشن کنیم
<script type="text/javascript">
var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        this.value++;
        var innerFunction = function() {
            alert(this.value);
        }
        innerFunction(); //Function invocation pattern
    }
}
obj.increment(); //Method invocation pattern
<script type="text/javascript">
Result : 500
از آنجا که  () innerFunction به شکل  Function invocation pattern فراخوانی شده است به متغیر value در داخل تابع increment دسترسی نداریم و حوزه دسترسی global میشود و اگر در حوزه global نیز این متغیر تعریف نشده بود به خطای undefined میرسیدیم .
برای حل این گونه مشکلات ساختار کد نویسی ما بایستی به شکل زیر باشد :
<script type="text/javascript">
var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        var that = this;
        that.value++;
        var innerFunction = function() {
            alert(that.value);
        }
        innerFunction(); //Function invocation pattern
    }
}
obj.increment();
<script type="text/javascript">
Result : 1
در واقع با تعریف یک متغیر با نام مثلا that و انتساب شی  this به آن میتوان در توابع بعدی که به شکل   Function invocation pattern فراخوانی میگردند به این متغیر دسترسی داشت .
Constructor Invocation :
در این روش برای فراخوانی تابع از کلمه new استفاده میکنیم. در این حالت یک شیء مجزا ایجاد شده و به متغیر دلخواه ما اختصاص پیدا می‌کند. به عنوان مثال داریم :
 var Dog = function(name) {   
  //this == brand new object ({});    
    this.name = name;    
    this.age = (Math.random() * 5) + 1;
};
var myDog = new Dog('Spike');
//myDog.name == 'Spike'
//myDog.age == 2
var yourDog = new Dog('Spot');
//yourDog.name == 'Spot'
//yourDog.age == 4
در این مورد با استفاده از New باعث میشویم همه خواص و متدهای تابع function برای هر نمونه از آن که ساخته میشود ( از طریق مفهوم Prototype که قبلا درباره آن بحث شد) بطور مجزا اختصاص یابد. در مثال بالا شی mydog چون حاوی یک نمونه از تابع dog بصورت  Constructor Invocation میباشد، در نتیجه به خواص تابع dog از قبیل name  و age دسترسی داریم. در اینجا اگر کلمه new استفاده نشود به این خواص دسترسی نداریم؛ در واقع با اینکار، this به mydog اختصاص پیدا میکند.
اگر از new استفاده نشود متغیر myDog ،undefined میشود.
یک مثال دیگر :
var createCallBack = function(init) { //First function
    return new function() { //Second function by Constructor Invocation
        var that = this;
        this.message = init;
        return function() { //Third function
            alert(that.message);
        }
    }
}
window.addEventListener('load', createCallBack("First Message"));
window.addEventListener('load', createCallBack("Second Message"));
در مثال بالا از مفهوم closure  نیز در مثالمان استفاده کرده ایم .
Apply And Call Invocation:
تمامی توابع جاوااسکریپت دارای دو متد توکار apply() و call() هستند که توسط این متدها میتوان این توابع را با context دلخواه فراخوانی کرد.
نحوه فراخوانی به شکل مقابل است :
myFunction.apply(thisContext, arrArgs);
myFunction.call(thisContext, arg1, arg2, arg3, ..., argN);
که thisContext به حوزه اجرایی (execution context) تابع اشاره میکند. تفاوت دو متد apply() و call() در نحوه فرستادن آرگومانها به تابع میباشد که در اولی توسط آرایه اینکار انجام میشود و در دومی همه آرگومانها را بطور صریح نوشته و با کاما از هم جدا میکنیم .
مثال :
var contextObject = {
testContext: 10
}
var otherContextObject = {
testContext: "Hello World!"
}
var testContext = 15; // Global variable
function testFunction() {
alert(this.testContext);
}
testFunction(); // This will alert 15
testFunction.call(contextObject); // Will alert 10
testFunction.apply(otherContextObject); // Will alert "Hello World”
در این مثال دو شی متفاوت با خواص همنام تعریف کرده و یک متغیر global نیز تعریف میکنیم. در انتها یک تابع تعریف میکنیم که مقدار this.testContext را نمایش میدهد. در ابتدا حوزه اجرایی تابع (this) کل window جاری میباشد و وقتی testFunction() اجرا شود مقدار متغیر global نمایش داده میشود. در اجرای دوم this به contextObject اشاره کرده و حوزه اجرایی عوض میشود و در نتیجه مقدار testContext مربوطه که در این حالت 10 میباشد نمایش داده میشود و برای فراخوانی سوم نیز به همین شکل .
یک مثال کاملتر :
var o = {
  i : 0,
  F : function() {
    var a = function() { this.i = 42; };
    a();
    document.write(this.i);
  }
};
o.F();
Result :0
خط o.f() تابع f را به شکل Method invocation اجرا میکند. در داخل تابع f یک تابع دیگر به شکل function invocation اجرا میشود که در اینحال this به global object اشاره میکند و باعث میشود مقدار i در خروجی 0 چاپ شود .
برای حل این مشکل 2 راه وجود دارد  
راه اول :
var p = {
  i : 0,
  F : function() {
    var a = function() { this.i = 42; };
    a.apply(this);
    document.write(this.i);
  }
};
 p.F();
Result :42
با اینکار this را موقع اجرای تابع درونی برایش فرستاده تا حوزه اجرای تابع عوض شود و به i دسترسی پیدا کنیم .
یا اینکه همانند مثالهای قبلی :
var q = {
  i: 0,
  F: function F() {
    var that = this;
    var a = function () {
      that.i = 42;
    }
    a();
    document.write(this.i);
  }
}
 q.F();

منابع :
Javascript programmer,s refrence
 
مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی 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)



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.