نظرات مطالب
راه اندازی StimulSoft Report در ASP.NET MVC
با توجه به عرضه نسخه net core.، این گزارشگر هم تغییراتی در نسخه‌های اخیر خود داده و هم اینکه پیشرفت‌های زیادی نیز کرده است.
بعد از نصب نسخه مورد نظر (برای نسخه 3.1 نیاز به نصب استیمول 2020 است) طبق این لینک مشکل Trial بودن (برای ما)  را حل کنید.
در مورد این لینک چند نکته قابل ذکر است:
1- از شما میخواهد که dll‌های جایگزین بعد از build، با dll‌ها اصلی جابجا شوند که انجام این مورد در Visual Studio به خاطر داشتن UI برای اینکار به راحتی انجام می‌شد ولی در Visual Studio Code و Rider، چنین موردی باید به صورت دستی صورت بگیرد؛ به همین علت در فایل csproj پروژه اصلی، کدهای زیر را اضافه کنید:
  <Target Name="PostBuild" AfterTargets="PostBuildEvent">
    <Exec Command="xcopy /y /d $(ProjectDir)Packages\*.* $(OutDir)" />
  </Target>
2- همچنین در نهایت باید فایل لایسنس نیز در پروژه قرار گرفته و در Startup به استیمول معرفی گرد. به همین جهت کلاس زیر را اضافه کنید:
 public static class StimulSoftLicense
    {
        public static void LoadLicense(IWebHostEnvironment environment)
        {
            var contentRoot = environment.ContentRootPath;
            var licenseFile = System.IO.Path.Combine(contentRoot,"Reports", "license.key");
            Stimulsoft.Base.StiLicense.LoadFromFile(licenseFile);
        }
    }
که در خط دوم مسیر فایل لایسنس معرفی شده است که برای پروژه شما باید اصلاح شود و در startup.cs به صورت زیر استفاده میشود:
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
           StimulSoftLicense.LoadLicense(env);
}
برای نمایش گزارش همه چیز کماکان مثل سابق است فقط یک سری جزئیات تغییر نموده است:
        public IActionResult RequestReport(int id)
        {
            return View();
        }

        public IActionResult LoadReportData(int id)
        {
            StiReport report = new StiReport();
            report.Load(StiNetCoreHelper.MapPath(this, "Reports/Requests/RequestInfo.mrt"));
            var landDetailsReport = GetOwnerReportData(id);
            report.RegBusinessObject("Land", landDetailsReport);

            return StiNetCoreViewer.GetReportResult(this, report);
        }
         public IActionResult ViewerEvent()
        {
            return StiNetCoreViewer.ViewerEventResult(this);
        }
همچنین در ویو RequestReport.cshtml کد به شکل زیر تغییر میکند:
<div style="direction: ltr">
    
@Html.StiNetCoreViewer(new StiNetCoreViewerOptions()
{
    Theme = StiViewerTheme.Office2007Silver,
    
    Appearance =
    {
        RightToLeft = true,
        //ShowTooltips = false,
        ShowTooltipsHelp = false,
        
    },
Localization = "~/Reports/fa.xml",
    Actions =
    {
        GetReport = "LoadReportData",
        ViewerEvent = "ViewerEvent",
        
    },
    Toolbar =
    {
      ShowAboutButton  = false,
        ShowOpenButton = false,
        ShowSaveButton = true,
        ShowFindButton = false,
        ShowEditorButton = false,
        ShowDesignButton = false,
        ShowBookmarksButton = false,
        ShowResourcesButton = false,
        ShowParametersButton = false,
        ShowPinToolbarButton = false

    },
    Exports =
    {
        DefaultSettings =
        {
            ExportToPdf =
            {
                CreatorString = "SANA"
            }
        },
        ShowExportDialog = false,
        ShowExportToDocument = false,
        ShowExportToExcel = false,
        ShowExportToExcel2007 = false,
        ShowExportToHtml = false,
        ShowExportToHtml5 = false,
        ShowExportToImageBmp = false,
        ShowExportToImageJpeg = false,
        ShowExportToImagePcx = false,
        ShowExportToImagePng = false,
        ShowExportToImageMetafile = false,
        ShowExportToImageTiff = false,
        ShowExportToOpenDocumentCalc = false,
        ShowExportToMht = false,
        ShowExportToOpenDocumentWriter = false,
        ShowExportToXps = false,
        ShowExportToSylk = false,
        ShowExportToRtf = false,
        ShowExportToExcelXml = false,
        ShowExportToText = false,
        ShowExportToWord2007 = false,
        ShowExportToImageGif = false,
        ShowExportToCsv = false,
        ShowExportToDbf = false,
        
        ShowExportToDif = false,
        ShowExportToImageSvg = false,
        ShowExportToImageSvgz = false,
        ShowExportToPowerPoint = false,
        ShowExportToXml = false,
        ShowExportToJson = false
    }
})
</div>
با توجه به نیاز کد بالا را تغییر دهید. تقریبا تا حدی خصوصیت‌ها تغییر کرده‌اند ولی نه خیلی.
یک نکته مهم در مورد div که این helper را محصور کرده وجود دارد و آن این است که حالت RTL  قالب شما روی Helper استیمول تاثیر میگذارد ( به خصوص روی خود گزارش) برای همین از تنظیمات داخل خود Helper برای اینکار استفاده میکنیم؛ ولی اگر قالب RTL باشد و استیمول هم روی RTL تنظیم شود در نوار ابزار، ترتیب ابزار‌ها معکوس میگردد. به همین دلیل حالت پیش فرض، یعنی LTR میباشد و با تظیم Helper به این مهم دست پیدا می‌کنیم.
کدهای زیر را نیز به فایل css صفحه اضافه کنید تا در گزارش ساز استیمول اعمال گردد:
.stiJsViewerClearAllStyles
{
    font-family: "IRANSans" !important;
}

//مخصوص دیالوگ تنظیمات خروجی در صورت فعال بودن
.stiJsViewerGroupPanelContainer{
    direction: rtl !important;
}
.stiJsViewerFormButtonDefault{
    direction: rtl !important;
}
.stiJsViewerFormButtonOver{
    direction: rtl !important;
}
.stiJsViewerFormButtonDefault>table>tbody>tr>td{
    text-align: right !important;
}
.stiJsViewerFormButtonOver>table>tbody>tr>td{
    text-align: right !important;
}
.stiJsViewerFormHeader{
    direction: rtl;
}
.stiJsViewerFormHeader>table>tbody>tr>td{
    text-align: right !important;
}
.stiJsViewerCheckBox{
    direction:rtl !important;
}
.stiJsViewerDropdownPanel{
    direction:rtl !important;
}
.stiJsViewerFormContainer td.stiJsViewerClearAllStyles{
    direction:rtl !important;
}
 .stiMvcViewerReportPanel table{
     direction:ltr !important;
 }
مطالب
فراخوانی GraphQL API در یک کلاینت Angular
در قسمت‌های قبل ( ^  ، ^ و  ^ ) GraphQL  را در ASP.Net Core راه اندازی کردیم و در قسمت ( فراخوانی GraphQL API در یک کلاینت ASP.NET Core  ) از GraphQL API  فراهم شده در یک کلاینت ASP Net Core  استفاده کردیم. اکنون می‌خواهیم چگونگی استفاده از GraphQL را در انگیولار، یاد بگیریم.
Apollo Angular، به شما اجازه میدهد داده‌ها را از یک سرور GraphQL دریافت و از آن برای ساختن UI ‌های واکنشی و پیچیده در انگیولار استفاده کنید. وقتی که از Apollo Client استفاده می‌کنیم، نیازی نیست هیچ چیز خاصی را در مورد سینتکس query ‌ها یادبگیریم؛ به دلیل اینکه همه چیز همان استاندارد GraphQL می‌باشد. هر چیزی را که شما در GraphQL query IDE تایپ می‌کنید، می‌توانید آن‌ها را در کد‌های Apollo Client نیز قرار دهید.

Installation with Angular Schematics 
 
بعد از ایجاد یک پروژه انگیولار با دستور زیر
ng new apollo-angular-project
در ترمینال VS Code  دستور زیر را وارد نمایید: 
ng add apollo-angular
تنها کاری که بعد از اجرای دستور بالا نیاز است انجام دهیم، تنظیم آدرس سرور GraphQL می‌باشد. از این رو فایل graphql.module.ts را باز کنید و متغیر uri را مقدار دهی کنید.
const uri = 'https://localhost:5001/graphql';
اکنون همه چیز تمام شده‌است. شما می‌توانید اولین query خود را اجرا کنید. 

Installation without Angular Schematics 

اگر می‌خواهید Apollo را بدون کمک گرفتن از Angular Schematics نصب کنید، در ابتدا کتابخانه‌های زیر را نصب نمائید:
npm install --save apollo-angular \
  apollo-angular-link-http \
  apollo-link \
  apollo-client \
  apollo-cache-inmemory \
  graphql-tag \
  graphql
کتابخانه apollo-client نیازمند AsyncIterable می‌باشد. به همین جهت مطمئن شوید فایل tsconfig.json شما به صورت زیر است:
{
  "compilerOptions": {
    // ...
    "lib": [
      "es2017",
      "dom",
      "esnext.asynciterable"
    ]
  }
}
اکنون شما تمام وابستگی‌های مورد نیاز را دارید. سپس می‌توانید اولین Apollo Client خود را اجرا کنید. 
در ادامه، فایل app.module.ts را باز کرده و آن را مطابق زیر ویرایش کنید:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HttpClientModule } from "@angular/common/http";
import { ApolloModule, APOLLO_OPTIONS } from "apollo-angular";
import { HttpLinkModule, HttpLink } from "apollo-angular-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";


@NgModule({
  declarations:
    [
      AppComponent
    ],
  imports:
    [
      BrowserModule,
      HttpClientModule,
      ApolloModule,
      HttpLinkModule
    ],
  providers:
    [
      {
        provide: APOLLO_OPTIONS,
        useFactory: (httpLink: HttpLink) => {
          return {
            cache: new InMemoryCache(),
            link: httpLink.create({
              uri: "https://localhost:5001/graphql"
            })
          }
        },
        deps:
          [
            HttpLink
          ]
      }],
  bootstrap:
    [
      AppComponent
    ]
})
export class AppModule { }
چه کاری در اینجا انجام شده است:
- با استفاده از سرویس apollo-angular-link-http و HttpLink، کلاینت را به یک سرور GraphQL متصل می‌کنیم. 
- apollo-cache-inmemory و InMemoryCache محلی برای ذخیره سازی داده‌ها می‌باشد. 
- APOLLO_OPTIONS فراهم کننده تنظیمات Apollo Client است.
- HttpLink نیاز به HttpClient دارد. به همین خاطر است که ما از HttpClientModule استفاده کرده‌ایم. 

Links and Cache 
 Apollo Client، یک لایه واسط شبکه قابل تعویض را دارد که به شما اجازه می‌دهد تا تنظیم کنید که چگونه query ‌ها در HTTP ارسال شوند؛ یا کل بخش Network را با چیزی کاملا سفارشی سازی شده جایگزین کنید؛ مثل یک websocket transport.
apollo-angular-link-http : از Http  برای ارسال query ‌ها استفاده می‌کند. 
apollo-cache-inmemory : پیاده سازی کش پیش فرض برای Apollo Client 2.0 می‌باشد. 
 
نکته
 Apollo یک سرویس export شده انگیولار از apollo-angular، برای به اشتراک گذاشتن داده‌های GraphQL با UI شما می‌باشد.

شروع کار
به همان روش که فایل‌های Model را برای کلاینت ASP.NET Core ایجاد کردیم، در اینجا هم ایجاد می‌کنیم. کار را با ایجاد کردن یک پوشه جدید به نام types، شروع می‌کنیم و چند type را در آن تعریف خواهیم کرد ( OwnerInputType ،AccountType  و OwnerType ):
export type OwnerInputType = {
    name: string;
    address: string;
}
export type AccountType = {
    'id': string;
    'description': string;
    'ownerId' : string;
    'type': string;
}
import { AccountType } from './accountType';
 
export type OwnerType = {
    'id': string;
    'name': string;
    'address': string;
    'accounts': AccountType[];
}

سپس یک سرویس را به نام graphql ایجاد می‌کنیم: 
ng g s graphql
و آن را همانند زیر ویرایش می‌کنیم: 
import { Injectable } from '@angular/core';
import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';

@Injectable({
  providedIn: 'root'
})
export class GraphqlService {

  constructor(private apollo: Apollo) { }
  
}
اکنون همه چیز آماده است تا تعدادی query و mutation را اجرا کنیم ( providedIn ).

بازیابی تمامی Owner ها 
سرویس graphql  را باز می‌کنیم و آن را همانند زیر ویرایش می‌کنیم ( اضافه کردن متد getOwners ):
  public getOwners = () => {
    return this.apollo.query({
      query: gql`query getOwners{
      owners{
        id,
        name,
        address,
        accounts{
          id,
          description,
          type
        }
      }
    }`
    });
  }

سپس فایل app.component.ts را باز کرده و همانند زیر ویرایش می‌کنیم: 
export class AppComponent implements OnInit {

  public owners: OwnerType[];
  public loading = true;

  constructor(private graphQLService: GraphqlService) { }

  ngOnInit() {
    this.graphQLService.getOwners().subscribe(result => {
      this.owners = result.data["owners"] as OwnerType[];
      this.loading = result.loading;
    });
  }
}

و هم چنین app.component.html:
<div>
    <div *ngIf="!this.loading">
        <table>
            <thead>
                <tr>
                    <th>
                        #
                    </th>
                    <th>
                        نام و نام خانوادگی
                    </th>
                    <th>
                        آدرس
                    </th>
                </tr>
            </thead>

            <tbody>
                <ng-container *ngFor="let item of this.owners;let idx=index" [ngTemplateOutlet]="innertable"
                    [ngTemplateOutletContext]="{item:item, index:idx}"></ng-container>
            </tbody>

        </table>
    </div>
    <div *ngIf="this.loading">
        <p>
            در حال بارگذاری لیست ...
        </p>
    </div>
</div>


<ng-template #innertable let-item="item" let-idx="index">
    <tr>
        <td>{{idx+1}}</td>
        <td>{{item.name}}</td>
        <td>{{item.address}}</td>
    </tr>
    <tr *ngIf="this.item.accounts && this.item.accounts.length > 0">
        <td colspan="4">
            <div>
                <p>Accounts</p>
            </div>
            <div>
                <table>
                    <thead>
                        <tr>
                            <th>#</th>
                            <th>نوع</th>
                            <th>توضیحات</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr *ngFor="let innerItem of this.item.accounts;let innerIndex=index">
                            <td>
                                {{innerIndex+1}}
                            </td>
                            <td>
                                {{innerItem.type}}
                            </td>
                            <td>
                                {{innerItem.description}}
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </td>
    </tr>
</ng-template>
اکنون جهت اجرای پروژه، کد‌های ضمیمه شده در پایان قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را دانلود نمایید و آن را اجرا کنید: 
dotnet restore
dotnet run
سپس پروژه را اجرا کنید: 
ng serve

خروجی به صورت زیر می‌باشد (لیست تمامی Owner ‌ها به همراه Account‌های مربوط به هر Owner):


در متد getOwner، بجای apollo.query می‌توان از apollo.watchQuery استفاده کرد که در نمونه زیر، در ابتدا، GraphQL query را در تابع gql ( از graphql-tag ) برای خصوصیت query در متد apollo.watchQuery پاس میدهیم.
  public getOwners = () => {
    return this.apollo.watchQuery<any>({
      query: gql`query getOwners{
        owners{
          id,
          name,
          address,
          accounts{
            id,
            description,
            type
          }
        }
      }`
    })
  }

و سپس در کامپوننت:
export class AppComponent implements OnInit {

  loading: boolean;
  public owners: OwnerType[];

  private querySubscription: Subscription;

  constructor(private graphQLService: GraphqlService) { }

  ngOnInit() {
    this.querySubscription = this.graphQLService.getOwners()
      .valueChanges
      .subscribe(result => {
        this.loading = result.loading;
        this.owners = result.data["owners"] as OwnerType[];
      });
  }

  ngOnDestroy() {
    this.querySubscription.unsubscribe();
  }
}
متد watchQuery، شیء QueryRef را برگشت می‌دهد که خصوصیت valueChanges را دارد که آن یک Observable می‌باشد. Observable تنها یک بار صادر خواهد شد و آن زمانی است که query کامل می‌شود و خصوصیت loading به false  تنظیم خواهد شد؛ مگر اینکه شما پارامتر notifyOnNetworkStatusChange را در watchQuery  به true  تنظیم کنید. شیء منتقل شده از طریق یک Observable، شامل خصوصیات loading ،error و data است. هنگامی که نتیجه query برگشت داده می‌شود، به خصوصیت data انتساب داده می‌شود. 
 


مابقی query ‌ها و mutation ها از سرویس graphql
بازیابی یک Owner  مشخص 

  public getOwner = (id) => {
    return this.apollo.query({
      query: gql`query getOwner($ownerID: ID!){
      owner(ownerId: $ownerID){
        id,
        name,
        address,
        accounts{
          id,
          description,
          type
        }
      }
    }`,
      variables: { ownerID: id }
    })
  }

ایجاد یک Owner جدید 
  public createOwner = (ownerToCreate: OwnerInputType) => {
    return this.apollo.mutate({
      mutation: gql`mutation($owner: ownerInput!){
        createOwner(owner: $owner){
          id,
          name,
          address
        }
      }`,
      variables: { owner: ownerToCreate }
    })
  }

ویرایش یک Owner 
  public updateOwner = (ownerToUpdate: OwnerInputType, id: string) => {
    return this.apollo.mutate({
      mutation: gql`mutation($owner: ownerInput!, $ownerId: ID!){
        updateOwner(owner: $owner, ownerId: $ownerId){
          id,
          name,
          address
        }
      }`,
      variables: { owner: ownerToUpdate, ownerId: id }
    })
  }

و در نهایت حذف یک Owner
  public deleteOwner = (id: string) => {
    return this.apollo.mutate({
      mutation: gql`mutation($ownerId: ID!){
        deleteOwner(ownerId: $ownerId)
       }`,
      variables: { ownerId: id }
    })
  }
اکنون شما می‌توانید با ویرایش کردن فایل app.component.ts یا ایجاد کردن کامپوننت‌های جدید، از نتایج بدست آمده استفاده کنید.


کد‌های کامل این قسمت را از ایجا دریافت کنید : GraphQL_Angular.zip
کد‌های کامل قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را از اینجا دریافت کنید :  ASPCoreGraphQL_3.rar  
مطالب
معرفی و استفاده از DDL Triggers در SQL Server

استفاده از DDL Trigger

امکان ایجاد Trigger برای عملیات (DDL(Data Definition Language از SQL Server 2005  فراهم گردید. عملیاتی مانند ایجاد یک جدول جدید در بانک اطلاعاتی، اضافه شدن یک Login جدید و یا ایجاد یک بانک اطلاعاتی جدید را به وسیله این نوع Trigger‌ها می‌توان کنترل نمود. در حقیقت DDL Trigger به شما اجازه می‌دهد که از  تاثیر تعدادی از دستورات DDL جلوگیری کنید. بدین ترتیب که تقریباً هر دستور DDL به طور خودکار، تراکنشی (Transactional) اجرا می‌شود . می‌توان با دستور ROLLBACK TRANSACTION اجرای دستور DDL را لغو نمود. توجه شود همه دستورات DDL به صورت تراکنشی اجرا نمی‌شوند، به عنوان مثال دستور  ALTER DATABASE ممکن است Database را تغییر دهد. در این صورت ساختار فایلی Database را تغییر می‌دهد، از آنجائی که سیستم عامل ویندوز به صورت تراکنشی عمل نمی‌کند بنابراین شما نمی‌توانید این عمل فایل سیستمی را لغو نمائید. به هر حال شما می‌توانید Trigger را با ALTER DATABASE فعال (fire) کنید برای عملیات Auditing، ولی نمی‌توان از انجام عمل  ALTER  DATABASE جلوگیری کرد.
برای نمونه می‌خواهیم از حذف و یا تغییر جداول یک بانک اطلاعاتی که به صورت عملیاتی در حال سرویس دهی است جلوگیری کنیم، برای اینکار از دستورهای زیر استفاده می‌کنیم:
create trigger Prevent_AlterDrop
on database
for drop_table, alter_table
as
  print 'table can not be dropped or altered'
  rollback transaction
از عبارت  ON  برای مشخص کردن محدوده Trigger در سطح SQL Instance (در این صورت ON All SERVER نوشته می‌شود) و یا در سطح Database (در این حالت ON DATABASE نوشته می‌شود)  استفاده می‌شود و از عبارت FOR  برای مشخص کردن رویداد یا گروه رویدادی که سبب فراخوانی  Trigger می‌شود، استفاده  خواهد شد. 

1- معرفی تابع ()EVENTDATA

  این تابع، یک تابع سیستمی مهم است که در DDL Trigger استفاده می‌شود. در حالیکه DDL Trigger در هر سطحی فعال (fire) شود تابع سیستمی ()EVENTDATA فراخوانی (raise) می‌شود. خروجی تابع در قالب XML است. می‌توان اطلاعات را از تابع EVENTDATA دریافت کرد و آنها را در یک جدول با فیلدی از جنس XML و یا با استفاده از XPath Query  ثبت کرد (Logging). عناصر کلیدی (Key Elements) تابع EVENTDATA  به شرح زیر است:
•  EventType: نوع رویدادی که باعث فراخوانی Trigger شده است.
•  PostTime: زمانی که رویداد رخ می‌دهد.
•  SPID :SPID کاربری که باعث ایجاد رویداد شده است.
•  ServerName:  نام SQL Instance  که  رویداد در آن رخ داده است.
•  LoginName: نام Login که عمل مربوط به وقوع رویداد را اجرا می‌کند.
•  UserName: نام User که عمل مربوط به وقوع رویداد را اجرا می‌کند.
•  DatabaseName: نام Database که رویداد در آن رخ می‌دهد.
•  ObjectType: نوع Object که اصلاح، حذف و یا ایجاد شده است.
•  ObjectName: نام Object که اصلاح، حذف و یا ایجاد شده است.
•  TSQLCommand: دستور T-SQL که اجرا شده و باعث اجرا شدن Trigger شده است. 

2- بررسی یک سناریو نمونه

  برای نمونه در دستورات زیر جدولی با نام ddl_log
  CREATE TABLE ddl_log 
  (
     EventType nvarchar(100), 
 PostTime datetime,
 SPID nvarchar(100),
 ServerName nvarchar(100),
 LoginName nvarchar(100),
 UserName  nvarchar(100),
 DatabaseName  nvarchar(100),
 ObjectName  nvarchar(100),
 ObjectType  nvarchar(100),
 DefaultSchema nvarchar(100),
 [SID]  nvarchar(100),
 TSQLCommand nvarchar(2000));
و یک Trigger با نام log برای رویدادهایی که در سطح Database رخ می‌دهد، ایجاد می‌کنیم.
CREATE TRIGGER [Log] ON DATABASE
 FOR DDL_DATABASE_LEVEL_EVENTS
 AS
 DECLARE @data XML
 SET @data = EVENTDATA()
 INSERT INTO ddl_log
 VALUES (
   @data.value('(/EVENT_INSTANCE/EventType)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/PostTime)[1]',   'datetime'),
   @data.value('(/EVENT_INSTANCE/SPID)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ServerName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/LoginName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/UserName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/DatabaseName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ObjectName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ObjectType)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/DefaultSchema)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/SID)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/TSQLCommand)[1]',   'nvarchar(2000)'));
نمونه ای از مقادیر ذخیره شده در جدول  ddl_log به شکل زیر خواهد بود:

3- ملاحظات

  در صورت فعال شدن  Trigger می‌توان برخی موارد مانند محدودیت زمانی، کاربر اجرا کننده و ... را  اضافه نمود. برای مثال در دستور زیر اجازه تغییرات در این زمان ( بین  7:00 A.M. تا .8:00 P.M ) امکان پذیر نیست و در صورت اقدام پیغام خطا دریافت می‌کنید و دستورات Create لغو خواهند شد و اگر خارج از زمان فوق دستورات DDL را اجرا کنید دستورات به طور موفقیت آمیز اجرا می‌شود و البته تغییرات نیز Log می‌شوند. 
 

  این Trigger تاثیرات کمی بر روی کارایی دارد به این دلیل که معمولاً رویداد‌های DDL به ندرت رخ می‌دهد. می‌توانید هنگامی که قصد دارید دستورات DDL را اجرا کنید موقتاً Trigger را با دستورات زیر غیر فعال نمائید:

پس از Overrdie کردن می‌توانید مجدداً Trigger را فعال کنید: 


4- معرفی DDL Event Groups: 

برای مشاهده جزئیات بیشتر می‌توانید به این لینک مراجعه کنید.

Server Level

DDL_SERVER_LEVEL_EVENTS

 DDL_LINKED_SERVER_EVENTS
 DDL_LINKED_SERVER_LOGIN_EVENTS
 DDL_REMOTE_SERVER_EVENTS
 DDL_EXTENDED_PROCEDURE_EVENTS
 DDL_MESSAGE_EVENTS
 DDL_ENDPOINT_EVENTS
 DDL_SERVER_SECURITY_EVENTS
 DDL_LOGIN_EVENTS
 DDL_GDR_SERVER_EVENTS
 DDL_AUTHORIZATION_SERVER_EVENT Database Level

Database Level

DDL_DATABASE_LEVEL_EVENTS  
DDL_TABLE_VIEW_EVENTS
DDL_TABLE-EVENTS
DDL_VIEW_EVENTS
DDL_INDEX_EVENTS
DDL_STATISTICS_EVENTS
DDL_DATABASE_SECURITY_EVENTS
DDL_CERTIFICATE_EVENTS
DDL_USER_EVENTS
DDL_ROLE_EVENTS
DDL_APPLICATION_ROLE_EVENTS
DDL_SCHEMA_EVENTS
DDL_GDR_DATABASE_EVENTS
DDL_AUTHORIZATION_DATABASE_EVENTS
DDL_FUNCTION_EVENTS
DDL_PROCEDUER_EVENTS
DDL_TRIGGER_EVENTS
DDL_PARTITION_EVENTS
DDL_PARTITION_FUNCTION_EVENTS
DDL_PARTITION_SCHEME_EVENTS
DDL_SSB_EVENTS
DDL_MESSAGE_TYPE_EVENTS
DDL_CONTRACT_EVENTS
DDL_QUEUE_EVENTS
DDL_SERVER_EVENTS
DDL_ROUTE_EVENTS
DDL_REMOTE_SERVICE_BINDING_EVENTS
DDL_XML_SCHEMA_COLEECTION_EVENTS
DDL_FULLTEXT_CATALOG_EVENTS
DDL_DEFAULT_EVENTS
DDL_EXTENDED_PROPERTY_EVENTS
DDL_PLAN_GUIDE_EVENTS
DDL_RULE_EVENTS
DDL_SYNONYM_EVENTS
DDL_EVENT_NOTIFICATION_EVENTS
DDL_ASSEMBLY_EVENTS
DDL_TYPE_EVENTS


مطالب
نحوه‌ی استفاده از ViewComponent درون Controller
در ASP.NET Core یک View Component، در نهایت خلاصه‌ایی از قابلیت‌هایی را ارائه میدهد که قرار است توسط یک کنترلر مدیریت شوند؛ زیرا پارامترهای یک View Component از طریق یک HTTP Request تامین نمی‌شوند. یعنی به صورت مستقیم از طریق درخواست‌های HTTP قابل دسترسی نیستند. فرض کنید در یک برنامه می‌خواهیم لیست کاربران سایت را نمایش دهیم تا با کلیک بر روی نام کاربر، امکان ویرایش کاربر انتخاب شده را داشته باشیم. با کلیک بر روی لینک مورد نظر، اطلاعات درخواست، به کنترلر UserManagerController و سپس اکشن متد Edit ارسال خواهد شد. در حالت عادی باید یک ViewComponent برای لیست کاربران و همچنین یک UserManagerController، برای ویرایش کاربر درون پروژه داشته باشیم:
public class UserListViewComponent : ViewComponent
{
    private readonly UserRepository repository;

    public UserListViewComponent(UserRepository repository)
    {
        this.repository = repository;
    }
    public IViewComponentResult Invoke()
    {
        var users = repository.GetUsers().Take(10).ToList();
        return View(model: users);
    }
}

ویووی کامپوننت فوق نیز به این صورت تعریف شده است:
@model IList<User>
@foreach (var user in Model)
{
    <li>
        <a asp-controller="UserManager" asp-action="Edit" asp-route-id="@user.Id">@user.Name</a>
    </li>
}
<a class="btn btn-info" asp-controller="UserManager" asp-action="Index">More...</a>

کنترلر UserManager نیز یک چنین تعریفی را دارد:
public class UserManagerController : Controller
{
    private readonly UserRepository repository;

    public UserManagerController(UserRepository repository)
    {
        this.repository = repository;
    }

    public ViewResult Index()
    {
        var users = repository.GetUsers().ToList();
        return View(users);
    }

    public ViewResult Edit(int id)
    {
        var user = repository.GetUsers().FirstOrDefault(u => u.Id == id);
        return View(user);
    }

    [HttpPost]
    public IActionResult Edit(User user)
    {
        repository.Edit(user);
        return RedirectToAction("Index", "Home");
    }
}

در ادامه ویووهای تعریف شده‌ی برای کنترلر فوق را نیز مشاهده میکنید:
// Views/UserManager/Edit.cshtml
@model User
<div class="row">
    <div class="col-md-8">
        <form method="post">
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Name"></label>
                <input asp-for="Name" class="form-control"/>
            </div>
            <div class="form-group">
                <label asp-for="LastName"></label>
                <input asp-for="LastName" class="form-control"/>
            </div>
            <div class="form-group">
                <label asp-for="Age"></label>
                <input asp-for="Age" class="form-control"/>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
        </form>
    </div>
</div>

// Views/UserManager/Index.cshtml
@model IList<User>
<table class="table">
    <tr>
        <td>Id</td>
        <td>Name</td>
        <td>LastName</td>
        <td>Age</td>
    </tr>
    @foreach (var user in Model)
    {
         <tr>
            <td>@user.Id</td>
            <td>@user.Name</td>
            <td>@user.LastName</td>
            <td>@user.Age</td>
        </tr>
    }
</table>

همانطور که مشاهده می‌کنید، کنترلر UserManager و کامپوننت UserList، به ترتیب کار مدیریت و نمایش کاربران را انجام میدهند و منطقاً هر دو جزو قابلیت‌های User هستند. برای جلوگیری از تکرار کد، می‌توانیم کنترلر و ویو‌وکامپوننت فوق را با هم ادغام کنیم؛ در واقع می‌توانیم UserListViewComponent را درون UserManagerController تعریف کنیم. برای این کار کافی است فایل UserManagerController را اینگونه تغییر دهیم:
[ViewComponent(Name = "UserList")]
public class UserManagerController : Controller
{
    private readonly UserRepository repository;

    public UserManagerController(UserRepository repository)
    {
        this.repository = repository;
    }

    public ViewResult Index()
    {
        var users = repository.GetUsers().ToList();
        return View(users);
    }

    public ViewResult Edit(int id)
    {
        var user = repository.GetUsers().FirstOrDefault(u => u.Id == id);
        return View(user);
    }

    [HttpPost]
    public IActionResult Edit(User user)
    {
        repository.Edit(user);
        return RedirectToAction("Index", "Home");
    }

    public IViewComponentResult Invoke()
    {
        var users = repository.GetUsers().Take(10).ToList();
        return new ViewViewComponentResult
        {
            ViewData = new ViewDataDictionary<IList<User>>(ViewData, users)
        };
    }
}

  
توضیحات:
همانطور که پیش‌تر نیز بحث شده است، از ویژگی ViewComponent زمانی استفاده خواهد شد که کلاس موردنظر از کلاس پایه ViewComponent ارث‌بری نکرده باشد و همچنین نام کلاس به ViewComponent ختم نشده باشد. با تعیین پراپرتی Name، یک نام برای ViewComponent تعیین کرده‌ایم که در نهایت درون ویوو، توسط Component.Invoke@  قابل فراخوانی باشد. همچنین از آنجائیکه UserManagerController از کلاس پایه ViewComponent ارث‌بری نکرده است، در نتیجه به اشیاء IViewComponentResult دسترسی نداریم، از این جهت به صورت مستقیم ViewViewComponentResult را ایجاد کرده‌ایم و مدلی که قرار است که به ویوو کامپوننت پاس داده شود را مقداردهی کرده‌ایم.

محل تعریف Viewها
Viewهای کنترلر و همچنین ویووکامپوننت مانند روش فوق قابل ترکیب نیستند؛ در نتیجه نیازی به تغییر هیچکدام از ویووها نخواهیم داشت.
UserManagerController:
/Views/UserManager/Edit.cshtml
/Views/UserManager/Index.cshtml

UserListViewComponent:
/Views/Shared/Components/UserList/Default.cshtml

نکته: دقت داشته باشید که ویجت نمایش لیست کاربران که پیشتر به صورت مستقل از عملکرد یک اکشن متد کار می‌کرده، قرار نیست جایگزین لیست کاربران (اکشن متد Index درون کنترلر UserManager) شود؛ یعنی به صورت مستقل از آن عمل میکند. هدف بیشتر قرار دادن View Component موردنظر درون کنترلر UserManager است.
مطالب
React 16x - قسمت 22 - ارتباط با سرور - بخش 1 - برپایی تنظیمات
هر برنامه‌ی وب، دارای یک frontend و یک backend است. تا اینجا، تمام تمرکز این سری، بر روی پیاده سازی frontend بود و هیچکدام از برنامه‌هایی را که تکمیل کردیم، تبادل اطلاعاتی را با وب سرویس‌های backend نداشتند؛ اما به عنوان یک توسعه دهنده‌ی React، نیاز است با نحوه‌ی ارتباط با سرور آشنایی داشت که در طی چند قسمت به آن می‌پردازیم.


ایجاد برنامه‌ی backend ارائه دهنده‌ی REST API

در اینجا یک برنامه‌ی ساده‌ی ASP.NET Core Web API را جهت تدارک سرویس‌های backend، مورد استفاده قرار می‌دهیم. هرچند این مورد الزامی نبوده و اگر علاقمند بودید که مستقل از آن کار کنید، حتی می‌توانید از سرویس آنلاین JSONPlaceholder نیز برای این منظور استفاده کنید که یک Fake Online REST API است. کار آن ارائه‌ی یک سری endpoint است که به صورت عمومی از طریق وب قابل دسترسی هستند. می‌توان به این endpintها درخواست‌های HTTP خود را مانند GET/POST/DELETE/UPDATE ارسال کرد و از آن اطلاعاتی را دریافت نمود و یا تغییر داد. به هر کدام از این endpointها یک API گفته می‌شود که جهت آزمایش برنامه‌ها بسیار مناسب هستند. برای نمونه در قسمت resources آن اگر به آدرس https://jsonplaceholder.typicode.com/posts مراجعه کنید، می‌توان لیستی از مطالب را با فرمت JSON مشاهده کرد. کار آن ارائه‌ی آرایه‌ای از اشیاء جاوا اسکریپتی قابل استفاده‌ی در برنامه‌های frontend است. بنابراین زمانیکه یک HTTP GET را به این endpoint ارسال می‌کنیم، آرایه‌ای از اشیاء مطالب را دریافت خواهیم کرد. همین endpoint، امکان تغییر این اطلاعات را توسط برای مثال HTTP Delete نیز میسر کرده‌است.

اگر علاقمندید بودید می‌توانید از JSONPlaceholder استفاده کنید و یا در ادامه دقیقا ساختار همین endpoint ارائه‌ی مطالب آن‌را با ASP.NET Core Web API نیز پیاده سازی می‌کنیم (برای مطالعه‌ی قسمت «ارتباط با سرور» اختیاری است و از هر REST API مشابهی که توسط nodejs یا PHP و غیره تولید شده باشد نیز می‌توان استفاده کرد):

مدل مطالب
namespace sample_22_backend.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }

        public int UserId { set; get; }
    }
}
ساختار این مدل، با ساختار مدل مطالب JSONPlaceholder یکی درنظر گرفته شده‌است، تا مطلب قابلیت پیگیری بیشتری را پیدا کند.


منبع داده‌ی فرضی مطالب

برای ارائه‌ی ساده‌تر برنامه، یک منبع داده‌ی درون حافظه‌ای را به همراه یک سرویس، در اختیار کنترلر مطالب، قرار می‌دهیم:
using System;
using System.Collections.Generic;
using System.Linq;
using sample_22_backend.Models;

namespace sample_22_backend.Services
{
    public interface IPostsDataSource
    {
        List<Post> GetAllPosts();
        bool DeletePost(int id);
        Post AddPost(Post post);
        bool UpdatePost(int id, Post post);
        Post GetPost(int id);
    }

    /// <summary>
    /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
    /// </summary>
    public class PostsDataSource : IPostsDataSource
    {
        private readonly List<Post> _allPosts;

        public PostsDataSource()
        {
            _allPosts = createDataSource();
        }

        public List<Post> GetAllPosts()
        {
            return _allPosts;
        }

        public Post GetPost(int id)
        {
            return _allPosts.Find(x => x.Id == id);
        }

        public bool DeletePost(int id)
        {
            var item = _allPosts.Find(x => x.Id == id);
            if (item == null)
            {
                return false;
            }

            _allPosts.Remove(item);
            return true;
        }

        public Post AddPost(Post post)
        {
            var id = 1;
            var lastItem = _allPosts.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }

            post.Id = id;
            _allPosts.Add(post);
            return post;
        }

        public bool UpdatePost(int id, Post post)
        {
            var item = _allPosts
                .Select((pst, index) => new { Item = pst, Index = index })
                .FirstOrDefault(x => x.Item.Id == id);
            if (item == null || id != post.Id)
            {
                return false;
            }

            _allPosts[item.Index] = post;
            return true;
        }

        private static List<Post> createDataSource()
        {
            var list = new List<Post>();
            var rnd = new Random();
            for (var i = 1; i < 10; i++)
            {
                list.Add(new Post { Id = i, UserId = rnd.Next(1, 1000), Title = $"Title {i} ...", Body = $"Body {i} ..." });
            }
            return list;
        }
    }
}
در این سرویس، نیازمندی‌های کنترلر مطالب مانند ارائه لیست تمام مطالب، نمایش اطلاعات یک مطلب، به روز رسانی، ایجاد و حذف یک مطلب، تدارک دیده شده‌اند. سپس از این سرویس در کنترلر زیر استفاده می‌کنیم:


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

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using sample_22_backend.Models;
using sample_22_backend.Services;

namespace sample_22_backend.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class PostsController : ControllerBase
    {
        private readonly IPostsDataSource _postsDataSource;

        public PostsController(IPostsDataSource postsDataSource)
        {
            _postsDataSource = postsDataSource;
        }

        [HttpGet]
        public ActionResult<List<Post>> GetPosts()
        {
            return _postsDataSource.GetAllPosts();
        }

        [HttpGet("{id}")]
        public ActionResult<Post> GetPost(int id)
        {
            var post = _postsDataSource.GetPost(id);
            if (post == null)
            {
                return NotFound();
            }
            return Ok(post);
        }

        [HttpDelete("{id}")]
        public ActionResult DeletePost(int id)
        {
            var deleted = _postsDataSource.DeletePost(id);
            if (deleted)
            {
                return Ok();
            }
            return NotFound();
        }

        [HttpPost]
        public ActionResult<Post> CreatePost([FromBody]Post post)
        {
            post = _postsDataSource.AddPost(post);
            return CreatedAtRoute(nameof(GetPost), new { post.Id }, post);
        }

        [HttpPut("{id}")]
        public ActionResult<Post> UpdatePost(int id, [FromBody]Post post)
        {
            var updated = _postsDataSource.UpdatePost(id, post);
            if (updated)
            {
                return Ok(post);
            }
            return NotFound();
        }
    }
}
این کنترلر که در مسیر شروع شده‌ی با https://localhost:5001/api قرار می‌گیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شده‌است که در ادامه، در برنامه‌ی React خود از آن‌ها استفاده خواهیم کرد. پس از ایجاد این پروژه‌ی web api، یک نمونه خروجی آن در مسیر https://localhost:5001/api/posts، به صورت زیر خواهد بود:


البته نمایش فرمت شده‌ی JSON در مرورگر کروم، نیاز به نصب این افزونه را دارد.


ایجاد ساختار ابتدایی برنامه‌ی ارتباط با سرور

در اینجا برای بررسی کار با سرور، یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> create-react-app sample-22-frontend
> cd sample-22-frontend
> npm start
در ادامه توئیتر بوت استرپ 4 را نیز نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save bootstrap
سپس برای افزودن فایل bootstrap.css به پروژه‌ی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.

سپس فایل app.js را به شکل زیر تکمیل می‌کنیم:
import "./App.css";

import React, { Component } from "react";

class App extends Component {
  state = {
    posts: []
  };

  handleAdd = () => {
    console.log("Add");
  };

  handleUpdate = post => {
    console.log("Update", post);
  };

  handleDelete = post => {
    console.log("Delete", post);
  };

  render() {
    return (
      <React.Fragment>
        <button className="btn btn-primary mt-1 mb-1" onClick={this.handleAdd}>
          Add
        </button>
        <table className="table">
          <thead>
            <tr>
              <th>Title</th>
              <th>Update</th>
              <th>Delete</th>
            </tr>
          </thead>
          <tbody>
            {this.state.posts.map(post => (
              <tr key={post.id}>
                <td>{post.title}</td>
                <td>
                  <button
                    className="btn btn-info btn-sm"
                    onClick={() => this.handleUpdate(post)}
                  >
                    Update
                  </button>
                </td>
                <td>
                  <button
                    className="btn btn-danger btn-sm"
                    onClick={() => this.handleDelete(post)}
                  >
                    Delete
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </React.Fragment>
    );
  }
}

export default App;
که حاصل آن، یک دکمه، برای افزودن مطلبی جدید، به همراه جدولی است از مطالب که قصد داریم در ادامه، اطلاعات آن‌را از سرور دریافت کرده و حذف و یا به روز رسانی کنیم:



نگاهی به انواع و اقسام HTTP Client‌های مهیا

در ادامه نیاز خواهیم داشت تا از طریق برنامه‌های React خود، درخواست‌های HTTP را به سمت سرور (یا همان برنامه‌ی backend) ارسال کنیم، تا بتوان اطلاعاتی را از آن دریافت کرد و یا تغییری را در اطلاعات موجود، ایجاد نمود. همانطور که پیشتر نیز در این سری عنوان شد، React برای این مورد نیز راه‌حل توکاری را به همراه ندارد و تنها کار آن، رندر کردن View و مدیریت DOM است. البته شاید این مورد یکی از مزایای کار با React نیز باشد! چون در این حالت می‌توانید از کتابخانه‌هایی که خودتان ترجیح می‌دهید، نسبت به کتابخانه‌هایی که به شما ارائه/تحمیل (!) می‌شوند (مانند برنامه‌های Angular) آزادی انتخاب کاملی را داشته باشید. برای مثال هرچند Angular به همراه یک HTTP Module توکار است، اما تاکنون چندین بار بازنویسی از ابتدا شده‌است! ابتدا با یک کتابخانه‌ی HTTP مقدماتی شروع کردند. بعدی آن‌را منسوخ شده اعلام و با یک ماژول جدید جایگزین کردند. بعد در نگارشی دیگر، چون این کتابخانه وابسته‌است به RxJS و خود RxJS نیز بازنویسی کامل شد، روش کار کردن با این HTTP Module نیز مجددا تغییر پیدا کرد! بنابراین اگر با Angular کار می‌کنید، باید کارها را آنگونه که Angular می‌پسندد، انجام دهید؛ اما در اینجا خیر و آزادی انتخاب کاملی برقرار است.
بنابراین اکنون این سؤال مطرح می‌شود که در React، برای برقراری ارتباط با سرور، چه باید کرد؟ در اینجا آزاد هستید برای مثال از Fetch API جدید مرورگرها و یا روش Ajax ای مبتنی بر XML قدیمی‌تر آن‌ها، استفاده کنید (اطلاعات بیشتر) و یا حتی اگر علاقمند باشید می‌توانید از محصور کننده‌های آن مانند jQuery Ajax استفاده کنید. بنابراین اگر با jQuery Ajax راحت هستید، به سادگی می‌توانید از آن در برنامه‌های React نیز استفاده کنید. اما ... ما در اینجا از یک کتابخانه‌ی بسیار محبوب و قدرتمند HTTP Client، به نام Axios (اکسیوس/ یک واژه‌ی یونانی به معنای «سودمند») استفاده خواهیم کرد که فقط تعداد بار دانلود هفتگی آن، 6 میلیون بار است!


نصب Axios در برنامه‌ی React این قسمت

برای نصب کتابخانه‌ی Axios، در ریشه‌ی پروژه‌ی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
پس از برپایی این مقدمات، ادامه‌ی مطلب «ارتباط با سرور» را در قسمت بعدی پیگیری می‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-22-frontend-part-01.zip و sample-22-backend-part-01.zip
مطالب
طراحی گردش کاری با استفاده از State machines - قسمت دوم
معرفی کتابخانه stateless به عنوان جایگزین سبک وزنی برای Windows workflow foundation

کتابخانه سورس باز Stateless، برای طراحی و پیاده سازی «ماشین‌های حالت گردش کاری مانند» تهیه شده و مزایای زیر را نسبت به Windows workflow foundation دارا است:
- جمعا  30 کیلوبایت است!
- تمام اجزای آن سورس باز است.
- دارای API روان و ساده‌ای است.
- امکان تبدیل UML state diagrams، به نمونه معادل Stateless بسیار ساده و سریع است.
- به دلیل code first بودن، کار کردن با آن برای برنامه نویس‌ها ساده‌تر بوده و افزودن یا تغییر اجزای آن با کدنویسی به سادگی میسر است.

دریافت کتابخانه Stateless از Google code و یا از NuGet


پیاده سازی مثال کلید برق با Stateless

در ادامه همان مثال ساده  کلید برق قسمت قبل را با Stateless پیاده سازی خواهیم کرد:
using System;
using Stateless;

namespace StatelessTests
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                string on = "On", off = "Off";
                var space = ' ';

                var onOffSwitch = new StateMachine<string, char>(initialState: off);

                onOffSwitch.Configure(state: off).Permit(trigger: space, destinationState: on);
                onOffSwitch.Configure(state: on).Permit(trigger: space, destinationState: off);

                Console.WriteLine("Press <space> to toggle the switch. Any other key will raise an error.");

                while (true)
                {
                    Console.WriteLine("Switch is in state: " + onOffSwitch.State);
                    var pressed = Console.ReadKey(true).KeyChar;
                    onOffSwitch.Fire(trigger: pressed);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception: " + ex.Message);
                Console.WriteLine("Press any key to continue...");
                Console.ReadKey(true);
            }
        }
    }
}
کار با ایجاد یک وهله از ماشین حالت (new StateMachine) آغاز می‌شود. حالت آغازین آن (initialState) مطابق مثال قسمت قبل، مساوی off است.
امضای کلاس StateMachine را در ذیل مشاهده می‌کنید؛ جهت توضیح آرگومان‌های جنریک string و char معرفی شده در مثال:
public class StateMachine<TState, TTrigger>
که اولی بیانگر نوع حالات قابل تعریف است و دومی نوع رویداد قابل دریافت را مشخص می‌کند.
برای مثال در اینجا حالات روشن و خاموش، با رشته‌های on و off مشخص شده‌اند و رویداد قابل قبول دریافتی، کاراکتر فاصله است.
سپس نیاز است این ماشین حالت را برای معرفی رویدادهایی (trigger در اینجا) که سبب تغییر حالت آن می‌شوند، تنظیم کنیم. اینکار توسط متدهای Configure و Permit انجام خواهد شد. متد Configure، یکی از حالات از پیش تعیین شده را جهت تنظیم، مشخص می‌کند و سپس در متد Permit تعیین خواهیم کرد که بر اساس رخدادی مشخص (برای مثال در اینجا فشرده شدن کلید space) وضعیت حالت جاری، به وضعیت جدیدی (destinationState) منتقل شود.
نهایتا این ماشین حالت در یک حلقه بی‌نهایت مشغول به کار خواهد شد. برای نمونه یک Thread پس زمینه (BackgroundWorker) نیز می‌تواند همین کار را در برنامه‌های ویندوزی انجام دهد.


یک نکته
علاوه بر روش‌های یاد شده‌ی تشخیص الگوی ماشین حالت که در قسمت قبل بررسی شدند، مورد refactoring انبوهی از if و elseها و یا switchهای بسیار طولانی را نیز می‌توان به این لیست افزود.



استفاده از Stateless Designer برای تولید کدهای ماشین حالت

کتابخانه Stateless دارای یک طراح و Code generator بصری سورس باز است که آن‌را به شکل افزونه‌ای برای VS.NET می‌توانید در سایت Codeplex دریافت کنید. این طراح از کتابخانه GLEE برای رسم گراف استفاده می‌کند.

کار مقدماتی با آن به نحو زیر است:
الف) فایل StatelessDesignerPackage.vsix را از سایت کدپلکس دریافت و نصب کنید. البته نگارش فعلی آن فقط با VS 2012 سازگار است.
ب) ارجاعی را به اسمبلی stateless به پروژه خود اضافه نمائید (به یک پروژه جدید یا از پیش موجود).
ج) از منوی پروژه، گزینه Add new item را انتخاب کرده و سپس در صفحه ظاهر شده، گزینه جدید Stateless state machine را انتخاب و به پروژه اضافه نمائید.
کار با این طراح، با ادیت XML آن شروع می‌شود. برای مثال گردش کاری ارسال و تائید یک مطلب جدید را در بلاگی فرضی، به نحو زیر وارد نمائید:
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema">
  <settings>
    <itemname>BlogPostStateMachine</itemname>
    <namespace>StatelessTests</namespace>
    <class>public</class>
  </settings>
  <triggers>     
    <trigger>Save</trigger>
    <trigger>RequireEdit</trigger>
    <trigger>Accept</trigger>
    <trigger>Reject</trigger>
  </triggers>
  <states>     
    <state start="yes">Begin</state>
    <state>InProgress</state>     
    <state>Published</state>      
    <state>Rejected</state>      
  </states>
  <transitions>
    <transition trigger="Save" from="Begin" to="InProgress" />

    <transition trigger="Accept" from="InProgress" to="Published" />
    <transition trigger="Reject" from="InProgress" to="Rejected" />

    <transition trigger="Save" from="InProgress" to="InProgress" />

    <transition trigger="RequireEdit" from="Published" to="InProgress" />
    <transition trigger="RequireEdit" from="Rejected" to="InProgress" />
  </transitions>
</statemachine>
حاصل آن گراف زیر خواهد بود:


به علاوه کدهای زیر که به صورت خودکار تولید شده‌اند:
using Stateless;

namespace StatelessTests
{
  public class BlogPostStateMachine
  {
    public delegate void UnhandledTriggerDelegate(State state, Trigger trigger);
    public delegate void EntryExitDelegate();
    public delegate bool GuardClauseDelegate();

    public enum Trigger
    {
      Save,
      RequireEdit,
      Accept,
      Reject,
    }

    public enum State
    {
      Begin,
      InProgress,
      Published,
      Rejected,
    }

    private readonly StateMachine<State, Trigger> stateMachine = null;

    public EntryExitDelegate OnBeginEntry = null;
    public EntryExitDelegate OnBeginExit = null;
    public EntryExitDelegate OnInProgressEntry = null;
    public EntryExitDelegate OnInProgressExit = null;
    public EntryExitDelegate OnPublishedEntry = null;
    public EntryExitDelegate OnPublishedExit = null;
    public EntryExitDelegate OnRejectedEntry = null;
    public EntryExitDelegate OnRejectedExit = null;
    public GuardClauseDelegate GuardClauseFromBeginToInProgressUsingTriggerSave = null;
    public GuardClauseDelegate GuardClauseFromInProgressToPublishedUsingTriggerAccept = null;
    public GuardClauseDelegate GuardClauseFromInProgressToRejectedUsingTriggerReject = null;
    public GuardClauseDelegate GuardClauseFromInProgressToInProgressUsingTriggerSave = null;
    public GuardClauseDelegate GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = null;
    public GuardClauseDelegate GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = null;
    public UnhandledTriggerDelegate OnUnhandledTrigger = null;

    public BlogPost()
    {
      stateMachine = new StateMachine<State, Trigger>(State.Begin);
      stateMachine.Configure(State.Begin)
        .OnEntry(() => { if (OnBeginEntry != null) OnBeginEntry(); })
        .OnExit(() => { if (OnBeginExit != null) OnBeginExit(); })
        .PermitIf(Trigger.Save, State.InProgress , () => { if (GuardClauseFromBeginToInProgressUsingTriggerSave != null) return GuardClauseFromBeginToInProgressUsingTriggerSave(); return true; } )
      ;
      stateMachine.Configure(State.InProgress)
        .OnEntry(() => { if (OnInProgressEntry != null) OnInProgressEntry(); })
        .OnExit(() => { if (OnInProgressExit != null) OnInProgressExit(); })
        .PermitIf(Trigger.Accept, State.Published , () => { if (GuardClauseFromInProgressToPublishedUsingTriggerAccept != null) return GuardClauseFromInProgressToPublishedUsingTriggerAccept(); return true; } )
        .PermitIf(Trigger.Reject, State.Rejected , () => { if (GuardClauseFromInProgressToRejectedUsingTriggerReject != null) return GuardClauseFromInProgressToRejectedUsingTriggerReject(); return true; } )
        .PermitReentryIf(Trigger.Save , () => { if (GuardClauseFromInProgressToInProgressUsingTriggerSave != null) return GuardClauseFromInProgressToInProgressUsingTriggerSave(); return true; } )
      ;
      stateMachine.Configure(State.Published)
        .OnEntry(() => { if (OnPublishedEntry != null) OnPublishedEntry(); })
        .OnExit(() => { if (OnPublishedExit != null) OnPublishedExit(); })
        .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit(); return true; } )
      ;
      stateMachine.Configure(State.Rejected)
        .OnEntry(() => { if (OnRejectedEntry != null) OnRejectedEntry(); })
        .OnExit(() => { if (OnRejectedExit != null) OnRejectedExit(); })
        .PermitIf(Trigger.RequireEdit, State.InProgress , () => { if (GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit != null) return GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit(); return true; } )
      ;
      stateMachine.OnUnhandledTrigger((state, trigger) => { if (OnUnhandledTrigger != null) OnUnhandledTrigger(state, trigger); });
    }

    public bool TryFireTrigger(Trigger trigger)
    {
      if (!stateMachine.CanFire(trigger))
      {
        return false;
      }
      stateMachine.Fire(trigger);
      return true;
    }

    public State GetState
    {
      get
      {
        return stateMachine.State;
      }
    }
  }
}
توضیحات:

ماشین حالت فوق دارای چهار حالت شروع، در حال بررسی، منتشر شده و رد شده است. معمول است که این چهار حالت را به شکل یک enum معرفی کنند که در کدهای تولیدی فوق نیز به همین نحو عمل گردیده و public enum State معرف چهار حالت ذکر شده است. همچنین رویدادهای ذخیره، نیاز به ویرایش، ویرایش، تائید و رد نیز توسط public enum Trigger معرفی شده‌اند.
در قسمت Transitions، بر اساس یک رویداد (Trigger در اینجا)، انتقال از یک حالت به حالتی دیگر را سبب خواهیم شد.
تعاریف اصلی تنظیمات ماشین حالت، در سازنده کلاس BlogPostStateMachine انجام شده است. این تعاریف نیز بسیار ساده هستند. به ازای هر حالت، یک Configure داریم. در متدهای OnEntry و OnExit هر حالت، یک سری callback function فراخوانی خواهند شد. برای مثال در حالت Rejected یا Approved می‌توان ایمیلی را به ارسال کننده مطلب جهت یادآوری وضعیت رخ داده، ارسال نمود.
متدهای PermitIf سبب انتقال شرطی، به حالتی دیگر خواهند شد. برای مثال رد یا تائید یک مطلب نیاز به دسترسی مدیریتی خواهد داشت. این نوع موارد را توسط delgateهای Guard ایی که برای مدیریت شرط‌ها ایجاد کرده است، می‌توان تنظیم کرد. PermitReentryIf سبب بازگشت مجدد به همان حالت می‌گردد. برای مثال ویرایش و ذخیره یک مطلب در حال انتشار، سبب تائید یا رد آن نخواهد شد؛ صرفا عملیات ذخیره صورت گرفته و ماشین حالت مجددا در همان مرحله باقی خواهد ماند.

نحوه استفاده از ماشین حالت تولیدی:
همانطور که عنوان شد، حداقل استفاده از ماشین‌های حالت، refactoing انبوهی از if و else‌ها است که در حالت مدیریت یک چنین گردش‌های کاری باید تدارک دید.
namespace StatelessTests
{
    public class BlogPostManager
    {
        private BlogPostStateMachine _stateMachine;
        public BlogPostManager()
        {
            configureWorkflow();
        }

        private void configureWorkflow()
        {
            _stateMachine = new BlogPostStateMachine();

            _stateMachine.GuardClauseFromBeginToInProgressUsingTriggerSave = () => { return UserCanPost; };
            _stateMachine.OnBeginExit = () => { /* save data + save state + send an email to admin */ };

            _stateMachine.GuardClauseFromInProgressToPublishedUsingTriggerAccept = () => { return UserIsAdmin; };
            _stateMachine.GuardClauseFromInProgressToRejectedUsingTriggerReject = () => { return UserIsAdmin; };
            _stateMachine.GuardClauseFromInProgressToInProgressUsingTriggerSave = () => { return UserHasEditRights; };
            _stateMachine.OnInProgressExit = () => { /* save data + save state + send an email to user */ };

            _stateMachine.OnPublishedExit = () => { /* save data + save state + send an email to admin */ };
            _stateMachine.GuardClauseFromPublishedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; };

            _stateMachine.OnRejectedExit = () => { /* save data + save state + send an email to admin */ };
            _stateMachine.GuardClauseFromRejectedToInProgressUsingTriggerRequireEdit = () => { return UserHasEditRights; };
        }

        public bool UserIsAdmin
        {
            get
            {
                return true; // TODO: Evaluate if user is an admin.
            }
        }

        public bool UserCanPost
        {
            get
            {
                return true; // TODO: Evaluate if user is authenticated
            }
        }

        public bool UserHasEditRights
        {
            get
            {
                return true; // TODO: Evaluate if user is owner or admin
            }
        }

        // User actions
        public void Save() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Save); }
        public void RequireEdit() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.RequireEdit); }

        // Admin actions        
        public void Accept() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Accept); }
        public void Reject() { _stateMachine.TryFireTrigger(BlogPostStateMachine.Trigger.Reject); }
    }
}
در کلاس فوق، نحوه استفاده از ماشین حالت تولیدی را مشاهده می‌کنید. در delegateهای Guard، سطوح دسترسی انجام عملیات بررسی خواهند شد. برای مثال، از بانک اطلاعاتی بر اساس اطلاعات کاربر جاری وارد شده به سیستم اخذ می‌گردند. در متدهای Exit هر مرحله، کارهای ذخیره سازی اطلاعات در بانک اطلاعاتی، ذخیره سازی حالت (مثلا در یک فیلد که بعدا قابل بازیابی باشد) صورت می‌گیرد و در صورت نیاز ایمیلی به اشخاص مختلف ارسال خواهد شد.
برای به حرکت درآوردن این ماشین، نیاز به یک سری اکشن متد نیز می‌باشد. تعدادی از این موارد را در انتهای کلاس فوق ملاحظه می‌کنید. کد نویسی آن‌ها در حد فراخوانی متد TryFireTrigger ماشین حالت است.

یک نکته:
ماشین حالت تولیدی به صورت پیش فرض در حالت State.Begin قرار دارد. می‌توان این مورد را از بانک اطلاعاتی خواند و سپس مقدار دهی نمود تا با هر بار وهله سازی ماشین حالت دقیقا مشخص باشد که در چه مرحله‌ای قرار داریم و TryFireTrigger بتواند بر این اساس تصمیم‌گیری کند که آیا مجاز است عملیاتی را انجام دهد یا خیر.
مطالب
خواندن اطلاعات از سرور و نمایش آن توسط Angular در ASP.NET MVC
می خواهیم یک مثال ساده از دریافت اطلاعات از سرور و نمایش آن در یک View را توسط AngularJS، با هم بررسی کنیم.
همانطور که می‌دانید برای نمایش تعدادی از اشیاء در انگولار می‌توان به این صورت نیز عمل کرد:
<div ng-init="products=[
     {id:1,name:'product1',price:25000,description:'description of product'},
     {id:2,name:'product2',price:5000,description:'description of product'},
     {id:3,name:'product3',price:5000,description:'description of product'},
     {id:4,name:'product4',price:2000,description:'description of product'},
     {id:5,name:'product5',price:255000,description:'description of product'}
     ]">
    <div>
        <div>
            <table>
                <tr>
                    <th>Id</th>
                    <th>Product Name</th>
                    <th>Price</th>
                    <th>Description</th>
                </tr>
                <tr ng-repeat="product in products">
                    <td>{{product.id}}</td>
                    <td>{{product.name}}</td>
                    <td>{{product.price}}</td>
                    <td>{{product.description}}</td>
                </tr>
            </table>
        </div>
    </div>
</div>
در کد فوق توسط ویژگی ng-init می‌توانیم داده هایمان را Initialize کنیم و در نهایت توسط ویژگی ng-repeat می‌توانیم داده هایمان را در صفحه نمایش دهیم. این ویژگی دقیقاً مانند یک حلقه foreach عمل میکند؛ مثلا معادل آن در Razor سمت سرور، به این صورت است:
@foreach (var product in Model.products)
{
    <td>product.id</td>
    <td>product.name</td>
    <td>product.price</td>
    <td>product.description</td>
}
خوب؛ حالا میخواهیم این اطلاعات را از سمت سرور بخوانیم و به صورت فوق نمایش دهیم. ابتدا مدل مان را به این صورت تعریف می‌کنیم :
namespace AngularAndMvc.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public float Price { get; set; }
        public string Description { get; set; }
    }
}
سپس در داخل کنترلر زیر اطلاعات را به صورت in memory data تعریف می‌کنیم (جهت سهولت دموی کار) و به view مورد نظر پاس می‌دهیم. البته شما می‌توانید این اطلاعات را از دیتابیس بخوانید؛ روال کار فرقی نمی‌کند:
namespace AngularAndMvc.Controllers
{
    public class ProductController : Controller
    {        
        public ActionResult Index()
        {
            return View("Index", "", GetSerializedProduct());
        }

        public string GetSerializedProduct()
        {
            var products = new[] 
            { 
                new Product{Id=1,Name="product1",Price=4500,Description="description of this product"},
                new Product{Id=2,Name="product2",Price=500,Description="description of this product"},
                new Product{Id=3,Name="product3",Price=400,Description="description of this product"},
                new Product{Id=4,Name="product4",Price=5500,Description="description of this product"},
                new Product{Id=5,Name="product5",Price=66500,Description="description of this product"}
            };
            var settings = new JsonSerializerSettings { ContractResolver=new CamelCasePropertyNamesContractResolver()};
            return JsonConvert.SerializeObject(products,Formatting.None,settings);
        }
   }
}
همانطور که در کد بالا مشخص است، اطلاعات را به صورت JSON به View مان پاس داده ایم و برای اینکه ابتدای نام مقادیر بازگشتی به صورت حروف بزرگ نباشند (به صورت خودکار تبدیل به camel case شوند) پارامتر settings را برای متد SerializeObject تعیین کرده‌ایم:
var settings = new JsonSerializerSettings { ContractResolver=new CamelCasePropertyNamesContractResolver()};
return JsonConvert.SerializeObject(products,Formatting.None,settings);
view را نیز به این صورت تغییر می‌دهیم :
@model string
<div ng-init="products = @Model">
    <div>
        <div>
            <table>
                <tr>
                    <th>Id</th>
                    <th>Product Name</th>
                    <th>Price</th>
                    <th>Description</th>
                </tr>
                <tr ng-repeat="product in products">
                    <td>{{product.id}}</td>
                    <td>{{product.name}}</td>
                    <td>{{product.price}}</td>
                    <td>{{product.description}}</td>
                </tr>
            </table>
        </div>
    </div>
</div>
تنها تغییری که در کد فوق اعمال شده است، به جای اینکه ویژگی ng-init را به صورت inline مقداردهی کنیم آن را از کنترلر دریافت کرده ایم.
در خروجی هم اطلاعات به این صورت نمایش داده می‌شوند :

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

مطالب
بررسی تغییرات Blazor 8x - قسمت دوم - بررسی حالت رندر سمت سرور
در قسمت قبل، حالت‌های مختلف رندر کامپوننت‌ها را در Blazor 8x معرفی کردیم. در این قسمت می‌خواهیم نحوه‌ی کارکرد دو حالت InteractiveServer و StreamRendering را به همراه چند مثال بررسی کنیم.


معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8

پس از نصب SDK دات نت 8، دیگر خبری از قالب‌های قدیمی پروژه‌های blazor server و blazor wasm نیست! در اینجا در ابتدا باید مشخص کرد که سطح تعاملی برنامه در چه حدی است. در ادامه 4 روش شروع پروژه‌های Blazor 8x را مشاهده می‌کنید که توسط پرچم interactivity--، نوع رندر برنامه در آن‌ها مشخص شده‌است:

اجرای قسمت‌های تعاملی برنامه بر روی سرور:
dotnet new blazor --interactivity Server

اجرای قسمت‌های تعاملی برنامه در مرورگر، توسط فناوری وب‌اسمبلی:
dotnet new blazor --interactivity WebAssembly

برای اجرای قسمت‌های تعاملی برنامه، ابتدا حالت Server فعالسازی می‌شود تا فایل‌های WebAssembly دریافت شوند، سپس فقط از WebAssembly استفاده می‌کند:
dotnet new blazor --interactivity Auto

فقط از حالت SSR یا همان static server rendering استفاده می‌شود (این نوع برنامه‌ها تعاملی نیستند):
dotnet new blazor --interactivity None

سایر گزینه‌ها را با اجرای دستور dotnet new blazor --help می‌توانید مشاهده کنید.


نکته‌ی مهم! در قالب‌های آماده‌ی Blazor 8x، حالت SSR، پیش‌فرض است.

هرچند در تمام پروژه‌های فوق، انتخاب حالت‌های مختلف رندر را مشاهده می‌کنید، اما این انتخاب‌ها صرفا دو مقصود مهم را دنبال می‌کنند:
الف) تنظیم فایل Program.cs برنامه جهت افزودن وابستگی‌های مورد نیاز، به صورت خودکار.
ب) ایجاد پروژه‌ی کلاینت (علاوه بر پروژه‌ی سرور)، در صورت نیاز. برای مثال حالت‌های وب‌اسمبلی و Auto، هر دو به همراه یک پروژه‌ی کلاینت وب‌اسمبلی هم هستند؛ اما حالت‌های Server و None، خیر.

در تمام این پروژه‌ها هر صفحه و یا کامپوننتی که ایجاد می‌شود، به صورت پیش‌فرض بر اساس SSR رندر و نمایش داده خواهد شد؛ مگر اینکه به صورت صریحی این نحوه‌ی رندر را بازنویسی کنیم. برای مثال مشخص کنیم که قرار است بر اساس Blazor Server اجرا شود و یا وب‌اسمبلی و یا حالت Auto.

اما ... اگر علاقمند بودیم تا به حالت‌های پیش‌از دات نت 8 رجوع کنیم و تمام برنامه را به صورت یک‌دست تعاملی کنیم و حالت SSR پیش‌فرض نباشد، می‌توان از پرچم all-interactive-- که به انتهای دستورات فوق قابل افزوده شدن است، کمک گرفت. این پرچم، فایل App.Razor را جهت تنظیم سراسری حالت‌های رندر، ویرایش می‌کند. این مورد را در ادامه‌ی این مطلب، در قسمت «روشی ساده برای تعاملی کردن کل برنامه» بیشتر بررسی می‌کنیم.


بررسی حالت Server side rendering

برای بررسی این حالت یک پوشه‌ی جدید را ایجاد کرده و توسط خط فرمان، دستور dotnet new blazor --interactivity Server را در ریشه‌ی آن اجرا می‌کنیم. پس از ایجاد ساختار ابتدایی پروژه بر اساس این قالب انتخابی، فایل Program.cs جدید آن، چنین شکلی را دارد:
var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();
مهم‌ترین قسمت‌های آن، متدهای AddInteractiveServerComponents و AddInteractiveServerRenderMode هستند که server-side rendering را به همراه امکان داشتن کامپوننت‌های تعاملی، میسر می‌کنند.
server-side rendering به این معنا است که برنامه‌ی سمت سرور، کل DOM و HTML نهایی را تولید کرده و به مرورگر کاربر ارائه می‌کند. مرورگر هم این DOM را نمایش می‌دهد. فقط همین! در اینجا هیچ خبری از اتصال دائم SignalR نیست و محتوای ارائه شده، یک محتوای استاتیک است. این حالت رندر، برای ارائه‌ی محتواهای فقط خواندنی غیرتعاملی، فوق العاده‌است؛ امکان از لحظه‌ای که نیاز به کلیک بر روی دکمه‌ای باشد، دیگر پاسخگو نیست. به همین جهت در اینجا امکان تعاملی کردن تعدادی از کامپوننت‌های ویژه و مدنظر نیز پیش‌بینی شده‌اند تا بتوان به ترکیبی از server-side rendering و client-side rendering رسید.
حالت پیش‌فرض در اینجا، ارائه‌ی محتوای استاتیک است. بنابراین هر کامپوننتی در اینجا ابتدا بر روی سرور رندر شده (HTML ابتدایی آن آماده شده) و به سمت مرورگر کاربر ارسال می‌شود. اگر کامپوننتی نیاز به امکانات تعاملی داشت باید آن‌را دقیقا توسط ویژگی InteractiveXYZ مشخص کند؛ مانند مثال زیر:
@page "/counter"
@rendermode InteractiveServer

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
همانطور که مشاهده می‌کنید، این کامپوننت از روش رندر InteractiveServer استفاده می‌کند. برای درک نحوه‌ی کارکرد آن، همین سطر را حذف، یا کامنت کنید و سپس برنامه را اجرا کنید. در حین مشاهده‌ی این صفحه، همه چیز به خوبی نمایش داده می‌شود و حتی دکمه‌ی Click me هم مشخص است. اما ... با کلیک بر روی آن اتفاقی رخ نمی‌دهد! علت اینجا است که اکنون این صفحه، یک صفحه‌ی کاملا استاتیک است و دیگر تعاملی نیست.
در ادامه، مجددا سطر کامنت شده را به حالت عادی برگردانید و سپس برنامه را اجرا کنید. پیش از باز کردن صفحه‌ی Counter، ابتدا developer tools مرورگر خود را گشوده و برگه‌ی network آن‌را انتخاب و سپس صفحه‌ی Counter را باز کنید. در این لحظه‌است که مشاهده می‌کنید یک اتصال وب‌سوکت برقرار شد. این اتصال است که قابلیت‌های تعاملی صفحه را برقرار کرده و مدیریت می‌کند (این اتصال دائم SignalR است که این صفحه را همانند برنامه‌های Blazor Web Server پیشین مدیریت می‌کند).

یک نکته: در برنامه‌های Blazor Server سنتی، امکان فعالسازی قابلیتی به نام prerender نیز وجود دارد. یعنی سرور، ابتدا صفحه را رندر کرده و محتوای استاتیک آن‌را به سمت مرورگر کاربر ارسال می‌کند و سپس اتصال SignalR برقرار می‌شود. در دات نت 8، این حالت، حالت پیش‌فرض است. اگر آن‌را نمی‌خواهید باید به نحو زیر غیرفعالش کنید:
@rendermode InteractiveServerRenderModeWithoutPrerendering

@code{
  static readonly IComponentRenderMode InteractiveServerRenderModeWithoutPrerendering = 
        new InteractiveServerRenderMode(false);
}


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

اگر می‌خواهید رفتار برنامه را همانند Blazor Server سابق کنید و نمی‌خواهید به ازای هر کامپوننت، نحوه‌ی رندر آن‌را به صورت سفارشی انتخاب کنید، فقط کافی است فایل App.razor را باز کرده:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="MyApp.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>

<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>
و قسمت‌های HeadOutlet و مسیریابی آن‌را به صورت زیر تغییر دهید تا به کل برنامه اعمال شود:
<HeadOutlet @rendermode="@InteractiveServer" />
...
<Routes @rendermode="@InteractiveServer" />
این مورد دقیقا همان کاری است که پرچم all-interactive-- در هنگام ایجاد پروژه‌های جدید Blazor 8x به کمک NET CLI.، انجام می‌دهد. یک چنین گزینه‌ای در ویژوال استودیو نیز هنگام ایجاد پروژه‌ها‌ی جدید Blazor وجود دارد و به شما این امکان را می‌دهد که بین حالت‌های تعاملی Per page/component و Global، یکی را انتخاب کنید. در حین استفاده‌ی از CLI، نیازی به ذکر حالت تعاملی Per page/component نیست؛ چون حالت پیش‌فرض، یا همان SSR است. حالت Global هم فقط فایل App.Razor را به صورت فوق، ویرایش و تنظیم می‌کند.


در این حالت دیگر نیازی نیست تا به ازای هر کامپوننت و صفحه، نحوه‌ی رندر را مشخص کنیم؛ چون این نحوه، از بالاترین سطح، به تمام زیرکامپوننت‌ها به ارث می‌رسد (درباره‌ی این نکته در قسمت قبل، توضیحاتی ارائه شد).


بررسی حالت Streaming Rendering

در اینجا مثال پیش‌فرض Weather.razor قالب پیش‌فرض مورد استفاده‌ی جاری را کمی تغییر داده‌ایم که کدهای نهایی آن به صورت زیر است (2 قسمت forecasts.AddRange_ را اضافه‌تر دارد):

@page "/weather"
@attribute [StreamRendering(prerender: true)]

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>

@if (_forecasts == null)
{
    <p>
        <em>Loading...</em>
    </p>
}
else
{
    <table class="table">
        <thead>
        <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </tr>
        </thead>
        <tbody>
        @foreach (var forecast in _forecasts)
        {
            <tr>
                <td>@forecast.Date.ToShortDateString()</td>
                <td>@forecast.TemperatureC</td>
                <td>@forecast.TemperatureF</td>
                <td>@forecast.Summary</td>
            </tr>
        }
        </tbody>
    </table>
}

@code {
    private List<WeatherForecast>? _forecasts;

    protected override async Task OnInitializedAsync()
    {
    // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(500);

        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[]
                        {
                            "Freezing",
                            "Bracing",
                            "Chilly",
                            "Cool",
                            "Mild",
                            "Warm",
                            "Balmy",
                            "Hot",
                            "Sweltering",
                            "Scorching",
                        };
        _forecasts = GetWeatherForecasts(startDate, summaries).ToList();
        StateHasChanged();

    // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(1000);
        _forecasts.AddRange(GetWeatherForecasts(startDate, summaries));
        StateHasChanged();

        await Task.Delay(1000);
        _forecasts.AddRange(GetWeatherForecasts(startDate, summaries));
    }

    private static IEnumerable<WeatherForecast> GetWeatherForecasts(DateOnly startDate, string[] summaries)
    {
        return Enumerable.Range(1, 5)
                         .Select(index => new WeatherForecast
                                          {
                                              Date = startDate.AddDays(index),
                                              TemperatureC = Random.Shared.Next(-20, 55),
                                              Summary = summaries[Random.Shared.Next(summaries.Length)],
                                          });
    }

    private class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }

}
برای بررسی این مثال، ابتدا سطر ویژگی StreamRendering را حذف و یا کامنت کرده و سپس برنامه را اجرا کنید. پس از اجرای برنامه، با انتخاب مشاهده‌ی صفحه‌ی Weather، هیچگاه قسمت loading که در حالت forecasts == null_ قرار است ظاهر شود، نمایش داده نمی‌شود؛ چون در این حالت (حذف نوع رندر)، صفحه‌ی نهایی که به کاربر ارائه خواهد شد، یک صفحه‌ی استاتیک کاملا رندر شده‌ی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحه‌ی نهایی را دریافت و مشاهده کند. در این حالت امکانات تعاملی Blazor server وجود خارجی ندارند.
در ادامه مجددا سطر ویژگی StreamRendering را به حالت قبلی برگردانید و برنامه را اجرا کنید. در این حالت ابتدا قسمت loading ظاهر می‌شود و سپس در طی چند مرحله با توجه به Task.Delay‌های قرار داده شده، صفحه رندر شده و تکمیل می‌شود.
اتفاقی که در اینجا رخ می‌دهد، استفاده از فناوری  HTML Streaming است که مختص به مایکروسافت هم نیست. در حالت Streaming، هربار قطعه‌ای از HTML ای که قرار است به کاربر ارائه شود، به صورت جریانی به سمت مرورگر ارسال می‌شود و مرورگر این قطعه‌ی جدید را بلافاصله نمایش می‌دهد. نکته‌ی جالب این روش، عدم نیاز به اتصال SignalR و یا اجرای WASM درون مرورگر است.

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


روش نهایی معرفی نحوه‌ی رندر صفحات

بجای استفاده از ویژگی‌های RenderModeXyz جهت معرفی نحوه‌ی رندر کامپوننت‌ها و صفحات (که تا پیش از نگارش RTM معرفی شده بودند و چندبار هم تغییر کردند)، می‌توان از دایرکتیو جدیدی به نام rendermode@ با سه مقدار InteractiveServer، InteractiveWebAssembly و InteractiveAuto استفاده کرد. برای سهولت تعریف این موارد باید سطر ذیل را به فایل Imports.razor_ اضافه نمود:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
در این حالت تعریف قدیمی سطر ذیل:
@attribute [RenderModeInteractiveServer]
به صورت زیر:
@rendermode InteractiveServer
تبدیل می‌شود.

اگر هم قصد سفارشی سازی آن‌ها را دارید، برای مثال می‌خواهید prerender را در آن‌ها false کنید، روش کار به صورت زیر است:
@rendermode renderMode

@code {
    static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false);
}
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت چهارم - data binding
در قسمت قبل، نگاهی مقدماتی داشتیم به مبحث data binding. در ادامه، این مبحث را به همراه pipes، جهت اعمال تغییرات بر روی اطلاعات، پیگیری خواهیم کرد.


انقیاد به خواص یا property binding

قابلیت property binding این امکان را فراهم می‌کند که یکی از خواص المان‌های HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
 <img [src]='producr.imageUrl'>
در این مثال، خاصیت src المان تصویر، به آدرس تصویر یک محصول متصل شده‌است.
در حین تعریف property binding، مقصد اتصال، داخل براکت‌ها قرار می‌گیرد و خاصیت مدنظر المان را مشخص می‌کند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار می‌گیرد.
اگر اینکار را بخواهیم با interpolation معرفی شده‌ی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
 <img src={{producr.imageUrl}}>
در اینجا نه از []، برای معرفی مقصد اتصال استفاده شده‌است و نه از "" برای مشخص سازی منبع اتصال. این نوع اتصال یک طرفه است (از منبع به مقصد).

خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیه‌ی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
 <img src='http://www.mysite.com/images/{{producr.imageUrl}}'>


تکمیل قالب کامپوننت لیست محصولات

اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شده‌ی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع داده‌ی آن متصل می‌کنیم:
<tbody>
    <tr *ngFor='#product of products'>
        <td>
            <img [src]='product.imageUrl'
                 [title]='product.productName'>
        </td>
        <td>{{ product.productName }}</td>
        <td>{{ product.productCode }}</td>
        <td>{{ product.releaseDate }}</td>
        <td>{{ product.price }}</td>
        <td>{{ product.starRating }}</td>
    </tr>
</tbody>
در این حالت اگر برنامه را اجرا کنیم به خروجی ذیل خواهیم رسید:


هرچند اینبار تصاویر محصولات نمایش داده شده‌اند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آن‌را تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه می‌کنیم:
export class ProductListComponent {
    pageTitle: string = 'Product List';
    imageWidth: number = 50;
    imageMargin: number = 2;
    products: any[] = [
        // as before...
    ];
}
البته همانطور که پیشتر نیز ذکر شد، چون مقادیر پیش فرض این خواص عددی هستند، نیازی به ذکر صریح نوع number در اینجا وجود ندارد (type inference).
پس از تعریف این خواص، امکان دسترسی به آن‌ها در قالب کامپوننت وجود خواهد داشت:
<tbody>
    <tr *ngFor='#product of products'>
        <td>
            <img [src]='product.imageUrl'
                 [title]='product.productName'
                 [style.width.px]='imageWidth'
                 [style.margin.px]='imageMargin'>
        </td>
همانطور که مشاهده می‌کنید، چون خاصیت‌های جدید تعریف شده، جزئی از خواص اصلی کلاس هستند و نه خواص اشیاء لیست محصولات، دیگر به همراه .product ذکر نشده‌اند.
همچنین در اینجا نحوه‌ی style binding را نیز مشاهده می‌کنید. مقصد اتصال همیشه با [] مشخص می‌شود و سپس کار با ذکر .style شروع شده و پس از آن نام خاصیت مدنظر عنوان خواهد شد. اگر نیاز به ذکر واحدی وجود داشت، پس از درج نام خاصیت، قید خواهد شد. برای مثال [style.fontSize.em] و یا [%.style.fontSize]


یک نکته:
اگر مثال را قدم به قدم دنبال کرده باشید، با افزودن style binding و بارگذاری مجدد صفحه، احتمالا تغییراتی را مشاهده نخواهید کرد. این مورد به علت کش شدن قالب قبلی و یا فایل جاوا اسکریپتی متناظر با آن است (فایلی که خواص عرض و حاشیه‌ی تصویر به آن اضافه شده‌اند).
یک روش ساده‌ی حذف کش آن، بازکردن آدرس http://localhost:2222/app/products/product-list.component.js در مرورگر به صورت مجزا و سپس فشردن دکمه‌های ctrl+f5 بر روی آن است.


پاسخ دادن به رخدادها و یا event binding

تا اینجا تمام data binding‌های تعریف شده‌ی ما یک طرفه بودند؛ از خواص کلاس کامپوننت به اجزای قالب متناظر با آن. اما گاهی از اوقات نیاز است تا با کلیک کاربر بر روی دکمه‌ای، عملی خاص صورت گیرد و در این حالت، جهت ارسال اطلاعات، از قالب کامپوننت، به متدها و خواص کلاس متناظر با آن خواهند بود. کامپوننت به اعمال کاربر از طریق event binding گوش فرا می‌دهد:
 <button (click)='toggleImage()'>
syntax آن بسیار شبیه است به حالت property binding و در اینجا بجای [] از () جهت مشخص سازی رخدادی خاص از المان مدنظر استفاده می‌شود. سمت راست این انتساب، متدی است که داخل "" قرار می‌گیرد و این متد متناظر است با متدی مشخص در کلاس متناظر با کامپوننت جاری.
در این حالت اگر کاربر روی دکمه‌ی تعریف شده کلیک کند، متد toggleImage موجود در کلاس متناظر، فراخوانی خواهد شد.
چه رخدادهایی را در اینجا می‌توان ذکر کرد؟ پاسخ آن‌را در آدرس ذیل می‌توانید مشاهده کنید:
https://developer.mozilla.org/en-US/docs/Web/Events

این syntax جدید AngularJS 2.0 سطح API آن‌را کاهش داده است. دیگر در اینجا نیازی نیست تا به ازای هر رخداد ویژه‌ای، یک دایرکتیو و یا syntax خاص آن‌را در مستندات آن
جستجو کرد. فقط کافی است syntax جدید (نام رخداد) را مدنظر داشته باشید.


تکمیل مثال نمایش لیست محصولات با فعال سازی دکمه‌ی Show Image آن

در اینجا قصد داریم با کلیک بر روی دکمه‌ی Show image، تصاویر موجود در ستون تصاویر، مخفی و یا نمایان شوند. برای این منظور خاصیت جدیدی را به نام showImage به کلاس ProductListComponent اضافه می‌کنیم. بنابراین فایل product-list.component.ts را گشوده و تغییر ذیل را به آن اعمال کنید:
export class ProductListComponent {
    pageTitle: string = 'Product List';
    imageWidth: number = 50;
    imageMargin: number = 2;
    showImage: boolean = false;
در اینجا خاصیت Boolean جدیدی به نام showImage با مقدار اولیه‌ی false تعریف شده‌است. به این ترتیب تصاویر، در زمان اولین بارگذاری صفحه نمایش داده نخواهند شد.
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه می‌کنیم:
export class ProductListComponent {
    // as before ...
 
    toggleImage(): void {
        this.showImage = !this.showImage;
    }
}
در کلاس‌های TypeScript نیازی به ذکر صریح واژه‌ی کلیدی function برای تعریف متدی وجود ندارد. این متد، خروجی هم ندارد، بنابراین نوع خروجی آن void مشخص شده‌است. در بدنه‌ی این متد، وضعیت خاصیت نمایش تصویر معکوس می‌شود.
پس از این تغییرات، اکنون می‌توان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
<button class='btn btn-primary'
        (click)='toggleImage()'>
    Show Image
</button>
در اینجا click به عنوان رخداد مقصد، مشخص شده‌است. سپس آن‌را به متد toggleImage کلاس ProductListComponent متصل می‌کنیم.
خوب، تا اینجا اگر کاربر بر روی دکمه‌ی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحله‌ی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شده‌است. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المان‌های DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه می‌کنیم:
<tr *ngFor='#product of products'>
    <td>
        <img *ngIf='showImage'
             [src]='product.imageUrl'
             [title]='product.productName'
             [style.width.px]='imageWidth'
             [style.margin.px]='imageMargin'>
    </td>
با توجه به ngIf* تعریف شده، المان تصویر تنها زمانی به DOM اضافه خواهد شد که مقدار خاصیت showImage مساوی true باشد.

اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمی‌شود. پس از کلیک بر روی دکمه‌ی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.

یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
<button class='btn btn-primary'
        (click)='toggleImage()'>
    {{showImage ? 'Hide' : 'Show'}} Image
</button>
در اینجا اگر مقدار خاصیت showImage مساوی true باشد، مقدار رشته‌ای Hide و اگر false باشد، مقدار رشته‌ای show بجای {{}} درج خواهد شد.



بررسی انقیاد دو طرفه یا two-way binding

تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیف‌های جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده می‌شود:
 <input [(ngModel)]='listFilter' >
ابتدا [] ذکر می‌شود تا مشخص شود که این عملیات در اصل یک property binding است؛ از خاصیت عمومی به نام listFilter به المان textbox تعریف شده.
سپس () تعریف شده‌است تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.

در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را داده‌اند یا «موز درون جعبه»! موز همان event binding است که داخل جعبه‌ی property binding قرار می‌گیرد!

خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشته‌ای listFilter را به آن اضافه می‌کنیم:
export class ProductListComponent {
    pageTitle: string = 'Product List';
    imageWidth: number = 50;
    imageMargin: number = 2;
    showImage: boolean = false;
    listFilter: string = 'cart';
سپس فایل قالب product-list.component.html را گشوده و انقیاد دو طرفه را به آن اعمال می‌کنیم:
<div class='panel-body'>
    <div class='row'>
        <div class='col-md-2'>Filter by:</div>
        <div class='col-md-4'>
            <input type='text'
                   [(ngModel)]='listFilter' />
        </div>
    </div>
    <div class='row'>
        <div class='col-md-6'>
            <h3>Filtered by: {{listFilter}}</h3>
        </div>
    </div>
در اینجا انقیاد دو طرفه، توسط ngModel، به خاصیت listFilter کلاس، در المان input تعریف شده، صورت گرفته است‌. سپس توسط interpolation مقدار این تغییر را در قسمت Filtered by به صورت یک برچسب نمایش می‌دهیم.


پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیه‌ی cart را خواهد داشت و اگر آن‌را تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by می‌توان مشاهده کرد. به این رخداد two-way binding می‌گویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمی‌شود که آن‌را در قسمت بعد تکمیل خواهیم کرد.


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

تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده می‌شود و کار آن‌ها تغییر داده‌ها، پیش از نمایش آن‌ها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شده‌است.
در اینجا یک مثال ساده‌ی pipes را مشاهده می‌کنید:
 {{ product.productCode | lowercase }}
پس از قید نام خاصیتی که قرار است نمایش داده شود، حرف pipe یا | قرار گرفته و سپس نوع pipe ذکر می‌شود. به این ترتیب کد محصول، پیش از نمایش، ابتدا به حروف کوچک تبدیل شده و سپس نمایش داده می‌شود.

از pipes در property binding هم می‌توان استفاده کرد:
 [title]='product.productName | uppercase'
در اینجا برای مثال عنوان تصویر با حروف بزرگ نمایش داده خواهد شد.

و یا می‌توان pipes را به صورت زنجیره‌ای نیز تعریف کرد:
 {{ product.price | currency | lowercase }}
در اینجا pipe توکار currency سبب نمایش سه حرف اول واحد پولی، با حروف بزرگ می‌شود. اگر علاقمند بودیم که آن‌را با حروف کوچک نمایش دهیم می‌توان یک pipe دیگر را در انتهای این زنجیره قید کرد.

بعضی از pipes، پارامتر هم قبول می‌کنند:
 {{ product.price | currency:'USD':true:'1.2-2' }}
در اینجا هر پارامتر با یک : مشخص می‌شود. برای مثال pipe واحد پولی، سه پارامتر را دریافت می‌کند: یک کد دلخواه، نمایش یا عدم نمایش علامت پولی، بجای کد دلخواه و مشخصات ارقام نمایش داده شده. برای مثال 2-1.2، یعنی حداقل یک عدد پیش از اعشار، حداقل دو عدد پس از اعشار و حداکثر دو عدد پس از اعشار باید ذکر شوند (یعنی در نهایت دو رقم اعشار مجاز است).
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.

در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
<td>{{ product.productName }}</td>
<td>{{ product.productCode | lowercase }}</td>
<td>{{ product.releaseDate }}</td>
<td>{{ product.price | currency:'USD':true:'1.2-2'}}</td>
<td>{{ product.starRating }}</td>
در اینجا با استفاده از pipes، شماره محصول با حروف کوچک و قیمت آن تا حداکثر دو رقم اعشار، فرمت خواهند شد.
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:


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


خلاصه‌ی بحث

data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن می‌شود. در AngularJS 2.0، چهار نوع binding وجود دارند:


interpolation، عبارت رشته‌ای محاسبه شده را در بین المان‌های DOM درج می‌کند و یا می‌تواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM می‌شود.
event binding به رخ‌دادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری می‌شود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافته‌ی توسط کاربر را به کلاس انجام می‌دهد.
اطلاعات نمایش داده شده‌ی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده می‌شود.