مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت دهم - کار با فرم‌ها - قسمت اول
هر برنامه‌ی وبی، نیاز به کار با فرم‌های وب را دارد و به همین جهت، AngularJS 2.0 به همراه دو نوع از فرم‌ها است: فرم‌های مبتنی بر قالب‌ها و فرم‌های مبتنی بر مدل‌ها.
کار با فرم‌های مبتنی بر قالب‌ها ساده‌تر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی داده‌های ورودی توسط کاربر، در اختیار ما قرار می‌دهند. اما فرم‌های مبتنی بر مدل‌ها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودی‌های کاربران، ارائه می‌دهند. در این قسمت فرم‌های مبتنی بر قالب‌ها (Template-driven forms) را بررسی می‌کنیم.


ساخت فرم مبتنی بر قالب‌های ثبت یک محصول جدید

در ادامه‌ی مثال این سری، می‌خواهیم به کاربران، امکان ثبت اطلاعات یک محصول جدید را نیز بدهیم. به همین جهت فایل‌های جدید product-form.component.ts و product-form.component.html را به پوشه‌ی App\products برنامه اضافه می‌کنیم (جهت تعریف کامپوننت فرم جدید به همراه قالب HTML آن).
الف) محتوای کامل product-form.component.html
<form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">
                Add Product
            </h3>
        </div>
        <div class="panel-body form-horizontal">
            <div class="form-group">
                <label for="productName" class="col col-md-2 control-label">Name</label>
                <div class="controls col col-md-10">
                    <input ngControl="productName" id="productName" required
                           #productName="ngForm"
                           (change)="log(productName)"
                           minlength="3"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.productName"/>
                    <div *ngIf="productName.touched && productName.errors">
                        <label class="text-danger" *ngIf="productName.errors.required">
                            Name is required.
                        </label>
                        <label class="text-danger" *ngIf="productName.errors.minlength">
                            Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.
                        </label>
                    </div>
                </div>
            </div>
            <div class="form-group">
                <label for="productCode" class="col col-md-2 control-label">Code</label>
                <div class="controls col col-md-10">
                    <input ngControl="productCode" id="productCode" required
                           #productCode="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.productCode"/>
                    <label class="text-danger" *ngIf="productCode.touched && !productCode.valid">
                        Code is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="releaseDate" class="col col-md-2 control-label">Release Date</label>
                <div class="controls col col-md-10">
                    <input ngControl="releaseDate" id="releaseDate" required
                           #releaseDate="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.releaseDate"/>
                    <label class="text-danger" *ngIf="releaseDate.touched && !releaseDate.valid">
                        Release Date is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="price" class="col col-md-2 control-label">Price</label>
                <div class="controls col col-md-10">
                    <input ngControl="price" id="price" required
                           #price="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.price"/>
                    <label class="text-danger" *ngIf="price.touched && !price.valid">
                        Price is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="description" class="col col-md-2 control-label">Description</label>
                <div class="controls col col-md-10">
                    <textarea ngControl="description" id="description" required
                              #description="ngForm"
                              rows="10" type="text" class="form-control"
                              [(ngModel)]="productModel.description"></textarea>
                    <label class="text-danger" *ngIf="description.touched && !description.valid">
                        Description is required.
                    </label>
                </div>
            </div>
            <div class="form-group">
                <label for="imageUrl" class="col col-md-2 control-label">Image</label>
                <div class="controls col col-md-10">
                    <input ngControl="imageUrl" id="imageUrl" required
                           #imageUrl="ngForm"
                           type="text" class="form-control"
                           [(ngModel)]="productModel.imageUrl"/>
                    <label class="text-danger" *ngIf="imageUrl.touched && !imageUrl.valid">
                        Image is required.
                    </label>
                </div>
            </div>
        </div>
        <footer class="panel-footer">
            <button [disabled]="!f.valid"
                    type="submit" class="btn btn-primary">
                Submit
            </button>
        </footer>
    </div>
</form>

ب) محتوای کامل product-form.component.ts
import { Component } from 'angular2/core';
import { Router } from 'angular2/router';
import { IProduct } from './product';
import { ProductService } from './product.service';
 
@Component({
    //selector: 'product-form',
    templateUrl: 'app/products/product-form.component.html'
})
export class ProductFormComponent {
 
    productModel = <IProduct>{}; // creates an empty object of an interface
 
    constructor(private _productService: ProductService, private _router: Router) { }
 
    log(productName): void {
        console.log(productName);
    }
 
    onSubmit(form): void {
        console.log(form);
        console.log(this.productModel);
 
        this._productService.addProduct(this.productModel)
            .subscribe((product: IProduct) => {
                console.log(`ID: ${product.productId}`);
                this._router.navigate(['Products']);
            });
    }
}

اکنون ریز جزئیات و تغییرات این دو فایل را قدم به قدم بررسی خواهیم کرد.

تا اینجا در فایل product-form.component.html یک فرم ساده‌ی HTML ایی مبتنی بر بوت استرپ 3 را تهیه کرده‌ایم. نکات ابتدایی آن، دقیقا مطابق است با مستندات بوت استرپ 3؛ از لحاظ تعریف form-horizontal و سپس ایجاد یک div با کلاس form-group و قرار دادن المان‌هایی با کلاس‌های form-control در آن. همچنین برچسب‌های تعریف شده‌ی با ویژگی for، در این المان‌ها، جهت بالارفتن دسترسی پذیری به عناصر فرم، اضافه شده‌اند. این مراحل در مورد تمام فرم‌های استاندارد وب صادق هستند و نکته‌ی جدیدی ندارند.

در ادامه تعاریف AngularJS 2.0 را به این فرم اضافه کرد‌ه‌ایم. در اینجا هر کدام از المان‌های ورودی، تبدیل به Controlهای AngularJS 2.0 شده‌اند. کلاس Control، خواص ویژه‌ای را در اختیار ما قرار می‌دهد. برای مثال value یا مقدار این المان چیست؟ وضعیت touched و untouched آن چیست؟ (آیا کاربر فوکوس را به آن منتقل کرده‌است یا خیر؟) آیا dirty است؟ (مقدار آن تغییر کرده‌است؟) و یا شاید هم pristine است؟ (مقدار آن تغییری نکرده‌است). علاوه بر این‌ها دارای خاصیت valid نیز می‌باشد (آیا اعتبارسنجی آن موفقیت آمیز است؟)؛ به همراه خاصیت errors که مشکلات اعتبارسنجی موجود را باز می‌گرداند.
<div class="form-group">
    <label for="description" class="col col-md-2 control-label">Description</label>
    <div class="controls col col-md-10">
        <textarea ngControl="description" id="description" required
                  #description="ngForm"
                  rows="10" type="text" class="form-control"
                  [(ngModel)]="productModel.description"></textarea>
        <label class="text-danger" *ngIf="description.touched && !description.valid">
            Description is required.
        </label>
    </div>
</div>
در اینجا کلاس مفید دیگری به نام ControlGroup نیز درنظر گرفته شده‌است. برای مثال هر فرم، یک ControlGroup است (گروهی متشکل از کنترل‌ها، در صفحه). البته می‌توان یک فرم بزرگ را به چندین ControlGroup نیز تقسیم کرد. تمام خواصی که برای کلاس Control ذکر شدند، در مورد کلاس ControlGroup نیز صادق هستند. با این تفاوت که این‌بار اگر به خاصیت valid آن مراجعه کردیم، یعنی تمام کنترل‌های قرار گرفته‌ی در آن گروه معتبر هستند و نه صرفا یک تک کنترل خاص. به همین ترتیب خاصیت errors نیز تمام خطاهای اعتبارسنجی یک گروه را باز می‌گرداند.
هر دو کلاس Control و ControlGroup از کلاس پایه‌ای به نام AbstractControl مشتق شده‌اند و این کلاس پایه است که خواص مشترک یاد شده را به همراه دارد.

بنابراین برای کار ساده‌تر با یک فرم AngularJS 2.0، کل فرم را تبدیل به یک ControlGroup کرده و سپس هر کدام از المان‌های ورودی را تبدیل به یک Control مجزا می‌کنیم. کار برقراری این ارتباط، با استفاده از دایرکتیو ویژه‌ای به نام ngControl انجام می‌شود. بنابراین دایرکتیو ngControl، با نامی دلخواه و معین، به تمام المان‌های ورودی، انتساب داده شده‌است.
هرچند در این مثال نام ngControl‌ها با مقدار id هر کنترل یکسان درنظر گرفته شده‌است، اما ارتباطی بین این دو نیست. مقدار id جهت استفاده‌ی در DOM کاربرد دارد و مقدار ngControl توسط AngularJS 2.0 استفاده می‌شود. جهت رسیدن به کدهایی یکدست، بهتر است این نام‌ها را یکسان درنظر گرفت؛ اما هیچ الزامی هم ندارد.

برای بررسی جزئیات این اشیاء کنترل، در المان productName، یک متغیر محلی را به نام productName# تعریف کرده‌ایم و آن‌را به دایرکتیو ngControl انتساب داده‌ایم. این انتساب توسط ngForm انجام شده‌است. زمانیکه AngularJS 2.0 یک متغیر محلی تنظیم شده‌ی به ngForm را مشاهده می‌کند، آن‌را به صورت خودکار به ngControl همان المان ورودی متصل می‌کند. سپس این متغیر محلی را به متد log ارسال کرده‌ایم. این متد در کلاس کامپوننت جاری تعریف شده‌است و کار آن نمایش شیء Control جاری در کنسول developer tools مرورگر است.
<input ngControl="productName" id="productName" required
       #productName="ngForm"
       (change)="log(productName)"
       minlength="3"
       type="text" class="form-control"
       [(ngModel)]="productModel.productName"/>


همانطور که در تصویر مشاهده می‌کنید، عناصر یک شیء Control، در کنسول نمایش داده شده‌اند و در اینجا بهتر می‌توان خواصی مانند valid و امثال آن‌را که به همراه این کنترل وجود دارند، مشاهده کرد. برای مثال خاصیت dirty آن true است چون مقدار آن المان ورودی، تغییر کرده‌است.

بنابراین تا اینجا با استفاده از دایرکتیو ngControl، یک المان ورودی را به یک شیء Control متصل کردیم. همچنین نحوه‌ی تعریف یک متغیر محلی را در المانی و سپس ارسال آن را به کلاس متناظر با کامپوننت فرم، نیز بررسی کردیم.


افزودن اعتبارسنجی به فرم ثبت محصولات

به کنترل‌هایی که به صورت فوق توسط ngControl ایجاد می‌شوند، اصطلاحا implicitly created controls می‌گویند؛ یا به عبارتی ایجاد آن‌ها به صورت «ضمنی» توسط AngularJS 2.0 انجام می‌شود که نمونه‌ای از آن‌را در تصویر فوق نیز مشاهده کردید. این نوع کنترل‌های ضمنی، امکانات اعتبارسنجی محدودی را در اختیار دارند؛ که تنها سه مورد هستند:
الف) required
ب) minlength
ج) maxlength

این‌ها ویژگی‌های استاندارد اعتبارسنجی HTML 5 نیز هستند. نمونه‌ای از اعمال این موارد را با افزودن ویژگی required، به المان‌های فرم ثبت محصولات فوق، مشاهده می‌کنید.
سپس نیاز داریم تا خطاهای اعتبارسنجی را در مقابل هر المان ورودی نمایش دهیم.
<textarea ngControl="description" id="description" required
          #description="ngForm"
          rows="10" type="text" class="form-control"></textarea>
<div class="alert alert-danger" *ngIf="description.touched && !description.valid">
    Description is required.
</div>
پس از افزودن ویژگی required به یک المان، افزودن و نمایش خطاهای اعتبارسنجی، شامل سه مرحله‌ی زیر است:
الف) ایجاد یک div ساده جهت نمایش پیام خطای اعتبار سنجی
ب) افزودن یک متغیر محلی با # و تنظیم شده‌ی به ngForm، جهت دسترسی به شیء کنترل ایجاد شده
ج) استفاده از این متغیر محلی در دایرکتیو ساختاری ngIf* جهت دسترسی به خاصیت valid آن کنترل. بر مبنای مقدار این خاصیت است که تصمیم گرفته می‌شود، پیام اجباری بودن پر کردن فیلد نمایش داده شود یا خیر.
در اینجا یک سری کلاس بوت استرپ 3 هم جهت نمایش بهتر این پیام خطای اعتبارسنجی، اضافه شده‌اند.

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



بهبود شیوه نامه‌ی پیش فرض المان‌های ورودی اطلاعات در AngularJS 2.0

می‌خواهیم اگر اعتبارسنجی یک المان ورودی با شکست مواجه شد، یک حاشیه‌ی قرمز، در اطراف آن نمایش داده شود. این مورد را با توجه به اینکه AngularJS 2.0، شیوه نامه‌های ویژه‌ای را به صورت خودکار به المان‌ها اضافه می‌کند، می‌توان به صورت سراسری به تمام فرم‌ها اضافه کرد. برای این منظور فایل app.component.css واقع در ریشه‌ی پوشه‌ی app را گشوده و تنظیمات ذیل را به آن اضافه کنید:
.ng-touched.ng-invalid{
    border: 1px solid red;
}

ویژگی‌های اضافه شده‌ی در حالت شکست اعتبارسنجی؛ مانند ng-invalid


ویژگی‌های اضافه شده‌ی در حالت موفقیت اعتبارسنجی؛ مانند ng-valid



مدیریت چندین ویژگی اعتبارسنجی یک المان با هم

گاهی از اوقات نیاز است برای یک المان ورودی، چندین نوع اعتبارسنجی مختلف را تعریف کرد. برای مثال فرض کنید که ویژگی‌های required و همچنین minlength، برای نام محصول تنظیم شده‌اند. در این حالت ذکر productName.valid خیلی عمومی است و هر دو حالت اجباری بودن فیلد و حداقل طول آن‌را با هم به همراه دارد:
<div class="alert alert-danger" *ngIf="productName.touched && !productName.valid">
   Name is required.
</div>
بنابراین در این حالت از روش ذیل استفاده می‌شود:
<div *ngIf="productName.touched && productName.errors">
    <div class="alert alert-danger" *ngIf="productName.errors.required">
        Name is required.
    </div>
    <div class="alert alert-danger" *ngIf="productName.errors.minlength">
        Name should be minimum 3 characters.
    </div>
</div>
خاصیت errors نیز یکی دیگر از خواص شیء کنترل است. اگر نال بود، یعنی خطایی وجود ندارد و در غیراینصورت، به ازای هر نوع اعتبارسنجی تعریف شده، خواصی به آن اضافه می‌شوند. بنابراین ذکر productName.errors.required به این معنا است که آیا خاصیت errors، دارای کلیدی به نام required است؟ اگر بله، یعنی این فیلد هنوز پر نشده‌است.
همچنین چون در این حالت productName.touched نیاز است چندین بار تکرار شود، می‌توان آن‌را در یک div محصور کننده‌ی دو div مورد نیاز جهت نمایش خطاهای اعتبارسنجی قرار داد. به علاوه بررسی نال نبودن productName.errors نیز در div محصور کننده صورت گرفته‌است و دیگر نیازی نیست این بررسی را به ngIfهای داخلی اضافه کرد.

نکته 1
اگر علاقمند بودید تا جزئیات خاصیت errors را مشاهده کنید، آن‌را می‌توان توسط pipe توکاری به نام json به صورت موقت نمایش داد و بعد آن‌را حذف کرد:
 <div *ngIf="productName.touched && productName.errors">
  {{ productName.errors | json }}

نکته 2
بجای ذکر مستقیم عدد سه در «minimum 3 characters»، می‌توان این عدد را مستقیما از تعریف ویژگی minlength نیز استخراج کرد:
 Name should be minimum {{ productName.errors.minlength.requiredLength }} characters.


بررسی ngForm

شبیه به ngControl که یک المان ورودی را به یک کنترل AngularJS 2.0 متصل می‌کند، دایرکتیو دیگری نیز به نام ngForm وجود دارد که کل فرم را به شیء ControlGroup بایند می‌کند و برخلاف ngControl، نیازی به ذکر صریح آن وجود ندارد. هر زمانیکه AngularJS 2.0، المان استاندارد فرمی را در صفحه مشاهده می‌کند، این اتصالات را به صورت خودکار برقرار خواهد کرد.
ngForm دارای خاصیتی است به نام ngSumbit که از نوع EventEmitter است (نمونه‌ای از آن را در مبحث کامپوننت‌های تو در تو پیشتر ملاحظه کرده‌اید). بنابراین از آن می‌توان جهت اتصال رخداد submit فرم، به متدی در کلاس کامپوننت خود، استفاده کرد. متد متصل به این رخداد، زمانی فراخوانی می‌شود که کاربر بر روی دکمه‌ی submit کلیک کند:
 <form #f="ngForm" (ngSubmit)="onSubmit(f.form)">
همچنین در اینجا متغیر محلی f جهت دسترسی به شیء ControlGroup و ارسال آن به متد onSubmit تعریف شده‌است (شبیه به متغیرهای محلی دسترسی به ngControl که پیشتر جهت نمایش خطاهای اعتبارسنجی، اضافه کردیم).

پس از تعریف این رخداد و اتصال آن در قالب کامپوننت، اکنون می‌توان متد onSubmit را در کلاس آن نیز اضافه کرد.
onSubmit(form): void {
   console.log(form);
}
فعلا هدف از این متد، نمایش جزئیات شیء form دریافتی، در کنسول developer tools است.



غیرفعال کردن دکمه‌ی submit در صورت وجود خطاهای اعتبارسنجی

در قسمت بررسی ngForm، یک متغیر محلی را به نام f ایجاد کردیم که به شیء ControlGroup فرم جاری اشاره می‌کند. از این متغیر و خاصیت valid آن می‌توان با کمک property binding به خاصیت disabled یک دکمه، آن‌را به صورت خودکار فعال یا غیرفعال کرد:
<button [disabled]="!f.valid"
        type="submit" class="btn btn-primary">
    Submit
</button>
هر زمانیکه کل فرم از لحاظ اعتبارسنجی مشکلی نداشته باشد، دکمه‌ی submit فعال می‌شود و برعکس.



نمایش فرم افزودن محصولات توسط سیستم Routing

با نحوه‌ی تعریف مسیریابی‌ها در قسمت قبل آشنا شدیم. برای نمایش فرم افزودن محصولات، می‌توان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before...
import { ProductFormComponent }  from './products/product-form.component';
 
@Component({
    //same as before…
    template: `
                //same as before…
                    <li><a [routerLink]="['AddProduct']">Add Product</a></li>
               //same as before…
    `,
    //same as before…
})
@RouteConfig([
    //same as before…
    { path: '/addproduct', name: 'AddProduct', component: ProductFormComponent }
])
//same as before...
ابتدا به RouteConfig، مسیریابی کامپوننت فرم افزودن محصولات اضافه شده‌است. سپس ماژول این کلاس در ابتدای فایل import شده و در آخر routerLink آن به قالب سایت و منوی بالای سایت اضافه شده‌است.



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

برای اتصال المان‌های فرم به یک مدل، این مدل را به صورت یک خاصیت عمومی، در سطح کلاس کامپوننت فرم، تعریف می‌کنیم:
 productModel = <IProduct>{}; // creates an empty object of an interface
اگر از اینترفیسی مانند IProduct که در قسمت‌های قبل این سری تعریف شد، نیاز است شیء جدیدی ساخته شود، الزاما نیازی نیست تا یک کلاس جدید را از آن مشتق کرد و بعد متغیر new ClassName را تهیه کرد. در TypeScript می‌توان به صورت خلاصه از syntax فوق نیز استفاده کرد.
پس از تعریف خاصیت productModel، اکنون کافی است با استفاده از two-way data binding، آن‌را به المان‌های فرم نسبت دهیم. برای مثال:
<textarea ngControl="description" id="description" required
          #description="ngForm"
          rows="10" type="text" class="form-control"
          [(ngModel)]="productModel.description"></textarea>
در اینجا با استفاده از ngModel و انقیاد دو طرفه، کار اتصال به خاصیت توضیحات شیء محصول انجام شده‌است. اکنون بلافاصله تغییرات اعمالی به فرم، به مدل متناظر منعکس می‌شود و برعکس. این ngModel را به تمام المان‌های ورودی فرم متصل خواهیم کرد.
پس از تعریف یک چنین اتصالی، دیگر نیازی به مقدار دهی پارامتر onSubmit(f.form) نیست. زیرا شیء productModel، در متد onSumbit در دسترس است و این شیء همواره حاوی آخرین تغییرات اعمالی به المان‌های فرم است.

پس از اینکه فرم را به مدل آن متصل کردیم، فایل product.service.ts را گشوده و متد جدید addProduct را به آن اضافه کنید:
addProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC
        let options = new RequestOptions({ headers: headers });
 
    return this._http.post(this._addProductUrl, JSON.stringify(product), options)
        .map((response: Response) => <IProduct>response.json())
        .do(data => console.log("Product: " + JSON.stringify(data)))
        .catch(this.handleError);
}
کار این متد، ارسال شیء محصول به یک اکشن متد برنامه‌ی ASP.NET MVC جاری است. با جزئیات کار با obsevables درمطلب «دریافت اطلاعات از سرور» پیشتر آشنا شده‌ایم.
نکته‌ی مهم اینجا است که content type پیش فرض ارسالی متد post آن، plain text است و در این حالت ASP.NET MVC شیء JSON دریافتی از کلاینت را پردازش نخواهد کرد. بنابراین نیاز است تا هدر content type را به صورت صریحی در اینجا ذکر نمود؛ در غیراینصورت در سمت سرور، شاهد نال بودن مقادیر دریافتی از کاربران خواهیم بود.
امضای سمت سرور متد دریافت اطلاعات از کاربر، چنین شکلی را دارد (تعریف شده در فایل Controllers\HomeController.cs):
 [HttpPost]
public ActionResult AddProduct(Product product)
{

اشیاء هدرها و تنظیمات درخواست، در متد addProduct سرویس ProductService، در ماژول‌های ذیل تعریف شده‌اند که باید به ابتدای فایل product.service.ts اضافه شوند:
 import { Headers, RequestOptions } from 'angular2/http';

پس از تعریف متد addProduct در سرویس ProductService، اکنون با استفاده از ترزیق این سرویس به سازنده‌ی کلاس فرم ثبت یک محصول جدید، می‌توان متد this._productService.addProduct را جهت ارسال productModel به سمت سرور، در متد onSubmit فراخوانی کرد:
//Same as before…
import { IProduct } from './product';
import { ProductService } from './product.service';
 
@Component({
//Same as before…
})
export class ProductFormComponent {
 
    productModel = <IProduct>{}; // creates an empty object of an interface
 
    constructor(private _productService: ProductService, private _router: Router) { }
 
    //Same as before… 

    onSubmit(form): void {
        console.log(form);
        console.log(this.productModel);
 
        this._productService.addProduct(this.productModel)
            .subscribe((product: IProduct) => {
                console.log(`ID: ${product.productId}`);
                this._router.navigate(['Products']);
            });
    }
}
همانطور که ذکر شد، از آنجائیکه شیء productModel حاوی آخرین تغییرات اعمالی توسط کاربر است، اکنون می‌توان پارامتر form متد onSubmit را حذف کرد.
در اینجا پس از فراخوانی متد addProduct، متد subscribe، در انتهای زنجیره، فراخوانی شده‌است. کار آن هدایت کاربر به صفحه‌ی نمایش لیست محصولات است. در اینجا this._router از طریق تزریق وابستگی‌های سرویس مسیریاب به سازنده‌ی کلاس، تامین شده‌است. نمونه‌ی آن‌را در قسمت «افزودن دکمه‌ی back با کدنویسی» مربوطه به مطلب آشنایی با مسیریابی، پیشتر مطالعه کرده‌اید.



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


خلاصه‌ی بحث

فرم‌های template driven در AngularJS 2.0 به این نحو طراحی می‌شوند:
 1) ابتدا فرم HTML را به حالت معمولی آن طراحی می‌کنیم؛ با تمام المان‌های آن.
 2) به تمام المان‌های فرم، دیراکتیو ngControl را متصل می‌کنیم، تا AngularJS 2.0 آن‌را تبدیل به یک کنترل خاص خودش کند. کنترلی که دارای خواصی مانند valid و touched است.
 3) سپس برای دسترسی به این کنترل ایجاد شده‌ی به صورت ضمنی، یک متغیر محلی آغاز شده‌ی با # را به تمام المان‌ها اضافه می‌کنیم.
 4) اعتبارسنجی‌هایی را مانند required  به المان‌های فرم اضافه می‌کنیم.
 5) از متغیر محلی تعریف شده و ngIf* برای بررسی خواصی مانند valid و touched برای نمایش خطاهای اعتبارسنجی کمک گرفته می‌شود.
 6) پس از تعریف فرم، تعریف ngControlها، تعریف متغیر محلی شروع شده‌ی با # و افزودن خطاهای اعتبارسنجی، اکنون نوبت به ارسال این اطلاعات به سرور است. بنابراین رخداد ngSubmit را باید به متدی در کلاس کامپوننت جاری متصل کرد.
 7) اکنون که با کلیک بر روی دکمه‌ی submit فرم، متد onSubmit متصل به ngSubmit فراخوانی می‌شود، نیاز است بین المان‌های فرم HTML و کلاس کامپوننت، ارتباط برقرار کرد. این‌کار را توسط two-way data binding و تعریف ngModel بر روی تمام المان‌های فرم، انجام می‌دهیم. این ngModelها، به یک خاصیت عمومی که متناظر است با وهله‌ای از شیء مدل فرم، متصل هستند. بنابراین این مدل، در هر لحظه، بیانگر آخرین تغییرات کاربر است و از آن می‌توان برای ارسال اطلاعات به سرور استفاده کرد.
 8) پس از اتصال فرم به کلاس متناظر با آن، اکنون سرویس محصولات را تکمیل کرده و به آن متد HTTP Post را جهت ارسال اطلاعات سمت کاربر، به سرور، اضافه می‌کنیم. در اینجا نکته‌ی مهم، تنظیم content type ارسالی به سمت سرور است. در غیراینصورت فریم ورک سمت سرور قادر به تشخیص JSON بودن این اطلاعات نخواهد شد.
مطالب
پیاده سازی SoftDelete در EF Core
در مورد حذف منطقی در EF 6x، پیشتر مطالبی را در این سایت مطالعه کرده‌اید:
- «پیاده سازی حذف منطقی در Entity framework» حذف منطقی، یکی از الگوهای بسیار پرکاربرد در برنامه‌های تجاری است. توسط آن بجای حذف فیزیکی اطلاعات، آن‌ها را تنها به عنوان رکوردی حذف شده، «علامتگذاری» می‌کنیم. مزایای آن نیز به شرح زیر هستند:
- داشتن سابقه‌ی حذف اطلاعات
- جلوگیری از cascade delete
- امکان بازیابی رکوردها و امکان ایجاد قسمتی به نام recycle bin در برنامه (شبیه به recycle bin در ویندوز که امکان بازیابی موارد حذف شده را می‌دهد)
- امکان داشتن رکوردهایی که در یک برنامه (به ظاهر) حذف شده‌اند، اما هنوز در برنامه‌ی دیگری در حال استفاده هستند.
- بالابردن میزان امنیت برنامه. فرض کنید سایت شما هک شده و شخصی، دسترسی به پنل مدیریتی و سطوح دسترسی مدیریتی برنامه را پیدا کرده‌است. در این حالت حذف تمام رکوردهای سایت توسط او، تنها به معنای تغییر یک بیت، از یک به صفر است و بازگرداندن این درجه از خسارت، تنها با روشن کردن این بیت، برطرف می‌شود.

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


نیاز به تعریف دو خاصیت جدید در هر جدول

هر جدولی که قرار است soft delete به آن اعمال شود، باید دارای دو فیلد جدید bool IsDeleted و DateTime? DeletedAt باشد. می‌توان این خواص را به هر موجودیتی به صورت دستی اضافه کرد و یا می‌توان ابتدا یک کلاس پایه‌ی abstract را برای آن ایجاد کرد:
using System;

namespace EFCoreSoftDelete.Entities
{
    public abstract class BaseEntity
    {
        public int Id { get; set; }


        public bool IsDeleted { set; get; }
        public DateTime? DeletedAt { set; get; }
    }
}
و سپس موجودیت‌هایی را که قرار است از soft delete پشتیبانی کنند، توسط آن علامتگذاری کرد؛ مانند موجودیت Blog:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Blog : BaseEntity
    {
        public string Name { set; get; }

        public virtual ICollection<Post> Posts { set; get; }
    }

    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder.Property(blog => blog.Name).HasMaxLength(450).IsRequired();
            builder.HasIndex(blog => blog.Name).IsUnique();

            builder.HasData(new Blog { Id = 1, Name = "Blog 1" });
            builder.HasData(new Blog { Id = 2, Name = "Blog 2" });
            builder.HasData(new Blog { Id = 3, Name = "Blog 3" });
        }
    }
}
که هر بلاگ از تعدادی مطلب تشکیل شده‌است:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Post : BaseEntity
    {
        public string Title { set; get; }

        public Blog Blog { set; get; }
        public int BlogId { set; get; }
    }

    public class PostConfiguration : IEntityTypeConfiguration<Post>
    {
        public void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.Property(post => post.Title).HasMaxLength(450);
            builder.HasOne(post => post.Blog).WithMany(blog => blog.Posts).HasForeignKey(post => post.BlogId);

            builder.HasData(new Post { Id = 1, BlogId = 1, Title = "Post 1" });
            builder.HasData(new Post { Id = 2, BlogId = 1, Title = "Post 2" });
            builder.HasData(new Post { Id = 3, BlogId = 1, Title = "Post 3" });
            builder.HasData(new Post { Id = 4, BlogId = 1, Title = "Post 4" });
            builder.HasData(new Post { Id = 5, BlogId = 2, Title = "Post 5" });
        }
    }
}
مزیت علامتگذاری این کلاس‌ها، امکان کوئری گرفتن از آن‌ها نیز می‌باشد که در ادامه از آن استفاده خواهیم کرد.


حذف خودکار رکوردهایی که Soft Delete شده‌اند، از نتیجه‌ی کوئری‌ها و گزارشات

تا اینجا فقط دو خاصیت ساده را به کلاس‌های مدنظر خود اضافه کرده‌ایم. پس از آن یا می‌توان در هر جائی برای مثال شرط context.Blogs.Where(blog => !blog.IsDeleted) را به صورت دستی اعمال کرد و در گزارشات، رکوردهای حذف منطقی شده را نمایش نداد و یا از زمان ارائه‌ی EF Core 2x می‌توان برای آن‌ها Query Filter تعریف کرد. برای مثال می‌توان به تنظیمات موجودیت Blog و یا Post مراجعه نمود و با استفاده از متد HasQueryFilter، همان شرط blog => !blog.IsDeleted را به صورت سراسری به تمام کوئری‌های مرتبط با این موجودیت‌ها اعمال کرد:
    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            // ...
            builder.HasQueryFilter(blog => !blog.IsDeleted);
        }
    }
از این پس ذکر context.Blogs دقیقا معنای context.Blogs.Where(blog => !blog.IsDeleted) را می‌دهد و دیگر نیازی به ذکر صریح شرط متناظر با soft delete نیست.
در این حالت کوئری‌های نهایی به صورت خودکار دارای شرط زیر خواهند شد:
SELECT [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[IsDeleted] <> CAST(1 AS bit)


اعمال خودکار QueryFilter مخصوص Soft Delete به تمام موجودیت‌ها

همانطور که عنوان شد، مزیت علامتگذاری موجودیت‌ها با کلاس پایه‌ی BaseEntity، امکان کوئری گرفتن از آن‌ها است:
namespace EFCoreSoftDelete.DataLayer
{
    public static class GlobalFiltersManager
    {
        public static void ApplySoftDeleteQueryFilters(this ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model
                                                    .GetEntityTypes()
                                                    .Where(eType => typeof(BaseEntity).IsAssignableFrom(eType.ClrType)))
            {
                entityType.addSoftDeleteQueryFilter();
            }
        }

        private static void addSoftDeleteQueryFilter(this IMutableEntityType entityData)
        {
            var methodToCall = typeof(GlobalFiltersManager)
                                .GetMethod(nameof(getSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)
                                .MakeGenericMethod(entityData.ClrType);
            var filter = methodToCall.Invoke(null, new object[] { });
            entityData.SetQueryFilter((LambdaExpression)filter);
        }

        private static LambdaExpression getSoftDeleteFilter<TEntity>() where TEntity : BaseEntity
        {
            return (Expression<Func<TEntity, bool>>)(entity => !entity.IsDeleted);
        }
    }
}
در اینجا در ابتدا تمام موجودیت‌هایی که از BaseEntity ارث بری کرده‌اند، یافت می‌شوند. سپس بر روی آن‌ها قرار است متد SetQueryFilter فراخوانی شود. این متد بر اساس تعاریف EF Core، یک LambdaExpression کلی را قبول می‌کند که نمونه‌ی آن در متد getSoftDeleteFilter تعریف شده و سپس توسط متد addSoftDeleteQueryFilter به صورت پویا به modelBuilder اعمال می‌شود.

محل اعمال آن نیز در انتهای متد OnModelCreating است تا به صورت خودکار به تمام موجودیت‌های موجود اعمال شود:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {

        //...


        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseEntity).Assembly);
            modelBuilder.ApplySoftDeleteQueryFilters();
        }


مشکل! هنوز هم حذف فیزیکی رخ می‌دهد!

تنظیمات فوق، تنها بر روی کوئری‌های نوشته شده تاثیر دارند؛ اما هیچگونه تاثیری را بر روی متد Remove و سپس SaveChanges نداشته و در این حالت، هنوز هم حذف واقعی و فیزیکی رخ می‌دهد.
 برای رفع این مشکل باید به EF Core گفت، هر چند دستور حذف صادر شده، اما آن‌را تبدیل به دستور Update کن؛ یعنی فیلد IsDelete را به 1 و فیلد DeletedAt را با زمان جاری مقدار دهی کن:
namespace EFCoreSoftDelete.DataLayer
{
    public static class AuditableEntitiesManager
    {
        public static void SetAuditableEntityOnBeforeSaveChanges(this ApplicationDbContext context)
        {
            var now = DateTime.UtcNow;

            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        //TODO: ...
                        break;
                    case EntityState.Modified:
                        //TODO: ...
                        break;
                    case EntityState.Deleted:
                        entry.State = EntityState.Unchanged; //NOTE: For soft-deletes to work with the original `Remove` method.

                        entry.Entity.IsDeleted = true;
                        entry.Entity.DeletedAt = now;
                        break;
                }
            }
        }
    }
}
در اینجا با استفاده از سیستم tracking، رکوردهای حذف شده‌ی با وضعیت EntityState.Deleted، به وضعیت EntityState.Unchanged تغییر پیدا می‌کنند، تا دیگر حذف نشوند. اما در ادامه چون دو خاصیت IsDeleted و DeletedAt این موجودیت، ویرایش می‌شوند، وضعیت جدید Modified خواهد بود که به کوئری‌های Update تفسیر می‌شوند. به این ترتیب می‌توان همانند قبل یک رکورد را حذف کرد:
var post1 = context.Posts.Find(1);
if (post1 != null)
{
   context.Remove(post1);

   context.SaveChanges();
}
اما دستوری که توسط EF Core صادر می‌شود، یک Update است:
Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:11:32' (Nullable = true), @p1='True'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

محل اعمال متد SetAuditableEntityOnBeforeSaveChanges فوق، پیش از فراخوانی SaveChanges و به صورت زیر است:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            ChangeTracker.DetectChanges();

            beforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges(acceptAllChangesOnSuccess);

            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

        // ...

        private void beforeSaveTriggers()
        {
            setAuditProperties();
        }

        private void setAuditProperties()
        {
            this.SetAuditableEntityOnBeforeSaveChanges();
        }
    }
}


مشکل! رکوردهای وابسته حذف نمی‌شوند!

حالت پیش‌فرض حذف رکوردها در EFCore به cascade delete تنظیم شده‌است. یعنی اگر blog با id=1 حذف شود، نه فقط این blog، بلکه تمام مطالب وابسته‌ی به آن نیز حذف خواهند شد. اما در اینجا اگر این بلاگ را حذف کنیم:
 ar blog1 = context.Blogs.FirstOrDefault(blog => blog.Id == 1);
if (blog1 != null)
{
   context.Remove(blog1);

   context.SaveChanges();
}
تنها تک رکورد متناظر با آن حذف منطقی شده و مطالب متناظر با آن خیر. برای رفع این مشکل باید به صورت زیر عمل کرد:
var blog1AndItsRelatedPosts = context.Blogs
    .Include(blog => blog.Posts)
    .FirstOrDefault(blog => blog.Id == 1);
if (blog1AndItsRelatedPosts != null)
{
    context.Remove(blog1AndItsRelatedPosts);

    context.SaveChanges();
}
ابتدا باید رکوردهای وابسته را توسط یک Include به حافظه وارد کرد و سپس دستور Delete را بر روی کل آن صادر نمود که یک چنین خروجی را تولید می‌کند:
SELECT [t].[Id], [t].[DeletedAt], [t].[IsDeleted], [t].[Name], [t0].[Id], [t0].[BlogId], [t0].[DeletedAt], [t0].[IsDeleted], [t0].[Title]
FROM (
SELECT TOP(1) [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE ([b].[IsDeleted] <> CAST(1 AS bit)) AND ([b].[Id] = 1)
) AS [t]
LEFT JOIN (
SELECT [p].[Id], [p].[BlogId], [p].[DeletedAt], [p].[IsDeleted], [p].[Title]
FROM [Posts] AS [p]
WHERE [p].[IsDeleted] <> CAST(1 AS bit)
) AS [t0] ON [t].[Id] = [t0].[BlogId]
ORDER BY [t].[Id], [t0].[Id]

Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:25:00' (Nullable = true), @p1='True',
 @p5='2', @p3='2020-09-17T05:25:00' (Nullable = true), @p4='True', @p8='3',
@p6='2020-09-17T05:25:00' (Nullable = true), @p7='True',
 @p11='4', @p9='2020-09-17T05:25:00' (Nullable = true), @p10='True'], CommandType='Text', CommandTimeout='30']

SET NOCOUNT ON;
UPDATE [Blogs] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p3, [IsDeleted] = @p4
WHERE [Id] = @p5;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p6, [IsDeleted] = @p7
WHERE [Id] = @p8;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p9, [IsDeleted] = @p10
WHERE [Id] = @p11;
SELECT @@ROWCOUNT;
ابتدا اولین بلاگ را حذف منطقی کرده؛ سپس تمام مطالب متناظر با آن‌را که پیشتر حذف منطقی نشده‌اند، یکی یکی به صورت حذف شده، علامتگذاری می‌کند. به این ترتیب cascade delete منطقی نیز در اینجا میسر می‌شود.


یک نکته: مشکل حذف منطقی و رکوردهای منحصربفرد

فرض کنید در جدولی، فیلد نام کاربری را به عنوان یک فیلد منحصربفرد تعریف کرده‌اید و اکنون رکوردی در این بین، حذف منطقی شده‌است. مشکلی که در آینده بروز خواهد کرد، عدم امکان ثبت رکورد جدیدی با همان نام کاربری است که حذف منطقی شده‌است؛ چون یک unique index بر روی آن وجود دارد. در این حالت اگر از SQL Server استفاده می‌کنید، از قابلیتی به نام filtered indexes پشتیبانی می‌کند که در آن امکان تعریف یک شرط و predicate، در حین تعریف ایندکس‌ها وجود دارد. در این حالت می‌توان رکوردهای حذف منطقی شده را به ایندکس وارد نکرد.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreSoftDelete.zip
مطالب
روش آپلود فایل‌ها به همراه اطلاعات یک مدل در برنامه‌های Blazor WASM 5x
از زمان Blazor 5x، امکان آپلود فایل به صورت استاندارد به Blazor اضافه شده‌است که نمونه‌ی Blazor Server آن‌را پیشتر در مطلب «Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر» مطالعه کردید. در تکمیل آن، روش آپلود فایل‌ها در برنامه‌های WASM را نیز بررسی خواهیم کرد. این برنامه از نوع hosted است؛ یعنی توسط دستور dotnet new blazorwasm --hosted ایجاد شده‌است و به صورت خودکار دارای سه بخش Client، Server و Shared است.



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

فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کرده‌ایم:
using System.ComponentModel.DataAnnotations;

namespace BlazorWasmUpload.Shared
{
    public class User
    {
        [Required]
        public string Name { get; set; }

        [Required]
        [Range(18, 90)]
        public int Age { get; set; }
    }
}

ساختار کنترلر Web API دریافت کننده‌ی مدل برنامه

در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
namespace BlazorWasmUpload.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class FilesController : ControllerBase
    {
        [HttpPost]
        public async Task<IActionResult> CreateUser(
            [FromForm] User userModel,
            [FromForm] IList<IFormFile> inputFiles = null)
یعنی در سمت Web API، قرار است اطلاعات مدل User و همچنین لیستی از فایل‌های آپلودی (احتمالی و اختیاری) را یکجا و در طی یک عملیات Post، دریافت کنیم. در اینجا نام پارامترهایی را هم که انتظار داریم، دقیقا userModel و inputFiles هستند. همچنین فایل‌های آپلودی باید بتوانند ساختار IFormFile استاندارد ASP.NET Core را تشکیل داده و به صورت خودکار به پارامترهای تعریف شده، bind شوند. به علاوه content-type مورد انتظار هم FromForm است.


ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایل‌های انتخابی کاربر

کدهای کامل سرویسی که می‌تواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorWasmUpload.Client.Services
{
    public interface IFilesManagerService
    {
        Task<HttpResponseMessage> PostModelWithFilesAsync<T>(string requestUri,
            IEnumerable<IBrowserFile> browserFiles,
            string fileParameterName,
            T model,
            string modelParameterName);
    }

    public class FilesManagerService : IFilesManagerService
    {
        private readonly HttpClient _httpClient;

        public FilesManagerService(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<HttpResponseMessage> PostModelWithFilesAsync<T>(
            string requestUri,
            IEnumerable<IBrowserFile> browserFiles,
            string fileParameterName,
            T model,
            string modelParameterName)
        {
            var requestContent = new MultipartFormDataContent();
            requestContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");

            if (browserFiles?.Any() == true)
            {
                foreach (var file in browserFiles)
                {
                    var stream = file.OpenReadStream(maxAllowedSize: 512000 * 1000);
                    requestContent.Add(content: new StreamContent(stream, (int)file.Size), name: fileParameterName, fileName: file.Name);
                }
            }

            requestContent.Add(
                content: new StringContent(JsonSerializer.Serialize(model), Encoding.UTF8, "application/json"),
                name: modelParameterName);

            var result = await _httpClient.PostAsync(requestUri, requestContent);
            result.EnsureSuccessStatusCode();
            return result;
        }
    }
}
توضیحات:
- کامپوننت استاندارد InputFiles در Blazor Wasm، می‌تواند لیستی از IBrowserFile‌های انتخابی توسط کاربر را در اختیار ما قرار دهد.
- fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شده‌است.
- model جنریک، برای نمونه وهله‌ای از شیء User است که به یک فرم Blazor متصل است.
- modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شده‌است.

- در ادامه یک MultipartFormDataContent را تشکیل داده‌ایم. توسط این ساختار می‌توان فایل‌ها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContent‌ها را اضافه می‌کنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل می‌شوند. متد OpenReadStream به صورت پیش‌فرض فقط فایل‌هایی تا حجم 500 کیلوبایت را پردازش می‌کند و اگر فایلی حجیم‌تر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت می‌توان توسط پارامتر maxAllowedSize آن، این مقدار پیش‌فرض را تغییر داد.

- در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شده‌است. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشته‌ی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشته‌ی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی می‌توان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال می‌شود؛ چون MultipartFormDataContent ویژه‌ای را داریم، model-binder پیش‌فرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.

- تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده می‌کنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.

- در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.

پس از تهیه‌ی سرویس ویژه‌ی فوق که می‌تواند اطلاعات فایل‌ها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگی‌ها در فایل Program.cs برنامه‌ی کلاینت است:
namespace BlazorWasmUpload.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...

            builder.Services.AddScoped<IFilesManagerService, FilesManagerService>();

            // ...
        }
    }
}


تکمیل فرم ارسال اطلاعات مدل و فایل‌های همراه آن در برنامه‌ی Blazor WASM

در ادامه پس از تشکیل IFilesManagerService، نوبت به استفاده‌ی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر می‌دهیم:
@code
{
    IReadOnlyList<IBrowserFile> SelectedFiles;
    User UserModel = new User();
    bool isProcessing;
    string UploadErrorMessage;
در اینجا فیلدهای مورد استفاده‌ی در فرم برنامه مشخص شده‌اند:
- SelectedFiles همان لیست فایل‌های انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص می‌کنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایل‌ها مانند «فقط تصاویر را انتخاب کنید» را تعریف می‌کند.

بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل می‌دهیم:
@page "/"

@using System.IO
@using BlazorWasmUpload.Shared
@using BlazorWasmUpload.Client.Services

@inject IFilesManagerService FilesManagerService

<h3>Post a model with files</h3>

<EditForm Model="UserModel" OnValidSubmit="CreateUserAsync">
    <DataAnnotationsValidator />
    <div>
        <label>Name</label>
        <InputText @bind-Value="UserModel.Name"></InputText>
        <ValidationMessage For="()=>UserModel.Name"></ValidationMessage>
    </div>
    <div>
        <label>Age</label>
        <InputNumber @bind-Value="UserModel.Age"></InputNumber>
        <ValidationMessage For="()=>UserModel.Age"></ValidationMessage>
    </div>
    <div>
        <label>Photos</label>
        <InputFile multiple disabled="@isProcessing" OnChange="OnInputFileChange" />
        @if (!string.IsNullOrWhiteSpace(UploadErrorMessage))
        {
            <div>
                @UploadErrorMessage
            </div>
        }
        @if (SelectedFiles?.Count > 0)
        {
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Size (bytes)</th>
                        <th>Last Modified</th>
                        <th>Type</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var selectedFile in SelectedFiles)
                    {
                        <tr>
                            <td>@selectedFile.Name</td>
                            <td>@selectedFile.Size</td>
                            <td>@selectedFile.LastModified</td>
                            <td>@selectedFile.ContentType</td>
                        </tr>
                    }
                </tbody>
            </table>
        }
    </div>
    <div>
        <button disabled="@isProcessing">Create user</button>
    </div>
</EditForm>
توضیحات:
- UserModel که وهله‌ی از شیء اشتراکی User است، به EditForm متصل شده‌است.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت می‌کنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر می‌کند. پس از انتخاب فایل‌ها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
    private void OnInputFileChange(InputFileChangeEventArgs args)
    {
        var files = args.GetMultipleFiles(maximumFileCount: 15);
        if (args.FileCount == 0 || files.Count == 0)
        {
            UploadErrorMessage = "Please select a file.";
            return;
        }

        var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
        if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
        {
            UploadErrorMessage = "Please select .jpg/.jpeg/.png files only.";
            return;
        }

        SelectedFiles = files;
        UploadErrorMessage = string.Empty;
    }
- در اینجا امضای متد رویداد گردان OnChange را مشاهده می‌کنید. توسط متد GetMultipleFiles می‌توان لیست فایل‌های انتخابی توسط کاربر را دریافت کرد. نیاز است پارامتر maximumFileCount آن‌را نیز تنظیم کنیم تا دقیقا مشخص شود چه تعداد فایلی مدنظر است؛ بیش از آن، یک استثناء را صادر می‌کند.
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش می‌دهیم.
- در پایان این متد، لیست فایل‌های دریافتی را به فیلد SelectedFiles انتساب می‌دهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.

مرحله‌ی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
    private async Task CreateUserAsync()
    {
        try
        {
            isProcessing = true;
            await FilesManagerService.PostModelWithFilesAsync(
                        requestUri: "api/Files/CreateUser",
                        browserFiles: SelectedFiles,
                        fileParameterName: "inputFiles",
                        model: UserModel,
                        modelParameterName: "userModel");
            UserModel = new User();
        }
        finally
        {
            isProcessing = false;
            SelectedFiles = null;
        }
    }
- در اینجا زمانیکه isProcessing به true تنظیم می‌شود، دکمه‌ی ارسال اطلاعات، غیرفعال خواهد شد؛ تا از کلیک چندباره‌ی بر روی آن جلوگیری شود.
- سپس روش استفاده‌ی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده می‌کنید که اطلاعات فایل‌ها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال می‌کند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آماده‌ی دریافت اطلاعات بعدی می‌کنیم.


تکمیل کنترلر Web API دریافت کننده‌ی مدل برنامه

در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کننده‌ی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آن‌را مشاهده می‌کنید:
using System.IO;
using Microsoft.AspNetCore.Mvc;
using BlazorWasmUpload.Shared;
using Microsoft.AspNetCore.Hosting;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using BlazorWasmUpload.Server.Utils;
using System.Linq;

namespace BlazorWasmUpload.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class FilesController : ControllerBase
    {
        private const int MaxBufferSize = 0x10000;

        private readonly IWebHostEnvironment _webHostEnvironment;
        private readonly ILogger<FilesController> _logger;

        public FilesController(
            IWebHostEnvironment webHostEnvironment,
            ILogger<FilesController> logger)
        {
            _webHostEnvironment = webHostEnvironment;
            _logger = logger;
        }

        [HttpPost]
        public async Task<IActionResult> CreateUser(
            //[FromForm] string userModel, // <-- this is the actual form of the posted model
            [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
            [FromForm] IList<IFormFile> inputFiles = null)
        {
            /*var user = JsonSerializer.Deserialize<User>(userModel);
            _logger.LogInformation($"userModel.Name: {user.Name}");
            _logger.LogInformation($"userModel.Age: {user.Age}");*/

            _logger.LogInformation($"userModel.Name: {userModel.Name}");
            _logger.LogInformation($"userModel.Age: {userModel.Age}");

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

            if (inputFiles?.Any() == true)
            {
                foreach (var file in inputFiles)
                {
                    if (file == null || file.Length == 0)
                    {
                        continue;
                    }

                    var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                    using var fileStream = new FileStream(filePath,
                                                            FileMode.Create,
                                                            FileAccess.Write,
                                                            FileShare.None,
                                                            MaxBufferSize,
                                                            useAsync: true);
                    await file.CopyToAsync(fileStream);
                    _logger.LogInformation($"Saved file: {filePath}");
                }
            }

            return Ok();
        }
    }
}
نکات تکمیلی این کنترلر را در مطلب «بررسی روش آپلود فایل‌ها در ASP.NET Core» می‌توانید مطالعه کنید و از این لحاظ هیچ نکته‌ی جدیدی را به همراه ندارد؛ بجز پارامتر userModel آن:
[ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
همانطور که عنوان شد، userModel ارسالی به سمت سرور چون به همراه تعدادی فایل است، به صورت خودکار به شیء User نگاشت نخواهد شد. به همین جهت نیاز است model-binder سفارشی زیر را برای آن تهیه کرد:
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace BlazorWasmUpload.Server.Utils
{
    public class JsonModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult != ValueProviderResult.None)
            {
                bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

                var valueAsString = valueProviderResult.FirstValue;
                var result = JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType);
                if (result != null)
                {
                    bindingContext.Result = ModelBindingResult.Success(result);
                    return Task.CompletedTask;
                }
            }

            return Task.CompletedTask;
        }
    }
}
در اینجا مقدار رشته‌ای پارامتر مزین شده‌ی توسط JsonModelBinder فوق، توسط متد استاندارد JsonSerializer.Deserialize تبدیل به یک شیء شده و به آن پارامتر انتساب داده می‌شود. اگر نخواهیم از این model-binder سفارشی استفاده کنیم، ابتدا باید پارامتر دریافتی را رشته‌ای تعریف کنیم و سپس خودمان کار فراخوانی متد JsonSerializer.Deserialize را انجام دهیم:
[HttpPost]
public async Task<IActionResult> CreateUser(
            [FromForm] string userModel, // <-- this is the actual form of the posted model
            [FromForm] IList<IFormFile> inputFiles = null)
{
  var user = JsonSerializer.Deserialize<User>(userModel);


یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمی‌شود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه می‌شود و این API استاندارد، هنوز از streaming پشتیبانی نمی‌کند . حتی ممکن است با کمی جستجو به راه‌حل‌هایی که سعی کرده‌اند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راه‌حل‌ها تنها کاری را که انجام می‌دهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده می‌شود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmUpload.zip
مطالب
اصول طراحی شیء گرا: OO Design Principles - قسمت سوم

اصل هفتم: Liskove Substitution Principle

"ارث بری باید به صورتی باشد که زیر نوع را بتوان بجای ابر نوع استفاده کرد"

این اصل می‌گوید اگر قرار است از ارث بری استفاده شود، نحوه‌ی استفاده باید بدین گونه باشد که اگر یک شیء از کلاس والد ( Base-Parent-Super type ) داشته باشیم، باید بتوان آن را با شیء کلاس فرزند ( Sub Type-Child ) بدون هیچ گونه تغییری در منطق کد استفاده کننده از شیء مورد نظر، تغییر داد. به زبان ساده باید بتوان شیء فرزند را جایگزین شیء والد کرد.

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

با رعایت این اصل، میتوانیم در مواقعی که شروط مرتبط با کلاس فرزند را نداریم و یک سری منطق و قیود کلی مرتبط با کلاس والد را داریم، از شیء کلاس والد استفاده نماییم و وظیفه  نمونه گیری (instantiation ) آن را به یک کلاس دیگر محول کنیم. به مثال زیر توجه کنید:

 public class Parent
    {
        public string Name { get; set; }
        public int X { get; set; }
        public int Y { get; set; }
        public Parent()
        {
            X = Y = 0;
        }
        public virtual void Move()
        {
            X += 5;
            Y += 5;
        }
        public void Shoot() { }
        public virtual void Pass() { } 
    }
    public class Child1 : Parent
    {
        public override void Move()
        {
            X += 10;
            Y += 10;
        }
    }
    public class Child2 : Parent
    {
        public override void Move()
        {
            X += 20;
            Y += 20;
        }
    }
    public enum State
    {
        Start,
        Move,
        Shoot,
        Pass
    }
    public class Creator
    {
        public static Parent GetInstance(bool? condition)
        {
            if (condition == null)
            {
                return new Parent();
            }
            if (condition == true)
            {
                return new Child1();
            }
            else
            {
                return new Child2();
            }
        }
    }
    public class Context
    {
        public void SetState(ref State s)
        {
            s = State.Move;
        }
        public void Main()
        {
            State state =State.Start;
            
            // در مورد نوع این شیء چیزی نمیدانیم و وابسته به شرایط نوع آن متغیر است
            // در حقیقت شیء کلاس فرزند را جای شیء کلاس والد  قرار می‌دهیم و نه بالعکس

            Parent obj = Creator.GetInstance(null);
            
            // منطق برنامه وضعیت را تغییر می‌دهد
            SetState(ref state);

            // قواعد کلی و عمومی که بدون در نظر گرفتن کلاس (نوع) شیء بر آن اعمال می‌شود
            switch (state)
            {
                case State.Move:
                    obj.Move();
                    break;
                case State.Shoot:
                    obj.Shoot();
                    break;
                case State.Pass:
                    obj.Pass();
                    break;
                default:
                    break;
            }           
        }
    }

همانطور که در کدها نیز توضیح داده‌ام، کلاس‌های فرزند را جایگزین کلاس والد کرده‌ایم. اگر می‌خواستیم عکس رابطه را (شیء والد را به شیء فرزند انتقال دهیم) اعمال کنیم باید تغییر زیر را ایجاد میکردیم که با خطا روبرو خواهد شد:

Child1 obj = Creator.GetInstance(null);



اصل هشتم: Interface segregation

"واسط‌های کوچک بهتر از واسط‌های حجیم است"

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

 public interface IHuman
    {
        void Move();
        void Eat();
        void LevelUp();
        void FireBullet();
    }
    public class Player : IHuman
    {
        public void Eat() { }
        public void FireBullet() { }
        public void LevelUp() { }
        public void Move() { }
    }
    public class Enemy : IHuman
    {
        public void Eat() { }
        public void FireBullet() { }
        public void LevelUp() { }
        public void Move() { }
    }
    public class Citizen : IHuman
    {
        public void Eat() { }
        public void FireBullet() { }
        public void LevelUp() { }
        public void Move() { }
    }

در این مثال که مربوط به مدل یک بازی با نقش‌های بازیکن، دشمن و شهروند (بی گناه!) است، طراحی به گونه‌ای است که دشمن و شهروند، توابعی را که نیاز ندارند، باید پیاده سازی کنند. در دشمن: Eat(), LevelUp() و در شهروند: Eat(), LevelUp(), FireBullet() . لذا واسط IHuman یک واسط کلی با وظیفه‌های متعدد است.

در مدل بهبود یافته که کلاس‌ها با پسوند Better بازنویسی شده‌اند داریم:

public interface IMovable { void Move(); }
    public interface IEatable { void Eat(); }
    public interface IPlayer { void LevelUp(); }
    public interface IShooter { void FireBullet(); }
    public class PlayerBetter : IPlayer, IMovable, IEatable, IShooter
    {
        public void Eat() { }
        public void FireBullet() { }
        public void LevelUp() { }
        public void Move() { }
    }
    public class EnemyBetter : IMovable, IShooter
    {
        public void FireBullet() { }
        public void Move() { }
    }
    public class CitizenBetter : IMovable
    {
        public void Move() { }
    }

در اینجا برای هر وظیفه یک واسط تعریف کرده ایم که باعث قوی شدن معنای هر واسط می‌شود.



اصل نهم: Dependency inversion

"وابستگی بین ماژول‌ها  را به وابستگی آن‌ها به انتزاع (واسط) تغییر بده"

این اصل که نمود آن را در الگو‌های طراحی dependency injection و factory میبینیم، میگوید که ماژول‌های بالادست (ماژول استفاده کننده ماژول پایین دست) به جای آنکه ارجاع مستقیمی را به ماژول‌های پایین دست داشته باشند، به انتزاعی (واسط) ارجاع بدهند که ماژول پایین دست آنرا پیاده سازی می‌کند یا به ارث میبرد. در واقع این اصل برای از بین بردن وابستگی قوی بین ماژول‌های بالا دست و پایین دست، به میدان آمده است. دو حکم اصلی از این اصل بر می‌آید:

الف – ماژول‌های بالا دست نباید وابسته به ماژول‌های پایین دست باشند. هر دو باید وابسته به انتزاع (واسط) باشند. وابستگی ماژول بالا دست از نوع ارجاع و وابستگی ماژول پایین دست از نوع ارث بری است.

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

در مثال زیر با نمونه‌ای از طراحی ناقض این اصل روبرو هستیم:

public class Controller
    {
        public Service Service { get; set; }
        public Controller()
        {
            // کنترلر باید نحوه نمونه گیری را بداند (ورودی‌های لازم) و این از وظایف آن خارج است
            Service = new Service(1);
        }
        public void DoWork()
        {
            Service.RunService();
        }
    }

    public class Service
    {
        public int State { get; set; }
        public Service(int s)
        {
            State = s;
        }
        public void RunService() { }
    }

در این مثال کلاس کنترلر، ماژول بالادست و کلاس سرویس، ماژول پایین دست محسوب میگردد. در ادامه طراحی مطلوب را نیز ارائه داده‌ام:

public class ControllerBetter
    {
        // ارجاع به واسط باعث انعطاف و کاهش وابستگی شده است
        public IService Service { get; set; }
        public ControllerBetter(IService service)
        {
            // یک کلاس دیگر وظیفه ارسال سرویس به سازنده کلاس کنترلر را دارد 
            // و مسئولیت نمونه گیری را از دوش کنترلر برداشته است
            Service = service;
        }
        public void DoWork()
        {
            Service.RunService();
        }
    }
    // کاهش وابستگی با تعریف واسط و تغییر وابستگی مستقیم بین کنترلر و سرویس
    public interface IService
    {
        void RunService();
    }
    // وابستگی جزییات به انتزاع
    public class ServiceBetter : IService
    {
        public int State { get; set; }
        public ServiceBetter(int s)
        {
            State = s;
        }
        public void RunService() { }
    }

نحوه بهبود طراحی را در توضیحات داخل کد مشاهده میکنید. در مقاله بعدی به اصول GRASP خواهم پرداخت. 

نظرات مطالب
Attribute Routing در ASP.NET MVC 5
سلام
الان من در قسمت route.config به این صورت کدهام تعریف شده

       public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.MapMvcAttributeRoutes();
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "List", id = UrlParameter.Optional }
            );                
        }
و اومدم توی کنترل Home و اکشن موبوط به اون که دارای یک پارامتر دریافتی برای پیج هست این کد رو تعریف کردم:
[Route("Page/{page?}")]
        public ActionResult List(int page=1)
        {
اما وقتی برنامه رو اجرا میکنم خطای 404 رو میده. ممنون میشم راهنمایی کنید مشکل از کجاست.
مطالب
دات نت 4 و کلاس Lazy

یکی از الگوهای برنامه نویسی شیء گرا، Lazy Initialization Pattern نام دارد که دات نت 4 پیاده سازی آن‌را سهولت بخشیده است.
در دات نت 4 کلاس جدیدی به فضای نام System اضافه شده است به نام Lazy و هدف از آن lazy initialization است؛ من ترجمه‌اش می‌کنم وهله سازی با تاخیر یا به آن on demand construction هم گفته‌اند (زمانی که به آن نیاز هست ساخته خواهد شد).
فرض کنید در برنامه‌ی خود نیاز به شیءایی دارید و ساخت این شیء بسیار پرهزینه است. نیازی نیست تا بلافاصله پس از تعریف، این شیء ساخته شود و تنها زمانیکه به آن نیاز است باید در دسترس باشد. کلاس Lazy جهت مدیریت اینگونه موارد ایجاد شده است. تنها کاری که در اینجا باید صورت گیرد، محصور کردن آن شیء هزینه‌بر توسط کلاس Lazy است:

Lazy<ExpensiveResource> ownedResource = new Lazy<ExpensiveResource>();

در این حالت برای دسترسی به شیء ساخته شده از ExpensiveResource ، می‌توان از خاصیت Value استفاده نمود (ownedResource.Value). تنها در حین اولین دسترسی به ownedResource.Value ، شیء ExpensiveResource ساخته خواهد شد و نه پیش از آن و نه در اولین جایی که تعریف شده است. پس از آن این حاصل cache شده و دیگر وهله سازی نخواهد شد.
ownedResource دارای خاصیت IsValueCreated نیز می‌باشد و جهت بررسی ایجاد آن شیء می‌تواند مورد استفاده قرار گیرد. برای مثال قصد داریم اطلاعات ExpensiveResource را ذخیره کنیم اما تنها در حالتیکه یکبار مورد استفاده قرار گرفته باشد.
کلاس Lazy دارای دو متد سازنده‌ی دیگر نیز می‌باشد:
public Lazy(bool isThreadSafe);
public Lazy(Func<T> valueFactory, bool isThreadSafe);

و هدف از آن استفاده‌ی صحیح از این متد در محیط‌های چند ریسمانی است. بدیهی است در این نوع محیط‌ ها علاقه‌ای نداریم که در یک لحظه توسط چندین ترد مختلف، سبب ایجاد وهله‌های ناخواسته‌ا‌ی از ExpensiveResource شویم و تنها یک مورد از آن کافی است یا به قولی thread safe, lazy initialization of expensive objects
بدیهی است اگر برنامه‌ی شما چند ریسمانی نیست می‌توانید این مکانیزم را کنسل کرده و اندکی کارآیی برنامه را با حذف قفل‌های همزمانی این کلاس بالا ببرید.

مثال اول:

using System;
using System.Threading;

namespace LazyExample
{
class Program
{
static void Main()
{
Console.WriteLine("Before assignment");
var slow = new Lazy<Slow>();
Console.WriteLine("After assignment");

Thread.Sleep(1000);

Console.WriteLine(slow);
Console.WriteLine(slow.Value);

Console.WriteLine("Press a key...");
Console.Read();
}
}


class Slow
{
public Slow()
{
Console.WriteLine("Start creation");
Thread.Sleep(2000);
Console.WriteLine("End creation");
}
}
}
خروجی این برنامه به شرح زیر است:

Before assignment
After assignment
Value is not created.
Start creation
End creation
LazyExample.Slow
Press a key...

همانطور که ملاحظه می‌کنید تنها در حالت دسترسی به مقدار Value شیء slow ، عملا وهله‌ای از آن ساخته خواهد شد.

مثال دوم:
شاید نیاز به مقدار دهی خواص کلاس پرهزینه‌ وجود داشته باشد. برای مثال علاقمندیم خاصیت SomeProperty کلاس ExpensiveClass را مقدار دهی کنیم. برای این منظور می‌توان به شکل ذیل عمل کرد (یک Func<t>را می‌توان به سازنده‌ی آن ارسال نمود):

using System;

namespace LazySample
{
class Program
{
static void Main()
{
var expensiveClass =
new Lazy<ExpensiveClass>
(
() =>
{
var fobj = new ExpensiveClass
{
SomeProperty = 100
};
return fobj;
}
);

Console.WriteLine("expensiveClass has value yet {0}",
expensiveClass.IsValueCreated);

Console.WriteLine("expensiveClass.SomeProperty value {0}",
(expensiveClass.Value).SomeProperty);

Console.WriteLine("expensiveClass has value yet {0}",
expensiveClass.IsValueCreated);

Console.WriteLine("Press a key...");
Console.Read();
}
}

class ExpensiveClass
{
public int SomeProperty { get; set; }

public ExpensiveClass()
{
Console.WriteLine("ExpensiveClass constructed");
}
}
}

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

اشتراک‌ها
معرفی Rivets.js

Lightweight and powerful data binding + templating solution for building modern web applications. 

معرفی Rivets.js