مطالب
استفاده از لنگر (anchor) برای اسکرول به قسمت خاصی از صفحه در Blazor Server
فرض کنید کدی مانند زیر را در یک کامپوننت داریم و انتظار این است که با کلیک بر روی Section2، به بخش مورد نظر اسکرول شویم:
@page "/test"

<nav>
    <!-- یک روش -->
    <a href="#section2">Section2</a>

    <!-- روش دیگر -->
    <NavLink href="#section2">Section2</NavLink>    
</nav>

@* ... *@


<h2 id="section2">It's Section2.</h2>
@* ... *@
اما متاسفانه در Blazor Server تا نسخه فعلی آن (نسخه هفت)، این کار ساده به راحتی امکان‌پذیر نیست. همانطور که ملاحظه می‌کنید، به دو روش، نویگیشن انجام شده‌است؛ اما هیچ‌یک ما را به هدف نمی‌رسانند. دلیل این موضوع، رفتار Blazor Server در بارگذاری صفحات می‌باشد. در حقیقت المان‌ها موقع بارگذاری، هنوز در صفحه وجود ندارند. در واقع ابتدا نیاز است که اتصال SignalR برقرار شود و سپس داده‌ها از سرور دریافت شوند (مگر در حالت pre-rendered که مشکلات خاص خود را در پی دارد).
برای انجام این کار دو روش وجود دارد؛ یکی بر پایه‌ی جاوااسکریپت است و دیگری توسط توابع داخلی Blazor JS.


روش جاوااسکریپتی

ابتدا یک کامپوننت را به نام AnchorNavigation ایجاد می‌نماییم:
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@implements IDisposable
@code {
    protected override void OnInitialized()
    {
        NavigationManager.LocationChanged += OnLocationChanged;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await ScrollToFragment();
    }

    public void Dispose()
    {
        NavigationManager.LocationChanged -= OnLocationChanged;
    }

    private async void OnLocationChanged(object sender, LocationChangedEventArgs e)
    {
        await ScrollToFragment();
    }

    private async Task ScrollToFragment()
    {
        var uri = new Uri(NavigationManager.Uri, UriKind.Absolute);
        var fragment = uri.Fragment;
        if (fragment.StartsWith('#'))
        {
            // Handle text fragment (https://example.org/#test:~:text=foo)
            // https://github.com/WICG/scroll-to-text-fragment/
            var elementId = fragment.Substring(1);
            var index = elementId.IndexOf(":~:", StringComparison.Ordinal);
            if (index > 0)
            {
                elementId = elementId.Substring(0, index);
            }

            if (!string.IsNullOrEmpty(elementId))
            {
                await JSRuntime.InvokeVoidAsync("BlazorScrollToId", elementId);
            }
        }
    }
}
سپس کد جاوا اسکریپتی زیر را در جایی قبل از فراخوانی <script src="_framework/blazor.server.js"></script> قرار می‌دهیم (برای مثال اگر می‌خواهیم در اکثر صفحات از آن بهره ببریم، آن را در layout.cshtmlـ قرار می‌دهیم).
function BlazorScrollToId(id) {
            const element = document.getElementById(id);
            if (element instanceof HTMLElement) {
                element.scrollIntoView({
                    behavior: "smooth",
                    block: "start",
                    inline: "nearest"
                });
            }
        }
حال در هر کامپوننتی که نیاز به استفاده از لنگر (anchor) داریم، به شکل زیر عمل می‌کنیم:
@page "/"

<PageTitle>Index</PageTitle>

<a href="#section2">
    <h1>Section2</h1>
</a>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<div id="section2">
    <h2>It's Section2. </h2>
</div>

<AnchorNavigation />


روش استفاده از توابع داخلی Blazor JS 

می توان از ElementReference و FocusAsync که در حقیقت مربوط به خود Blazor JS می‌باشند استفاده نمود. اینبار کدهای کامپوننت AnchorNavigation را به شکل زیر تغییر می‌دهیم:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using System.Diagnostics.CodeAnalysis;

namespace TestAnchorNavigation;

public class AnchorNavigation: ComponentBase, IDisposable
{
    private bool _setFocus;

    [Inject] private NavigationManager NavManager { get; set; } = default!;
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter] public string? BookmarkName { get; set; }
    [DisallowNull] public ElementReference? Element { get; private set; }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(0, "span");
        builder.AddAttribute(2, "tabindex", "-1");
        builder.AddContent(3, this.ChildContent);
        builder.AddElementReferenceCapture(4, this.SetReference);
        builder.CloseElement();
    }

    protected override void OnInitialized()
        => NavManager.LocationChanged += this.OnLocationChanged;

    protected override void OnParametersSet()
        => _setFocus = this.IsMe();

    private void SetReference(ElementReference reference)
        => this.Element = reference;

    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        if (this.IsMe())
        {
            _setFocus = true;
            this.StateHasChanged();
        }
    }

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (_setFocus)
            await this.Element!.Value.FocusAsync(false);

        _setFocus = false;
    }

    private bool IsMe()
    {
        string? elementId = null;

        var uri = new Uri(this.NavManager.Uri, UriKind.Absolute);
        if (uri.Fragment.StartsWith('#'))
        {
            elementId = uri.Fragment.Substring(1);
            return elementId == BookmarkName;
        }
        return false;
    }

    public void Dispose()
        => NavManager.LocationChanged -= this.OnLocationChanged;
}
و پیرو آن، صفحه‌ی موردنظر برای استفاده از لنگر نیز به شکل زیر تغییر خواهد کرد:
@page "/"

<PageTitle>Index</PageTitle>

<NavLink href="#section2">
    <h1>Section2</h1>
</NavLink>

<SurveyPrompt Title="How is Blazor working for you?" />

<div style="height: 2000px">

</div>

<AnchorNavigation BookmarkName="section2">
      <h2>It's Section2. </h2>
</AnchorNavigation>
مطالب
استفاده از لوسین برای انجام محاسبات آماری بر روی متون
احتمالا یک سری از کارهای اینفوگرافیک مانند tags cloud و words cloud را دیده‌اید. برای مثال در یک سخنرانی خاص، سخنران بیشتر از چه واژه‌هایی استفاده کرده است و سپس ترسیم درشت‌تر واژه‌هایی با تکرار بیشتر در یک تصویر نهایی. محاسبات آماری این نوع بررسی‌ها را توسط لوسین نیز می‌توان انجام داد که در ادامه به نحوه انجام آن خواهیم پرداخت.

بررسی آماری واژه‌های بکار رفته در شاهنامه

مرحله اول: ایجاد ایندکس

using System;
using System.Collections.Generic;
using System.IO;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Store;

namespace ShaahnamehAnalysis
{
    public static class CreateIndex
    {
        static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_CURRENT;

        static HashSet<string> getStopWords()
        {
            var result = new HashSet<string>();
            var stopWords = new[]
            {
                "به",
                "با",
                "از",
                "تا",
                "و",
                "است",
                "هست",
                "هستم",
                "هستیم",
                "هستید",
                "هستند",
                "نیست",
                "نیستم",
                "نیستیم",
                "نیستند",
                "اما",
                "یا",
                "این",
                "آن",
                "اینجا",
                "آنجا",
                "بود",
                "باد",
                "برای",
                "که",
                "دارم",
                "داری",
                "دارد",
                "داریم",
                "دارید",
                "دارند",
                "چند",
                "را",
                "ها",
                "های",
                "می",
                "هم",
                "در",
                "باشم",
                "باشی",
                "باشد",
                "باشیم",
                "باشید",
                "باشند",
                "اگر",
                "مگر",
                "بجز",
                "جز",
                "الا",
                "اینکه",
                "چرا",
                "کی",
                "چه",
                "چطور",
                "چی",
                "چیست",
                "آیا",
                "چنین",
                "اینچنین",
                "نخست",
                "اول",
                "آخر",
                "انتها",
                "صد",
                "هزار",
                "میلیون",
                "ملیون",
                "میلیارد",
                "ملیارد",
                "یکهزار",
                "تریلیون",
                "تریلیارد",
                "میان",
                "بین",
                "زیر",
                "بیش",
                "روی",
                "ضمن",
                "همانا",
                "ای",
                "بعد",
                "پس",
                "قبل",
                "پیش",
                "هیچ",
                "همه",
                "واما",
                "شد",
                "شده",
                "شدم",
                "شدی",
                "شدیم",
                "شدند",
                "یک",
                "یکی",
                "نبود",
                "میکند",
                "میکنم",                
                "میکنیم",
                "میکنید",
                "میکنند",
                "میکنی",
                "طور",
                "اینطور",
                "آنطور",
                "هر",
                "حال",
                "مثل",
                "خواهم",
                "خواهی",
                "خواهد",
                "خواهیم",
                "خواهید",
                "خواهند",
                "داشته",
                "داشت",
                "داشتی",
                "داشتم",
                "داشتیم",
                "داشتید",
                "داشتند",
                "آنکه",
                "مورد",
                "کنید",
                "کنم",
                "کنی",
                "کنند",
                "کنیم",
                "نکنم",
                "نکنی",
                "نکند",
                "نکنیم",
                "نکنید",
                "نکنند",
                "نکن",
                "بگو",
                "نگو",
                "مگو",
                "بنابراین",
                "بدین",
                "من",
                "تو",
                "او",
                "ما",
                "شما",
                "ایشان",
                "ی",
                "ـ",
                "هایی",
                "خیلی",
                "بسیار",
                "1",
                "بر",
                "l",
                "شود",
                "کرد",
                "کرده",
                "نیز",
                "خود",
                "شوند",
                "اند",
                "داد",
                "دهد",
                "گشت",
                "ز",
                "گفت",
                "آمد",
                "اندر",
                "چون",
                "بد",
                "چو",
                "همی",
                "پر",
                "سوی",
                "دو",
                "گر",
                "بی",
                "گرد",
                "زین",
                "کس",
                "زان",
                "جای",
                "آید"
            };

            foreach (var item in stopWords)
                result.Add(item);

            return result;
        }

        public static void CreateShaahnamehIndex(string file = "shaahnameh.txt")
        {
            var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
            var analyzer = new StandardAnalyzer(_version, getStopWords());
            using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
            {
                var section = string.Empty;
                foreach (var line in File.ReadAllLines(file))
                {
                    int result;
                    if (int.TryParse(line, out result))
                    {
                        var postDocument = new Document();
                        postDocument.Add(new Field("Id", result.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
                        postDocument.Add(new Field("Body", section, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
                        writer.AddDocument(postDocument);
                        section = string.Empty;
                    }
                    else
                        section += line;
                }

                writer.Optimize();
                writer.Commit();
                writer.Close();
                directory.Close();
            }
        }
    }
}

با ایجاد ایندکس‌های لوسین پیشتر در این سایت آشنا شده‌اید . روش کار نیز همانند سابق است. اطلاعات خود را، به هر فرمتی که تهیه شده باید تبدیل به اشیاء Document لوسین کرد. برای مثال در اینجا فقط یک فایل txt داریم که تشکیل شده است از تمام صفحات. به ازای هر صفحه، یک شیء Document تهیه و نوشته خواهد شد. همچنین در تهیه ایندکس از یک سری از واژه‌‌های بسیار متداول مانند «از»، «به»، «اندر»، (stopWords) صرفنظر شده است.


مرحله دوم: ایجاد ابر واژه‌ها

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Lucene.Net.Index;
using Lucene.Net.Store;

namespace ShaahnamehAnalysis
{
    [DebuggerDisplay("{Frequency}, {Text}")]
    public class Tag
    {
        public string Text { set; get; }

        /// <summary>
        /// The frequency of a term is defined as the number of 
        /// documents in which a specific term appears.
        /// </summary>
        public int Frequency { set; get; }
    }

    public static class WordsCloud
    {
        /// <summary>
        /// Create Words Cloud
        /// </summary>
        /// <param name="threshold">every term that appears in more than x Body</param>
        public static IList<Tag> Create(int threshold = 200)
        {
            var path = Environment.CurrentDirectory + "\\LuceneIndex";

            var results = new List<Tag>();
            var field = "Body";

            IndexReader indexReader = IndexReader.Open(FSDirectory.Open(path ), true);

            var termFrequency = indexReader.Terms();
            while (termFrequency.Next())
            {
                if (termFrequency.DocFreq() >= threshold && termFrequency.Term.Field == field)
                {
                    results.Add(new Tag { Text = termFrequency.Term.Text, Frequency = termFrequency.DocFreq() });
                }
            }
            return results.OrderByDescending(x => x.Frequency).ToList();
        }
    }
}

پس از اینکه ایندکس لوسین تهیه شد، می‌توان به مداخل موجود در آن توسط متد indexReader.Terms دسترسی یافت.
نکته جالب آن فراهم بودن DocFreq هر واژه ایندکس شده است (فرکانس تکرار واژه؛ تعداد اشیاء Document ایی که واژه مورد نظر در آن‌ها تکرار شده است). برای مثال در اینجا اگر واژه‌ای 200 بار یا بیشتر در صفحات مختلف شاهنامه تکرار شده باشد، به عنوان یک واژه پر اهمیت انتخاب شده و به ابر واژه‌های نهایی اضافه می‌گردد.


مرحله سوم: استفاده از نتایج

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;

namespace ShaahnamehAnalysis
{
    class Program
    {
        static void Main(string[] args)
        {
            CreateIndex.CreateShaahnamehIndex();
            var wordsCloudList = WordsCloud.Create();

            var data = wordsCloudList.Select(x => x.Text + ", " + x.Frequency)
                                     .Aggregate((s1, s2) => s1 + Environment.NewLine + s2);
            var output = "ShaahnamehAnalysis.txt";
            File.WriteAllText(output, data);
            Process.Start(output);
        }
    }
}

که نتیجه 15 مورد اول آن به صورت زیر است:
واژه |  فرکانس
شاه, 1191
دل, 1088
سر, 1070
کار, 840
لشکر, 801
تخت, 755
روز, 745
ایران, 740
جهان, 724
مرد, 660
دست, 630
تاج, 623
نزدیک, 623
گیتی, 585
راه, 584


فایل‌های کامل این مثال را از اینجا می‌توانید دریافت کنید:
ShaahnamehAnalysis.zip

مطالب
واکشی اطلاعات به صورت chunk chunk (تکه تکه) و نمایش در ListView
یکی از مسائلی که در هنگام کار با کنترلهای داده‌ای نظیر ListView , GridView و .. با آن روبرو هستیم مسئله صفحه بندی می‌باشد و در بسیاری از موارد، کل اطلاعات در هر درخواست، بارگذاری میشود. در حالیکه روش بهینه به این صورت است که با توجه به PageSize و Index رکورد، می‌توان تعداد رکورد مورد نظر در همان صفحه را بارگذاری کرد، نه کل رکوردها را.
در این مثال که از Ef Code First و الگوی Unit Of Work استفاده کرده ام ، قصد نمایش اطلاعات در یک ListView و صفحه بندی آن را بصورت chunk chunk دارم.
برای آشنایی بیشتر با الگوی Unit Of Work  می‌توانید به مقاله UOW در همین سایت مراجعه کنید.
   
ابتدا یک کنترل ListView روی صفحه ایجاد می‌کنیم. برای آشنایی بیشتر با این کنترل و بررسی قابلیتهای آن میتوان از این مقاله معرفی کنترل‌های ListView و DataPager استفاده کنید.

سپس با توجه به الگوی Unit Of Work از یک مدل ساده استفاده می‌کنیم. همچنین برای بارگذاری اطلاعات به صورت صفحه به صفحه، نیاز به داشتن Index رکورد و PageSize، جهت محاسبه تعداد رکورد مورد نیاز داریم.

public class User
    {       
        public Int64 UserId { get; set; }       
        public String UserName { get; set; }
     }        
 public interface IUserService
    {        
        int GetCustomerCount();
        List<User> GetCustomers(int StartIndex, int PageSize);
    }
public class ImplUserService : IUserService
    {
        IUnitOfWork _uow;
        IDbSet<User> _user;

        public ImplUserService(IUnitOfWork uow)
        {
            _uow = uow;
            _user = uow.Set<User>();
        }

        public int GetCustomerCount()
        {
            int totalCount = _user.ToList().Count;             
            return totalCount;
        }

        public List<User> GetCustomers(int StartIndex, int PageSize)
        {
            return _user.OrderBy(i => i.UserId).Skip(StartIndex).Take(PageSize).ToList();
        }
  }
در کدهای بالا متد GetCustomerCount تعداد کل رکوردهایی که باید واکشی شوند را مشخص می‌کند. همچنین می‌توان این تعداد را در cache یا viewState ذخیره کرد تا در دفعات بعدی در صورت خالی نبودن مقدار Cacheبا viewState ، نیاز به محاسبه مجدد count نداشته باشیم.

متد GetCustomer که کار اصلی را انجام میدهد از دو پارامتر استفاده می‌کند: StartIndex نقطه شروع و PageSize تعداد رکورد مورد نظر. در اینجا از دستورات Linq استفاده شده و دستور Skip مشخص میکند از کدام شماره رکورد به بعد شروع به واکشی نماید و دستور Take مشخص می‌کند که چه تعداد رکورد را واکشی نماید.

حالا به سراغ کدهای HTML می‌رویم. در آنجا علاوه بر ListView نیاز به DataPager جهت صفحه بندی و Object DataSource جهت کنترل بارگذاری اطلاعات به صورت chunk chunk داریم.
<asp:ListView ID="ListView1" runat="server" DataSourceID="ObjectDataSource1">
            <ItemTemplate>
                <tr style="background-color: #DCDCDC; color: #000000;">
                    <td>
                        <asp:Label ID="UserIdLabel" runat="server" Text='<%# Eval("UserId") %>' />
                    </td>
                    <td>
                        <asp:Label ID="UserNameLabel" runat="server" Text='<%# Eval("UserName") %>' />
                    </td>
                </tr>
            </ItemTemplate>
            
            
            <LayoutTemplate>
                <table runat="server">
                    <tr runat="server">
                        <td runat="server">
                            <table id="itemPlaceholderContainer" runat="server" border="1" style="background-color: #FFFFFF;
                                border-collapse: collapse; border-color: #999999; border-style: none; border-width: 1px;">
                                <tr runat="server" style="background-color: #DCDCDC; color: #000000;">
                                    <th runat="server">
                                        UserId
                                    </th>
                                    <th runat="server">
                                        UserName
                                    </th>
                                </tr>
                                <tr id="itemPlaceholder" runat="server">
                                </tr>
                            </table>
                        </td>
                    </tr>
                    <tr runat="server">
                        <td runat="server" style="text-align: center; background-color: #CCCCCC;
                            color: #000000;">
                            <asp:DataPager ID="DataPager1" runat="server" PageSize="2">
                                <Fields>
                                    <asp:NextPreviousPagerField ButtonType="Button" ShowFirstPageButton="True" ShowNextPageButton="False"
                                        ShowPreviousPageButton="False" />
                                    <asp:NumericPagerField />
                                    <asp:NextPreviousPagerField ButtonType="Button" ShowLastPageButton="True" ShowNextPageButton="False"
                                        ShowPreviousPageButton="False" />
                                </Fields>
                            </asp:DataPager>
                        </td>
                    </tr>
                </table>
            </LayoutTemplate>                        
        </asp:ListView>
همانظور که مشاهده میکنید در DataPager ، مقدار PageSize مشخص شده است.اما Object DataSource
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" EnablePaging="True" SelectCountMethod="GetCustomerCount"
            SelectMethod="GetCustomers" TypeName="UserService.ImplUserService" EnableViewState="False"
            MaximumRowsParameterName="PageSize" StartRowIndexParameterName="StartIndex">
که فکر می‌کنم با توجه به متدهای تعریف شده در بالا ، تعریف Object DataSource کاملاٌ گویا میباشد.

نکته مهم:
اگر الان برنامه را اجرا کنید با خطای No parameterless constructor defined for this object ر وبرو خواهید شد که جهت حل این مشکل از کد زیر استفاده میکنیم.
protected void ObjectDataSource1_ObjectCreating(object sender, ObjectDataSourceEventArgs e)
        {            
            e.ObjectInstance = ObjectFactory.GetInstance<IUserService>();            
        }
در واقع نیاز است تا یک وهله از کلاس مورد نظر را به Object DataSource معرفی کنیم.
حال با اجرای برنامه و Trace آن متوجه خواهید شد که با کلیک بر روی شماره صفحه، تنها به تعداد رکوردهای همان صفحه، واکشی خواهیم داشت.

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

مطالب
ایجاد BootstrapSwitch در MVC
در  مقاله‌ی قبلی ما بخشی از BootstrapDialog را با استفاده از Reflection  پیاده سازی کردیم. دلیل اینکه پیاده سازی کاملی از آن نداشتیم، متغیر بودن مقادیر و پیچیده‌تر شدن و طولانی تر شدن کد نویسی آن بود که برای آن کد ارزش زیادی نداشت تا وقت بیشتری صرف شود. ولی در اینجا بخاطر پیچیدگی کمتر، به طور کامل از Reflection استفاده شده است.
شیء BootstrapSwitch یک چک باکس است که با استفاده از جی کوئری و استایل‌ها به یک سوئیچ انیمیشنی زیبا تبدیل شده است که خودم به شخصه علاقه زیادی به استفاده‌ی از آن در پروژه‌های شخصی پیدا کرده‌ام. غیر از زیبایی، حس خوبی از کارکرد برنامه میدهد.
فایل‌های موردنیاز را دانلود کرده و آن‌ها را در ابتدای صفحه و با رعایت ترتیب صدا بزنید:
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> 
  <link href="~/content/css/bootstrap-switch.min.css" rel=stylesheet"></link>
<script src="~/Scripts/bootstrap-switch.min.js"></script>
نکته مهم: فایل css شامل دو نسخه هست که یکی از آن برای Bootstrap2 و دیگری برای نسخه 3 آن است که نسبت به آن نسخه، استایل مناسب را انتخاب کنید.
پروژه‌ی اصلی را دریافت کنید و آن را به solution خود اضافه کنید. پروژه به دو بخش اصلی Controls و Models تقسیم می‌شود که بخش مدل آن، برای ایجاد ساختارهای آن و در بخش کنترل، برای ترسیم آن به صورت HtmlHelper به کار می‌رود.
ابتدا قبل از هر چیزی یک شیء از کلاس BootstrapSwitchModel ایجاد کنید و مقادیر دلخواه خود را به خصوصیت‌های آن نسبت دهید:
var model=BootstrapSwitchModel();

//وضعیت فعال بودن و غیرفعال بودن سوئیچ
model.Checked=true;

//اندازه آن
model.Size=BootstrapSize..normal;

//یک انیمیشن ساده موقع سوئیچ کردن دارد
model.Animate=true;

//به چک باکس عادی تبدیل میشود
model.Disabled=true;

//غیرفعال شده و به صورت فقط خواندنی قابل دسترس است
model.Readonly=true;

//رنگ قعال بودن
model.OnColor=BootstrapColor.Success;

//رنگ غیرفعال بودن
model.OffColor=BootstrapColor.Danger;

//متن نمایشی در هنگام فعال بودن
model.OnText="On";

//متن نمایشی در حالت عدم انتخاب
model.OffText="Off";

//بین دو حالت روشن و خاموش نمایش داده میشود
model.label="Public Display";

//تعیین میزان اندازه برچسب  بالا
model.LabelWidth=100;

//سوئیچ به صورت آینه ای معکوس میشود
model.Inverse=false;

//کلاسی جهت تغییر استایل سوئیچ
model.BaseClass="myclass";

//تعیین کلاس برای تگ اصلی پدر
model.WrapperClass="wclass";

//فقط یکی از چند سوئیچ میتواند فعال باشد
model.RadioAllOff=false;

//یک سوئیچ در حالت عادی فقط یکی از
//وضعیت‌ها را نمایش میده ولی در این حالت
//سوئیچ در ابتدا بین این دو وضعیت گیر کرده است
model.Indeterminate=true;

//اندازه سمت چپ و راست سوئیچ
model.HandleWidth=25;
برای ترسیم آن در یک ویو هم به صورت زیر عمل کنید:
@{
var model=BootstrapSwitchModel();
....}

@HTML.BootstrapSwitch("id",model);
برای اطلاع از رویدادهای این کنترل، مستندات آن را مطالعه کنید و از id برای ارتباط با آن استفاده می‌کنند.
مطالب
آموزش فایرباگ - #1
فایرباگ نام ابزاری است که همه ما نام آن را شنیده ایم یا با آن کار کرده ایم .
این ابزار امکانات و قابلیت‌های فراوانی دارد که فراگیری آنها برای یک توسعه گر یا طراح وب سایت ضروری به نظر می‌رسد .
در سری مقالات آموزش فایرباگ ، با این ابزار محبوب و پرکاربرد بیشتر آشنا می‌شویم .


نصب و اجرای فایرباگ :
ابتدا وارد این صفحه شوید و ورژن مناسب مرورگرتان را نصب نمایید .
پس از نصب و دوباره باز کردن مرورگر ، میتوانید با کلیک کردن بروی آیکون فایرباگ یا فشردن کلید F12 ، فایرباگ را اجرا کنید .


تب‌ها :
فایرباگ در حالت پیشفرض دارای 6 تب می‌باشد .
  • Console : در این تب دو بخش وجود دارد :
    در بخش Log هشدار‌ها ، پیغام‌ها ، درخواست‌های XHR و ... نمایش داده می‌شوند .
    بخش دیگر هم که در سمت راست قرار دارد ، مخصوص اجرای کدهای جاوا اسکریپت می‌باشد .



  • HTML : در این تب HTML صفحه را بصورت درختی و بصورت Live مشاهده می‌کنید .
    Live بودن به این معنی است دقیقا چیزی را مشاهده می‌کنید که مرورگر در حال تفسیر هست و نه چیزی که به مرورگر ارسال شده .
    همچنین در این تب می‌توانید محتوای HTML قسمت Body صفحه را ویرایش کنید .


  • CSS : در این تب می‌توانید تعاریف CSS موجود در صفحه را مشاهده/ویرایش/حذف/فعال-غیرفعال نمایید .
    همچنین می‌توانید تمام فایل‌های استایلی که به صفحه اضافه شده اند را مشاهده و تعیین کنید که تعاریف موجود در کدام فایل‌ها در این محیط نمایش داده بشوند .




  • Script : در این تب می‌توانید به خطایابی کدهای جاوا اسکریپت بپردازید . همچنین مانند تب CSS می‌توانید یکی از فایل‌های اضافه شده به صفحه برای نمایش در این بخش ، چند مورد را انتخاب کنید .



  • DOM : در این تب می‌توانید ساختار DOM صفحه جاری را مشاهده نمایید .


  • Net : در این تب می‌توانید درخواست هایی که از صفحه ارسال می‌شوند را زیر نظر بگیرید .
    این تب اطلاعات کاملی درباره‌ی هر درخواست ارائه می‌کند .
    از جمله : نوع درخواست ( Post/Get ) ، حجم درخواست و پاسخ ، مدت زمان درخواست ، تعداد درخواست‌های ارسال شده ، وضعیت درخواست‌ها و ...



در قسمت‌های بعدی هر تب را به صورت جداگانه بررسی می‌کنیم .
اشتراک‌ها
بوت استرپ 4 الفا 2 منتشر شد

Here’s a look at a handful of the changes since our last alpha:

  • Overhauled spacing utilities to use a numerical tiering (to avoid confusion with grid tiers).
  • Continued refactoring efforts to replace markup-specific selectors with classes across several components (including pagination, lists, and more). Still more to do here with additional components.
  • Reverted media queries and grid containers from rems to pixels as viewports are not affected by font-size. See #17403 for details. We’ve got a ton of grid work left, too. Feel free to follow along with#18471.
  • Reverted .0625rem width borders to 1px for more consistent component borders that avoid zoom and font-size bugs across browsers.
  • Renamed .img-responsive to .img-fluid to avoid future confusion on the various responsive image solutions out there.
  • Replaced ZeroClipboard with clipboard.js for Flash-independent copy buttons.
  • Inputs and buttons now share the same border variable to ensure components are always sized similarly.
  • Updated all pseudo-element selectors to use the spec’s preferred double colon (e.g., ::before as opposed to :before).
  • Cards now have outline variants and mixins to support extending base classes further.
  • Utility classes for floats and text alignment now have responsive ranges. This means we’ve dropped the non-responsive classes to avoid duplication.
  • Added support for jQuery 2.
  • And hundreds more Sass improvements, bug fixes, documentation updates, and more. 
بوت استرپ 4 الفا 2 منتشر شد
مطالب
طراحی ValidationAttribute دلخواه و هماهنگ سازی آن با ASP.NET MVC
در سری پست‌های آقای مهندس یوسف نژاد با عنوان Globalization در ASP.NET MVC روشی برای پیاده سازی کار با Resource‌ها در ASP.NET با استفاده از دیتابیس شرح داده شده است. یکی از کمبودهایی که در این روش وجود داشت عدم استفاده از این نوع Resourceها از طریق Attributeها در ASP.NET MVC بود. برای استفاده از این روش در یک پروژه به این مشکل برخورد کردم و پس از تحقیق و بررسی چند پست سرانجام در این پست  پاسخ خود را پیدا کرده و با ترکیب این روش با روش آقای یوسف نژاد موفق به پیاده سازی Attribute دلخواه شدم.
در این پست و با استفاده از سری پست‌های آقای مهندس یوسف نژاد  در این زمینه، یک Attribute جهت هماهنگ سازی با سیستم اعتبار سنجی ASP.NET MVC در سمت سرور و سمت کلاینت (با استفاده از jQuery Validation) بررسی خواهد شد.

قبل از شروع مطالعه سری پست‌های MVC و Entity Framework الزامی است.

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

using System;

public class SampleModel
{
public DateTime StartDate { get; set; }
public string Data { get; set; }
public int Id { get; set; }
با استفاده از این مدل در برنامه در زمان ثبت داده‌ها هیچ گونه خطایی صادر نمی‌شود. برای اینکه بتوان از سیستم خطای پیش فرض ASP.NET MVC کمک گرفت می‌توان مدل را به صورت زیر تغییر داد.
using System;
using System.ComponentModel.DataAnnotations;

public class SampleModel
{
    [Required(ErrorMessage = "Start date is required")]
    public DateTime StartDate { get; set; }

    [Required(ErrorMessage = "Data is required")]
    public string Data { get; set; }

    public int Id { get; set; }
}
حال ویو این مدل را طراحی می‌کنیم.
@model SampleModel
@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<section>
    <header>
        <h3>SampleModel</h3>
    </header>
    @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" })

    @using (Html.BeginForm("SaveData", "Sample", FormMethod.Post))
    {
        <p>
            @Html.LabelFor(x => x.StartDate)
            @Html.TextBoxFor(x => x.StartDate)
            @Html.ValidationMessageFor(x => x.StartDate)
        </p>
        <p>
            @Html.LabelFor(x => x.Data)
            @Html.TextBoxFor(x => x.Data)
            @Html.ValidationMessageFor(x => x.Data)
        </p>
        <input type="submit" value="Save"/>
    }
</section>
و بخش کنترلر آن را به صورت زیر پیاده سازی می‌کنیم.
 public class SampleController : Controller
    {
        //
        // GET: /Sample/

        public ActionResult Index()
        {
            return View();
        }

        public ActionResult SaveData(SampleModel item)
        {
            if (ModelState.IsValid)
            {
                //save data
            }
            else
            {
                ModelState.AddModelError("","لطفا خطاهای زیر را برطرف نمایید");
                RedirectToAction("Index", item);
            }
            return View("Index");
        }
    }
حال با اجرای این کد و زدن دکمه Save صفحه مانند شکل پایین خطاها را نمایش خواهد داد.

تا اینجای کار روال عادی همیشگی است. اما برای طراحی Attribute دلخواه جهت اعتبار سنجی (مثلا برای مجبور کردن کاربر به وارد کردن یک فیلد با پیام دلخواه و زبان دلخواه) باید یک کلاس جدید تعریف کرده و از کلاس RequiredAttribute ارث ببرد. در پارامتر ورودی این کلاس جهت کار با Resource‌های ثابت در نظر گرفته شده است اما برای اینکه فیلد دلخواه را از دیتابیس بخواند این روش جوابگو نیست. برای انجام آن باید کلاس RequiredAttribute بازنویسی شود.

کلاس طراحی شده باید به صورت زیر باشد:

public class VegaRequiredAttribute : RequiredAttribute, IClientValidatable
    {
#region Fields (2) 

        private readonly string _resourceId;
        private String _resourceString = String.Empty;

#endregion Fields 

#region Constructors (1) 

        public VegaRequiredAttribute(string resourceId)
        {
            _resourceId = resourceId;
            ErrorMessage = _resourceId;
            AllowEmptyStrings = true;
        }

#endregion Constructors 

#region Properties (1) 

        public new String ErrorMessage
        {
            get { return _resourceString; }
            set { _resourceString = GetMessageFromResource(value); }
        }

#endregion Properties 

#region Methods (2) 

// Public Methods (1) 

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {

            yield return new ModelClientValidationRule
            {
                ErrorMessage = GetMessageFromResource(_resourceId),
                ValidationType = "required"
            };
        }
// Private Methods (1) 

        private string GetMessageFromResource(string resourceId)
        {
            var errorMessage = HttpContext.GetGlobalResourceObject(_resourceId, "Yes") as string;
            return errorMessage ?? ErrorMessage;
        }

#endregion Methods 
    }
در این کلاس دو نکته وجود دارد.
1- ابتدا دستور
HttpContext.GetGlobalResourceObject(_resourceId, "Yes") as string;  

که عنوان کلید Resource را از سازنده کلاس گرفته (کد اقای یوسف نژاد) رشته معادل آن را از دیتابیس بازیابی میکند.

2- ارث بری از اینترفیس IClientValidatable، در صورتی که از این اینترفیس ارث بری نداشته باشیم. Validator طراحی شده در طرف کلاینت کار نمی‌کند. بلکه کاربر با کلیک بروی دکمه مورد نظر داده‌ها را به سمت سرور ارسال می‌کند. در صورت وجود خطا در پست بک خطا نمایش داده خواهد شد. اما با ارث بری از این اینترفیس و پیاده سازی متد GetClientValidationRules می‌توان تعریف کرد که در طرف کلاینت با استفاده از Unobtrusive jQuery  پیام خطای مورد نظر به کنترل ورودی مورد نظر (مانند تکست باکس) اعمال می‌شود. مثلا در این مثال خصوصیت data-val-required به input هایی که قبلا در مدل ما Reqired تعریف شده اند اعمال می‌شود.

حال در مدل تعریف شده می‌توان به جای Required می‌توان از VegaRequiredAttribute مانند زیر استفاده کرد. (همراه با نام کلید مورد نظر در دیتابیس)

public class SampleModel
{
    [VegaRequired("RequiredMessage")]
    public DateTime StartDate { get; set; }

    [VegaRequired("RequiredMessage")]
    public string Data { get; set; }

    public int Id { get; set; }
}
ورودی Validator مورد نظر نام کلیدی است به زبان دلخواه که عنوان آن RequiredMessage تعریف شده است و مقدار آن در دیتابیس مقداری مانند "تکمیل این فیلد الزامی است" است. با این کار در زمان اجرا با استفاده از این ولیدتور ابتدا کلید مورد نظر با توجه به زبان فعلی از دیتابیس بازیابی شده و در متادیتابی مدل ما قرار می‌گیرد. به جای استفاده از Resource‌ها می‌توان پیام‌های خطای دلخواه را در دیتابیس ذخیره کرد و در مواقع ضروری جهت جلوگیری از تکرار از آنها استفاده نمود.
با اجرای برنامه اینبار خروجی به شکل زیر خواهد بود.

جهت فعال ساری اعتبار سنجی سمت کلاینت ابتدا باید اسکریپت‌های زیر به صفحه اضافه شود.
<script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
سپس در فایل web.config تنظیمات زیر باید اضافه شود
<appSettings>
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
سپس برای اعمال Validator طراحی شده باید توسط کدهای جاوا اسکریپت زیر داده‌های مورد نیاز سمت کلاینت رجیستر شوند.
<script type="text/javascript">
        jQuery.validator.addMethod('required', function (value, element, params) {
            if (value == null | value == "") {
                return false;
            } else {
                return true;
            }

        }, '');

        jQuery.validator.unobtrusive.adapters.add('required', {}, function (options) {
            options.rules['required'] = true;
            options.messages['required'] = options.message;
        });
    </script>
البته برای مثال ما قسمت بالا به صورت پیش فرض رجیستر شده است اما در صورتی که بخواهید یک ولیدتور دلخواه و غیر استاندارد بنویسید روال کار به همین شکل است.
موفق و موید باشید.
منابع ^ و ^ و ^ و ^
مطالب
شروع کار با ASP.NET Web API 2
HTTP تنها برای به خدمت گرفتن صفحات وب نیست. این پروتکل همچنین پلتفرمی قدرتمند برای ساختن API هایی است که سرویس‌ها و داده را در دسترس قرار می‌دهند. این پروتکل ساده، انعطاف پذیر و در همه جا حاضر است. هر پلتفرمی که فکرش را بتوانید بکنید کتابخانه ای برای HTTP دارد، بنابراین سرویس‌های HTTP می‌توانند بازه بسیار گسترده ای از کلاینت‌ها را پوشش دهند، مانند مرورگرها، دستگاه‌های موبایل و اپلیکیشن‌های مرسوم دسکتاپ.

ASP.NET Web API فریم ورکی برای ساختن API‌های وب بر روی فریم ورک دات نت است. در این مقاله با استفاده از این فریم ورک، API وبی خواهیم ساخت که لیستی از محصولات را بر می‌گرداند. صفحه وب کلاینت، با استفاده از jQuery نتایج را نمایش خواهد داد.


یک پروژه Web API بسازید

در ویژوال استودیو 2013 پروژه جدیدی از نوع ASP.NET Web Application بسازید و نام آن را "ProductsApp" انتخاب کنید.

در دیالوگ New ASP.NET Project قالب Empty را انتخاب کنید و در قسمت "Add folders and core references for" گزینه Web API را انتخاب نمایید.

می توانید از قالب Web API هم استفاده کنید. این قالب با استفاده از ASP.NET MVC صفحات راهنمای API را خواهد ساخت. در این مقاله از قالب Empty استفاده میکنیم تا تمرکز اصلی، روی خود فریم ورک Web API باشد. بطور کلی برای استفاده از این فریم ورک لازم نیست با ASP.NET MVC آشنایی داشته باشید.

افزودن یک مدل

یک مدل (model) آبجکتی است که داده اپلیکیشن شما را معرفی می‌کند. ASP.NET Web API می‌تواند بصورت خودکار مدل شما را به JSON, XML و برخی فرمت‌های دیگر مرتب (serialize) کند، و سپس داده مرتب شده را در بدنه پیام HTTP Response بنویسد. تا وقتی که یک کلاینت بتواند فرمت مرتب سازی داده‌ها را بخواند، می‌تواند آبجکت شما را deserialize کند. اکثر کلاینت‌ها می‌توانند XML یا JSON را تفسیر کنند. بعلاوه کلاینت‌ها می‌توانند فرمت مورد نظرشان را با تنظیم Accept header در پیام HTTP Request مشخص کنند.

بگذارید تا با ساختن مدلی ساده که یک محصول (product) را معرفی میکند شروع کنیم.

کلاس جدیدی در پوشه Models ایجاد کنید.

نام کلاس را به "Product" تغییر دهید، و خواص زیر را به آن اضافه کنید.

namespace ProductsApp.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

افزودن یک کنترلر

در Web API کنترلر‌ها آبجکت هایی هستند که درخواست‌های HTTP را مدیریت کرده و آنها را به اکشن متدها نگاشت می‌کنند. ما کنترلری خواهیم ساخت که می‌تواند لیستی از محصولات، یا محصولی بخصوص را بر اساس شناسه برگرداند. اگر از ASP.NET MVC استفاده کرده اید، با کنترلرها آشنا هستید. کنترلرهای Web API مشابه کنترلر‌های MVC هستند، با این تفاوت که بجای ارث بری از کلاس Controller از کلاس ApiController مشتق می‌شوند.

کنترلر جدیدی در پوشه Controllers ایجاد کنید.

در دیالوگ Add Scaffold گزینه Web API Controller - Empty را انتخاب کرده و روی Add کلیک کنید.

در دیالوگ Add Controller نام کنترلر را به "ProductsController" تغییر دهید و روی Add کلیک کنید.

توجه کنید که ملزم به ساختن کنترلرهای خود در پوشه Controllers نیستید، و این روش صرفا قراردادی برای مرتب نگاه داشتن ساختار پروژه‌ها است. کنترلر ساخته شده را باز کنید و کد زیر را به آن اضافه نمایید.

using ProductsApp.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;

namespace ProductsApp.Controllers
{
    public class ProductsController : ApiController
    {
        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public IHttpActionResult GetProduct(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }
}
برای اینکه مثال جاری را ساده نگاه داریم، محصولات مورد نظر در یک آرایه استاتیک ذخیره شده اند. مسلما در یک اپلیکیشن واقعی برای گرفتن لیست محصولات از دیتابیس یا منبع داده ای دیگر کوئری می‌گیرید.

کنترلر ما دو متد برای دریافت محصولات تعریف می‌کند:

  • متد GetAllProducts لیست تمام محصولات را در قالب یک <IEnumerable<Product بر می‌گرداند.
  • متد GetProductById سعی می‌کند محصولی را بر اساس شناسه تعیین شده پیدا کند.

همین! حالا یک Web API ساده دارید. هر یک از متدهای این کنترلر، به یک یا چند URI پاسخ می‌دهند:

 URI  Controller Method
api/products/  GetAllProducts
 api/products/id/  GetProductById

برای اطلاعات بیشتر درباره نحوه نگاشت درخواست‌های HTTP به اکشن متدها توسط Web API به این لینک مراجعه کنید.


فراخوانی Web API با جاوا اسکریپت و jQuery

در این قسمت یک صفحه HTML خواهیم ساخت که با استفاده از AJAX متدهای Web API را فراخوانی می‌کند. برای ارسال درخواست‌های آژاکسی و بروز رسانی صفحه بمنظور نمایش نتایج دریافتی از jQuery استفاده میکنیم.

در پنجره Solution Explorer روی نام پروژه کلیک راست کرده و گزینه Add, New Item را انتخاب کنید.

در دیالوگ Add New Item قالب HTML Page را انتخاب کنید و نام فایل را به "index.html" تغییر دهید.

حال محتوای این فایل را با لیست زیر جایگزین کنید.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Product App</title>
</head>
<body>

  <div>
    <h2>All Products</h2>
    <ul id="products" />
  </div>
  <div>
    <h2>Search by ID</h2>
    <input type="text" id="prodId" size="5" />
    <input type="button" value="Search" onclick="find();" />
    <p id="product" />
  </div>

  <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.min.js"></script>
  <script>
    var uri = 'api/products';

    $(document).ready(function () {
      // Send an AJAX request
      $.getJSON(uri)
          .done(function (data) {
            // On success, 'data' contains a list of products.
            $.each(data, function (key, item) {
              // Add a list item for the product.
              $('<li>', { text: formatItem(item) }).appendTo($('#products'));
            });
          });
    });

    function formatItem(item) {
      return item.Name + ': $' + item.Price;
    }

    function find() {
      var id = $('#prodId').val();
      $.getJSON(uri + '/' + id)
          .done(function (data) {
            $('#product').text(formatItem(data));
          })
          .fail(function (jqXHR, textStatus, err) {
            $('#product').text('Error: ' + err);
          });
    }
  </script>
</body>
</html>
راه‌های مختلفی برای گرفتن jQuery وجود دارد، در این مثال از Microsoft Ajax CDN استفاده شده. می‌توانید این کتابخانه را از http://jquery.com دانلود کنید و بصورت محلی استفاده کنید. همچنین قالب پروژه‌های  Web API این کتابخانه را به پروژه نیز اضافه می‌کنند.


گرفتن لیستی از محصولات

برای گرفتن لیستی از محصولات، یک درخواست HTTP GET به آدرس "api/products/" ارسال کنید.

تابع getJSON یک درخواست آژاکسی ارسال می‌کند. پاسخ دریافتی هم آرایه ای از آبجکت‌های JSON خواهد بود. تابع done در صورت موفقیت آمیز بودن درخواست، اجرا می‌شود. که در این صورت ما DOM را با اطلاعات محصولات بروز رسانی می‌کنیم.

$(document).ready(function () {
    // Send an AJAX request
    $.getJSON(apiUrl)
        .done(function (data) {
            // On success, 'data' contains a list of products.
            $.each(data, function (key, item) {
                // Add a list item for the product.
                $('<li>', { text: formatItem(item) }).appendTo($('#products'));
            });
        });
});

گرفتن محصولی مشخص

برای گرفتن یک محصول توسط شناسه (ID) آن کافی است یک درخواست HTTP GET به آدرس "api/products/id/" ارسال کنید.

function find() {
    var id = $('#prodId').val();
    $.getJSON(apiUrl + '/' + id)
        .done(function (data) {
            $('#product').text(formatItem(data));
        })
        .fail(function (jqXHR, textStatus, err) {
            $('#product').text('Error: ' + err);
        });
}
برای این کار هنوز از getJSON برای ارسال درخواست آژاکسی استفاده می‌کنیم، اما اینبار شناسه محصول را هم به آدرس درخواستی اضافه کرده ایم. پاسخ دریافتی از این درخواست، اطلاعات یک محصول با فرمت JSON است.


اجرای اپلیکیشن

اپلیکیشن را با F5 اجرا کنید. صفحه وب باز شده باید چیزی مشابه تصویر زیر باشد.

برای گرفتن محصولی مشخص، شناسه آن را وارد کنید و روی Search کلیک کنید.

اگر شناسه نامعتبری وارد کنید، سرور یک خطای HTTP بر می‌گرداند.


استفاده از F12 برای مشاهده درخواست‌ها و پاسخ ها

هنگام کار با سرویس‌های HTTP، مشاهده‌ی درخواست‌های ارسال شده و پاسخ‌های دریافتی بسیار مفید است. برای اینکار می‌توانید از ابزار توسعه دهندگان وب استفاده کنید، که اکثر مرورگرهای مدرن، پیاده سازی خودشان را دارند. در اینترنت اکسپلورر می‌توانید با F12 به این ابزار دسترسی پیدا کنید. به برگه Network بروید و روی Start Capturing کلیک کنید. حالا صفحه وب را مجددا بارگذاری (reload) کنید. در این مرحله اینترنت اکسپلورر ترافیک HTTP بین مرورگر و سرور را تسخیر می‌کند. می‌توانید تمام ترافیک HTTP روی صفحه جاری را مشاهده کنید.

به دنبال آدرس نسبی "api/products/" بگردید و آن را انتخاب کنید. سپس روی Go to detailed view کلیک کنید تا جزئیات ترافیک را مشاهده کنید. در نمای جزئیات، می‌توانید header‌ها و بدنه درخواست‌ها و پاسخ‌ها را ببینید. مثلا اگر روی برگه Request headers کلیک کنید، خواهید دید که اپلیکیشن ما در Accept header داده‌ها را با فرمت "application/json" درخواست کرده است.

اگر روی برگه Response body کلیک کنید، می‌توانید ببینید چگونه لیست محصولات با فرمت JSON سریال شده است. همانطور که گفته شده مرورگرهای دیگر هم قابلیت‌های مشابهی دارند. یک ابزار مفید دیگر Fiddler است. با استفاده از این ابزار می‌توانید تمام ترافیک HTTP خود را مانیتور کرده، و همچنین درخواست‌های جدیدی بسازید که این امر کنترل کاملی روی HTTP headers به شما می‌دهد.


قدم‌های بعدی

  • برای یک مثال کامل از سرویس‌های HTTP که از عملیات POST,PUT و DELETE پشتیبانی می‌کند به این لینک مراجعه کنید.
  • برای اطلاعات بیشتر درباره طراحی واکنش گرا در کنار سرویس‌های HTTP به این لینک مراجعه کنید، که اپلیکیشن‌های تک صفحه ای (SPA) را بررسی می‌کند.
مطالب
آپلود فایل‌ها توسط برنامه‌های React به یک سرور ASP.NET Core به همراه نمایش درصد پیشرفت
قصد داریم اطلاعات یک فرم React را به همراه دو فایل الصاقی به آن، به سمت یک سرور ASP.NET Core ارسال کنیم؛ بطوریکه درصد پیشرفت ارسال فایل‌ها، زمان سپری شده، زمان باقی مانده و سرعت آپلود نیز گزارش داده شوند:



پیشنیازها
«بررسی روش آپلود فایل‌ها در ASP.NET Core»
«ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax »

- در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


برپایی پروژه‌های مورد نیاز

ابتدا یک پوشه‌ی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا می‌کنیم:
 dotnet new react
در مورد این قالب که امکان تجربه‌ی توسعه‌ی یکپارچه‌ی ASP.NET Core و React را میسر می‌کند، در مطلب «روش یکی کردن پروژه‌های React و ASP.NET Core» بیشتر بحث کرده‌ایم.
سپس در این پوشه، پوشه‌ی ClientApp پیش‌فرض آن‌را حذف می‌کنیم؛ چون کمی قدیمی است. همچنین فایل‌های کنترلر و سرویس آب و هوای پیش‌فرض آن‌را به همراه پوشه‌ی صفحات Razor آن، حذف و پوشه‌ی خالی wwwroot را نیز به آن اضافه می‌کنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
از تنظیم زیر استفاده کرده‌ایم تا با هر بار تغییری در کدهای پروژه‌ی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
بدیهی است در این حالت باید از طریق خط فرمان به پوشه‌ی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود. البته ما برنامه را به صورت یکپارچه بر روی پورت 5001 وب سرور ASP.NET Core، مرور می‌کنیم.

اکنون در ریشه‌ی پروژه‌ی ASP.NET Core ایجاد شده، دستور زیر را صادر می‌کنیم تا پروژه‌ی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
سپس وارد این پوشه‌ی جدید شده و بسته‌های زیر را نصب می‌کنیم:
> cd clientapp
> npm install --save bootstrap axios react-toastify
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود که برای افزودن فایل bootstrap.css آن به پروژه‌ی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.
- برای نمایش پیام‌های برنامه از کامپوننت react-toastify استفاده می‌کنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آن‌را اضافه کنیم:
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
همچنین نیاز است ToastContainer را به ابتدای متد render آن نیز اضافه کرد:
  render() {
    return (
      <React.Fragment>
        <ToastContainer />
- برای ارسال فایل‌ها به سمت سرور از کتابخانه‌ی معروف axios استفاده خواهیم کرد.


ایجاد کامپوننت React فرم ارسال فایل‌ها به سمت سرور

پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
import React, { useState } from "react";
import axios from "axios";
import { toast } from "react-toastify";

export default function UploadFileSimple() {

  const [description, setDescription] = useState("");
  const [selectedFile1, setSelectedFile1] = useState();
  const [selectedFile2, setSelectedFile2] = useState();


  return (
    <form>
      <fieldset className="form-group">
        <legend>Support Form</legend>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="description">
            Description
          </label>
          <input
            type="text"
            className="form-control"
            name="description"
            onChange={event => setDescription(event.target.value)}
            value={description}
          />
        </div>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="file1">
            File 1
          </label>
          <input
            type="file"
            className="form-control"
            name="file1"
            onChange={event => setSelectedFile1(event.target.files[0])}
          />
        </div>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="file2">
            File 2
          </label>
          <input
            type="file"
            className="form-control"
            name="file2"
            onChange={event => setSelectedFile2(event.target.files[0])}
          />
        </div>

        <div className="form-group row">
          <button
            className="btn btn-primary"
            type="submit"
          >
            Submit
          </button>
        </div>
      </fieldset>
    </form>
  );
}
کاری که تا این مرحله انجام شده، بازگشت UI فرم برنامه توسط یک functional component است.
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شده‌اند.
- مرحله‌ی بعد، دسترسی به فایل‌های انتخابی کاربر و همچنین مقدار توضیحات وارد شده‌است. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کرده‌ایم:
  const [description, setDescription] = useState("");
  const [selectedFile1, setSelectedFile1] = useState();
  const [selectedFile2, setSelectedFile2] = useState();
با React Hooks، بجای تعریف یک state، به صورت خاصیت، آن‌را صرفا use می‌کنیم و یا همان useState، که یک تابع است و باید در ابتدای کامپوننت، مورد استفاده قرار گیرد. این متد برای شروع به کار، نیاز به یک state آغازین را دارد؛ مانند انتساب یک رشته‌ی خالی به description. سپس اولین خروجی متد useState که داخل یک آرایه مشخص شده‌است، همان متغیر description است که توسط state ردیابی خواهد شد. اینبار بجای متد this.setState قبلی که یک متد عمومی بود، متدی اختصاصی را صرفا جهت تغییر مقدار همین متغیر description به نام setDescription به عنوان دومین خروجی متد useState، تعریف می‌کنیم. بنابراین متد useState، یک initialState را دریافت می‌کند و سپس یک مقدار را به همراه یک متد، جهت تغییر state آن، بازگشت می‌دهد. همین کار را برای دو فیلد دیگر نیز تکرار کرده‌ایم. بنابراین selectedFile1، فایلی است که توسط متد setSelectedFile1 تنظیم خواهد شد و این تنظیم، سبب رندر مجدد UI نیز خواهد گردید.
- پس از طراحی state این فرم، مرحله‌ی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، می‌توان آن‌را به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخ‌داد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
          <input
            type="text"
            className="form-control"
            name="description"
            onChange={event => setDescription(event.target.value)}
            value={description}
          />
در مورد فیلدهای دریافت فایل‌ها، روش انجام اینکار به صورت زیر است:
          <input
            type="file"
            className="form-control"
            name="file1"
            onChange={event => setSelectedFile1(event.target.files[0])}
          />
چون المان‌های دریافت فایل می‌توانند بیش از یک فایل را نیز دریافت کنند (اگر ویژگی multiple، به تعریف تگ آن‌ها اضافه شود)، به همین جهت خاصیت files بر روی آن‌ها قابل دسترسی شده‌است. اما چون در اینجا ویژگی multiple ذکر نشده‌است، بنابراین تنها یک فایل توسط آن‌ها قابل دریافت است و به همین جهت دسترسی به اولین فایل و یا files[0] را در اینجا مشاهده می‌کنید. بنابراین با فراخوانی متد setSelectedFile1، اکنون متغیر حالت selectedFile1، مقدار دهی شده و قابل استفاده است.


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

در فرم‌های معمولی، عموما داده‌ها به صورت یک شیء JSON به سمت سرور ارسال می‌شوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرم‌هایی که به همراه المان‌های دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه می‌شود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا می‌کند:
<form enctype="multipart/form-data" action="/upload" method="post">
   <input id="file-input" type="file" />
</form>
اما اگر قرار باشد همین فرم را توسط جاوا اسکریپت به سمت سرور ارسال کنیم، روش کار به صورت زیر است:
let file = document.getElementById("file-input").files[0];
let formData = new FormData();
 
formData.append("file", file);
fetch('/upload/image', {method: "POST", body: formData});
ابتدا به خاصیت files و اولین فایل آن دسترسی پیدا کرده و سپس شیء استاندارد FormData را بر اساس آن و تمام فیلدهای فرم تشکیل می‌دهیم. FormData ساختاری شبیه به یک دیکشنری را دارد و از کلیدهایی که متناظر با Id المان‌های فرم و مقادیری متناظر با مقادیر آن المان‌ها هستند، تشکیل می‌شود که توسط متد append آن، به این دیکشنری اضافه خواهند شد. در آخر هم شیء formData را به سمت سرور ارسال می‌کنیم.
در یک برنامه‌ی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام داده‌ایم. در مرحله‌ی بعد، شیء FormData را تشکیل خواهیم داد:
  // ...

export default function UploadFileSimple() {
  // ...

  const handleSubmit = async event => {
    event.preventDefault();

    const formData = new FormData();
    formData.append("description", description);
    formData.append("file1", selectedFile1);
    formData.append("file2", selectedFile2);


      toast.success("Form has been submitted successfully!");

      setDescription("");
  };

  return (
    <form onSubmit={handleSubmit}>
    </form>
  );
}
به همین جهت، ابتدا کار مدیریت رخ‌داد onSubmit فرم را انجام داده و توسط آن با استفاده از متد preventDefault، از post-back متداول فرم به سمت سرور جلوگیری می‌کنیم. سپس شیء FormData را بر اساس مقادیر حالت متناظر با المان‌های فرم، تشکیل می‌دهیم. کلیدهایی که در اینجا ذکر می‌شوند، نام خواص مدل متناظر سمت سرور را نیز تشکیل خواهند داد.


ارسال مدل داده‌های فرم React به سمت سرور

پس از تشکیل شیء FormData در متد مدیریت کننده‌ی handleSubmit، اکنون با استفاده از کتابخانه‌ی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
  // ...

export default function UploadFileSimple() {
  const apiUrl = "https://localhost:5001/api/SimpleUpload/SaveTicket";

  // ...
  const [isUploading, setIsUploading] = useState(false);

  const handleSubmit = async event => {
    event.preventDefault();

    const formData = new FormData();
    formData.append("description", description);
    formData.append("file1", selectedFile1);
    formData.append("file2", selectedFile2);

    try {
      setIsUploading(true);

      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        }}
      });

      toast.success("Form has been submitted successfully!");

      console.log("uploadResult", data);

      setIsUploading(false);
      setDescription("");
    } catch (error) {
      setIsUploading(false);
      toast.error(error);
    }
  };


  return (
  // ...
  );
}
در اینجا نحوه‌ی ارسال شیء FormData را توسط کتابخانه‌ی axios به سمت سرور مشاهده می‌کنید. با استفاده از متد post آن، به سمت مسیر api/SimpleUpload/SaveTicket که آن‌را در ادامه تکمیل خواهیم کرد، شیء formData متناظر با اطلاعات فرم، به صورت async، ارسال شده‌است. همچنین headers آن نیز به همان «"enctype="multipart/form-data» که پیشتر توضیح داده شد، تنظیم شده‌است.
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده می‌کنید. از آن می‌توان برای فعال و غیرفعال کردن دکمه‌ی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
<button
   disabled={ isUploading }
   className="btn btn-primary"
   type="submit"
>
  Submit
</button>
به این ترتیب اگر فراخوانی await axios.post هنوز به پایان نرسیده باشد، مقدار isUploading مساوی true بوده و سبب غیرفعال شدن دکمه‌ی submit می‌شود.


اعتبارسنجی سمت کلاینت فایل‌های ارسالی به سمت سرور

در اینجا شاید نیاز باشد نوع و یا اندازه‌ی فایل‌های انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه می‌کنیم:
  const isFileValid = selectedFile => {
    if (!selectedFile) {
      // toast.error("Please select a file.");
      return false;
    }

    const allowedMimeTypes = [
      "image/png",
      "image/jpeg",
      "image/gif",
      "image/svg+xml"
    ];
    if (!allowedMimeTypes.includes(selectedFile.type)) {
      toast.error(`Invalid file type: ${selectedFile.type}`);
      return false;
    }

    const maxFileSize = 1024 * 500;
    const fileSize = selectedFile.size;
    if (fileSize > maxFileSize) {
      toast.error(
        `File size ${(fileSize / 1024).toFixed(
          2
        )} KB must be less than ${maxFileSize / 1024} KB`
      );
      return false;
    }

    return true;
  };
در اینجا ابتدا بررسی می‌شود که آیا فایلی انتخاب شده‌است یا خیر؟ سپس فایل انتخاب شده، باید دارای یکی از MimeTypeهای تعریف شده باشد. همچنین اندازه‌ی آن نیز نباید بیشتر از 500 کیلوبایت باشد. در هر کدام از این موارد، یک خطا توسط react-toastify به کاربر نمایش داده خواهد شد.

اکنون برای استفاده‌ی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کننده‌ی submit اطلاعات:
  const handleSubmit = async event => {
    event.preventDefault();

    if (!isFileValid(selectedFile1) || !isFileValid(selectedFile2)) {
      return;
    }
در ابتدای متد مدیریت کننده‌ی handleSubmit، متد isFileValid را بر روی دو متغیر حالتی که حاوی اطلاعات فایل‌های انتخابی توسط کاربر هستند، فراخوانی می‌کنیم.

ب) استفاده‌ی از آن جهت غیرفعال کردن دکمه‌ی submit:
<button
            disabled={
              isUploading ||
              !isFileValid(selectedFile1) ||
              !isFileValid(selectedFile2)
            }
            className="btn btn-primary"
            type="submit"
>
   Submit
</button>
می‌توان دقیقا در همان زمانیکه کاربر فایلی را انتخاب می‌کند نیز به انتخاب او واکنش نشان داد. چون مقدار دهی‌های متغیرهای حالت، همواره سبب رندر مجدد فرم می‌شوند و در این حالت مقدار ویژگی disabled نیز محاسبه‌ی مجدد خواهد شد، بنابراین در همان زمانیکه کاربر فایلی را انتخاب می‌کند، متد isFileValid نیز بر روی آن فراخوانی شده و در صورت نیاز، خطایی به او نمایش داده می‌شود.


نمایش درصد پیشرفت آپلود فایل‌ها

کتابخانه‌ی axios، امکان دسترسی به میزان اطلاعات آپلود شده‌ی به سمت سرور را به صورت یک رخ‌داد فراهم کرده‌است که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایل‌ها استفاده می‌کنیم:
      const startTime = Date.now();

      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        },
        onUploadProgress: progressEvent => {
          const { loaded, total } = progressEvent;

          const timeElapsed = Date.now() - startTime;
          const uploadSpeed = loaded / (timeElapsed / 1000);

          setUploadProgress({
            queueProgress: Math.round((loaded / total) * 100),
            uploadTimeRemaining: Math.ceil((total - loaded) / uploadSpeed),
            uploadTimeElapsed: Math.ceil(timeElapsed / 1000),
            uploadSpeed: (uploadSpeed / 1024).toFixed(2)
          });
        }
      });
هر بار که متد رویدادگردان onUploadProgress فراخوانی می‌شود، به همراه اطلاعات شیء progressEvent است که خواص loaded آن به معنای میزان اطلاعات آپلود شده و total هم جمع کل اندازه‌ی اطلاعات در حال ارسال است. بر این اساس و همچنین زمان شروع عملیات، می‌توان اطلاعاتی مانند درصد پیشرفت عملیات، مدت زمان باقیمانده، مدت زمان سپری شده و سرعت آپلود اطلاعات را محاسبه کرد و سپس توسط آن، شیء state ویژه‌ای را به روز رسانی کرد که به صورت زیر تعریف می‌شود:
  const [uploadProgress, setUploadProgress] = useState({
    queueProgress: 0,
    uploadTimeRemaining: 0,
    uploadTimeElapsed: 0,
    uploadSpeed: 0
  });
هر بار به روز رسانی state، سبب رندر مجدد UI می‌شود. به همین جهت متدی را برای رندر جدولی که اطلاعات شیء state فوق را نمایش می‌دهد، به صورت زیر تهیه می‌کنیم:
  const showUploadProgress = () => {
    const {
      queueProgress,
      uploadTimeRemaining,
      uploadTimeElapsed,
      uploadSpeed
    } = uploadProgress;

    if (queueProgress <= 0) {
      return <></>;
    }

    return (
      <table className="table">
        <thead>
          <tr>
            <th width="15%">Event</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>
              <strong>Elapsed time</strong>
            </td>
            <td>{uploadTimeElapsed} second(s)</td>
          </tr>
          <tr>
            <td>
              <strong>Remaining time</strong>
            </td>
            <td>{uploadTimeRemaining} second(s)</td>
          </tr>
          <tr>
            <td>
              <strong>Upload speed</strong>
            </td>
            <td>{uploadSpeed} KB/s</td>
          </tr>
          <tr>
            <td>
              <strong>Queue progress</strong>
            </td>
            <td>
              <div
                className="progress-bar progress-bar-info progress-bar-striped"
                role="progressbar"
                aria-valuemin="0"
                aria-valuemax="100"
                aria-valuenow={queueProgress}
                style={{ width: queueProgress + "%" }}
              >
                {queueProgress}%
              </div>
            </td>
          </tr>
        </tbody>
      </table>
    );
  };
و این متد را به این شکل در ذیل المان fieldset فرم، اضافه می‌کنیم تا کار رندر نهایی را انجام دهد:
{showUploadProgress()}
هربار که state به روز می‌شود، مقدار شیء uploadProgress دریافت شده و بر اساس آن، 4 سطر جدول نمایش پیشرفت آپلود، تکمیل می‌شوند.
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایل‌ها استفاده شده‌است. اگر style آن‌را هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.

یک نکته: اگر می‌خواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگه‌ی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایل‌ها، 100 درصد را مشاهده خواهید کرد.


دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیره‌ی فایل‌های آن‌

بر اساس نحوه‌ی تشکیل FormData سمت کلاینت:
const formData = new FormData();
formData.append("description", description);
formData.append("file1", selectedFile1);
formData.append("file2", selectedFile2);
مدل سمت سرور معادل با آن به صورت زیر خواهد بود:
using Microsoft.AspNetCore.Http;

namespace UploadFilesSample.Models
{
    public class Ticket
    {
        public int Id { set; get; }

        public string Description { set; get; }

        public IFormFile File1 { set; get; }

        public IFormFile File2 { set; get; }
    }
}
که در اینجا هر selectedFile سمت کلاینت، به یک IFormFile سمت سرور نگاشت می‌شود. نام این خواص نیز باید با نام کلیدهای اضافه شده‌ی به دیکشنری FormData، یکی باشند.
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using UploadFilesSample.Models;

namespace UploadFilesSample.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SimpleUploadController : Controller
    {
        private readonly IWebHostEnvironment _environment;

        public SimpleUploadController(IWebHostEnvironment environment)
        {
            _environment = environment;
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
        {
            var file1Path = await saveFileAsync(ticket.File1);
            var file2Path = await saveFileAsync(ticket.File2);

            //TODO: save the ticket ... get id

            return Created("", new { id = 1001 });
        }

        private async Task<string> saveFileAsync(IFormFile file)
        {
            const string uploadsFolder = "uploads";
            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            //TODO: Do security checks ...!

            if (file == null || file.Length == 0)
            {
                return string.Empty;
            }

            var filePath = Path.Combine(uploadsRootFolder, file.FileName);
            using (var fileStream = new FileStream(filePath, FileMode.Create))
            {
                await file.CopyToAsync(fileStream);
            }

            return $"/{uploadsFolder}/{file.Name}";
        }
    }
}
توضیحات تکمیلی:
- تزریق IWebHostEnvironment در سازنده‌ی کلاس کنترلر، سبب می‌شود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایل‌های نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه می‌کنید، هنوز هم model binding کار کرده و می‌توان شیء Ticket را به نحو متداولی دریافت کرد:
public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
ویژگی FromForm نیز مرتبط است به هدر multipart/form-data ارسالی از سمت کلاینت:
      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        }}
      });


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگی‌های سمت کلاینت نیز می‌شود، ابتدا به پوشه‌ی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار می‌کند. سپس به پوشه‌ی اصلی برنامه‌ی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائه‌ی برنامه‌ی React بر روی پورت 5001 می‌شود.
اشتراک‌ها
کتابخانه SmartWizard

Smart Wizard is a flexible and heavily customizable jQuery step wizard plugin with Bootstrap support. It is easy to implement and gives a neat and stylish interface for your forms, checkout screen, registration steps etc. Based on the feedback from our users over the past years we have come up with the best ever built jQuery wizard plugin of all time.  Demo

Features:

  • Bootstrap support
  • Responsive themes
  • Heavily customizable toolbar, option to add extra buttons
  • Theme support with various themes included
  • Customizable css styles
  • Url navigation and step selection
  • Public methods for external function call
  • Enhanced event support
  • In-built wizard reset method
  • Ajax content loading with option to specify individual url for steps
  • Keyboard navigation
کتابخانه SmartWizard