مطالب دوره‌ها
شروع به کار با RavenDB
پیشنیاز‌های بحث
- مروری بر مفاهیم مقدماتی NoSQL
- رده‌ها و انواع مختلف بانک‌های اطلاعاتی NoSQL
- چه زمانی بهتر است از بانک‌های اطلاعاتی NoSQL استفاده کرد و چه زمانی خیر؟

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


RavenDB چیست؟

RavenDB یک بانک اطلاعاتی سورس باز NoSQL سندگرای تهیه شده با دات نت  است. ساختار کلی بانک‌های اطلاعاتی NoSQL سندگرا، از لحاظ نحوه ذخیره سازی اطلاعات، با بانک‌های اطلاعاتی رابطه‌ای متداول، کاملا متفاوت است. در اینگونه بانک‌های اطلاعاتی، رکوردهای اطلاعات، به صورت اشیاء JSON ذخیره می‌شوند. اشیاء JSON یا JavaScript Object Notation بسیار شبیه به anonymous objects سی شارپ هستند. JSON روشی است که توسط آن JavaScript اشیاء خود را معرفی و ذخیره می‌کند. به عنوان رقیبی برای XML مطرح است؛ نسبت به XML اندکی فشرده‌تر بوده و عموما دارای اسکیمای خاصی نیست و در بسیاری از اوقات تفسیر المان‌های آن به مصرف کننده واگذار می‌شود.
در JSON عموما سه نوع المان پایه مشاهده می‌شوند:
- اشیاء که به صورت {object} تعریف می‌شوند.
- مقادیر "key":"value" که شبیه به نام خواص و مقادیر آن‌ها در دات نت هستند.
- و آرایه‌ها به صورت [array]
همچنین ترکیبی از این سه عنصر یاد شده نیز همواره میسر است. برای مثال، یک key مشخص می‌تواند دارای مقداری حاوی یک آرایه یا شیء نیز باشد.
JSON: JavaScript Object Notation

document :{ 
   key: "Value",
   another_key: {
      name: "embedded object"
   },
   some_date: new Date(),
   some_number: 12
}

C# anonymous object

var Document = new { 
   Key= "Value",
   AnotherKey= new {
      Name = "embedded object"
   },
   SomeDate = DateTime.Now(),
   SomeNumber = 12
};
به این ترتیب می‌توان به یک ساختار دلخواه و بدون اسکیما، از هر سند به سند دیگری رسید. اغلب بانک‌های اطلاعاتی سندگرا، اینگونه اسناد را در زمان ذخیره سازی، به یک سری binary tree تبدیل می‌کنند تا تهیه کوئری بر روی آن‌ها بسیار سریع شود. مزیت دیگر استفاده از JSON، سادگی و سرعت بالای Serialize و Deserialize اطلاعات آن برای ارسال به کلاینت‌ها و یا دریافت آن‌ها است؛ به همراه فشرده‌تر بودن آن نسبت به فرمت‌های مشابه دیگر مانند XML.


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


مجوز استفاده از RavenDB

هرچند مجموعه سرور و کلاینت RavenDB سورس باز هستند، اما این مورد به معنای رایگان بودن آن نیست. مجوز استفاده از RavenDB نوع خاصی به نام AGPL است. به این معنا که یا کل کار مشتق شده خود را باید به صورت رایگان و سورس باز ارائه دهید و یا اینکه مجوز استفاده از آن‌را برای کارهای تجاری بسته خود خریداری نمائید. نسخه استاندارد آن نزدیک به هزار دلار است و نسخه سازمانی آن نزدیک به 2800 دلار به ازای هر سرور.


شروع به نوشتن اولین برنامه کار با RavenDB

ابتدا یک پروژه کنسول ساده را آغاز کنید. سپس کلاس‌های مدل زیر را به آن اضافه نمائید:
using System.Collections.Generic;

namespace RavenDBSample01.Models
{
    public class Question
    {
        public string By { set; get; }
        public string Title { set; get; }
        public string Content { set; get; }

        public List<Comment> Comments { set; get; }
        public List<Answer> Answers { set; get; }

        public Question()
        {
            Comments = new List<Comment>();
            Answers = new List<Answer>();
        }
    }
}

namespace RavenDBSample01.Models
{
    public class Comment
    {
        public string By { set; get; }
        public string Content { set; get; }
    }
}

namespace RavenDBSample01.Models
{
    public class Answer
    {
        public string By { set; get; }
        public string Content { set; get; }
    }
}
سپس به کنسول پاور شل نیوگت در ویژوال استودیو مراجعه کرده و دستورات ذیل را جهت افزوده شدن وابستگی‌های مورد نیاز RavenDB، صادر کنید:
PM> Install-Package RavenDB.Client
PM> Install-Package RavenDB.Server
به این ترتیب بسته‌های کلاینت (مورد نیاز جهت برنامه نویسی) و سرور RavenDB به پروژه جاری اضافه خواهند شد (نگارش 2.5 در زمان نگارش این مطلب؛ جمعا نزدیک به 75 مگابایت).
اکنون به پوشه packages\RavenDB.Server.2.5.2700\tools مراجعه کرده و برنامه Raven.Server.exe را اجرا کنید تا سرور RavenDB شروع به کار کند. این سرور به صورت پیش فرض بر روی پورت 8080 اجرا می‌شود. از این جهت که در RavenDB نیز همانند سایر Document Stores مطرح، امکان دسترسی به اسناد از طریق REST API و Urlها وجود دارد.
البته لازم به ذکر است که RavenDB در 4 حالت برنامه کنسول (همین سرور فوق)، نصب به عنوان یک سرویس ویندوز NT، هاست شدن در IIS و حالت مدفون شده یا Embedded قابل استفاده است.

خوب؛ همین اندازه برای برپایی اولیه RavenDB کفایت می‌کند.
using Raven.Client.Document;
using RavenDBSample01.Models;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
                               {
                                   Url = "http://localhost:8080"
                               }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    session.Store(new Question
                    {
                        By = "users/Vahid",
                        Title = "Raven Intro",
                        Content = "Test...."
                    });

                    session.SaveChanges();
                }
            }
        }
    }
}
اکنون کدهای برنامه کنسول را به نحو فوق برای ذخیره سازی اولین سند خود، تغییر دهید.
کار با ایجاد یک DocumentStore که به آدرس سرور اشاره می‌کند و کار مدیریت اتصالات را برعهده دارد، شروع خواهد شد. اگر نمی‌خواهید Url را درون کدهای برنامه مقدار دهی کنید، می‌توان از فایل کانفیگ برنامه نیز برای این منظور کمک گرفت:
<connectionStrings>
   <add name="ravenDB" connectionString="Url=http://localhost:8080"/>
</connectionStrings>
در این حالت باید خاصیت ConnectionStringName شیء DocumentStore را مقدار دهی نمود.
 سپس با ایجاد Session در حقیقت یک Unit of work آغاز می‌شود که درون آن می‌توان انواع و اقسام دستورات را صادر نمود و سپس در پایان کار، با فراخوانی SaveChanges، این اعمال ذخیره می‌گردند. در RavenDB یک سشن باید طول عمری کوتاه داشته باشد و اگر تعداد عملیاتی که در آن صادر کرده‌اید، زیاد است با خطای زیر متوقف خواهید شد:
 The maximum number of requests (30) allowed for this session has been reached.
البته این نوع محدودیت‌ها عمدی است تا برنامه نویس به طراحی بهتری برسد.

در یک برنامه واقعی، ایجاد DocumentStore یکبار در آغاز کار برنامه باید انجام گردد. اما هر سشن یا هر واحد کاری آن، به ازای تراکنش‌های مختلفی که باید صورت گیرند، بر روی این DocumentStore، ایجاد شده و سپس بسته خواهند شد. برای مثال در یک برنامه ASP.NET، در فایل Global.asax در زمان آغاز برنامه، کار ایجاد DocumentStore انجام شده و سپس به ازای هر درخواست رسیده، یک سشن RavenDB ایجاد و در پایان درخواست، این سشن آزاد خواهد شد.

برنامه را اجرا کنید، سپس به کنسول سرور RavenDB که پیشتر آن‌را اجرا نمودیم مراجعه نمائید تا نمایی از عملیات انجام شده را بتوان مشاهده کرد:
Raven is ready to process requests. Build 2700, Version 2.5.0 / 6dce79a Server started in 14,438 ms
Data directory: D:\Prog\RavenDBSample01\packages\RavenDB.Server.2.5.2700\tools\Database\System
HostName: <any> Port: 8080, Storage: Esent
Server Url: http://localhost:8080/
Available commands: cls, reset, gc, q
Request #   1: GET     -   514 ms - <system>   - 404 - /docs/Raven/Replication/Destinations
Request #   2: GET     -   763 ms - <system>   - 200 - /queries/?&id=Raven%2FHilo%2Fquestions&id=Raven%2FServerPrefixForHilo
Request #   3: PUT     -   185 ms - <system>   - 201 - /docs/Raven/Hilo/questions
Request #   4: POST    -   103 ms - <system>   - 200 - /bulk_docs
        PUT questions/1
زمانیکه سرور RavenDB در حالت دیباگ در حال اجرا باشد، لاگ کلیه اعمال انجام شده را در کنسول آن می‌توان مشاهده نمود. همانطور که مشاهده می‌کنید، یک کلاینت RavenDB با این بانک اطلاعاتی با پروتکل HTTP و یک REST API ارتباط برقرار می‌کند. برای نمونه، کلاینت در اینجا با اعمال یک HTTP Verb خاص به نام PUT، اطلاعات را درون بانک اطلاعاتی ذخیره کرده است. تبادل اطلاعات نیز با فرمت JSON انجام می‌شود.
عملیات PUT حتما نیاز به یک Id از پیش مشخص دارد و این Id، پیشتر در سطری که Hilo در آن ذکر شده (یکی از الگوریتم‌های محاسبه Id در RavenDB)، محاسبه گردیده است. برای نمونه در اینجا الگوریتم Hilo مقدار "questions/1" را به عنوان Id محاسبه شده بازگشت داده است.
در سطری که عملیات Post به آدرس bulk_docs سرور ارسال گردیده است، کار ارسال یکباره چندین شیء JSON برای کاهش رفت و برگشت‌ها به سرور انجام می‌شود.

و برای کوئری گرفتن مقدماتی از اطلاعات ثبت شده می‌توان نوشت:
 using (var session = store.OpenSession())
{
  var question1 = session.Load<Question>("questions/1");
  Console.WriteLine(question1.By);
}

نگاهی به بانک اطلاعاتی ایجاد شده

در همین حال که سرور RavenDB در حال اجرا است، مرورگر دلخواه خود را گشوده و سپس آدرس http://localhost:8080 را وارد نمائید. بلافاصله، کنسول مدیریتی تحت وب این بانک اطلاعاتی که با سیلورلایت نوشته شده است، ظاهر خواهد شد:


و اگر بر روی هر سطر اطلاعات دوبار کلیک کنید، به معادل JSON آن نیز خواهید رسید:


اینبار برنامه را به صورت زیر تغییر دهید تا روابط بین کلاس‌ها را نیز پیاده سازی کند:
using System;
using Raven.Client.Document;
using RavenDBSample01.Models;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
                               {
                                   Url = "http://localhost:8080"
                               }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    var question = new Question
                    {
                        By = "users/Vahid",
                        Title = "Raven Intro",
                        Content = "Test...."
                    };                 
                    question.Answers.Add(new Answer
                    {
                         By = "users/Farid",
                         Content = "بررسی می‌شود"
                    });
                    session.Store(question);

                    session.SaveChanges();
                }

                using (var session = store.OpenSession())
                {
                    var question1 = session.Load<Question>("questions/1");
                    Console.WriteLine(question1.By);
                }
            }
        }
    }
}
در اینجا یک سؤال به همراه پاسخی به آن تعریف شده است. همچنین در مرحله بعد، نحوه کوئری گرفتن مقدماتی از اطلاعات را بر اساس Id سند مرتبط، مشاهده می‌کنید. چون یک Session، الگوی واحد کار را پیاده سازی می‌کند، اگر پس از Load یک سند، خواصی از آن‌را تغییر دهیم و در پایان Session متد SaveChanges فراخوانی شود، به صورت خودکار این تغییرات به بانک اطلاعاتی نیز اعمال خواهند شد (روش به روز رسانی اطلاعات). این مورد بسیار شبیه است به مباحث پایه ای Change tracking که در بسیاری از ORMهای معروف تاکنون پیاده سازی شده‌اند. روش حذف اطلاعات نیز به همین ترتیب است. ابتدا سند مورد نظر یافت شده و سپس متد session.Delete بر روی این شیء یافت شده فراخوانی گردیده و در پایان سشن باید SaveChanges جهت نهایی شدن تراکنش فراخوانی گردد.

اگر برنامه فوق را اجرا کرده و به ساختار اطلاعات ذخیره شده نگاهی بیندازیم به شکل زیر خواهیم رسید:


نکته جالبی که در اینجا وجود دارد، عدم نیاز به join نویسی برای دریافت اطلاعات وابسته به یک شیء است. اگر سؤالی وجود دارد، پاسخ‌های به آن و یا سایر نظرات، یکجا داخل همان سؤال ذخیره می‌شوند و به این ترتیب سرعت دسترسی نهایی به اطلاعات بیشتر شده و همچنین قفل گذاری روی سایر اسناد کمتر. این مساله نیز به ذات NoSQL و یا غیر رابطه‌ای RavenDB بر می‌گردد. در بانک‌های اطلاعاتی NoSQL، مفاهیمی مانند کلیدهای خارجی، JOIN بین جداول و امثال آن وجود خارجی ندارند. برای نمونه اگر به کلاس‌های مدل‌های برنامه دقت کرده باشید، خبری از وجود Id در آن‌ها نیست. RavenDB یک Document store است و نه یک Relation store. در اینجا کل درخت تو در توی خواص یک شیء دریافت و به صورت یک سند ذخیره می‌شود. به حاصل این نوع عملیات در دنیای بانک‌های اطلاعاتی رابطه‌ای، Denormalized data هم گفته می‌شود.
البته می‌توان به کلاس‌های تعریف شده خاصیت رشته‌ای Id را نیز اضافه کرد. در این حالت برای مثال در حالت فراخوانی متد Load، این خاصیت رشته‌ای، با Id تولید شده توسط RavenDB مانند "questions/1" مقدار دهی می‌شود. اما از این Id برای تعریف ارجاعات به سؤالات و پاسخ‌های متناظر استفاده نخواهد شد؛ چون تمام آن‌ها جزو یک سند بوده و داخل آن قرار می‌گیرند.
مطالب
Upload چند فایل بطور هم زمان در ASP.NET 4.5
چندی پیش امکان بارگذاری چندین فایل بطور هم زمان روی سرور با استفاده از کنترل‌های Telerik یا DevExpress مهیا می‌شد. همچنین به کمک jQuery تکنیک هایی وجود داشت. اما در HTML5 می‌توان از تگ زیر استفاده کرد:
<input type="file" multiple="multiple" name="FileUpload1" id="FileUpload1" />
یکی از امکانات جدید ASP.NET4.5 سازگاری کنترل‌های سمت سرور با HTML5 است. از این رو به کنترل FileUpload خصوصیاتی از قبیل HasFiles و PostedFiles و  AllowMultiple اضافه شده است:
<asp:FileUpload ID="FileUpload1" runat="server" AllowMultiple="true" />
این کنترل در مرورگرهای متفاوت بصورت‌های مختلفی نمایش داده می‌شود. نمایش در مرورگر Chrome بصورت زیر خواهد بود:
 

و در Opera:

 
با این تفاسیر در سمت سرور، کار دشواری پیش روی ما نخواهد بود:
protected void Upload_Click(object sender, EventArgs e)
{
  if (FileUpload1.HasFiles)
  {
    string rootPath = Server.MapPath("~/App_Data/");
    foreach (HttpPostedFile file in FileUpload1.PostedFiles)
    {
      file.SaveAs(Path.Combine(rootPath, file.FileName));
      Label1.Text += String.Format("{0}<br />", file.FileName);
    }
  }
}
البته از آنجایی که هدف از این مطلب معرفی یکی از قابلیت‌های جدید HTML5 و ASP.NET4.5 است، کد بالا بسیار ساده نوشته شده است و فایل‌های ارسال شده به سرور را در پوشه App_Data و بدون در نظر گرفتن مسائل عرف درباره Upload فایل، ذخیره می‌کند.
مطالب
نمایش خطاهای اعتبارسنجی سمت سرور ASP.NET Core در برنامه‌های Angular
در مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها» با نحوه‌ی تنظیمات اعتبارسنجی سمت کلاینت برنامه‌های Angular آشنا شدیم. اما اگر مدل سمت سرور ما یک چنین شکلی را داشته باشد که به همراه خطاهای اعتبارسنجی سفارشی نیز هست:
using System;
using System.ComponentModel.DataAnnotations;

namespace AngularTemplateDrivenFormsLab.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Movie Title is Required")]
        [MinLength(3, ErrorMessage = "Movie Title must be at least 3 characters")]
        public string Title { get; set; }

        [Required(ErrorMessage = "Movie Director is Required.")]
        public string Director { get; set; }

        [Range(0, 100, ErrorMessage = "Ticket price must be between 0 and 100.")]
        public decimal TicketPrice { get; set; }

        [Required(ErrorMessage = "Movie Release Date is required")]
        public DateTime ReleaseDate { get; set; }
    }
}
و همچنین کنترلر و اکشن متد دریافت کننده‌ی آن نیز به صورت ذیل تعریف شده باشد:
using AngularTemplateDrivenFormsLab.Models;
using Microsoft.AspNetCore.Mvc;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class MoviesController : Controller
    {
        [HttpPost]
        public IActionResult Post([FromBody]Movie movie)
        {
            if (ModelState.IsValid)
            {
                // TODO: save ...
                return Ok(movie);
            }

            ModelState.AddModelError("", "This record already exists."); // a cross field validation
            return BadRequest(ModelState);
        }
    }
}
دو نوع خطای اعتبارسنجی سمت سرور را به سمت کلاینت ارسال خواهیم کرد:
الف) خطاهای اعتبارسنجی در سطح فیلدها
زمانیکه return BadRequest(ModelState) صورت می‌گیرد، محتویات شیء ModelState به همراه status code مساوی 400 به سمت کلاینت ارسال خواهد شد. در شیء ModelState یک دیکشنری که کلیدهای آن، نام خواص و مقادیر متناظر با آن‌ها، خطاهای اعتبارسنجی تنظیم شده‌ی در مدل است، قرار دارند.
ب) خطاهای اعتبارسنجی عمومی
در این بین می‌توان دیکشنری ModelState را توسط متد AddModelError نیز تغییر داد و برای مثال کلید آن‌را مساوی "" تعریف کرد. در این حالت یک چنین خطایی به کل فرم اشاره می‌کند و نه به یک خاصیت خاص.

نمونه‌ای از خروجی نهایی ارسالی به سمت کاربر:
 {"":["This record already exists."],"TicketPrice":["Ticket price must be between 0 and 100."]}

به همین جهت نیاز است بتوان خطاهای حالت (الف) را دقیقا در ذیل هر فیلد و خطاهای حالت (ب) را در بالای فرم به صورت عمومی به کاربر نمایش داد:



پردازش و دریافت خطاهای اعتبارسنجی سمت سرور در یک برنامه‌ی Angular

با توجه به اینکه سرور، شیء ModelState را توسط return BadRequest به سمت کلاینت ارسال می‌کند، برای پردازش دیکشنری دریافتی از سمت آن، تنها کافی است قسمت بروز خطای عملیات ارسال اطلاعات را بررسی کنیم:


در این HttpErrorResponse دریافتی، دو خاصیت error که همان آرایه‌ی دیکشنری نام خواص و پیام‌های خطای مرتبط با هر کدام و status code دریافتی مهم هستند:
  errors: string[] = [];

  processModelStateErrors(form: NgForm, responseError: HttpErrorResponse) {
    if (responseError.status === 400) {
      const modelStateErrors = responseError.error;
      for (const fieldName in modelStateErrors) {
        if (modelStateErrors.hasOwnProperty(fieldName)) {
          const modelStateError = modelStateErrors[fieldName];
          const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
          if (control) {
            // integrate into Angular's validation
            control.setErrors({
              modelStateError: { error: modelStateError }
            });
          } else {
            // for cross field validations -> show the validation error at the top of the screen
            this.errors.push(modelStateError);
          }
        }
      }
    } else {
      this.errors.push("something went wrong!");
    }
  }

  lowerCaseFirstLetter(data: string): string {
    return data.charAt(0).toLowerCase() + data.slice(1);
  }
توضیحات:
در اینجا از آرایه‌ی errors برای نمایش خطاهای عمومی در سطح فرم استفاده می‌کنیم. این خطاها در ModelState، دارای کلید مساوی "" هستند. به همین جهت حلقه‌ای را بر روی شیء responseError.error تشکیل می‌دهیم. به این ترتیب می‌توان به نام خواص و همچنین خطاهای متناظر با آن‌ها رسید.
 const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
از نام خاصیت یا فیلد، جهت یافتن کنترل متناظر با آن، در فرم جاری استفاده می‌کنیم. ممکن است کنترل تعریف شده camel case و یا pascal case باشد. به همین جهت دو حالت بررسی را در اینجا مشاهده می‌کنید.
در ادامه اگر control ایی یافت شد، توسط متد setErrors، کلید جدید modelStateError را که دارای خاصیت سفارشی error است، تنظیم می‌کنیم. با اینکار سبب خواهیم شد تا خطای اعتبارسنجی دریافتی از سمت سرور، با سیستم اعتبارسنجی Angular یکی شود. به این ترتیب می‌توان این خطا را دقیقا ذیل همین کنترل در فرم نمایش داد. اگر کنترلی یافت نشد (کلید آن "" بود و یا جزو نام کنترل‌های موجود در آرایه‌ی form.controls نبود)، این خطا را به آرایه‌ی errors اضافه می‌کنیم تا در بالاترین سطح فرم قابل نمایش شود.

نحوه‌ی استفاده‌ی از متد processModelStateErrors فوق را در متد submitForm، در قسمت شکست عملیات ارسال اطلاعات، مشاهده می‌کنید:
  model = new Movie("", "", 0, "");
  successfulSave: boolean;
  errors: string[] = [];

  constructor(private movieService: MovieService) { }

  ngOnInit() {
  }

  submitForm(form: NgForm) {
    console.log(form);

    this.errors = [];
    this.movieService.postMovieForm(this.model).subscribe(
      (data: Movie) => {
        console.log("Saved data", data);
        this.successfulSave = true;
      },
      (responseError: HttpErrorResponse) => {
        this.successfulSave = false;
        console.log("Response Error", responseError);
        this.processModelStateErrors(form, responseError);
      });
  }


نمایش خطاهای اعتبارسنجی عمومی فرم

اکنون که کار مقدار دهی آرایه‌ی errors انجام شده‌است، می‌توان حلقه‌ای را بر روی آن تشکیل داد و عناصر آن‌را در بالای فرم، به صورت عمومی و مستقل از تمام فیلدهای آن نمایش داد:
<form #form="ngForm" (submit)="submitForm(form)" novalidate>
  <div class="alert alert-danger" role="alert" *ngIf="errors.length > 0">
    <ul>
      <li *ngFor="let error of errors">
        {{ error }}
      </li>
    </ul>
  </div>
  <div class="alert alert-success" role="alert" *ngIf="successfulSave">
    Movie saved successfully!
  </div>



نمایش خطاهای اعتبارسنجی در سطح فیلدهای فرم

با توجه به تنظیم خطاهای اعتبارسنجی کنترل‌های Angular در متد processModelStateErrors و داشتن کلید جدید modelStateError
control.setErrors({
  modelStateError: { error: modelStateError }
});
اکنون می‌توان از این کلید جدید (ctrl.errors.modelStateError)، به صورت ذیل جهت نمایش خطای متناظر با آن (ctrl.errors.modelStateError.error) استفاده کرد:


<ng-template #validationErrorsTemplate let-ctrl="control">
  <div *ngIf="ctrl.invalid && ctrl.touched">
    <div class="alert alert-danger"  *ngIf="ctrl.errors.required">
      This field is required.
    </div>
    <div class="alert alert-danger"  *ngIf="ctrl.errors.minlength">
      This field should be minimum {{ctrl.errors.minlength.requiredLength}} characters.
    </div>
    <div class="alert alert-danger"  *ngIf="ctrl.errors.maxlength">
      This field should be max {{ctrl.errors.maxlength.requiredLength}} characters.
    </div>
    <div class="alert alert-danger"  *ngIf="ctrl.errors.pattern">
      This field's pattern: {{ctrl.errors.pattern.requiredPattern}}
    </div>
    <div class="alert alert-danger"  *ngIf="ctrl.errors.modelStateError">
      {{ctrl.errors.modelStateError.error}}
    </div>
  </div>
</ng-template>
چون تکرار خطاهای اعتبارسنجی در ذیل هر فیلد، فرم را بیش از اندازه شلوغ می‌کند، می‌توان توسط یک ng-template این کدهای تکراری را تبدیل به یک قالب کرد و اکنون استفاده‌ی از این قالب، به سادگی فراخوانی یک ng-container است:
  <div class="form-group" [class.has-error]="releaseDate.invalid && releaseDate.touched">
    <label class="control-label" for="releaseDate">Release Date</label>
    <input type="text" name="releaseDate" #releaseDate="ngModel"  class="form-control"
      required [(ngModel)]="model.releaseDate" />
    <ng-container *ngTemplateOutlet="validationErrorsTemplate; context:{ control: releaseDate }"></ng-container>
  </div>
در اینجا در ngTemplateOutlet، ابتدا نام قالب متناظر ذکر می‌شود و سپس در context آن، نام خاصیت control را که توسط قالب دریافت می‌شود، به template reference variable متناظری تنظیم می‌کنیم، تا به کنترل جاری اشاره کند. به این ترتیب می‌توان به فرم‌هایی خلوت‌تر و با قابلیت مدیریت بهتری رسید.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
سازماندهی برنامه‌های Angular توسط ماژول‌ها
توضیحات تکمیلی در مورد Core Module

اگر به CoreModule تعریف شده دقت کنید، یک چنین تعریفی در آن ذکر شده‌است:
   providers: [ UserRepositoryService ]
برای درک یک چنین تعریفی، نیاز است با نحوه‌ی عملکرد Angular Injector آشنا شد. برای این منظور فرض کنید برنامه‌ای را با سه ماژول طراحی کرده‌اید: یک App Module و دو ماژول معمولی و دیگری Lazy loaded. به ازای هر برنامه، Angular یک Root Injector را ایجاد می‌کند و کار آن تزریق سرویس‌ها، در مکان‌هایی است که به آن‌ها نیاز هست. این Root Injector نیز در App Module و در بالاترین سطح برنامه تشکیل می‌شود.
اکنون فرض کنید سرویسی را در ماژول معمولی غیر Lazy loaded تعریف کرده‌اید. Angular این سرویس را در Root Injector ثبت می‌کند. به این ترتیب این سرویس در کل برنامه قابل دسترسی می‌شود. اما اگر سرویسی را در یک ماژول Lazy loaded تعریف کنید، Angular کار ایجاد یک Injector جداگانه را برای آن ماژول و در داخل آن انجام می‌دهد. این Injector مجزا است از Root Injector قرار گرفته‌ی در App Module. سپس این سرویس در این Injector جدید ثبت می‌شود. به این ترتیب، وهله‌ی تهیه شده‌ی از این سرویس، تنها درون این ماژول Lazy loaded قابل دسترسی است. حتی اگر این دو سرویس ثبت شده‌ی در Root Injector و Injector مخصوص ماژول Lazy loaded از یک کلاس تهیه شوند، باز هم دو وهله‌ی مختلف از آن ارائه می‌شوند. برای مثال اگر UserRepositoryService را در ماژول معمولی و ماژول Lazy loaded مورد استفاده قرار دهیم، دو وهله‌ی مجزای از آن‌ها تشکیل خواهد شد که دیگر با هم همگام نبوده و کار ردیابی اطلاعات کاربر جاری سیستم را با مشکل مواجه می‌کنند.
ایجاد وهله‌ی دوم از یک سرویس، تنها منحصر است به ماژول‌های Lazy loaded. اما اگر این سرویس در ماژول‌های معمولی مختلفی مورد استفاده قرار گیرد، Angular تنها یک وهله از آن‌را ایجاد خواهد کرد. به همین جهت است که در این‌جا CoreModule را تعریف کردیم. این ماژول مکانی است که قرار است سرویس‌های اشتراکی در کل برنامه در آن قرار گیرند. چون این ماژول هیچگاه Lazy loaded نخواهد شد، هرگاه سرویسی را توسط آن ارائه دهیم، در Root Injector مربوط به App Module ثبت می‌شود. به این ترتیب تبدیل به یک Singleton Service قابل دسترسی در کل برنامه می‌شود.
باید دقت داشت که از لحاظ فنی، تفاوتی بین Core Module و یک ماژول معمولی غیر Lazy loaded نیست و بیشتر هدف از آن نظم بخشیدن به تعریف سرویس‌هایی است که قرار است در طول عمر برنامه و در تمام انواع ماژول‌های آن، توسط یک وهله قابل دسترسی شوند.
بنابراین تفاوتی نمی‌کند که یک سرویس را درون Core Module تعریف کنید و یا یک ماژول معمولی. این سرویس همواره در Root Injecror ثبت خواهد شد؛ مگر اینکه آن ماژول Lazy loaded باشد. در این حالت اگر سرویسی تنها قرار است در یک ماژول خاص استفاده شود، بهتر است آن‌را جهت مدیریت بهتر برنامه، درون همان پوشه تعریف کرد. هرچند از لحاظ فنی، این سرویس‌ها نهایتا در Root Injector مربوط به App Module ثبت و ارائه می‌شوند (از این لحاظ فرقی بین یک Core Module و Feature Modules نیست).
مطالب
سازگارسازی کلاس‌های اعتبارسنجی Twitter Bootstrap 3 با فرم‌های ASP.NET MVC
چندی پیش در همین وب‌سایت مطلبی تحت عنوان «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» منتشر شد. این مقاله مرتبط با نسخه دوم فریم‌ورک محبوب Bootstrap بود. قصد داریم به بازنویسی کدهای مرتبط بپردازیم و کلاس‌های مرتبط با نسخه سوم این فریم‌ورک را هم با فرم‌های خودمان سازگار کنیم. مثل مقاله‌ی ذکر شده توضیحات را با یک مثال همراه می‌کنم.

مدل برنامه
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace FormValidationWithBootstrap.Models
{
    [Table("Product")]
    public class ProductModel
    {
        [Key]
        public int Id { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")]
        [Display(Name = "نام کالا")]
        public string Name { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [Display(Name = "قیمت")]
        [DataType(DataType.Currency)]
        public double Price { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [Display(Name = "موجودی")]
        public int Qty { get; set; }
    }
}
قرار هست که جدولی داشته باشیم با نام Product برای ثبت محصولات. مدل برنامه شامل خاصیت‌های مرتبط و همچنین اعتبارسنجی‌های مد نظر ما هست.

کنترلر برنامه
using System.Web.Mvc;
using FormValidationWithBootstrap.Models;

namespace FormValidationWithBootstrap.Controllers
{
    public class ProductController : Controller
    {
        // GET: Product
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult New()
        {
            return View();
        }

        [HttpPost]
        public ActionResult New(ProductModel product)
        {
            if (!ModelState.IsValid)
                return View(product);

            if (product.Name != "پفک")
            {
                ModelState.AddModelError("", "لطفا مشکلات را برطرف کنید!");
                ModelState.AddModelError("Name", "فقط محصولی با نام پفک قابل ثبت است :)");
                return View(product);
            }
            // todo:save...
            return RedirectToAction("Index");
        }
    }
}
در قسمت کنترلر نیز اتفاق خاصی نیفتاده و کارهای پایه فقط انجام شده؛ ضمن اینکه آمدیم برای داشتن خطاهای سفارشی نام محصول را چک کردیم و گفتیم اگر نام محصول چیزی غیر از «پفک» بود، از سمت سرور خطایی را صادر کند و بگوید که فقط پفک قابل ثبت هست.

View برنامه
@model FormValidationWithBootstrap.Models.ProductModel
@{
    ViewBag.Title = "New";
}
<h2>کالای جدید</h2>
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    <div>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "alert alert-danger" })
        <div>
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            @Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            @Html.LabelFor(model => model.Qty, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Qty, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Qty, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            <div>
                <input type="submit" value="ثبت" />
                <input type="reset" value="ریست" />
                @Html.ActionLink("بازگشت به لیست", "Index", "Product", null, new {@class="btn btn-default"})
            </div>
        </div>
    </div>
}
فایل View برنامه با Scafflod Templateها ساخته شده و چون از Visual Studio 2013 استفاده شده، به‌صورت پیش‌فرض با بوت‌استرپ سازگار هست. تغییری که ایجاد شده تعویض کلاس مربوط به ValidationSummary هست که به alert alert-danger تغییر پیدا کرده و همچین دو دکمه «ریست» و «بازگشت به لیست» هم به کنار دکمه «ثبت» اضافه شده.

در فرم بالا شاهد هستیم که با کلیک بر روی دکمه ثبت تنها خطاهای مرتبط با هر ردیف ظاهر شده‌اند و هیچ تغییر رنگی که حاصل از کلاس‌های مرتبط با Bootstrap باشند حاصل نشده. برای رفع این مشکل کافی‌‌است اسکریپت زیر، به انتهای فایل View برنامه اضافه شود تا پیش‌فرض‌های jQuery Validator را تغییر دهیم و آن‌ها را با بوت‌استرپ سازگار کنیم. همچنین در حالت ارسال فرم به سرور و Postback و نمایش خطاهای سفارشی، قسمت بررسی field-validation-error صورت می‌گیرد و در صورتیکه موردی را پیدا کند، به سطر مرتبط با آن کلاس has-error اضافه خواهد شد. 
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    <script>
        // override jquery validate plugin defaults
        $.validator.setDefaults({
            highlight: function (element) {
                $(element).closest('.form-group').addClass('has-error');
            },
            unhighlight: function (element) {
                $(element).closest('.form-group').removeClass('has-error').addClass('has-success');
            },
            errorElement: 'span',
            errorClass: 'help-block',
            errorPlacement: function (error, element) {
                if (element.parent('.input-group').length) {
                    error.insertAfter(element.parent());
                } else {
                    error.insertAfter(element);
                }
            }
        });
        $(function () {
            $('form').each(function () {
                $(this).find('div.form-group').each(function () {
                    if ($(this).find('span.field-validation-error').length > 0) {
                        $(this).addClass('has-error');
                    }
                });
            });
        });
    </script>
}

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

همچنین هنگامیکه کاربر فیلد را به درستی وارد کرد، رنگ فیلد و همچین آن ردیف به سبز تغییر خواهد کرد.

و همچنین در حالت رخ‌داد یک خطای سفارشی پس از postback از سمت سرور به حالت زیر خواهیم رسیذ.

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

FormValidationWithBootstrap.rar 

بازخوردهای پروژه‌ها
self registeration httpmodule
با سلام و عرض خدا قوت و تشکر بابت به اشتراک گذاری 
جهت فارغ شدن از تنظیمات ثبت httpmodule در فایل web.config و درگیر شدن با نسخ iis می‌توان کلاسی به نام ModuleRegistretion  ایجاد کرده و ویژگی assembly را در بالای کدهای خود قبل از شروع تعریف فضای نام قرار داده تا متد RegisterModule  در ابتدای چرخه حیات برنامه اجرا شده و با اجرای دستور   HttpApplication.RegisterModule موجبات ثبت httpmodule فراهم گردد . 
[assembly: PreApplicationStartMethod(typeof(Statistics.Modules.ModuleRegistretion), "RegisterModule")]

namespace Statistics.Modules
{
    public class ModuleRegistretion
    {
        public static void RegisterModule()
        {
            HttpApplication.RegisterModule(typeof(StatModule));
        }
    }
...
}

 همچنین در کدهای ماژول قسمت 
XDocument xdoc = XDocument.Load("http://www.freegeoip.net/xml/" + GetIPAddress());

 بهتر است استثناهای مرتبط مدیریت شوند تا در صورت اختلال و عدم دسترسی به آدرس خارجی ذکر شده مشکلی برای برنامه ایجاد نشود .

نظرات مطالب
جزئیات برنامه نویسی افزونه فارسی به پارسی
- فایل xml با یونیکد مشکلی ندارد و اینجا هم میشد از xml استفاده کرد.
برای نمونه یکی از برنامه نویسان گرامی در سایت
http://psoft.ir/?p=23
از xml بعنوان دیتابیس نمایش اشعار، پند و اندرز و امثال آن استفاده کرده.

- حقیقت امر این است که در این سایت من از بحث وارز و امثال آن پرهیز خواهم داشت. ولی محض نمونه این عبارات را به همراه pdf در انتهای آن در گوگل جستجو کنید کتابهای مربوطه رو می‌تونید به سادگی پیدا کنید:
Wrox.Professional.VSTO.2005.Visual.Studio.2005.Tools.for.Office
Visual.Studio.Tools.for.Office.Using.C.Sharp.with.Excel.Word.Outlook.and.InfoPath

موفق باشید
مطالب دوره‌ها
متدهای توکار استفاده از نوع داده‌ای XML - قسمت اول
در دو قسمت قبل، XQuery را به عنوان یک زبان برنامه نویسی استاندارد مورد بررسی قرار دادیم. در ادامه قصد داریم ترکیب آن‌را با توابع ویژه توکار SQL Server جهت کار با نوع داده‌ای XML، مانند exists، modify و امثال آن، تکمیل نمائیم. اگر بخاطر داشته باشید، 5 متد توکار جهت کار با نوع داده‌ای XML در SQL Server پیش بینی شده‌اند:
- query : xml را به عنوان ورودی گرفته و نهایتا یک خروجی XML دیگر را بر می‌گرداند.
- exist : خروجی bit دارد؛ true یا false. ورودی آن یک XQuery است.
- value : یک خروجی SQL Type را ارائه می‌دهد.
- nodes : خروجی جدولی دارد.
- modify : برای تغییر اطلاعات بکار می‌رود.


استفاده از متد exist به عنوان جایگزین سبک وزن XML Schema

یکی از کاربردهای متد exist، تعریف قید بر روی یک ستون XML ایی جدول است. این روش، راه حل دوم و ساده‌ای است بجای استفاده از XML Schema برای ارزیابی و اعتبارسنجی کل سند. پیشنیاز اینکار، تعریف قید مدنظر توسط یک تابع جدید است:
CREATE FUNCTION dbo.checkPerson(@data XML)
RETURNS BIT WITH SCHEMABINDING AS
BEGIN
   RETURN @data.exist('/people/person')
END
GO

CREATE TABLE tblXML
(
id INT PRIMARY KEY,
doc XML CHECK(dbo.checkPerson(doc)=1)  
)
GO
متد checkPerson به دنبال وجود نود people/person، در ریشه‌ی سند XML در حال ذخیره شدن می‌گردد. پس از تعریف این متد، نحوه‌ی استفاده از آن‌را توسط عبارت check در حین تعریف ستون doc ملاحظه می‌کنید.

اکنون برای آزمایش آن خواهیم داشت:
 INSERT INTO tblXML (id,  doc) VALUES
(
 1, '<people><person name="Vahid"/></people>'
)

INSERT INTO tblXML (id,  doc) VALUES
(
 2, '<people><emp name="Vahid"/></people>'
)
Insert اول با موفقیت انجام خواهد شد. اما Insert دوم با خطای ذیل متوقف می‌شود:
 The INSERT statement conflicted with the CHECK constraint "CK__tblXML__doc__060DEAE8".
The conflict occurred in database "testdb", table "dbo.tblXML", column 'doc'.
The statement has been terminated.
همچنین باید در نظر داشت که امکان ترکیب یک XML Schema و تابع اعمال قید نیز با هم وجود دارند. برای مثال از XML Schema برای تعیین اعتبار ساختار کلی سند در حال ذخیره سازی استفاده می‌شود و همچنین نیاز است تا منطق تجاری خاصی را توسط یک تابع، پیاده سازی کرده و در این بین اعمال نمود.


استفاده از متد value برای دریافت اطلاعات

با کاربرد مقدماتی متد value در بازگشت یک مقدار scalar در قسمت‌های قبل آشنا شدیم. در ادامه مثال‌های کاربردی‌تر را بررسی خواهیم کرد.
ابتدا جدول زیر را با یک ستون XML در آن درنظر بگیرید:
 CREATE TABLE xml_tab
(
 id INT IDENTITY PRIMARY KEY,
 xml_col  XML
)
سپس چند ردیف را به آن اضافه می‌کنیم:
 INSERT INTO xml_tab
VALUES ('<people><person name="Vahid"/></people>')
INSERT INTO xml_tab
VALUES ('<people><person name="Farid"/></people>')
در ادامه می‌خواهیم id و نام اشخاص ذخیره شده در جدول را بازیابی کنیم:
SELECT
   id,
   xml_col.value('(/people/person/@name)[1]', 'varchar(50)') AS name
FROM
xml_tab
متد vlaue یک XPath را دریافت کرده، به همراه نوع آن و صفر یا یک نود را بازگشت خواهد داد. به همین جهت، با توجه به عدم تعریف اسکیما برای سند XML در حال ذخیره شدن، نیاز است اولین نود را صریحا مشخص کنیم.


یک نکته
اگر نیاز به خروجی از نوع XML است، بهتر است از متد query که در دو قسمت قبل بررسی شد، استفاده گردد. خروجی متد query همیشه یک untyped XML است یا نال. البته می‌توان خروجی آن‌را به یک typed XML دارای Schema نیز نسبت داد. در اینجا اعتبارسنجی در حین انتساب صورت خواهد گرفت.


استفاده از متد value برای تعریف قیود

از متد value همچنین می‌توان برای تعریف قیود پیشرفته نیز استفاده کرد. برای مثال فرض کنیم می‌خواهیم ویژگی Id سند XML در حال ذخیره شدن، حتما مساوی ستون Id جدول باشد. برای این منظور ابتدا نیاز است همانند قبل یک تابع جدید را ایجاد نمائیم:
 CREATE FUNCTION getIdValue(@doc XML)
RETURNS int WITH SCHEMABINDING AS
BEGIN
  RETURN @doc.value('/*[1]/@Id', 'int')
END
این تابع یک int را باز می‌گرداند که حاصل مقدار ویژگی Id اولین نود ذیل ریشه است. اگر این نود، ویژگی Id نداشته باشد، null بر می‌گرداند.
سپس از این تابع در عبارت check برای مقایسه ویژگی Id سند XML در حال ذخیره شدن و id ردیف جاری استفاده می‌شود:
 CREATE TABLE docs_tab
(
id INT PRIMARY KEY,
doc XML,
CONSTRAINT id_chk CHECK(dbo.getIdValue(doc)=id)  
)
نحوه‌ی تعریف آن اینبار توسط عبارت CONSTRAINT است؛ زیرا در سطح جدول باید عمل کند (ارجاعی را به یک فیلد آن دارد) و نه در سطح یک فیلد؛ مانند مثال ابتدای بحث جاری.
در ادامه برای آزمایش آن خواهیم داشت:
 INSERT INTO docs_tab (id,  doc) VALUES
(
 1, '<Invoice Id="1"/>'
)

INSERT INTO docs_tab (id,  doc) VALUES
(
 2, '<Invoice Id="1"/>'
)
Insert اول با توجه به یکی بودن مقدار ویژگی Id آن با id ردیف، با موفقیت ثبت می‌شود. ولی رکورد دوم خیر:
 The INSERT statement conflicted with the CHECK constraint "id_chk".
The conflict occurred in database "testdb", table "dbo.docs_tab".
The statement has been terminated.


استفاده از متد value برای تعریف primary key

پیشتر عنوان شد که از فیلدهای XML نمی‌توان به عنوان کلید یک جدول استفاده کرد؛ چون امکان مقایسه‌ی محتوای کل آن‌ها وجود ندارد. اما با استفاده از متد value می‌توان مقدار دریافتی را به عنوان یک کلید اصلی محاسبه شده، ثبت کرد:
 CREATE TABLE Invoices
(
 doc XML,
 id AS dbo.getIdValue(doc) PERSISTED PRIMARY KEY
)
Id در اینجا یک computed column است. همچنین باید به صورت PERSISTED علامتگذاری شود تا سپس به عنوان PRIMARY KEY  قابل استفاده باشد.
برای آزمایش آن سعی می‌کنیم دو رکورد را که حاوی ویژگی id برابری هستند، ثبت کنیم:
 INSERT INTO Invoices VALUES
(
 '<Invoice Id="1"/>'
)
INSERT INTO Invoices VALUES
(
 '<Invoice Id="1"/>'
)
مورد اول با موفقیت ثبت می‌شود. مورد دوم خیر:
 Violation of PRIMARY KEY constraint 'PK__Invoices__3213E83F145C0A3F'.
Cannot insert duplicate key in object 'dbo.Invoices'. The duplicate key value is (1).
The statement has been terminated.


توابع دسترسی به مقدار داده‌ها در XQuery

تابع data ، string و text برای دسترسی به مقدار داده‌ها در XQuery پیش بینی شده‌اند.
اگر سعی کنیم مثال زیر را اجرا نمائیم:
 DECLARE @doc XML
SET @doc = '<foo bar="baz" />'
SELECT @doc.query('/foo/@bar')
با خطای ذیل متوقف خواهیم شد:
 XQuery [query()]: Attribute may not appear outside of an element
علت اینجا است که خروجی query از نوع XML است و ما در XPath نوشته شده درخواست بازگشت مقدار یک ویژگی را کرده‌ایم که نمی‌تواند به عنوان ریشه یک سند XML بازگشت داده شود. برای بازگشت مقدار ویژگی bar که baz است باید از متد data استفاده کرد:
 DECLARE @doc XML
SET @doc = '<foo bar="baz" />'
SELECT @doc.query('data(/foo/@bar)')
متد data می‌تواند بیش از یک مقدار را در یک توالی بازگشت دهد:
 DECLARE @x XML
SET @x = '<x>hello<y>world</y></x><x>again</x>'
SELECT @x.query('data(/*)')
در اینجا توسط متد data درخواست بازگشت کلیه root elementsهای سند XML را کرده‌ایم. خروجی آن helloworld again خواهد بود.
اما اگر همین مثال را با متد string اجرا کنیم:
 DECLARE @x XML
SET @x = '<x>hello<y>world</y></x><x>again</x>'
SELECT @x.query('string(/*)')
به خطای آشنای ذیل برخواهیم خورد:
 XQuery [query()]: 'string()' requires a singleton (or empty sequence), found operand of type 'element(*,xdt:untyped) *'
در اینجا چون تابع string باید بیش از یک نود را پردازش کند، خطایی را صادر کرده‌است. برای رفع آن باید دقیقا مشخص کنیم که برای مثال تنها اولین عضو توالی را بازگشت بده:
 SELECT @x.query('string(/*[1])')
خروجی آن helloworld است.
برای دریافت تمام کلمات توسط متد string می‌توان از اسلش کمک گرفت:
 SELECT @x.query('string(/)')
با خروجی helloworldagain که تنها یک string value محسوب می‌شود؛ برخلاف حالت استفاده از متد data که دو مقدار یک توالی را بازگشت داده است.
نمونه‌ی دیگر آن مثال زیر است:
 DECLARE @x XML = '<age>12</age>'
SELECT @x.query('string(/age[1])')
در اینجا نیز باید حتما اولین المان، صراحتا مشخص شود. هرچند به نظر این سند untyped XML تنها یک المان دارد، اما XQuery ذکر شده پیش از اجرای آن، تعیین اعتبار می‌شود. برای عدم ذکر اولین آیتم (در صورت نیاز)، باید XML Schema سند مرتبط، تعریف و در حین تعریف و انتساب مقدار آن، مشخص گردد. همچنین در اینجا به مباحث content و document که در قسمت‌های پیشین نیز ذکر شد باید دقت داشت. حالت پیش فرض content است و می‌تواند بیش از یک root element داشته باشد.

متد text اندکی متفاوت عمل می‌کند. برای بررسی آن، ابتدا یک schema collection جدید را تعریف می‌کنیم که داری تک المانی رشته‌ای است به نام Root.
 CREATE XML SCHEMA COLLECTION root_el AS
'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
                  targetNamespace="urn:geo">
      <xs:element name="Root" type="xs:string" />    
</xs:schema>
'
GO
در ادامه اگر متد text را بر روی یک untyped XML که SChema آن مشخص نشده‌است، فراخوانی کنیم:
 DECLARE @xmlDoc XML
SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>'
SELECT @xmlDoc.query('
declare namespace g="urn:geo";
/g:Root/text()
')
مقدار datadata... این المان Root را بازگشت خواهد داد. اینبار اگر untyped XML را با تعریف schema آن تبدیل به typed XML کنیم:
 DECLARE @xmlDoc XML(root_el)
SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>'
SELECT @xmlDoc.query('
declare namespace g="urn:geo";
/g:Root[1]/text()
')
به خطای ذیل برخواهیم خورد:
 XQuery [query()]: 'text()' is not supported on simple typed or 'http://www.w3.org/2001/XMLSchema#anyType'
elements, found 'element(g{urn:geo}:Root,xs:string) *'.
زمانیکه از Schema استفاده می‌شود، دیگر نیازی به استفاده از متد text نیست. فقط کافی است متد text را حذف کرده و بجای آن از متد data استفاده کنیم:
 DECLARE @xmlDoc XML(root_el)
SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>'
SELECT @xmlDoc.query('
declare namespace g="urn:geo";
data(/g:Root[1])
')
به علاوه، در خطا ذکر شده‌است که متد text را بر روی  simple types نمی‌توان بکار برد. این محدودیت در مورد complex types که نمونه‌ای از آن‌را در قسمت معرفی Schema با تعریف Point مشاهده کردید، وجود ندارد. اما متد data قابل استفاده بر روی complex types نیست. ولی می‌توان متد data و text را با هم ترکیب کرد؛ برای مثال
data(/age/text())
 اگر complex node را untyped تعریف کنیم (schema را قید نکنیم)، استفاده از متد data در اینجا نیز وجود خواهد داشت.
مطالب
آشنایی با CLR: قسمت دوازدهم
متادیتاها شامل بلوکی از داده‌های باینری هستند که شامل چندین جدول شده و جدول‌ها نیز به سه دسته تقسیم می‌شوند:
  1. جداول تعاریف Definition Table
  2. جداول ارجاع References Table
  3. جداول manifest

جداول تعریف

جدول زیر تعدادی از جداول تعریف‌ها را توضیح می‌دهد:
 ModuleDef  شامل آدرس یا مدخلی است که ماژول در آن تعریف شده است. این آدرس شامل نام ماژول به همراه پسوند آن است؛ بدون ذکر مسیر. در صورتی که کامپایل به صورت GUID انجام گرفته باشد، Version ID ماژول هم همراه آن‌ها خواهد بود. در صورتیکه نام فایل تغییر کند، این جدول باز نام اصلی ماژول را به همراه خواهد داشت. هر چند تغییر نام فایل به شدت رد شده و ممکن است باعث شود CLR نتواند در زمان اجرا آن را پیدا کند.
 TypeDef  شامل یک مدخل ورودی برای هر نوعی است که تعریف شده است. هر آدرس ورودی شامل نام نوع ، پرچمها (همان مجوز‌های public و private و ...) می‌باشد. همچنین شامل اندیس هایی به متدها است که شامل جدول MethodDef می‌باشند یا فیلدهایی که شامل جدول FieldDef می‌باشند و الی آخر...
 MethodDef  شامل آدرسی برای هر متد تعریف شده در ماژول است که شامل  نام متد و پرچم هاست. همچنین شامل امضای متد و نقطه‌ی آغاز کد IL آن در ماژول هم می‌شود و آن آدرس هم میتواند ارجاعی به جدول ParamDef جهت شناسایی پارامترها باشد.
 FieldDef  شامل اطلاعاتی در مورد فیلدهاست که این اطلاعات ، پرچم، نام و نوع فیلد را مشخص می‌کنند.
 ParamDef  حاوی اطلاعات پارامتر متدهاست که این اطلاعات شامل پرچم‌ها (in , out ,retval) ، نوع و نام است.
 PropertyDef   برای هر پراپرتی یا خصوصیت، شامل یک آدرس است که شامل نام، نوع و پرچم می‌شود.
 EventDef  برای هر رویداد شامل یک آدرس است که این آدرس شامل نام و نوع است.

جداول ارجاعی
موقعی که کد شما کامپایل می‌شود، اگر شما به اسمبلی دیگری ارجاع داشته باشید، از جداول ارجاع کمک گرفته می‌شود که در جدول زیر تعدادی از این جداول فهرست شده‌اند:
 AssemblyRef  شامل آدرس اسمبلی است که ماژولی به آن ارجاع داده است و این آدرس شامل اطلاعات ضروری جهت اتصال به اسمبلی می‌شود و این اطلاعات شامل نام اسمبلی (بدون ذکر پسوند و مسیر)، شماره نسخه اسمبلی، سیستم فرهنگی و منطقه‌ای تعیین شده اسمبلی culture و یک کلید عمومی که عموما توسط ناشر ایجاد می‌گردد که هویت ناشر آن اسمبلی را مشخص می‌کند. هر آدرس شامل یک پرچم و یک کد هش هست که بری ارزیابی از صحت و بی خطا بودن بیت‌های اسمبلی ارجاع شده Checksum استفاده می‌شود.
 ModuleRef  شامل یک آدرس ورودی به هدر PE ماژول است به نوع‌های پیاده سازی شده آن ماژول در آن اسمبلی. هر آدرس شامل نام فایل و پسوند آن بدون ذکر مسیر است. این جدول برای اتصال به نوع‌هایی استفاده می‌شود که در یک ماژول متفاوت از ماژول اسمبلی صدا زده شده پیاده سازی شده است.
 TypeRef  شامل یک آدرس یا ورودی برای هر نوعی است که توسط ماژول ارجاع داده شده است. هر آدرس شامل نام نوع و آدرسی است که نوع در آن جا قرار دارد. اگر این نوع داخل نوع دیگری پیاده سازی شود، ارجاعات به سمت یک جدول TypeDef خواهد بود. اگر نوع داخل همان ماژول تعریف شده باشد، ارجاع به سمت جدول ModuleDef خواهد بود و اگر نوع در ماژول دیگری از آن اسمبلی پیاده سازی شده باشد، ارجاع به سمت یک جدول ModuleRef خواهد بود و اگر نوع در یک اسمبلی جداگانه تعریف شده باشد، ارجاع به جدول AssemblyRef خواهد بود.
 MemberRef  شامل یک آدرس ورودی برای هر عضو (فیلد و متدها و حتی پراپرتی و رویدادها) است که توسط آن آن ماژول ارجاع شده باشد. هر آدرس شامل نام عضو، امضاء و یک اشاره‌گر به جدول TypeRef است، برای نوع‌هایی که به تعریف عضو پرداخته‌اند. 
البته جداولی که در بالا هستند فقط تعدادی از آن جداول هستند، ولی قصد ما تنها یک آشنایی کلی با جداول هر قسمت بود. در مورد جداول manifest بعد‌تر صحبت می‌کنیم.
ابزارهای متنوع و زیادی هستند که برای بررسی و آزمایش متادیتاها استفاده می‌شوند. یکی از این ابزارها ILDasm.exe می‌باشد. برای دیدن متادیتاهای یک فایل اجرایی فرمان زیر را صادر کنید:
ILDasm Program.exe
صدور فرمان بالا باعث اجرای ILDasm و بارگزاری اسمبلی‌های program.exe می‌شود. برای مشاهده‌ی اطلاعات جداول متا به صورت شکیل و قابل خواندن برای انسان، در منوی برنامه، مسیر زیر را طی کنید:
View/MetaInfo/Show
با طی کردن گزینه‌های بالا، اطلاعات به صورت زیر نمایش داده می‌شوند:
===========================================================
ScopeName : Program.exe
MVID : {CA73FFE8­0D42­4610­A8D3­9276195C35AA}
===========================================================
Global functions
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Global fields
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Global MemberRefs
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
TypeDef #1 (02000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
TypDefName: Program (02000002)
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]
[BeforeFieldInit] (00100101)
Extends : 01000001 [TypeRef] System.Object
Method #1 (06000001) [ENTRYPOINT]
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
MethodName: Main (06000001)
Flags : [Public] [Static] [HideBySig] [ReuseSlot] (00000096)
RVA : 0x00002050
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
No arguments.
Method #2 (06000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
MethodName: .ctor (06000002)
Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName]
[RTSpecialName] [.ctor] (00001886)
RVA : 0x0000205c
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
TypeRef #1 (01000001)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x01000001
ResolutionScope: 0x23000001
TypeRefName: System.Object
MemberRef #1 (0a000004)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Member: (0a000004) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
TypeRef #2 (01000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x01000002
ResolutionScope: 0x23000001
TypeRefName: System.Runtime.CompilerServices.CompilationRelaxationsAttribute
MemberRef #1 (0a000001)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Member: (0a000001) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
1 Arguments
Argument #1: I4
TypeRef #3 (01000003)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x01000003
ResolutionScope: 0x23000001
TypeRefName: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute
MemberRef #1 (0a000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Member: (0a000002) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
TypeRef #4 (01000004)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x01000004
ResolutionScope: 0x23000001
TypeRefName: System.Console
MemberRef #1 (0a000003)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Member: (0a000003) WriteLine:
CallCnvntn: [DEFAULT]
ReturnType: Void
1 Arguments
Argument #1: String
Assembly
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x20000001
Name : Program
Public Key :
Hash Algorithm : 0x00008004
Version: 0.0.0.0
Major Version: 0x00000000
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
Flags : [none] (00000000)
CustomAttribute #1 (0c000001)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
CustomAttribute Type: 0a000001
CustomAttributeName:
System.Runtime.CompilerServices.CompilationRelaxationsAttribute ::
instance void .ctor(int32)
Length: 8
Value : 01 00 08 00 00 00 00 00 > <
ctor args: (8)
CustomAttribute #2 (0c000002)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
CustomAttribute Type: 0a000002
CustomAttributeName: System.Runtime.CompilerServices.RuntimeCompatibilityAttribute ::
instance void .ctor()
Length: 30
Value : 01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78 > T WrapNonEx<
: 63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01 >ceptionThrows <
ctor args: ()
AssemblyRef #1 (23000001)
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
Token: 0x23000001
Public Key or Token: b7 7a 5c 56 19 34 e0 89
Name: mscorlib
Version: 4.0.0.0
Major Version: 0x00000004
Minor Version: 0x00000000
Build Number: 0x00000000
Revision Number: 0x00000000
Locale: <null>
HashValue Blob:
Flags: [none] (00000000)
User Strings
­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­
70000001 : ( 2) L"Hi"
Coff symbol name overhead: 0
لازم نیست که تمامی اطلاعات بالا را به طور کامل بفهمید. همین که متوجه شوید برنامه شامل  TypeDef است که نام آن Program است و این نوع به صورت یک کلاس عمومی sealed است که از نوع system.object ارث بری کرده است (یک نوع ارجاع از اسمبلی دیگر) و برنامه شامل دو متد main و یک سازنده ctor. است، کافی هست.
متد Main یک متد عمومی و ایستا static است که شامل کد IL است و هیچ خروجی ندارد و هیچ آرگومانی را نمی‌پزیرد. متد سازنده عمومی است و شامل کد IL است، سازنده هیچ نوع خروجی ندارد و هیچ آرگومانی هم نمی‌پذیرد و یک اشاره‌گر که به یک object در حافظه که موقع صدا زدن ساخته خواهد شد.
ابزار ILDasm امکاناتی بیشتری از آنچه که دیدید ارائه می‌کند. به عنوان نمونه اگر مسیر زیر را در منوها طی کنید:
View/statistics
اطلاعات آماری زیر نمایش داده می‌شود:
File size : 3584
PE header size : 512 (496 used) (14.29%)
PE additional info : 1411 (39.37%)
Num.of PE sections : 3
CLR header size : 72 ( 2.01%)
CLR meta­data size : 612 (17.08%)
CLR additional info : 0 ( 0.00%)
CLR method headers : 2 ( 0.06%)
Managed code : 20 ( 0.56%)
Data : 2048 (57.14%)
Unaccounted : ­1093 (­30.50%)
Num.of PE sections : 3
.text ­ 1024
.rsrc ­ 1536
.reloc ­ 512
CLR meta­data size : 612
Module ­ 1 (10 bytes)
TypeDef ­ 2 (28 bytes) 0 interfaces, 0 explicit layout
TypeRef ­ 4 (24 bytes)
MethodDef ­ 2 (28 bytes) 0 abstract, 0 native, 2 bodies
MemberRef ­ 4 (24 bytes)
CustomAttribute­ 2 (12 bytes)
Assembly ­ 1 (22 bytes)
AssemblyRef ­ 1 (20 bytes)
Strings ­ 184 bytes
Blobs ­ 68 bytes
UserStrings ­ 8 bytes
Guids ­ 16 bytes
Uncategorized ­ 168 bytes
CLR method headers : 2
Num.of method bodies ­ 2
Num.of fat headers ­ 0
Num.of tiny headers ­ 2
Managed code : 20
Ave method size ­ 10
اطلاعات بالا شامل نمایش حجم فایل به بایت و سایر قسمت‌های تشکیل دهنده فایل است...
توجه: ILDasm یک باگ دارد که بر نمایش اندازه‌ی فایل تاثیر می‌گذارد و باعث می‌شود شما نتوانید به اطلاعات ثبت شده اعتماد داشته باشید.
مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی 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)



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