نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
روش استفاده از TypeScript در پروژه‌های Blazor
شاید علاقمند باشید تا اسکریپت‌های مورد نیاز یک پروژه‌ی Blazor را با TypeScript تهیه کنید؛ تا از مزایای بررسی نوع‌ها، intellisense قوی، null checking و غیره بهره‌مند شوید و سپس توسط کامپایلر آن، حاصل را به کدهای نهایی js تبدیل کنید. برای اینکار می‌توان مراحل زیر را طی کرد:

الف) تهیه فایل تنظیمات کامپایلر TypeScript
نیاز است فایل tsconfig.json را در ریشه‌ی پروژه، جائیکه فایل csproj قرار دارد، با محتوای زیر ایجاد کرد:
{
  "compilerOptions": {
    "strict": true,
    "removeComments": false,
    "sourceMap": false,
    "noEmitOnError": true,
    "target": "ES2020",
    "module": "ES2020",
    "outDir": "wwwroot/scripts"
  },
  "include": [
    "Scripts/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
در این حالت فرض بر این است که فایل‌های ts. در پوشه‌ی scripts قرار گرفته‌اند و فایل‌های نهایی کامپایل شده در پوشه‌ی wwwroot/scripts تولید خواهند شد.

ب) فعالسازی کامپایلر TypeScript به ازای هر بار build برنامه
برای اینکار نیاز است فایل csproj را به صورت زیر تکمیل کرد:
<Project Sdk="Microsoft.NET.Sdk.Razor">
  <ItemGroup>
    <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.3.5">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <Content Remove="tsconfig.json" />
  </ItemGroup>
  <ItemGroup>
    <TypeScriptCompile Include="tsconfig.json">
      <CopyToOutputDirectory>Never</CopyToOutputDirectory>
    </TypeScriptCompile>
  </ItemGroup>
</Project>
با اینکار ابزار TypeScript.MSBuild اضافه شده، بر اساس tsconfig.json قسمت الف، کار کامپایل فایل‌های ts را به صورت خودکار انجام می‌دهد.

ج) یک مثال از تبدیل کدهای js به ts
فرض کنید کدهای سراسری زیر را داریم که به شیء window اضافه شده‌اند:
window.exampleJsFunctions = {
  showPrompt: function (message) {
    return prompt(message, 'Type anything here');
  }
};
اکنون برای تبدیل آن به ts.، می‌توان به صورت زیر، فضای نام و کلاسی را ایجاد کرد:
namespace JSInteropWithTypeScript {
   export class ExampleJsFunctions {
        public showPrompt(message: string): string {
            return prompt(message, 'Type anything here');
        }
    }
}

export function showPrompt(message: string): string {
   var fns = new JSInteropWithTypeScript. ExampleJsFunctions();
   return fns.showPrompt(message);
}
قسمت مهم آن، export function انتهایی است. این موردی است که توسط Blazor قابل شناسایی و استفاده است.

د) روش استفاده از خروجی کامپایل شده‌ی TypeScript در کامپوننت‌های Blazor
پس از کامپایل قطعه کد فوق، ابتدا مسیر قابل دسترسی به فایل js قرار گرفته شده در پوشه‌ی wwwroot را مشخص می‌کنیم که همواره با الگوی زیر است. همچنین اینبار IJSObjectReference است که امکان دسترسی به export function یاد شده را میسر می‌کند:
private const string ScriptPath = "./_content/----namespace-here---/scripts/file.js";
private IJSObjectReference scriptModule;
دو تعریف فوق، فیلدهایی هستند که در سطح کامپوننت تعریف می‌شوند. سپس مقدار دهی آن‌ها در OnAfterRenderAsync صورت می‌گیرد تا کار import ماژول را انجام دهد:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
  if (scriptModule == null)
    scriptModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", ScriptPath);
پس از این مرحله، امکان کار با ماژول بارگذاری شده، به صورت متداولی میسر می‌شود و می‌توان export function‌ها را در اینجا فراخوانی کرد:
await scriptModule.InvokeVoidAsync("exported fn name", params);
در آخر کار هم باید آن‌را dispose کرد؛ که روش آن به صورت زیر است:
- ابتدا باید این کامپوننت، IAsyncDisposable را پیاده سازی کند:
public partial class MyComponent : IAsyncDisposable
سپس پیاده سازی آن به صورت زیر انجام می‌شود:
public async ValueTask DisposeAsync()
{
  if (scriptModule != null)
  {
    await scriptModule.DisposeAsync();
  }
}
مطالب
معرفی افزونه‌های مفید VSCode جهت کار با Angular
VSCode یکی از بهترین ادیتورهایی است که از آن می‌توان برای توسعه‌ی برنامه‌های Angular استفاده کرد و در این بین افزونه‌های ویژه‌ای جهت کار با Angular برای آن تدارک دیده شده‌اند که در ادامه تعدادی از مهم‌ترین‌های آن‌ها را بررسی می‌کنیم.


Angular Essentials

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


Angular Language Service

نگارش‌های اخیر Angular به همراه یک سرویس زبان نیز می‌باشند که به ادیتورهای مختلف این امکان را می‌دهد تا توسط این ویژگی بتوانند قابلیت‌های ویرایشی بهتری را جهت برنامه‌های Angular ارائه کنند. برای مثال ویرایش مطلوب قالب‌های کامپوننت‌های Angular و استفاده‌ی از Syntax خاص آن، موردی است که توسط هیچکدام از HTML ادیتورهای موجود پشتیبانی نمی‌شود. اکنون به کمک سرویس زبان Angular و افزونه‌ی ویژه‌ی آن برای VSCode که توسط تیم اصلی Angular توسعه یافته‌است، امکان ویرایش غنی قالب‌های HTML ایی آن فراهم شده‌است. این افزونه یک چنین قابلیت‌هایی را فراهم می‌کند:
الف) AOT Diagnostic messages
اگر قالب HTML ایی مورد استفاده (چه به صورت inline و چه در یک فایل html مجزا) به خاصیتی تعریف نشده اشاره کند، بلافاصله خطای مرتبطی ظاهر خواهد شد:


ب) Completions lists یا همان Intellisense
ج) امکان Go to definition با کلیک راست بر روی خواص و متدهای ذکر شده‌ی در قالب.
د) Quick info که با نزدیک کردن اشاره‌گر ماوس به خاصیت یا متدی در صفحه، اطلاعات بیشتری را در مورد آن نمایش می‌دهد.


angular2-inline

علاوه بر افزونه‌ی سرویس زبان‌های Angular، این افزونه نیز قابلیت درک قالب‌های inline کامپوننت‌ها را داشته و به همراه syntax highlighting و همچنین Intellisense است.


Auto Import

حین کار با TypeScript، هر ماژولی که در صفحه ارجاعی داشته باشد، باید در ابتدای فایل جاری import شود. افزونه‌ی Auto Import با بررسی ماژول‌های موجود و فراهم آوردن Intellisense ایی بر اساس آن‌ها، این‌کار را ساده‌تر می‌کند:


بنابراین این افزونه صرفا مختص به Angular نیست و برای کارهای متداول TypeScript نیز بسیار مفید است.


TSLint

این افزونه ابزار TSLint را با VSCode یکپارچه می‌کند. بنابراین نیاز است پیش از نصب این افزونه، وابستگی‌های ذیل را نیز به صورت سراسری نصب کرد:
 > npm install -g tslint typescript
کار TSLint انجام static code analysis است؛ چیزی شبیه به افزونه‌هایی مانند ری‌شارپر در ویژوال استودیو که راهنماهایی را در مورد بهتر کردن کیفیت کدهای نوشته شده ارائه می‌دهد.
تعدادی از امکانات آن‌را پس از نصب، با فشردن دکمه‌ی F1 می‌توان مشاهده کرد:


برای مثال تولید فایل tslint.json، امکان سفارشی سازی موارد بررسی شونده‌ی توسط این افزونه را فراهم می‌کند و اگر برنامه‌ی خود را توسط Angular CLI ایجاد کرده‌اید، این فایل هم اکنون در ریشه‌ی پروژه قرار دارد.
در مورد TSLint در مطلب «Angular CLI - قسمت دوم - ایجاد یک برنامه‌ی جدید» بیشتر توضیح داده شده‌است و اینبار به کمک این افزونه، خطاهای یاد شده را دقیقا درون محیط ادیتور و به صورت خودکار و یکپارچه‌ای مشاهده خواهید کرد.



Angular v4 TypeScript Snippets

سیستم کار VSCode مبتنی بر ایجاد فایل‌های خالی است و مفهوم قالب‌های از پیش آماده‌ی فایل‌ها در آن وجود ندارد. اما با کمک Code Snippets می‌توان این خلاء را پر کرد. افزونه‌ی Angular v4 TypeScript Snippets دقیقا به همین منظور طراحی شده‌است و زمانیکه حروف -a یا -rx را در صفحه تایپ می‌کنید، منویی ظاهر خواهد شد که توسط آن می‌توان قالب ابتدایی شروع به کار با انواع و اقسام جزئیات پروژه‌های Angular را تهیه کرد.



Path Intellisense

این افزونه مسیر فایل‌های موجود را به صورت یک Intellisense ارائه می‌کند و به این صورت به سادگی می‌توان مسیرهای اسکریپت‌ها و یا شیوه‌نامه‌ها را در ادیتور انتخاب و وارد کرد.

مسیرراه‌ها
React 16x
پیش نیاز ها
کامپوننت ها
ترکیب کامپوننت ها
طراحی یک گرید
مسیریابی 
کار با فرم ها
ارتباط با سرور
احراز هویت و اعتبارسنجی کاربران 
React Hooks  
توزیع برنامه

مدیریت پیشرفته‌ی حالت در React با Redux و Mobx   

       Redux
       MobX  

مطالب تکمیلی 
    مطالب
    React 16x - قسمت 13 - طراحی یک گرید - بخش 3 - مرتب سازی اطلاعات
    تا اینجا صفحه بندی و فیلتر کردن اطلاعات را پیاده سازی کردیم. در این قسمت شروع به refactoring کامپوننت movies کرده، جدول آن‌را تبدیل به یک کامپوننت مجزا می‌کنیم و سپس مرتب سازی اطلاعات را نیز به آن اضافه خواهیم کرد.


    استخراج جدول فیلم‌ها

    در طراحی فعلی کامپوننت movies، مشکل کوچکی وجود دارد: این کامپوننت تا اینجا، ترکیبی شده‌است از دو کامپوننت صفحه بندی و نمایش لیست گروه‌ها، به همراه جزئیات کامل یک جدول بسیار طولانی. به این مشکل، mixed levels of abstractions می‌گویند. در اینجا دو کامپوننت سطح بالا را داریم، به همراه یک جدول سطح پایین که تمام مشخصات آن در معرض دید هستند و با هم مخلوط شده‌اند. یک چنین کدی، یکدست به نظر نمی‌رسد. به همین جهت اولین کاری را که در ادامه انجام خواهیم داد، تعریف یک کامپوننت جدید و انتقال تمام جزئیات جدول نمایش ردیف‌های فیلم‌ها، به آن است. برای این منظور فایل جدید src\components\moviesTable.jsx را ایجاد کرده و توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت MoviesTable را تولید می‌کنیم. این کامپوننت را در پوشه‌ی common قرار ندادیم؛ از این جهت که قابلیت استفاده‌ی مجدد در سایر برنامه‌ها را ندارد. کار آن تنها مرتبط و مختص به اشیاء فیلمی است که در سرویس‌های برنامه داریم. البته در ادامه، این جدول را نیز به چندین کامپوننت با قابلیت استفاده‌ی مجدد، خواهیم شکست؛ اما فعلا در اینجا با اصل کدهای سطح پایین جدول نمایش داده شده‌ی در کامپوننت movies، شروع می‌کنیم، آن‌ها را cut کرده و به متد رندر کامپوننت جدید MoviesTable منتقل می‌کنیم.

    پس از انتقال کامل تگ table از کامپوننت movies به داخل متد رندر کامپوننت MoviesTable، در ابتدای آن توسط Object Destructuring، یک آرایه و دو رخ‌د‌‌ادی را که برای مقدار دهی قسمت‌های مختلف آن نیاز داریم، از props فرضی، استخراج می‌کنیم. اینکار کمک می‌کند تا بتوان اینترفیس این کامپوننت را به خوبی مشخص و طراحی کرد:
    class MoviesTable extends Component {
      render() {
        const { movies, onDelete, onLike } = this.props;

    پس از تعریف متغیرهای مورد نیاز، ابتدا برای اینکه بتوانیم در اینجا نیز مجددا از کامپوننت Like استفاده کنیم، کلاس آن‌را از ماژول مرتبط import می‌کنیم:
    import Like from "./common/like";
    سپس از onLike تعریف شده، بجای this.handleLike قبلی استفاده می‌کنیم:
    // ...
    <Like liked={movie.liked} onClick={() => onLike(movie)} />

    همچنین در جائیکه onClick دکمه‌ی حذف به this.handleDelete کامپوننت movies متصل بود، از onDelete تعریف شده‌ی در ابتدای متد رندر فوق استفاده خواهیم کرد:
    <button
      onClick={() => onDelete(movie)}
      className="btn btn-danger btn-sm"
    >
      Delete
    </button>

    همین اندازه تغییر، این کامپوننت جدید را مجددا قابل استفاده می‌کند. بنابراین به کامپوننت movies بازگشته و ابتدا کلاس آن‌را import می‌کنیم:
    import MoviesTable from "./moviesTable";
    و سپس المان آن‌را در محل قبلی جدول درج شده، تعریف می‌کنیم:
    <MoviesTable
      movies={movies}
      onDelete={this.handleDelete}
      onLike={this.handleLike}
    />
    همانطور که مشاهده می‌کنید، ویژگی‌های تعریف شده‌ی در اینجا همان‌هایی هستند که با استفاده از Object Destructuring در ابتدای متد رندر کامپوننت MoviesTable، تعریف کردیم.
    پس از این تغییرات، متد رندر کامپوننت movies چنین شکلی را پیدا کرده‌است که در آن سه کامپوننت سطح بالا درج شده‌اند و در یک سطح از abstraction قرار دارند و دیگر مخلوطی از المان‌های سطح بالا و سطح پایین را نداریم:
        return (
          <div className="row">
            <div className="col-3">
              <ListGroup
                items={this.state.genres}
                onItemSelect={this.handleGenreSelect}
                selectedItem={this.state.selectedGenre}
              />
            </div>
            <div className="col">
              <p>Showing {totalCount} movies in the database.</p>
              <MoviesTable
                movies={movies}
                onDelete={this.handleDelete}
                onLike={this.handleLike}
              />
              <Pagination
                itemsCount={totalCount}
                pageSize={this.state.pageSize}
                onPageChange={this.handlePageChange}
                currentPage={this.state.currentPage}
              />
            </div>
          </div>
        );


    صدور رخ‌داد مرتب سازی اطلاعات

    اکنون نوبت فعالسازی کلیک بر روی سرستون‌های جدول نمایش داده شده و مرتب سازی اطلاعات جدول بر اساس ستون انتخابی است. به همین جهت در کامپوننت MoviesTable، رویداد onSort را هم به لیستی از خواصی که از props انتظار داریم، اضافه می‌کنیم که در نهایت در کامپوننت movies، به یک متد رویدادگردان متصل می‌شود:
    class MoviesTable extends Component {
      render() {
        const { movies, onDelete, onLike, onSort } = this.props;

    سپس رویداد کلیک بر روی هر سر ستون را توسط onSort و نام خاصیتی که به آن ارسال می‌شود، به استفاده کننده‌ی از کامپوننت MoviesTable منتقل می‌کنیم تا بر اساس نام این خاصیت، کار مرتب سازی اطلاعات را انجام دهد:
        return (
          <table className="table">
            <thead>
              <tr>
                <th style={{ cursor: "pointer" }} onClick={() => onSort("title")}>Title</th>
                <th style={{ cursor: "pointer" }} onClick={() => onSort("genre.name")}>Genre</th>
                <th style={{ cursor: "pointer" }} onClick={() => onSort("numberInStock")}>Stock</th>
                <th style={{ cursor: "pointer" }} onClick={() => onSort("dailyRentalRate")}>Rate</th>
                <th />
                <th />
              </tr>
            </thead>

    در ادامه به کامپوننت movies مراجعه کرده و رویداد onSort را مدیریت می‌کنیم. برای این منظور ویژگی جدید onSort را به المان MoviesTable اضافه کرده و آن‌را به متد handleSort متصل می‌کنیم:
    <MoviesTable
      movies={movies}
      onDelete={this.handleDelete}
      onLike={this.handleLike}
      onSort={this.handleSort}
    />
    متد handleSort هم به صورت زیر تعریف می‌شود:
      handleSort = column => {
        console.log("handleSort", column);
      };


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

    تا اینجا اگر دقت کرده باشید، هر زمانیکه شماره صفحه‌ای تغییر می‌کند یا گروه فیلم خاصی انتخاب می‌شود، ابتدا state را به روز رسانی می‌کنیم که در نتیجه‌ی آن، کار رندر مجدد کامپوننت در DOM مجازی React صورت می‌گیرد. سپس در متد رندر، کار تغییر اطلاعات آرایه‌ی فیلم‌ها را جهت نمایش به کاربر، انجام می‌دهیم.
    بنابراین ابتدا در متد رویدادگران handleSort، با فراخوانی متد setState، مقدار path دریافتی حاصل از کلیک بر روی یک سرستون را به همراه صعودی و یا نزولی بودن مرتب سازی، در state کامپوننت جاری تغییر می‌دهیم:
      handleSort = path => {
        console.log("handleSort", path);
        this.setState({ sortColumn: { path, order: "asc" } });
      };
    البته بهتر است این sortColumn تعریف شده‌ی در اینجا را به تعریف خاصیت state نیز به صورت مستقیم اضافه کنیم تا در اولین بار نمایش صفحه، تعریف شده و قابل دسترسی باشد:
    class Movies extends Component {
      state = {
        // ...
        sortColumn: { path:"title", order: "asc" }
      };

    سپس متد getPagedData را که در قسمت قبل اضافه و تکمیل کردیم، جهت اعمال این خواص به روز رسانی می‌کنیم:
      getPagedData() {
        const {
          pageSize,
          currentPage,
          selectedGenre,
          movies: allMovies,
          sortColumn
        } = this.state;
    
        let filteredMovies =
          selectedGenre && selectedGenre._id
            ? allMovies.filter(m => m.genre._id === selectedGenre._id)
            : allMovies;
    
        filteredMovies = filteredMovies.sort((movie1, movie2) =>
          movie1[sortColumn.path] > movie2[sortColumn.path]
            ? sortColumn.order === "asc"
              ? 1
              : -1
            : movie2[sortColumn.path] > movie1[sortColumn.path]
            ? sortColumn.order === "asc"
              ? -1
              : 1
            : 0
        );
    
        const first = (currentPage - 1) * pageSize;
        const last = first + pageSize;
        const pagedMovies = filteredMovies.slice(first, last);
    
        return { totalCount: filteredMovies.length, data: pagedMovies };
      }
    در اینجا کار sort بر اساس sortColumn.path و sortColumn.order پس از فیلتر شدن اطلاعات و پیش از صفحه بندی، انجام می‌شود. در مورد متد sort و filter و امثال آن می‌توانید به مطلب «بررسی معادل‌های LINQ در TypeScript» برای مطالعه‌ی بیشتر مراجعه کنید.

    همچنین می‌خواهیم اگر با کلیک بر روی ستونی، روش و جهت مرتب سازی آن صعودی بود، نزولی شود و یا برعکس که یک روش پیاده سازی آن‌را در اینجا مشاهده می‌کنید:
      handleSort = path => {
        console.log("handleSort", path);
        const sortColumn = { ...this.state.sortColumn };
        if (sortColumn.path === path) {
          sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
        } else {
          sortColumn.path = path;
          sortColumn.order = "asc";
        }
        this.setState({ sortColumn });
      };
    چون می‌خواهیم خواص this.state.sortColumn را تغییر دهیم و تغییر مستقیم state در React مجاز نیست، ابتدا یک clone از آن‌را ایجاد کرده و سپس بر روی این clone کار می‌کنیم. در نهایت این شیء جدید را بجای شیء قبلی در state به روز رسانی خواهیم کرد.


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

    اگر قرار باشد کامپوننت MoviesTable را در جای دیگری مورد استفاده‌ی مجدد قرار دهیم، زمانیکه این جدول سبب صدور رخ‌دادی می‌شود، باید منطقی را که در متد handleSort فوق مشاهده می‌کنید، مجددا به همین شکل تکرار کنیم. بنابراین این منطق متعلق به کامپوننت MoviesTable است و زمانیکه onSort را فراخوانی می‌کند، بهتر است بجای ارسال path یا همان نام فیلدی که قرار است مرتب سازی بر اساس آن انجام شود، شیء sortColumn را به عنوان خروجی بازگشت دهد. به همین جهت، این منطق را به کلاس MoviesTable منتقل می‌کنیم:
    class MoviesTable extends Component {
      raiseSort = path => {
        console.log("raiseSort", path);
        const sortColumn = { ...this.props.sortColumn };
        if (sortColumn.path === path) {
          sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
        } else {
          sortColumn.path = path;
          sortColumn.order = "asc";
        }
        this.props.onSort(sortColumn);
      };
    در این متد جدید بجای this.state.sortColumn قبلی، اینبار sortColumn را از props دریافت می‌کنیم. بنابراین نیاز خواهد بود تا ویژگی جدید sortColumn را به تعریف المان MoviesTable در کامپوننت movies، اضافه کنیم:
    <MoviesTable
      movies={movies}
      onDelete={this.handleDelete}
      onLike={this.handleLike}
      onSort={this.handleSort}
      sortColumn={this.state.sortColumn}
    />

     همچنین در کامپوننت MoviesTable، کار فراخوانی onSort را جهت بازگشت sortColumn محاسبه شده در همین متد raiseSort انجام می‌دهیم. بنابراین تمام onSortهای هدر جدول به this.raiseSort تغییر می‌کنند:
        return (
          <table className="table">
            <thead>
              <tr>
                <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("title")}>Title</th>
                <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("genre.name")}>Genre</th>
                <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("numberInStock")}>Stock</th>
                <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("dailyRentalRate")}>Rate</th>
                <th />
                <th />
              </tr>
            </thead>


    کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-13.zip
    مطالب
    اعمال صفحه بندی به کمک OFFSET و FETCH در SQL Server 2012
    در T-SQL 2012 قابلیت صفحه بندی، نمایش خروجی یک Query فراهم گردیده است، که برای نرم افزارهای تحت وب بسیار پرکاربرد میباشد، به عنوان مثال، از جمله کاربردهای بارز آن، می‌توان به نمایش نتیجه یک جستجو بصورت صفحه بندی با تعداد رکورد محدود،اشاره نمود.
     مایکروسافت برای ایجاد قابلیت صفحه بندی و محدود نمودن نمایش خروجی یک Query، تغییراتی را در Syntax مربوط به Order by ایجاد نموده است، که در ذیل مشاهده می‌نمایید:
    ORDER BY order_by_expression
        [ COLLATE collation_name ] 
        [ ASC | DESC ] 
        [ ,...n ] 
    [ <offset_fetch> ]
    
    
    <offset_fetch> ::=
    { 
        OFFSET { integer_constant | offset_row_count_expression } { ROW | ROWS }
        [
          FETCH { FIRST | NEXT } {integer_constant | fetch_row_count_expression } { ROW | ROWS } ONLY
        ]
    }
    
    OFFSET (نقطه شروع) :  شامل یک پارامتر است،بطوریکه،پارامتر فوق می‌تواند یک عدد (integer_constant) یا یک عبارت (offset_row_count_expression) بپذیرد. در اینجا منظور از عبارت می‌تواند یک Subquery باشد، که خروجی آن فقط یک مقدار عددی است. یا یک متغیر و غیرو... 
    در مورد ROW یا ROWS باید بگویم باهم فرقی ندارند.
    FETCH : همانند OFFSET شامل یک پارامتر است، و پارامتر آن می‌تواند یک عدد یا عبارت بپذیرد.
    Next یا First نیز با هم تفاوتی ندارند و جهت سازگاری با ANSI می‌باشند.
    OFFSET : در وافع تعداد سطر قابل حذف، پیش از نمایش اولین سطر در خروجی را بیان می‌کند.
    FETCH : بیانگر تعداد رکورد قابل نمایش در یک صفحه می‌باشد.
    برای درک بیشتر مثالی می‌زنیم:
    ابتدا بوسیله Script زیر یک جدول ایجاد می‌نماییم، سپس چند رکورد درون آن درج می‌کنیم:
    Create Table Testoffset
    (BusinessEntityID int,
    FirstName varchar(100) ,
    LastName varchar(100)
    );
    
    Insert into Testoffset (BusinessEntityID,FirstName,LastName)
                Values(1,'Ken','Sánchez')
        ,(2,'Terri','Duffy')
    ,(3,'Roberto','Tamburello')
    ,(4,'Rob','Walters')
    ,(5,'Gail','Erickson')
    ,(6,'Jossef','Goldberg')
    ,(7,'Dylan','Miller')
    ,(8,'Diane','Margheim')
    ,(9,'Gigi','Matthew')
    ,(10,'Michael','Raheem')
    در ادامه Script زیر را اجرا نمایید، تا تعداد رکورد‌های درج شده را مشاهده کنید:

    در شکل، سه سطر (منظور رکورد 4و5و6) در کادر قرمز رنگ دیده می‌شود، می‌خواهیم Script ی ایجاد نماییم، که فقط سه سطر فوق را نمایش دهد. بنابراین خواهیم داشت:
    SELECT BusinessEntityID, FirstName, LastName
    FROM Testoffset
    ORDER BY BusinessEntityID
    OFFSET 3 ROWS
    FETCH First 3 ROWS only
    خروجی:

    اگر به Query اجرا شده دقت کنیم. در قسمت Order By جلوی Offset مقدار 3 اختصاص داده شده بود، یعنی نقطه شروع از سطر چهارم میباشد، به عبارت دیگر مقداری که به Offset اختصاص داده می‌شود، به SQL Server می‌فهماند،چه تعداد رکورد را نمایش ندهد. اگر شکل اول و دوم را با هم مقایسه نمایید، براحتی متوجه می‌شوید که OFFSET نقطه شروع را مشخص کرده است.
    مقداریکه برای Fetch در نظر گرفته شده بود برابر 3 است، که بیانگر تعداد سطر نمایش داده شده در خروجی از نقطه آغازین (offset) می‌باشد.
    امیدوارم مفید واقع شده باشد.
    مطالب
    ایجاد جداول بهینه سازی شده برای حافظه در SQL Server 2014
    پس از نگاهی به مفاهیم مقدماتی OLTP درون حافظه‌ای در SQL Server 2014، در ادامه به نحوه‌ی انجام تنظیمات خاص جداول بهینه سازی شده برای حافظه خواهیم پرداخت.


    ایجاد یک بانک اطلاعاتی با پشتیبانی از جداول بهینه سازی شده برای حافظه

    برای ایجاد جداول بهینه سازی شده برای حافظه، ابتدا نیاز است تا تنظیمات خاصی را به بانک اطلاعاتی آن اعمال کنیم. برای اینکار می‌توان یک بانک اطلاعاتی جدید را به همراه یک filestream filegroup ایجاد کرد که جهت جداول بهینه سازی شده برای حافظه، ضروری است؛ یا اینکه با تغییر یک بانک اطلاعاتی موجود و افزودن filegroup یاد شده نیز می‌توان به این مقصود رسید.
    در اینگونه جداول خاص، اطلاعات در حافظه‌ی سیستم ذخیره می‌شوند و برخلاف جداول مبتنی بر دیسک سخت، صفحات اطلاعات وجود نداشته و نیازی نیست تا به کش بافر وارد شوند. برای مقاصد ذخیره سازی نهایی اطلاعات جداول بهینه سازی شده برای حافظه، موتور OLTP درون حافظه‌ای آن، فایل‌های خاصی را به نام checkpoint در یک filestream filegroup ایجاد می‌کند که از آن‌ها جهت ردیابی اطلاعات استفاده خواهد کرد و نحوی ذخیره سازی اطلاعات در آن‌ها از شیوه‌ی با کارآیی بالایی به نام append only mode پیروی می‌کند.
    با توجه به متفاوت بودن نحوه‌ی ذخیره سازی نهایی اطلاعات اینگونه جداول و دسترسی به آن‌ها از طریق استریم‌ها، توصیه شده‌است که filestream filegroup‌های تهیه شده را در یک SSD یا Solid State Drive قرار دهید.

    پس از اینکه بانک اطلاعاتی خود را به روش‌های معمول ایجاد کردید، به برگه‌ی خواص آن در management studio مراجعه کنید. سپس صفحه‌ی file groups را در آن انتخاب کرده و در پایین برگه‌ی آن، در قسمت جدید memory optimized data، بر روی دکمه‌ی Add کلیک کنید. سپس نام دلخواهی را وارد نمائید.


    پس از ایجاد یک گروه فایل جدید، به صفحه‌ی files خواص بانک اطلاعاتی مراجعه کرده و بر روی دکمه‌ی Add کلیک کنید. سپس File type این ردیف اضافه شده را از نوع file stream data و file group آن‌را همان گروه فایلی که پیشتر ایجاد کردیم، تنظیم کنید. در ادامه logical name دلخواهی را وارد کرده و در آخر بر روی دکمه‌ی Ok کلیک کنید تا تنظیمات مورد نیاز جهت تعاریف جدول بهینه سازی شده برای حافظه به پایان برسد.


    این مراحل را توسط دو دستور T-SQL ذیل نیز می‌توان سریعتر انجام داد:
    USE [master]
    GO
    ALTER DATABASE [testdb2] 
          ADD FILEGROUP [InMemory_InMemory] CONTAINS MEMORY_OPTIMIZED_DATA 
    GO
    ALTER DATABASE [testdb2] 
          ADD FILE ( NAME = N'InMemory_InMemory', FILENAME = N'D:\SQL_Data\MSSQL11.MSSQLSERVER\MSSQL\DATA\InMemory_InMemory' ) 
          TO FILEGROUP [InMemory_InMemory]
    GO

    ساختار گروه فایل بهینه سازی شده برای حافظه

    گروه فایل بهینه سازی شده برای حافظه، دارای چندین دربرگیرنده است که هر کدام چندین فایل را در خود جای خواهند داد:
    - Root File که در برگیرنده‌ی متادیتای اطلاعات است.
    - Data File که شامل ردیف‌های اطلاعات ثبت شده در جداول بهینه سازی شده‌ی برای حافظه هستند. این ردیف‌ها همواره به انتهای data file اضافه می‌شوند و دسترسی به آن‌ها ترتیبی است. کارآیی IO این روش نسبت به روش دسترسی اتفاقی به مراتب بالاتر است. حداکثر اندازه این فایل 128 مگابایت است و پس از آن یک فایل جدید ساخته می‌شود.
    - Delta File شامل ردیف‌هایی است که حذف شده‌اند. به ازای هر ردیف، حداقل اطلاعاتی از آن را در خود ذخیره خواهد کرد؛ شامل ID ردیف حذف شده و شماره تراکنش آن. همانطور که پیشتر نیز ذکر شد، این موتور جدید درون حافظه‌ای، برای یافتن راه چاره‌ای جهت به حداقل رسانی قفل گذاری بر روی اطلاعات، چندین نگارش از ردیف‌ها را به همراه timestamp آن‌ها در خود ذخیره می‌کند. به این ترتیب، هر به روز رسانی به همراه یک حذف و سپس ثبت جدید است. به این ترتیب دیگر بانک اطلاعاتی نیازی نخواهد داشت تا به دنبال رکورد موجود برگردد و سپس اطلاعات آن‌را به روز نماید. این موتور جدید فقط اطلاعات به روز شده را در انتهای رکوردهای موجود با فرمت خود ثبت می‌کند.


    ایجاد جداول بهینه سازی شده برای حافظه

    پس از آماده سازی بانک اطلاعاتی خود و افزودن گروه فایل استریم جدیدی به آن برای ذخیره سازی اطلاعات جداول بهینه سازی شده برای حافظه، اکنون می‌توانیم اینگونه جداول خاص را در کنار سایر جداول متداول موجود، تعریف و استفاده نمائیم:
    -- It is not a Memory Optimized
    CREATE TABLE tblNormal
    (
       [CustomerID] int NOT NULL PRIMARY KEY NONCLUSTERED, 
       [Name] nvarchar(250) NOT NULL,
       CustomerSince DATETIME not NULL
          INDEX [ICustomerSince] NONCLUSTERED
    )
    
    --  DURABILITY = SCHEMA_AND_DATA
    CREATE TABLE tblMemoryOptimized_Schema_And_Data
    (
        [CustomerID] INT NOT NULL 
    PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
        [Name] NVARCHAR(250) NOT NULL,
        [CustomerSince] DATETIME NOT NULL
    INDEX [ICustomerSince] NONCLUSTERED
    ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA)
    
    
    -- DURABILITY = SCHEMA_ONLY
    CREATE TABLE tblMemoryOptimized_Schema_Only
    (
        [CustomerID] INT NOT NULL 
    PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
        [Name] NVARCHAR(250) NOT NULL,
        [CustomerSince] DATETIME NOT NULL
    INDEX [ICustomerSince] NONCLUSTERED
    ) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY)
    در اینجا سه جدول را مشاهده می‌کنید که در بانک اطلاعاتی آماده شده در مرحله‌ی قبل، ایجاد خواهند شد. مورد اول یک جدول معمولی است که از آن برای مقایسه سرعت ثبت اطلاعات با سایر جداول ایجاد شده، استفاده خواهد شد.
    همانطور که مشخص است، دو جدول بهینه سازی شده برای حافظه، همان سه ستون جدول معمولی مبتنی بر دیسک سخت را دارا هستند؛ اما با این تفاوت‌ها:
    - دارای ویژگی MEMORY_OPTIMIZED = ON می‌باشند. به این ترتیب اینگونه جداول نسبت به جداول متداول مبتنی به دیسک سخت متمایز خواهند شد.
    - دارای ویژگی DURABILITY بوده و توسط مقدار SCHEMA_AND_DATA آن مشخص می‌کنیم که آیا قرار است اطلاعات و ساختار جدول، ذخیره شوند یا تنها قرار است ساختار جدول ذخیره گردد (حالت SCHEMA_ONLY).
    - بر روی ستون Id آن‌ها یک hash index ایجاد شده‌است که وجود آن ضروری است و در کل بیش از 8 ایندکس را نمی‌توان تعریف کرد.
    برخلاف ایندکس‌های B-tree جداول مبتنی بر سخت دیسک، ایندکس‌های جداول بهینه سازی شده برای حافظه، اطلاعات را تکرار نمی‌کنند. این‌ها صرفا اشاره‌گرهایی هستند به ردیف‌های اصلی اطلاعات. به این معنا که این ایندکس‌ها لاگ نشده و همچنین بر روی سخت دیسک ذخیره نمی‌شوند. کار بازسازی مجدد آن‌ها در اولین بار بازیابی بانک اطلاعاتی و آغاز آن به صورت خودکار انجام می‌شود. به همین جهت مباحثی مانند index fragmentation و نگهداری ایندکس‌ها دیگر در اینجا معنا پیدا نمی‌کنند.
    دو نوع ایندکس را در اینجا می‌توان تعریف کرد. اولین آن‌ها hash index است و دومین آن‌ها range index. هش ایندکس‌ها برای حالاتی که در کوئری‌ها از عملگر تساوی استفاده می‌شود بسیار مناسب هستند. برای عملگرهای مقایسه‌ای از ایندکس‌های بازه‌ای استفاده می‌شود.
    همچنین باید دقت داشت که پس از ایجاد ایندکس‌ها، دیگر امکان تغییر آن‌ها و یا تغییر ساختار جدول ایجاد شده نیست.
    همچنین ایندکس‌های تعریف شده در جداول بهینه سازی شده برای حافظه، تنها بر روی ستون‌هایی غیرنال پذیر از نوع BIN2 collation مانند int و datetime قابل تعریف هستند. برای مثال اگر سعی کنیم بر روی ستون Name ایندکسی را تعریف کنیم، به این خطا خواهیم رسید:
     Indexes on character columns that do not use a *_BIN2 collation are not supported with indexes on memory optimized tables.
    - در حین تعریف هش ایندکس‌ها، مقدار BUCKET_COUNT نیز باید تنظیم شود. هر bucket توسط مقداری که حاصل هش کردن یک ستون است مشخص می‌شود. کلیدهای منحصربفرد دارای هش‌های یکسان در bucketهای یکسانی ذخیره می‌شوند. به همین جهت توصیه شده‌است که حداقل مقدار bucket تعیین شده در اینجا مساوی یا بیشتر از مقدار تعداد کلیدهای منحصربفرد یک جدول باشد؛ مقدار پیش فرض 2 برابر توسط مایکروسافت توصیه شده‌است.
    - نوع‌های قابل تعریف ستون‌ها نیز در اینجا به موارد ذیل محدود هستند و جمع طول آن‌ها از 8060 نباید بیشتر شود:
     bit, tinyint, smallint, int, bigint, money, smallmoney, float, real, datetime, smalldatetime, datetime2,
    date, time, numberic, decimal, char(n),  varchar(n) ,nchar(n),  nvarchar(n), sysname, binary(n),
    varbinary(n), and Uniqueidentifier


    همچنین در management studio، گزینه‌ی جدید new -> memory optimized table نیز اضافه شده‌است و انتخاب آن سبب می‌شود تا قالب T-SQL ایی برای تهیه این نوع جداول، به صورت خودکار تولید گردد.


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


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

    در مثال زیر، 100 هزار رکورد را در سه جدولی که پیشتر ایجاد کردیم، ثبت کرده و سپس مدت زمان اجرای هر کدام از مجموعه عملیات را بر حسب میلی ثانیه بررسی می‌کنیم:
    set statistics time off
    SET STATISTICS IO Off
    set nocount on
    go
    -----------------------------
    
    Print 'insert into tblNormal'
    
    DECLARE @start datetime = getdate()
    declare @insertCount int = 100000
    declare @startId int = 1
    declare @customerID int = @startId
    
    while @customerID < @startId + @insertCount
    begin
        insert into tblNormal values (@customerID, 'Test', '2013-01-01T00:00:00')
        set @customerID +=1
    end
    
    Print DATEDIFF(ms,@start,getdate());
    go
    -----------------------------
    
    Print 'insert into tblMemoryOptimized_Schema_And_Data'
    
    DECLARE @start datetime = getdate()
    declare @insertCount int = 100000
    declare @startId int = 1
    declare @customerID int = @startId
    
    while @customerID < @startId + @insertCount
    begin
        insert into tblMemoryOptimized_Schema_And_Data values (@customerID, 'Test', '2013-01-01T00:00:00')
        set @customerID +=1
    end
    Print DATEDIFF(ms,@start,getdate());
    Go
    -----------------------------
    
    Print 'insert into tblMemoryOptimized_Schema_Only'
    
    DECLARE @start datetime = getdate()
    declare @insertCount int = 100000
    declare @startId int = 1
    declare @customerID int = @startId
    
    while @customerID < @startId + @insertCount
    begin
        insert into tblMemoryOptimized_Schema_Only values (@customerID, 'Test', '2013-01-01T00:00:00')
        set @customerID +=1
    end
    Print DATEDIFF(ms,@start,getdate());
    
    Go
    با این خروجی تقریبی که بر اساس توانمندی‌های سخت افزاری سیستم می‌تواند متفاوت باشد:
     insert into tblNormal
    36423
    
    insert into tblMemoryOptimized_Schema_And_Data
    30516
    
    insert into tblMemoryOptimized_Schema_Only
    3176
    و برای حالت select خواهیم داشت:
     set nocount on
    print 'tblNormal'
    set statistics time on
    select count(CustomerID) from tblNormal
    set statistics time off
    go
    print 'tblMemoryOptimized_Schema_And_Data'
    set statistics time on
    select count(CustomerID) from tblMemoryOptimized_Schema_And_Data
    set statistics time off
    go
    print 'tblMemoryOptimized_Schema_Only'
    set statistics time on
    select count(CustomerID) from tblMemoryOptimized_Schema_Only
    set statistics time off
    go
    با این خروجی
     tblNormal
     SQL Server Execution Times:
    CPU time = 46 ms,  elapsed time = 52 ms.
    
    tblMemoryOptimized_Schema_And_Data
     SQL Server Execution Times:
    CPU time = 32 ms,  elapsed time = 33 ms.
    
    tblMemoryOptimized_Schema_Only
     SQL Server Execution Times:
    CPU time = 31 ms,  elapsed time = 30 ms.
    تاثیر جداول بهینه سازی شده برای حافظه را در 350K inserts بهتر می‌توان با نمونه‌های متداول مبتنی بر دیسک مقایسه کرد.


    برای مطالعه بیشتر

    Getting started with SQL Server 2014 In-Memory OLTP
    Introduction to SQL Server 2014 CTP1 Memory-Optimized Tables
    Overcoming storage speed limitations with Memory-Optimized Tables for SQL Server
    Memory-optimized Table – Day 1 Test
    Memory-Optimized Tables – Insert Test
    Memory Optimized Table – Insert Test …Again
    نظرات مطالب
    نمونه‌ای از تزریق اس کیوال جهت درج تبلیغات مخفی شده‌ی در رکوردهای سایت
    ممنون جناب نصیری.
    برای اینکه متوجه بشیم داده‌های دیتابیس ما آلوده شده یا نه میتونیم یک همچین کوئری رو اجرا کنیم :
    DECLARE
        @search_string  VARCHAR(100),
        @table_name     SYSNAME,
        @table_id       INT,
        @column_name    SYSNAME,
        @sql_string     VARCHAR(2000)
    
    SET @search_string = 'display:none'
    
    DECLARE tables_cur CURSOR FOR SELECT name, object_id FROM sys.objects WHERE type = 'U'
    
    OPEN tables_cur
    
    FETCH NEXT FROM tables_cur INTO @table_name, @table_id
    
    WHILE (@@FETCH_STATUS = 0)
    BEGIN
        DECLARE columns_cur CURSOR FOR SELECT name FROM sys.columns WHERE object_id = @table_id AND system_type_id IN (167, 175, 231, 239)
    
        OPEN columns_cur
    
        FETCH NEXT FROM columns_cur INTO @column_name
        WHILE (@@FETCH_STATUS = 0)
        BEGIN
            SET @sql_string = 'IF EXISTS (SELECT * FROM ' + @table_name + ' WHERE [' + @column_name + '] LIKE ''%' + @search_string + '%'') PRINT ''' + @table_name + ', ' + @column_name + ''''
    
            EXECUTE(@sql_string)
    
            FETCH NEXT FROM columns_cur INTO @column_name
        END
    
        CLOSE columns_cur
    
        DEALLOCATE columns_cur
    
        FETCH NEXT FROM tables_cur INTO @table_name, @table_id
    END
    
    CLOSE tables_cur
    
    DEALLOCATE tables_cur
    با توجه به پارامتر search_string ، لیست جدول‌ها و ستون هایی که اون مقدار داخلش هستند رو در خروجی نمایش میده. فرضا من تو دوتا از ستون‌های ، دوتا از جدولام مقادیر الوده رو قرار دادم( به صورت دستی) و خروجی به این صورت نمایش داده شد :

    Roles, Description
    Articles, Subject
    که درست هستش. برای ترمیمش هم همچین کوئری رو در sql اجرا کردم تا دستورات آپدیت حاصل بشن :

    DECLARE
        @search_string  VARCHAR(100),
        @table_name     SYSNAME,
        @table_id       INT,
        @column_name    SYSNAME,
        @sql_string     VARCHAR(2000)
    
    SET @search_string = 'display:none' -- The spammy text to search for.
    
    DECLARE tables_cur CURSOR FOR SELECT name, object_id FROM sys.objects WHERE type = 'U'
    
    OPEN tables_cur
    
    FETCH NEXT FROM tables_cur INTO @table_name, @table_id
    
    WHILE (@@FETCH_STATUS = 0)
    BEGIN
        DECLARE columns_cur CURSOR FOR SELECT name FROM sys.columns WHERE object_id = @table_id AND system_type_id IN (167, 175, 231, 239)
    
        OPEN columns_cur
    
        FETCH NEXT FROM columns_cur INTO @column_name
        WHILE (@@FETCH_STATUS = 0)
        BEGIN
            SET @sql_string = 'IF EXISTS (SELECT * FROM [' + @table_name + '] WHERE [' + @column_name + '] LIKE ''%' + @search_string + '%'') PRINT '' update [' + @table_name + '] set [' + @column_name + '] = substring([' + @column_name + '], 1, charindex(''''<'''', [' + @column_name + '])-1) where [' + @column_name + '] like ''''%<%'''''''
            --PRINT @sql_string
            EXECUTE(@sql_string)
    
            FETCH NEXT FROM columns_cur INTO @column_name
        END
    
        CLOSE columns_cur
    
        DEALLOCATE columns_cur
    
        FETCH NEXT FROM tables_cur INTO @table_name, @table_id
    END
    
    CLOSE tables_cur
    
    DEALLOCATE tables_cur

    و خروجی به این صورت حاصل شد :

     update [Roles] set [Description] = substring([Description], 1, charindex('<', [Description])-1) where [Description] like '%<%'
     update [Articles] set [Subject] = substring([Subject], 1, charindex('<', [Subject])-1) where [Subject] like '%<%'

    و هر دو دستور رو اجرا کردم و داده‌ها ترمیم پیدا کردن.

    نکته مهم : تمامی موارد گفته شده جنبه تستی داره چون  آلوده شدن و ترمیم شدن به صورت دستی رخ داده و روی هیچ دیتابیس واقعی آلوده شده تست نشده.

    مطالب
    OpenCVSharp #9
    تغییر اندازه، و چرخش تصاویر

    در OpenCV با استفاده از مفهومی به نام affine transform، امکان تغییر اندازه و همچنین چرخش تصاویر میسر می‌شود. در اینجا، تصویر در یک ماتریس دو در سه ضرب می‌شود تا انتقالات یاد شده، انجام شوند.
    private static void rotateImage(double angle, double scale, Mat src, Mat dst)
    {
        var imageCenter = new Point2f(src.Cols / 2f, src.Rows / 2f);
        var rotationMat = Cv2.GetRotationMatrix2D(imageCenter, angle, scale);
        Cv2.WarpAffine(src, dst, rotationMat, src.Size());
    }
    متد فوق کار چرخش تصویر مبدا (src) را به تصویر مقصد (dst) انجام می‌دهد. این عملیات توسط متد WarpAffine مدیریت شده و مهم‌ترین پارامتر آن، پارامتر سوم آن است که ماتریس تعریف کننده‌ی انتقالات تعریف شده توسط متد GetRotationMatrix2D است. در اینجا مرکز مشخص شده، زاویه و مقیاس، نحوه‌ی چرخش را تعریف می‌کنند.
    برای مشاهده‌ی بهتر تاثیر پارامترهای مختلف در اینجا، به مثال ذیل دقت کنید:
    using OpenCvSharp;
    using OpenCvSharp.CPlusPlus;
     
    namespace OpenCVSharpSample09
    {
        class Program
        {
            static void Main(string[] args)
            {
                using (var src = new Mat(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor))
                using (var dst = new Mat())
                {
                    src.CopyTo(dst);
     
                    using (var window = new Window("Resize/Rotate/Blur",
                                                    image: dst, flags: WindowMode.AutoSize))
                    {
                        var angle = 0.0;
                        var scale = 0.7;
     
                        var angleTrackbar = window.CreateTrackbar(
                            name: "Angle", value: 0, max: 180,
                            callback: pos =>
                            {
                                angle = pos;
                                rotateImage(angle, scale, src, dst);
                                window.Image = dst;
                            });
     
                        var scaleTrackbar = window.CreateTrackbar(
                            name: "Scale", value: 1, max: 10,
                            callback: pos =>
                            {
                                scale = pos / 10f;
                                rotateImage(angle, scale, src, dst);
                                window.Image = dst;
                            }); 
     
                        angleTrackbar.Callback.DynamicInvoke(0);
                        scaleTrackbar.Callback.DynamicInvoke(1);
     
                        Cv2.WaitKey();
                    }
                }
            }
     
            private static void rotateImage(double angle, double scale, Mat src, Mat dst)
            {
                var imageCenter = new Point2f(src.Cols / 2f, src.Rows / 2f);
                var rotationMat = Cv2.GetRotationMatrix2D(imageCenter, angle, scale);
                Cv2.WarpAffine(src, dst, rotationMat, src.Size());
            }
        }
    }
    با این خروجی:


    در این مثال، مانند مطلب قسمت قبل، ابتدا یک پنجره‌ی سازگار با C++ API ایجاد شده و سپس دو tracker به آن اضافه شده‌اند. این trackers کار دریافت ورودی اطلاعات را از کاربر به عهده دارند (دریافت مقادیر زاویه‌ی چرخش و مقیاس) و مقادیر دریافتی از آن‌ها، در نهایت به متد rotateImage ارسال می‌شوند. این متد کار چرخش و تغییر مقیاس تصویر اصلی را انجام داده و نتیجه را به تصویر dst کپی می‌کند. در آخر تصویر dst در پنجره به روز شده و نمایش داده می‌شود.


    تغییر اندازه‌ی تصاویر

    اگر صرفا قصد تغییر اندازه‌ی تصاویر را دارید (بدون چرخش آن‌ها)، متد ویژه‌ای به نام Resize برای این منظور تدارک دیده شده‌است:
    var resizeTrackbar = window.CreateTrackbar(
        name: "Resize", value: 1, max: 100,
        callback: pos =>
        {
            Cv2.Resize(src, dst,
                new Size(src.Width + pos, src.Height + pos),
                interpolation: Interpolation.Cubic);
            window.Image = dst;
        });
    در اینجا یک tracker دیگر به پنجره‌ی اصلی اضافه شده و توسط آن کار تعیین تغییر اندازه‌ی تصویر انجام می‌شود. نکته‌ی مهم این متد، امکان تعیین الگوریتم تغییر اندازه است که برای مثال در اینجا از Interpolation.Cubic استفاده شده‌است (احتمالا با این نام‌ها در برنامه‌های معروف کار با تصاویر، مانند فتوشاپ آشنایی دارید).

    اگر می‌خواهید مقادیر پارامترهای چرخشی تصویر نیز در اینجا اعمال شوند، می‌توان به نحو ذیل عمل کرد:
    var resizeTrackbar = window.CreateTrackbar(
        name: "Resize", value: 1, max: 100,
        callback: pos =>
        {
            rotateImage(angle, scale, src, dst);
            Cv2.Resize(dst, dst,
                new Size(src.Width + pos, src.Height + pos),
                interpolation: Interpolation.Cubic);
            window.Image = dst;
        });
    در این کد ابتدا تصویر اصلی چرخش یافته و سپس در متد Resize از این تصویر چرخش یافته، به عنوان src استفاده می‌شود (هر دو پارامتر متد Resize به dst تنظیم شده‌اند).



    مات کردن تصاویر

    در OpenCV با استفاده از متدهای GaussianBlur و یا medianBlur ، می‌توان تصاویر را  مات کرد که نمونه‌ای از آن‌را در ادامه ملاحظه می‌کنید:
    var blurTrackbar = window.CreateTrackbar(
       name: "Blur", value: 1, max: 100,
       callback: pos =>
       {
           if (pos % 2 == 0) pos++;
     
           rotateImage(angle, scale, src, dst);
           Cv2.GaussianBlur(dst, dst, new Size(pos, pos), sigmaX: 0);
           window.Image = dst;
       });
    در اینجا ابتدا تصویر اصلی به متد چرخش تصویر ارسال شده و نتیجه‌ی آن در متد GaussianBlur استفاده خواهد شد. اندازه‌ی مشخص شده‌ی در این متد باید توسط اعداد فرد تعیین گردد. پارامتر sigmaX به معنای standard deviation در جهت x است و اگر صفر تعیین شود، برای محاسبه‌ی آن از پارامتر اندازه‌ی تعیین شده کمک گرفته خواهد شد.



    کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
    مطالب
    آشنایی با WPF قسمت سوم: Layouts بخش دوم

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


    GridPanel

    پنل پیش فرضی است که موقع ایجاد یک پروژه‌ جدید WPF ایجاد می‌شود. چیدمان این نوع پنل به صورت سطر و ستون است و کارکرد آن بسیار مشابه جداول در HTML می‌باشد؛ با این تفاوت که در اینجا انعطاف پذیری بیشتری وجود دارد. هر سلول می‌تواند شامل چندین کنترل شود و یا هر کنترل می‌تواند چندین سلول را به خود احتصاص دهند و حتی می‌تواند روی کنترل‌های دیگر قرار بگیرند و همپوشانی کنترل‌ها را داشته باشیم.

    تگ Grid Panel شامل دو تگ برای تعریف سطرها و ستون‌ها می‌باشد با استفاده از تگ Row Definition و Column Definition به تعیین تعداد سطر و ستون‌ها و اندازه آن‌ها می‌پردازیم:
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="28" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="200" />
        </Grid.ColumnDefinitions>
    </Grid>
    گرید پنل بالا شامل 4 سطر و دو ستون است و تعیین اندازه آن‌ها توسط دو خاصیت Width و  Height مشخص شده است که نحوه مقداردهی آن‌ها به صورت زیر است:
    Fixed : یک مقدار ثابت، مثل سطر آخری که در کد بالا قرار می‌گیرد. این مقدار بر اساس یک واحد منطقی است و نه پیکسل که در این مقاله قبلا بررسی کرده‌ایم.
    Auto : به مقداری که احتیاج دارد فضایی را بخود اختصاص می‌دهد.
    * : هر آنچه از فضای موجود باقی مانده است را به خود اختصاص می‌دهد. علامت ستاره یک واحد نسبی است؛ به این صورت که می‌توانید مقدار فضا را به صورت زیر نیز بیان کنید.*3 و *2 به این معنی است که از پنج قسمت فضای باقیمانده سه قسمت و بعدی دو قسمت  را به خود اختصاص می‌دهد. عبارت * با *1 برابر است. عموما با این علامت فضا را به شکل درصد بیان می‌کنند:
     <ColumnDefinition Width="69*" />   <!-- Take 69% of remainder -->
        <ColumnDefinition Width="31*"/> <!-- Take 31% of remainder -->

    نحوه‌ی اضافه کردنالمان‌ها به گرید به صورت زیر پس از تعیین تعداد سطرها و ستون‌ها انجام می‌گیرد و جایگاه هر المان در ستون یا سطر مربوطه توسط یک attached Dependency Property به نام‌های Grid.Column یا Grid.Row صورت می‌گیرد. خصوصیات Horizontal alignment و vertical Alignment هم برای تعیین موقعیت قرار گیری اشیاء در سلول به کار می‌روند و فاصله‌ی آن‌ها (کنترل ها) از لبه‌های گرید با margin محاسبه می‌شود.
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="28" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="200" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
        <Label Grid.Row="1" Grid.Column="0" Content="E-Mail:"/>
        <Label Grid.Row="2" Grid.Column="0" Content="Comment:"/>
        <TextBox Grid.Column="1" Grid.Row="0" Margin="3" />
        <TextBox Grid.Column="1" Grid.Row="1" Margin="3" />
        <TextBox Grid.Column="1" Grid.Row="2" Margin="3" />
        <Button Grid.Column="1" Grid.Row="3" HorizontalAlignment="Right" 
                MinWidth="80" Margin="3" Content="Send"  />
    </Grid>

    تغییر اندازه در سمت کد هم می‌تواند توسط کدهای صورت گیرد.
    Auto sized GridLength.Auto
    Star sized new GridLength(1,GridUnitType.Star)
    Fixed size new GridLength(100,GridUnitType.Pixel)
    مثال:
    Grid grid = new Grid();
     
    ColumnDefinition col1 = new ColumnDefinition();
    col1.Width = GridLength.Auto;
    ColumnDefinition col2 = new ColumnDefinition();
    col2.Width = new GridLength(1,GridUnitType.Star);
     
    grid.ColumnDefinitions.Add(col1);
    grid.ColumnDefinitions.Add(col2);

    قابلیت تغییر اندازه‌ی سطر و ستون توسط کاربر
    یکی از تگ‌های ویژه داخل گری،د تگ Grid Splitter است. برای قرارگیری تگ splitter ابتدا باید یک سطر یا ستون بین سطر و ستون هایی که میخواهید از یکدیگر جدا شوند ایجاد کنید و اندازه‌ی آن را auto تعیین کنید و سپس مانند بقیه‌ی اشیا توسط Grid.Column یا Grid.Row مانند کد زیر تگ splitter را به آن اختصاص دهید.
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Label Content="Left" Grid.Column="0" />
        <GridSplitter HorizontalAlignment="Right" 
                      VerticalAlignment="Stretch" 
                      Grid.Column="1" ResizeBehavior="PreviousAndNext"
                      Width="5" Background="#FFBCBCBC"/>
        <Label Content="Right" Grid.Column="2" />
    </Grid>
    خاصیت ResizeBehavior مشخص می‌کند که ستون یا سطرهای کناری کدام باید تغییر اندازه داشته باشند.
     BasedOnAlignment   مقدار پیش فرض این گزینه است و مشخص می‌کند سطر یا ستونی طرفی باید تغییر اندازه دهد که در Alignment آن آمده است
     CurrentAndNext   ستون یا سطر جاری  به همراه ستون یا سطر بعدی
     PreviousAndCurrent   ستون یا سطر جاری  به همراه ستون یا سطر قبلی
     PreviousAndNext   سطر یا ستون قبلی و بعدی که بهترین گزینه برای انتخاب است.

    خاصیت ResizeDirection جهت تغییر اندازه را مشخص می‌کند که شامل سه مقدار Row,Column و Auto است که مقدار پیش فرض آن auto است و نیازی به ذکر آن نیست و خود سیستم میداند که باید تغییر اندازه در چه جهتی صورت بگیرد.


    ساخت Custom Layout یا یک پنل سفارشی (اختصاصی)
    در این دو قسمت، شما با پنل‌های متفاوتی آشنا شدید که قابلیت‌های مفیدی داشتند؛ ولی گاهی اوقات هیچ کدام از این‌ها به کار شما نمی‌آیند و دوست دارید پنلی داشته باشید که مطابق میل شما عمل کند. برای ساخت یک پنل سفارشی یک کلاس می‌سازیم که از کلاس Panel ارث بری می‌کند. در اینجا دو متد برای Override کردن وجود دارند:
    MeasureOverride : تعیین اندازه پنل بر اساس اندازه تعیین شده برای المان‌های فرزند و فضای موجود.
    ArrangeOverride: مرتب سازی المان‌ها در فضای موجود نهایی.

    کد نمونه:
    public class MySimplePanel : Panel
    {
        // Make the panel as big as the biggest element
        protected override Size MeasureOverride(Size availableSize)
        {
            Size maxSize = new Size();
     
            foreach( UIElement child in InternalChildern)
            {
                child.Measure( availableSize );
                maxSize.Height = Math.Max( child.DesiredSize.Height, maxSize.Height);
                maxSize.Width= Math.Max( child.DesiredSize.Width, maxSize.Width);
            }
        }
     
        // Arrange the child elements to their final position
        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach( UIElement child in InternalChildern)
            {
                child.Arrange( new Rect( finalSize ) );
            }
        }
    }
    لینک‌های زیر تعدادی از پنل‌های سفارشی پر طرفدار هستند که بر روی اینترنت به اشتراک گذاشته شده اند:
    TreeMapPanel
    Animating Tile Panel
    Radial Panel
    Element Flow Panel
    Ribbon Panel

    خواصی که باید در Layout‌ها با آنها بیشتر آشنا شویم:
    Horizontal & Vertical Alignment
    با دادن این خاصیت به کنترل‌های موجود، نحوه قرار گیری و موقعیت آن‌ها مشخص می‌گردد. جدول زیر بر ساس انواع موقعیت‌های مختلف تشکیل شده است:

    Margin & Padding
    این خاصیت‌ها حتما برای شما آشنا هستند. خاصیت margin فاصله کنترل از لبه‌های Layout است و خاصیت Padding فاصله محتویات کنترل از لبه‌های کنترل است.

    Clipping
    در صورتی که خاصیت ClipToBounds پنل برابر False باشند به این معناست که المان‌ها میتوانند از لبه‌های پنل خارج شوند، در صورتی که برابر True باشد مقدار خارج شده نمایش نمی‌یابد.

    Scrolling
    موقعیکه از پنلی استفاده می‌کنید که با تمام شدن ناحیه‌اش روبرو شده‌اید ولی کنترل‌های داخلش هنوز ادامه دارند، نیاز به یک اسکرول به شدت احساس می‌شود. در این حالت می‌توان از ScrollViewer استفاده کرد.
    <ScrollViewer>
        <StackPanel>
            <Button Content="First Item" />
            <Button Content="Second Item" />
            <Button Content="Third Item" />
        </StackPanel>
    </ScrollViewer>



    مطالب
    نمایش خطاهای اعتبارسنجی سمت سرور ASP.NET Core در برنامه‌های Angular
    در مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها» با نحوه‌ی تنظیمات اعتبارسنجی سمت کلاینت برنامه‌های Angular آشنا شدیم. اما اگر مدل سمت سرور ما یک چنین شکلی را داشته باشد که به همراه خطاهای اعتبارسنجی سفارشی نیز هست:
    using System;
    using System.ComponentModel.DataAnnotations;
    
    namespace AngularTemplateDrivenFormsLab.Models
    {
        public class Movie
        {
            public int Id { get; set; }
    
            [Required(ErrorMessage = "Movie Title is Required")]
            [MinLength(3, ErrorMessage = "Movie Title must be at least 3 characters")]
            public string Title { get; set; }
    
            [Required(ErrorMessage = "Movie Director is Required.")]
            public string Director { get; set; }
    
            [Range(0, 100, ErrorMessage = "Ticket price must be between 0 and 100.")]
            public decimal TicketPrice { get; set; }
    
            [Required(ErrorMessage = "Movie Release Date is required")]
            public DateTime ReleaseDate { get; set; }
        }
    }
    و همچنین کنترلر و اکشن متد دریافت کننده‌ی آن نیز به صورت ذیل تعریف شده باشد:
    using AngularTemplateDrivenFormsLab.Models;
    using Microsoft.AspNetCore.Mvc;
    
    namespace AngularTemplateDrivenFormsLab.Controllers
    {
        [Route("api/[controller]")]
        public class MoviesController : Controller
        {
            [HttpPost]
            public IActionResult Post([FromBody]Movie movie)
            {
                if (ModelState.IsValid)
                {
                    // TODO: save ...
                    return Ok(movie);
                }
    
                ModelState.AddModelError("", "This record already exists."); // a cross field validation
                return BadRequest(ModelState);
            }
        }
    }
    دو نوع خطای اعتبارسنجی سمت سرور را به سمت کلاینت ارسال خواهیم کرد:
    الف) خطاهای اعتبارسنجی در سطح فیلدها
    زمانیکه return BadRequest(ModelState) صورت می‌گیرد، محتویات شیء ModelState به همراه status code مساوی 400 به سمت کلاینت ارسال خواهد شد. در شیء ModelState یک دیکشنری که کلیدهای آن، نام خواص و مقادیر متناظر با آن‌ها، خطاهای اعتبارسنجی تنظیم شده‌ی در مدل است، قرار دارند.
    ب) خطاهای اعتبارسنجی عمومی
    در این بین می‌توان دیکشنری ModelState را توسط متد AddModelError نیز تغییر داد و برای مثال کلید آن‌را مساوی "" تعریف کرد. در این حالت یک چنین خطایی به کل فرم اشاره می‌کند و نه به یک خاصیت خاص.

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

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



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

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


    در این HttpErrorResponse دریافتی، دو خاصیت error که همان آرایه‌ی دیکشنری نام خواص و پیام‌های خطای مرتبط با هر کدام و status code دریافتی مهم هستند:
      errors: string[] = [];
    
      processModelStateErrors(form: NgForm, responseError: HttpErrorResponse) {
        if (responseError.status === 400) {
          const modelStateErrors = responseError.error;
          for (const fieldName in modelStateErrors) {
            if (modelStateErrors.hasOwnProperty(fieldName)) {
              const modelStateError = modelStateErrors[fieldName];
              const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
              if (control) {
                // integrate into Angular's validation
                control.setErrors({
                  modelStateError: { error: modelStateError }
                });
              } else {
                // for cross field validations -> show the validation error at the top of the screen
                this.errors.push(modelStateError);
              }
            }
          }
        } else {
          this.errors.push("something went wrong!");
        }
      }
    
      lowerCaseFirstLetter(data: string): string {
        return data.charAt(0).toLowerCase() + data.slice(1);
      }
    توضیحات:
    در اینجا از آرایه‌ی errors برای نمایش خطاهای عمومی در سطح فرم استفاده می‌کنیم. این خطاها در ModelState، دارای کلید مساوی "" هستند. به همین جهت حلقه‌ای را بر روی شیء responseError.error تشکیل می‌دهیم. به این ترتیب می‌توان به نام خواص و همچنین خطاهای متناظر با آن‌ها رسید.
     const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
    از نام خاصیت یا فیلد، جهت یافتن کنترل متناظر با آن، در فرم جاری استفاده می‌کنیم. ممکن است کنترل تعریف شده camel case و یا pascal case باشد. به همین جهت دو حالت بررسی را در اینجا مشاهده می‌کنید.
    در ادامه اگر control ایی یافت شد، توسط متد setErrors، کلید جدید modelStateError را که دارای خاصیت سفارشی error است، تنظیم می‌کنیم. با اینکار سبب خواهیم شد تا خطای اعتبارسنجی دریافتی از سمت سرور، با سیستم اعتبارسنجی Angular یکی شود. به این ترتیب می‌توان این خطا را دقیقا ذیل همین کنترل در فرم نمایش داد. اگر کنترلی یافت نشد (کلید آن "" بود و یا جزو نام کنترل‌های موجود در آرایه‌ی form.controls نبود)، این خطا را به آرایه‌ی errors اضافه می‌کنیم تا در بالاترین سطح فرم قابل نمایش شود.

    نحوه‌ی استفاده‌ی از متد processModelStateErrors فوق را در متد submitForm، در قسمت شکست عملیات ارسال اطلاعات، مشاهده می‌کنید:
      model = new Movie("", "", 0, "");
      successfulSave: boolean;
      errors: string[] = [];
    
      constructor(private movieService: MovieService) { }
    
      ngOnInit() {
      }
    
      submitForm(form: NgForm) {
        console.log(form);
    
        this.errors = [];
        this.movieService.postMovieForm(this.model).subscribe(
          (data: Movie) => {
            console.log("Saved data", data);
            this.successfulSave = true;
          },
          (responseError: HttpErrorResponse) => {
            this.successfulSave = false;
            console.log("Response Error", responseError);
            this.processModelStateErrors(form, responseError);
          });
      }


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

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



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

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


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


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