مطالب
استفاده از LINQ to XML جهت خواندن فیدهای RSS

مثال زیر را به عنوانی نمونه‌ای از کاربرد LINQ to XML برای خواندن فیدهای RSS که اساسا به فرمت XML هستند می‌توان ارائه داد.
ابتدا کد کامل مثال را در نظر بگیرید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace LinqToRSS
{
public static class LanguageExtender
{
public static string SafeValue(this XElement input)
{
return (input == null) ? string.Empty : input.Value;
}

public static DateTime SafeDateValue(this XElement input)
{
return (input == null) ? DateTime.MinValue : DateTime.Parse(input.Value);
}
}

public class RssEntry
{
public string Title { set; get; }
public string Description { set; get; }
public string Link { set; get; }
public DateTime PublicationDate { set; get; }
public string Author { set; get; }
public string BlogName { set; get; }
public string BlogAddress { set; get; }
}

public class Rss
{
static XElement selectDate(XElement date1, XElement date2)
{
return date1 ?? date2;
}

public static List<RssEntry> GetEntries(string feedUrl)
{
//applying namespace in an XElement
XName xn = XName.Get("{http://purl.org/dc/elements/1.1/}creator");//{namespace}root
XName xn2 = XName.Get("{http://purl.org/dc/elements/1.1/}date");

var feed = XDocument.Load(feedUrl);
if (feed.Root == null) return null;

var items = feed.Root.Element("channel").Elements("item");
var feedQuery =
from item in items
select new RssEntry
{
Title = item.Element("title").SafeValue(),
Description = item.Element("description").SafeValue(),
Link = item.Element("link").SafeValue(),
PublicationDate =
selectDate(item.Element(xn2), item.Element("pubDate")).SafeDateValue(),
Author = item.Element(xn).SafeValue(),
BlogName = item.Parent.Element("title").SafeValue(),
BlogAddress = item.Parent.Element("link").SafeValue()
};

return feedQuery.ToList();
}
}

class Program
{
static void Main(string[] args)
{
List<RssEntry> entries = Rss.GetEntries("http://weblogs.asp.net/aspnet-team/rss.aspx");
if (entries != null)
foreach (var item in entries)
Console.WriteLine(item.Title);

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

توضیحات:
1- در این مثال فقط جهت سهولت بیان آن در یک صفحه، تمامی کلاس‌های تعریف شده در یک فایل آورده شدند. این روش صحیح نیست و باید به ازای هر کلاس یک فایل جدا در نظر گرفته شود.
2- کلاس LanguageExtender از قابلیت extension methods سی شارپ 3 استفاده می‌کند. به این صورت کلاس XElement دات نت بسط یافته و دو متد به آن اضافه می‌شود که به سادگی در کدهای خود می‌توان از آن‌ها استفاده کرد. هدف آن هم بررسی نال بودن یک آیتم دریافتی و ارائه‌ی حاصلی امن برای این مورد است.
3- کلاس RssEntry به جهت استفاده در خروجی کوئری LINQ تعریف شد. می‌خواهیم خروجی نهایی، یک لیست جنریک از نوع RssEntry باشد.
4- متد اصلی برنامه، GetEntries است. این متد آدرس اینترنتی یک فید را دریافت کرده و پس از آنالیز، آن‌را به صورت یک لیست بر می‌گرداند.

<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://weblogs.asp.net/utility/FeedStylesheets/rss.xsl" media="screen"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>Latest Microsoft Blogs</title>
<link>http://weblogs.asp.net/aspnet-team/default.aspx</link>
<description />
<dc:language>en</dc:language>
<generator>CommunityServer 2007 SP1 (Build: 20510.895)</generator>
<item>
<title>Comments on my recent benchmarks.</title>
<link>http://misfitgeek.com/blog/aspnet/comments-on-my-recent-benchmarks/</link>
<pubDate>Mon, 10 Aug 2009 23:33:59 GMT</pubDate>
<guid isPermaLink="false">c06e2b9d-981a-45b4-a55f-ab0d8bbfdc1c:7166225</guid>
<dc:creator>Misfit Geek: msft</dc:creator>
<slash:comments>0</slash:comments>
<wfw:commentRss xmlns:wfw="http://wellformedweb.org/CommentAPI/">http://weblogs.asp.net/aspnet-team/rsscomments.aspx?PostID=7166225</wfw:commentRss>
<comments>http://misfitgeek.com/blog/aspnet/comments-on-my-recent-benchmarks/#comments</comments>
<description>Overall I’ve been pretty impressed ...</description>
<category domain="http://weblogs.asp.net/aspnet-team/archive/tags/ASP.NET/default.aspx">ASP.NET</category>
</item>
</channel>
</rss>
برای نمونه خروجی یک فید می‌تواند به صورت فوق باشد. آیتم‌های آن به صورت قابل بیان است:
var items = feed.Root.Element("channel").Elements("item");
و نکته مهمی که اینجا وجود دارد، اعمال فضاهای نام بکار رفته در این فایل xml پیشرفته می‌باشد. برای اعمال فضاهای نام به یکی از دو روش زیر می‌توان عمل کرد:

XName.Get("{mynamespace}root");
//or
XName.Get("root", "mynamespace");

مطالب
تهیه‌ی گزارش از آخرین ارائه‌های یک پروژه در GitHub
چندی قبل مطلب «اطلاع از بروز رسانی نرم افزار ساخته شده» را در سایت جاری مطالعه کردید. در این روش بسیار متداول، شماره نگارش‌های جدید برنامه در یک فایل XML و مانند آن قرار می‌گیرند و برنامه هربار این فایل را جهت یافتن شماره‌های مندرج در آن اسکن می‌کند. اگر پروژه‌ی شما سورس باز است و در GitHub هاست شده، روش دیگری نیز برای یافتن این اطلاعات وجود دارد. در GitHub می‌توان از طریق آدرسی به شکل https://api.github.com/repos/user_name/project_name/releases به اطلاعات آخرین ارائه‌های یک پروژه (قرار گرفته در برگه‌ی releases آن) با فرمت JSON دسترسی یافت (یک مثال). در ادامه قصد داریم روش استفاده‌ی از آن‌را بررسی کنیم.


ساختار JSON ارائه‌های یک پروژه در GitHub

ساختار کلی اطلاعات ارائه‌های یک پروژه در GitHub چنین شکلی را دارد:
[
  {
    release_info,
    "assets": [
      {
      }
    ]
  }
]
در اینجا آرایه‌ای از اطلاعات ارائه‌ی یک پروژه ارسال می‌شود. هر ارائه نیز دارای دو قسمت است: لینکی به صفحه‌ی اصلی release در GitHub و سپس آرایه‌ای به نام assets که در آن اطلاعات فایل‌های پیوستی مانند نام فایل، آدرس، اندازه و امثال آن قرار گرفته‌اند.



تهیه‌ی کلاس‌های معادل فرمت JSON ارائه‌های برنامه در GitHub

اگر بخواهیم قسمت‌های مهم خروجی JSON فوق را تبدیل به کلاس‌های معادل دات نتی کنیم، به دو کلاس ذیل خواهیم رسید:
using Newtonsoft.Json;
using System;

namespace ApplicationAnnouncements
{
    public class GitHubProjectRelease
    {
        [JsonProperty(PropertyName = "url")]
        public string Url { get; set; }

        [JsonProperty(PropertyName = "assets_url")]
        public string AssetsUrl { get; set; }

        [JsonProperty(PropertyName = "upload_url")]
        public string UploadUrl { get; set; }

        [JsonProperty(PropertyName = "html_url")]
        public string HtmlUrl { get; set; }

        [JsonProperty(PropertyName = "id")]
        public int Id { get; set; }

        [JsonProperty(PropertyName = "tag_name")]
        public string TagName { get; set; }

        [JsonProperty(PropertyName = "target_commitish")]
        public string TargetCommitish { get; set; }

        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }

        [JsonProperty(PropertyName = "body")]
        public string Body { get; set; }

        [JsonProperty(PropertyName = "draft")]
        public bool Draft { get; set; }

        [JsonProperty(PropertyName = "prerelease")]
        public bool PreRelease { get; set; }

        [JsonProperty(PropertyName = "created_at")]
        public DateTime CreatedAt { get; set; }

        [JsonProperty(PropertyName = "published_at")]
        public DateTime PublishedAt { get; set; }

        [JsonProperty(PropertyName = "assets")]
        public Asset[] Assets { get; set; }
    }

    public class Asset
    {
        [JsonProperty(PropertyName = "url")]
        public string Url { get; set; }

        [JsonProperty(PropertyName = "id")]
        public int Id { get; set; }

        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }

        [JsonProperty(PropertyName = "label")]
        public string Label { get; set; }

        [JsonProperty(PropertyName = "content_type")]
        public string ContentType { get; set; }

        [JsonProperty(PropertyName = "state")]
        public string State { get; set; }

        [JsonProperty(PropertyName = "size")]
        public int Size { get; set; }

        [JsonProperty(PropertyName = "download_count")]
        public int DownloadCount { get; set; }

        [JsonProperty(PropertyName = "created_at")]
        public DateTime CreatedAt { get; set; }

        [JsonProperty(PropertyName = "updated_at")]
        public DateTime UpdatedAt { get; set; }
    }
}
در اینجا از ویژگی JsonProperty جهت معرفی نام‌های واقعی خواص ارائه شده‌ی توسط GitHub استفاده کرده‌ایم.
پس از تشکیل این کلاس‌ها، مرحله‌ی بعد، دریافت اطلاعات JSON از آدرس API ارائه‌های پروژه در GitHub و سپس نگاشت آن‌ها می‌باشد:
using (var webClient = new WebClient())
{
    webClient.Headers.Add("user-agent", "DNTProfiler");
    var jsonData = webClient.DownloadString(url);
    var gitHubProjectReleases = JsonConvert.DeserializeObject<GitHubProjectRelease[]>(jsonData);

    foreach (var release in gitHubProjectReleases)
    {
       foreach (var asset in release.Assets)
       {
           // …
       }
    }
}
حین کار با WebClient یا هر روش دیگری که برای دریافت اطلاعات از وب استفاده می‌کنید، حتما نیاز است user-agent را ذکر کرد. در غیر اینصورت GitHub درخواست شما را برگشت خواهد زد. پس از دریافت اطلاعات JSON، با استفاده از متد JsonConvert.DeserializeObject کتابخانه‌ی JSON.NET، می‌توان آن‌ها را تبدیل به آرایه‌ای از GitHubProjectRelease کرد.

یک نکته: اگر به صفحه‌ی اصلی ارائه‌های یک پروژه در GitHub دقت کنید، شماره‌ی تعداد بار دریافت یک ارائه مشخص نشده‌است. در این API، عدد DownloadCount، بیانگر تعداد بار دریافت پروژه‌ی شما است.
مطالب
پیاده سازی Remote Validation در Blazor
فرم‌های Blazor به همراه پشتیبانی از ویژگی Remote که به همراه ASP.NET Core ارائه می‌شود، نیستند. هرچند می‌توان در حین ارسال فرم به سرور، نتیجه‌ی اعتبارسنجی از راه دور و سمت سرور را به کاربر نمایش داد، اما تجربه‌ی کاربری آن در حد Remote validation نیست. یعنی می‌خواهیم در حین ورود اطلاعات و یا انتقال focus به کنترل دیگری، اعتبارسنجی سمت سرور صورت گیرد و نه فقط در زمان ارسال کل اطلاعات به سرور، در پایان کار. در این مطلب روشی را جهت پیاده سازی این عملیات بررسی خواهیم کرد.


ایجاد یک پروژه‌ی ابتدایی Blazor WASM

پروژه‌ای را که در این مطلب تکمیل خواهیم کرد، از نوع Blazor WASM هاست شده‌است. بنابراین در پوشه‌ی فرضی BlazorAsyncValidation، دستور «dotnet new blazorwasm --hosted» را صادر می‌کنیم تا ساختار ابتدایی پروژه که به همراه یک کلاینت Blazor WASM و یک سرور ASP.NET Core Web API است، تشکیل شود. از قسمت Web API، برای پیاده سازی اعتبارسنجی سمت سرور استفاده خواهیم کرد.


مدل ثبت نام برنامه

مدل ثبت نام برنامه تنها از یک خاصیت نام تشکیل شده و در پروژه‌ی Shared قرار می‌گیرد تا هم در کلاینت و هم در سرور قابل استفاده باشد:
using System.ComponentModel.DataAnnotations;

namespace BlazorAsyncValidation.Shared;

public class UserDto
{
    [Required] public string Name { set; get; } = default!;
}
این نام است که می‌خواهیم عدم تکراری بودن آن‌را حین ثبت نام در سمت سرور، بررسی کنیم. به همین جهت کنترلر API زیر را برای آن تدارک خواهیم دید.


کنترلر API ثبت نام برنامه

کنترلر زیر که در پوشه‌ی BlazorAsyncValidation\Server\Controllers قرار می‌گیرد، منطق بررسی تکراری نبودن نام دریافتی از برنامه‌ی کلاینت را شبیه به منطق remote validation استاندارد MVC، پیاده سازی می‌کند که در نهایت یک true و یا false را باز می‌گرداند.
در اینجا خروجی بازگشت داده کاملا در اختیار شما است و نیازی نیست تا حتما ارتباطی با MVC داشته باشد؛ چون مدیریت سمت کلاینت بررسی آن‌را خودمان انجام خواهیم داد و نه یک کتابخانه‌ی از پیش نوشته شده و مشخص.
using BlazorAsyncValidation.Shared;
using Microsoft.AspNetCore.Mvc;

namespace BlazorAsyncValidation.Server.Controllers;

[ApiController, Route("api/[controller]/[action]")]
public class RegisterController : ControllerBase
{
    [HttpPost]
    public IActionResult IsUserNameUnique([FromBody] UserDto userModel)
    {
        if (string.Equals(userModel?.Name, "Vahid", StringComparison.OrdinalIgnoreCase))
        {
            return Ok(false);
        }

        return Ok(true);
    }
}

غنی سازی فرم استاندارد Blazor جهت انجام Remote validation



اگر بخواهیم از EditForm استاندارد Blazor در حالت متداول آن و بدون هیچ تغییری استفاده کنیم، مانند مثال زیر که InputText متصل به خاصیت Name مربوط به Dto برنامه را نمایش می‌دهد:
@page "/"

<PageTitle>Index</PageTitle>

<h2>Register</h2>

<EditForm EditContext="@EditContext" OnValidSubmit="DoSubmitAsync">
    <DataAnnotationsValidator/>
    <div class="row mb-2">
        <label class="col-form-label col-lg-2">Name:</label>
        <div class="col-lg-10">
            <InputText @bind-Value="Model.Name" class="form-control"/>
            <ValidationMessage For="@(() => Model.Name)"/>
        </div>
    </div>

    <button class="btn btn-secondary" type="submit">Submit</button>
</EditForm>
 تنها رخ‌دادی که در اختیار ما قرار می‌گیرد، رخ‌داد submit (در حالت موفقیت اعتبارسنجی سمت کلاینت و یا تنها submit معمولی) است. بنابراین برای دسترسی به رخ‌دادهای بیشتر EditForm، نیاز است با EditContext آن کار کنیم:
public partial class Index
{
    private const string UserValidationUrl = "/api/Register/IsUserNameUnique";

    private ValidationMessageStore? _messageStore;
    [Inject] private HttpClient HttpClient { set; get; } = default!;
    private EditContext? EditContext { set; get; }

    private UserDto Model { get; } = new();
به همین جهت EditContext را در سطح کامپوننت جاری تعریف کرده و همچنین سرویس HttpClient را جهت ارسال اطلاعات Name و دریافت پاسخ true/false از سرور و یک ValidationMessageStore را برای نگهداری تعاریف خطاهای سفارشی که قرار است در فرم نمایش داده شوند، معرفی می‌کنیم.
ValidationMessageStore به همراه متد Add است و اگر به آن نام فیلد مدنظر را به همراه پیامی، اضافه کنیم، این اطلاعات را به صورت خطای اعتبارسنجی توسط کامپوننت ValidationMessage نمایش می‌دهد.

محل مقدار دهی اولیه‌ی این اشیاء نیز در روال رویدادگردان OnInitialized به صورت زیر است:
    protected override void OnInitialized()
    {
        EditContext = new EditContext(Model);
        _messageStore = new ValidationMessageStore(EditContext);
        EditContext.OnFieldChanged += (sender, eventArgs) =>
        {
            var fieldIdentifier = eventArgs.FieldIdentifier;
            _messageStore?.Clear(fieldIdentifier);
            _ = InvokeAsync(async () =>
            {
                var errors = await OnValidateFieldAsync(fieldIdentifier.FieldName);
                if (errors?.Any() != true)
                {
                    return;
                }

                foreach (var error in errors)
                {
                    _messageStore?.Add(fieldIdentifier, error);
                }

                EditContext.NotifyValidationStateChanged();
            });
            StateHasChanged();
        };
        EditContext.OnValidationStateChanged += (sender, eventArgs) => StateHasChanged();
        EditContext.OnValidationRequested += (sender, eventArgs) => _messageStore?.Clear();
    }
در اینجا ابتدا یک نمونه‌ی جدید از EditContext، بر اساس Model فرم، تشکیل می‌شود. سپس ValidationMessageStore سفارشی که قرار است خطاهای اعتبارسنجی remote ما را نمایش دهد نیز نمونه سازی می‌شود. در ادامه امکان دسترسی به رخ‌دادهای OnFieldChanged ، OnValidationStateChanged و OnValidationRequested را خواهیم داشت که در حالت معمولی کار با EditForm میسر نیستند.
برای مثال اگر فیلدی تغییر کند، رویداد OnFieldChanged صادر می‌شود. در همینجا است که کار فراخوانی متد OnValidateFieldAsync که در ادامه معرفی می‌شود را انجام می‌دهیم تا کار اعتبارسنجی Async سمت سرور را انجام دهد. اگر نتیجه‌ای به همراه آن بود، توسط messageStore به صورت یک خطای اعتبارسنجی نمایش داده خواهد شد و همچنین EditContext نیز با فراخوانی متد NotifyValidationStateChanged، وادار به به‌روز رسانی وضعیت اعتبارسنجی خود می‌گردد.

متد سفارشی OnValidateFieldAsync که کار اعتبارسنجی سمت سرور را انجام می‌دهد، به صورت زیر تعریف شده‌است:
    private async Task<IList<string>?> OnValidateFieldAsync(string fieldName)
    {
        switch (fieldName)
        {
            case nameof(UserDto.Name):
                var response = await HttpClient.PostAsJsonAsync(
                    UserValidationUrl,
                    new UserDto { Name = Model.Name });
                var responseContent = await response.Content.ReadAsStringAsync();
                if (string.Equals(responseContent, "false", StringComparison.OrdinalIgnoreCase))
                {
                    return new List<string>
                    {
                        $"`{Model.Name}` is in use. Please choose another name."
                    };
                }

                // TIP: It's better to use the `DntDebounceInputText` component for this case to reduce the network round-trips.

                break;
        }

        return null;
    }
در اینجا درخواستی به سمت آدرس api/Register/IsUserNameUnique ارسال شده و سپس بر اساس مقدار true و یا false آن، پیامی را بازگشت می‌دهد. اگر نال را بازگشت دهد یعنی مشکلی نبوده.

یک نکته: InputText استاندارد در حالت معمول آن، پس از تغییر focus به یک کنترل دیگر، سبب بروز رویداد OnFieldChanged می‌شود و نه در حالت فشرده شدن کلیدها. به همین جهت اگر برنامه پیوستی را می‌خواهید آزمایش کنید، نیاز است فقط focus را تغییر دهید و یا یک کنترل سفارشی را برای اینکار توسعه دهید.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorAsyncValidation.zip
مطالب
React 16x - قسمت 32 - React Hooks - بخش 3 - نکات ویژه‌ی برقراری ارتباط با سرور
در قسمت‌های 22 تا 25 این سری، روش برقراری ارتباط با سرور را در برنامه‌های React، توسط کتابخانه‌ی معروف Axios، بررسی کردیم. در این قسمت می‌خواهیم همان نکات را زمانیکه قرار است از کامپوننت‌های تابعی، به همراه useState hook و useEffect hook استفاده کنیم، مرور نمائیم.


برپایی پیش‌نیازها

در اینجا نیز از همان برنامه‌ای که در قسمت 30، برای بررسی مثال‌های React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانه‌ی Axios را نیز نصب می‌کنید. به همین جهت در ریشه‌ی پروژه‌ی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
برنامه‌ی backend مورد استفاده هم همان برنامه‌ای است که از قسمت 22 شروع به توسعه‌ی آن کردیم و کدهای کامل آن‌را از پیوست‌های انتهای بحث، می‌توانید دریافت کنید. این برنامه که در مسیر شروع شده‌ی با https://localhost:5001/api قرار می‌گیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شده‌است. برای راه اندازی آن، به پوشه‌ی این برنامه، مراجعه کرده و فایل dotnet_run.bat آن‌را اجرا کنید، تا endpointهای REST Api آن قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts آن در مرورگر دسترسی یافت.
در ادامه می‌خواهیم در برنامه‌ی React خود، لیست مطالب برنامه‌ی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمه‌های search و clear نیز به آن اضافه کنیم.


دریافت اطلاعات اولیه از سرور، درون useEffect Hook

پس از نصب پیش‌نیازها و راه اندازی برنامه‌ی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد می‌کنیم:
{
   "apiUrl": "https://localhost:5001/api"
}
سپس فایل جدید src\components\part03\Search.jsx را جهت توسعه‌ی کامپوننت جستجوی این قسمت ایجاد می‌کنیم و ساختار ابتدایی آن‌را با import وابستگی‌های React و مسیر فوق، به صورت یک function که در همان محل قابل export است، ایجاد می‌کنیم، تا فعلا یک React.Fragment را بازگشت دهد:
import React from "react";
import {apiUrl} from "../../config.json";

export default function App() {
  return <></>;
}
بر این اساس، کامپوننت App فایل index.js را به صورت زیر از کامپوننت App فوق، تامین خواهیم کرد:
import App from "./components/part03/Search";
اکنون می‌خواهیم اولین درخواست خود را به سمت backend server ارسال کنیم. برای این منظور در کامپوننت‌های تابعی، از useEffect Hook استفاده می‌شود؛ چون کار با یک API خارجی نیز یک side effect محسوب می‌گردد. بنابراین متد useEffect را import کرده و سپس آن‌را بالای return، فراخوانی می‌کنیم. درون آن نیاز است اطلاعات را از سرور دریافت کنیم و برای اینکار از کتابخانه‌ی axios که آن‌را در قسمت 23 معرفی کردیم، استفاده خواهیم کرد. به همین جهت import آن‌را نیز در این ماژول خواهیم داشت:
import axios from "axios";
import React, { useEffect, useState } from "react";

import { apiUrl } from "../../config.json";

export default function App() {
  useEffect(() => {
    axios
      .get(apiUrl + "/posts/search?query=")
      .then(response => console.log(response.data));
  });
  return <></>;
}
در اینجا با استفاده از متد get کتابخانه‌ی axios، درخواستی را به آدرس https://localhost:5001/api/posts/search، با یک کوئری استرینگ خالی، ارسال کرده‌ایم تا تمام داده‌ها را بازگشت دهد. روش قدیمی استفاده‌ی از axios را که با استفاده از Promiseها و متد then آن است، در اینجا ملاحظه می‌کنید که خروجی خاصیت data شیء response دریافتی را لاگ کرده‌است:


اکنون می‌خواهیم این اطلاعات دریافتی را در برنامه‌ی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقه‌ای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آن‌را import کرده و سپس آن‌را تعریف و در قسمت then، فراخوانی می‌کنیم:
import axios from "axios";
import React, { useEffect, useState } from "react";

import { apiUrl } from "../../config.json";

export default function App() {
  const [results, setResults] = useState([]);

  useEffect(() => {
    axios.get(apiUrl + "/posts/search?query=").then(response => {
      console.log(response.data);
      setResults(response.data);
    });
  });
چون اطلاعات بازگشتی به صورت یک آرایه‌است، مقدار اولیه‌ی متد useState را با یک آرایه‌ی خالی مقدار دهی کرده‌ایم. سپس برای مقدار دهی متغیر results موجود در state، به متد setResults تعریف شده‌ی توسط useState، مقدار response.data را ارسال می‌کنیم. در این حالت اگر برنامه را ذخیره کرده و اجرا کنید .... برنامه و همچنین مرورگر، هنگ می‌کنند!


همانطور که مشاهده می‌کنید، یک حلقه‌ی بی پایان در اینجا رخ داده‌است! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامه‌ی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرنده‌ی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا می‌شود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننت‌های کلاسی را با هم پوشش می‌دهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شده‌است و پس از آن، مجددا effect method فراخوانی می‌شود و ... این حلقه هیچ‌گاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ می‌کنند!

در این برنامه فعلا می‌خواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایه‌ی خالی مقدار دهی می‌کنیم:
  useEffect(() => {
   // ...
  }, []);
تا اینجا موفق شدیم متد setResults را تنها در اولین بار نمایش کامپوننت، فراخوانی کنیم که در نتیجه‌ی آن، متغیر results موجود در state، مقدار دهی شده و همچنین کار رندر مجدد کامپوننت در صف قرار می‌گیرد. بنابراین مرحله‌ی بعد، تکمیل قسمت return کامپوننت تابعی است تا آرایه‌ی results را نمایش دهد:
//...

export default function App() {
  // ...
  return (
    <>
      <table className="table">
        <thead>
          <tr>
            <th>Title</th>
          </tr>
        </thead>
        <tbody>
          {results.map(post => (
            <tr key={post.id}>
              <td>{post.title}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}
در اینجا ابتدا یک فرگمنت را توسط </><> تعریف کرده‌ایم و سپس در داخل آن می‌توان المان‌های فرزند را قرار داد. سپس برای ایجاد trهای جدول، یک حلقه را توسط results.map، بر روی عناصر دریافتی از آرایه‌ی مطالب، تشکیل داده‌ایم. چون این حلقه بر روی trهای پویا تشکیل می‌شود، هر tr، نیاز به یک key دارد، تا در DOM مجازی React قابل شناسایی و ردیابی شود که در آخر یک چنین شکلی را ایجاد می‌کند:



استفاده ازAsync/Await  برای دریافت اطلاعات، درون یک  useEffect Hook

اکنون می‌خواهیم درون effect method یک useEffect Hook، روش قدیمی استفاده‌ی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آن‌را بیشتر بررسی کردیم، جایگزین کنیم.
  useEffect(async () => {
    const { data } = await axios.get(apiUrl + "/posts/search?query=");
    console.log(data);
    setResults(data);
  }, []);
خروجی متد axios.get، یک شیء Promise است که نتیجه‌ی عملیات async را بازگشت می‌دهد. در جاوا اسکریپت مدرن، می‌توان از واژه‌ی کلیدی await برای دسترسی به شیء response دریافتی از آن، استفاده کرد. سپس هر جائیکه از واژه‌ی کلیدی await استفاده می‌شود، متد جاری را باید با واژه‌ی کلیدی async نیز مزین کرد. با انجام اینکار و اجرای برنامه، اخطار زیر در کنسول توسعه دهندگان مرورگر ظاهر می‌شود؛ هرچند نتیجه نهایی هم هنوز نمایش داده می‌شود:
Warning: An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise.
این اخطار به این معنا است که effect function تعریف شده را نمی‌توان به صورت async تعریف کرد و از چنین قابلیتی پشتیبانی نمی‌شود. یک effect function حداکثر می‌تواند یک متد دیگر را بازگشت دهد (و یا هیچ چیزی را بازگشت ندهد) که نمونه‌ی آن‌را در قسمت 30، با متدهایی که کار پاکسازی منابع را انجام می‌دادند، بررسی کردیم. اگر متدی را مزین به واژه‌ی کلیدی async کردیم، یعنی این متد در اصل یک Promise را بازگشت می‌دهد؛ اما یک effect function، حداکثر یک تابع دیگر را می‌تواند بازگشت دهد تا componentWillUnmount را پیاده سازی کند.

برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
  useEffect(() => {
    getResults();
  }, []);

  const getResults = async () => {
    const { data } = await axios.get(apiUrl + "/posts/search?query=");
    console.log(data);
    setResults(data);
  };
مشکل یا محدودیتی برای ایجاد متدهای async، در خارج از یک effect function وجود ندارد. به همین جهت اعمالی را که نیاز به Async/Await دارند، در این متدهای مجزا انجام داده و سپس می‌توان آن‌ها را درون effect function، به نحوی که ملاحظه می‌کنید، فراخوانی کرد. با این تغییر، هنوز هم اطلاعات نهایی، بدون مشکل دریافت می‌شوند، اما دیگر اخطاری در کنسول توسعه دهندگان مرورگر درج نخواهد شد.


پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا

تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آن‌را از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقه‌ی بی‌نهایت وارد نشود. اکنون این سؤال مطرح می‌شود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شده‌ی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آن‌را با استفاده از useState Hook و یک مقدار اولیه‌ی دلخواه تنظیم می‌کنیم:
export default function App() {
  // ...
  const [query, setQuery] = useState("Title");
سپس المان textbox زیر را هم به بالای المان جدول، اضافه می‌کنیم:
<input
  type="text"
  name="query"
  className="form-control my-3"
  placeholder="Search..."
  onChange={event => setQuery(event.target.value)}
  value={query}
/>
این کنترل توسط رویداد onChange، عبارت تایپ شده را به متد setQuery ارسال کرده و در نتیجه‌ی آن، کار تنظیم متغیر query در state کامپوننت جاری، صورت می‌گیرد. همچنین با تنظیم value={query}، سبب خواهیم شد تا این کنترل، به یک المان کنترل شده‌ی توسط state تبدیل شود و در ابتدای نمایش فرم، مقدار ابتدایی useState را نمایش دهد.
اکنون که متغیر query دارای مقدار شده‌است، می‌توان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
const { data } = await axios.get(
   `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
);
استفاده از تابع encodeURIComponent، سبب می‌شود تا اگر کاربر برای مثال "Text 1" را وارد کرد، فاصله‌ی بین دو عبارت، به درستی encode شده و یک کوئری مانند https://localhost:5001/api/posts/search?query=Title%201 به سمت سرور ارسال گردد.

تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمی‌شود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get می‌شود، طوری تنظیم شده‌است که فقط یکبار، آن‌هم پس از رندر اولیه‌ی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایه‌ای که به عنوان پارامتر دوم متد useEffect تعریف شده، می‌توان اجرای مجدد effect function آن‌را وابسته‌ی به تغییرات متغیر query در state کامپوننت کرد:
  useEffect(() => {
    getResults();
  }, [query]);
اکنون اگر برنامه را ذخیره کرده و اجرا کنید، با هربار ورود اطلاعات درون textbox جستجو، یک کوئری جدید به سمت سرور ارسال شده و نتیجه‌ی جستجوی انجام شده، به صورت یک جدول رندر می‌شود:



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


تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد می‌کند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن می‌توان یک دکمه‌ی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف می‌کنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آن‌را مجددا به یک آرایه‌ی خالی تنظیم می‌کنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره می‌کند، در بالای جدول نتایج مطالب، قرار می‌دهیم:
<button
  className="btn btn-primary"
  type="button"
  onClick={getResults}
>
  Search
</button>
تا اینجا اگر کاربر اطلاعاتی را وارد کرده و سپس بر روی دکمه‌ی Search فوق کلیک کند، نتایج جستجوی خودش را در جدول ذیل آن مشاهده می‌کند. اکنون می‌خواهیم این امکان را به کاربران بدهیم که با فشردن دکمه‌ی enter درون textbox جستجو، همین قابلیت جستجو را در اختیار داشته باشند؛ تا دیگر الزامی به کلیک بر روی دکمه‌ی Search، نباشد. برای اینکار تنها کافی است، کل مجموعه‌ی textbox و دکمه را درون یک المان form قرار دهیم و نوع button را نیز به submit تغییر دهیم. سپس onClick دکمه را حذف کرده و بجای آن رویداد onSubmit فرم را پیاده سازی می‌کنیم:
<form onSubmit={handleSearch}>
  <div className="input-group my-3">
    <label htmlFor="query" className="form-control-label sr-only"></label>
    <input
type="text"
id="query"
name="query"
className="form-control"
placeholder="Search ..."
onChange={event => setQuery(event.target.value)}
value={query}
    />
    <div className="input-group-append">
<button className="btn btn-primary" type="submit">
  Search
</button>
    </div>
  </div>
</form>
در اینجا یک المان فرم، به همراه یک textbox و button از نوع submit تعریف شده‌اند. رویداد onSubmit نیز به متد منتسب به متغیر handleSearch، متصل شده‌است تا با فشردن دکمه‌ی enter توسط کاربر در این textbox، کار جستجوی مجدد، صورت گیرد:
  const handleSearch = event => {
    event.preventDefault();
    getResults();
  };
تا اینجا اگر برنامه را ذخیره کرده و "Text 1" را در textbox جستجو، وارد کرده و enter کنیم، همانند تصویر فوق، رکورد متناظری از سرور دریافت و نمایش داده می‌شود.


افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook

در ادامه می‌خواهیم یک دکمه‌ی جدید را در کنار دکمه‌ی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شده‌ی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشته‌ی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آن‌را با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم می‌کنیم». برای انجام یک چنین کاری در اینجا، Hook ویژه‌ای به نام useRef معرفی شده‌است. بنابراین برای پیاده سازی این نیازمند‌ی‌ها، ابتدا دکمه‌ی Clear را در کنار دکمه‌ی Search قرار می‌دهیم:
<button
  type="button"
  onClick={handleClearSearch}
  className="btn btn-info"
>
  Clear
</button>
سپس رویداد onClick آن‌را به متد منتسب به متغیر handleClearSearch، مرتبط می‌کنیم:
import React, { useEffect, useRef, useState } from "react";
// ...

export default function App() {
  // ...
  const searchInputRef = useRef();


  const handleClearSearch = () => {
    setQuery("");
    searchInputRef.current.focus();
  };
در اینجا ابتدا useRef را import کرده‌ایم، تا توسط آن بتوان یک متغیر از نوع React.MutableRefObject را ایجاد کرد. سپس در متد منتسب به handleClearSearch، ابتدا با فراخوانی setQuery، مقدار query را در state کامپوننت، پاک کرده و سپس به کمک این شیء Ref، دسترسی مستقیمی به شیء textbox یافته و متد focus آن‌را فراخوانی می‌کنیم (شیء current آن، معادل DOM Element متناظر است).
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آن‌را به ویژگی ref المان، انتساب داد:
<input
  type="text"
  id="query"
  name="query"
  className="form-control"
  placeholder="Search ..."
  onChange={event => setQuery(event.target.value)}
  value={query}
  ref={searchInputRef}
/>
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنیم، با کلیک بر روی دکمه‌ی Clear، متن textbox جستجو حذف شده و سپس کرسر مجددا به همان textbox برای ورود اطلاعات، منتقل می‌شود.


نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور


البته در اینجا با هر بار کلیک بر روی دکمه‌ی جستجو، نتیجه‌ی نهایی به سرعت نمایش داده می‌شود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ می‌دهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه می‌کنیم:
const [loading, setLoading] = useState(false);
تا با فراخوانی متد setLoading آن بتوان سبب رندر مجدد UI شد و پیامی را نمایش داد و یا مخفی کرد:
  const getResults = async () => {
    setLoading(true);
    const { data } = await axios.get(
      `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
    );
    console.log(data);
    setResults(data);
    setLoading(false);
  };
متد setLoading در ابتدای متد منتسب به متغیر getResults، مقدار متغیر loading را در state به true تنظیم می‌کند و در پایان عملیات، به false. اکنون بر این اساس می‌توان UI متناظری را نمایش داد:
      {loading ? (
        <div className="alert alert-info">Loading results...</div>
      ) : (
        <table className="table">
          <thead>
            <tr>
              <th>Title</th>
            </tr>
          </thead>
          <tbody>
            {results.map(post => (
              <tr key={post.id}>
                <td>{post.title}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
در اینجا با استفاده از یک ternary operator، اگر loading به true تنظیم شده باشد، یک div به همراه عبارت Loading results، نمایش داده می‌شود؛ در غیراینصورت، جدول اطلاعات مطالب، نمایش داده خواهد شد.

برای آزمایش آن می‌توان سرعت اتصال را در برگه‌ی شبکه‌ی ابزارهای توسعه دهندگان مرورگر، تغییر داد:



مدیریت خطاها در حین اعمال async

آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت می‌گیرد:
// ...

export default function App() {
  // ...
  const [error, setError] = useState(null);

  // ...

  const getResults = async () => {
    setLoading(true);

    try {
      const { data } = await axios.get(
        `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
      );
      console.log(data);
      setResults(data);
    } catch (err) {
      setError(err);
    }

    setLoading(false);
  };
در حین فراخوانی await axios.get، اگر خطایی رخ دهد، این کتابخانه استثنایی را صادر خواهد کرد که می‌توان به جزئیات آن در بدنه‌ی catch نوشته شده دسترسی یافت و برای مثال آن‌را به کاربر نمایش داد. برای این منظور ابتدا state مخصوص آن‌را ایجاد می‌کنیم و سپس توسط فراخوانی متد setError آن، کار رندر مجدد کامپوننت را در صف انجام قرار خواهیم داد.در نهایت برای نمایش آن می‌توان یک div را به پایین جدول اضافه نمود:
{error && <div className="alert alert-warning">{error.message}</div>}
برای آزمایش آن، برنامه‌ی backend را که در حال اجرا است، خاتمه دهید و سپس در برنامه سعی کنید به آن متصل شوید:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip
مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
Ajax.BeginForm در ASP.NET MVC از jQuery Ajax برای ارسال مقادیر فرم، به سرور استفاده می‌کند. در این بین اگر یکی از عناصر فرم، المان ارسال فایل به سرور باشد، مقدار دریافتی در سمت سرور نال خواهد بود. مشکل اینجا است که نمی‌توان به کمک Ajax معمولی (یا به عبارتی XMLHttpRequest) فایلی را به سرور ارسال کرد. یا باید از سیلورلایت یا فلش استفاده نمود و یا از مرورگرهایی که XMLHttpRequest Level 2 را پشتیبانی می‌کنند (از IE 10 به بعد مثلا) که امکان Ajax upload توکار به همراه گزارش درصد آپلود را بدون نیاز به فلش یا سیلورلایت، دارند.
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل می‌شود، مقادیر معمولی فرم (تمام المان‌ها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback می‌شود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر می‌رسد. برای انجام اینکار تنها کافی است از افزونه‌ی AjaxFileUpload استفاده کنیم که در ادامه نحوه‌ی استفاده از آن‌را بررسی خواهیم کرد.


پیشنیازها

در ادامه فرض بر این است که افزونه‌ی AjaxFileUpload را دریافت کرده و به فایل Layout برنامه افزوده‌اید:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.11.1.min.js"></script>
    <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
    <script src="~/Scripts/ajaxfileupload.js"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>


مدل، کنترلر و View برنامه

مدل برنامه مشخصات یک محصول است:
namespace MVCAjaxFormUpload.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

کنترلر آن از سه متد تشکیل شده‌است:
using System.Threading;
using System.Web;
using System.Web.Mvc;
using MVCAjaxFormUpload.Models;

namespace MVCAjaxFormUpload.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Product product)
        {
            var isAjax = this.Request.IsAjaxRequest();

            return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public ActionResult UploadFiles(HttpPostedFileBase image1, int id)
        {
            var isAjax = this.Request.IsAjaxRequest();
            Thread.Sleep(3000); //شبیه سازی عملیات طولانی
            return Json(new { FileName = "/Uploads/filename.ext" }, "text/html", JsonRequestBehavior.AllowGet);
        }
    }
}
Index اول، کار نمایش صفحه‌ی ارسال اطلاعات را انجام خواهد داد.
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام می‌دهد. HttpPost آن معمولی است.

و کدهای View این مثال نیز به شرح زیر است:
@model MVCAjaxFormUpload.Models.Product
@{
    ViewBag.Title = "Index";
}

<h2>Ajax Form Upload</h2>

@using (Ajax.BeginForm(actionName: "Index",
                       controllerName: "Home",
                       ajaxOptions: new AjaxOptions { HttpMethod = "POST" },
                       routeValues: null,
                       htmlAttributes: new { id = "uploadForm" }))
{
    <label>Name:</label>
    @Html.TextBoxFor(model => model.Name)
    <br />
    <label>Image:</label>
    <br />
    <input type="file" name="Image1" id="Image1" />
    <br />
    <input type="submit" value="Submit" />
    <img id="loading" src="~/Content/Images/loading.gif" style="display:none;">
}

@section Scripts
{
    <script type="text/javascript">
        $(function () {
            $('#uploadForm').submit(function () {
                $("#loading").show();
                $.ajaxFileUpload({
                    url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود
                    secureuri: false,
                    fileElementId: 'Image1', // آی دی المان ورودی فایل
                    dataType: 'json',
                    data: { id: 1, data: 'test' }, // اطلاعات اضافی در صورت نیاز
                    success: function (data, status) {
                        $("#loading").hide();
                        if (typeof (data.FileName) != 'undefined') {
                            alert(data.FileName);
                        }
                    },
                    error: function (data, status, e) {
                        $("#loading").hide();
                        alert(e);
                    }
                });
            });
        });
    </script>
}
فرمی که توسط Ajax.BeginForm تشکیل شده‌است، یک فرم معمولی Ajax ایی است و نکته‌ی جدیدی ندارد. تنها در آن یک المان ارسال فایل قرار گرفته‌است و همچنین Id آن‌را نیز جهت استفاده توسط jQuery مشخص کرده‌ایم.
در ادامه نحوه‌ی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده می‌کنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره می‌کند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحه‌است. از قسمت data جهت ارسال اطلاعات اضافه‌تری به اکشن متد UploadFiles استفاده می‌شود. سایر قسمت‌های آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا می‌شوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمه‌ی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمه‌ی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام می‌شوند.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
MVCAjaxFormUpload.zip
مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت سوم - Data binding
در قسمت قبل، ساختار فرم ثبت اطلاعات کارمندان را تکمیل کردیم. در این قسمت قصد داریم این اطلاعات را در کامپوننت آن توسط data binding دریافت کنیم.


نقش ngModel در data binding

ngModel دایرکتیوی است که وجود آن سبب می‌شود تا Angular آن المان ورودی خاص را تحت نظر قرار دهد:
<!--no binding -->
<input name="firstname" ngModel>
در حالت تعریفی فوق، هیچگونه عملیات data binding ایی صورت نمی‌گیرد؛ اما Angular به علت وجود ngModel، از وجود این فیلد مطلع شده‌است. اما کامپوننت برنامه اطلاعات خاصی را دریافت نخواهد کرد.
برای رفع این مشکل می‌توان با data binding یک طرفه شروع کرد:
<!--one way binding -->
<input name="firstname" [ngModel]="firstName">
در اینجا از syntax ویژه‌ی property binding استفاده شده و ngModel داخل [] قرار گرفته‌است و به firstName تنظیم شده‌است. در این حالت Angular در کامپوننت متناظر با این قالب HTML ایی، به دنبال یک خاصیت عمومی به نام firstName می‌گردد و مقدار اولیه‌ی این فیلد را از آن دریافت می‌کند.
در حالت data binding یک طرفه، اگر کاربر اطلاعات فیلد firstname را در فرم برنامه تغییر دهد، این اطلاعات به خاصیت عمومی firstName منعکس نخواهد شد.
برای رفع این مشکل (در صورت نیاز)، می‌توان از data binding دو طرفه استفاده کرد:
<!--two way binding -->
<input name="firstname" [ngModel]="firstName"
(ngModelChange)="firstName=$event">
این حالت شبیه به حالت data binding یک طرفه است؛ با این تفاوت که رویدادگردانی ngModelChange نیز به آن اضافه شده‌است. در اینجا event$ به مقدار فیلد تغییر یافته اشاره می‌کند و آن‌را به firstName انتساب می‌دهد.
البته این حالت دو طرفه، syntax ساده شده‌ی زیر را که به banana in the box نیز معروف شده‌است (موز همان () است و جعبه به [] اشاره می‌کند)، نیز می‌تواند داشته باشد که بیشتر مورد استفاده قرار می‌گیرد:
<!--two way binding -->
<input name="firstname" [(ngModel)]="firstName">


تعریف مدل فرم ثبت اطلاعات کارمندان

برای نگهداری اطلاعات فرم کارمندان، کلاس Employee را به ماژول Employee اضافه می‌کنیم:
 > ng g cl Employee/Employee
با این خروجی:
 installing class
  create src\app\Employee\employee.ts
سپس ساختار این کلاس را به نحو ذیل تکمیل خواهیم کرد که هر کدام از خواص آن، معادل یکی از المان‌های فرم است:
export class Employee {
  constructor(
    public firstName: string,
    public lastName: string,
    public isFullTime: boolean,
    public paymentType: string,
    public primaryLanguage: string
  ) {}
}
TypeScript این امکان را می‌دهد تا بتوان خواص عمومی را مستقیما در سازنده‌ی کلاس تعریف کرد. بنابراین در اینجا برای نمونه firstName هم یکی از آرگومان‌های سازنده‌ی کلاس کارمند است و هم یک خاصیت عمومی تعریف شده‌ی در آن. به علاوه در اینجا می‌توان به این خواص، مقادیر پیش فرضی را نیز انتساب داد تا در حین وهله سازی آن بتوان از تعریف اجباری یک سری از پارامترها صرفنظر کرد.

پس از آن، به فایل employee-register.component.ts مراجعه کرده و وهله‌ای از کلاس را به صورت یک خاصیت عمومی در اختیار قالب HTML ایی آن که فرم جاری را تشکیل می‌دهد، قرار می‌دهیم:
import { Employee } from "app/employee/employee";

export class EmployeeRegisterComponent implements OnInit {
  languages = ["Persian", "English", "Spanish", "Other"];
  model = new Employee("Vahid", "N", true, "FullTime", "Persian");
ابتدا کلاس کارمند import شده و سپس وهله‌ای از آن به نام model، به صورت یک خاصیت عمومی در اختیار قالب آن قرار گرفته‌است.


تغییر قالب فرم ثبت اطلاعات کارمندان برای اتصال به model

در ادامه، مرحله به مرحله قالب فرم جاری را جهت اتصال به شیء model فوق تغییر خواهیم داد:

اتصال به Text boxes

  <form #form="ngForm" novalidate>
    <div class="form-group">
      <label>First Name</label>
      <input type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
    </div>

    <div class="form-group">
      <label>Last Name</label>
      <input type="text" class="form-control" name="lastName" [(ngModel)]="model.lastName">
    </div>
همانطور که مشاهده می‌کنید، اینبار ngModel خالی قسمت قبل را توسط syntax تکمیلی banana in the box به data binding دو طرفه تغییر داده‌ایم. به این ترتیب در ابتدای نمایش فرم، این دو فیلد، مقادیر اولیه نام و نام خانوادگی را از شیء model دریافت کرده و نمایش می‌دهند. به علاوه اگر فرم نیز تغییر کند، این اطلاعات به شیء model و خواص آن نیز منعکس می‌شوند.

برای بررسی این مورد، در پایان فرم جهت دیباگ data binding، اطلاعاتی را که در مدل داریم و همچنین اطلاعاتی را که Angular در حال نظارت بر آن‌ها است، به صورت json در صفحه درج می‌کنیم:
    <button class="btn btn-primary" type="submit">Ok</button>
  </form>
  Model: {{ model | json }}
  <br> Angular: {{ form.value | json }}
  <br> form.pristine: {{ form.pristine }}
برای مثال یکبار [()] را به [] تبدیل کنید و سپس سعی در تغییر مقادیر فرم نمائید. مشاهده می‌کنید هرچند این اطلاعات تحت نظارت Angular هستند، اما چون data binding به حالت یک طرفه تغییر کرده‌است، دیگر انعکاس آن‌ها، در Model مشاهده نمی‌شوند.


اتصال به Check boxes

    <div class="checkbox">
      <label>
            <input type="checkbox" name="is-full-time"
                   [(ngModel)]="model.isFullTime"> Full Time Employee
            </label>
    </div>
روش کار در اینجا نیز همانند قبل است. با استفاده از data binding دو طرفه، مقدار checkbox را به یک خاصیت عمومی boolean انتساب داده‌ایم و برعکس (زمانیکه فرم برای بار اول نمایش داده می‌شود، مقدار اولیه‌ی خود را از شیء model دریافت می‌کند).

اتصال به Radio buttons

    <label>Payment Type</label>
    <div class="radio">
      <label>
            <input type="radio" name="paymentType" value="FullTime" checked
                   [(ngModel)]="model.paymentType">
                Full Time
            </label>
    </div>
    <div class="radio">
      <label>
            <input type="radio" name="paymentType" value="PartTime"
                   [(ngModel)]="model.paymentType">
                Part Time
            </label>
    </div>
روش اتصال به radio buttons نیز بر اساس data binding دو طرفه‌است. فقط در اینجا دقیقا یک خاصیت مشخص، به چندین radio button متصل شده‌است و در نهایت در این گروه که بر اساس name هایی یکسان تشکیل شده‌است، یک مقدار انتخاب می‌شود و مقدار آن از ویژگی value المان متناظر دریافت می‌گردد.

اتصال به Drop downs

    <div class="form-group">
      <label>Primary Language</label>
      <select class="form-control" name="primaryLanguage" [(ngModel)]="model.primaryLanguage">
          <option *ngFor="let lang of languages">
             {{ lang }}
          </option>
      </select>
    </div>
در اینجا نیز ابتدا نامی به این المان انتساب داده شده‌است و سپس توسط data binding دو طرفه، خاصیت متناظری از مدل را به این المان متصل کرده‌ایم یا برعکس؛ زمانیکه این فرم برای اولین بار نمایش داده می‌شود، مقدار اولیه‌ی این فیلد بر اساس مقدار آن در شیء model تعیین می‌شود:



نحوه‌ی فراخوانی یک متد در حین data binding دو طرفه

همانطور که در ابتدای بحث نیز عنوان شد، data binding دو طرفه را به نحو دیگری نیز می‌توان تعریف کرد:

<div class="form-group">
            <label>First Name</label>
            <input type="text" class="form-control" name="firstName" 
                [ngModel]="model.firstName" 
                (ngModelChange)="firstNameToUpperCase($event)">
</div>
در اینجا بجای استفاده‌ی از syntax معروف banana in the box، از روش اتصال یک طرفه و سپس دریافت تغییرات از طریق یک رخدادگردان استفاده شده‌است. مزیت این روش امکان دسترسی همزمان به مقدار وارد شده‌ی توسط کاربر، در کامپوننت متناظر می‌باشد:
  firstNameToUpperCase(value: string) {
    if (value.length > 0)
      this.model.firstName = value.charAt(0).toUpperCase() + value.slice(1);
    else
      this.model.firstName = value;
  }
برای مثال در اینجا اگر کاربر حرف اول یک نام را با حروف کوچک وارد کند، توسط این متد به حرف بزرگ تبدیل شده و جایگزین می‌شود. این جایگزینی نیز بلافاصله در فرم منعکس خواهد شد.

در قسمت بعد مباحث اعتبارسنجی فرم‌های مبتنی بر قالب‌ها را بررسی می‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطلب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال اول اکتفاء خواهد شد.


تغییرات مورد نیاز جهت فعال سازی ویرایش، حذف و افزودن رکوردهای jqGrid

می‌خواهیم در بدو نمایش گرید، یک ستون خاص دارای دکمه‌های ویرایش و حذف ظاهر شوند:


برای اینکار تنها کافی است در انتهای ستون‌های تعریف شده، یک ستون خاص را با formatter مساوی actions ایجاد کنیم:
                colModel: [
                    {
                        // سایر ستون‌ها
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false, formatter: 'actions',
                        formatoptions: {
                            keys: true
                        }
                    }
                ],
برای اینکه دکمه‌های ویرایش و حذف ردیف‌های آن عمل کنند:


نیاز است تعاریف سایر ستون‌هایی را که باید قابلیت ویرایش داشته باشند، به نحو ذیل تغییر دهیم:
                colModel: [
                    {
                        name: 'Id', index: 'Id', align: 'right', width: 70,
                        editable: false
                    },
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 100,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 110,
                        editable: true, edittype: 'select',
                        editoptions: {
                            dataUrl: '@Url.Action("SuppliersSelect","Home")'
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Category.Id', index: 'Category.Id', align: 'right', width: 110,
                        editable: true, edittype: 'select',
                        editoptions: {
                            dataUrl: '@Url.Action("CategoriesSelect","Home")'
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Price', index: 'Price', align: 'center', width: 100,
                        formatter: 'currency',
                        formatoptions:
                        {
                            decimalSeparator: '.',
                            thousandsSeparator: ',',
                            decimalPlaces: 2,
                            prefix: '$'
                        },
                        editable: true, edittype: 'text',
                        editrules: {
                            required: true,
                            number: true,
                            minValue: 0
                        }
                    },
                    {
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false, formatter: 'actions',
                        formatoptions: {
                            keys: true
                        }
                    }
                ],
- در اینجا هر ستونی که دارای خاصیت editable مساوی true است، قابلیت ویرایش پیدا می‌کند.
- edittype آن بیانگر کنترلی است که باید حین ویرایش آن سلول خاص ظاهر شود. برای مثال اگر text باشد، یک text box و اگر مانند حالت Supplier.Id مساوی select تعریف شود، یک drop down را ظاهر خواهد کرد. برای مقدار دهی این drop down می‌توان editoptions و سپس dataUrl آن‌را مقدار دهی نمود.
        public ActionResult SuppliersSelect()
        {
            var list = ProductDataSource.LatestProducts;
            var suppliers = list.Select(x => new SelectListItem
            {
                Text = x.Supplier.CompanyName,
                Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture)
            }).ToList();
            return PartialView("_SelectPartial", suppliers);
        }
در مثال فوق، این dataUrl به اکشن متد SuppliersSelect اشاره می‌کند که نهایتا لیستی از تولید کننده‌ها را توسط partial view ذیل بازگشت می‌دهد:
 @model IList<SelectListItem>

@Html.DropDownList("srch", Model)
در کل مقادیر قابل تنظیم در اینجا شامل  text، textarea، select، checkbox، password، button، image، file و custom هستند.
- خاصیت editrules، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شده‌است. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool، خاصیت‌های minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است (مثال‌های حالت custom را در منابع انتهای بحث می‌توانید مطالعه کنید).
- پس از اینکه مشخص شدند کدامیک از ستون‌ها باید قابلیت ویرایش داشته باشند، مسیری که باید اطلاعات نهایی را به سرور ارسال کند، توسط خاصیت editurl مشخص می‌شود:
$('#list').jqGrid({
   caption: "آزمایش چهارم",
   //url from wich data should be requested
   url: '@Url.Action("GetProducts","Home")',
   //url for edit operation
   editurl: '@Url.Action("EditProduct","Home")',
اکشن متد متناظر با این آدرس یک چنین شکلی را می‌تواند داشته باشد:
        [HttpPost]
        public ActionResult EditProduct(Product postData)
        {
            //todo: Edit product based on postData

            return Json(true);
        }
- تعاریف مسیرهای ارسال اطلاعات Add و Delete، در قسمت تنظیمات navGrid باید ذکر شوند:
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: false, search: false },
                //edit options
                {},
                //add options
                { width: 'auto', url: '@Url.Action("AddProduct","Home")' },
                //delete options
                { url: '@Url.Action("DeleteProduct","Home")' }
                );
امضای این اکشن متدها نیز بسیار شبیه به اکشن متد ویرایش است:
        [HttpPost]
        public ActionResult DeleteProduct(string id)
        {
            //todo: Delete product
            return Json(true);
        }

        [HttpPost]
        public ActionResult AddProduct(Product postData)
        {
            //todo: Add product to repository
            return Json(true);
        }
- حالت ویرایش و حذفی که تا اینجا بررسی شد (ستون actions)، جزو خواص توکار این گرید است. اگر بخواهیم آن‌ها را دستی فعال کنیم (جهت اطلاعات عمومی) می‌توان از فراخوانی متد ذیل نیز کمک گرفت:
        var lastSel;
        function inlineEdit() {
            $('input[name=rdEditApproach]').attr('disabled', true);
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: false, search: false },
                //edit options
                {},
                //add options
                { width: 'auto', url: '@Url.Action("AddProduct","Home")' },
                //delete options
                { url: '@Url.Action("DeleteProduct","Home")' }
                );
            //add onSelectRow event to support inline edit
            $('#list').setGridParam({
                onSelectRow: function (id) {
                    if (id && id != lastSel) {
                        //save changes in row
                        $('#list').saveRow(lastSel, false);
                        lastSel = id;
                    }
                    //trigger inline edit for row
                    $('#list').editRow(id, true);
                }
            });
        };
در اینجا ابتدا همان تنظیمات مسیرهای Add و Delete انجام شده‌است. سپس با فراخوانی دستی متد editRow در زمان کلیک بر روی یک ردیف، همان کاری را که ستون actions در جهت فعال سازی خودکار حالت ویرایش سلول‌ها انجام می‌دهد، می‌توان شبیه سازی کرد. متد saveRow نیز کار ارسال اطلاعات تغییر کرده را به سرور انجام می‌دهد.
- برای فعال سازی خودکار فرم‌های افزودن رکوردها و یا ویرایش ردیف‌های موجود می‌توان از فراخوانی متد formEdit ذیل کمک گرفت:
        function formEdit() {
            $('input[name=rdEditApproach]').attr('disabled', true);
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    width: 'auto', checkOnUpdate: true, checkOnSubmit: true,
                    beforeShowForm: function (form) {
                        centerDialog(form, $('#list'));
                    }
                },
                //add options
                {
                    width: 'auto', url: '@Url.Action("AddProduct","Home")',
                    reloadAfterSubmit: false, checkOnUpdate: true, checkOnSubmit: true,
                    beforeShowForm: function (form) {
                        centerDialog(form, $('#list'));
                    }
                },
                //delete options
                {
                    url: '@Url.Action("DeleteProduct","Home")', reloadAfterSubmit: false
                })
            .jqGrid('navButtonAdd', "#pager", {
                caption: "حذف ردیف‌های انتخابی", title: "Delete Toolbar", buttonicon: 'ui-icon ui-icon-trash',
                onClickButton: function () {
                    var idsList = jQuery("#list").jqGrid('getGridParam', 'selarrrow');
                    alert(idsList);
                    //jQuery("#list").jqGrid('delGridRow',idsList,{reloadAfterSubmit:false});
                }
            });
        };

        function centerDialog(form, grid) {
            var dlgDiv = $("#editmod" + grid[0].id);
            var parentDiv = dlgDiv.parent(); // div#gbox_list
            var dlgWidth = dlgDiv.width();
            var parentWidth = parentDiv.width();
            var dlgHeight = dlgDiv.height();
            var parentHeight = parentDiv.height();
            var parentTop = parentDiv.offset().top;
            var parentLeft = parentDiv.offset().left;
            dlgDiv[0].style.top =  Math.round(  parentTop  + (parentHeight-dlgHeight)/2  ) + "px";
            dlgDiv[0].style.left = Math.round(  parentLeft + (parentWidth-dlgWidth  )/2 )  + "px";
        }
ابتدای تنظیمات آن، شاهد add: true, del: true, edit: true هستید. این مورد سبب می‌شود تا در فوتر گرید، سه دکمه‌ی افزودن، ویرایش و حذف ردیف‌ها ظاهر شوند:


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


و با کلیک بر روی دکمه‌ی ویرایش ردیفی انتخاب شده، صفحه‌ی ویرایش آن ردیف به همراه مقادیر سلول‌های آن ظاهر خواهند شد:


تنظیمات قسمت‌های Add و Delete ویرایش توسط فرم‌ها، با حالت ویرایش داخل ردیفی آنچنان تفاوتی ندارد. فقط در اینجا پیش از نمایش فرم، از متد centerDialog برای نمایش صفحات افزودن و ویرایش رکوردها در وسط صفحه، استفاده شده‌است. توسط checkOnUpdate: true, checkOnSubmit: true سبب خواهیم شد تا اگر کاربر مقادیر موجود فرمی را تغییر داده‌است و سعی در بستن فرم، بدون ذخیره سازی اطلاعات کند، پیغام هشدار دهنده‌ای به او نمایش داده شود که آیا می‌خواهید تغییرات را ذخیره کنید یا خیر؟


- در انتهای متد formEdit، به کمک متد jqGrid و پارامتر navButtonAdd یک دکمه‌ی سفارشی را نیز اضافه کرده‌ایم. اگر به ستون پس از شماره‌های خودکار ردیف‌ها، در سمت راست گرید دقت کنید، یک سری chekbox قابل مشاهده هستند. برای فعال سازی خودکار آن‌ها کافی است خاصیت multiselect گرید به true تنظیم شود. اکنون برای دسترسی به این ستون‌های انتخاب شده، می‌توان از متد jqGrid به همراه پارامترهای getGridParam و selarrrow استفاده کرد. خروجی آن، لیست idهای ستون‌ها است.


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

Common Editing Properties
Inline Editing
Form Editing
Cell Editing


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid04.zip
مطالب دوره‌ها
استفاده از StructureMap جهت تزریق وابستگی‌ها در برنامه‌های WPF و الگوی MVVM
در این قسمت قصد داریم همانند کنترلرها در ASP.NET MVC، کار تزریق وابستگی‌ها را در متدهای سازنده ViewModelهای WPF بدون استفاده از الگوی Service locator انجام دهیم؛ برای مثال:
    public class TestViewModel
    {
        private readonly ITestService _testService;
        public TestViewModel(ITestService testService) //تزریق وابستگی در سازنده کلاس
        {
            _testService = testService;
        }
و همچنین کار اتصال یک ViewModel، به View متناظر آن‌را نیز خودکار کنیم. قراردادی را نیز در اینجا بکار خواهیم گرفت:
نام تمام Viewهای برنامه به View ختم می‌شوند و نام ViewModelها به ViewModel. برای مثال TestViewModel و TestView معرف یک ViewModel و View متناظر خواهند بود.


ساختار کلاس‌های لایه سرویس برنامه

namespace DI07.Services
{
    public interface ITestService
    {
        string Test();
    }
}

namespace DI07.Services
{
    public class TestService: ITestService
    {
        public string Test()
        {
            return "برای آزمایش";
        }
    }
}
یک پروژه WPF را آغاز کرده و سپس یک پروژه Class library دیگر را به نام Services با دو کلاس و اینترفیس فوق، به آن اضافه کنید. هدف از این کلاس‌ها صرفا آشنایی با نحوه تزریق وابستگی‌ها در سازنده یک کلاس ViewModel در WPF است.


علامتگذاری ViewModelها

در ادامه یک اینترفیس خالی را به نام IViewModel مشاهده می‌کنید:
namespace DI07.Core
{
    public interface IViewModel // از این اینترفیس خالی برای یافتن و علامتگذاری ویوو مدل‌ها استفاده می‌کنیم
    {
    }
}
از این اینترفیس برای علامتگذاری ViewModelهای برنامه استفاده خواهد شد. این روش، یکی از انواع روش‌هایی است که در مباحث Reflection برای یافتن کلاس‌هایی از نوع مشخص استفاده می‌شود.
برای نمونه کلاس TestViewModel برنامه، با پیاده سازی IViewModel، به نوعی نشانه گذاری نیز شده است:
using DI07.Services;
using DI07.Core;

namespace DI07.ViewModels
{
    public class TestViewModel : IViewModel // علامتگذاری ویوو مدل
    {
        private readonly ITestService _testService;
        public TestViewModel(ITestService testService) //تزریق وابستگی در سازنده کلاس
        {
            _testService = testService;
        }

        public string Data
        {
            get { return _testService.Test(); }
        }
    }
}


تنظیمات آغازین IoC Container مورد استفاده

در کلاس استاندارد App برنامه WPF خود، کار تنظیمات اولیه StructureMap را انجام خواهیم داد:
using System.Windows;
using DI07.Core;
using DI07.Services;
using StructureMap;

namespace DI07
{
    public partial class App
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            ObjectFactory.Configure(cfg =>
            {
                cfg.For<ITestService>().Use<TestService>();

                cfg.Scan(scan =>
                {
                    scan.TheCallingAssembly();
                    // Add all types that implement IView into the container, 
                    // and name each specific type by the short type name.
                    scan.AddAllTypesOf<IViewModel>().NameBy(type => type.Name);
                    scan.WithDefaultConventions();
                });
            });
        }
    }
}
در اینجا عنوان شده است که اگر نیاز به نوع ITestService وجود داشت، کلاس TestService را وهله سازی کن.
همچنین در ادامه از قابلیت اسکن این IoC Container برای یافتن کلاس‌هایی که IViewModel را در اسمبلی جاری پیاده سازی کرده‌اند، استفاده شده است. متد NameBy، سبب می‌شود که بتوان به این نوع‌های یافت شده از طریق نام کلاس‌های متناظر دسترسی یافت.


اتصال خودکار ViewModelها به Viewهای برنامه

using System.Windows.Controls;
using StructureMap;

namespace DI07.Core
{
    /// <summary>
    /// Stitches together a view and its view-model
    /// </summary>
    public static class ViewModelFactory
    {
        public static void WireUp(this ContentControl control)
        {
            var viewName = control.GetType().Name;
            var viewModelName = string.Concat(viewName, "Model"); //قرار داد نامگذاری ما است
            control.Loaded += (s, e) =>
            {
                control.DataContext = ObjectFactory.GetNamedInstance<IViewModel>(viewModelName);
            };
        }
    }
}
اکنون که کار علامتگذاری ViewModelها انجام شده و همچنین IoC Container ما می‌داند که چگونه باید آن‌ها را در اسمبلی جاری جستجو کند، مرحله بعدی، ایجاد کلاسی است که از این تنظیمات استفاده می‌کند. در کلاس ViewModelFactory، متد WireUp، وهله‌ای از یک View را دریافت کرده، نام آن‌را استخراج می‌کند و سپس بر اساس قراردادی که در ابتدای بحث وضع کردیم، نام ViewModel متناظر را یافته و سپس زمانیکه این View بارگذاری می‌شود، به صورت خودکار DataContext آن‌را به کمک StructureMap وهله سازی می‌کند. این وهله سازی به همراه تزریق خودکار وابستگی‌ها در سازنده کلاس ViewModel نیز خواهد بود.


استفاده از کلاس ViewModelFactory

در ادامه کدهای TestView و پنجره اصلی برنامه را مشاهده می‌کنید:

<UserControl x:Class="DI07.Views.TestView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Text="{Binding Data}" />
    </Grid>
</UserControl>


<Window x:Class="DI07.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Views="clr-namespace:DI07.Views"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Views:TestView />
    </Grid>
</Window>
در فایل Code behind مرتبط با TestView تنها کافی است سطر فراخوانی this.WireUp اضافه شود تا کار تزریق وابستگی‌ها، وهله سازی ViewModel متناظر و همچنین مقدار دهی DataContext آن به صورت خودکار انجام شود:
using DI07.Core;

namespace DI07.Views
{
    public partial class TestView
    {
        public TestView()
        {
            InitializeComponent();
            this.WireUp(); //تزریق خودکار وابستگی‌ها و یافتن ویوو مدل متناظر
        }
    }
}

دریافت پروژه کامل این قسمت
  DI07.zip
مطالب
Globalization در ASP.NET MVC - قسمت چهارم
در قسمت قبل مقدمه ای راجع به انواع منابع موجود در ASP.NET و برخی مسائل پیرامون آن ارائه شد. در این قسمت راجع به نحوه رفتار ASP.NET در برخورد با انواع منابع بحث می‌شود.

مدیریت منابع در ASP.NET 
در مدل پرووایدر منابع در ASP.NET کار مدیریت منابع از کلاس ResourceProviderFactory شروع می‌شود. این کلاس که از نوع abstract تعریف شده است، دو متد برای فراهم کردن پرووایدرهای کلی و محلی دارد.
کلاس پیش‌فرض در ASP.NET برای پیاده‌سازی ResourceProviderFactory در اسمبلی System.Web قرار دارد. این کلاس که ResXResourceProviderFactory نام دارد نمونه‌هایی از کلاس‌های LocalResxResourceProvider و GlobalResxResourceProvider را برمی‌گرداند. درباره این کلاس‌ها در ادامه بیشتر بحث خواهد شد.

نکته: هر سه کلاس پیش‌فرض اشاره شده در بالا و نیز سایر کلاس‌های مربوط به عملیات مدیریت منابع در آن‌ها، همگی در فضای نام System.Web.Compilation قرار دارند و متاسفانه دارای سطح دسترسی internal هستند. بنابراین به صورت مستقیم در دسترس نیستند.

برای نمونه با توجه به تصویر فرضی نشان داده شده در قسمت قبل، در اولین بارگذاری صفحه SubDir1\Page1.aspx عبارات ضمنی بکاربرده شده در این صفحه برای منابع محلی (در قسمت قبل شرح داده شده است) باعث فراخوانی متد مربوط به Local Resources در کلاس ResXResourceProviderFactory می‌شود. این متد نمونه‌ای از کلاس LocalResXResourceProvider برمی‌گرداند. (در ادامه با نحوه سفارشی‌سازی این کلاس‌ها نیز آشنا خواهیم شد).
رفتار پیش‌فرض این پرووایدر این است که نمونه‌ای از کلاس ResourceManager با توجه به کلید درخواستی برای صفحه موردنظر (مثلا نوع Page1.aspx در اسمبلی App_LocalResources.subdir1.XXXXXX که در تصویر موجود در قسمت قبل نشان داده شده است) تولید می‌کند. حال این کلاس با استفاده از کالچر مربوط به درخواست موردنظر، ورودی موردنظر را از منبع مربوطه استخراج می‌کند. مثلا اگر کالچر موردبحث es (اسپانیایی) باشد، اسمبلی ستلایت موجود در مسیر نسبی \es\ انتخاب می‌شود.
برای روشن‌تر شدن بحث به تصویر زیر که عملیات مدیریت منابع پیش فرض در ASP.NET در درخواست صفحه Page1.aspx از پوشه SubDir1 را نشان می‌دهد، دقت کنید:

همانطور که در قسمت اول این سری مطالب عنوان شد، رفتار کلاس ResourceManager برای یافتن کلیدهای Resource، استخراج آن از نزدیکترین گزینه موجود است. یعنی مثلا برای یافتن کلیدی در کالچر es در مثال بالا، ابتدا اسمبلی‌های مربوط به این کالچر جستجو می‌شود و اگر ورودی موردنظر یافته نشد، جستجو در اسمبلی‌های ستلایت پیش‌فرض سیستم موجود در ریشه فولدر bin برنامه ادامه می‌یابد، تا درنهایت نزدیک‌ترین گزینه پیدا شود (فرایند fallback).

نکته: همانطور که در تصویر بالا نیز مشخص است، نحوه نامگذاری اسمبلی منابع محلی به صورت <App_LocalResources.<SubDirectory>.<A random code است.

نکته: پس از اولین بارگذاری هر اسمبلی، آن اسمبلی به همراه خود نمونه کلاس ResourceManager که مثلا توسط کلاس LocalResXResourceProvider تولید شده است در حافظه سرور کش می‌شوند تا در استفاده‌های بعدی به کار روند.

نکته: فرایند مشابه‌ای برای یافتن کلیدها در منابع کلی (Global Resources) به انجام می‌رسد. تنها تفاوت آن این است که کلاس ResXResourceProviderFactory نمونه‌ای از کلاس GlobalResXResourceProvider تولید می‌کند.

چرا پرووایدر سفارشی؟
تا اینجا بالا با کلیات عملیاتی که ASP.NET برای بارگذاری منابع محلی و کلی به انجام می‌رساند، آشنا شدیم. حالا باید به این پرسش پاسخ داد که چرا پرووایدری سفارشی نیاز است؟ علاوه بر دلایلی که در قسمت‌های قبلی به آنها اشاره شد، می‌توان دلایل زیر را نیز برشمرد:
 
- استفاده از منابع و یا اسمبلی‌های ستلایت موجود - اگر بخواهید در برنامه خود از اسمبلی‌هایی مشترک، بین برنامه‌های ویندوزی و وبی استفاده کنید، و یا بخواهید به هردلیلی از اسمبلی‌های جداگانه‌ای برای این منابع استفاده کنید، مدل پیش‌فرض موجود در ASP.NET جوابگو نخواهد بود.

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

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

مدل پرووایدر منابع
همانطور که قبلا هم اشاره شد، وظیفه استخراج داده‌ها از Resourceها به صورت پیش‌فرض، درنهایت بر عهده نمونه‌ای از کلاس ResourceManager است. در واقع این کلاس کل فرایند انتخاب مناسب‌ترین کلید از منابع موجود را با توجه به کالچر رابط کاربری (UI Culture) در ثرد جاری کپسوله می‌کند. درباره این کلاس در ادامه بیشتر بحث خواهد شد.
هم‌چنین بازهم همانطور که قبلا توضیح داده شد، استفاده از ورودی‌های منابع موجود به دو روش انجام می‌شود. استفاده از عبارات بومی‌سازی و نیز با استفاده از برنامه‌نویسی که ازطریق دومتد GetLocalResourceObject و GetGlobalResourceObject انجام می‌شود. درضمن کلیه عبارات بومی‌سازی در زمان رندر صفحات وب درنهایت تبدیل به فراخوانی‌هایی از این دو متد در کلاس TemplateControl خواهند شد.
عملیات پس از فراخوانی این دو متد جایی است که مدل Resource Provider پیش‌فرض ASP.NET وارد کار می‌شود. این فرایند ابتدا با فراخوانی نمونه‌ای از کلاس ResourceProviderFactory آغاز می‌شود که پیاده‌سازی پیش‌فرض آن در کلاس ResXResourceProviderFactory قرار دارد.
این کلاس سپس با توجه به نوع منبع درخواستی (Global یا Local) نمونه‌ای از پرووایدر مربوطه (که باید اینترفیس IResourceProvider را پیاده‌سازی کرده باشند) را تولید می‌کند. پیاده‌سازی پیش‌فرض این پرووایدرها در ASP.NET در کلاس‌های GlobalResXResourceProvider و LocalResXResourceProvider قرار دارد.
این پروایدرها درنهایت باتوجه به محل ورودی درخواستی، نمونه مناسب از کلاس RsourceManager را تولید و استفاده می‌کنند.
هم‌چنین در پروایدرهای محلی، برای استفاده از عبارات بومی‌سازی ضمنی، نمونه‌ای از کلاس ResourceReader مورد استفاده قرار می‌گیرد. در زمان تجزیه و تحلیل صفحه وب درخواستی در سرور، با استفاده از این کلاس کلیدهای موردنظر یافته می‌شوند. این کلاس درواقع پیاده‌سازی اینترفیس IResourceReader بوده که حاوی یک Enumerator که جفت داده‌های Key-Value از کلیدهای Resource را برمی‌گرداند، است.
تصویر زیر نمایی کلی از فرایند پیش‌فرض موردبحث را نشان می‌دهد:

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

پیاده‌سازی‌ها
کلاس ResourceProviderFactory به صورت زیر تعریف شده است:
public abstract class ResourceProviderFactory
{
    public abstract IResourceProvider CreateGlobalResourceProvider(string classKey);
    public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath);
}
همانطور که مشاهده می‌کنید دو متد برای تولید پرووایدرهای مخصوص منابع کلی و محلی در این کلاس وجود دارد. پرووایدر کلی تنها نیاز به نام کلید Resource برای یافتن داده موردنظر دارد. اما پرووایدر محلی به مسیر صفحه درخواستی برای اینکار نیاز دارد که با توجه به توضیحات ابتدای این مطلب کاملا بدیهی است.
پس از تولید پرووایدر موردنظر با استفاده از متد مناسب با توجه به شرایط شرح داده شده در بالا، نمونه تولیدشده از کلاس پرووایدر موردنظر وظیفه فراهم‌کردن کلیدهای Resource را برعهده دارد. پرووایدرهای موردبحث باید اینترفیس IResourceProvider را که به صورت زیر تعریف شده است، پیاده سازی کنند:
public interface IResourceProvider
{
    IResourceReader ResourceReader { get; }
    object GetObject(string resourceKey, CultureInfo culture);
}
همانطور که می‌بینید این پرووایدرها باید یک RsourceReader برای خواندن کلیدهای Resource فراهم کنند. همچنین یک متد با عنوان GetObject که کار اصلی برگرداندن داده ذخیره‌شده در ورودی موردنظر را برعهده دارد باید در این پرووایدرها پیاده‌سازی شود. همانطور که قبلا اشاره شد، پیاده‌سازی پیش‌فرض این کلاس‌ها درنهایت نمونه‌ای از کلاس ResourceManager را برای یافتن مناسب‌ترین گزینه از بین کلیدهای موجود تولید می‌کند. این نمونه مورد بحث در متد GetObject مورد استفاده قرار می‌گیرد. 

نکته: کدهای نشان‌داده‌شده در ادامه مطلب با استفاده از ابزار محبوب ReSharper استخراج شده‌اند. این ابزار برای دریافت این کدها معمولا از APIهای سایت SymbolSource.org استفاده می‌کند. البته منبع اصلی تمام کدهای دات نت فریمورک همان referencesource.microsoft.com است.
 
کلاس ResXResourceProviderFactory
پیاده‌سازی پیش‌فرض کلاس ResourceProviderFactory در ASP.NET که در کلاس ResXResourceProviderFactory قرار دارد، به صورت زیر است:
// Type: System.Web.Compilation.ResXResourceProviderFactory
// Assembly: System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
// Assembly location: C:\Windows\Microsoft.NET\assembly\GAC_32\System.Web\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Web.dll

using System.Runtime;
using System.Web;
namespace System.Web.Compilation
{
  internal class ResXResourceProviderFactory : ResourceProviderFactory
  {
    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    public ResXResourceProviderFactory()    {    }
    public override IResourceProvider CreateGlobalResourceProvider(string classKey)
    {
      return (IResourceProvider) new GlobalResXResourceProvider(classKey);
    }
    public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
    {
      return (IResourceProvider) new LocalResXResourceProvider(VirtualPath.Create(virtualPath));
    }
  }
}
در این کلاس برای تولید پرووایدر منابع محلی از کلاس VirtualPath استفاده شده است که امکاناتی جهت استخراج مسیرهای موردنظر با توجه به مسیر نسبی و مجازی ارائه‌شده فراهم می‌کند. متاسفانه این کلاس نیز با سطح دسترسی internal تعریف شده است و امکان استفاده مستقیم از آن وجود ندارد.
 
کلاس GlobalResXResourceProvider
پیاده‌سازی پیش‌فرض اینترفیس IResourceProvider در ASP.NET برای منابع کلی که در کلاس GlobalResXResourceProvider قرار دارد، به صورت زیر است:
internal class GlobalResXResourceProvider : BaseResXResourceProvider
{
  private string _classKey;
  internal GlobalResXResourceProvider(string classKey)
  {
    _classKey = classKey;
  }
  protected override ResourceManager CreateResourceManager()
  {
    string fullClassName = BaseResourcesBuildProvider.DefaultResourcesNamespace + "." + _classKey;
    // If there is no app resource assembly, return null
    if (BuildManager.AppResourcesAssembly == null)
      return null;
    ResourceManager resourceManager = new ResourceManager(fullClassName, BuildManager.AppResourcesAssembly);
    resourceManager.IgnoreCase = true;
    return resourceManager;
  }
  public override IResourceReader ResourceReader
  {
    get
    {
      // App resources don't support implicit resources, so the IResourceReader should never be needed 
      throw new NotSupportedException();
    }
  }
}
در این کلاس عملیات تولید نمونه مناسب از کلاس ResourceManager انجام می‌شود. مقدار BaseResourcesBuildProvider.DefaultResourcesNamespace به صورت زیر تعریف شده است:
internal const string DefaultResourcesNamespace = "Resources";
که قبلا هم درباره این مقدار پیش فرض اشاره‌ای شده بود.
پارامتر classKey درواقع اشاره به نام فایل اصلی منبع کلی دارد. مثلا اگر این مقدار برابر Resource1 باشد، کلاس ResourceManager برای نوع داده Resources.Resource1 تولید خواهد شد.
هم‌چنین اسمبلی موردنظر برای یافتن ورودی‌های منابع کلی که از BuildManager.AppResourcesAssembly دریافت شده است، به صورت پیش فرض هم‌نام با مسیر منابع کلی و با عنوان App_GlobalResources تولید می‌شود.
کلاس BuildManager فرایندهای کامپایل کدها و  صفحات برای تولید اسمبلی‌ها و نگهداری از آن‌ها در حافظه را مدیریت می‌کند. این کلاس که محتوای نسبتا مفصلی دارد (نزدیک به 2000 خط کد) به صورت public و sealed تعریف شده است. بنابراین با ریفرنس دادن اسمبلی System.Web در فضای نام System.Web.Compilation در دسترس است، اما نمی‌توان کلاسی از آن مشتق کرد. BuildManager حاوی تعداد زیادی اعضای استاتیک برای دسترسی به اطلاعات اسمبلی‌هاست. اما متاسفانه بیشتر آن‌ها سطح دسترسی عمومی ندارند.

نکته: همانطور که در بالا نیز اشاره شد، ازآنجاکه کلاس ResourceReader در اینجا تنها برای عبارات بومی سازی ضمنی کاربرد دارد، و نیز عبارات بومی‌سازی ضمنی تنها برای منابع محلی کاربرد دارند، در این کلاس برای خاصیت مربوطه در پیاده سازی اینترفیس IResourceProvider یک خطای عدم پشتیبانی (NotSupportedException) صادر شده است.

کلاس LocalResXResourceProvider
پیاده‌سازی پیش‌فرض اینترفیس IResourceProvider در ASP.NET برای منابع محلی که در کلاس LocalResXResourceProvider قرار دارد، به صورت زیر است:
internal class LocalResXResourceProvider : BaseResXResourceProvider
{
  private VirtualPath _virtualPath;
  internal LocalResXResourceProvider(VirtualPath virtualPath)
  {
    _virtualPath = virtualPath;
  }
  protected override ResourceManager CreateResourceManager()
  {
    ResourceManager resourceManager = null;
    Assembly pageResAssembly = GetLocalResourceAssembly();
    if (pageResAssembly != null)
    {
      string fileName = _virtualPath.FileName;
      resourceManager = new ResourceManager(fileName, pageResAssembly);
      resourceManager.IgnoreCase = true;
    }
    else
    {
      throw new InvalidOperationException(SR.GetString(SR.ResourceExpresionBuilder_PageResourceNotFound));
    }
    return resourceManager;
  }
  public override IResourceReader ResourceReader
  {
    get
    {
      // Get the local resource assembly for this page 
      Assembly pageResAssembly = GetLocalResourceAssembly();
      if (pageResAssembly == null) return null;
      // Get the name of the embedded .resource file for this page 
      string resourceFileName = _virtualPath.FileName + ".resources";
      // Make it lower case, since GetManifestResourceStream is case sensitive 
      resourceFileName = resourceFileName.ToLower(CultureInfo.InvariantCulture);
      // Get the resource stream from the resource assembly
      Stream resourceStream = pageResAssembly.GetManifestResourceStream(resourceFileName);
      // If this page has no resources, return null 
      if (resourceStream == null) return null;
      return new ResourceReader(resourceStream);
    }
  }
  [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
  private Assembly GetLocalResourceAssembly()
  {
    // Remove the page file name to get its directory
    VirtualPath virtualDir = _virtualPath.Parent;
    // Get the name of the local resource assembly
    string cacheKey = BuildManager.GetLocalResourcesAssemblyName(virtualDir);
    BuildResult result = BuildManager.GetBuildResultFromCache(cacheKey);
    if (result != null)
    {
      return ((BuildResultCompiledAssembly)result).ResultAssembly;
    }
    return null;
  }
}
عملیات موجود در این کلاس باتوجه به فرایندهای مربوط به یافتن اسمبلی مربوطه با استفاده از مسیر ارائه‌شده، کمی پیچیده‌تر از کلاس قبلی است.
در متد GetLocalResourceAssembly عملیات یافتن اسمبلی متناظر با درخواست جاری انجام می‌شود. اینکار باتوجه به نحوه نامگذاری اسمبلی منابع محلی که در ابتدای این مطلب اشاره شد انجام می‌شود. مثلا اگر صفحه درخواستی در مسیر SubDir1/Page1.aspx/~ باشد، در این متد با استفاده از ابزارهای موجود عنوان اسمبلی نهایی برای این مسیر که به صورت App_LocalResources.SubDir1.XXXXX است تولید و درنهایت اسمبلی مربوطه استخراج می‌شود.
درضمن در اینجا هم کلاس ResourceManager برای نوع داده متناظر با نام فایل اصلی منبع محلی تولید می‌شود. مثلا برای مسیر مجازی SubDir1/Page1.aspx/~ نوع داده‌ای با نام Page1.aspx درنظر گرفته خواهد شد (با توجه به نام فایل منبع محلی که باید به صورت Page1.aspx.resx باشد. در قسمت قبل در این باره شرح داده شده است).

نکته: کلاس SR (مخفف String Resources) که در فضای نام System.Web قرار دارد، حاوی عناوین کلیدهای Resourceهای مورداستفاده در اسمبلی System.Web است. این کلاس با سطح دسترسی internal و به صورت sealed تعریف شده است. عنوان تمامی کلیدها به صورت ثوابتی از نوع رشته تعریف شده‌‌اند.
SR درواقع یک Wrapper بر روی کلاس ResourceManager است تا از تکرار عناوین کلیدهای منابع که از نوع رشته هستند، در جاهای مختلف برنامه جلوگیری شود. کار این کلاس مشابه کاری است که کتابخانه T4MVC برای نگهداری عناوین کنترلرها و اکشن‌ها به صورت رشته‌های ثابت انجام می‌دهد. از این روش در جای جای دات نت فریمورک برای نگهداری رشته‌های ثابت استفاده شده است!
 
نکته: باتوجه به استفاده از عبارات بومی‌سازی ضمنی در استفاده از ورودی‌های منابع محلی، خاصیت ResourceReader در این کلاس نمونه‌ای متناظر برای درخواست جاری از کلاس ResourceReader با استفاده از Stream استخراج شده از اسمبلی یافته شده، تولید می‌کند.

کلاس پایه BaseResXResourceProvider
کلاس پایه BaseResXResourceProvider که در دو پیاده‌سازی نشان داده شده در بالا استفاده شده است (هر دو کلاس از این کلاس مشتق شده‌اند)، به صورت زیر است: 
internal abstract class BaseResXResourceProvider : IResourceProvider
{
  private ResourceManager _resourceManager;
  ///// IResourceProvider implementation
  public virtual object GetObject(string resourceKey, CultureInfo culture)
  {
    // Attempt to get the resource manager
    EnsureResourceManager();
    // If we couldn't get a resource manager, return null 
    if (_resourceManager == null) return null;
    if (culture == null) culture = CultureInfo.CurrentUICulture;
    return _resourceManager.GetObject(resourceKey, culture);
  }
  public virtual IResourceReader ResourceReader { get { return null; } }
  ///// End of IResourceProvider implementation 
  protected abstract ResourceManager CreateResourceManager();
  private void EnsureResourceManager()
  {
    if (_resourceManager != null)  return;
    _resourceManager = CreateResourceManager();
  }
}
در این کلاس پیاده‌سازی اصلی اینترفیس IResourceProvider انجام شده است. همانطور که می‌بینید کار نهایی استخراج ورودی‌های منابع در متد GetObject با استفاده از نمونه فراهم شده از کلاس ResourceManager انجام می‌شود.

نکته: دقت کنید که در کد بالا درصورت فراهم نکردن مقداری برای کالچر، از کالچر UI در ثرد جاری (CultureInfo.CurrentUICulture) به عنوان مقدار پیش‌فرض استفاده می‌شود.

کلاس ResourceManager
در زمان اجرا ASP.NET کلید مربوط به منبع موردنظر را با استفاده از کالچر جاری UI انتخاب می‌کند. در قسمت اول این سری مطالب شرح کوتاهی بابت انواع کالچرها داده شد، اما برای توضیحات کاملتر به اینجا مراجعه کنید.
در ASP.NET به صورت پیش‌فرض تمام منابع در زمان اجرا از طریق نمونه‌ای از کلاس ResourceManager در دسترس خواهند بود. به ازای هر نوع Resource که درخواستی برای یک کلید آن ارسال می‌شود یک نمونه از کلاس ResourceManager ساخته می‌شود. در این هنگام (یعنی پس از اولین درخواست به کلیدهای یک منبع) اسمبلی ستلایت مناسب آن پس از یافته شدن (یا تولیدشدن در زمان اجرا) به دامین ASP.NET جاری بارگذاری می‌شود و تا زمانیکه این دامین Unload نشود در حافظه سرور باقی خواهد ماند.

نکته: کلاس ResourceManager تنها توانایی استخراج کلیدهای Resource از اسمبلی‌های ستلایتی (فایل‌های resources. که در قسمت اول به آن‌ها اشاره شد) که در AppDomain جاری بارگذاری شده‌اند را دارد.

کلاس ResourceManager به صورت زیر نمونه سازی می‌شود:
System.Resources.ResourceManager(string baseName, Assembly assemblyName)
پارامتر baseName به نام کامل ریشه اسمبلی اصلی موردنظر(با فضای نام و ...) اما بدون پسوند اسمبلی مربوطه (resources.) اشاره دارد. این نام که برابر نام کلاس نهایی تولیدشده برای منبع موردنظر است همنام با فایل اصلی و پیش‌فرض منبع (فایلی که حاوی عنوان هیچ زبان و کالچری نیست) تولید می‌شود. مثلا برای اسمبلی ستلایت با عنوان MyApplication.MyResource.fa-IR.resources باید از عبارت MyApplication.MyResource استفاده شود.
پارامتر assemblyName نیز به اسمبلی حاوی اسمبلی ستلایت اصلی اشاره دارد. درواقع همان اسمبلی اصلی که نوع داده مربوط به فایل منبع اصلی درون آن embed شده است.
مثلا:
var manager = new System.Resources.ResourceManager("Resources.Resource1", typeof(Resource1).Assembly)
یا
var manager = new System.Resources.ResourceManager("Resources.Resource1", Assembly.LoadFile(@"c:\MyResources\MyGlobalResources.dll"))
 
روش دیگری نیز برای تولید نمونه‌ای از این کلاس وجود دارد که با استفاده از متد استاتیک زیر که در خود کلاس ResourceManager تعریف شده است انجام می‌شود:
public static ResourceManager CreateFileBasedResourceManager(string baseName, string resourceDir, Type usingResourceSet)
در این متد کار استخراج ورودی‌های منابع مستقیما از فایل‌های resources. انجام می‌شود. در اینجا baseName نام فایل اصلی منبع بدون پیشوند resources. است. resourceDir نیز مسیری است که فایل‌های resources. در آن قرار دارند. usingResourceSet نیز نوع کلاس سفارشی سازی شده از ResourceSet برای استفاده به جای کلاس پیش‌فرض است که معمولا مقدار null برای آن وارد می‌شود تا از همان کلاس پیش‌فرض استفاده شود (چون برای بیشتر نیازها همین کلاس پیش‌فرض کفایت می‌کند).
 
نکته: برای تولید فایل resources. از یک فایل resx. میتوان از ابزار resgen همانند زیر استفاده کرد:
resgen d:\MyResources\MyResource.fa.resx

نکته: عملیاتی که درون کلاس ResourceManager انجام می‌شود پیچیده‌تر از آن است که به نظر می‌آید. این عملیات شامل فرایندهای بسیاری شامل بارگذاری کلیدهای مختلف یافته شده و مدیریت ذخیره موقت آن‌ها در حافظه (کش)، کنترل و مدیریت انواع Resource Setها، و مهمتر از همه مدیریت عملیات Fallback و ... که در نهایت شامل هزاران خط کد است که با یک جستجوی ساده قابل مشاهده و بررسی است (^).

نمونه‌سازی مناسب از ResourceManager
در کدهای نشان داده شده در بالا برای پیاده‌سازی پیش‌فرض در ASP.NET، مهمترین نکته همان تولید نمونه مناسب از کلاس ResourceManager است. پس از آماده شدن این کلاس عملیات استخراج ورودی‌های منابع براحتی و با مدیریت کامل انجام می‌شود. اما ازآنجاکه تقریبا تمامی APIهای موردنیاز با سطح دسترسی internal تعریف شده‌اند، متاسفانه تهیه و تولید این نمونه مناسب خارج از اسمبلی System.Web به صورت مستقیم وجود ندارد.
درهرصورت، برای آشنایی بیشتر با فرایند نشان داده شده، تولید این نمونه مناسب و استفاده مستقیم از آن می‌تواند مفید و نیز جالب باشد. پس از کمی تحقیق و با استفاده از Reflection به کدهای زیر رسیدم:
private ResourceManager CreateGlobalResourceManager(string classKey)
{
  var baseName = "Resources." + classKey;
  var buildManagerType = typeof(BuildManager);
  var property = buildManagerType.GetProperty("AppResourcesAssembly", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField);
  var appResourcesAssembly = (Assembly)property.GetValue(null, null);
  return new ResourceManager(baseName, appResourcesAssembly) { IgnoreCase = true };
}
تنها نکته کد فوق دسترسی به اسمبلی منابع کلی در خاصیت AppResourcesAssembly از کلاس BuildManager با استفاده از BindingFlagهای نشان داده شده است.
نحوه استفاده از این متد هم به صورت زیر است:
var manager = CreateGlobalResourceManager("Resource1");
Label1.Text = manager.GetString("String1");
اما برای منابع محلی کار کمی پیچیده‌تر است. کد مربوط به تولید نمونه مناسب از ResourceManager برای منابع محلی به صورت زیر خواهد بود:
private ResourceManager CreateLocalResourceManager(string virtualPath)
{
  var virtualPathType = typeof(BuildManager).Assembly.GetType("System.Web.VirtualPath", true);
  var virtualPathInstance = Activator.CreateInstance(virtualPathType, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { virtualPath }, CultureInfo.InvariantCulture);
  var buildResultCompiledAssemblyType = typeof(BuildManager).Assembly.GetType("System.Web.Compilation.BuildResultCompiledAssembly", true);
  var propertyResultAssembly = buildResultCompiledAssemblyType.GetProperty("ResultAssembly", BindingFlags.NonPublic | BindingFlags.Instance);
  var methodGetLocalResourcesAssemblyName = typeof(BuildManager).GetMethod("GetLocalResourcesAssemblyName", BindingFlags.NonPublic | BindingFlags.Static);
  var methodGetBuildResultFromCache = typeof(BuildManager).GetMethod("GetBuildResultFromCache", BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(string) }, null);

  var fileNameProperty = virtualPathType.GetProperty("FileName");
  var virtualPathFileName = (string)fileNameProperty.GetValue(virtualPathInstance, null);

  var parentProperty = virtualPathType.GetProperty("Parent");
  var virtualPathParent = parentProperty.GetValue(virtualPathInstance, null);
      
  var localResourceAssemblyName = (string)methodGetLocalResourcesAssemblyName.Invoke(null, new object[] { virtualPathParent });
  var buildResultFromCache = methodGetBuildResultFromCache.Invoke(null, new object[] { localResourceAssemblyName });
  Assembly localResourceAssembly = null;
  if (buildResultFromCache != null)
    localResourceAssembly = (Assembly)propertyResultAssembly.GetValue(buildResultFromCache, null);

  if (localResourceAssembly == null)
    throw new InvalidOperationException("Unable to find the matching resource file.");

  return new ResourceManager(virtualPathFileName, localResourceAssembly) { IgnoreCase = true };
}
ازجمله نکات مهم این متد تولید یک نمونه از کلاس VirtualPath برای Parse کردن مسیر مجازی واردشده برای صفحه درخواستی است. از این کلاس برای بدست آوردن نام فایل منبع محلی به همراه مسیر فولدر مربوطه جهت استخراج اسمبلی متناظر استفاده میشود.
نکته مهم دیگر این کد دسترسی به متد GetLocalResourcesAssemblyName از کلاس BuildManager است که با استفاده از مسیر فولدر مربوط به صفحه درخواستی نام اسمبلی منبع محلی مربوطه را برمی‌گرداند.
درنهایت با استفاده از متد GetBuildResultFromCache از کلاس BuildManager اسمبلی موردنظر بدست می‌آید. همانطور که از نام این متد برمی‌آید این اسمبلی از کش خوانده می‌شود. البته مدیریت این اسمبلی‌ها کاملا توسط BuildManager و سایر ابزارهای موجود در ASP.NET انجام خواهد شد.

نحوه استفاده از متد فوق نیز به صورت زیر است: 
var manager = CreateLocalResourceManager("~/Default.aspx");
Label1.Text = manager.GetString("Label1.Text");
 
نکته: ارائه و شرح کدهای پیاده‌سازی‎‌های پیش‌فرض برای آشنایی با نحوه صحیح سفارشی سازی این کلاس‌ها آورده شده است. پس با دقت بیشتر بر روی این کدها سعی کنید نحوه پیاده‌سازی مناسب را برای سفارشی‌سازی موردنظر خود پیدا کنید.

تا اینجا با مقدمات فرایند تولید پرووایدرهای سفارشی برای استفاده در فرایند بارگذاری ورودی‌های Resourceها آشنا شدیم. در ادامه به بحث تولید پرووایدرهای سفارشی برای استفاده از دیگر انواع منابع (به غیر از فایل‌های resx.) خواهم پرداخت.

منابع:
مطالب
Blazor 5x - قسمت 15 - کار با فرم‌ها - بخش 3 - ویرایش اطلاعات
در قسمت قبل، ویژگی‌های ثبت اطلاعات یک اتاق جدید و سپس نمایش لیست آن‌ها را تکمیل کردیم. در این قسمت می‌خواهیم امکان ویرایش آن‌ها را نیز اضافه کنیم.


افزودن دکمه‌ی ویرایش، به رکوردهای لیست اتاق‌ها و نمایش جزئیات رکورد در حال ویرایش

تا اینجا کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor دارای یک چنین مسیریابی است:
@page "/hotel-room/create"
در ادامه می‌خواهیم اگر کاربری برای مثال به آدرس hotel-room/edit/1 مراجعه کرد، اطلاعات رکورد متناظر با آن نمایش داده شود؛ تا بتواند آن‌ها را ویرایش کند. یعنی می‌خواهیم از همین صفحه، هم برای ویرایش اطلاعات موجود و هم برای ثبت اطلاعات جدید استفاده کنیم. بنابراین نیاز به تعریف مسیریابی دومی در کامپوننت HotelRoomUpsert وجود دارد:
@page "/hotel-room/create"
@page "/hotel-room/edit/{Id:int}"
در اینجا مسیریابی دوم تعریف شده، یک پارامتر مقید شده‌ی به نوع int را انتظار دارد. مزیت این مقید سازی، نمایش خودکار صفحه‌ی «یافت نشد» تعریف شده‌ی در فایل BlazorServer.App\App.razor، با ورود اطلاعاتی غیر عددی است. مسیریابی اول، برای ثبت اطلاعات مورد استفاده قرار می‌گیرد و مسیریابی دوم، برای ویرایش اطلاعات.
پس از تعریف مسیریابی دریافت پارامتر Id رکورد در حال ویرایش، نحوه‌ی واکنش نشان دادن به آن در کامپوننت HotelRoomUpsert.razor به صورت زیر است:
@code
{
    // ...

    [Parameter] public int? Id { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
        }
        else
        {
            // Create Mode
            HotelRoomModel = new HotelRoomDTO();
        }
    }

    // ...
}
- در اینجا پارامتر عددی Id مسیریابی را از نوع nullable تعریف کرده‌ایم. علت اینجا است که اگر کاربر با مسیریابی اول تعریف شده، به این کامپوننت برسد، یعنی قصد ثبت اطلاعات جدیدی را دارد. بنابراین ذکر Id، اختیاری است.
- سپس می‌خواهیم در زمان بارگذاری کامپوننت جاری، اگر مقدار Id، تنظیم شده بود، تمام فیلدهای فرم متصل به شیء HotelRoomModel را به صورت خودکار بر اساس اطلاعات رکورد متناظر با آن در بانک اطلاعاتی، مقدار دهی اولیه کنیم.
<EditForm Model="HotelRoomModel" OnValidSubmit="HandleHotelRoomUpsert">
چون فرم جاری توسط کامپوننت EditForm تعریف شده‌است و مدل آن به HotelRoomModel متصل است، بر اساس two-way binding‌های تعریف شده‌ی در اینجا، اگر مقدار Model را به روز رسانی کنیم، به صورت خودکار تمام فیلدهای متصل به آن نیز مقدار دهی اولیه خواهند شد.
کار مقدار دهی اولیه‌ی فیلدهای یک کامپوننت نیز باید در روال رویداد گردان OnInitializedAsync انجام شود که نمونه‌ای از آن را در کدهای فوق مشاهده می‌کنید. در این مثال اگر Id مقدار داشته باشد، مقدار آن‌را به متد GetHotelRoomAsync ارسال کرده و سپس شیء DTO دریافتی از آن‌را به مدل فرم انتساب می‌دهیم تا فرم ویرایشی، قابل استفاده شود:


در ادامه برای ساده سازی رسیدن به مسیرهایی مانند hotel-room/edit/1، به کامپوننت Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و در همان ردیفی که اطلاعات رکورد یک اتاق را نمایش می‌دهیم، لینکی را نیز به صفحه‌ی ویرایش آن، اضافه می‌کنیم:
<tr>
    <td>@room.Name</td>
    <td>@room.Occupancy</td>
    <td>@room.RegularRate.ToString("c")</td>
    <td>@room.SqFt</td>
    <td>
        <NavLink href="@($"hotel-room/edit/{room.Id}")" class="btn btn-primary">Edit</NavLink>
    </td>
</tr>
برای این منظور فقط کافی است در جائیکه tr هر رکورد رندر می‌شود، یک td مخصوص NavLink منتهی به hotel-room/edit/{room.Id} را نیز تعریف کنیم:



مدیریت ثبت اطلاعات ویرایش شده‌ی یک اتاق، در بانک اطلاعاتی

در حین تکمیل این قسمت می‌خواهیم پیام‌هایی مانند موفقیت آمیز بودن عملیات را نیز به کاربر نمایش دهیم. به همین جهت مراحل «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» را برای افزودن کتابخانه‌ی معروف جاوا اسکریپتی Toastr طی می‌کنیم که شامل این قسمت‌ها است:
- دریافت و نصب بسته‌های jquery و toastr
- اصلاح فایل Pages\_Host.cshtml برای افزودن مداخل فایل‌های CSS و JS بسته‌های نصب شده
- تعریف فایل جدید wwwroot\js\common.js برای سادگی کار با توابع جاوا اسکریپتی toastr و افزودن مدخل آن به فایل Pages\_Host.cshtml
- تعریف متدهای الحاقی JSRuntimeExtensions ، جهت کاهش کدهای تکراری فراخوانی متدهای toastr و افزودن فضای نام آن به فایل Imports.razor_

جزئیات این موارد را در قسمت یازدهم این سری می‌توانید مطالعه کنید و از تکرار آن‌ها در اینجا صرفنظر می‌شود. همچنین کدهای تکمیل شده‌ی این قسمت را از انتهای مطلب جاری نیز می‌توانید دریافت کنید.

همچنین پیش از تکمیل ادامه‌ی کدهای ویرایش اطلاعات، نیاز است متد IsRoomUniqueAsync را به صورت زیر اصلاح کنیم:
namespace BlazorServer.Services
{
    public interface IHotelRoomService
    {
        Task<bool> IsRoomUniqueAsync(string name, int roomId);

        // ...
    }
}

namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        // ...
 
        public Task<bool> IsRoomUniqueAsync(string name, int roomId)
        {
            if (roomId == 0)
            {
                // Create Mode
                return _dbContext.HotelRooms
                                .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                                .AnyAsync(x => x.Name != name);
            }
            else
            {
                // Edit Mode
                return _dbContext.HotelRooms
                                .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                                .AnyAsync(x => x.Name != name && x.Id != roomId);
            }
        }
    }
}
پیشتر در قست 13، بررسی منحصربفرد بودن نام اتاق، از طریق بررسی وجود نام آن در بانک اطلاعاتی صورت می‌گرفت. اینکار در حین ثبت اطلاعات یک رکورد جدید (Id==0) مشکلی را ایجاد نمی‌کند. اما اگر در حال ویرایش اطلاعات باشیم، چون این نام پیشتر ثبت شده‌است، پیام تکراری بودن نام اتاق را دریافت می‌کنیم؛ حتی اگر در اینجا نام اتاق در حال ویرایش را تغییر نداده باشیم و همان نام قبلی باشد. به همین جهت نیاز است در حالت ویرایش اطلاعات، اگر نامی در سایر رکوردها و نه رکوردی با Id مساوی فرم در حال ویرایش، با نام جدید یکی بود، آنگاه آن نام را به صورت تکراری گزارش دهیم که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.

اکنون متد HandleHotelRoomUpsert کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor جهت مدیریت ثبت و ویرایش اطلاعات به صورت زیر تغییر می‌کند:
// ...
@inject IJSRuntime JsRuntime


@code
{
   // ...

    private async Task HandleHotelRoomUpsert()
    {
        var isRoomUnique = await HotelRoomService.IsRoomUniqueAsync(HotelRoomModel.Name, HotelRoomModel.Id);
        if (!isRoomUnique)
        {
            await JsRuntime.ToastrError($"The room name: `{HotelRoomModel.Name}` already exists.");
            return;
        }

        if (HotelRoomModel.Id != 0 && Title == "Update")
        {
            // Update Mode
            var updateResult = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel);
            await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully.");
        }
        else
        {
            // Create Mode
            var createResult = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
            await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully.");
        }

        NavigationManager.NavigateTo("hotel-room");
    }
}
- در ابتدا چون می‌خواهیم پیام‌هایی را توسط Toastr نمایش دهیم، سرویس IJSRuntime را به کامپوننت جاری تزریق کرده‌ایم که این سرویس، امکان دسترسی به متدهای الحاقی ToastrError و ToastrSuccess تعریف شده‌ی در فایل Utils\JSRuntimeExtensions.cs را می‌دهد (کدهای آن در قسمت 11 ارائه شدند).
- در ابتدا عدم تکراری بودن نام اتاق بررسی می‌شود:


- در آخر بر اساس Id مدل فرم، حالت ویرایش و یا ثبت اطلاعات را تشخیص می‌دهیم. البته Id مدل فرم، در حالت ثبت اطلاعات نیز صفر است؛ به علت فراخوانی ()HotelRoomModel = new HotelRoomDTO که سبب مقدار دهی Id آن با عدد پیش‌فرض صفر می‌شود. بنابراین صرفا بررسی Id مدل، کافی نیست و برای مثال می‌توان عنوان تنظیم شده‌ی در متد OnInitializedAsync را نیز بررسی کرد.
- پس از تشخیص حالت ویرایش و یا ثبت، یکی از متدهای متناظر HotelRoom Service را مانند UpdateHotelRoomAsync و یا CreateHotelRoomAsync فراخوانی کرده و سپس پیامی را به کاربر نمایش داده و او را به صفحه‌ی نمایش لیست اتاق‌ها، هدایت می‌کنیم:




کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-15.zip