مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت هفتم - سفارشی سازی ظاهر مستندات API
امکان کنترل کامل و سفارشی سازی ظاهر نهایی Swagger-UI وجود دارد که جزئیات آن‌را در این قسمت بررسی خواهیم کرد.


بهبود ظاهر کامنت‌ها با بکارگیری Markdown

Markdown زبان سبکی است برای تعیین شیوه‌نامه‌ی نمایش متون ساده. اگر پیشتر با سیستم ارسال نظرات Github و یا Stackoverflow کار کرده باشید، قطعا با آن آشنایی دارید. توضیحات کامل و جزئیات آن‌را می‌توانید در آدرس markdownguide.org مطالعه کنید. خوشبختانه امکان استفاده‌ی از Markdown در OpenAPI spec نیز وجود دارد که سبب بهبود ظاهر مستندات نهایی حاصل از آن خواهد شد.
در قسمت سوم، سعی کردیم مثالی را توسط remarks، به قسمت Patch اضافه کنیم که ظاهر آن، آنچنان مطلوب به نظر نمی‌رسد و بهتر است آن‌را به صورت یک قطعه کد نمایش داد:


برای بهبود این ظاهر می‌توان از Markdown استفاده کرد. بنابراین ابتدا تمام backslash‌های اضافه شده را که جهت نمایش خطوط جدید اضافه شده بودند، حذف می‌کنیم. در Markdown خطوط جدید با درج حداقل 2 فاصله (space) و یک سطر جدید مشخص می‌شوند. همچنین استفاده‌ی از ** سبب ضخیم شدن نمایش عبارت‌ها می‌شود. برای اینکه قطعه کد نوشته شده را در Markdown به صورت کدی با پس زمینه‌ای مشخص نمایش دهیم، پیش از شروع هر سطر آن نیاز است یک tab و یا 4 فاصله (space) درج شوند:
/// <remarks>
/// Sample request (this request updates the author's **first name**)
///
///     PATCH /authors/id
///     [
///         {
///           "op": "replace",
///           "path": "/firstname",
///           "value": "new first name"
///           }
///     ]
/// </remarks>
پس از این تغییرات خواهیم داشت:

که نسبت به حالت قبلی بسیار بهتر به نظر می‌رسد.
در نگارش فعلی، استفاده‌ی از Markdown برای توضیحات remarks، پارامترها و response codes پشتیبانی می‌شود؛ اما نه برای قسمت summary که برای نگارش بعدی درنظر گرفته شده‌است. همچنین از قابلیت‌های پیشترفته‌ی Markdown هم استفاده نکنید (در کل نیاز به مقداری سعی و خطا دارد تا مشخص شود چه قابلیت‌هایی را پشتیبانی می‌کند).


سفارشی سازی مقدماتی UI به کمک تنظیمات API آن

جائیکه تنظیمات میان‌افزار Swashbuckle.AspNetCore در کلاس Starup تعریف می‌شوند، می‌توان تغییراتی را نیز در UI آن اعمال کرد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            app.UseSwaggerUI(setupAction =>
            {
                setupAction.SwaggerEndpoint(
                    url: "/swagger/LibraryOpenAPISpecification/swagger.json",
                    name: "Library API");
                setupAction.RoutePrefix = "";

                setupAction.DefaultModelExpandDepth(2);
                setupAction.DefaultModelRendering(Swashbuckle.AspNetCore.SwaggerUI.ModelRendering.Model);
                setupAction.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
                setupAction.EnableDeepLinking();
                setupAction.DisplayOperationId();
            });
    // ...
        }
    }
}
با این خروجی که به علت تنظیم DocExpansion آن به None، اینبار لیست قابلیت‌ها را به صورت باز شده نمایش نمی‌دهد:


همچنین چون DefaultModelRendering به Model تنظیم شده‌است، اینبار بجای مثال، مشخصات مدل را به صورت پیش‌فرض نمایش می‌دهد:


کار DisplayOperationId نمایش Id هر Operation است؛ مانند get_api_authors. در اینجا Operation همان مداخل API ما هستند و به عنوان هر قسمت، Tag گفته می‌شود؛ مانند Authors و یا Books:


با فعالسازی EnableDeepLinking، آدرس‌هایی مانند tagName/# و یا tagName/OperationId/# قابلیت مرور و بازشدن خودکار را پیدا می‌کنند (tagName همان نام کنترلر است و OperationId همان اکشن متدی که عمومی شده‌است). برای مثال اگر آدرس https://localhost:5001/index.html#/Books/get_api_authors__authorId__books را در یک برگه‌ی جدید مرورگر باز کنیم، بلافاصله گروه Books، باز شده و سپس به اکشن متد یا مدخلی که Id آن ذکر شده‌است، هدایت می‌شویم:



اعمال تغییرات پیشرفته در UI

Swagger-UI در اصل از یک سری فایل html، css، js و فونت تشکیل شده‌است که آن‌ها را می‌توانید در آدرس src/Swashbuckle.AspNetCore.SwaggerUI مشاهده کنید. برای مثال فایل index.html آن‌را در اینجا می‌توانید مشاهده کنید. اصل آن در div ای با id مساوی swagger-ui رخ می‌دهد و در این قسمت است که رابط کاربری آن به صورت پویا تولید شده و رندر خواهد شد. بررسی فایل‌های js و css آن در این مخزن کد مشکل است؛ چون نگارش فشرده شده‌ی آن هستند. به همین جهت می‌توان به مخزن کد اصلی Swagger-UI که نگارش جایگذاری شده‌ی آن (embedded) توسط Swashbuckle.AspNetCore ارائه می‌شود، رجوع کرد. برای مثال در پوشه‌ی src/styles آن، اصل فایل‌های css آن برای سفارشی سازی وجود دارند.

پس از اعمال تغییرات خود، می‌توانید css و یا js سفارشی خود را به نحو زیر به تنظیمات app.UseSwaggerUI سیستم معرفی کنید:
setupAction.InjectStylesheet("/Assets/custom-ui.css");
setupAction.InjectJavaScript("/Assets/custom-js.js");
باید دقت داشت که این فایل‌ها باید در پوشه‌ی wwwroot قرار گرفته و قابل دسترسی باشند.
برای اعمال تغییرات اساسی و تزریق صفحه‌ی index.html جدیدی، می‌توان به صورت زیر عمل کرد:
setupAction.IndexStream = () => 
    GetType().Assembly.GetManifestResourceStream(
  "OpenAPISwaggerDoc.Web.EmbeddedAssets.index.html");
نکته‌ی مهم: اینبار این فایل باید به صورت embedded ارائه شود. به همین جهت در مثال فوق، عبارت OpenAPISwaggerDoc.Web به فضای نام اصلی اسمبلی جاری اشاره می‌کند. سپس EmbeddedAssets، نام پوشه‌ای است که فایل index.html سفارشی سازی شده، در آن قرار خواهد گرفت. اکنون برای اینکه این فایل را به صورت EmbeddedResource معرفی کنیم، نیاز است فایل csproj را به نحو زیر ویرایش کرد:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <None Remove="EmbeddedAssets\index.html" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="EmbeddedAssets\index.html" />
  </ItemGroup>
</Project>

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-07.zip
مطالب
نحوه ایجاد یک گزارش فاکتور فروش توسط PdfReport
شکل زیر را که شبیه به یک فاکتور فروش است درنظر بگیرید:



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

در ادامه کدهای کامل این مثال را مشاهده می‌کنید. همچنین این کد و کلاس‌های وابسته به آن مانند User و TransparentTemplate به سورس‌های کتابخانه PdfReport نیز اضافه شده‌اند.
using System;
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfReportSamples.Models;
using PdfReportSamples.Templates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.Tax
{
    public class TaxPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.CustomTemplate(new TransparentTemplate());
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 7; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PreviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");

                events.MainTableAdded(args =>
                {
                    var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
                    var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);

                    var others = Math.Round(balance * 1.8 / 100);
                    var tax = Math.Round(balance * 2.2 / 100);
                    var total = balance + tax + others;

                    var taxTable = new PdfPTable(args.Table); // Create a clone of the MainTable's structure                   

                    taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "عوارض";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", others);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "جمع کل";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", total);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "قابل پرداخت";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = total.NumberToText(Language.Persian) + " ریال";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                            cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                        });

                    args.PdfDoc.Add(taxTable);
                });
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\TaxReportSample.pdf"));
        }
    }
}
توضیحات:
تنها تفاوت این مثال با مثال‌های قبلی، کدهای مرتبط با متد events.MainTableAdded می‌باشند.
توسط متد args.LastOverallAggregateValueOf، می‌توان به مقدار نهایی متد تجمعی تعریف شده برای یک ستون خاص دسترسی یافت:
var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);
سپس بر این اساس، امکان محاسبه مالیات و عوارض میسر می‌شود:
var others = Math.Round(balance * 1.8 / 100);
var tax = Math.Round(balance * 2.2 / 100);
var total = balance + tax + others;
در ادامه نیاز داریم تا یک جدول جدید را ذیل جدول اصلی ایجاد کنیم. نکته مهم این جدول جدید، هماهنگی عرض ستون‌های آن با ستون‌های جدول اصلی است. به همین منظور می‌توان از خاصیت args.Table جهت دسترسی به خواص جدول اصلی استفاده کرد و جدول جدیدی را ایجاد نمود:
var taxTable = new PdfPTable(args.Table);
از اینجا به بعد دیگر به عهده خودتان است. می‌توانید از دانش iTextSharp خود استفاده کرده و ردیف‌های این جدول جدید را پر کنید. یا اینکه می‌توانید از متد کمکی توکار AddSimpleRow به نحو زیر استفاده نمائید:
taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });
با توجه به اینکه قصد نداریم در سه ستون اول این جدول جدید، عنصری را نمایش دهیم، آن‌ها را با null مقدار دهی کرده و سپس ستون برچسب و ستون مقدار را اضافه می‌کنیم (آرگومان‌های این متد به صورت params تعریف شده‌اند. بنابراین هر تعداد ستون که نیاز باشد قابل تعریف است).
با مقدار دهی data، مقدار مورد نظر در آن سلول ثبت می‌گردد. با مقدار دهی خواص cellProperties، نوع قلم، جهت قرارگیری و سایر تنظیماتی را که ملاحظه می‌کنید، می‌توان اعمال کرد.
و در آخر لازم است که این جدول جدید را به شیء Document اضافه کنیم تا نمایش داده شود:
args.PdfDoc.Add(taxTable);

یک نکته:
متد NumberToText جزئی از کتابخانه PdfReport (تعریف شده در فضای نام PdfRpt.Core.Helper) است و برای نمایش رقم به حروف می‌تواند مورد استفاده قرار گیرد:
total.NumberToText(Language.Persian)

 
مطالب
React 16x - قسمت 33 - React Hooks - بخش 4 - useContext Hook
در سری بررسی اعتبارسنجی و احراز هویت کاربران در React، برای انتقال داده‌های کاربر وارد شده‌ی به سیستم، از روش انتقال props، از بالاترین کامپوننت موجود در component tree، به پایین‌ترین کامپوننت آن، به این نحو فرضی استفاده کردیم:
ابتدا شیء user، در بالاترین سطح، دریافت شده و به صفحه‌ای خاص از طریق ویژگی‌های props ارسال می‌شود:
<Page user={user}  />
سپس این کامپوننت Page، کامپوننت PageLayout را رندر می‌کند که آن نیز باید به اطلاعات کاربر دسترسی داشته باشد. بنابراین شیء user را مجددا به این کامپوننت از طریق props ارسال می‌کنیم:
<PageLayout user={user} />
بعد همین کامپوننت PageLayout، کامپوننت NavBar را رندر می‌کند که آن نیز باید بداند کاربر وارد شده‌ی به سیستم کیست؟ به همین جهت یکبار دیگر از طریق props، اطلاعات کاربر را به کامپوننت بعدی موجود در درخت کامپوننت‌ها انتقال می‌دهیم:
<NavigationBar user={user}  />
و همینطور الی آخر. به این روش props drilling گفته می‌شود و ... الگوی مذمومی است. در دنیای واقعی، اطلاعات کاربر و یا خصوصا تنظیمات برنامه مانند آدرس REST API endpoints استفاده شده‌ی در آن، باید بین بسیاری از کامپوننت‌ها به اشتراک گذاشته شود و عموما سطوح به اشتراک گذاری آن، بسیار عمیق‌تر است از سطوحی که در این مثال ساده عنوان شدند. از زمان ارائه‌ی React 16.3.0، راه حل بهتری برای مدیریت اینگونه مسایل با ارائه‌ی React Context ارائه شده‌است که آن‌را در ادامه در دو حالت کامپوننت‌های کلاسی و همچنین تابعی، بررسی خواهیم کرد.


ایجاد شیء Context در برنامه‌های React

React Context، راه حلی است جهت به اشتراک گذاری داده‌ها، در بین انواع و اقسام کامپوننت‌های یک برنامه، بدون اینکه نیازی باشد این اطلاعات را توسط props، از یک سطح، به سطحی دیگر، به صورت دستی انتقال داد. برای ایجاد یک نمونه‌ی از آن، ابتدا پوشه‌ی جدید src\contexts را افزوده و سپس فایل src\contexts\userContext.js را درون آن، با محتوای زیر ایجاد می‌کنیم:
import React from "react";

export const UserContext = React.createContext({ user: {} });

export const UserProvider = UserContext.Provider;
export const UserConsumer = UserContext.Consumer;
متد React.createContext، یک شیء Context را بازگشت می‌دهد. این شیء، دو کامپوننت مهم Provider و Consumer را به همراه دارد که امکان اشتراک به داده‌های مرتبط با آن‌را میسر می‌کنند. زمانیکه React کامپوننتی را رندر می‌کند که مشترک یک شیء Context است، این کامپوننت، امکان خواندن اطلاعات شیء Context را از نزدیک‌ترین کامپوننتی در درخت کامپوننت‌ها که یک Provider را برای آن ارائه داده‌است، خواهد داشت.


تامین یک شیء Context در برنامه، در یک کامپوننت کلاسی و یا تابعی

تا اینجا یک شیء Context را به همراه اجزای export شده‌ی Provider و Consumer آن ایجاد کردیم. اکنون نوبت به پیاده سازی قسمت Provider آن است:
import "../../App.css";

import React, { Component } from "react";

import { UserProvider } from "../../contexts/userContext";
import Main from "./Main";

class App extends Component {
  state = {
    user: { name: "User 1" }
  };

  componentDidMount() {
    // get user from the server or local storage and then set the currently logged in user to the this.state
  }

  render() {
    return (
      <>
        <h1>App Class</h1>
        <UserProvider value={this.state.user}>
          <Main />
        </UserProvider>
      </>
    );
  }
}

export default App;
در این کامپوننت کلاسی (و یا تابعی، نحوه‌ی تعریف UserProvider در هر دو یکی است)، خاصیت user، به state کامپوننت اضافه شده‌است. سپس برای مثال می‌توان این خاصیت را در رویداد componentDidMount از سرور و یا محل ذخیره سازی دیگری دریافت و آنگاه state را بر این اساس به روز رسانی کرد.
در ادامه قصد داریم اطلاعات این شیء user موجود در state را با تمام کامپوننت‌هایی که در درخت رندر کامپوننت جاری قرار می‌گیرند و با کامپوننت Main شروع می‌شوند، به اشتراک بگذاریم. این به اشتراک گذاری با import شیء UserProvider از ماژول contexts/userContext به نحوی که مشاهده می‌کنید، انجام می‌شود. شیء UserProvider، کار محصور سازی کامپوننت Main را انجام می‌دهد. سپس این Provider می‌تواند مقداری را توسط ویژگی value خود دریافت کند که برای مثال در اینجا شیء user است. اکنون این value تا n سطح بعدی که از کامپوننت Main مشتق می‌شوند نیز در دسترس خواهد بود.

یک نکته: متد React.createContext به همراه یک آرگومان defaultValue اختیاری است که در اختیار Consumerهای آن قرار داده می‌شود؛ اگر Provider متناظر با آن‌، در درخت کامپوننت‌های برنامه، یافت نشود. یعنی تعریف Provider الزامی نیست. اگر نیاز است مقدار ثابتی را بین چندین کامپوننت به اشتراک بگذارید، فقط کافی است آن‌ها را توسط React.createContext مقدار دهی اولیه کرده و ... استفاده کنید:
export const DefaultRouteContext = React.createContext({ path: '/welcome' });


خواندن شیء Context در کامپوننتی دیگر

اکنون که یک تامین کننده‌ی Context را ایجاد کردیم، برای خواندن اطلاعات آن در درخت کامپوننت‌های محصور شده‌ی توسط UserProvider، می‌توان به صورت زیر عمل کرد:
import React from "react";

import { UserConsumer } from "../../contexts/userContext";

export default function Main(props) {
  return (
    <>
      <UserConsumer>
        {value => <div>User name: {value.name}.</div>}
      </UserConsumer>
    </>
  );
}
ابتدا UserConsumer را از ماژول contexts/userContext دریافت می‌کنیم. سپس برای دسترسی به خاصیت name شیء ارائه شده‌ی توسط UserProvider، باید قسمتی از متد رندر کامپوننت را توسط شیء UserConsumer، محصور کرد و سپس value آن‌را به نحوی که مشاهده می‌کنید، خواند. Consumer، یک تابع را به عنوان فرزند دریافت می‌کند. این تابع مقدار شیء تامین شده‌ی توسط Context را دریافت کرده (همان value={this.state.user} نزدیک‌ترین کامپوننتی که به همراه یک Provider است) و سپس یک المان React را بازگشت می‌دهد که در این محل رندر خواهد شد.

خروجی برنامه پس از این تغییرات به صورت زیر است:



ساده سازی دسترسی به UserConsumer توسط useContext Hook

نحوه‌ی تعریف یک Provider و محصور سازی فرزندانی که باید از آن ارث‌بری کنند، در بین کامپوننت‌های کلاسی و تابعی، یکی است. اما در کامپوننت‌های تابعی حداقل می‌توان نحوه‌ی دسترسی به UserConsumer را به نحو زیر توسط useContext Hook ساده کرد:
import React, { useContext } from "react";

import { UserContext } from "../../contexts/userContext";

export default function Main() {
  const value = useContext(UserContext);
  return (
    <>
      <div>User name: {value.name}.</div>
    </>
  );
}
متد useContext ابتدا شیء UserContext مهیا شده‌ی توسط ماژول contexts/userContext را دریافت می‌کند. سپس خروجی آن، همان value تنظیم شده‌ی توسط نزدیک‌ترین Provider آن در component tree است. این روش، بار ذهنی کمتری را نسبت به حالت قبلی استفاده‌ی از UserConsumer و کار با یک تابع درون آن‌را به همراه دارد؛ ساده‌تر خوانده می‌شود، ساده‌تر استفاده می‌شود. فقط باید دقت داشت که این متد، کل شیء Context را دریافت می‌کند و نه فقط شیء UserConsumer آن‌را.

مزیت دیگر این روش، ساده سازی کار با چندین شیء Context است. برای مثال اگر دو شیء Context را تعریف کرده باشید، خواندن دو مقدار از آن‌ها، پیشتر چنین شکل تو در تویی را توسط دو Consumer پیدا می‌کرد:
function HeaderBar() {
  return (
    <CurrentUser.Consumer>
      {user =>
        <Notifications.Consumer>
          {notifications =>
            <header>
              Welcome back, {user.name}!
              You have {notifications.length} notifications.
            </header>
          }
      }
    </CurrentUser.Consumer>
  );
}
اما اکنون با استفاده از useContext، نوشتن و خواندن آن به سادگی چند سطر زیر است که بسیار منطقی‌تر و عادی‌تر به نظر می‌رسد:
function HeaderBar() {
  const user = useContext(CurrentUser);
  const notifications = useContext(Notifications);
return (
    <header>
      Welcome back, {user.name}!
      You have {notifications.length} notifications.
    </header>
  );
}


ارسال اطلاعات به کامپوننت Context Provider، از طریق کامپوننت‌های فرزند

تا اینجا با استفاده از React Context، اطلاعات یک Provider را با فرزندان آن به اشتراک گذاشتیم؛ عکس این عمل نیز میسر است. برای اینکار، همانند تمام کامپوننت‌های دیگری که برای ارسال اطلاعات به فراخوان خود از طریق رخ‌دادها عمل می‌کنند، می‌توان یک متد رویدادگردان را در کامپوننت والد، به استفاده کنند‌ه‌ی از Context ارسال کرد:
import "../../App.css";

import React, { Component } from "react";

import { UserProvider } from "../../contexts/userContext";
import Main from "./Main2";

class App extends Component {
  state = {
    user: { name: "User 1" }
  };

  componentDidMount() {
    // get user from the server or local storage and then set the currently logged in user to the this.state
  }

  logout = () => {
    console.log("logout");
    this.setState({ user: {} });
  };

  render() {
    const contextValue = {
      user: this.state.user,
      logoutUser: this.logout
    };
    return (
      <>
        <h1>App Class</h1>
        <UserProvider value={contextValue}>
          <Main />
        </UserProvider>
      </>
    );
  }
}

export default App;
در اینجا ابتدا به خاصیت logout، متدی را نسبت داده‌ایم که با فراخوانی آن، اطلاعات شیء user موجود در state کامپوننت جاری را پاک می‌کند. سپس این خاصیت را به صورت یک خاصیت جدید، به شیءای که به ویژگی value شیء UserProvider انتساب داده شده، اضافه می‌کنیم.
اکنون تمام استفاده کننده‌های از این UserProvider می‌توانند با فراخوانی متد منتسب به logout، سبب پاک شدن اطلاعات کاربر موجود در state کامپوننت App، به روز رسانی state و در نتیجه‌ی آن، رندر مجدد کامپوننت و ارائه‌ی یک UserProvider جدید، با اطلاعاتی جدید به فرزندان آن شوند:
import React, { useContext } from "react";

import { UserContext } from "../../contexts/userContext";

export default function Main() {
  const { user, logoutUser } = useContext(UserContext);
  return (
    <>
      <div>User name: {user.name}.</div>
      <button type="button" className="btn btn-primary" onClick={logoutUser}>
        Logout user
      </button>
    </>
  );
}
در این کامپوننت مصرف کننده‌ی Context، اینبار، مقدار دریافتی، یک شیء با چندین خاصیت است. بنابراین می‌توان با استفاده از Object Destructuring، خواص آن‌را استخراج و استفاده کرد. برای مثال با انتساب onClick={logoutUser} به دکمه‌ی خروج، این کامپوننت می‌تواند اطلاعات state و سپس Context ارائه شده‌ی در کامپوننت App را تغییر دهد.

روش انجام اینکار بدون استفاده از useContext را نیز در ادامه مشاهده می‌کنید که در ابتدا نیاز به تعریف تابعی را دارد که همان خواص استخراجی را دریافت می‌کند. سپس باید بر اساس آن‌ها، المان‌های مدنظر نمایش نام کاربر و دکمه‌ی خروج او را بازگشت داد:
import React from "react";

import { UserConsumer } from "../../contexts/userContext";

export default function Main(props) {
  return (
    <>
      <UserConsumer>
        {({ user, logoutUser }) => (
          <>
            <div>User name: {user.name}.</div>
            <button
              type="button"
              className="btn btn-primary"
              onClick={logoutUser}
            >
              Logout user
            </button>
          </>
        )}
      </UserConsumer>
    </>
  );
}


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-30-part-04.zip
نظرات مطالب
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();
  }
}
مطالب
کار با Razor در ASP.NET Core 2.0
پیش نویس: این مقاله ترجمه شده فصل 5 کتاب Pro Asp.Net Core MVC2 می‌باشد.


ایجاد یک پروژه با استفاده Razor

در ادامه با هم یک مثال را با استفاده از Razor ایجاد می‌کنیم. یک پروژه جدید را با قالب Empty و با نام Razor ایجاد می‌کنیم.

مراحل:

1- ابتدا در کلاس startup قابلیت MVC را فعال می‌کنیم؛ با قرار دادن کد زیر در متد ConfigureServices:
 services.AddMvc();
و بعد کد زیر را که مربوط به اجرای پروژه‌ی hello Word است ، از متد Configure حذف می‌کنیم:
app.Run(async (context) =>
{
   await context.Response.WriteAsync("Hello World!");
});
در نهایت محتویات  فایل StartUp به صورت زیر می‌باشد:

namespace Razor
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            //app.Run(async (context) =>
            //{
            //    await context.Response.WriteAsync("Hello World!");
            //});
        }
    }
}


ایجاد یک Model
 یک پوشه جدید را به نام Models ایجاد و بعد در این پوشه یک کلاس را به نام Product ایجاد می‌کنیم و کدهای زیر را در آن قرار میدهیم:
namespace Razor.Models
{
    public class Product
    {
        public int ProductID { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public string Category { set; get; }
    }
}

ایجاد Controller
تنظیمات پیشفرض را در فایل Startup انجام داده‌ایم. درخواست‌هایی را که توسط کاربر ارسال میشوند، به controller پیشفرضی که نامش در اینجا Home است، ارسال می‌کند. حالا ما یک پوشه جدید را به نام Controllers ایجاد می‌کنیم و در آن یک کنترلر جدید را به نام HomeController ایجاد می‌کنیم و کدهای زیر را در آن قرار میدهیم:
namespace Razor.Controllers
{
    public class HomeController : Controller
    {
        // GET: /<controller>/
        public ViewResult Index()
        {
            Product myProduct = new Product
            {
                ProductID = 1,
                Name = "Kayak",
                Description = "A boat for one person",
                Category = "Watersports",
                Price = 275M
            };
            return View(myProduct);
        }
    }
}
در این کلاس یک Action Method را به نام index ایجاد می‌کنیم. سپس در آن یک شیء را از مدل ایجاد و مقدار دهی و آن‌را به View ارسال می‌کنیم تا در زمان بارگذاری View از این شیء استفاده نماییم. نیاز نیست نام View را مشخص کنید. به صورت پیشفرض نام View با نام اکشن متد یکسان می‌باشد.

 
ایجاد View
 برای ایجاد یک View پیشفرض برای Action Method فوق در پوشه Views/Home یک MVC View Page (Razor View Page) را به نام Index.schtml ایجاد می‌کنیم.
- نکته1: پوشه View و داخل آن Home را ایجاد کنید.
- نکته2: معادل MVC View Page در نسخه جدید، Razor View می‌باشد. اگر در لیست این آیتم را انتخاب کنید، در توضیحات پنل سمت راست میتوانید این مطلب را مشاهده کنید.
- نکته3: دقت نمایید برای اینکه پروژه net Core2. باشد و تمام مشخصات موردنظر را داشته باشد، باید نگارش ویژوال استودیو VS 2017.15.6.6 و یا بیشتر باشد.
 
@model Razor.Models.Product
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width"/>
    <title>Index</title>
</head>
<body>
    Content will go here
</body>
</html>

تا اینجا ما یک پروژه ساده را ایجاد نموده‌ایم که قابلیت استفاده‌ی از Razor را هم دارد. در ادامه نحوه‌ی استفاده از امکانات Razor شرح داده میشوند.


استفاده از Model در یک View
برای استفاده از شیء مدل در View، باید در View به آن شیء و مشخصات آن دسترسی داشته باشیم که این دسترسی را Razor با استفاده از کاراکتر @ برای ما ایجاد می‌کند. برای اتصال به Model از عبارت model@ (حتما باید حروف کوچک باشد) استفاده می‌کنیم و برای دسترسی به مشخصات مدل از عبارت Model@ (حتما باید حرف اول آن بزرگ باشد) استفاده می‌کنیم. به کد زیر دقت کنید:

@model Razor.Models.Product
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width"/>
    <title>Index</title>
</head>
<body>
  @Model.Name
</body>
</html>
خط اولی که در View تعریف شده است، با استفاده از عبارت model@ مانند تعریف نوع مدل می‌باشد و کار اتصال مدل به View را انجام میدهد و همین خط باعث میشود زمانی که شما در تگ body عبارت Model@ وبعد دات (.) را میزنید، لیست خصوصیات آن مدل ظاهر میشوند. لیست شدن خصوصیات بعد از دات(.) یکی از کارهای پیشفرض ویژوال استودیو می‌باشد؛ برای اینکه از خطاهای احتمالی کاربر جلوگیری کند.

نتیجه خروجی بالا مانند زیر می‌باشد:

 



معرفی View Imports

زمانیکه بخواهیم به یک کلاس در View دسترسی داشته باشیم، باید فضای نام آن کلاس را مانند کد زیر در بالای View اضافه کنیم. حالا اگر بخواهیم به چند کلاس دسترسی داشته باشیم، باید این کار را به ازای هر کلاس در هر View انجام دهیم که سبب ایجاد کدهای اضافی در View‌ها میشود. برای بهبود این وضعیت می‌توانید یک کلاس View Import را در پوشه‌ی Views ایجاد کنید و تمام فضاهای نام را در آن قرار دهید. با اینکار تمام فضاهای نامی که در این کلاس View Import قرار گرفته‌اند، در تمام Viewهای موجود در پوشه Views قابل دسترسی خواهند بود.

در پوشه View راست کلیک کرده و گزینه Add و بعد New Item را انتخاب می‌کنیم و در کادر باز شده، آیتم MVC View Import Page (در نسخه جدید نام آن  Razor View Imports است) انتخاب می‌کنیم. ویژوال استودیو به صورت پیش فرض نام ViewImports.cshtml_ را برای آن قرار میدهد.


نکته: استاندارد نام گذاری این View این می‌باشد که ابتدای آن کاراکتر (_) حتما وجود داشته باشد.
 
در کلاس تعریف شده با استفاده از عبارت using@ فضای نام‌های خود را قرار میدهیم؛ مانند زیر:
 @using Razor.Models
در این کلاس شما فقط میتوانید فضاهای نام را مانند بالا قرار دهید. پس از آم قسمت فضاهای نام اضافی در Viewها قابل حذف میشوند و در این حالت فقط نام کلاس مدل را در بالای فرم قرار میدهیم مانند زیر:
@model Product
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width"/>
    <title>Index</title>
</head>
<body>
  @Model.Name
</body>
</html>


Layout ها

یکی دیگر از عبارت‌های مهم Razor که در فایل Index وجود دارد، عبارت زیر است:
@{
    Layout = null;
}
شما می‌توانید در بین {} کدهای سی شارپ را قرار دهید. حالا مقدار Layout را مساوی نال قرار داده‌ایم که بگوییم View مستقلی است و از قالب مشخصی استفاده نمی‌کند.

از Layout برای طراحی الگوی Viewها استفاده می‌کنیم. اگر بخواهیم برای View ها یک قالب طراحی کنیم و این الگو بین تمام یا چندتای از آن‌ها مشترک باشد، کدهای مربوط به الگو را با استفاده از Layout ایجاد می‌کنیم و از آن در View ها استفاده می‌کنیم. اینکار برای جلوگیری از درج کدهای تکراری قالب در برنامه انجام میشود. با اینکار اگر بخواهیم در الگو تغییری را انجام دهیم، این تغییر را در یک قسمت انجام میدهم و سپس به تمام Viewها اعمال میشود.
 
Layout
طرحبندی  Viewهای برنامه بطور معمول بین چند View مشترک است و طبق استاندارد ویژوال استودیو در پوشه‌ی Views/Shared قرار میگیرد. برای ایجاد Layout، روی پوشه Views/shared راست کلیک کرده و بعد گزینه Add وبعد NewItem و سپس گزینه MVC View Layout Page (نام آن در نسخه جدید Razor Layout است) را انتخاب می‌کنیم و ابتدای نام آن را به صورت پیشفرض کاراکتر (_) قرار میدهیم.
 


هنگام ایجاد این فایل توسط ویژوال استودیو، کدهای زیر به صورت پیش فرض در فایل ایجاد شده وجود دارند: 
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>
طرحبندی‌ها فرم خاصی از View هستند و دو عبارت @ در کدهای آن وجود دارد. در اینجا فراخوانی RenderBody@ سبب درج محتویات View مشخص شده توسط Action Method در این مکان می‌شود. عبارت دیگری که در اینجا وجود دارد، ViewBag است که برای مشخص کردن عنوان در اینجا استفاده شده‌است.
ViewBag ویژگی مفیدی است که اجازه می‌دهد تا مقادیر و داده‌ها در برنامه گردش داشته باشند و در این مورد بین یک View و Layout منتقل شوند. در ادامه خواهید دید وقتی Layout را به یک نمایه اعمال می‌کنیم، این مورد چگونه کار می‌کند.

عناصر HTML در یک Layout به هر View که از آن استفاده می‌کند، اعمال و توسط آن یک الگو برای تعریف محتوای معمولی ارائه می‌شود؛ مانند کدهای زیر. من برخی از نشانه گذاری‌های ساده را به Layout اضافه کردم تا اثر قالب آن آشکارتر شود:
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <style>
        #mainDiv {
            padding: 20px;
            border: solid medium black;
            font-size: 20pt
        }
    </style>
</head>
<body>
    <h1>Product Information</h1>
    <div id="mainDiv">
        @RenderBody()
    </div>
</body>
</html>
در اینجا یک عنصر عنوان و همچنین بعضی از CSS‌ها را به عنصر div که حاوی عبارت RenderBody@ است، اضافه کرده‌ام؛ فقط برای اینکه مشخص شود، چه محتوایی از طرحبندی سایت می‌آید و چه چیزی از View.
 

اعمال Layout

برای اعمال کردن Layout به یک View، نیاز است مشخصه Layout آن‌را مقدار دهی و سپس Htmlهای اضافی موجود در آن‌را مانند المنت‌های head و Body حذف کنید؛ همانند کدهای زیر:
@model Product
@{
    Layout = "_BasicLayout";
    ViewBag.Title = "Product";
}
در خاصیت Layout، مقدار را برابر نام فایل Layout، بدون پسوند cshtml آن قرار میدهیم. Razor در مسیر پوشه Views/shared و پوشه Views/Home فایل Layout را جستجو می‌کند.
در اینجا عبارت ViewBag.Title را نیز مقدار دهی می‌کنیم. زمانیکه فایل فراخوانی میشود، عنوان آن صفحه با این مقدار، جایگزین خواهد شد.
تغییرات این View بسیار چشمگیر است؛ حتی برای چنین برنامه ساده‌ای. طرحبندی شامل تمام ساختار مورد نیاز برای هر پاسخ HTML است که View را به صورت یک محتوای پویا ارائه می‌دهد و داده‌ها را به کاربر منتقل می‌کند. هنگامیکه MVC فایل Index.cshtmal را پردازش می‌کند، این طرحبندی برای ایجاد پاسخ HTML نهایی یکپارچه می‌شود؛ مانند عکس زیر:
 


 
View Start

بعضی موارد هنوز در برنامه وجود دارند که می‌توان کنترل بیشتری بر روی آن‌ها داشته باشید. مثلا اگر بخواهیم نام یک فایل layout را تغییر دهیم، مجبور هستیم تمام Viewهایی را که از آن Layout استفاده می‌کنند، پیدا کنید و نام Layout استفاده شده در آن‌ها را تغییر دهیم. اینکار احتمال خطای بالایی دارد و امکان دارد بعضی View ها از قلم بیفتند و برنامه دچار خطا شود. بنابراین با استفاده از View Start می‌توانیم این مشکل را برطرف کنیم. وقتی نام Layout تغییر کرد، تنها کافی است نام آن‌را در View Start تغییر دهیم. اکنون زمانیکه برنامه را اجرا می‌کنیم، MVC به دنبال فایل View Start می‌گردد و اگر اطلاعاتی داشته باشد، آن را اجرا می‌کند و الویت این فایل از تمام فایل‌های دیگر بیشتر است و ابتدا تمام آنها اجرا میشوند.

برای ایجاد یک فایل شروع مشاهده، روی پوشه‌ی Views کلیک راست کرده و گزینه add->New Items را انتخاب می‌کنیم و از پنجره باز شده گزینه ( Razor View Start ) Mvc View Start Page را انتخاب می‌کنیم؛ مانند تصویر زیر:


ویژوال استودیو به صورت پیش فرض نام ViewStart.cshtml_ را به عنوان نام آن قرار میدهد؛ شما گزینه‌ی Create را در این حالت انتخاب کنید. محتویات فایل ایجاد شده به صورت زیر می‌باشد:
@{
    Layout = "_Layout";
}
برای اعمال Layout جدید به تمام Viewها، مقدار Layout را معادل طرحبندی خود تغییر میدهیم؛ مانند کد زیر: 
@{
    Layout = "_BasicLayout";
}
از آنجا که فایل View Start دارای مقداری برای Layout می‌باشد، می‌توانیم عبارت‌های مربوطه را در Index.cshtml‌ها حذف کنیم:
@model Product
@{
    ViewBag.Title = "Product";
}
در اینجا لازم نیست مشخص کنیم که من می‌خواهم از فایل View Start استفاده کنم. MVC این فایل را پیدا خواهد کرد و از محتویات آن به طور خودکار استفاده می‌کند. البته باید دقت داشت که مقادیر تعریف شده‌ی در فایل View اولویت دارند و باعث میشوند با معادل‌های فایل View Start جایگزین شوند.

شما همچنین می‌توانید چندین فایل View Start را برای تنظیم مقادیر پیش فرض قسمت‌های مختلف برنامه، استفاده کنید. یک فایل Razor همواره توسط نزدیک‌ترین فایل View start، پردازش می‌شود. به این معنا که شما می‌توانید تنظیمات پیش فرض را با افزودن یک فایل View Start به پوشه Views / Home و یا Views / Shared لغو کنید.

نکته: درک تفاوت میان حذف محتویات فایل View Start یا مساوی Null قرار دادن آن مهم است. اگر View شما مستقل است و شما نمی‌خواهید از آن استفاده کنید، بنابراین مقدار Layout آن‌را صریحا برابر Null قرار دهید. اگر مقدار دهی صریح شما مشخصه Layout را نادیده بگیرید، Mvc فرض می‌کند که میخواهید layout را داشته باشید و مقدار آن را از فایل View Start تامین می‌کند.
 

استفاده از عبارت‌های شرطی در Razor
 
حالا که من اصول و مبانی View و Layout را به شما نشان دادم، قصد دارم به انواع مختلفی از اصطلاحات که Razor آن‌ها را پشتیبانی می‌کند و نحوه استفاده‌ی از آنها را برای ایجاد محتوای نمایشی، ارائه دهم. در یک برنامه MVC، بین نقش‌هایی که توسط View و Action متدها انجام می‌شود، جدایی روشنی وجود دارد. در اینجا قوانین ساده‌ای وجود دارند که در جدول زیر مشخص شده‌اند:

کامپوننت 
انجام میشود 
انجام نمیشود 
  Action Method    یک شیء ViewModel را به View ارسال می‌کند.
  یک فرمت داده را به View ارسال می‌کند.
  View    از شیء ViewModel برای ارائه محتوا به کاربر استفاده می‌کند.
  هر جنبه‌ای از شیء View Model مشخصات را تغییر می‌دهد.
 
برای به دست آوردن بهترین نتیجه از MVC، نیاز به تفکیک و جداسازی بین قسمت‌های مختلف برنامه را دارید. همانطور که می‌بینید، می‌توانید کاملا با Razor کار کنید و این نوع فایل‌ها شامل دستورالعمل‌های سی شارپ نیز هستند. اما شما نباید از Razor برای انجام منطق کسب و کار استفاده کنید و یا هر گونه اشیاء Domain Model خود را دستکاری کنید. کد زیر نشان میدهد که یک عبارت جدید به View اضافه میشود:
*@
@model Product
@{

    ViewBag.Title = "Product";
}
<p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p>
می‌توان برای خصوصیت price، در اکشن متد فرمتی را تعریف و بعد آن را به View ارسال کنیم. این روش کار می‌کند، اما استفاده از این رویکرد منافع الگوی MVC را تضعیف می‌کند و توانایی من برای پاسخ دادن به تغییرات در آینده را کاهش می‌دهد. باید به یاد داشته باشید که در ASP NET Core MVC، استفاده مناسب از الگوی MVC اجتناب ناپذیر است و شما باید از تاثیر تصمیمات طراحی و کدگذاری که انجام می‌دهید مطلع باشید.
 

پردازش داده‌ها در مقابل فرمت

تفاوت بین پردازش داده و قالب بندی داده مهم است.
- نمایش فرمت داده‌ها: به همین دلیل در آموزش قبل من یک نمونه از شیء کلاس Product را برای View ارسال کرده‌ام و نه فرمت خاص یک شیء را به صورت یک رشته نمایشی.
- پردازش داده: انتخاب اشیاء داده‌‌ای برای نمایش، مسئولیت کنترلر است و در این حالت مدلی را برای دریافت و تغییر داده مورد نیاز، فراخوانی می‌کند.
گاهی سخت است که متوجه شویم کدی جهت پردازش داده است و یا فرمت آن.


اضافه نمودن مقدار داده ای

ساده‌ترین کاری را که می‌توانید با یک عبارت Razor انجام دهید این است که یک مقدار داده را در نمایش دهید. رایج‌ترین کار برای انجام آن، استفاده از عبارت Model@ است. ویوو Index یک مثال از این مورد است؛ شبیه به این مورد:
 <p>Product Name: @Model.Name</p>
شما همچنین می‌توانید یک مقدار را با استفاده قابلیت ViewBag نیز به View ارسال نمایید که از این قابلیت در Layout برای تنظیم کردن محتوای عنوان استفاده کردیم. اما در حالت زیر یک مدل نوع دار را به سمت View ارسال کرده‌ایم:
using Microsoft.AspNetCore.Mvc;
using Razor.Models;


namespace Razor.Controllers
{
    public class HomeController : Controller
    {
        // GET: /<controller>/
        public ViewResult Index()
        {
            Product myProduct = new Product
            {
                ProductID = 1,
                Name = "Kayak",
                Description = "A boat for one person",
                Category = "Watersports",
                Price = 275M
            };
            return View(myProduct);
        }
    }
}

خصوصیت ViewBag یک شیء پویا را باز می‌گرداند که می‌تواند برای تعیین خواص دلخواهی مورد استفاده قرار گیرد. از آنجا که ویژگی ViewBag پویا است، لازم نیست که نام خصوصیات را پیش از آن اعلام کنم. اما این بدان معنا است که ویژوال استودیو قادر به ارائه پیشنهادهای تکمیل کننده برای ViewBag نیست.
در مثال زیر از یک مدل نوع دار و مزایای به همراه آن استفاده شده‌است: 
 <p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> <p>Stock Level: @ViewBag.StockLevel</p>
نتیجه آن‌را در زیر می‌توانید مشاهده کنید:



تنظیم مقادیر مشخص

شما همچنین می‌توانید از عبارات Razor برای تعیین مقدار عناصر، استفاده کنید:
@model Product
@{

    ViewBag.Title = "Product";
}
p>Product Name: @Model.Name</p> <p>Product Price: @($"{Model.Price:C2}")</p> 
<p>Stock Level: @ViewBag.StockLevel</p>
<div data-productid="@Model.ProductID" data-stocklevel="@ViewBag.StockLevel">    
<p>Product Name: @Model.Name</p>    
<p>Product Price: @($"{Model.Price:C2}")</p>   
 <p>Stock Level: @ViewBag.StockLevel</p> 
</div>
در اینجا از عبارات Razor، برای تعیین مقدار برای برخی از ویژگی‌های داده در عنصر div استفاده کرده‌ام.

نکته: ویژگی‌های داده‌ها که نام آنها *-data است، روشی برای ایجاد ویژگی‌های سفارشی برای سال‌ها بوده است و بعنوان بخشی از استاندارد HTML5 است. عموما کدهای جاوا اسکریپت از آن‌ها برای یافتن اطلاعات استفاده می‌کنند.

اگر برنامه را اجرا کنید و به منبع HTML که به مرورگر فرستاده شده نگاهی بیندازید، خواهید دید که Razor مقادیر صفات را تعیین کرده است؛ مانند این:
<div data-productid="1" data-stocklevel="2">    <p>Product Name: Kayak</p>    <p>Product Price: £275.00</p>    <p>Stock Level: 2</p> </div>


استفاده از عبارت‌های شرطی

Razor قادر به پردازش عبارات شرطی است. در ادامه کدهای Index View را که در آن دستورات شرطی اضافه شده‌اند می‌بینید:

@model Product
@{ ViewBag.Title = "Product Name"; }
<div data-productid="@Model.ProductID" data-stocklevel="@ViewBag.StockLevel">  
  <p>Product Name: @Model.Name</p>   
 <p>Product Price: @($"{Model.Price:C2}")</p> 
   <p>Stock Level:       
 @switch (ViewBag.StockLevel)
{
    case 0:@:Out of Stock                break;           
    case 1:          
    case 2:        
    case 3:            
    <b>Low Stock (@ViewBag.StockLevel)</b>         
       break;      
    default:            
    @: @ViewBag.StockLevel in Stock          
      break;      
  }    
</p>
</div>


برای شروع یک عبارت شرطی، یک علامت @ را در مقابل کلمه کلیدی if یا swicth سی شارپ قرار دهید. سپس بخش کد را داخل } قرار می‌دهیم. درون قطعه کد Razor، می‌توانید عناصر HTML و مقادیر داده را در خروجی نمایش دهید؛ مانند:
 <b>Low Stock (@ViewBag.StockLevel)</b>
در اینجا لازم نیست عناصر یا عبارات را در نقل قول قرار دهیم و یا آنها را به روش خاصی تعریف کنیم. موتور Razor این را به عنوان خروجی برای پردازش تفسیر خواهد کرد.
با این حال، اگر می‌خواهید متن واقعی را در نظر بگیرید و دستورات Razor را لغو کنید،‌می‌توانید از :@ استفاده کنید تا عین آن عبارت درج شود.
نظرات مطالب
AngularJS #4
مشکل از این بود که بنده Button را به صورت ذیل تعریف کرده بودم ..
<button ng-click="addComment()">Send</button>
در صورتی که میبایست به صورت ذیل تعریف میشد :
<button type="button" ng-click="addComment()">Send</button>


مطالب
مسیریابی در Angular - قسمت سوم - پارامترهای مسیریابی
گاهی از اوقات نیاز است به همراه مسیریابی، اطلاعاتی را نیز به آن‌ها ارسال کنیم. برای مثال در حین نمایش لیست محصولات، برای هدایت به صفحه‌ی نمایش جزئیات هر محصول، نیاز است Id هر محصول نیز به همراه مسیریابی، به کامپوننت مقصد ارسال شود. اینکار توسط route parameters قابل مدیریت است.

تنظیم مسیریابی‌ها جهت درج پارامترها

پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابی‌ها مشخص کرد.
در ادامه‌ی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه می‌کنیم:
>ng g c product/ProductDetail
>ng g c product/ProductEdit
این دستورات سبب به روز رسانی خودکار قسمت declarations فایل src\app\product\product.module.ts نیز خواهند شد.

در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آن‌را به شکل ذیل تکمیل خواهیم کرد:
import { ProductEditComponent } from './product-edit/product-edit.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
import { ProductListComponent } from './product-list/product-list.component';

const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  { path: 'products/:id', component: ProductDetailComponent },
  { path: 'products/:id/edit', component: ProductEditComponent }
];
اولین شیء مسیریابی تعریف شده را در قسمت‌های پیشین بررسی کردیم که امکان نمایش کامپوننت لیست محصولات را توسط یک routerLink در منوی سایت میسر می‌کند.
دومین شیء مسیریابی، از مسیر ریشه‌ی یکسانی استفاده می‌کند (products) که علت آن‌را در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص می‌کند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری می‌خواهیم id محصولات را انتقال دهیم، Id نام‌گرفته‌است؛ وگرنه هر نام دیگری نیز می‌تواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشه‌ی یکسانی استفاده می‌کند و تفاوت آن‌را با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کرده‌ایم.

در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نام‌های پس از : در یک مسیریابی، باید منحصربفرد باشند.


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

پیش از ادامه‌ی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمت‌های قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینک‌هایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه می‌کنیم:
 > ng g i product/IProduct
و آن‌را به نحو ذیل تکمیل خواهیم کرد:
export interface IProduct {
    id: number;
    productName: string;
    productCode: string;
}


تشکیل یک منبع اطلاعات درون حافظه‌ای

یکی از بسته‌های Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائه‌ی مطلب).
به همین جهت ابتدا بسته‌ی مرتبط با آن‌را نصب کنید:
 >npm install angular-in-memory-web-api --save
ذکر پارامتر save در اینجا، سبب به روز رسانی فایل package.json نیز خواهد شد:
"dependencies": {
   "angular-in-memory-web-api": "^0.3.1"
},

سپس کلاس ProductData را به ماژول محصولات اضافه می‌کنیم:
 > ng g cl product/ProductData
این کلاس را در ادامه به صورت ذیل تکمیل خواهیم کرد:
import { IProduct } from './iproduct';
import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api';

export class ProductData implements InMemoryDbService, InMemoryBackendConfig {
    createDb() {
        let products: IProduct[] = [
            {
                'id': 1,
                'productName': 'Product 1',
                'productCode': '0011'
            },
            {
                'id': 2,
                'productName': 'Product 2',
                'productCode': '0023'
            },
            {
                'id': 5,
                'productName': 'Product 5',
                'productCode': '0048'
            },
            {
                'id': 8,
                'productName': 'Product 8',
                'productCode': '0022'
            },
            {
                'id': 10,
                'productName': 'Product 10',
                'productCode': '0042'
            }
        ];
        return { products };
    }
}
همانطور که ملاحظه می‌کنید، کلاسی که قرار است  به عنوان منبع داده‌ی بسته‌ی angular-in-memory-web-api بکار رود باید InMemoryDbService, InMemoryBackendConfig را نیز پیاده سازی کند که نمونه‌ای از آن‌را در اینجا برای بازگشت یک لیست درون حافظه‌ای محصولات، مشاهده می‌کنید.

در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
import { ProductData } from './product/product-data';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),

    ProductModule,
    UserModule,

    AppRoutingModule
  ],
- ابتدا مکان یافت شدن ماژول‌های مورد نیاز ProductData و InMemoryWebApiModule تعریف شده‌اند و سپس InMemoryWebApiModule.forRoot را جهت تشکیل یک Web API آزمایشی، برای ارائه‌ی اطلاعات کلاس ProductData، به لیست imports اضافه کرده‌ایم. باید دقت داشت که نیاز است تعریف InMemoryWebApiModule پس از تعریف HttpModule باشد تا بتواند تعدادی از پیش فرض‌های آن را بازنویسی کند.
- در اینجا یک delay را هم در تنظیمات آن مشاهده می‌کنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.


تعریف سرویس HTTP کار با منبع اطلاعات درون حافظه‌ای

پس از تعریف REST Web API درون حافظه‌ای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
 >ng g s product/product -m product/product.module
که سبب افزوده شدن سرویس product.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول product.module.ts نیز می‌شود:
 installing service
  create src\app\product\product.service.spec.ts
  create src\app\product\product.service.ts
  update src\app\product\product.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمی‌شود.

پس از ایجاد قالب ابتدایی فایل product.service.ts، آن‌را به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core';
import { Http, Response, Headers, RequestOptions } from '@angular/http';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/of';

import { IProduct } from './iproduct';

@Injectable()
export class ProductService {
  private baseUrl = 'api/products';

  constructor(private http: Http) { }

  getProducts(): Observable<IProduct[]> {
    return this.http.get(this.baseUrl)
      .map(this.extractData)
      .do(data => console.log('getProducts: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  getProduct(id: number): Observable<IProduct> {
    if (id === 0) {
      return Observable.of(this.initializeProduct());
    };
    const url = `${this.baseUrl}/${id}`;
    return this.http.get(url)
      .map(this.extractData)
      .do(data => console.log('getProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  deleteProduct(id: number): Observable<Response> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    const url = `${this.baseUrl}/${id}`;
    return this.http.delete(url, options)
      .do(data => console.log('deleteProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  saveProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    if (product.id === 0) {
      return this.createProduct(product, options);
    }
    return this.updateProduct(product, options);
  }

  private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> {
    product.id = undefined;
    return this.http.post(this.baseUrl, product, options)
      .map(this.extractData)
      .do(data => console.log('createProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> {
    const url = `${this.baseUrl}/${product.id}`;
    return this.http.put(url, product, options)
      .map(() => product)
      .do(data => console.log('updateProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  private extractData(response: Response) {
    let body = response.json();
    return body.data || {};
  }

  private handleError(error: Response): Observable<any> {
    console.error(error);
    return Observable.throw(error.json().error || 'Server error');
  }

  initializeProduct(): IProduct {
    // Return an initialized object
    return {
      id: 0,
      productName: null,
      productCode: null
    };
  }
}
این سرویس HTTP، به سرویس Web API آزمایشی واقع در آدرس  baseUrl، متصل خواهد شد:
   private baseUrl = 'api/products';
از آن برای دریافت لیست محصولات (getProducts)، دریافت جزئیات یک محصول (getProduct)، حذف یک محصول (deleteProduct)، ثبت و یا به روز رسانی یک محصول (saveProduct) استفاده خواهیم کرد.


نمایش لیست محصولات

اکنون پس از این مقدمات می‌توانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آن‌را به نحو ذیل تکمیل کنید:
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit } from '@angular/core';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  pageTitle = 'Product List';
  errorMessage: string;

  products: IProduct[];

  constructor(private productService: ProductService,
    private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.productService.getProducts()
      .subscribe(products => this.products = products,
      error => this.errorMessage = <any>error);
  }
}
در اینجا با استفاده از سرویس محصولاتی که پیشتر ایجاد کردیم، کار دریافت لیست محصولات انجام شده و سپس به خاصیت عمومی products نسبت داده می‌شود. این خاصیت را در قالب این کامپوننت نمایش خواهیم داد. به همین جهت فایل src\app\product\product-list\product-list.component.html را گشوده و آن‌را به نحو ذیل تکمیل کنید:
<div class="panel panel-default">
  <div class="panel-heading">
    {{pageTitle}}
  </div>

  <div class="panel-body">
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>

    <div class="table-responsive">
      <table class="table" *ngIf="products && products.length">
        <thead>
          <tr>
            <th>Product</th>
            <th>Code</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let product of products">
            <td><a [routerLink]="['/products', product.id]">
                  {{product.productName}}
                </a>
            </td>
            <td>{{ product.productCode}}</td>
            <td>
              <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']">
                Edit
              </a>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>
اکنون اگر برنامه را توسط دستور ng serve -o ساخته و اجرا کنید، چنین صفحه‌ای قابل مشاهده خواهد بود:


توضیحات:

پس از تعریف مسیریابی‌های صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آن‌ها به لینک‌هایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحه‌ی مشاهده‌ی جزئیات آن متصل کرده‌ایم:
<a [routerLink]="['/products', product.id]">
                  {{product.productName}}
</a>
برای مشاهده‌ی جزئیات هر محصول نیاز است Id آن محصول را به عنوان پارامتر مسیریابی ارسال کنیم. به همین جهت این Id را به عنوان پارامتری جدید، به routerLink انتساب داده‌ایم.
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کرده‌ایم.
<a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']">
     Edit
</a>
در اینجا ابتدا root URL segment ذکر می‌شود. سپس پارامترهای متغیر مسیریابی و همچنین ثوابت آن مسیر خاص نیز باید ذکر شوند. اگر URL segment ثابت edit‌، در انتها ذکر نشود، این مسیر با تنظیم 'path: 'products/:id تطابق داده خواهد شد و نه با حالت 'path: 'products/:id/edit.

به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه می‌کنیم:
<li>
      <a [routerLink]="['/products', 0, 'edit']">Add Product</a>
</li>
در اینجا عدد صفر را به عنوان پارامتر یا Id محصول جدید، به همان صفحه‌ی ویرایش اطلاعات یک محصول، ارسال کرده‌ایم. اگر به سرویس محصولات دقت کنید،
  saveProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    if (product.id === 0) {
      return this.createProduct(product, options);
    }
    return this.updateProduct(product, options);
  }
اگر id مساوی صفر باشد، یک محصول جدید ایجاد خواهد شد و اگر غیر صفر باشد، این محصول از پیش موجود، به روز رسانی می‌گردد.

همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز می‌توان بکار برد. برای مثال:
 this.router.navigate(['products', this.product.id]);


خواندن و پردازش پارامترهای مسیریابی

تا اینجا لیست محصولات را نمایش دادیم و همچنین لینک‌هایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحله‌ی بعد، پیاده سازی این کامپوننت‌ها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننت‌ها قرار می‌دهد. بنابراین برای دسترسی به آن‌ها تنها کافی است این سرویس را به سازنده‌ی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم می‌شود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id می‌توان به صورت ذیل عمل کرد:
 let id = +this.route.snapshot.params['id'];

بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف می‌شود:
 { path: 'products/:id', component: ProductDetailComponent }
سپس این مسیریابی توسط لینک ذیل فعال می‌شود:
<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
اکنون برای دریافت مقدار این پارامتر از URL جاری، می‌توان از this.route.snapshot.params['id'] استفاده کرد. این id دقیقا نام همان متغیری است که در تعریف مسیریابی ذکر شده‌است و حساس به کوچکی و بزرگی حروف می‌باشد.

ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز می‌باشد:
this.route.params.subscribe(
         params => {
            let id = +params['id'];
            this.getProduct(id);
         }
      );
هر زمان که پارامترهای مسیریابی تغییر کنند، این Observable به آن‌ها گوش فرا داده و برنامه را از این تغییرات مطلع می‌سازد.

یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشته‌ی دریافتی، به عدد می‌گردد.


تکمیل کامپوننت نمایش جزئیات یک محصول

در ادامه قالب ابتدایی مشاهده‌ی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.component.html',
  styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
  pageTitle = 'Product Detail';
  product: IProduct;
  errorMessage: string;

  constructor(private productService: ProductService,
    private route: ActivatedRoute) { }

  ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }

  getProduct(id: number) {
    this.productService.getProduct(id).subscribe(
      product => this.product = product,
      error => this.errorMessage = <any>error);
  }
}
در این حالت اگر آدرس http://localhost:4200/products/1 توسط کاربر درخواست شود، نیاز است بتوان id=1 آن‌را از مسیرجاری استخراج کرد. به همین جهت سرویس ActivatedRoute به سازنده‌ی کلاس کامپوننت جزئیات محصول تزریق شده‌است. هرچند می‌توان از این سرویس در همان سازنده‌ی کلاس نیز استفاده کرد، اما انجام اعمال async آغازین یک کامپوننت بهتر است به ngOnInit منتقل شوند تا سبب تاخیری در آغاز و نمایش کامپوننت نگردند. این life cycle hook، پس از آغاز کامپوننت فراخوانی می‌گردد. به همین جهت ذکر implements OnInit را در قسمت تعریف کلاس مشاهده می‌کنید.
در متد OnInit، مقدار id، از snapshot دریافت می‌گردد. سپس این id به متد getProduct ارسال می‌شود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.


برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
<div class="panel panel-primary" *ngIf="product">
  <div class="panel-heading" style="font-size:large">
    {{pageTitle + ": " + product.productName}}
  </div>

  <div class="panel-body">
    <div>
      Name: {{product.productName}}
    </div>
    <div>
      Code: {{product.productCode}}
    </div>
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
  </div>

  <div class="panel-footer">
    <a class="btn btn-default" [routerLink]="['/products']">
      <i class="glyphicon glyphicon-chevron-left"></i> Back
    </a>
    <a class="btn btn-primary" style="width:80px;margin-left:10px" 
       [routerLink]="['/products', product.id, 'edit']">
       Edit
    </a>
  </div>
</div>
در اینجا علاوه بر استفاده از شیء product در جهت نمایش جزئیات محصول انتخابی، دو دکمه‌ی back و edit نیز اضافه شده‌اند که اولی صفحه‌ی لیست محصولات را مجددا نمایش می‌دهد و دومی کار هدایت به صفحه‌ی ویرایش جزئیات این محصول را میسر می‌کند.


تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول

از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ProductRoutingModule
  ]
کد کامل کامپوننت ویرایش و افزودن جزئیات یک محصول به شرح ذیل است (فایل src\app\product\product-edit\product-edit.component.ts):
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-edit',
  templateUrl: './product-edit.component.html',
  styleUrls: ['./product-edit.component.css']
})
export class ProductEditComponent implements OnInit {
  pageTitle = 'Product Edit';
  product: IProduct;
  errorMessage: string;

  constructor(private productService: ProductService,
    private route: ActivatedRoute,
    private router: Router) { }

  ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }

  getProduct(id: number): void {
    this.productService.getProduct(id)
      .subscribe(
      (product: IProduct) => this.onProductRetrieved(product),
      (error: any) => this.errorMessage = <any>error
      );
  }

  onProductRetrieved(product: IProduct): void {
    this.product = product;

    if (this.product.id === 0) {
      this.pageTitle = 'Add Product';
    } else {
      this.pageTitle = `Edit Product: ${this.product.productName}`;
    }
  }

  deleteProduct(): void {
    if (this.product.id === 0) {
      // Don't delete, it was never saved.
      this.onSaveComplete();
    } else {
      if (confirm(`Really delete the product: ${this.product.productName}?`)) {
        this.productService.deleteProduct(this.product.id)
          .subscribe(
          () => this.onSaveComplete(`${this.product.productName} was deleted`),
          (error: any) => this.errorMessage = <any>error
          );
      }
    }
  }

  saveProduct(): void {
    if (true === true) {
      this.productService.saveProduct(this.product)
        .subscribe(
        () => this.onSaveComplete(`${this.product.productName} was saved`),
        (error: any) => this.errorMessage = <any>error
        );
    } else {
      this.errorMessage = 'Please correct the validation errors.';
    }
  }

  onSaveComplete(message?: string): void {
    if (message) {
      // TODO: show msg
    }

    // Navigate back to the product list
    this.router.navigate(['/products']);
  }
}
به همراه کد کامل قالب آن (فایل src\app\product\product-edit\product-edit.component.html):
<div class="panel panel-primary">
  <div class="panel-heading">
    {{pageTitle}}
  </div>

  <div class="panel-body" *ngIf="product">
    <form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm">
      <fieldset>
        <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || 
                                               productNameVar.dirty) && 
                                               !productNameVar.valid }">
          <label class="col-md-2 control-label" for="productNameId">Product Name</label>

          <div class="col-md-8">
            <input class="form-control" id="productNameId" type="text" placeholder="Name (required)" 
                   required minlength="3" [(ngModel)]=product.productName 
                   name="productName" #productNameVar="ngModel" />
            <span class="help-block" *ngIf="(productNameVar.touched ||
                                                         productNameVar.dirty) &&
                                                         productNameVar.errors">
                <span *ngIf="productNameVar.errors.required">
                    Product name is required.
                </span>
                <span *ngIf="productNameVar.errors.minlength">
                    Product name must be at least three characters.
                </span>
            </span>
          </div>
        </div>

        <div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || 
                                               productCodeVar.dirty) && 
                                               !productCodeVar.valid }">
          <label class="col-md-2 control-label" for="productCodeId">Product Code</label>

          <div class="col-md-8">
            <input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" 
                   required [(ngModel)]=product.productCode
                   name="productCode" #productCodeVar="ngModel" />
            <span class="help-block" *ngIf="(productCodeVar.touched ||
                                                         productCodeVar.dirty) &&
                                                         productCodeVar.errors">
                <span *ngIf="productCodeVar.errors.required">
                     Product code is required.
                </span>
            </span>
          </div>
        </div>

        <div class="form-group">
          <div class="col-md-4 col-md-offset-2">
            <span>
                 <button class="btn btn-primary"
                         type="submit"
                         style="width:80px;margin-right:10px"
                         [disabled]="!productForm.valid"
                         (click)="saveProduct()">
                         Save
                 </button>
             </span>
             <span>
                 <a class="btn btn-default"
                    [routerLink]="['/products']">
                       Cancel
                 </a>
             </span>
             <span>
                 <a class="btn btn-default"
                    (click)="deleteProduct()">
                     Delete
                  </a>
             </span>
          </div>
        </div>
      </fieldset>
    </form>
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
  </div>
</div>

توضیحات:

نکته‌ی مهمی را که در این کدها می‌خواهیم بررسی کنیم، متد ngOnInit آن‌است:
ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }
برنامه را یکبار توسط دستور ng server -o ساخته و اجرا کنید.
 - ابتدا لیست محصولات را مشاهده کنید.
 - سپس بر روی دکمه‌ی edit محصول شماره یک کلیک نمائید:


تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شده‌است. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیده‌است.
 - در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:


همانطور که مشاهده می‌کنید، هرچند URL صفحه تغییر کرده‌است، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشده‌اند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کرده‌ایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمی‌شود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر  root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمی‌شود. به همین جهت است که فیلدهای این فرم تغییر نکرده‌اند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آن‌ها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe می‌توان انجام داد:
  ngOnInit(): void {
    this.route.params.subscribe(
      params => {
        let id = +params['id'];
        this.getProduct(id);
      }
    );
  }
در اینجا چون کامپوننت به علت نحوه‌ی تعریف مسیریابی آن مجددا آغاز نمی‌شود، شیء route.snapshot برای دسترسی به پارامترهای تغییر کرده‌ی مسیریابی، کارآیی لازم را نداشته و باید از روش دوم دسترسی به آن مقادیر که یک Observable است و به تغییرات پارامترها گوش فرا می‌دهد، استفاده کرد.

یک نکته: هر زمانیکه از Observable‌ها استفاده می‌شود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت می‌شود.


روش خواندن پارامترهای مسیریابی در +Angular 4

روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کرده‌اند که توسط شیء paramMap مدیریت می‌شود:
    // For Angular 4+
    let id = +this.route.snapshot.paramMap.get('id');
    this.getProduct(id);

    // OR
    this.route.paramMap.subscribe(params => {
          let id = +params['id'];
          this.getProduct(id);
        });
در اینجا دو روش دسترسی به پارامتر id را مشاهده می‌کنید. در حالت کار با snapshot متد paramMap.get اینبار یک رشته را قبول می‌کند و یا بجای params می‌توان از paramMap استفاده کرد.


تعریف پارامترهای اختیاری مسیریابی

فرض کنید یک صفحه‌ی جستجو را طراحی کرده‌اید که در آن می‌توان نام و کد محصول را جستجو کرد. سپس می‌خواهیم این پارامترها را به صفحه‌ی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی می‌توان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
 { path: 'products/:name/:code', component: ProductListComponent }
و سپس می‌توان یک چنین لینکی را جهت فعالسازی آن اضافه کرد:
 [routerLink]="['/products', productName, productCode]"
این روش به همراه URLهایی ناخوانا خواهد بود که قسمت‌های مختلف آن مشخص نیستند و هر بار که قرار باشد گزینه‌ی دیگری را به جستجو اضافه کرد، نیاز است این پارامترها را نیز تغییر داد. همچنین در حین جستجو ممکن است تعدادی از فیلدها اختیاری باشند و نه اجباری. برای حل این مشکل امکان تعریف پارامترهای اختیاری مسیریابی نیز پیش بینی شده‌است. دراین حالت تعریف مسیریابی صفحه‌ی نمایش لیست محصولات به صورت ذیل خواهد بود (بدون ذکر هیچ پارامتری):
 { path: 'products', component: ProductListComponent },
و سپس لینکی که به آن تعریف می‌شود، نحوه‌ی تعریف خاصی را خواهد داشت:
 [routerLink]="['/products', {name: productName, code: productCode}]"
در اینجا پارامترهای اختیاری به صورت یک سری key/value در آرایه‌ی پارامترهای مسیریابی مشخص می‌شوند و هربار که نیاز به تغییر آن‌ها بود، نیازی نیست تا تعریف مسیریابی اصلی مرتبط را تغییر داد. باید دقت داشت که پارامترهای اختیاری باید همواره پس از پارامترهای اجباری در این آرایه، ذکر شوند.
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
 http://localhost:4200/products;name=Controller;code=gmg
نحوه‌ی خواندن این پارامترها، دقیقا همانند نحوه‌ی خواندن پارامترهای اجباری هستند و در اینجا از نام key‌ها برای اشاره‌ی به آن‌ها استفاده می‌شود:
let name = this.route.snapshot.params['name'];
let code = this.route.snapshot.params['code'];

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


تعریف پارامترهای کوئری در مسیریابی

فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کرده‌اید. اکنون اگر بر روی لینک مشاهده‌ی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده می‌شود. در یک چنین حالتی برای بهبود تجربه‌ی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمت‌های مختلف حفظ کرد:
 http://localhost:42000/products/5?filterBy=product1
در این لینک جزئیات محصول 5 نمایش داده خواهد شد. پس از عدد 5، پارامترهای کوئری ذکر شده‌اند و برخلاف پارامترهای اختیاری مسیریابی، در بین مسیریابی و هدایت کاربران به صفحات مختلف، حفظ خواهند شد.
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
 { path: 'products', component: ProductListComponent}
اعمال پارامترهای کوئری مختلف، به لینک‌های تعریف شده، توسط دایرکتیو queryParams صورت می‌گیرد و در اینجا یک مجموعه‌ی از key/valueها ذکر خواهند شد:
<a [routerLink] = "['/products', product.id]"
     [queryParams] = "{ filterBy: 'er', showImage: true }">
{{product.productName}}
</a>
در این مثال یک ثابت دلخواه er مشخص شده‌است. بدیهی است می‌توان متغیری را نیز بجای این ثابت تعریف کرد (یک خاصیت عمومی تعریف شده‌ی در سطح کامپوننت که به تکست‌باکس جستجو متصل است).

و یا با کدنویسی به صورت ذیل است:
this.router.navigate(['/products'],
   {
       queryParams: { filterBy: 'er', showImage: true}
   }
);
باید دقت داشت که چون این پارامترهای کوئری در بین مسیریابی به صفحات مختلف حفظ می‌شوند، نباید کلیدهای انتخاب شده‌ی در اینجا با سایر کلیدهای موجود در صفحات دیگر تداخل پیدا کنند.

یک مشکل: اگر در صفحه‌ی نمایش جزئیات یک محصول، دکمه‌ی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمی‌شوند. مرحله‌ی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
 [preserveQueryParams] = "true"
یعنی دکمه‌ی back به این شکل تغییر می‌کند:
<a class="btn btn-default"
           [routerLink]="['/products']"
           [preserveQueryParams]="true">
            <i class="glyphicon glyphicon-chevron-left"></i> Back
</a>
و یا استفاده از { preserveQueryParams: true} در حین کدنویسی.
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافته‌است و حالت قبلی منسوخ شده اعلام گردیده‌است.
this.router.navigate(['/products'],
   { queryParamsHandling: 'preserve'}
);

پس از بازگشت به صفحه‌ی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شده‌اند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آن‌ها در متد ngOnInit باید به صورت ذیل عمل کرد:
var filter = this.route.snapshot.queryParams['filterBy'] || '';
var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
علت تعریف '' || این است که ممکن است filterBy تعریف نشده باشد (برای حالتی که صفحه برای بار اول نمایش داده می‌شود) و دلیل تعریف 'true' === این است که مقادیر دریافتی در اینجا رشته‌ای هستند و نه boolean. به همین جهت باید با رشته‌ی true مقایسه شوند.

در مثال تکمیل شده‌ی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شده‌است:
 >ng g p product/ProductFilter
فایل src\app\product\product-filter.pipe.ts با این محتوا:
import { PipeTransform, Pipe } from '@angular/core';
import { IProduct } from './iproduct';

@Pipe({
  name: 'productFilter'
})
export class ProductFilterPipe implements PipeTransform {
  transform(value: IProduct[], filterBy: string): IProduct[] {
    filterBy = filterBy ? filterBy.toLocaleLowerCase() : null;
    return filterBy ? value.filter((product: IProduct) =>
      product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value;
  }
}
و سپس تعریف تکست باکس فیلتر کردن در ابتدای قالب src\app\product\product-list\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" *ngIf="listFilter">
            <div class="col-md-6">
                <h3>Filtered by: {{listFilter}} </h3>
            </div>
    </div>
و اعمال این فیلتر به حلقه‌ی نمایش ردیف‌های جدول؛ به همراه تعریف پارامتر کوئری:
<tr *ngFor="let product of products | productFilter:listFilter">
            <td><a [routerLink]="['/products', product.id]"
                   [queryParams]="{filterBy: listFilter}">
                  {{product.productName}}
                </a>
            </td>


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


و در این حالت اگر بر روی دکمه‌ی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده می‌شود:


که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت می‌شود:
@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  listFilter: string;

  ngOnInit(): void {
    this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
آزمایش Web APIs توسط Postman - قسمت دوم - ایجاد گردش کاری بین درخواست‌ها
تا اینجا مثال‌هایی را که بررسی کردیم، متکی به خود بودند و اطلاعات هر کدام، از یک درخواست به درخواستی دیگر کاملا متفاوت بود. همچنین اطلاعات ارسالی و یا دریافتی توسط آن‌ها نیز ثابت و از پیش تعیین شده بود.


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

در Postman یک برگه‌ی جدید را باز کنید و سپس آدرس https://httpbin.org/uuid را در حالت Get درخواست نمائید:


این درخواست، یک Guid اتفاقی جدید را باز می‌گرداند. هربار که بر روی دکمه‌ی Send کلیک کنیم، مقدار Guid دریافتی متفاوت خواهد بود. این خروجی دقیقا حالتی است که در برنامه‌های دنیای واقعی با آن سر و کار داریم. در این نوع برنامه‌ها، پیشتر نمی‌توان مقدار خروجی دریافتی از سرور را پیش‌بینی کرد. همچنین علاقمندیم این مقدار دریافتی (از درخواست 1) را به Post request ای که در انتهای قسمت قبل ایجاد کردیم (درخواست 2)، ارسال کنیم و اطلاعاتی را بین درخواست‌ها به اشتراک بگذاریم.
برای این منظور، مجموعه‌ی httpbin ای را که در قسمت قبل ایجاد کردیم، انتخاب کرده و Post request ذخیره شده‌ی در آن‌را انتخاب کنید تا مجددا جزئیات آن بازیابی شود:

اکنون با مراجعه به برگه‌ی بدنه‌ی درخواست آن، قصد داریم یک خاصیت جدید id را با guid دریافتی از سرور توسط درخواستی دیگر، مقدار دهی کنیم:


برای دسترسی به این اطلاعات، می‌توان از ویژگی بسیار قدرتمند postman به نام متغیرها استفاده کرد. به همین جهت به برگه‌ی درخواست دریافت guid مراجعه کرده و برگه‌ی Tests آن‌را باز کنید:


این قسمت پس از پایان درخواست جاری، اجرا می‌شود. بنابراین دراینجا فرصت خواهیم داشت تا مقدار دریافتی از سرور را در یک متغیر ذخیره کنیم. سپس می‌توان از این متغیر در حین ارسال درخواستی دیگر استفاده کرد. در پنل کنار textbox نوشتن آزمون‌ها، تعدادی نمونه کد هم وجود دارند. برای مثال در اینجا نمونه کد «Set a global variable» را با کلیک بر روی آن انتخاب می‌کنیم:
 pm.globals.set("uuid", "foo");
در این مثال key/value این متغیر سراسری، با یک کلید مشخص و مقداری ثابت، مقدار دهی شده‌اند.

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


در ادامه برای استفاده‌ی از این متغیر سراسری، به برگه‌ی Post request مراجعه کرده و بدنه‌ی درخواست را به صورت زیر ویرایش می‌کنیم:
{
"name": "Vahid",
"id": "{{uuid}}"
}
در جائیکه بدنه‌ی درخواست درج می‌شود، متغیرها باید درون {{ }} قرار گیرند.
سپس این درخواست را ارسال کنید، در خروجی دریافتی از سرور httpbin، خاصیت و مقدار جدید id قابل مشاهده هستند که به معنای ارسال صحیح آن به سرور است:
    "json": {
        "id": "foo",
        "name": "Vahid"
    },
در اینجا، foo، مقداری است که از یک درخواست دیگر خوانده شده و توسط درخواست مجزای post جاری، ارسال گردیده‌است.
در این مرحله بر روی دکمه‌ی Save برگه‌ی uuid کلیک کرده و آن‌را در مجموعه‌ی httpbin، ذخیره کنید.


روش دسترسی و به اشتراک گذاری مقدار متغیر uuid دریافتی

تا اینجا متغیر سراسری uuid تنظیم شده، دارای یک مقدار ثابت است. برای تنظیم آن به مقدار Guid دریافتی از سرور، مجددا به برگه‌ی Tests درخواست uuid مراجعه می‌کنیم و آن‌را به نحو زیر تکمیل خواهیم کرد:



pm.response حاوی کل اطلاعات شیء response است و برای مثال بدنه‌ی response دریافتی از سمت سرور httpbin، یک چنین شکلی را دارد:
{
    "uuid": "4594e7ad-cae3-487b-bd42-fc49c312c0e9"
}
با فراخوانی متد json بر روی این شیء، اکنون می‌توان به اطلاعات آن همانند یک شیء متداول جاوا اسکریپتی که دارای کلید و خاصیت uuid است، دسترسی یافت:
let jsonResponse = pm.response.json();
pm.globals.set("uuid", jsonResponse.uuid);
پس از این تنظیم، مجددا درخواست را ارسال کنید که سبب دریافت uuid جدیدی می‌شود:
{
    "uuid": "83d437a8-bce6-438b-8693-068e5399182c"
}
برای بررسی نتیجه‌ی این عملیات، با کلیک بر روی دکمه‌‌ای با آیکن چشم، در بالای سمت راست صفحه، چنین خروجی قابل مشاهده خواهد بود:


که به معنای دسترسی به مقدار متغیر uuid دریافتی از سمت سرور و تنظیم آن به عنوان یک متغیر سراسری است.
اکنون اگر مجددا به برگه‌ی مجزای Post request مراجعه و آن‌را ارسال کنیم، در خروجی بدنه‌ی response دریافتی از سمت سرور:
   "json": {
        "id": "83d437a8-bce6-438b-8693-068e5399182c",
        "name": "Vahid"
    },
دقیقا ارسال همان مقدار متغیر دریافتی از برگه‌ای دیگر که توسط متغیر {{uuid}} تنظیم شده‌، قابل مشاهده‌است.
تا اینجا تمام برگه‌های باز را ذخیره کنید:


در این تصویر، uuid پس از Post request قرار گرفته، چون ترتیب ذخیره سازی آن‌ها به همین صورت بوده‌است. اما نیاز است uuid را به پیش از درخواست post، منتقل کرد. برای اینکار می‌توان از drag/drop آیتم آن کمک گرفت.


نوشتن یک آزمون ساده

هدف اصلی از برگه‌ی Tests هر درخواست در Postman، نوشتن آزمون‌های مختلف است. به همین جهت برای نوشتن یک آزمون ساده، به برگه‌ی Post request که هم اکنون باز است، مراجعه کرده و سپس برگه‌ی Tests آن‌را به صورت زیر تنظیم می‌کنیم:


pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});
در اینجا در پنل code snippets کناری آن، بر روی لینک و گزینه‌ی «Status code: code is 200» کلیک کرده‌ایم تا به صورت خودکار، قطعه کد فوق را تولید کند. البته Tests را در قسمت‌های بعدی با جزئیات بیشتری بررسی خواهیم کرد. در اینجا بیشتر هدف آشنایی مقدماتی با برگه‌ی Tests آن است.
این آزمون، بررسی می‌کند که آیا status code بازگشتی از سرور 200 است یا خیر؟ برای اجرای آن، فقط کافی است بر روی دکمه Send این برگه کلیک کنید:


نتیجه‌ی آن‌را در برگه‌ی جدید Test Results، در قسمت نمایش response دریافتی از سمت سرور، می‌توان مشاهده کرد که بیانگر موفقیت آمیز بودن آزمایش انجام شده‌است.
اگر علاقمندید تا حالت شکست آن‌را نیز مشاهده کنید، بجای عدد 200، عدد 404 را وارد کرده و مجددا درخواست را ارسال کنید.


اجرای ترتیبی آیتم‌های یک مجموعه

تا اینجا اگر درخواست‌ها را در مجموعه‌ی httpbin ذخیره کرده و همچنین ترتیب آن‌ها را نیز به نحوی که گفته شد، اصلاح کرده باشید، یک چنین شکلی را باید مشاهده کنید:


در اینجا برای اجرای ترتیبی این آیتم‌ها و اجرای گردش کاری کوچکی که ایجاد کرده‌ایم (درخواست post باید پس از درخواست uuid و بر اساس مقدار دریافتی آن از سرور اجرا شود)، می‌توان از collection runner برنامه‌ی Postman استفاده کرد:


با کلیک بر روی دکمه‌ی Runner، در نوار ابزار برنامه‌ی Postman، صفحه‌ی Collection runner آن ظاهر می‌شود. در این صفحه ابتدا httpbin collection را انتخاب می‌کنیم که سبب نمایش محتویات و اجزای آن خواهد شد. سپس بدون ایجاد هیچگونه تغییری در تنظیمات این صفحه، بر روی دکمه‌ی run httpbin کلیک می‌کنیم:


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


در اینجا هر مرحله، پس از مرحله‌ای دیگر اجرا شده و اگر آزمایشی نیز به همراه آن‌ها بوده باشد نیز اجرا شده‌است. همچنین اگر بر روی نام هر کدام از مراحل کلیک کنیم، می‌توان جزئیات آن‌ها را نیز دقیقا بررسی کرد:


امکان اجرای یک چنین قابلیتی توسط برنامه‌ی CLI ای به نام newman نیز به همراه Postman وجود دارد که جهت استفاده‌ی در continuous integration servers می‌تواند بسیار مفید باشد. جزئیات آن‌را در قسمت‌های بعدی بررسی خواهیم کرد.
نظرات مطالب
VS Code برای توسعه دهندگان ASP.NET Core - قسمت اول - نصب و راه اندازی
- اگر از VS 2017 به روز رسانی نشده استفاده کنید، چون این افزونه از MSBuild آن به صورت پیش‌فرض استفاده می‌کند (یعنی اگر VS 2017 را بر روی سیستم تشخیص دهد، مطلقا از MSBuild به همراه نصاب خودش استفاده نمی‌کند)، مشکلات زیادی را شاهد خواهید بود. یا باید کلا VS 2017 را حذف کنید تا از MSBuild توکار خودش استفاده کند، یا اگر نیاز به VS 2017 دارید، در نصاب آن، گزینه‌ی «NET Core Build Tools" workload."» را انتخاب کنید، تا حداقل این یک مورد را به روز رسانی کند که شامل MSBuild جدید هم هست.
- همچنین نصب آخرین نگارش Dev Pack دات نت و  SDK مخصوص NET Core. را هم فراموش نکنید (هر دو با هم).
نظرات مطالب
ارتقاء به ASP.NET Core 2.0 - معرفی بسته‌ی Microsoft.AspNetCore.All
افزونه‌ی #C مخصوص VSCode دقیقا همان روزی که NET Core 2.0. ارائه شد، به روز شده‌است. بنابراین پس از نصب SDK جدید، یکبار VSCode را بسته، به اینترنت متصل شوید، سپس VSCode را باز کنید. در برگه‌ی افزونه‌ها مشاهده خواهید کرد که این افزونه به روز شده‌است و باید صفحه را reload کنید. پس از آن یک فایل #C را هم باز کنید تا کار دریافت دیباگر جدید آن آغاز شود. اینجا است که کار به روز رسانی «دو مرحله‌ای» آن تکمیل می‌شود. پس از آن به ریشه‌ی پروژه وارد شده و دستور dotnet restore را صادر کنید تا وابستگی‌های شناسایی نشده، شناسایی شوند.