مطالب
پیاده سازی Open Search در ASP.NET MVC
اگر به امکانات مرورگرهای جدید دقت کرده باشید، امکان تعریف منبع جستجوی جدید، نیز برای آن‌ها وجود دارد. برای نمونه تصاویر ذیل مرتبط به مرورگرهای فایرفاکس و کروم هستند:




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


تهیه OpenSearchResult سفارشی

برنامه باید بتواند محتوای XML ایی ذیل را مطابق پروتکل Open Search به صورت پویا تهیه و در اختیار مرورگر قرار دهد:
<?xml version="1.0" encoding="UTF-8" ? />
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
    <ShortName>My Site's Asset Finder</ShortName>
    <Description>Find all your assets</Description>
    <Url type="text/html"
        method="get"
        template="http://MySite.com/Home/Search/?q=searchTerms"/>
    <InputEncoding>UTF-8</InputEncoding>
    <SearchForm>http://MySite.com/</SearchForm>
</OpenSearchDescription>
به همین جهت کلاس OpenSearchResult ذیل تهیه شده است تا انجام آن‌را با روشی سازگار با ASP.NET MVC سهولت بخشد:
using System;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Xml;

namespace WebToolkit
{
    public class OpenSearchResult : ActionResult
    {
        public string ShortName { set; get; }
        public string Description { set; get; }
        public string SearchForm { set; get; }
        public string FavIconUrl { set; get; }
        public string SearchUrlTemplate { set; get; }

        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");

            var response = context.HttpContext.Response;
            writeToResponse(response);
        }

        private void writeToResponse(HttpResponseBase response)
        {
            response.ContentEncoding = Encoding.UTF8;
            response.ContentType = "application/opensearchdescription+xml";
            using (var xmlWriter = XmlWriter.Create(response.Output, new XmlWriterSettings { Indent = true }))
            {
                xmlWriter.WriteStartElement("OpenSearchDescription", "http://a9.com/-/spec/opensearch/1.1/");

                xmlWriter.WriteElementString("ShortName", ShortName);
                xmlWriter.WriteElementString("Description", Description);
                xmlWriter.WriteElementString("InputEncoding", "UTF-8");
                xmlWriter.WriteElementString("SearchForm", SearchForm);

                xmlWriter.WriteStartElement("Url");
                xmlWriter.WriteAttributeString("type", "text/html");
                xmlWriter.WriteAttributeString("template", SearchUrlTemplate);                
                xmlWriter.WriteEndElement();

                xmlWriter.WriteStartElement("Image");
                xmlWriter.WriteAttributeString("width", "16");
                xmlWriter.WriteAttributeString("height", "16");
                xmlWriter.WriteString(FavIconUrl);
                xmlWriter.WriteEndElement();

                xmlWriter.WriteEndElement();
                xmlWriter.Close();
            }
        }
    }
}
کار این Action Result، تهیه محتوایی XML ایی مطابق نمونه‌ای است که در ابتدای توضیحات ملاحظه نمودید. توضیحات خواص آن‌، در ادامه مطلب ارائه شده‌اند.


تهیه OpenSearchController

در ادامه برای استفاده از Action Result سفارشی تهیه شده، نیاز است یک کنترلر را نیز به برنامه اضافه کنیم:
using System.Web.Mvc;

namespace Readers
{
    public partial class OpenSearchController : Controller
    {
        public virtual ActionResult Index()
        {
            var fullBaseUrl = Url.Action(result: MVC.Home.Index(), protocol: "http");
            return new OpenSearchResult
            {
                ShortName = ".NET Tips",
                Description = ".NET Tips Contents Search",
                SearchForm = fullBaseUrl,
                FavIconUrl = fullBaseUrl + "favicon.ico",
                SearchUrlTemplate = Url.Action(result: MVC.Search.Index(), protocol: "http") + "?term={searchTerms}"
            };
        }
    }
}
برای استفاده از OpenSearchResult به چند نکته باید دقت داشت:
الف) آدرس‌های مطرح شده در آن باید مطلق باشند و نه نسبی. به همین جهت پارامتر protocol در اینجا ذکر شده است تا سبب تولید یک چنین آدرس‌هایی گردد.
ب) Url.Action ایی که در اینجا استفاده شده است مطابق تعاریف T4MVC است؛ ولی کلیات آن با نمونه پیش فرض ASP.NET MVC تفاوتی نمی‌کند. توسط T4MVC بجای ذکر نام اکشن متد و کنترلر مد نظر به صورت رشته‌ای، می‌توان به صورت Strongly typed به این موارد ارجاع داد.
ج) تنها نکته مهم این کلاس، خاصیت SearchUrlTemplate است. قسمت انتهایی آن یعنی ={searchTerms} همیشه ثابت است. اما ابتدای این آدرس باید به کنترلر جستجوی شما که قادر است پارامتری را به شکل کوئری استرینگ دریافت کند، اشاره نماید.
د) FavIconUrl به آدرس یک آیکن در سایت شما اشاره می‌کند. برای نمونه ذکر favicon.ico پیش فرض سایت می‌تواند مفید باشد.


معرفی OpenSearchController به Header سایت

<link href="@Url.Action(result: MVC.OpenSearch.Index(), protocol: "http")" rel="search"
        title=".NET Tips Search"  type="application/opensearchdescription+xml" />
مرحله نهایی افزودن پروتکل Open search به سایت، مراجعه به فایل layout پروژه و افزودن link خاص فوق به آن است. در این لینک، href آن باید به مسیر کنترلر OpenSearchایی که در قسمت قبل تعریف کردیم، اشاره کند. این مسیر نیز باید مطلق باشد. به همین جهت پارامتر protocol آن مقدار دهی شده است.
مطالب دوره‌ها
بررسی جزئیات تزریق وابستگی‌ها در قالب پروژه WPF Framework
در قالب طراحی شده، نه در کدهای Viewهای اضافه شده و نه در ViewModelها، اثری از کدهای مرتبط با تزریق وابستگی‌ها و یا حتی وهله سازی ViewModel مرتبط با یک View مشاهده نمی‌شود. در ادامه قصد داریم جزئیات پیاده سازی آن‌را مرور کنیم.

مدیریت خودکار وهله سازی ViewModelها

اگر به فایل MVVM\ViewModelFactory.cs قرار گرفته در پروژه Common مراجعه کنید، کدهای کلاسی که کار وهله سازی ViewModelها را انجام می‌دهد، مشاهده خواهید کرد:
using System.Windows;
using StructureMap;

namespace WpfFramework1999.Common.MVVM
{
    /// <summary>
    /// Stitches together a view and its view-model
    /// </summary>
    public class ViewModelFactory
    {
        private readonly FrameworkElement _control;

        /// <summary>
        /// سازنده کلاس تزریق وابستگی‌ها به ویوو مدل و وهله سازی آن
        /// </summary>
        /// <param name="control">وهله‌ای از شیءایی که باید کار تزریق وابستگی‌ها در آن انجام شود</param>
        public ViewModelFactory(FrameworkElement control)
        {
            _control = control;
        }

        /// <summary>
        /// وهله متناظر با ویوو مدل
        /// </summary>
        public IViewModel ViewModelInstance { get; private set; }

        /// <summary>
        /// کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
        /// </summary>        
        public void WireUp()
        {
            var viewName = _control.GetType().Name;
            var viewModelName = string.Concat(viewName, "ViewModel"); //قرار داد نامگذاری ما است

            if (!_control.IsLoaded)
            {
                _control.Loaded += (s, e) =>
                {
                    setDataContext(viewModelName);
                };
            }
            else
            {
                setDataContext(viewModelName);
            }
        }

        private void setDataContext(string viewModelName)
        {
            //کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
            ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);
            if (ViewModelInstance == null) // این صفحه ویوو مدل ندارد
                return;

            _control.DataContext = ViewModelInstance;
        }
    }
}
در این کلاس، یک وهله از صفحه‌ای که توسط کاربر درخواست شده‌است، در سازنده کلاس دریافت گردیده و سپس در متد WireUp، بر اساس قرارداد نامگذاری که پیشتر نیز عنوان شد، ViewModel متناظر با نام View از IoC Container استخراج و وهله سازی می‌گردد. سپس این وهله به DataContext صفحه انتساب داده می‌شود.
چند سؤال مهم:
- IoC Container از کجا می‌داند که ViewModelها در کجا قرار دارند؟
- این کلاس ViewModelFactory چگونه به وهله‌ای از یک صفحه درخواستی توسط کاربر دسترسی پیدا می‌کند و در کجا؟


IoC Container از کجا می‌داند که ViewModelها در کجا قرار دارند؟

اگر بحث سری جاری را از ابتدا دنبال کرده باشید، عنوان شد که ViewModelها را در این قالب، باید مشتق شده از کلاس پایه‌ای به نام BaseViewModel تهیه کنیم. برای مثال:
/// <summary>
/// ویوو مدل افزودن و مدیریت کاربران
/// </summary>
public class AddNewUserViewModel : BaseViewModel
این کلاس پایه که در فایل MVVM\BaseViewModel.cs پروژه Common قرار دارد، به نحو زیر آغاز شده است:
/// <summary>
/// کلاس پایه ویوو مدل‌های برنامه که جهت علامتگذاری آن‌ها برای سیم کشی‌های تزریق وابستگی‌های برنامه نیز استفاده می‌شود
/// </summary>
public abstract class BaseViewModel : DataErrorInfoBase, INotifyPropertyChanged, IViewModel
اگر دقت کنید در اینجا اینترفیس IViewModel نیز ذکر شده است. این اینترفیس برای علامتگذاری ViewModelها و یافتن خودکار آن‌ها توسط IoC Container مورد استفاده درنظر گرفته شده است. اگر به فایل Core\IocConfig.cs پروژه Infrastructure مراجعه کنید، چنین تنظیمی را در آن مشاهده خواهید نمود:
// 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);
به این ترتیب StructureMap با اسکن اسمبلی Infrastructure کلیه کلاس‌های پیاده سازی کننده IViewModel را یافته و سپس آن‌ها را بر اساس نام متناظری که دارند، ذخیره می‌کند. با این تنظیم، اکنون در کلاس ViewModelFactory یک چنین کدی کار خواهد کرد:
 //کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);


کلاس ViewModelFactory چگونه به وهله‌ای از یک صفحه درخواستی توسط کاربر دسترسی پیدا می‌کند و در کجا؟

در اینجا قسمتی از کدهای فایل Core\FrameFactory.cs قرار گرفته در پروژه Infrastructure را ملاحظه می‌کنید:
namespace WpfFramework.Infrastructure.Core
{
    /// <summary>
    /// ایجاد یک کنترل فریم سفارشی که قابلیت تزریق وابستگی‌ها را به صورت خودکار دارد
    /// به همراه اعمال مسایل راهبری برنامه که از منوی اصلی دریافت می‌شوند
    /// </summary>
    public class FrameFactory : Frame
    {
        /// <summary>
        /// در اینجا می‌شود به وهله‌ای از صفحه‌ای که قرار است اضافه گردد دسترسی یافت
        /// </summary>
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            var newPage = newContent as FrameworkElement;
            if (newPage == null)
                return;

            _currentViewModelFactory = new ViewModelFactory(newPage);
            _currentViewModelFactory.WireUp(); //کار تزریق وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
        }
    }
}
در این کلاس، یک Frame سفارشی را طراحی کرده‌ایم؛ از این جهت که بتوان متد OnContentChanged آن‌را تحریف کرد. در این متد، newContent دقیقا وهله‌ای از صفحه جدیدی است که توسط کاربر درخواست شده‌است. خوب ... این وهله را داریم، بنابراین تنها کافی است آن‌را به کلاس ViewModelFactory ارسال کنیم و متد WireUp آن‌را بر روی وهله کلاس صفحه درخواستی فراخوانی نمائیم. به این ترتیب، صفحه‌ای نمایش داده خواهد شد که DataContext آن با وهله‌ای از ViewModel متناظر مقدار دهی شده‌است. از این جهت که این وهله سازی توسط IoC Container صورت می‌گیرد، کلیه وابستگی‌های تعریف شده در سازنده کلاس ViewModel نیز به صورت خودکار وهله سازی و مقدار دهی خواهند شد.

نهایتا فراخوانی متد IocConfig.Init، در فایل App.xaml.cs پروژه ریشه، در آغاز برنامه قرار گرفته است.
بازخوردهای دوره
الگوی معکوس سازی کنترل چیست؟
تعریف ساده دیگری از معکوس سازی کنترل:

فرض کنید دو کلاس مشتری و آدرس دارید، در حالت عادی کلاس مشتری ایجاد و استفاده از کلاس آدرس را کنترل می‌کند ولی با استفاده از اصل معکوس سازی کنترل، کلاس آدرس به کلاس مشتری می‌گوید که تو من را ایجاد نکن و من خودم را در جایی دیگر ایجاد می‌کنم.
مطالب
قابلیت Templated Razor Delegate
Razor دارای قابلیتی با نام Templated Razor Delegates است. همانطور که از نام آن مشخص است، یعنی Razor Template هایی که Delegate هستند. در ادامه این قابلیت را با ذکر چند مثال توضیح خواهیم داد.
مثال اول:
می‌خواهیم تعدادی تگ li را در خروجی رندر کنیم، این کار را می‌توانیم با استفاده از Razor helpers نیز به این صورت انجام دهیم:
@helper ListItem(string content) {
 <li>@content</li>
}
<ul>
 @foreach(var item in Model) {
 @ListItem(item)
 }
</ul>
همین کار را می‌توانیم توسط Templated Razor Delegate به صورت زیر نیز انجام دهیم:
@{
 Func<dynamic, HelperResult> ListItem = @<li>@item</li>;
}
<ul>
 @foreach(var item in Model) {
 @ListItem(item)
 }
</ul>
برای اینکار از نوع Func استفاده خواهیم کرد. این Delegate یک پارامتر را می‌پذیرد. این پارامتر می‌تواند از هر نوعی باشد. در اینجا از نوع dynamic استفاده کرده‌ایم. خروجی این Delegate نیز یک HelperResult است. همانطور که مشاهده می‌کنید آن را برابر با الگویی که قرار است رندر شود تعیین کرده‌ایم. در اینجا از یک پارامتر ویژه با نام item استفاده شده است. نوع این پارامتر dynamic است؛ یعنی همان مقداری که برای پارامتر ورودی Func انتخاب کردیم. در نتیجه پارامتر ورودی یعنی رشته item جایگزین item@ درون Delegate خواهد شد.
در واقع دو روش فوق خروجی یکسانی را تولید میکنند. برای حالت‌هایی مانند کار با آرایه‌ها و یا Enumerations بهتر است از روش دوم استفاده کنید؛ از این جهت که نیاز به کد کمتری دارد و نگهداری آن خیلی از روش اول ساده‌تر است.

مثال دوم:
اجازه دهید یک مثال دیگر را بررسی کنیم. به طور مثال معمولاً در یک فایل Layout برای بررسی کردن وجود یک section از کدهای زیر استفاده می‌کنیم:
<header>  
    @if (IsSectionDefined("Header"))  
    {  
        @RenderSection("Header")  
    }  
    else  
    {  
        <div>Default Content for Header Section</div>  
    }  
</header>
روش فوق به درستی کار خواهد کرد اما می‌توان آن را با یک خط کد، درون ویو نیز نوشت. در واقع می‌توانیم با استفاده از Templated Razor Delegate یک متد الحاقی برای کلاس ViewPage بنویسیم؛ به طوریکه یک محتوای پیش‌فرض را برای حالتی که section خاصی وجود ندارد، نمایش دهد:
public static HelperResult RenderSection(this WebViewPage page, string name,  
    Func<dynamic, HelperResult> defaultContent)  
{  
    if (page.IsSectionDefined(name))  
    {  
        return page.RenderSection(name);  
    }  
    return defaultContent(null);  
}
بنابراین درون ویو می‌توانیم از متد الحاقی فوق به این صورت استفاده کرد:
<header>  
   @this.RenderSection("Header", @<div>Default Content for Header Section</div>)  
</header>
نکته: جهت بوجود نیامدن تداخل با نمونه اصلی RenderSection درون ویو، از کلمه this استفاده کرده‌ایم.

مثال سوم: شبیه‌سازی کنترل Repeater:
یکی از ویژگی‌های جذاب WebForm کنترل Repeater است. توسط این کنترل به سادگی می‌توانستیم یکسری داده را نمایش دهیم؛ این کنترل در واقع یک کنترل DataBound و همچنین یک Templated Control است. یعنی در نهایت کنترل کاملی بر روی Markup آن خواهید داشت. برای نمایش هر آیتم خاص داخل لیست می‌توانستید از ItemTemplate استفاده کنید. همچنین می‌توانستید از AlternatingItemtemplate استفاده کنید. یا اگر می‌خواستید هر آیتم را با چیزی از یکدیگر جدا کنید، می‌توانستید از SeparatorTemplate استفاده کنید. در این مثال می‌خواهیم همین کنترل را در MVC شبیه‌سازی کنیم.
به طور مثال ویوی Index ما یک مدل از نوع IEnumerable<string> را دارد: 
@model IEnumerable<string>  
@{  
    ViewBag.Title = "Test";  
}
و اکشن متد ما نیز به این صورت اطلاعات را به ویوی فوق پاس میدهد: 
public ActionResult Index()  
{  
    var names = new string[]  
    {  
        "Vahid Nasiri",  
        "Masoud Pakdel",  
        ...  
     };  
  
    return View(names);  
}
 اکنون در ویوی Index می‌خواهیم هر کدام از اسامی فوق را نمایش دهیم. اینکار را می‌توانیم درون ویو با یک حلقه‌ی foreach و بررسی زوج با فرد بودن ردیف‌ها انجام دهیم اما کد زیادی را باید درون ویو بنویسیم. اینکار را می‌توانیم درون یک متد الحاقی نیز انجام دهیم. بنابراین یک متد الحاقی برای HtmlHelper به صورت زیر خواهیم نوشت: 
public static HelperResult Repeater<T>(this HtmlHelper html,  
    IEnumerable<T> items,  
    Func<T, HelperResult> itemTemplate,  
    Func<T, HelperResult> alternatingitemTemplate = null,  
    Func<T, HelperResult> seperatorTemplate = null)  
{  
    return new HelperResult(writer =>  
    {  
        if (!items.Any())  
        {  
            return;  
        }  
        if (alternatingitemTemplate == null)  
        {  
            alternatingitemTemplate = itemTemplate;  
        }  
        var lastItem = items.Last();  
        int ii = 0;  
        foreach (var item in items)  
        {  
           var func = ii % 2 == 0 ? itemTemplate : alternatingitemTemplate;  
           func(item).WriteTo(writer);  
           if (seperatorTemplate != null && !item.Equals(lastItem))  
           {  
               seperatorTemplate(item).WriteTo(writer);  
           }  
           ii++;  
        }  
    });  
}
توضیح کدهای فوق:
خوب، همانطور که ملاحظه می‌کنید متد را به صورت Generic تعریف کرده‌ایم، تا بتواند با انواع نوع‌ها به خوبی کار کند. زیرا ممکن است لیستی از اعداد را داشته باشیم. از آنجائیکه این متد را برای کلاس HtmlHelper می‌نویسیم، پارامتر اول آن را از این نوع می‌گیریم. پارامتر دوم آن، آیتم‌هایی است که می‌خواهیم نمایش دهیم. پارامتر‌های بعدی نیز به ترتیب برای ItemTemplate، AlternatingItemtemplate و SeperatorItemTemplate تعریف شده‌اند و از نوع Delegate با پارامتر ورودی T و خروجی HelperResult هستند. در داخل متدمان یک HelperResult را برمیگردانیم. این کلاس یک Action را از نوع TextWriter از ورودی می‌پذیرد. اینکار را با ارائه یک Lambda Expression با نام writer انجام می‌دهیم. در داخل این Delegate به تمام منطقی که برای نمایش یک آیتم نیاز هست دسترسی داریم. 
ابتدا بررسی کرده‌ایم که آیا آیتم برای نمایش وجود دارد یا خیر. سپس اگر AlternatingItemtemplate برابر با null بود همان ItemTemplate را در خروجی نمایش خواهیم داد. مورد بعدی دسترسی به آخرین آیتم در Collection است. زیرا بعد از هر آیتم باید یک SeperatorItemTemplate را در خروجی نمایش دهیم. سپس توسط یک حلقه درون آیتم‌ها پیمایش میکنیم و ItemTemplate و  AlternatingItemtemplate را توسط متغیر func از یکدیگر تشخیص می‌دهیم و در نهایت درون ویو به این صورت از متد الحاقی فوق استفاده می‌کنیم: 
@Html.Repeater(Model, @<div>@item</div>, @<p>@item</p>, @<hr/>)
متد الحاقی فوق قابلیت کار با انواع ورودی‌ها را دارد به طور مثال مدل زیر را در نظر بگیرید:
public class Product
{
        public int Id { set; get; }
        public string Name { set; get; }
}
می‌خواهیم اطلاعات مدل فوق را در ویوی مربوط درون یک جدول نمایش دهیم، می‌توانیم به این صورت توسط متد الحاقی تعریف شده اینکار را به این صورت انجام دهیم:
<table>
    <tr>
        <td>Id</td>
        <td>Name</td>
    </tr>
    @Html.Repeater(Model, @<tr><td>@item.Id</td><td>@item.Name</td></tr>)
</table>

مطالب
نمایش خطاهای اعتبارسنجی سمت سرور ASP.NET Core در برنامه‌های Angular
در مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها» با نحوه‌ی تنظیمات اعتبارسنجی سمت کلاینت برنامه‌های Angular آشنا شدیم. اما اگر مدل سمت سرور ما یک چنین شکلی را داشته باشد که به همراه خطاهای اعتبارسنجی سفارشی نیز هست:
using System;
using System.ComponentModel.DataAnnotations;

namespace AngularTemplateDrivenFormsLab.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Movie Title is Required")]
        [MinLength(3, ErrorMessage = "Movie Title must be at least 3 characters")]
        public string Title { get; set; }

        [Required(ErrorMessage = "Movie Director is Required.")]
        public string Director { get; set; }

        [Range(0, 100, ErrorMessage = "Ticket price must be between 0 and 100.")]
        public decimal TicketPrice { get; set; }

        [Required(ErrorMessage = "Movie Release Date is required")]
        public DateTime ReleaseDate { get; set; }
    }
}
و همچنین کنترلر و اکشن متد دریافت کننده‌ی آن نیز به صورت ذیل تعریف شده باشد:
using AngularTemplateDrivenFormsLab.Models;
using Microsoft.AspNetCore.Mvc;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class MoviesController : Controller
    {
        [HttpPost]
        public IActionResult Post([FromBody]Movie movie)
        {
            if (ModelState.IsValid)
            {
                // TODO: save ...
                return Ok(movie);
            }

            ModelState.AddModelError("", "This record already exists."); // a cross field validation
            return BadRequest(ModelState);
        }
    }
}
دو نوع خطای اعتبارسنجی سمت سرور را به سمت کلاینت ارسال خواهیم کرد:
الف) خطاهای اعتبارسنجی در سطح فیلدها
زمانیکه return BadRequest(ModelState) صورت می‌گیرد، محتویات شیء ModelState به همراه status code مساوی 400 به سمت کلاینت ارسال خواهد شد. در شیء ModelState یک دیکشنری که کلیدهای آن، نام خواص و مقادیر متناظر با آن‌ها، خطاهای اعتبارسنجی تنظیم شده‌ی در مدل است، قرار دارند.
ب) خطاهای اعتبارسنجی عمومی
در این بین می‌توان دیکشنری ModelState را توسط متد AddModelError نیز تغییر داد و برای مثال کلید آن‌را مساوی "" تعریف کرد. در این حالت یک چنین خطایی به کل فرم اشاره می‌کند و نه به یک خاصیت خاص.

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

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



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

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


در این HttpErrorResponse دریافتی، دو خاصیت error که همان آرایه‌ی دیکشنری نام خواص و پیام‌های خطای مرتبط با هر کدام و status code دریافتی مهم هستند:
  errors: string[] = [];

  processModelStateErrors(form: NgForm, responseError: HttpErrorResponse) {
    if (responseError.status === 400) {
      const modelStateErrors = responseError.error;
      for (const fieldName in modelStateErrors) {
        if (modelStateErrors.hasOwnProperty(fieldName)) {
          const modelStateError = modelStateErrors[fieldName];
          const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
          if (control) {
            // integrate into Angular's validation
            control.setErrors({
              modelStateError: { error: modelStateError }
            });
          } else {
            // for cross field validations -> show the validation error at the top of the screen
            this.errors.push(modelStateError);
          }
        }
      }
    } else {
      this.errors.push("something went wrong!");
    }
  }

  lowerCaseFirstLetter(data: string): string {
    return data.charAt(0).toLowerCase() + data.slice(1);
  }
توضیحات:
در اینجا از آرایه‌ی errors برای نمایش خطاهای عمومی در سطح فرم استفاده می‌کنیم. این خطاها در ModelState، دارای کلید مساوی "" هستند. به همین جهت حلقه‌ای را بر روی شیء responseError.error تشکیل می‌دهیم. به این ترتیب می‌توان به نام خواص و همچنین خطاهای متناظر با آن‌ها رسید.
 const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
از نام خاصیت یا فیلد، جهت یافتن کنترل متناظر با آن، در فرم جاری استفاده می‌کنیم. ممکن است کنترل تعریف شده camel case و یا pascal case باشد. به همین جهت دو حالت بررسی را در اینجا مشاهده می‌کنید.
در ادامه اگر control ایی یافت شد، توسط متد setErrors، کلید جدید modelStateError را که دارای خاصیت سفارشی error است، تنظیم می‌کنیم. با اینکار سبب خواهیم شد تا خطای اعتبارسنجی دریافتی از سمت سرور، با سیستم اعتبارسنجی Angular یکی شود. به این ترتیب می‌توان این خطا را دقیقا ذیل همین کنترل در فرم نمایش داد. اگر کنترلی یافت نشد (کلید آن "" بود و یا جزو نام کنترل‌های موجود در آرایه‌ی form.controls نبود)، این خطا را به آرایه‌ی errors اضافه می‌کنیم تا در بالاترین سطح فرم قابل نمایش شود.

نحوه‌ی استفاده‌ی از متد processModelStateErrors فوق را در متد submitForm، در قسمت شکست عملیات ارسال اطلاعات، مشاهده می‌کنید:
  model = new Movie("", "", 0, "");
  successfulSave: boolean;
  errors: string[] = [];

  constructor(private movieService: MovieService) { }

  ngOnInit() {
  }

  submitForm(form: NgForm) {
    console.log(form);

    this.errors = [];
    this.movieService.postMovieForm(this.model).subscribe(
      (data: Movie) => {
        console.log("Saved data", data);
        this.successfulSave = true;
      },
      (responseError: HttpErrorResponse) => {
        this.successfulSave = false;
        console.log("Response Error", responseError);
        this.processModelStateErrors(form, responseError);
      });
  }


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

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



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

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


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


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
Blazor 5x - قسمت هشتم - مبانی Blazor - بخش 5 - تامین محتوای نمایشی کامپوننت‌های فرزند توسط کامپوننت والد
یک نکته‌ی تکمیلی: روش ارسال داده‌ها به RenderFragmentها

در مباحث اعتبارسنجی و احراز هویت Blazor در قسمت‌های بعدی، به قطعه کد context@ داری در داخل یک RenderFragment خواهیم رسید:
<AuthorizeView>
    <Authorized>
                Hello, @context.User.Identity.Name!
    </Authorized>
</AuthorizeView>
اکنون این سؤال مطرح است که این context@ از کجا آمده‌است و چه مفهومی را دارد؟
برای پاسخ به این سؤال نیاز است با مفهوم «Templated components» در برنامه‌های Blazor آشنا شد. تا اینجا از RenderFragmentها صرفا جهت فراهم آوردن قسمت ثابتی از قالب کامپوننت جاری، توسط استفاده کننده‌ی از آن، کمک گرفتیم. اما در همان سمت استفاده کننده، امکان دسترسی به اطلاعات مهیای داخل آن فرگمنت نیز وجود دارد. برای نمونه به کدهای کامپوننت TableTemplate.razor دقت کنید:
@typeparam Titem

<table class="table">
    <thead>
        <tr>@TableHeader</tr>
    </thead>
    <tbody>
        @foreach (var item in Items)
        {
            <tr>@RowTemplate(item)</tr>
        }
    </tbody>
</table>

@code {
    [Parameter]
    public RenderFragment TableHeader { get; set; }

    [Parameter]
    public RenderFragment<TItem> RowTemplate { get; set; }

    [Parameter]
    public IReadOnlyList<TItem> Items { get; set; }
}
- این کامپوننت، قالب سفارشی ثابت هدر جدول را توسط یک RenderFragment، از بکارگیرنده‌ی خود دریافت می‌کند.
- همچنین یک RenderFragment جنریک را نیز مشاهده می‌کنید که قالب ردیف‌های جدول را تامین می‌کند. نوع جنریک قابل دسترسی در این کامپوننت، توسط دایرکتیو typeparam Titem@ تعریف شده‌ی در ابتدای آن، مشخص شده‌است.
- بنابراین هربار که ردیفی از بدنه‌ی جدول در حال رندر است، یک شیء item از نوع TItem را به قالب سفارشی تامین شده‌ی توسط بکارگیرنده‌ی خود ارسال می‌کند.
اکنون این سؤال مطرح می‌شود که چگونه می‌توان به شیء item، در سمت والد یا بکارگیرنده‌ی کامپوننت TableTemplate فوق دسترسی یافت؟
برای اینکار می‌توان از پارامتر ویژه‌ای به نام context که یک implicit parameter است و به صورت پیش‌فرض تعریف شده و مهیا است، در سمت کدهای بکارگیرند‌ه‌ی کامپوننت، استفاده کرد:
@page "/pets"
<h1>Pets</h1>
<TableTemplate Items="pets">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td>@context.PetId</td>
        <td>@context.Name</td>
    </RowTemplate>
</TableTemplate>

@code {
    private List<Pet> pets = new()
    {
        new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
        new Pet { PetId = 4, Name = "Salem Saberhagen" },
        new Pet { PetId = 7, Name = "K-9" }
    };

    private class Pet
    {
        public int PetId { get; set; }
        public string Name { get; set; }
    }
}
- در اینجا روش بکارگیری کامپوننت TableTemplate را در کامپوننتی دیگر مشاهده می‌کنید. توسط فرگمنت TableHeader، قالب ثابت هدر این جدول تامین شده‌است. توسط فرگمنت RowTemplate قالب پویای ردیف‌های جدول ارائه شده‌است.
- همچنین در اینجا پارامتر ویژه‌ای به نام context@ را نیز مشاهده می‌کنید. این پارامتر همان شیء item ای است که در حین رندر هر ردیف جدول، به فرگمنت RowTemplate ارسال می‌شود. به این ترتیب کامپوننت والد می‌تواند از اطلاعات در حال رندر توسط کامپوننت فرگمنت دار، مطلع شود و از آن استفاده کند. در این مثال، نوع context@، از نوع کلاس Pet است که سعی شده بر اساس نوع پارامتر Items ارسالی به آن، توسط کامپایلر تشخیص داده شود. حتی می‌توان این نوع را به صورت صریحی نیز مشخص کرد:
<TableTemplate Items="pets" TItem="Pet">
این مورد زمانی مفید است که چندین پارامتر جنریک را در کامپوننت تعریف کرده باشیم:
<SomeGenericComponent TParam1="Person" TParam2="Supplier" TItem=etc/>
و یا می‌توان نام پارامتر ویژه‌ی context@ را در تمام RenderFragmentهای جنریک، با ذکر ویژگی Context برای مثال به pet تغییر داد:
<TableTemplate Items="pets" Context="pet">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate>
        <td>@pet.PetId</td>
        <td>@pet.Name</td>
    </RowTemplate>
</TableTemplate>
و یا حتی می‌توان این نام را به صورت اختصاصی به ازای هر RenderFragment جنریک، مشخص کرد. برای اینکار ویژگی Context را دقیقا بر روی RenderFragment مدنظر قرار می‌دهیم:
<TableTemplate Items="pets">
    <TableHeader>
        <th>ID</th>
        <th>Name</th>
    </TableHeader>
    <RowTemplate Context="pet">
        <td>@pet.PetId</td>
        <td>@pet.Name</td>
    </RowTemplate>
</TableTemplate>
این تغییرنام بهتر است زمانی صورت گیرد که نام پیش‌فرض context، با نام مشابه دیگری در کامپوننت جاری، تداخل پیدا کرده‌است.
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت دهم - کار با فرم‌ها - قسمت اول
هر برنامه‌ی وبی، نیاز به کار با فرم‌های وب را دارد و به همین جهت، AngularJS 2.0 به همراه دو نوع از فرم‌ها است: فرم‌های مبتنی بر قالب‌ها و فرم‌های مبتنی بر مدل‌ها.
کار با فرم‌های مبتنی بر قالب‌ها ساده‌تر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی داده‌های ورودی توسط کاربر، در اختیار ما قرار می‌دهند. اما فرم‌های مبتنی بر مدل‌ها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودی‌های کاربران، ارائه می‌دهند. در این قسمت فرم‌های مبتنی بر قالب‌ها (Template-driven forms) را بررسی می‌کنیم.


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

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

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

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

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

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

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

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


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

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


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

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

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

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



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

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

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


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



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

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

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

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


بررسی ngForm

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

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



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

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



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

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



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

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

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

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

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

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



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


خلاصه‌ی بحث

فرم‌های template driven در AngularJS 2.0 به این نحو طراحی می‌شوند:
 1) ابتدا فرم HTML را به حالت معمولی آن طراحی می‌کنیم؛ با تمام المان‌های آن.
 2) به تمام المان‌های فرم، دیراکتیو ngControl را متصل می‌کنیم، تا AngularJS 2.0 آن‌را تبدیل به یک کنترل خاص خودش کند. کنترلی که دارای خواصی مانند valid و touched است.
 3) سپس برای دسترسی به این کنترل ایجاد شده‌ی به صورت ضمنی، یک متغیر محلی آغاز شده‌ی با # را به تمام المان‌ها اضافه می‌کنیم.
 4) اعتبارسنجی‌هایی را مانند required  به المان‌های فرم اضافه می‌کنیم.
 5) از متغیر محلی تعریف شده و ngIf* برای بررسی خواصی مانند valid و touched برای نمایش خطاهای اعتبارسنجی کمک گرفته می‌شود.
 6) پس از تعریف فرم، تعریف ngControlها، تعریف متغیر محلی شروع شده‌ی با # و افزودن خطاهای اعتبارسنجی، اکنون نوبت به ارسال این اطلاعات به سرور است. بنابراین رخداد ngSubmit را باید به متدی در کلاس کامپوننت جاری متصل کرد.
 7) اکنون که با کلیک بر روی دکمه‌ی submit فرم، متد onSubmit متصل به ngSubmit فراخوانی می‌شود، نیاز است بین المان‌های فرم HTML و کلاس کامپوننت، ارتباط برقرار کرد. این‌کار را توسط two-way data binding و تعریف ngModel بر روی تمام المان‌های فرم، انجام می‌دهیم. این ngModelها، به یک خاصیت عمومی که متناظر است با وهله‌ای از شیء مدل فرم، متصل هستند. بنابراین این مدل، در هر لحظه، بیانگر آخرین تغییرات کاربر است و از آن می‌توان برای ارسال اطلاعات به سرور استفاده کرد.
 8) پس از اتصال فرم به کلاس متناظر با آن، اکنون سرویس محصولات را تکمیل کرده و به آن متد HTTP Post را جهت ارسال اطلاعات سمت کاربر، به سرور، اضافه می‌کنیم. در اینجا نکته‌ی مهم، تنظیم content type ارسالی به سمت سرور است. در غیراینصورت فریم ورک سمت سرور قادر به تشخیص JSON بودن این اطلاعات نخواهد شد.
مطالب
نکته‌ای در استفاده از AutoMapper
مدل Student را به شکل زیر در نظر بگیرید
public class Student
   {
       public int Id { get; set; }
       public string Name { get; set; }
       public string Family { get; set; }
       public DateTime Birthdate { get; set; }
       public string Tel { get; set; }
       public string CellPhone { get; set; }
       [Email]
       public string Email { get; set; }
   }
: آن را به صورت زیر ViewModel 
 public class StudentViewModel
    {      
        public string Name { get; set; }
        public string Family { get; set; }
        public string Email { get; set; }
    }
برای نگاشت مجموعه‌ای از مدل Student به مجموعه‌ای از مدل StudentViewModel داریم:
public ActionResult Index()
   {
      var model = db.Students.ToList();
      AutoMapper.Mapper.CreateMap<Student,StudentViewModel>();
      var studentViewModel = AutoMapper.Mapper.Map<List<Student>, IEnumerable<StudentViewModel>>(model);
      return View(studentViewModel);
    }
که اگر دستور 
 AutoMapper.Mapper.CreateMap<Student,StudentViewModel>();
:را استفاده نکنیم، خطای زیر اتفاق می‌افتد
An exception of type 'AutoMapper.AutoMapperMappingException' occurred in AutoMapper.dll but was not handled in user code
مطالب
روش آپلود فایل‌ها به همراه اطلاعات یک مدل در برنامه‌های Blazor WASM 5x
از زمان Blazor 5x، امکان آپلود فایل به صورت استاندارد به Blazor اضافه شده‌است که نمونه‌ی Blazor Server آن‌را پیشتر در مطلب «Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر» مطالعه کردید. در تکمیل آن، روش آپلود فایل‌ها در برنامه‌های WASM را نیز بررسی خواهیم کرد. این برنامه از نوع hosted است؛ یعنی توسط دستور dotnet new blazorwasm --hosted ایجاد شده‌است و به صورت خودکار دارای سه بخش Client، Server و Shared است.



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

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

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

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

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

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


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

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

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

    public class FilesManagerService : IFilesManagerService
    {
        private readonly HttpClient _httpClient;

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

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

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

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

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

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

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

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

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

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

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

            // ...
        }
    }
}


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

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

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

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

@inject IFilesManagerService FilesManagerService

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmUpload.zip
بازخوردهای دوره
تزریق وابستگی‌ها در فیلترهای ASP.NET MVC
در مورد فیلترهای سراسری، حلقه‌ی زیر در کلاس StructureMapFilterProvider فراخوانی نخواهد شد:
var filters = base.GetFilters(controllerContext, actionDescriptor);
foreach (var filter in filters)
بنابراین container.BuildUp ایی هم بر روی فیلتر در حال اجرا، فراخوانی نمی‌شود و وابستگی‌های آن تامین نخواهند شد.
برای حل این مشکل، بجای روش معمول معرفی فیلترهای سراسری:
 GlobalFilters.Filters.Add(new LogAttribute());
بنویسید:
 GlobalFilters.Filters.Add(SmObjectFactory.Container.GetInstance<LogAttribute>());
در این حالت، در ابتدای کار برنامه، تمام وابستگی‌های مرتبط با LogAttribute هم وهله سازی می‌شوند.

مشکل!
این وهله سازی، فقط یکبار آن هم در ابتدای برنامه انجام می‌شود. یعنی وابستگی‌های استفاده شده‌ی در فیلتر سراسری، صرفنظر از طول عمر تعریف شده‌ی برای آن‌ها توسط IoC Container، دیگر وهله سازی مجدد نخواهند شد و این مساله برای حالت‌هایی مانند کار با دیتابیس مشکل ساز است.
برای حل این مشکل، اینترفیس IContainer را به فیلتر تزریق کنید:
public class LogAttribute : ActionFilterAttribute
{
    private readonly IContainer _container;
 
    //نباید به این صورت تعریف شود چون در فیلترهای سراسری فقط یکبار وهله سازی خواهد شد
    //public ILogActionService LogActionService { get; set; }
 
    public LogAttribute(IContainer container)
    {
        _container = container;
    }
 
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        _container.GetInstance<ILogActionService>().Log("......data......");
        //LogActionService.Log("......data......");
        base.OnActionExecuted(filterContext);
    }
}
در این حالت هرچند کلاس LogAttribute ایی که به صورت فیلتر سراسری تعریف شده‌است، یکبار در آغاز کار برنامه وهله سازی می‌شود، اما وابستگی‌های مورد نیاز آن، توسط container.GetInstance به ازای هر بار فراخوانی، مجددا ساخته خواهند شد و دیگر تک وهله‌ای نخواهند بود.

روش بهتر transient کردن وابستگی‌ها: استفاده از Func