مطالب
Roslyn #3
بررسی Syntax tree

زمانیکه صحبت از Syntax می‌شود، منظور نمایش متنی سورس کدها است. برای بررسی و آنالیز آن، نیاز است این نمایش متنی، به ساختار داده‌ای ویژه‌ای به نام Syntax tree تبدیل شود و این Syntax tree مجموعه‌ای است از tokenها. Tokenها بیانگر المان‌های مختلف یک زبان، شامل کلمات کلیدی، عملگرها و غیره هستند.


در تصویر فوق، مراحل تبدیل یک قطعه کد #C را به مجموعه‌ای از tokenهای معادل آن مشاهده می‌کنید. علاوه بر این‌ها، Roslyn syntax tree شامل موارد ویژه‌ای به نام Trivia نیز هست. برای مثال در حین نوشتن کدها، در ابتدای سطرها تعدادی space یا tab وجود دارند و یا در این بین ممکن است کامنتی نوشته شود. هرچند این موارد از دیدگاه یک کامپایلر بی‌معنا هستند، اما ابزارهای Refactoring ایی که به Trivia دقت نداشته باشند، خروجی کد به هم ریخته‌ای را تولید خواهند کرد و سبب سردرگمی استفاده کنندگان می‌شوند.


در تصویر فوق، اشاره‌گر ادیتور پس از تایپ semicolon قرار گرفته‌است. در این حالت می‌توانید دو نوع trivia مخصوص فضای خالی و کامنت‌ها را در syntax visualizer، مشاهده کنید.
به علاوه پس از هر token بازه‌ای از اعداد را مشاهده می‌کنید که بیانگر محل قرارگیری آن‌ها در سورس کد هستند. این محل‌ها جهت ارائه‌ی خطاهای دقیق مرتبط با آن نقاط، بسیار مفید هستند.
یک Syntax tree از مجموعه‌ای از syntax nodes تشکیل می‌شود و هر node شامل مواردی مانند تعاریف، عبارات و امثال آن است. در افزونه‌ی Syntax visualizer نودهایی که رنگ قرمز متمایل به قهوه‌ای دارند، بیانگر نودهای Trivia، نودهای آبی، Syntax nodes و نودهای سبز، Syntax token هستند.


مفاهیم این رنگ‌ها را با کلیک بر روی دکمه‌ی Legend هم می‌توان مشاهده کرد.


تفاوت Syntax با Semantics

در Roslyn امکان کار با Syntax و Semantics کدها وجود دارد.
یک Syntax، از گرامر زبان خاصی پیروی می‌کند. در Syntax اطلاعات بسیار زیادی وجود دارند که معنای برنامه را تغییر نمی‌دهند؛ مانند کامنت‌ها، فضاهای خالی و فرمت ویژه‌ی کدها. البته فضاهای خالی در زبان‌هایی مانند پایتون دارای معنا هستند؛ اما در سی‌شارپ خیر. همچنین در Syntax، توافق نامه‌ای وجود دارد که بیانگر تعدادی واژه‌ی از پیش رزرو شده، مانند کلمات کلیدی هستند.
اما Semantics در نقطه‌ی مقابل Syntax قرار می‌گیرد و بیانگر معنای سورس کد است. برای مثال در اینجا تقدم و تاخر عملگرها مفهوم پیدا می‌کنند و یا اینکه Type system چیست و چه نوع‌هایی را می‌توان به دیگری نسبت داد و تبدیل کرد. عملیات Binding در این مرحله رخ می‌دهد و مفهوم identifierها را مشخص می‌کند. برای مثال x در این قسمت از سورس کد، به چه معنایی است و به کجا اشاره می‌کند؟


خواص ویژه‌ی Syntax tree در Roslyn

- تمام اجزای کد را شامل عناصر سازنده‌ی زبان و همچنین Trivia، به همراه دارد.
- API آن توسط کتابخانه‌های ثالث قابل دسترسی است.
- Immutable طراحی شده‌است. به این معنا که زمانیکه syntax tree توسط Roslyn ایجاد شد، دیگر تغییر نمی‌کند. به این ترتیب امکان دسترسی همزمان و موازی به آن بدون نیاز به انواع قفل‌های مسایل همزمانی وجود دارد. اگر کتابخانه‌ی ثالثی به Syntax tree ارائه شده دسترسی پیدا می‌کند، می‌تواند کاملا مطمئن باشد که این اطلاعات دیگر تغییری نمی‌کنند و نیازی به قفل کردن آن‌ها نیست. همچنین این مساله امکان استفاده‌ی مجدد از sub treeها را در حین ویرایش کدها میسر می‌کند. به آن‌ها mutating trees نیز گفته می‌شود.
- مقاوم است در برابر خطاها. اگر از قسمت اول به خاطر داشته باشید، Roslyn می‌بایستی جایگزین کامپایلر دومی به نام کامپایلر پس زمینه‌ی ویژوال استودیو که خطوط قرمزی را ذیل سطرهای مشکل دار ترسیم می‌کند، نیز می‌شد. فلسفه‌ی طراحی این کامپایلر، مقاوم بودن در برابر خطاهای تایپی و هماهنگی آن با تایپ کدها توسط برنامه نویس بود. Syntax tree در Roslyn نیز چنین خاصیتی را دارد و اگر مشغول به تایپ شوید، باز هم کار کرده و اینبار خطاهای موجود را نمایش می‌دهد که می‌تواند توسط ابزارهای نمایش دهنده‌ی ویژوال استودیو یا سایر ابزارهای ثالث استفاده شود.


برای نمونه در تصویر فوق، تایپ semicolon فراموش شده‌است؛ اما همچنان Syntax tree در دسترس است و به علاوه گزارش می‌دهد که semicolon مفقود است و تایپ نشده‌است.


Parse سورس کد توسط Roslyn

ابتدا یک پروژه‌ی کنسول ساده‌ی دات نت 4.6 را در VS 2015 آغاز کنید. سپس از طریق خط فرمان نیوگت، دستور ذیل را صادر نمائید:
 PM> Install-Package Microsoft.CodeAnalysis
به این ترتیب API لازم جهت کار با Roslyn به پروژه اضافه خواهند شد.
سپس کدهای ذیل را به آن اضافه کنید:
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
 
namespace Roslyn01
{
    class Program
    {
        static void Main(string[] args)
        {
            parseText();
        }
 
        static void parseText()
        {
            var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar(int x) {} }");
            Console.WriteLine(tree.ToString());
            Console.WriteLine(tree.GetRoot().NormalizeWhitespace().ToString());
 
            var res = SyntaxFactory.ClassDeclaration("Foo")
                .WithMembers(SyntaxFactory.List<MemberDeclarationSyntax>(new[] {
                    SyntaxFactory.MethodDeclaration(
                        SyntaxFactory.PredefinedType(
                            SyntaxFactory.Token(SyntaxKind.VoidKeyword)
                        ),
                        "Bar"
                    )
                    .WithBody(SyntaxFactory.Block())
                }))
                .NormalizeWhitespace();
 
            Console.WriteLine(res);
        } 
    }
}
توضیحات:
کار Parse سورس کد دریافتی، بر اساس سرویس‌های زبان متناظر با آن‌ها آغاز می‌شود. برای مثال سرویس‌هایی مانند VisualBasicSyntaxTree و یا CSharpSyntaxTree مثال فوق که سورس کد مورد آنالیز آن، از نوع سی‌شارپ است.
این کلاس‌های Factory، دارای دو متد Create و ParseText هستند. کار متد ParseText آن مشخص است؛ یک قطعه‌ی متنی از کد را آنالیز کرده و معادل Syntax Tree آن‌را تولید می‌کند. متد Create آن، اشیایی مانند نودهای Syntax visualizer را دریافت کرده و بر اساس آن‌ها یک Syntax tree را تولید می‌کند.
کار با متد Create آنچنان ساده نیست. به همین جهت یکی از اعضای تیم Roslyn برنامه‌ای را به نام Roslyn Quoter ایجاد کرده‌است که نسخه‌ی آنلاین آن‌را در اینجا و سورس کد آن‌را در اینجا می‌توانید بررسی کنید.
جهت آزمایش، همان قطعه‌ی متنی سورس کد مثال فوق را در نسخه‌ی آنلاین آن جهت آنالیز و تولید ورودی متد Create، وارد کنید. خروجی آن‌را می‌توان مستقیما در متد Create بکار برد.


فرمت کردن خودکار کدها به کمک Roslyn

اگر بر روی tree حاصل، متد ToString را فراخوانی کنیم، خروجی آن مجددا سورس کد مورد آنالیز است. اگر علاقمند بودید که Roslyn به صورت خودکار کدهای ورودی را فرمت کند و تمام آن‌ها را در یک سطر نمایش ندهد، متد NormalizeWhitespace را بر روی ریشه‌ی Syntax tree فراخوانی کنید:
 tree.GetRoot().NormalizeWhitespace().ToString()
اینبار خروجی فراخوانی فوق به صورت ذیل است:
class Foo
{
    void Bar(int x)
    {
    }
}


کوئری گرفتن از سورس کد توسط Roslyn

در ادامه قصد داریم با سه روش مختلف کوئری گرفتن از Syntax tree، آشنا شویم. برای این منظور متد ذیل را به پروژه‌ای که در ابتدای برنامه آغاز کردیم، اضافه کنید:
static void querySyntaxTree()
{
    var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar() {} }");
    var node = (CompilationUnitSyntax)tree.GetRoot();
 
    // Using the object model
    foreach (var member in node.Members)
    {
        if (member.Kind() == SyntaxKind.ClassDeclaration)
        {
            var @class = (ClassDeclarationSyntax)member;
 
            foreach (var member2 in @class.Members)
            {
                if (member2.Kind() == SyntaxKind.MethodDeclaration)
                {
                    var method = (MethodDeclarationSyntax)member2;
                    // do stuff
                }
            }
        }
    }
 
 
    // Using LINQ query methods
    var bars = from member in node.Members.OfType<ClassDeclarationSyntax>()
               from member2 in member.Members.OfType<MethodDeclarationSyntax>()
               where member2.Identifier.Text == "Bar"
               select member2;
    var res = bars.ToList();
 
 
    // Using visitors
    new MyVisitor().Visit(node);
}
توضیحات:
روش اول کوئری گرفتن از Syntax tree، استفاده از object model آن است. در اینجا هربار، نوع و Kind هر نود را بررسی کرده و در نهایت به اجزای مدنظر خواهیم رسید. شروع کار هم با دریافت ریشه‌ی syntax tree توسط متد GetRoot و تبدیل نوع آن نود به CompilationUnitSyntax می‌باشد.
روش دوم استفاده از روش LINQ است؛ با توجه به اینکه ساختار یک Syntax tree بسیار شبیه است به LINQ to XML. در اینجا یک سری نود، ریشه و فرزندان آن‌ها را داریم که با روش LINQ بسیار سازگار هستند. برای نمونه در مثال فوق، در ریشه‌ی Parse شده، در تمام کلاس‌های آن، به دنبال متد یا متدهایی هستیم که نام آن‌ها Bar است.
و در نهایت روش مرسوم و متداول کار با Syntax trees، استفاده از الگوی Visitors است. همانطور که در کدهای دو روش قبل مشاهده می‌کنید، باید تعداد زیادی حلقه و if و else نوشت تا به جزء و المان مدنظر رسید. راه ساده‌تری نیز برای مدیریت این پیچیدگی وجود دارد و آن استفاده از الگوی Visitor است. کار این الگو ارائه‌ی متدهایی قابل override شدن است و فراخوانی آن‌ها، در طی حلقه‌هایی پشت صحنه که این Visitor را اجرا می‌کنند، صورت می‌گیرد. بنابراین در اینجا دیگر برای رسیدن به یک متد، حلقه نخواهید نوشت. تنها کاری که باید صورت گیرد، override کردن متد Visit المانی خاص در Syntax tree است.
هر نود در syntax tree دارای متدی است به نام Accept که یک Visitor را دریافت می‌کند. همچنین Visitorهای نوشته شده نیز دارای متد Visit یک نود هستند.
نمونه‌ای از این Visitors را در کلاس ذیل مشاهده می‌کنید:
class MyVisitor : CSharpSyntaxWalker
{
    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        if (node.Identifier.Text == "Bar")
        {
            // do stuff
        }
 
        base.VisitMethodDeclaration(node);
    }
}
در اینجا برای رسیدن به تعاریف متدها دیگر نیازی نیست تا حلقه نوشت. بازنویسی متد VisitMethodDeclaration، دقیقا همین کار را انجام می‌دهد و در طی پروسه‌ی Visit یک Syntax tree، اگر متدی در آن تعریف شده باشد، متد VisitMethodDeclaration حداقل یکبار فراخوانی خواهد شد.
کلاس پایه‌ی CSharpSyntaxWalker از کلاس CSharpSyntaxVisitor مشتق شده‌است و به تمام امکانات آن دسترسی دارد. علاوه بر آن‌ها، کلاس CSharpSyntaxWalker به Tokens و Trivia نیز دسترسی دارد.
نحوه‌ی استفاده از Visitor سفارشی نوشته شده نیز به صورت ذیل است:
 new MyVisitor().Visit(node);
در اینجا متد Visit این Visitor را بر روی نود ریشه‌ی Syntax tree اجرا کرده‌ایم.
مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی 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)



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
EF Code First #8
سلام در قسمت Self Referencing Entity اگر بخواهیم کلید خارجی نام دیگری داشته باشد طبق گفته شما که عمل کردم خطای Sequence contains no elements را میدهد.
using System.Collections.Generic;
 
namespace EF_Sample04.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
 
        public int? ManagerID { get; set; }
        public virtual Employee Manager { get; set; }       
    }
}
using EF_Sample04.Models;
using System.Data.Entity.ModelConfiguration; 
namespace EF_Sample04.Mappings
{
    public class EmployeeConfig : EntityTypeConfiguration<Employee>
    {
        public EmployeeConfig()
        {
            this.HasOptional(x => x.Manager)
                .WithMany()
                .HasForeignKey(x => x.ManagerID)
                .WillCascadeOnDelete(false);
        }
    }
}
مطالب
اشتباهات متداول برنامه‌نویس‌های دات نت

اشتباه 1:
استفاده از

throw ex;
بجای استفاده از
throw;
در حالت اول، تمام stack trace موجود تا نقطه‌ی فراخوانی دستور ذکر شده پاک خواهند شد، اما در حالت دوم stack trace‌ حفظ شده و دیباگ کردن کد را ساده می‌کند.

اشتباه 2:
درک اشتباه عملکرد متد replace :
string s = "Take this out";
s.Replace("this", "that"); //wrong
بجای استفاده از :
s = s.Replace("this", "that");  //correct

اگر از fxCop استفاده کنید، اینگونه خطاها را (عدم استفاده از مقدار بازگشتی) گوشزد می‌کند.

اشتباه 3:
استفاده‌ی بی دقت از متغیرهای استاتیک در یک برنامه وب. دو مثال زیر را در نظر بگیرید:
public static string GetCookieName(Cookie c)
{
return c.Name;
}

static List<string> cookieList = new List<string>();
public static void AddToCookieList(Cookie c)
{
cookieList.Add(c.Name);

}

برنامه‌های وب ذاتا چند ریسمانی هستند و زمانیکه یک متغیر را از نوع استاتیک تعریف می‌کنید، این متغیر، هنگام مراجعه‌ی کاربران بین آن‌ها به اشتراک گذاشته می‌شود و امکان تخریب یا استفاده‌ی ناصحیح از مقادیر آن‌ها وجود خواهد داشت. در حالت اول نیازی به مباحث همزمانی و قفل کردن منابع نیست زیرا متغیری که در متد استفاده می‌شود، thread safe است اما cookieList در مثال دوم خیر و حتما هنگام استفاده از آن باید مباحث قفل کردن منابع را درنظر داشت. (حتی اگر برنامه‌ی شما از نوع وبی هم نیست اما چند ریسمانی است این مطلب باز هم صادق می‌باشد)


اشتباه 4:
مقابله با خطاها به شکلی نادرست: (اصطلاحا خفه کردن خطاها!)
 try
{
//something
File.Delete(blah);
}
catch{}
در این حالت اگر خطایی رخ دهد کاربر متوجه نخواهد شد دقیقا مشکلی وجود داشته یا خیر و علت آن چیست.
هرچند گاهی از اوقات اگر خطای حاصل برای ما اهمیتی نداشت می‌توان از آن استفاده نمود، در غیراینصورت باید حتما از این روش پرهیز کرد.

اشتباه 5:
ارائه‌ی برنامه‌های ASP.Net با گزینه‌ی پیش فرض Debug=true در web.config که پیشتر در مورد آن در این سایت بحث شده است.


اشتباه 6:
عدم استفاده از امکانات ویژه‌ی دات نت فریم ورک هنگام کار با رشته‌ها:
string s = "This ";
s += "is ";
s += "not ";
s += "the ";
s += "best ";
s += "way.";

StringBuilder sb = new StringBuilder();
sb.Append("This ");
sb.Append("is ");
sb.Append("much ");
sb.Append("better. ");

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

اشتباه 7:
عدم استفاده از عبارت using هنگام استفاده از اشیایی از نوع Idisposable . مثالی در این مورد.

using (StreamReader reader=new StreamReader(file))
{
//your code here

}

اشتباه 8:
فراموش کردن بررسی نال بودن یک شیء هنگام استفاده از آن.
string val = Session["xyz"].ToString();

این نوع کد نویسی یکی از اشتباهات متداول تمامی تازه واردان به ASP.Net است. حتما باید پیش از استفاده از متد ToString بررسی شود که آیا این سشن نال است یا نه. در غیراینصورت حاصل کار فقط یک exception خواهد بود. (استفاده از افزونه‌ی ری شارپر در این موارد کمک بزرگی است، زیرا به محض قرار گرفتن مکان نما روی شیءایی که احتمال نال بودن آن میسر است، یک راهنما را به شما ارائه خواهد کرد)

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

مطالب
بررسی تغییرات Blazor 8x - قسمت هفتم - امکان تعریف جزیره‌های تعاملی Blazor WASM
در قسمت‌های قبل، نحوه‌ی تعریف جزیره‌های تعاملی Blazor Server را به همراه نکات مرتبط با آن‌ها بررسی کردیم. برای مثال مشاهده کردیم که چون Blazor Server و SSR هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی دارند و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت. در Blazor 8x، امکان تعریف جزیره‌های تعاملی Blazor WASM نیز وجود دارد که به همراه تعدادی نکته‌ی ویژه، در مورد نحوه‌ی مدیریت سرویس‌های مورد استفاده‌ی در این کامپوننت‌ها است.


معرفی برنامه‌ی Blazor WASM این مطلب

در این مطلب قصد داریم دقیقا قسمت جزیره‌ی تعاملی Blazor Server همان برنامه‌ی مطلب قبل را توسط یک جزیره‌ی تعاملی Blazor WASM بازنویسی کنیم و با نکات و تفاوت‌های ویژه‌ی آن آشنا شویم. یعنی زمانیکه صفحه‌ی SSR نمایش جزئیات یک محصول ظاهر می‌شود، نحوه‌ی رندر و پردازش کامپوننت نمایش محصولات مرتبط و مشابه، اینبار یک جزیره‌ی تعاملی Blazor WASM باشد. بنابراین قسمت عمده‌ای از کدهای این دو قسمت یکی است؛ فقط نحوه‌ی دسترسی به سرویس‌ها و محل قرارگیری تعدادی از فایل‌ها، متفاوت خواهد بود.


ایجاد یک پروژه‌ی جدید Blazor WASM تعاملی در دات نت 8

بنابراین در ادامه، در ابتدای کار نیاز است یک پوشه‌ی جدید را برای این پروژه، ایجاد کرده و بجای انتخاب interactivity از نوع Server:
dotnet new blazor --interactivity Server
اینبار برای اجرای در مرورگر توسط فناوری وب‌اسمبلی، نوع WebAssembly را انتخاب کنیم:
dotnet new blazor --interactivity WebAssembly
در این حالت، Solution ای که ایجاد می‌شود، به همراه دو پروژه‌‌است (برخلاف پروژه‌های Blazor Server تعاملی که فقط شامل یک پروژه‌ی سمت سرور هستند):
الف) یک پروژه‌ی سمت سرور (برای تامین backend و API و سرویس‌های مرتبط)
ب) یک پروژه‌ی سمت کلاینت (برای اجرای مستقیم درون مرورگر کاربر؛ بدون داشتن وابستگی مستقیمی به اجزای برنامه‌ی سمت سرور)

این ساختار، خیلی شبیه به ساختار پروژه‌های نگارش قبلی Blazor از نوع Hosted Blazor WASM است که در آن، یک پروژه‌ی ASP.NET Core هاست کننده‌ی پروژه‌ی Blazor WASM وجود دارد و یکی از کارهای اصلی آن، فراهم ساختن Web API مورد استفاده‌ی در پروژه‌ی WASM است.

در حالتیکه نوع تعاملی بودن پروژه را Server انتخاب کنیم (مانند مثال قسمت پنجم)، فایل Program.cs آن به همراه دو تعریف مهم زیر است که امکان تعریف کامپوننت‌های تعاملی سمت سرور را میسر می‌کنند:
// ...

builder.Services.AddRazorComponents()
       .AddInteractiveServerComponents();

// ...

app.MapRazorComponents<App>()
   .AddInteractiveServerRenderMode();
مهم‌ترین قسمت‌های آن، متدهای AddInteractiveServerComponents و AddInteractiveServerRenderMode هستند که server-side rendering را به همراه امکان داشتن کامپوننت‌های تعاملی، ممکن می‌کنند.

این تعاریف در فایل Program.cs (پروژه‌ی سمت سرور) قالب جدید Blazor WASM به صورت زیر تغییر می‌کنند تا امکان تعریف کامپوننت‌های تعاملی سمت کلاینت از نوع وب‌اسمبلی، میسر شود:
// ...

builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents();

// ...

app.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(Counter).Assembly);

نیاز به تغییر معماری برنامه جهت کار با جزایر Blazor WASM

همانطور که در قسمت پنجم مشاهده کردیم، تبدیل کردن یک کامپوننت Blazor، به کامپوننتی تعاملی برای اجرای در سمت سرور، بسیار ساده‌است؛ فقط کافی است rendermode@ آن‌را به InteractiveServer تغییر دهیم تا ... کار کند. اما تبدیل همان کامپوننت نمایش محصولات مرتبط، به یک جزیره‌ی وب‌اسمبلی، نیاز به تغییرات قابل ملاحظه‌ای را دارد؛ از این لحاظ که اینبار این قسمت قرار است بر روی مرورگر کاربر اجرا شود و نه بر روی سرور. در این حالت دیگر کامپوننت ما دسترسی مستقیمی را به سرویس‌های سمت سرور ندارد و برای رسیدن به این مقصود باید از یک Web API در سمت سرور کمک بگیرد و برای کار کردن با آن API در سمت کلاینت، از سرویس HttpClient استفاده کند. به همین جهت، پیاده سازی معماری این روش، نیاز به کار بیشتری را دارد:


همانطور که ملاحظه می‌کنید، برای فعالسازی یک جزیره‌ی تعاملی وب‌اسمبلی، نمی‌توان کامپوننت RelatedProducts آن‌را مستقیما در پروژه‌ی سمت سرور قرار داد و باید آن‌را به پروژه‌ی سمت کلاینت منتقل کرد. در ادامه پیاده سازی کامل این پروژه را با توجه به این تغییرات بررسی می‌کنیم.


مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول

از این جهت که مدل برنامه (که در قسمت پنجم معرفی شد) در دو پروژه‌ی Client و سرور قابل استفاده‌است، به همین جهت مرسوم است یک پروژه‌ی سوم Shared را نیز به جمع دو پروژه‌ی جاری solution اضافه کرد و فایل این مدل را در آن قرار داد. بنابراین این فایل را از پوشه‌ی Models پروژه‌ی سرور به پوشه‌ی Models پروژه‌ی جدید BlazorDemoApp.Shared در مسیر جدید BlazorDemoApp.Shared\Models\Product.cs منتقل می‌کنیم. مابقی کدهای آن با قسمت پنجم تفاوتی ندارد.
سپس به فایل csproj. پروژه‌ی کلاینت مراجعه کرده و ارجاعی را به پروژه‌ی جدید BlazorDemoApp.Shared اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorDemoApp.Shared\BlazorDemoApp.Shared.csproj" />
  </ItemGroup>

</Project>
نیازی نیست تا اینکار را برای پروژه‌ی سرور نیز تکرار کنیم؛ از این جهت که ارجاعی به پروژه‌ی کلاینت، در پروژه‌ی سرور وجود دارد که سبب دسترسی به این پروژه‌ی Shared هم می‌شود.


سرویس برنامه: سرویسی برای بازگشت لیست محصولات

چون Blazor Server و صفحات SSR آن هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی وجود داشته و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت. یعنی هنوز هم همان مسیر قبلی سرویس Services\ProductStore.cs در این پروژه‌ی سمت سرور نیز برقرار است و نیازی به تغییر مسیر آن نیست. البته بدیهی است چون این پروژه جدید است، باید این سرویس را در فایل Program.cs برنامه‌ی سمت سرور به صورت زیر معرفی کرد تا در فایل razor برنامه‌ی آن قابل دسترسی شود:
builder.Services.AddScoped<IProductStore, ProductStore>();


تکمیل فایل Imports.razor_ پروژه‌ی سمت سرور

جهت سهولت کار با برنامه، یک سری مسیر و using را نیاز است به فایل Imports.razor_ پروژه‌ی سمت سرور اضافه کرد:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
// ...
@using BlazorDemoApp.Client.Components.Store
@using BlazorDemoApp.Client.Components
سطر اول سبب می‌شود تا بتوان به سادگی به اعضای کلاس استاتیک RenderMode، در برنامه‌ی سمت سرور دسترسی یافت. دو using جدید دیگر سبب سهولت دسترسی به کامپوننت‌های قرارگرفته‌ی در این مسیرها در صفحات SSR برنامه‌ی سمت سرور می‌شوند.


تکمیل صفحه‌ی نمایش لیست محصولات

کدها و مسیر کامپوننت ProductsList.razor، با قسمت پنجم دقیقا یکی است. این صفحه، یک صفحه‌ی SSR بوده و در همان سمت سرور اجرا می‌شود و دسترسی آن به سرویس‌های سمت سرور نیز ساده بوده و همانند قبل است.


تکمیل صفحه‌ی نمایش جزئیات یک محصول

کدها و مسیر کامپوننت ProductDetails.razor، با قسمت پنجم دقیقا یکی است. این صفحه، یک صفحه‌ی SSR بوده و در همان سمت سرور اجرا می‌شود و دسترسی آن به سرویس‌های سمت سرور نیز ساده بوده و همانند قبل است ... البته بجز یک تغییر کوچک:
<RelatedProducts ProductId="ProductId" @rendermode="@InteractiveWebAssembly"/>
در اینجا حالت رندر این کامپوننت، به InteractiveWebAssembly تغییر می‌کند. یعنی اینبار قرار است تبدیل به یک جزیره‌ی وب‌اسمبلی شود و نه یک جزیره‌ی Blazor Server که آن‌را در قسمت پنجم بررسی کردیم.


تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط

پس از این توضیحات، به اصل موضوع این قسمت رسیدیم! کامپوننت سمت سرور RelatedProducts.razor قسمت پنجم ، از آنجا cut شده و به مسیر جدید BlazorDemoApp.Client\Components\Store\RelatedProducts.razor منتقل می‌شود. یعنی کاملا به پروژه‌ی وب‌اسمبلی منتقل می‌شود. بنابراین کدهای آن دیگر دسترسی مستقیمی به سرویس دریافت اطلاعات محصولات ندارند و برای اینکار نیاز است در سمت سرور، یک Web API Controller را تدارک ببینیم:
using BlazorDemoApp.Services;
using Microsoft.AspNetCore.Mvc;

namespace BlazorDemoApp.Controllers;

[ApiController]
[Route("/api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductStore _store;

    public ProductsController(IProductStore store) => _store = store;

    [HttpGet("[action]")]
    public IActionResult Related([FromQuery] int productId) => Ok(_store.GetRelatedProducts(productId));
}
این کلاس در مسیر Controllers\ProductsController.cs پروژه‌ی سمت سرور قرار می‌گیرد و کار آن، بازگشت اطلاعات محصولات مشابه یک محصول مشخص است.
برای اینکه مسیریابی این کنترلر کار کند، باید به فایل Program.cs برنامه، مراجعه و سطرهای زیر را اضافه کرد:
builder.Services.AddControllers();
// ...
app.MapControllers();

یک نکته: همانطور که مشاهده می‌کنید، در Blazor 8x، امکان استفاده از دو نوع مسیریابی یکپارچه، در یک پروژه وجود دارد؛ یعنی Blazor routing  و  ASP.NET Core endpoint routing. بنابراین در این پروژه‌ی سمت سرور، هم می‌توان صفحات SSR و یا Blazor Server ای داشت که مسیریابی آن‌ها با page@ مشخص می‌شوند و همزمان کنترلرهای Web API ای را داشت که بر اساس سیستم مسیریابی ASP.NET Core کار می‌کنند.

بر این اساس در پروژه‌ی سمت کلاینت، کامپوننت RelatedProducts.razor باید با استفاده از سرویس HttpClient، اطلاعات درخواستی را از Web API فوق دریافت و همانند قبل نمایش دهد که تغییرات آن به صورت زیر است:

@using BlazorDemoApp.Shared.Models
@inject HttpClient Http

<button class="btn btn-outline-secondary" @onclick="LoadRelatedProducts">Related products</button>

@if (_loadRelatedProducts)
{
    @if (_relatedProducts == null)
    {
        <p>Loading...</p>
    }
    else
    {
        <div class="mt-3">
            @foreach (var item in _relatedProducts)
            {
                <a href="/ProductDetails/@item.Id">
                    <div class="col-sm">
                        <h5 class="mt-0">@item.Title (@item.Price.ToString("C"))</h5>
                    </div>
                </a>
            }
        </div>
    }
}

@code{

    private IList<Product>? _relatedProducts;
    private bool _loadRelatedProducts;

    [Parameter]
    public int ProductId { get; set; }

    private async Task LoadRelatedProducts()
    {
        _loadRelatedProducts = true;
        var uri = $"/api/products/related?productId={ProductId}";
        _relatedProducts = await Http.GetFromJsonAsync<IList<Product>>(uri);
    }

}
و ... همین! اکنون برنامه قابل اجرا است و به محض نمایش صفحه‌ی جزئیات یک محصول انتخابی، کامپوننت RelatedProducts، در حالت وب‌اسمبلی جزیره‌ای اجرا شده و لیست این محصولات مرتبط را نمایش می‌دهد.
در ادامه یکبار برنامه را اجرا می‌کنیم و ... بلافاصله پس از انتخاب صفحه‌ی نمایش جزئیات یک محصول، با خطای زیر مواجه خواهیم شد!
System.InvalidOperationException: Cannot provide a value for property 'Http' on type 'RelatedProducts'.
There is no registered service of type 'System.Net.Http.HttpClient'.


اهمیت درنظر داشتن pre-rendering در حالت جزیره‌های وب‌اسمبلی

استثنائی را که مشاهده می‌کنید، به علت pre-rendering سمت سرور این کامپوننت، رخ‌داده‌است.
زمانیکه کامپوننتی را به این نحو رندر می‌کنیم:
<RelatedProducts ProductId="ProductId" @rendermode="@InteractiveWebAssembly"/>
به صورت پیش‌فرض در آن pre-rendering نیز فعال است؛ یعنی این کامپوننت دوبار رندر می‌شود:
الف) یکبار در سمت سرور تا HTML حداقل قالب آن، به همراه سایر قسمت‌های صفحه‌ی SSR جاری به سمت مرورگر کاربر ارسال شود.
ب) یکبار هم در سمت کلاینت، زمانیکه Blazor WASM بارگذاری شده و فعال می‌شود.

استثنائی را که مشاهده می‌کنیم، مربوط به حالت الف است. یعنی زمانیکه برنامه‌ی ASP.NET Core هاست برنامه، سعی می‌کند کامپوننت RelatedProducts را در سمت سرور رندر کند، اما ... ما سرویس HttpClient را در آن ثبت و فعالسازی نکرده‌ایم. به همین جهت است که عنوان می‌کند این سرویس را پیدا نکرده‌است. برای رفع این مشکل، چندین راه‌حل وجود دارند که در ادامه آن‌ها را بررسی می‌کنیم.


راه‌حل اول: ثبت سرویس HttpClient در سمت سرور

یک راه‌حل مواجه شدن با مشکل فوق، ثبت سرویس HttpClient در فایل Program.cs برنامه‌ی سمت سرور به صورت زیر است:
builder.Services.AddScoped(sp => new HttpClient 
{ 
    BaseAddress = new Uri("http://localhost/") 
});
پس از این تعریف، کامپوننت RelatedProducts، در حالت prerendering ابتدایی سمت سرور هم کار می‌کند و برنامه با استثنائی مواجه نخواهد شد.


راه‌حل دوم: استفاده از polymorphism یا چندریختی

برای اینکار اینترفیسی را طراحی می‌کنیم که قرارداد نحوه‌ی تامین اطلاعات مورد نیاز کامپوننت RelatedProducts را ارائه می‌کند. سپس یک پیاده سازی سمت سرور را از آن خواهیم داشت که مستقیما به بانک اطلاعاتی رجوع می‌کند و همچنین یک پیاده سازی سمت کلاینت را که از HttpClient جهت کار با Web API استفاده خواهد کرد.
از آنجائیکه این قرارداد نیاز است توسط هر دو پروژه‌ی سمت سرور و سمت کلاینت استفاده شود، باید آن‌را در پروژه‌ی Shared قرار داد تا بتوان ارجاعاتی از آن‌را به هر دو پروژه اضافه کرد؛ برای مثال در فایل BlazorDemoApp.Shared\Data\IProductStore.cs به صورت زیر:
using BlazorDemoApp.Shared.Models;

namespace BlazorDemoApp.Shared.Data;

public interface IProductStore
{
    IList<Product> GetAllProducts();
    Product GetProduct(int id);
    Task<IList<Product>?> GetRelatedProducts(int productId);
}
این همان اینترفیسی است که پیشتر در فایل ProductStore.cs سمت سرور تعریف کرده بودیم؛ با یک تفاوت: متد GetRelatedProducts آن async تعریف شده‌است که نمونه‌ی سمت کلاینت آن باید با متد GetFromJsonAsync کار کند که async است.
پیاده سازی سمت سرور این اینترفیس، کاملا مهیا است و فقط نیاز به تغییر زیر را دارد تا با خروجی Task دار هماهنگ شود:
public Task<IList<Product>?> GetRelatedProducts(int productId)
    {
        var product = ProductsDataSource.Single(x => x.Id == productId);
        return Task.FromResult<IList<Product>?>(ProductsDataSource.Where(p => product.Related.Contains(p.Id))
                                                                 .ToList());
    }
و اکشن متد متناظر هم باید به صورت زیر await دار شود تا خروجی صحیحی را ارائه دهد:
[HttpGet("[action]")]
public async Task<IActionResult> Related([FromQuery] int productId) =>
        Ok(await _store.GetRelatedProducts(productId));
همچنین پیشتر سرویس آن در فایل Program.cs برنامه‌ی سمت سرور، ثبت شده‌است و نیاز به نکته‌ی خاصی ندارد.

در ادامه نیاز است یک پیاده سازی سمت کلاینت را نیز از آن تهیه کنیم که در فایل BlazorDemoApp.Client\Data\ClientProductStore.cs درج خواهد شد:
public class ClientProductStore : IProductStore
{
    private readonly HttpClient _httpClient;

    public ClientProductStore(HttpClient httpClient) => _httpClient = httpClient;

    public IList<Product> GetAllProducts() => throw new NotImplementedException();

    public Product GetProduct(int id) => throw new NotImplementedException();

    public Task<IList<Product>?> GetRelatedProducts(int productId) =>
        _httpClient.GetFromJsonAsync<IList<Product>>($"/api/products/related?productId={productId}");
}
در این بین بر اساس نیاز کامپوننت نمایش لیست محصولات مشابه، فقط به متد GetRelatedProducts نیاز داریم؛ بنابراین فقط همین مورد در اینجا پیاده سازی شده‌است. پس از این تعریف، نیاز است سرویس فوق را در فایل Program.cs برنامه‌ی کلاینت هم ثبت کرد (به همراه سرویس HttpClient ای که در سازنده‌ی آن تزریق می‌شود):
builder.Services.AddScoped<IProductStore, ClientProductStore>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
به این ترتیب این سرویس در کامپوننت RelatedProducts قابل دسترسی می‌شود و جایگزین سرویس HttpClient تزریقی قبلی خواهد شد. به همین جهت به فایل کامپوننت ProductStore مراجعه کرده و فقط 2 سطر آن‌را تغییر می‌دهیم:
الف) معرفی سرویس IProductStore بجای HttpClient قبلی
@inject IProductStore ProductStore
ب) استفاده از متد GetRelatedProducts این سرویس:
private async Task LoadRelatedProducts()
{
   _loadRelatedProducts = true;
   _relatedProducts = await ProductStore.GetRelatedProducts(ProductId);
}
مابقی قسمت‌های این کامپوننت یکی است و تفاوتی با قبل ندارد.

اکنون اگر برنامه را اجرا کنیم، پس از مشاهده‌ی جزئیات یک محصول، بارگذاری کامپوننت Blazor WASM آن در developer tools مرورگر کاملا مشخص است:



راه‌حل سوم: استفاده از سرویس PersistentComponentState

با استفاده از سرویس PersistentComponentState می‌توان اطلاعات دریافتی از بانک‌اطلاعاتی را در حین pre-rendering در سمت سرور، به جزایر تعاملی انتقال داد و این روشی است که مایکروسافت برای پیاده سازی مباحث اعتبارسنجی و احراز هویت در Blazor 8x در پیش‌گرفته‌است. این راه‌حل را در قسمت بعد بررسی می‌کنیم.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-WebAssembly-Normal.zip
مطالب
تبدیل تعدادی تصویر به یک فایل PDF

صورت مساله:
تعدادی تصویر داریم، می‌خواهیم این‌ها را تبدیل به فایل PDF کنیم به این شرط که هر تصویر در یک صفحه مجزا قرار داده شود.
در ادامه برای این منظور از کتابخانه‌ی iTextSharp استفاده خواهیم کرد.

iTextSharp چیست؟
iTextSharp کتابخانه‌ی سورس باز و معروفی جهت تولید فایل‌های PDF ، توسط برنامه‌های مبتنی بر دات نت است. آن را از آدرس زیر می‌توان دریافت کرد:


کتابخانه iTextSharp نیز جزو کتابخانه‌هایی است که از جاوا به دات نت تبدیل شده‌اند. نام کتابخانه اصلی iText است و اگر کمی جستجو کنید می‌توانید کتاب 617 صفحه‌ای iText in Action از انتشارات MANNING را در این مورد نیز بیابید. هر چند این کتاب برای برنامه نویس‌های جاوا نوشته شده اما نام کلاس‌ها و متدها در iTextSharp تفاوتی با iText اصلی ندارند و مطالب آن برای برنامه نویس‌‌های دات نت هم قابل استفاده است.

مجوز استفاده از iTextSharp کدام است؟
مجوز این کتابخانه GNU Affero General Public License است. به این معنا که شما موظفید، تغییری در قسمت تهیه کننده خواص فایل PDF تولیدی که به صورت خودکار به نام کتابخانه تنظیم می‌شود، ندهید. اگر می‌خواهید این قسمت را تغییر دهید باید هزینه کنید. همچنین با توجه به اینکه این مجوز، GPL است یعنی زمانیکه از آن استفاده کردید باید کار خود را به صورت سورس باز ارائه دهید؛ درست خوندید! بله! مثل مجوز استفاده از نگارش عمومی و رایگان MySQL و اگر نمی‌خواهید اینکار را انجام دهید، در اینجا تاکید شده که باید کتابخانه را خریداری کنید.

نحوه استفاده از کتابخانه iTextSharp
در ابتدا کد تبدیل تصاویر به فایل PDF را در ذیل مشاهده خواهید کرد. فرض بر این است که ارجاعی را به اسمبلی itextsharp.dll اضافه کرده‌اید:
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
public class ImageToPdf
{
public iTextSharp.text.Rectangle PdfPageSize { set; get; }
public ImageFormat ImageCompressionFormat { set; get; }
public bool FitImagesToPage { set; get; }

public void ExportToPdf(IList<string> imageFilesPath, string outPdfPath)
{
using (var pdfDoc = new Document(PdfPageSize))
{
PdfWriter.GetInstance(pdfDoc, new FileStream(outPdfPath, FileMode.Create));
pdfDoc.Open();

foreach (var file in imageFilesPath)
{
var pngImg = iTextSharp.text.Image.GetInstance(file);

if (FitImagesToPage)
{
pngImg.ScaleAbsolute(pdfDoc.PageSize.Width, pdfDoc.PageSize.Height);
}
pngImg.SetAbsolutePosition(0, 0);

//add to page
pdfDoc.Add(pngImg);
//start a new page
pdfDoc.NewPage();
}
}
}
}
}
توضیحات:
استفاده از کتابخانه‌ی iTextSharp همیشه شامل 5 مرحله است. ابتدا شیء Document ایجاد می‌شود. سپس وهله‌ای از PdfWriter ساخته شده و Document جهت نوشتن در آن گشوده خواهد شد. در طی یک سری مرحله محتویات مورد نظر به Document اضافه شده و نهایتا این شیء بسته خواهد شد. البته در اینجا چون کلاس Document اینترفیس IDisposable را پیاده سازی کرده، بهترین روش استفاده از آن بکارگیری واژه کلیدی using جهت مدیریت منابع آن است. به این ترتیب کامپایلر به صورت خودکار قطعه try/finally مرتبط را جهت پاکسازی منابع، تشکیل خواهد داد.
اندازه صفحات توسط سازنده‌ی شیء Document مشخص خواهند شد. این شیء از نوع iTextSharp.text.Rectangle است؛ اما مقدار دهی آن توسط کلاس iTextSharp.text.PageSize صورت می‌گیرد که انواع و اقسام اندازه صفحات استاندارد در آن تعریف شده‌اند.
متد iTextSharp.text.Image.GetInstance که در این مثال جهت دریافت اطلاعات تصاویر مورد استفاده قرار گرفت، 15 overload دارد که از آدرس مستقیم یک فایل تا استریم مربوطه تا Uri یک آدرس وب را نیز می‌پذیرد و از این لحاظ بسیار غنی است.

مثالی در مورد نحوه استفاده از کلاس فوق:
using System.Collections.Generic;
using System.Drawing.Imaging;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
new ImageToPdf
{
FitImagesToPage = true,
ImageCompressionFormat = ImageFormat.Jpeg,
PdfPageSize = iTextSharp.text.PageSize.A4
}.ExportToPdf(
imageFilesPath: new List<string>
{
@"D:\3.jpg",
@"D:\4.jpg"
},
outPdfPath: @"D:\tst.pdf"
);
}
}
}