مطالب
استفاده از SQLDom برای آنالیز عبارات T-SQL، قسمت دوم
مدتی قبل مطلبی را در مورد کتابخانه‌ی ویژه SQL Server که یک T-SQL Parser تمام عیار است، در این سایت مطالعه کردید. در این قسمت، همان مطلب را به نحو بهتر و ساده‌تری بازنویسی خواهیم کرد.
مشکلی که در دراز مدت با SQLDom وجود خواهد داشت، مواردی مانند SelectStarExpression و CreateProcedureStatement و امثال آن هستند. این‌ها را از کجا باید تشخیص داد؟ همچنین مراحل بررسی این اجزاء، نسبتا طولانی هستند و نیاز به یک راه حل عمومی‌تر در این زمینه وجود دارد.

راه حلی برای این مشکل در مطلب «XML ‘Visualizer’ for the TransactSql.ScriptDom parse tree» ارائه شده‌است. در اینجا تمام اجزای TSqlFragment توسط Reflection مورد بررسی و استخراج قرار گرفته و نهایتا یک فایل XML از آن حاصل می‌شود.
اگر نکات ذکر شده در این مقاله را تبدیل به یک برنامه با استفاده مجدد کنیم، به چنین شکلی خواهیم رسید:


این برنامه را از اینجا می‌توانید دریافت کنید:
DomToXml.zip

همانطور که در تصویر مشاهده می‌کنید، اینبار به سادگی، SelectStarExpression قابل تشخیص است و تنها کافی است در T-SQL پردازش شده، به دنبال SelectStarExpression‌ها بود. برای اینکار جهت ساده شدن آنالیز می‌توان با ارث بری از کلاس پایه TSqlFragmentVisitor شروع کرد:
using System;
using System.Linq;
using Microsoft.SqlServer.TransactSql.ScriptDom;

namespace DbCop
{
    public class SelectStarExpressionVisitor : TSqlFragmentVisitor
    {
        public override void ExplicitVisit(SelectStarExpression node)
        {
            Console.WriteLine(
                  "`Select *` detected @StartOffset:{0}, Line:{1}, T-SQL: {2}",
                  node.StartOffset,
                  node.StartLine,
                  string.Join(string.Empty, node.ScriptTokenStream.Select(x => x.Text)).Trim());

            base.ExplicitVisit(node);
        }
    }
}
در کلاس پایه TSqlFragmentVisitor به ازای تمام اشیاء شناخته شده‌ی ScriptDom، یک متد ExplicitVisit قابل بازنویسی درنظر گرفته شده‌است. در اینجا برای مثال نمونه‌ی SelectStarExpression آن را بازنویسی کرده‌ایم.
مرحله‌ی بعد، اجرای این کلاس Visitor است:
    public static class GenericVisitor
    {
        public static void Start(string tSql, TSqlFragmentVisitor visitor)
        {
            IList<ParseError> errors;
            TSqlScript sqlFragment;
            using (var reader = new StringReader(tSql))
            {
                var parser = new TSql120Parser(initialQuotedIdentifiers: true);
                sqlFragment = (TSqlScript)parser.Parse(reader, out errors);
            }

            if (errors != null && errors.Any())
            {
                var sb = new StringBuilder();
                foreach (var error in errors)
                    sb.AppendLine(error.Message);

                throw new InvalidOperationException(sb.ToString());
            }
            sqlFragment.Accept(visitor);
        }
    }
در اینجا متد Accept کلاس TSql120Parser، امکان پذیرش یک Visitor را دارد. به این معنا که Parser در حال کار، هر زمانیکه در حال آنالیز قسمتی از T-SQL دریافتی بود، نتیجه را به اطلاع یکی از متدهای کلاس پایه TSqlFragmentVisitor نیز خواهد رساند. بنابراین دیگر نیازی به نوشتن حلقه و بررسی تک تک اجزای خروجی TSql120Parser نیست. اگر نیاز به بررسی SelectStarExpression داریم، فقط کافی است Visitor آن‌را طراحی کنیم.

مثالی از نحوه‌ی استفاده از کلاس GenericVisitor فوق را در اینجا ملاحظه می‌کنید:
 var tsql = @"WITH ctex AS (
SELECT * FROM sys.objects
)
SELECT * FROM ctex";
GenericVisitor.Start(tsql, new SelectStarExpressionVisitor());
مطالب
یافتن آدرس نهایی یک Url پس از Redirect

برای مثال آدرس http://feedproxy.google.com/~r/nettuts/~3/tWCksueANyY/ را در نظر بگیرید. گوگل پس از خرید feedburner ، از feedproxy.google.com جهت ردیابی آدرس‌های فیدها استفاده می‌کند؛‌ مثلا فید شما چند نفر خواننده دارد، کدام موارد بیشتر خوانده شده، با چه مرورگرهایی، از چه مکان‌هایی و مواردی از این دست. البته می‌توان در تنظیمات فیدبرنر این نوع آدرس دهی را خاموش کرد ولی به صورت پیش فرض فعال است. مشکل feedproxy.google.com هم این است که در ایران فیلتر است. بنابراین یافتن آدرس اصلی این نوع لینک‌ها پس از Redirect نهایی می‌تواند جهت ارائه عمومی آن‌ها مفید باشد. با استفاده از قطعه کد زیر می‌توان این آدرس را یافت:

using System;
using System.Net;

namespace Linq2Rss
{
public class RedirectFinder
{
public CookieContainer Cookies { set; get; }
public string Url { get; set; }

public string GetRedirectUrl()
{
var hops = 1;
const int MaxRedirects = 20;

do
{
var request = (HttpWebRequest)WebRequest.Create(Url);
request.UserAgent = "MyUA";
request.KeepAlive = true;
request.Referer = Url;
if (Cookies == null) Cookies = new CookieContainer();
request.CookieContainer = Cookies;
request.AllowAutoRedirect = false;

using (var webResp = request.GetResponse() as HttpWebResponse)
{
if ((webResp.StatusCode == HttpStatusCode.Found) ||
(webResp.StatusCode == HttpStatusCode.Redirect) ||
(webResp.StatusCode == HttpStatusCode.Moved) ||
(webResp.StatusCode == HttpStatusCode.MovedPermanently))
{
var newLocation = webResp.Headers["Location"];
if (newLocation.StartsWith("/"))
{
var uri = new Uri(Url);
Url = string.Format("{0}://{1}:{2}{3}", uri.Scheme, uri.Host, uri.Port, newLocation);
}
else
Url = newLocation;
}
else
{
if (webResp.StatusCode == HttpStatusCode.OK)
return Url;
}
}

hops++;
} while (hops <= MaxRedirects);
return Url;
}
}
}


برای یافتن آدرس واقعی یک Url پس از Redirect راهی بجز درخواست آن از وب سرور اولیه وجود ندارد. سپس وضعیت پاسخ داده شده بررسی می‌شود؛ اگر حاوی Found ، Moved یا Redirect بود، به این معنا است که باید آدرس جدید را از هدر پاسخ دریافتی استخراج کنیم. این آدرس، در کلید Location ذخیره می‌شود. اکنون یکبار دیگر نیاز است تا این آدرس جدید بررسی شود، زیرا ممکن است این مورد هم به آدرس دیگری اشاره کند. در کل، کد فوق 20 بار این بررسی را انجام خواهد داد (هر چند عموما در دو یا سه سعی به جواب خواهیم رسید).
مطالب
استفاده از لوسین برای برجسته سازی عبارت جستجو شده در نتایج حاصل
قسمت جستجوی سایت جاری رو با استفاده از لوسین بازنویسی کردم. خلاصه‌ای از نحوه انجام این‌کار رو در ادامه ملاحظه خواهید کرد:

1) دریافت کتابخانه‌های لازم
نیاز به کتابخانه‌های Lucene.NET و همچنین Lucene.Net Contrib است که هر دو مورد را به سادگی توسط NuGet می‌توانید دریافت و نصب کنید.
Highlighter استفاده شده، در کتابخانه Lucene.Net Contrib قرار دارد. به همین جهت این مورد را نیز باید جداگانه دریافت کرد.


2) تهیه منبع داده
در اینجا جهت سادگی کار فرض کنید که لیستی از مطالب را به فرمت زیر دراختیار داریم:
public class Post
{
    public int Id { set; get; }
    public string Title { set; get; }
    public string Body { set; get; }
}
تفاوتی نمی‌کند که از چه منبع داده‌ای استفاده می‌کنید. آیا قرار است یک سری فایل متنی ساده موجود در یک پوشه را ایندکس کنید یا تعدادی رکورد بانک اطلاعاتی؛ از NHibernate استفاده می‌کنید یا از Entity framework و یا از ADO.NET. کتابخانه Lucene مستقل است از منبع داده مورد استفاده و تنها اطلاعاتی با فرمت شیء Document معرفی شده به آن‌را می‌شناسد.


3) تبدیل اطلاعات به فرمت Lucene.NET
همانطور که عنوان شد نیاز است هر رکورد از اطلاعات خود را به شیء Document نگاشت کنیم. نمونه‌ای از اینکار را در متد ذیل مشاهده می‌نمائید:
static Document MapPostToDocument(Post post)
{
    var postDocument = new Document();
    postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
    postDocument.Add(new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
    postDocument.Add(new Field("Body", post.Body, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
    return postDocument;
}
این متد وهله‌ای از شیء Post را دریافت کرده و آن‌را تبدیل به یک سند Lucene می‌کند.
کار با ایجاد یک وهله از شیء Document شروع شده و سپس اطلاعات به صوت فیلدهایی به این سند اضافه می‌شوند.

توضیحات آرگومان‌های مختلف سازنده کلاس Field:
- در ابتدا نام فیلد مورد نظر ذکر می‌گردد.
- سپس مقدار متناظر با آن فیلد، به صورت رشته باید معرفی شود.
- آرگومان سوم آن مشخص می‌کند که اصل اطلاعات نیز علاوه بر ایندکس شدن باید در فایل‌های Lucene ذخیره شوند یا خیر. توسط Field.Store.YES مشخص می‌کنیم که بله؛ علاقمندیم تا اصل اطلاعات نیز از طریق Lucene قابل بازیابی باشند. این مورد جهت نمایش سریع نتایج جستجوها می‌تواند مفید باشد. اگر قرار نیست اطلاعاتی را از این فیلد خاص به کاربر نمایش دهید می‌توانید از گزینه Field.Store.NO استفاده کنید. همچنین امکان فشرده سازی اطلاعات ذخیره شده با انتخاب گزینه Field.Store.COMPRESS نیز میسر است.
- توسط آرگومان چهارم آن تعیین خواهیم کرد که اطلاعات فیلد مورد نظر ایندکس شوند یا خیر. مقدار Field.Index.NOT_ANALYZED سبب عدم ایندکس شدن فیلد Id می‌شوند (چون قرار نیست روی id در قسمت جستجوی عمومی سایت، جستجویی صورت گیرد). به کمک مقدار Field.Index.ANALYZED، مقدار معرفی شده، ایندکس خواهد شد.
- پارامتر پنجم آن‌را جهت سرعت عمل در نمایان سازی/برجسته کردن و highlighting عبارات جستجو شده در متن‌های یافت شده معرفی کرده‌ایم. الگوریتم‌های متناظر با این روش در فایل‌های Lucene.Net Contrib قرار دارند.


یک نکته
اگر اطلاعاتی را که قرار است ایندکس کنید از نوع HTML می‌باشند، بهتر است تمام تگ‌های آن‌را پیش از افزودن به لوسین حذف کنید. به این ترتیب نتایج جستجوی دقیق‌تری را می‌توان شاهد بود. برای این منظور می‌توان از متد ذیل کمک گرفت:
public static string RemoveHtmlTags(string text)
{
    return string.IsNullOrEmpty(text) ? string.Empty : Regex.Replace(text, @"<(.|\n)*?>", string.Empty);
}


4) تهیه Full text index به کمک Lucene.NET
تا اینجا توانستیم اطلاعات خود را به فرمت اسناد لوسین تبدیل کنیم. اکنون ثبت و تبدیل آن‌ها به فایل‌های Full text search لوسین به سادگی زیر است:
static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_29;
public static void CreateIdx(IEnumerable<Post> dataList)
{
    var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
    var analyzer = new StandardAnalyzer(_version);
    using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
    {
         foreach (var post in dataList)
        {
            writer.AddDocument(MapPostToDocument(post));
        }

        writer.Optimize();
        writer.Commit();
        writer.Close();
        directory.Close();
    }
}
ابتدا محل ذخیره سازی فایل‌های full text search مشخص می‌شوند. سپس آنالیز کننده اطلاعات باید معرفی شود. در ادامه به کمک این اطلاعات، شیء IndexWriter ایجاد و مستندات لوسین به آن اضافه می‌شوند. در آخر، این اطلاعات بهینه سازی شده و ثبت نهایی صورت خواهد گرفت.
ذکر version در اینجا ضروری است؛ از این جهت که اگر ایندکسی با فرمت مثلا LUCENE_29 تهیه شود ممکن است با نگارش بعدی این کتابخانه سازگار نباشد و در صورت ارتقاء، نتایج جستجوی انجام شده، کاملا بی‌ربط نمایش داده شوند. با ذکر صریح نگارش، دیگر این اتفاق رخ نخواهد داد.


نکته
StandardAnalyzer توکار لوسین، امکان دریافت لیستی از واژه‌هایی که نباید ایندکس شوند را نیز دارا است. اطلاعات بیشتر در اینجا.


5) به روز رسانی ایندکس‌ها
به کمک سه متد ذیل می‌توان اطلاعات ایندکس‌های موجود را به روز یا حذف کرد:
public static void UpdateIndex(Post post)
{
        var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
        var analyzer = new StandardAnalyzer(_version);
        using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
        {
            var newDoc = MapPostToDocument(post);

             indexWriter.UpdateDocument(new Term("Id", post.Id.ToString()), newDoc);
             indexWriter.Commit();
             indexWriter.Close();
             directory.Close();
         }
}

public static void DeleteIndex(Post post)
{
         var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
         var analyzer = new StandardAnalyzer(_version);
         using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
         {
             indexWriter.DeleteDocuments(new Term("Id", post.Id.ToString()));
             indexWriter.Commit();
             indexWriter.Close();
             directory.Close();
          }
}

public static void AddIndex(Post post)
{
      var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
      var analyzer = new StandardAnalyzer(_version, getStopWords());
      using (var indexWriter = new IndexWriter(directory, analyzer, create: false, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
      {
           var searchQuery = new TermQuery(new Term("Id", post.Id.ToString()));
           indexWriter.DeleteDocuments(searchQuery);

            var newDoc = MapPostToDocument(post);
            indexWriter.AddDocument(newDoc);
            indexWriter.Commit();
            indexWriter.Close();
            directory.Close();
        }
}
تنها نکته مهم این متدها، استفاده از متد IndexWriter با پارامتر create مساوی false است. به این ترتیب فایل‌های موجود بجای از نو ساخته شدن، به روز خواهند شد.
محل فراخوانی این متدها هم می‌تواند در کنار متدهای به روز رسانی اطلاعات اصلی در بانک اطلاعاتی برنامه باشند. اگر رکوردی اضافه یا حذف شده، ایندکس متناظر نیز باید به روز شود.


6) جستجو در اطلاعات ایندکس شده و نمایش آن‌ها به همراه نمایان/برجسته سازی عبارات جستجو شده
قسمت نهایی کار با لوسین و اطلاعات ایندکس‌های تهیه شده، کوئری گرفتن از آن‌ها است. متدهای کامل مورد نیاز را در ذیل مشاهده می‌کنید:

public static void Query(string term)
{
     var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex"));
     using (var searcher = new IndexSearcher(directory, readOnly: true))
     {
          var analyzer = new StandardAnalyzer(_version);
          var parser = new MultiFieldQueryParser(_version, new[] { "Body", "Title" }, analyzer);
          var query = parseQuery(term, parser);
          var hits = searcher.Search(query, 10).ScoreDocs;

          if (hits.Length == 0)
          {
               term = searchByPartialWords(term);
               query = parseQuery(term, parser);
               hits = searcher.Search(query, 10).ScoreDocs;
           }

           FastVectorHighlighter fvHighlighter = new FastVectorHighlighter(true, true);
           foreach (var scoreDoc in hits)
           {
               var doc = searcher.Doc(scoreDoc.doc);
               string bestfragment = fvHighlighter.GetBestFragment(
                                fvHighlighter.GetFieldQuery(query),
                                searcher.GetIndexReader(),
                                docId: scoreDoc.doc,
                                fieldName: "Body",
                                fragCharSize: 400);
                var id = doc.Get("Id");
                var title = doc.Get("Title");
                var score = scoreDoc.score;
                Console.WriteLine(bestfragment);
            }

            searcher.Close();
            directory.Close();
      }
   }

   private static Query parseQuery(string searchQuery, QueryParser parser)
   {
       Query query;
        try
        {
            query = parser.Parse(searchQuery.Trim());
        }
        catch (ParseException)
        {
            query = parser.Parse(QueryParser.Escape(searchQuery.Trim()));
        }
        return query;
   }

   private static string searchByPartialWords(string bodyTerm)
   {
       bodyTerm = bodyTerm.Replace("*", "").Replace("?", "");
       var terms = bodyTerm.Trim().Replace("-", " ").Split(' ')
                                .Where(x => !string.IsNullOrEmpty(x))
                                .Select(x => x.Trim() + "*");
       bodyTerm = string.Join(" ", terms);
       return bodyTerm;
   }
توضیحات:
اکثر سایت‌ها را که بررسی کنید، جستجوی بر روی یک فیلد را توضیح داده‌اند. در اینجا نحوه جستجو بر روی چند فیلد را به کمک MultiFieldQueryParser ملاحظه می‌کنید.
نکته‌ی مهمی را هم که در اینجا باید به آن دقت داشت، حساس بودن لوسین به کوچکی و بزرگی نام فیلدهای معرفی شده است و در صورت عدم رعایت این مساله، جستجوی شما نتیجه‌ای را دربر نخواهد داشت.
در ادامه برای parse اطلاعات، از متد کمکی parseQuery استفاده شده است. ممکن است به ParseException بخاطر یک سری حروف خاص بکارگرفته شده در عبارات مورد جستجو برسیم. در اینجا می‌توان توسط متد QueryParser.Escape، اطلاعات دریافتی را اصلاح کرد.
سپس نحوه استفاده از کوئری تهیه شده و متد Search را ملاحظه می‌کنید. در اینجا بهتر است تعداد رکوردهای بازگشت داده شده را تعیین کرد (به کمک آرگومان دوم متد جستجو) تا بی‌جهت سرعت عملیات را پایین نیاورده و همچنین مصرف حافظه سیستم را نیز بالا نبریم.
ممکن است تعداد hits یا نتایج حاصل صفر باشد؛ بنابراین بد نیست خودمان دست به کار شده و به کمک متد searchByPartialWords، ورودی کاربر را بر اساس زبان جستجوی ویژه لوسین اندکی بهینه کنیم تا بتوان به نتایج بهتری دست یافت.
در آخر نحوه کار با  ScoreDocs یافت شده را ملاحظه می‌کنید. اگر محتوای فیلد را در حین ایندکس سازی ذخیره کرده باشیم، به کمک متد doc.Get می‌توان به اطلاعات کامل آن نیز دست یافت.
همچنین نکته دیگری را که در اینجا می‌توان ملاحظه کرد استفاده از FastVectorHighlighter می‌باشد. به کمک این Highlighter ویژه می‌توان نتایج جستجو را شبیه به نتایج نمایش داده شده توسط موتور جستجوی گوگل درآورد. برای مثال اگر شخصی ef code first را جستجو کرد، توسط متد GetBestFragment، بهترین جزئی که شامل بیشترین تعداد حروف جستجو شده است، یافت گردیده و همچنین به کمک تگ‌های B، ضخیم نمایش داده خواهند شد.

 
مطالب
React 16x - قسمت 26 - احراز هویت و اعتبارسنجی کاربران - بخش 1 - ثبت نام و ورود به سیستم
می‌خواهیم به برنامه‌ی لیست فیلم‌هایی که تا این قسمت تکمیل کردیم، امکانات جدیدی را مانند ورود به سیستم، خروج از آن، کار با JWT، فراخوانی منابع محافظت شده‌ی سمت سرور، نمایش و یا مخفی کردن المان‌های صفحه بر اساس سطوح دسترسی کاربر و همچنین محافظت از مسیرهای مختلف تعریف شده‌ی در برنامه، اضافه کنیم.
برای قسمت backend، از همان برنامه‌ی تکمیل شده‌ی قسمت قبل استفاده می‌کنیم که به آن تولید مقدماتی JWTها نیز اضافه شده‌است. البته این سری، مستقل از قسمت سمت سرور آن تهیه خواهد شد و صرفا در حد دریافت توکن از سرور و یا ارسال مشخصات کاربر جهت لاگین، نیاز بیشتری به قسمت سمت سرور آن ندارد و تاکید آن بر روی مباحث سمت کلاینت React است. بنابراین اینکه چگونه این توکن را تولید می‌کنید، در اینجا اهمیتی ندارد و کلیات آن با تمام روش‌های پیاده سازی سمت سرور سازگار است (و مختص به فناوری خاصی نیست). پیشنیاز درک کدهای سمت سرور قسمت JWT آن، مطالب زیر هستند:
  1. «معرفی JSON Web Token»
  2. «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» 
  3. «پیاده سازی JSON Web Token با ASP.NET Web API 2.x»
  4. « آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT»  


ثبت یک کاربر جدید

فرم ثبت نام کاربران را در قسمت 21 این سری، در فایل src\components\registerForm.jsx، ایجاد و تکمیل کردیم. البته این فرم هنوز به backend server متصل نیست. برای کار با آن هم نیاز است شیءای را با ساختار زیر که ذکر سه خاصیت اول آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Users به صورت یک HTTP Post ارسال کنیم:
{
  "name": "string",
  "email": "string",
  "password": "string",
  "isAdmin": true,
  "id": 0
}
در سمت سرور هم در Services\UsersDataSource.cs که در انتهای بحث می‌توانید پروژه‌ی کامل آن‌را دریافت کنید، منحصربفرد بودن ایمیل وارد شده بررسی می‌شود و اگر یک رکورد دو بار ثبت شود، یک BadRequest را به همراه پیام خطایی، بازگشت می‌دهد.

اکنون نوبت به اتصال کامپوننت registerForm.jsx، به سرویس backend است. تا اینجا دو سرویس src\services\genreService.js و src\services\movieService.js را در قسمت قبل، به برنامه جهت کار کردن با endpoint‌های backend server، اضافه کردیم. شبیه به همین روش را برای کار با سرویس سمت سرور api/Users نیز در پیش می‌گیریم. بنابراین فایل جدید src\services\userService.js را با محتوای زیر، به برنامه‌ی frontend اضافه می‌کنیم:
import http from "./httpService";
import { apiUrl } from "../config.json";

const apiEndpoint = apiUrl + "/users";

export function register(user) {
  return http.post(apiEndpoint, {
    email: user.username,
    password: user.password,
    name: user.name
  });
}
توسط متد register این سرویس می‌توانیم شیء user را با سه خاصیت مشخص شده، از طریق HTTP Post، به آدرس api/Users ارسال کنیم. خروجی این متد نیز یک Promise است. در این سرویس، تمام متدهایی که قرار است با این endpoint سمت سرور کار کنند، مانند ثبت، حذف، دریافت اطلاعات و غیره، تعریف خواهند شد.
اطلاعات شیء user ای که در اینجا دریافت می‌شود، از خاصیت data کامپوننت RegisterForm تامین می‌گردد:
class RegisterForm extends Form {
  state = {
    data: { username: "", password: "", name: "" },
    errors: {}
  };
البته اگر دقت کرده باشید، در شیء منتسب به خاصیت data، خاصیتی به نام username تعریف شده‌است، اما در سمت سرور، نیاز است خاصیتی با نام Name را دریافت کنیم. یک چنین نگاشتی در داخل متد register سرویس کاربر، قابل مشاهده‌‌است. در غیراینصورت می‌شد در متد http.post، کل شیء user را به عنوان پارامتر دوم، درنظر گرفت و ارسال کرد.

پس از تعریف userService.js، به registerForm.jsx بازگشته و ابتدا امکانات آن‌را import می‌کنیم:
import * as userService from "../services/userService";
می‌شد این سطر را به صورت زیر نیز نوشت، تا تنها یک متد از ماژول userService را دریافت کنیم:
import { register } userService from "../services/userService";
اما روش as userService * به معنای import تمام متدهای این ماژول است. به این ترتیب نام ذکر شده‌ی پس از as، به عنوان شیءای که می‌توان توسط آن به این متدها دسترسی یافت، قابل استفاده می‌شود؛ مانند فراخوانی متد userService.register. اکنون می‌توان متد doSubmit این فرم را به سرور متصل کرد:
  doSubmit = async () => {
    try {
      await userService.register(this.state.data);
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors }; // clone an object
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };


مدیریت و نمایش خطاهای دریافتی از سمت سرور

در این حالت برای ارسال اطلاعات یک کاربر، در بار اول، یک چنین خروجی را از سمت سرور می‌توان شاهد بود که id جدیدی را به این رکورد نسبت داده‌است:


اگر مجددا همین رکورد را به سمت سرور ارسال کنیم، اینبار خطای زیر را دریافت خواهیم کرد:


که از نوع 400 یا همان BadRequest است:


بنابراین نیاز است بدنه‌ی response را در یک چنین مواردی که خطایی از سمت سرور صادر می‌شود، دریافت کرده و با به روز رسانی خاصیت errors در state فرم (همان قسمت بدنه‌ی catch کدهای فوق)، سبب درج و نمایش خودکار این خطا شویم:


پیشتر در قسمت بررسی «کار با فرم‌ها» آموختیم که برای مدیریت خطاهای پیش بینی شده‌ی دریافتی از سمت سرور، نیاز است قطعه کدهای مرتبط با سرویس http را در بدنه‌ی try/catch‌ها محصور کنیم. برای مثال در اینجا اگر ایمیل شخصی تکراری وارد شود، سرویس یک return BadRequest("Can't create the requested record.") را بازگشت می‌دهد که در اینجا status code معادل BadRequest، همان 400 است. بنابراین انتظار داریم که خطای 400 را از سمت سرور، تحت شرایط خاصی دریافت کنیم. به همین دلیل است که در اینجا تنها مدیریت status code=400 را در بدنه‌ی catch نوشته شده ملاحظه می‌کنید.
سپس برای نمایش آن، نیاز است خاصیت متناظری را که این خطا به آن مرتبط می‌شود، با پیام دریافت شده‌ی از سمت سرور، مقدار دهی کنیم که در اینجا می‌دانیم مرتبط با username است. به همین جهت سطر errors.username = ex.response.data، کار انتساب بدنه‌ی response را به خاصیت جدید errors.username انجام می‌دهد. در این حالت اگر به کمک متد setState، کار به روز رسانی خاصیت errors موجود در state را انجام دهیم، رندر مجدد فرم، در صف انجام قرار گرفته و در رندر بعدی آن، پیام موجود در errors.username، نمایش داده می‌شود.


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

فرم ورود به سیستم را در قسمت 18 این سری، در فایل src\components\loginForm.jsx، ایجاد و تکمیل کردیم که این فرم نیز هنوز به backend server متصل نیست. برای کار با آن نیاز است شیءای را با ساختار زیر که ذکر هر دو خاصیت آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Auth/Login به صورت یک HTTP Post ارسال کنیم:
{
  "email": "string",
  "password": "string"
}
با ارسال این اطلاعات به سمت سرور، درخواست Login انجام می‌شود. سرور نیز در صورت تعیین اعتبار موفقیت آمیز کاربر، به صورت زیر، یک JSON Web token را بازگشت می‌دهد:
var jwt = _tokenFactoryService.CreateAccessToken(user);
return Ok(new { access_token = jwt });
یعنی بدنه‌ی response رسیده‌ی از سمت سرور، دارای یک شیء JSON خواهد بود که خاصیت access_token آن، حاوی JSON Web token متعلق به کاربر جاری لاگین شده‌است. در آینده اگر این کاربر نیاز به دسترسی به یک api endpoint محافظت شده‌ای را در سمت سرور داشته باشد، باید این token را نیز به همراه درخواست خود ارسال کند تا پس از تعیین اعتبار آن توسط سرور، مجوز دسترسی به منبع درخواستی برای او صادر شود.

در ادامه برای تعامل با منبع api/Auth/Login سمت سرور، ابتدا یک سرویس مختص آن‌را در فایل جدید src\services\authService.js، با محتوای زیر ایجاد می‌کنیم:
import { apiUrl } from "../config.json";
import http from "./httpService";

const apiEndpoint = apiUrl + "/auth";

export function login(email, password) {
  return http.post(apiEndpoint + "/login", { email, password });
}
متد login، کار ارسال ایمیل و کلمه‌ی عبور کاربر را به اکشن متد Login کنترلر Auth، انجام می‌دهد و خروجی آن یک Promise است. برای استفاده‌ی از آن به کامپوننت src\components\loginForm.jsx بازگشته و متد doSubmit آن‌را به صورت زیر تکمیل می‌کنیم:
import * as auth from "../services/authService";

class LoginForm extends Form {
  state = {
    data: { username: "", password: "" },
    errors: {}
  };

  // ...

  doSubmit = async () => {
    try {
      const { data } = this.state;
      const {
        data: { access_token }
      } = await auth.login(data.username, data.password);
      console.log("JWT", access_token);
      localStorage.setItem("token", access_token);
      this.props.history.push("/");
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors };
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };
توضیحات:
- ابتدا تمام خروجی‌های ماژول authService را با نام شیء auth دریافت کرده‌ایم.
- سپس در متد doSubmit، اطلاعات خاصیت data موجود در state را که معادل فیلدهای فرم لاگین هستند، به متد auth.login برای انجام لاگین سمت سرور، ارسال کرده‌ایم. این متد چون یک Promise را باز می‌گرداند، باید await شود و پس از آن متد جاری نیز باید به صورت async معرفی گردد.
- همانطور که عنوان شد، خروجی نهایی متد auth.login، یک شیء JSON دارای خاصیت access_token است که در اینجا از خاصیت data خروجی نهایی دریافت شده‌است.
- سپس نیاز است برای استفاده‌های آتی، این token دریافتی از سرور را در جایی ذخیره کرد. یکی از مکان‌های متداول اینکار، local storage مرورگرها است (اطلاعات بیشتر).
- در آخر کاربر را توسط شیء history سیستم مسیریابی برنامه، به صفحه‌ی اصلی آن هدایت می‌کنیم.
- در اینجا قسمت catch نیز ذکر شده‌است تا خطاهای حاصل از return BadRequestهای دریافتی از سمت سرور را بتوان ذیل فیلد نام کاربری نمایش داد. روش کار آن، دقیقا همانند روشی است که برای فرم ثبت یک کاربر جدید استفاده کردیم.

اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، توکن دریافتی، در کنسول توسعه دهندگان مرورگر لاگ شده و سپس کاربر به صفحه‌ی اصلی برنامه هدایت می‌شود. همچنین این token ذخیره شده را می‌توان در ذیل قسمت application->storage آن نیز مشاهده کرد:



لاگین خودکار کاربر، پس از ثبت نام در سایت

پس از ثبت نام یک کاربر در سایت، بدنه‌ی response بازگشت داده شده‌ی از سمت سرور، همان شیء user است که اکنون Id او مشخص شده‌است. بنابراین اینبار جهت ارائه‌ی token از سمت سرور، بجای response body، از یک هدر سفارشی در فایل Controllers\UsersController.cs استفاده می‌کنیم (کدهای کامل آن در انتهای بحث پیوست شده‌است):
var jwt = _tokenFactoryService.CreateAccessToken(user);
this.Response.Headers.Add("x-auth-token", jwt);



در ادامه در کدهای سمت کلاینت src\components\registerForm.jsx، برای استخراج این هدر سفارشی، اگر شیء response دریافتی از سرور را لاگ کنیم:
const response = await userService.register(this.state.data);
console.log(response);
یک چنین خروجی را به همراه دارد که در آن، هدر سفارشی ما درج نشده‌است و فقط هدر content-type در آن مشخص است:


برای اینکه در کدهای سمت کلاینت بتوان این هدر سفارشی را خواند، نیاز است هدر مخصوص access-control-expose-headers را نیز به response اضافه کرد:
var jwt = _tokenFactoryService.CreateAccessToken(data);
this.Response.Headers.Add("x-auth-token", jwt);
this.Response.Headers.Add("access-control-expose-headers", "x-auth-token");
به این ترتیب وب سرور برنامه، هدر سفارشی را که قرار است برنامه‌ی کلاینت به آن دسترسی پیدا کند، مجاز اعلام می‌کند. اینبار اگر خروجی دریافتی از Axios را لاگ کنیم، در لیست هدرهای آن، هدر سفارشی x-auth-token نیز ظاهر می‌شود:


اکنون می‌توان این هدر سفارشی را در متد doSubmit کامپوننت RegisterForm، از طریق شیء response.headers خواند و در localStorage ذخیره کرد. سپس کاربر را توسط شیء history سیستم مسیریابی، به ریشه‌ی سایت هدایت نمود:
class RegisterForm extends Form {
  // ...

  doSubmit = async () => {
    try {
      const response = await userService.register(this.state.data);
      console.log(response);
      localStorage.setItem("token", response.headers["x-auth-token"]);
      this.props.history.push("/");
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors }; // clone an object
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-26-backend.zip و sample-26-frontend.zip
مطالب
جایگزین کردن jQuery با JavaScript خالص - قسمت اول - یافتن عناصر
jQuery سال‌ها به عنوان جزء اصلی توسعه‌ی برنامه‌های وب مطرح بوده‌است و برای بسیاری از توسعه دهندگان وب، یک پیشنیاز پیش‌فرض محسوب می‌شود؛ ساده‌است، قابل فهم است و به آن اطمینان داریم. زمانیکه از آن استفاده می‌کنیم دیگر نیازی نیست تا آنچنان به DOM، باگ‌های مرورگرها و یا رفتارهای متفاوت آن‌ها فکر کنیم. jQuery تمام این مشکلات را برای ما حل می‌کند. اما ... اگر روزی باگی در jQuery وجود داشت، نیاز به امکاناتی بود که هنوز در jQuery ظاهر نشده‌اند و یا حتی اجازه‌ی استفاده‌ی از jQuery را نداشته باشیم، در این حالت ... وحشت زده و تقریبا بدون هیچ نوع آمادگی به نظر خواهیم رسید.
خالق جی‌کوئری (John Resig)، این کتابخانه را در سال‌های 2006 زمانیکه Internet Explorer نگارش‌های 6 و 7 بیش از 60 درصد بازار مرورگرها را به خود اختصاص داده بودند، ارائه داد. بله؛ در آْن زمان JavaScript Web API بسیار خام، پایداری مرورگرها بسیار پایین و تطابق با استانداردهای وب در بین مرورگرهای مختلف نیز بسیار پایین بود. بنابراین علت محبوبیت کتابخانه‌ای که در این شرایط، تجربه‌ی کاری یکدستی را در بین مرورگرهای مختلف ارائه می‌داد، کاملا واضح بود. اما ... اکنون سال 2018 است و حتی مایکروسافت هم دیگر از نگارش‌های مختلف IE پشتیبانی نمی‌کند. DOM API موجود در مرورگرهای مدرن بسیار توانمند شده‌اند و در بین انواع و اقسام آن‌ها یکدست عمل می‌کنند. حتی اگر دلیل استفاده‌ی از jQuery ایجاد ساده‌تر حلقه‌ها بر روی اشیاء جاوا اسکریپتی باشد (رفع کمبودهای جاوا اسکریپت)، از زمان IE 9 به بعد، متدهای forEach و Object.keys به صورت توکار در جاوا اسکریپت وجود دارند و یا اگر نیاز به inArray.$ داشته باشید، متد Array.prototype.indexOf مدت‌ها است که جزئی از ES5 است. به همین جهت است که این روزها اخباری را مانند «GitHub نیز جی‌کوئری را کنار گذاشت» زیاد می‌شنوید. نه فقط کنار گذاشتن jQuery یک وابستگی ثالث را از برنامه حذف می‌کند، بلکه کار مستقیم با native API مرورگرها همواره به مراتب سریعتر است از کتابخانه‌هایی که سطح بالایی از abstraction آن‌ها را ارائه می‌دهند.


یافتن عناصر توسط JavaScript خالص

زمانیکه نیاز به انتخاب عناصری در صفحه باشند، بلافاصله ذهن ما به سمت ('myElement#')$ و ('myElement.')$ جی‌کوئری، معطوف می‌شود. اما ... این روزها برای انجام این نوع کارها واقعا نیازی به jQuery نیست!


یافتن عناصر بر اساس ID آن‌ها
 <div id="my-element-id"></div>
اگر بخواهیم این شیء div را بر اساس ID آن در صفحه بیابیم، روش کار آن با jQuery به صورت زیر است:
 var result = $('#my-element-id');
در اینجا این ID selector string، یک استاندارد W3C CSS1 است.
انجام این کار توسط web API و یا همان JavaScript خالص، چنین شکلی را دارد:
 var result = document.getElementById('my-element-id');
و جالب است بدانید این روش از زمان IE 5.5 وجود داشته‌است.
روش دیگر انجام اینکار با JavaScript به صورت زیر است:
  var result = document.querySelector('#my-element-id');
این روش و متد querySelector که بسیار شبیه به نمونه‌ی جی‌کوئری ارائه شده‌است، از زمان IE 8.0 قابل استفاده‌است.
در هر دو حالت، خروجی مقایسه‌ی ذیل، true است:
 result.id === 'my-element-id'; // returns true


یافتن عناصر بر اساس کلاس‌های CSS

<span class="some-class"></span>
با جی‌کوئری:
 var result = $('.some-class');
با جاوا اسکریپت خالص از زمان IE 8.0
  var result = document.getElementsByClassName('some-class');
و یا توسط querySelectorAll که شبیه به نمونه‌ی jQuery است و نیاز به پیشوند . را دارد:
 var result = document.querySelectorAll('.some-class');
در هر دو حالت، خروجی بازگشت داده شده، یک آرایه است:
 result[0].className === 'some-class'; // returns true


یافتن عناصر بر اساس تگ‌های عناصر

 <code>Console.WriteLine("Hello world!");</code>
با جی‌کوئری:
 var result = $('code');
با جاوا اسکریپت:
 var result = document.getElementsByTagName('code');
و یا
 var result = document.querySelectorAll('code');
در تمام این حالات، خروجی ارائه شده یک آرایه است:
 result[0].tagName === 'code'; // returns true



یافتن عناصر بر اساس کلاس نماها (Pseudo-classes)

Pseudo-classes از زمان ابتدایی‌ترین پیش‌نویس استاندارد CSS وجود داشته‌اند. برای مثال visited: یک Pseudo-classes است و به لینک‌های بازدید شده‌ی توسط کاربر اشاره می‌کند و یا focus: به المانی اشاره می‌کند که هم اکنون دارای focus است.
  <form>
     <label>Full Name
        <input name="full-name">
     </label>
     <label>Company
        <input name="company">
     </label>
  </form>
در این مثال اگر بخواهیم تکست باکسی را بیابیم که دارای focus است، روش جی‌کوئری آن به صورت زیر است:
  var focusedInputs = $('INPUT:focus');
و روش جاوا اسکریپتی آن به این صورت:
 var companyInput = document.querySelector('INPUT:focus');
کاری که در اینجا انجام شده ترکیب یک tag name و یک pseudo-class modifier است که جزئی از استاندارد CSS می‌باشد. بنابراین روش جی‌کوئری، چیزی بیشتر از انتقال این استاندارد به توابع بومی مرورگر نیست.


یافتن عناصر بر اساس ارتباط والد و فرزندی آن‌ها
  <div>
     <a href="https://www.dntips.ir">
        <span>Go to site</span>
     </a>
     <p>Some text</p>
     Some other text
  </div>
یافتن والدها:
روش یافتن والد anchor tag در جی‌کوئری توسط متد parent؛ با فرض اینکه a$ به شیء anchor اشاره می‌کند:
 var $result = $a.parent();
و در جاوا اسکریپت توسط خاصیت parentNode:
 var result = a.parentNode;

یافتن فرزندان:
در جی‌کوئری:
 var result = $('#myParent').children();
و برای یافتن فرزندان یک المان توسط CSS 2 child selectors:
 var result = document.querySelectorAll('DIV > *');
خروجی این کوئری، المان‌های a و p هستند و یا اگر فقط بخواهیم pها را انتخاب کنیم:
  var result = document.querySelectorAll('DIV > P');
روش دیگر انجام اینکار استفاده از خاصیت childNodes یک المان است:
 var result = document.getElementById('myParent').childNodes;
var result = div.childNodes;
البته این خاصیت آرایه‌ای، Text و Comments را هم علاوه بر عناصر بازگشت می‌دهد. البته اگر می‌خواهید آن‌ها را حذف کنید، از خاصیت children استفاده کنید:
 var result =document.getElementById('myParent').children;
و یا یافتن تمام المان‌های anchor ذیل المانی با Id مساوی myParent:
 var result =document.querySelectorAll('#myParent A');



جستجوی عناصر با صرفنظر کردن از بعضی از آن‌ها
  <ul role="menu">
     <li>choice 1</li>
     <li class="active">choice 2</li>
     <li>choice 3</li>
  </ul>
در این مثال گزینه‌ی دوم دارای class مساوی active است. اگر بخواهیم تمام liهایی را که دارای این کلاس نیستند، انتخاب کنیم، در جی‌کوئری خواهیم داشت:
 var $result = $('UL LI').not('.active');
و در جاوا اسکریپت:
 var result = document.querySelectorAll('UL LI:not(.active)');
هرچند JavaScript دارای متد not جی‌کوئری نیست، اما می‌توان از W3C CSS3 negation pseudo-class بجای آن استفاده کرد. مزیت آن، استاندارد بودن و عدم نیاز به کتابخانه‌ای ثالث برای تدارک آن است.


انتخاب چندین المان با هم

  <div id="link-container">
     <a href="https://github.com/VahidN">GitHub</a>
  </div>
  <ol>
     <li>one</li>
     <li>two</li>
  </ol>
  <span class="my-name">VahidN</span>
در اینجا می‌خواهیم المان‌های link-container، my-name و لیست مرتب شده را بدون نوشتن حلقه‌ای انتخاب کنیم. روش انجام اینکار در jQuery به صورت زیر است:
 var $result = $('#link-container, .my-name, OL');
و در جاوا اسکریپت خواهیم داشت:
 var result = document.querySelectorAll('#link-container, .my-name, OL');

یافتن گروهی از المان‌ها بر اساس نوع آن‌ها:
 var result = document.querySelectorAll(
 'BUTTON[type="submit"], INPUT[type="submit"]'
);
در اینجا تمام المان‌های ورودی از نوع <"button type="submit> و <"input type="submit> را بازگشت می‌دهد.


جایگزین کردن $ جی‌کوئری با جاوا اسکریپت

تا اینجا حتما به شباهت کدهای خالص جاوا اسکریپت و jQuery دقت کرده‌اید. اگر بخواهیم برای $ جی‌کوئری، یک معادل جاوا اسکریپتی تهیه کنیم، به قطعه کد زیر خواهیم رسید:
window.$ = function(selector) {
     return document.querySelectorAll(selector);
};
و نحوه‌ی استفاده‌ی از آن نیز همانند قبل است:
  $('.some-class');
  $('#some-id');
  $('.some-parent > .some-child');
  $('UL LI:not(.active)');
مطالب
ویژگی های کمتر استفاده شده در NET. - بخش چهارم

Parallel.For & Parallel.ForEach

Parallel.For – اجرای یک حلقه for که در آن عملیات تکرار  ممکن است به صورت موازی انجام شود.
var nums = Enumerable.Range( 0, 1000000 ).ToArray();
long total = 0;

// Use type parameter to make subtotal a long, not an int
Parallel.For< long >( 0, nums.Length, () => 0,
                      ( j, loop, subtotal ) =>
                      {
                          subtotal += nums[j];
                          return subtotal;
                      },
                      x => Interlocked.Add( ref total, x ) );
Console.WriteLine( "The total is {0:N0}", total ); 
Interlocked.Add با استفاده از این متد می‌توان دو عدد صحیح را با هم جمع کرد (به صورت thread safe) و نتیجه را در عدد اول ذخیره کرد.
Parallel.ForEach – اجرای یک حلقه foreach که در آن عملیات تکرار ممکن است به صورت موازی انجام شود.
var nums = Enumerable.Range( 0, 1000000 ).ToArray();
long total = 0;
Parallel.ForEach< int, long >( nums, // source collection
                               () => 0, // method to initialize the local variable
                               ( j, loop, subtotal ) => // method invoked by the loop on each iteration
                               {
                                   subtotal += j; //modify local variable
                                   return subtotal; // value to be passed to next iteration
                               },

                               // Method to be executed when each partition has completed.
                               // finalResult is the final value of subtotal for a particular partition.
                               finalResult => Interlocked.Add( ref total, finalResult ) );
Console.WriteLine( "The total from Parallel.ForEach is {0:N0}", total );


IsInfinity

تابع  IsInfinity  جهت ارزیابی یک مقدار اعشاری که به سمت مثبت یا منفی بی نهایت می‌باشد، استفاده می‌شود.
Console.WriteLine("IsInfinity(3.0 / 0) == {0}.", double.IsInfinity(3.0 / 0) ? "true" : "false");
مقدار خروجی مثال بالا true می‌باشد.

dynamic Type

با استفاده از نوع dynamic می توان عملیات چک کردن نوع در زمان کامپایل را پشت سر گذاشت و در عوض این عملیات را به زمان اجرا، موکول داد.

نکته: نوع dynamic همانند نوع object در بسیاری از شرایط، یکسان رفتار می‌کند. اگرچه عملیات‌هایی که شامل عبارت‌هایی از نوع dynamic هستند، یا نوع آن توسط کامپایلر بررسی می‌شوند و یا پذیرفته نمی‌شوند. کامپایلر اطلاعات مربوط به یک پردازش (روند) را یکجا بسته بندی می‌کند و این اطلاعات را بعداً در زمان اجرا ارزیابی می‌کند. به عنوان بخشی از این پردازش، متغیرهایی از نوع dynamic به متغیرهایی از نوع object کامپایل می‌شوند. بنابراین نوع dynamic فقط در زمان کامپایل وجود دارند (نه در زمان اجرا).

var i = 20;
dynamic dynamicVariable = i;
Console.WriteLine( dynamicVariable );

var stringVariable = "Example string.";
dynamicVariable = stringVariable;
Console.WriteLine( dynamicVariable );

var dateTimeVariable = DateTime.Today;
dynamicVariable = dateTimeVariable;
Console.WriteLine( dynamicVariable );

// The expression returns true unless dynamicVariable has the value null.
if ( dynamicVariable is dynamic )
    Console.WriteLine( "dynamicVariable variable is dynamic" );

// dynamic and the as operator.
dynamicVariable = i as dynamic;

// throw RuntimeBinderException if the associated object doesn't have the specified method.
// The code is still compiling successfully.
Console.WriteLine( dynamicVariable.ToNow1 );

همانطور که در مثال بالا مشاهده می‌کنید، شما می‌توانید متغیرهایی از نوع‌های مختلف را به یک شی از نوع dynamic اختصاص دهید. همچنین می‌توانید برای بررسی یک متغیر که از نوع dynamic است یا خیر، از عملگر is استفاده کنید. اگر یک خصوصیت را که وجود ندارد، درخواست کنید (خط آخر مثال بالا)، خطای RuntimeBinderException پرتاب می‌شود.


ExpandoObject

ExpandoObject  این امکان را فراهم می‌آورد که  در زمان اجرا، اعضای یک شیء به صورت پویا، اضافه و حذف شوند (همانند DataTableها).
dynamic sampleObject = new ExpandoObject();
sampleObject.FirstName = "Vahid";
sampleObject.LastName = "Mohammad Taheri";
sampleObject.Age = "28";
sampleObject.TestRemoveProperty = DateTime.Now;
sampleObject.AsString = new Action( () => Console.WriteLine( "{0} {1} is {2} years old.",
                                                                sampleObject.FirstName,
                                                                sampleObject.LastName,
                                                                sampleObject.Age ) );
sampleObject.AsString();
همانطور که در مثال بالا مشاهده می‌کنید، یک شیء با 4 خصوصیت و یک متد را ایجاد کردیم. حال برای حذف یکی از خصوصیت‌ها از روش زیر استفاده می‌کنیم.
( (IDictionary< String, Object >)sampleObject ).Remove( "TestRemoveProperty" );
و در صورت استفاده از خصوصیت حذف شده، خطای  RuntimeBinderException  پرتاب می‌شود.
مطالب
استفاده از Kendo UI TreeView به همراه یک منبع داده راه دور
یکی دیگر از ویجت‌های Kendo UI، ویجت نمایش ساختارهای درختی است به نام TreeView. در ادامه قصد داریم با نحوه‌ی نمایش آن، به کمک اطلاعات JSON دریافتی از سرور آشنا شویم.



ساختار مورد نیاز یک Kendo UI Tree View

فرض کنید قصد دارید نظرات تو در توی مطلبی را توسط Kendo UI Tree View نمایش دهید. مدل خود ارجاع دهنده‌ی آن می‌تواند چنین شکلی را داشته باشد:
namespace KendoUI11.Models
{
    public class BlogComment
    {
        public int Id { set; get; }
 
        public string Body { set; get; }
 
        public int? ParentId { get; set; }
 
        // مخصوص کندو یو آی هستند
        public bool HasChildren { get; set; }
        public string imageUrl { get; set; }
    }
}
سه خاصیت اول این کلاس همواره در تمام کلاس‌های خود ارجاع دهنده حضور دارند؛ شماره ردیف، متن و شماره Id والد احتمالی.
چند خاصیت بعدی مانند HasChildren و imageUrl مخصوص Kendo UI هستند. از imageUrl اختیاری می‌توان جهت نمایش آیکنی در کنار یک آیتم استفاده کرد و HasChildren به این معنا است که آیا گره جاری دارای عناصر فرزندی می‌باشد یا خیر.


تهیه یک منبع داده نمونه

شکل ابتدای مطلب، از طریق منبع داده ذیل تهیه شده‌است:
using System.Collections.Generic;
 
namespace KendoUI11.Models
{
    /// <summary>
    /// منبع داده فرضی جهت سهولت دموی برنامه
    /// </summary>
    public static class BlogCommentsDataSource
    {
        private static readonly IList<BlogComment> _cachedItems;
        static BlogCommentsDataSource()
        {
            _cachedItems = createBlogCommentsDataSource();
        }
 
        public static IList<BlogComment> LatestComments
        {
            get { return _cachedItems; }
        }
 
        /// <summary>
        /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
        /// </summary>
        private static IList<BlogComment> createBlogCommentsDataSource()
        {
            var list = new List<BlogComment>();
 
            var comment1 = new BlogComment
            {
                Id = 1, Body = "نظر من این است که", HasChildren = true, ParentId = null
            };
            list.Add(comment1);
 
            var comment12 = new BlogComment
            {
                Id = 2, Body = "پاسخی به نظر اول", HasChildren = true, ParentId = 1
            };
            list.Add(comment12);
 
            var comment12A = new BlogComment
            {
                Id = 3, Body = "پاسخی دیگری به نظر اول", HasChildren = false, ParentId = 1
            };
            list.Add(comment12A);
 
            var comment121 = new BlogComment
            {
                Id = 4, Body = "پاسخی به پاسخ به نظر اول", HasChildren = false, ParentId = 2
            };
            list.Add(comment121);
 
            var comment2 = new BlogComment
            {
                Id = 5, Body = "نظر 2", HasChildren = true, ParentId = null, imageUrl= "images/search.png"
            };
            list.Add(comment2);
 
            var comment21 = new BlogComment
            {
                Id = 6, Body = "پاسخ به نظر 2", HasChildren = false, ParentId = 5
            };
            list.Add(comment21);
 
            return list;
        }
    }
}
در اینجا نحوه‌ی مقدار دهی ParentId و HasChildren را جهت تو در تو سازی اطلاعات، مشاهده می‌کنید.
در این لیست دو رکورد، دارای ParentId مساوی null هستند. از این null بودن‌ها جهت کوئری گرفتن و نمایش ریشه‌های TreeView در ادامه استفاده خواهیم کرد.


بازگشت نظرات با فرمت JSON به سمت کلاینت

در ادامه یک کنترلر ASP.NET MVC را مشاهده می‌کنید که توسط اکشن متد GetBlogComments، رکوردهای مورد نظر را با فرمت JSON به سمت کلاینت ارسال می‌کند:
using System.Linq;
using System.Web.Mvc;
using KendoUI11.Models;
 
namespace KendoUI11.Controllers
{
 
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View(); // shows the page.
        }
 
        [HttpGet]
        public ActionResult GetBlogComments(int? id)
        {
            if (id == null)
            {
                //دریافت ریشه‌ها
                return Json(
                    BlogCommentsDataSource.LatestComments
                        .Where(x => x.ParentId == null) // ریشه‌ها
                        .ToList(),
                    JsonRequestBehavior.AllowGet);
            }
            else
            {
                //دریافت فرزندهای یک ریشه
                return Json(
                    BlogCommentsDataSource.LatestComments
                              .Where(x => x.ParentId == id)
                              .ToList(),
                              JsonRequestBehavior.AllowGet);
            }
        }
    }
}
اگر از سمت Kendo UI، مقدار id تنظیم نشود، به معنای درخواست نمایش ریشه‌ها است. در این حالت رکوردها را بر اساس مواردی که دارای ParentId مساوی null هستند، فیلتر خواهیم کرد.
اگر مقدار id به سمت سرور ارسال شود، یعنی کاربر گره و نودی را گشوده‌است. بر این اساس، تمامی فرزندان این گره را یافته و بازگشت می‌دهیم.


کدهای سمت کاربر نمایش Kendo UI Tree View

برای کار با Kendo UI TreeView نیاز است از منبع داده خاصی به نام HierarchicalDataSource به نحو ذیل استفاده کنیم. در قسمت transport آن مشخص می‌کنیم که اطلاعات باید از چه آدرسی خوانده شوند که در اینجا به آدرس اکشن متد  GetBlogComments اشاره می‌کند.
همچنین نیاز است مشخص کنیم کدامیک از خواص مدل بازگردانده شده، همان hasChildren است که در مثال فوق دقیقا به همین نام نیز تنظیم شده‌است.
<!--نحوه‌ی راست به چپ سازی -->
<div class="k-rtl k-header demo-section">
    <div id="my-treeview"></div>
</div>
 
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            var dataSource = new kendo.data.HierarchicalDataSource({
                transport: {
                    read: {
                        url: "@Url.Action("GetBlogComments", "Home")",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                schema: {
                    model: {
                        id: "Id",
                        hasChildren: "HasChildren"
                    }
                }
            });
 
            $("#my-treeview").kendoTreeView({
                //استفاده از قالب در صورت نیاز
                template: kendo.template($("#treeview-template").html()),
                checkboxes: {
                    checkChildren: false
                },
                dataSource: dataSource,
                dataTextField: "Body",
                //رخدادها
                select: function (e) { console.log("Selecting: " + this.text(e.node)); },
                check: function (e) { console.log("Checkbox changed :: " + this.text(e.node)); },
                change: function (e) { console.log("Selection changed"); },
                collapse: function (e) { console.log("Collapsing " + this.text(e.node)); },
                expand: function (e) { console.log("Expanding " + this.text(e.node)); }
            });
        });
    </script>
 
    <script id="treeview-template" type="text/kendo-ui-template">
        <strong> #: item.Body # </strong>
    </script>
 
    <style scoped>
        .demo-section {
            width: 100%;
            height: 300px;
        }
    </style>
}
 پس از تنظیم remote data source، اکنون نوبت به تعریف و تنظیم kendoTreeView است.
- در ابتدا به ازای هر ردیف این TreeView، از یک قالب استفاده شده‌است. تعریف این مورد اختیاری است. اگر نیاز به سفارشی سازی نحوه‌ی نمایش هر آیتم را داشتید، می‌توان از قالب‌ها استفاده کرد.
- قسمت checkboxes مشخص می‌کند که آیا نیاز است در کنار هر آیتم یک checkbox نیز نمایش داده شود یا خیر.
- dataSource را به HierarchicalDataSource تنظیم کرده‌ایم.
- dataTextField مشخص می‌کند که کدام فیلد دربرگیرنده‌ی متن هر آیتم TreeView است.
- تعدادی رخداد منتسب به TreeView نیز تنظیم شده‌اند که خروجی آن‌ها را در console تصویر ابتدای بحث مشاهده می‌کنید.


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

عموما اکثر کدهای موجود از روش زیر برای ساخت یک مسیر استفاده می‌کنند:
string path = somePath + "\\" + filename;

اما اگر همین برنامه تحت Mono در لینوکس اجرا شود به مشکل بر می‌خورد زیرا در لینوکس مسیرها این‌بار به صورت زیر هستند:
/somepath/filename

به همین جهت توصیه شده است برای ساخت مسیرها در برنامه‌ی خود، از متد Path.Combine موجود در فضای نام System.IO استفاده کنید زیرا این متد از مقادیر Path.DirectorySeperatorChar و Path.VolumeSeparatorChar جهت تهیه مسیر نهایی استفاده می‌کند. این مقادیر در ویندوز (\) و لینوکس (/) متفاوت بوده و به صورت خودکار در زمان اجرا توسط فریم ورک مورد استفاده مدیریت خواهند شد.
همچنین مزیت دیگر استفاده از Path.Combine ، تعیین اعتبار ورودی است؛ به این معنا که اگر از کاراکترهای غیرمجاز استفاده شود، یک استثناء صادر خواهد شد.

یک مورد دیگر هم شاید بد نباشد همینجا اضافه شود و آن هم فلسفه وجودی Environment.NewLine است. مطابق معمول رسم بر این است که سطر جدید با n\ در انتهای یک رشته مشخص شود اما این همیشه صحیح نیست و در پلتفرم‌های مختلف متفاوت است. Environment.NewLine در ویندوز مساوی r\n\ است و در سیستم‌های مبتنی بر Unix مساوی n\ خواهد بود. به همین جهت بهتر است از این پس بجای n\ از Environment.NewLine جهت مشخص سازی سطر جدید استفاده کنید.

نظرات مطالب
معرفی System.Text.Json در NET Core 3.0.
یک نکته‌ی تکمیلی: نوشتن تبدیلگرهای نوع‌ها برای System.Text.Json

System.Text.Json، در حال حاضر از مفهومی به نام type coercion/inference، پشتیبانی نمی‌کند. type coercion یعنی تبدیل یک مقدار، به مقداری دیگر که به صورت مستقیم قابل انتساب به یکدیگر نیستند. برای مثال اگر رشته‌ی "true" را درنظر بگیریم، قابلیت انتساب به یک خاصیت از نوع bool را ندارد. برای یک چنین مواردی در این API جدید، باید تبدیلگر نوشت.
یک مثال:
using System.Collections.Generic;
using System.Text.Json;

namespace JsonTests
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsInStock { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {            
         var products = JsonSerializer.Deserialize<List<Product>>("[{\"Id\":1026,\"Name\":\"P1\",\"IsInStock\":\"false\"}]");
        }
    }
}
در این مثال، خاصیت IsInStock از نوع bool است، اما مقداری را که باید از طریق متد Deserialize دریافت کنیم، یک رشته‌ی bool ای است که قابل انتساب به bool نیست. در این حالت اگر برنامه را اجرا کنیم به استثنای زیر خواهیم رسید:
An unhandled exception of type 'System.Text.Json.JsonException' occurred in System.Text.Json.dll
Inner exceptions found, see $exception in variables window for more details.
Innermost exception System.InvalidOperationException : Cannot get the value of a token type 'String' as a boolean.
برای رفع این مشکل، می‌توان تبدیلگر زیر را تدارک دید:
    public class BooleanConverter : JsonConverter<bool>
    {
        public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            if (value.Equals("true", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("yes", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("1", StringComparison.Ordinal))
            {
                return true;
            }

            if (value.Equals("false", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("no", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("0", StringComparison.Ordinal))
            {
                return false;
            }

            throw new NotSupportedException($"`{value}` can't be converted to `bool`.");
        }

        public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
        {
            switch (value)
            {
                case true:
                    writer.WriteStringValue("true");
                    break;
                case false:
                    writer.WriteStringValue("false");
                    break;
            }
        }
    }
برای نوشتن یک تبدیلگر bool، کلاس مرتبط، باید <JsonConverter<bool را پیاده سازی کند. کلاس JsonConverter نیز به صورت زیر تعریف شده‌است:
    public abstract class JsonConverter<T> : JsonConverter
    {
        protected internal JsonConverter();

        public override bool CanConvert(Type typeToConvert);
        public abstract T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
        public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
    }
و پیاده سازی دو متد Read و Write آن الزامی است.
در متد Read آن، مقدار رشته‌ای دریافت شده‌ی از منبع داده، در اختیار ما قرار می‌گیرد. سپس باید بر اساس این مقدار، مقدار متناظری را از نوع T که در اینجا bool است، بازگشت دهیم. برای مثال اگر یکی از مقادیر رشته‌ای true ،yes و 1 را دریافت کردیم، بجای آن true را بازگشت می‌دهیم.

اکنون برای استفاده‌ی از آن خواهیم داشت:
var options = new JsonSerializerOptions();
options.Converters.Add(new BooleanConverter());
var products = JsonSerializer.Deserialize<List<Product>>(
   "[{\"Id\":1026,\"Name\":\"P1\",\"IsInStock\":\"false\"}]",
   options);
و یا روش دیگر انجام اینکار، استفاده از ویژگی JsonConverter، برای معرفی تبدیلگر تهیه شده‌است:
[JsonConverter(typeof(BooleanConverter))]
public bool IsInStock { get; set; }

از این تبدیلگر برای حالت Serialize نیز می‌توان استفاده کرد:
var options  = new JsonSerializerOptions() {WriteIndented = true };
options.Converters.Add(new BooleanConverter());
var data = JsonSerializer.Serialize<List<Product>>(productList, options);
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت چهارم - انجام اعمال async با Redux
در قسمت اول این سری، با مفاهیم توابع خالص و ناخالص آشنا شدیم و همچنین عنوان شد که هرگونه تعامل با یک API خارجی به عنوان یک اثر جانبی و یا side effect در نظر گرفته شده و توابع را ناخالص می‌کند. به علاوه Redux تنها امکان کار با توابع خالص قابل پیش بینی را دارد و همچنین dispatch تمام اکشن‌ها توسط آن، به صورت پیش‌فرض synchronous است و نه asynchronous. اما در برنامه‌های واقعی نیاز است بتوان با یک API خارجی کارکرد و اطلاعاتی را از آن دریافت نمود و یا به آن ارسال کرد. برای رفع این مشکل، یک کتابخانه‌ی کمکی به نام redux-thunk ایجاد شده‌است که جزئیات کار با آن‌را در این قسمت بررسی می‌کنیم.


معرفی کتابخانه‌ی Redux Thunk

thunk، تابعی است که خروجی تابعی دیگر است؛ مانند مثال زیر:
function definitelyNotAThunk() {
   return function aThunk() {
     console.log('Hello, I am a thunk.');
   }
}
هدف اصلی از انجام یک چنین کاری، فراهم آوردن روشی برای اجرای به تاخیر افتاده‌است. برای مثال زمانیکه برای اجرای آن می‌نویسیم ()definitelyNotAThunk، تابع درونی آن هنوز اجرا نشده‌است. اجرای کامل آن چنین شکلی را دارد: ()()definitelyNotAThunk. حالا فرض کنید میان‌افزاری پیش از اجرای reducer قرار گرفته‌است که به تمام اشیاء رسیده‌ی به آن (یا همان اکشن‌ها در اینجا) گوش فرا می‌دهد. اگر اکشنی بجای یک شیء، یک تابع را بازگرداند، این میان‌افزار، آن‌را اجرا می‌کند یا همان ()() که عنوان شد. این کل کاری است که میان‌افزار 14 سطری redux-thunk انجام می‌دهد. زمانیکه از این میان‌افزار استفاده می‌شود، تابع درونی، دو پارامتر dispatch و getState را دریافت می‌کند. پارامتر dispatch که در حقیقت یک متد است، امکان اجرای اعمال synchronous و ارسال آن‌ها را به سمت reducer متناظر، میسر می‌کند.
برای مثال فرض کنید که نیاز است یک فراخوانی Ajax ای صورت گیرد و پس از پایان آن، جهت به روز رسانی state، یک شیء اکشن، به سمت reducer متناظری dispatch شود. مشکل اینجا است که نمی‌توان به Redux، یک callback حاصل از دریافت نتیجه‌ی عملیات Ajax ای و یا یک Promise را ارسال کرد. تمام این‌ها یک اثر جانبی یا side effect هستند که با توابع خالص Redux ای سازگاری ندارند. برای مدیریت یک چنین مواردی، یک میان‌افزار را به نام redux-thunk ایجاد کرده‌اند که اجازه‌ی dispatch تابعی را می‌دهد (همان thunk در اینجا) که قرار است action اصلی را در زمانی دیگر dispatch کند. به این ترتیب Redux اطلاعاتی را در مورد یک عمل async نخواهد داشت؛ میان‌افزاری در این بین آن‌را دریافت می‌کند و زمانیکه آن‌را dispatch می‌کنیم، آنگاه اکشن متناظر با آن، به redux منتقل می‌شود. به این ترتیب امکان منتظر ماندن تا زمان رسیدن پاسخ از شبکه، میسر می‌شود.
فرض کنید یک action creator متداول به صورت زیر ایجاد شده‌است:
export const getAllItems = () => ({
   type: UPDATE_ALL_ITEMS,
   items,
});
اکنون این سؤال مطرح می‌شود که چگونه می‌توان متوجه شد، پاسخی از سمت API دریافت شده‌است؟
برای پاسخ به این سؤال، اینبار action creator فوق را بر اساس الگوی redux-thunk به صورت زیر بازنویسی می‌کنیم:
export const getAllItems = () => {
  return dispatch => {
    Api.getAll().then(items => {
      dispatch({
        type: UPDATE_ALL_ITEMS,
        items,
      });
    });
  };
};
اینبار action creator ای را داریم که بجای بازگشت یک شیء، یک تابع را بازگشت داده‌است که به آن thunk گفته می‌شود و پارامتر dispatch را دریافت می‌کند. در این حالت زمانیکه یک Promise بازگشت داده می‌شود (امکان منتظر نتیجه شدن را فراهم می‌کند)، کار dispatch اکشن اصلی مدنظر (برای مثال ارسال آرایه‌ای از اشیاء)، صورت می‌گیرد.


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

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

پس از نصب پیش‌نیازها و راه اندازی برنامه‌ی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api، ایجاد می‌کنیم:
{
   "apiUrl": "https://localhost:5001/api"
}
در ادامه می‌خواهیم در برنامه‌ی React خود، لیست مطالب برنامه‌ی backend را از سرور دریافت کرده و نمایش دهیم.


افزودن میان‌افزار redux-thunk به برنامه

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


چون نیاز به عملیات async وجود دارد، باید از میان‌افزار مخصوص thunk برای انجام آن استفاده کرد. برای این منظور به فایل src\index.js مراجعه کرده و میان‌افزار thunk را توسط تابع applyMiddleware، به متد createStore، معرفی می‌کنیم:
import { applyMiddleware, compose, createStore } from "redux";
import thunk from "redux-thunk";

//...

const store = createStore(
  reducer,
  compose(
    applyMiddleware(thunk),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  )
);

//...
در اینجا چون نیاز است چندین تابع را به متد createStore ارسال کرد، باید از متد compose برای اعمال دسته جمعی آن‌ها کمک گرفت.


دریافت اطلاعات از یک API خارجی به کمک redux-thunk

پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux در قسمت قبل، پیاده سازی دریافت اطلاعات را بر اساس همان الگو، پیاده سازی می‌کنیم:
1)  ایجاد نام نوع اکشن متناظر با دکمه‌ی افزودن مقدار
به فایل src\constants\ActionTypes.js، نوع جدید دریافت مطالب را اضافه می‌کنیم:
export const GetPostsSuccess = "GetPostsSuccess";
export const GetPostsStarted = "GetPostsStarted";
export const GetPostsFailure = "GetPostsFailure";
در حین دریافت اطلاعات از API، حداقل سه اکشن نمایش loading (و یا GetPostsStarted در اینجا)، نمایش نهایی اطلاعات دریافت شده‌ی از سرور (و یا GetPostsSuccess در اینجا) و یا نمایش خطاهای حاصل (با نوع GetPostsFailure در اینجا) باید مدنظر باشند. به همین جهت سه نوع مختلف در اینجا تعریف شده‌اند.

2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کننده‌ی شیء اکشن ارسالی به reducer متناظری را تعریف می‌کنیم تا بتوان بر اساس نوع آن در reducer دریافت اطلاعات، منطق نهایی را پیاده سازی کرد:
import axios from "axios";

import { apiUrl } from "../config.json";
import * as types from "../constants/ActionTypes";

export const fetchPosts = () => {
  return (dispatch, getState) => {
    dispatch(getPostsStarted());
    axios.get(apiUrl + "/posts").then(response => {
      dispatch(getPostsSuccess(response.data)).catch(err => {
        dispatch(getPostsFailure(err));
      });
    });
  };
};

export const fetchPostsAsync = () => {
  return async (dispatch, getState) => {
    dispatch(getPostsStarted());
    try {
      const { data } = await axios.get(apiUrl + "/posts");
      console.log(data);
      dispatch(getPostsSuccess(data));
    } catch (error) {
      dispatch(getPostsFailure(error));
    }
  };
};

const getPostsSuccess = posts => ({
  type: types.GetPostsSuccess,
  payload: { posts }
});

const getPostsStarted = () => ({
  type: types.GetPostsStarted
});

const getPostsFailure = error => ({
  type: types.GetPostsFailure,
  payload: {
    error
  }
});
- در اینجا همان الگوی بازگشت یک تابع را بجای یک شیء، در توابع action creator، مشاهده می‌کنید.
- تابع fetchPosts، از همان روش قدیمی callback، برای مدیریت اطلاعات دریافتی از سرور استفاده می‌کند. زمانیکه اطلاعاتی دریافت شد، آن‌را با فراخوانی dispatch و با قالبی که تابع getPostsSuccess ارائه می‌دهد، به reducer متناظر، ارسال می‌کند.
- تابع fetchPostsAsync، نمونه‌ی به همراه async/await کار با کتابخانه‌ی axios است. هر دو روش callback و یا async/await در اینجا پشتیبانی می‌شوند.

به صورت پیش‌فرض action creators کتابخانه‌ی redux از اعمال async پشتیبانی نمی‌کنند. برای رفع این مشکل پس از ثبت میان‌افزار thunk، اینبار متدهای action creator، بجای بازگشت یک شیء، یک تابع را بازگشت می‌دهند که این تابع درونی در زمانی دیگر توسط میان‌افزار thunk و پیش از رسیدن به reducer، فراخوانی خواهد شد. این تابع درونی، دو پارامتر dispatch و  getState را دریافت می‌کند. هر دوی این‌ها نیز متد هستند. برای مثال اگر نیاز به دریافت وضعیت فعلی state در اینجا وجود داشت، می‌توان متد ()getState رسیده را فراخوانی کرد و حاصل آن‌را بررسی نمود. برای مثال شاید تصمیم گرفته شود که بر اساس وضعیت فعلی state، نیازی نیست تا اطلاعاتی از سرور دریافت شود و بهتر است همان اطلاعات کش شده‌ی موجود در state را بازگشت دهیم. البته در این مثال فقط از متد dispatch ارسالی، برای بازگشت نتیجه‌ی نهایی به reducer متناظر، استفاده شده‌است.

- در نهایت آرایه‌ی اشیاء مطلب دریافتی از سرور، به عنوان مقدار خاصیت posts شیء منتسب به خاصیت payload شیء ارسالی به reducer، در متد getPostsSuccess تعریف شده‌است. یعنی reducer متناظر، اطلاعات را از طریق خاصیت action.payload.posts شیء رسیده، دریافت می‌کند.
- همچنین دو اکشن شروع به دریافت اطلاعات (getPostsStarted) و بروز خطا (getPostsFailure) نیز در ابتدا و در قسمت catch عملیات async، به سمت reducer متناظر، dispatch خواهند شد.

3) ایجاد تابع reducer مخصوص دریافت اطلاعات از سرور
اکنون در فایل جدید src\reducers\posts.js، بر اساس نوع شیء رسیده و مقدار action.payload.posts آن، کار تامین آرایه‌ی posts موجود در state انجام می‌شود:
import * as types from "../constants/ActionTypes";

const initialState = { loading: false, posts: [], error: null };

export default function postsReducer(state = initialState, action) {
  switch (action.type) {
    case types.GetPostsStarted:
      return {
        loading: true,
        posts: [],
        error: null
      };
    case types.GetPostsSuccess:
      return {
        loading: false,
        posts: action.payload.posts,
        error: null
      };
    case types.GetPostsFailure:
      return {
        loading: false,
        posts: [],
        error: action.payload.error
      };
    default:
      return state;
  }
}
در این reducer با استفاده از یک switch، سه حالت ممکنی را که اکشن‌های رسیده‌ی به آن می‌توانند داشته باشند، مدیریت کرده‌ایم:
- در حالت آغاز کار و یا GetPostsStarted، با تنظیم خاصیت loading به true، سبب نمایش یک div «لطفا منتظر بمانید» خواهیم شد.
- در حالت دریافت نهایی اطلاعات از سرور، خاصیت loading به false تنظیم می‌شود تا div «لطفا منتظر بمانید» را مخفی کند. همچنین آرایه‌ی posts را نیز از payload رسیده استخراج کرده و به سمت کامپوننت‌ها ارسال می‌کند.
- در حالت بروز خطا و یا GetPostsFailure، خاصیت error شیء action.payload استخراج شده و جهت نمایش div متناظری، بازگشت داده می‌شود.

پس از تعریف این reducer باید آن‌را در فایل src\reducers\index.js به کمک combineReducers، با سایر reducer‌های موجود، ترکیب و یکی کرد تا در نهایت این rootReducer در فایل index.js اصلی برنامه، جهت ایجاد store اصلی redux، مورد استفاده قرار گیرد:
import { combineReducers } from "redux";

import counterReducer from "./counter";
import postsReducer from "./posts";

const rootReducer = combineReducers({
  counterReducer,
  postsReducer
});

export default rootReducer;


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

UI این قسمت از سه کامپوننت تشکیل شده‌است که کدهای کامل آن‌ها را در ادامه مشاهده می‌کنید:

الف) کامپوننت src\components\FetchPosts.jsx
import React from "react";

const FetchPosts = ({ fetchPostsAsync }) => {
  return (
    <section className="card mt-5">
      <div className="card-header text-center">
        <button className="btn btn-primary" onClick={fetchPostsAsync}>
          Fetch Posts
        </button>
      </div>
    </section>
  );
};

export default FetchPosts;
کار این کامپوننت، نمایش دکمه‌ی Fetch Posts است. با کلیک بر روی آن قرار است action creator ای به نام fetchPostsAsync اجرا شود که کدهای آن‌را پیشتر مرور کردیم.
همانطور که مشاهده می‌کنید، این کامپوننت هیچ اطلاعاتی از وجود کامپوننت دومی که قرار است لیست مطالب را نمایش دهد، ندارد. کارش تنها dispatch یک اکشن است.
بنابراین این کامپوننت از طریق props فقط یک اشاره‌گر به متد رویدادگردانی را دریافت می‌کند و اطلاعات دیگری را نیاز ندارد.

ب) کامپوننت src\components\Posts.jsx
import React from "react";

import Post from "./Post";

const Posts = ({ posts, loading, error }) => {
  return (
    <>
      <section className="card mt-5">
        <div className="card-header">
          <h2>Posts</h2>
        </div>
        <div className="card-body">
          {loading ? (
            <div className="alert alert-info">Loading ...</div>
          ) : (
            <div className="list-group list-group-flush">
              {posts.map(post => (
                <Post key={post.id} post={post} />
              ))}
            </div>
          )}
          {error && <div className="alert alert-warning">{error.message}</div>}
        </div>
      </section>
    </>
  );
};

export default Posts;
این کامپوننت، آرایه‌ای از اشیاء مطالب را دریافت کرده و با  ایجاد حلقه‌ای بر روی آن‌ها، توسط کامپوننت Post، هر کدام را در صفحه درج می‌کند. بنابراین این کامپوننت اکشنی را dispatch نمی‌کند. فقط از طریق props، یک آرایه‌ی posts، وضعیت جاری عملیات و خطاهای حاصل را باید دریافت کند.
در این کامپوننت اگر loading رسیده به true تنظیم شده باشد، یک div با عبارت loading نمایش داده می‌شود. در غیراینصورت، لیست مطالب را درج می‌کند. همچنین اگر خطایی نیز رخ داده باشد، آن‌را نیز درون یک div در صفحه نمایش می‌دهد.

ج) کامپوننت src\components\Post.jsx
import React from "react";

const Post = ({ post }) => {
  return (
    <article className="list-group-item">
      <header>
        <h2>{post.title}</h2>
      </header>
      <p>{post.body}</p>
    </article>
  );
};

export default Post;
این کامپوننت کار نمایش جزئیات هر رکورد مطلب را به عهده دارد؛ مانند نمایش عنوان و بدنه‌ی یک مطلب.


اتصال کامپوننت‌های FetchPosts و Posts به مخزن redux

مرحله‌ی آخر کار، تامین state کامپوننت‌های FetchPosts و Posts از طریق props است. به همین جهت باید دو دربرگیرنده را برای این دو کامپوننت ایجاد کنیم.
الف) ایجاد دربرگیرنده‌ی کامپوننت FetchPosts
برای این منظور فایل جدید src\containers\FetchPosts.js را با محتوای زیر ایجاد می‌کنیم:
import { connect } from "react-redux";

import { fetchPostsAsync } from "../actions";
import FetchPosts from "../components/FetchPosts";

const mapDispatchToProps = {
  fetchPostsAsync
};

export default connect(null, mapDispatchToProps)(FetchPosts);
- کار این تامین کننده، اتصال action creator ای به نام fetchPostsAsync به props کامپوننت FetchPosts است.
- چون اطلاعات state ای قرار نیست به این کامپوننت ارسال شود، تابع mapStateToProps را در اینجا مشاهده نمی‌کنید و با نال مقدار دهی شده‌است.

ب) ایجاد دربرگیرنده‌ی کامپوننت Posts
برای این منظور فایل جدید src\containers\Posts.js را با محتوای زیر ایجاد می‌کنیم:
import { connect } from "react-redux";

import Posts from "../components/Posts";

const mapStateToProps = state => {
  console.log("PostsContainer->mapStateToProps", state);
  return {
    ...state.postsReducer
  };
};

export default connect(mapStateToProps)(Posts);
- کار این تامین کننده، اتصال خاصیت posts بازگشت داده شده‌ی از طریق postsReducer، به props کامپوننت Posts است. البته چون کامپوننت Posts سه خاصیت { posts, loading, error } را از طریق props دریافت می‌کند، با استفاده از spread operator، هر سه خاصیت دریافتی از reducer را به صورت یک شیء جدید بازگشت داده‌ایم.
- کامپوننت Posts رویدادی را سبب نخواهد شد. به همین جهت تابع mapDispatchToProps را در اینجا تعریف و ذکر نکرده‌ایم.


استفاده از کامپوننت‌های دربرگیرنده جهت نمایش نهایی کامپوننت‌های تحت کنترل Redux

اکنون به فایل src\App.js مراجعه کرده و دو تامین کننده‌ی فوق را درج می‌کنیم:
import "./App.css";

import React from "react";

import CounterContainer from "./containers/Counter";
import FetchPostsContainer from "./containers/FetchPosts";
import PostsContainer from "./containers/Posts";

function App() {
  const prop1 = 123;
  return (
    <main className="container">
      <div className="row">
        <div className="col">
          <CounterContainer prop1={prop1} />
        </div>
        <div className="col">
          <FetchPostsContainer />
        </div>
        <div className="col">
          <PostsContainer />
        </div>
      </div>
    </main>
  );
}

export default App;
در اینجا FetchPostsContainer و PostsContainer سبب خواهند شد تا اتصالات مخزن اصلی redux، به کامپوننت‌هایی که توسط آن‌ها دربرگرفته شده‌اند، برقرار شود و کار تامین props آن‌ها صورت گیرد.

یک نکته: برای مثال در انتهای کامپوننت FetchPosts، سطر export default FetchPosts را داریم. اگر این سطر را حذف کنیم و بجای آن export default connect فوق را قرار دهیم، دیگر نیازی نخواهد بود تا FetchPostsContainer را از دربرگیرنده‌ها، import کرد و سپس بجای درج المان </FetchPosts> نوشت </FetchPostsContainer>. می‌توان همانند قبل از همان نام متداول </FetchPosts> استفاده کرد و import انجام شده نیز همانند سابق از همان فایل ماژول کامپوننت صورت می‌گیرد. یعنی می‌توان پوشه‌ی containers را حذف کرد و کدهای آن را دقیقا ذیل کلاس کامپوننت درج نمود.


کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-redux-mobx-part04-backend.zip و state-management-redux-mobx-part04-frontend.zip