مطالب دوره‌ها
جلوگیری از deadlock در برنامه‌های async
توضیح مطلب جاری نیاز به یک مثال دارد. به همین جهت یک برنامه‌ی WinForms یا WPF را آغاز کنید (تفاوتی نمی‌کند). سپس یک دکمه و یک برچسب را در صفحه قرار دهید. در ادامه کدهای فرم را به نحو ذیل تغییر دهید.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json.Linq;

namespace Async13
{
    public static class JsonExt
    {
        public static async Task<JObject> GetJsonAsync(this Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri);
                return JObject.Parse(jsonString);
            }
        }
    }

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnGo_Click(object sender, EventArgs e)
        {
            var url =
                "http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo";
            txtResult.Text = new Uri(url).GetJsonAsync().Result.ToString();
        }
    }
}
این کدها برای کامپایل نیاز به نصب بسته‌ی
 PM> Install-Package Newtonsoft.Json
و همچنین افزودن ارجاعی به اسمبلی استاندارد System.Net.Http نیز دارند.
در اینجا قصد داریم اطلاعات JSON دریافتی را در یک TextBox نمایش دهیم. کاری که انجام شده، فراخوانی متد async ایی است به نام GetJsonAsync و سپس استفاده از خاصیت Result این Task برای صبر کردن تا پایان عملیات.
اگر برنامه را اجرا کنید و بر روی دکمه‌ی دریافت اطلاعات کلیک نمائید، برنامه قفل خواهد کرد. چرا؟
البته تفاوتی هم نمی‌کند که این یک برنامه‌ی دسکتاپ است یا یک برنامه‌ی وب. در هر دو حالت یک deadlock کامل را مشاهده خواهید کرد.


علت بروز deadlock در کدهای async چیست؟

همواره نتیجه‌ی await، در context فراخوان آن بازگشت داده می‌شود. اگر برنامه‌ی دسکتاپ است، این context همان ترد اصلی UI برنامه می‌باشد و اگر برنامه‌ی وب است، این context، زمینه‌ی درخواست در حال پردازش می‌باشد.
خاصیت Result و یا استفاده از متد Wait یک Task، به صورت همزمان عمل می‌کنند و نه غیرهمزمان. متد GetJsonAsync یک Task ناتمام را که فراخوان آن باید جهت پایان‌اش صبر کند، بازگشت می‌دهد. سپس در همینجا کد فراخوان، تردجاری را توسط فراخوانی خاصیت Result قفل می‌کند. متد GetJsonAsync منتظر خواهد ایستاد تا این ترد آزاد شده و بتواند به کارش که بازگردان نتیجه‌ی عملیات به context جاری است، ادامه دهد.
به عبارتی، کدهای async منتظر پایان کار Result هستند تا نتیجه را بازگردانند. در همین لحظه کدهای همزمان برنامه نیز منتظر کدهای async هستند تا خاتمه یابند. نتیجه‌ی کار یک deadlock است.


روش‌های جلوگیری از deadlock در کدهای async؟

الف) در مورد متد ConfigureAwait در قسمت‌های قبل بحث شد و به عنوان یک best practice مطرح است:
    public static class JsonExt
    {
        public static async Task<JObject> GetJsonAsync(this Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri).ConfigureAwait(continueOnCapturedContext: false);
                return JObject.Parse(jsonString);
            }
        }
    }
با استفاده از ConfigureAwait false سبب خواهیم شد تا نتیجه‌ی عملیات به context جاری بازگشت داده نشود و نتیجه بر روی thread pool thread ادامه یابد. با اعمال این تغییر، کدهای متد btnGo_Click بدون مشکل اجرا خواهند شد.

ب) راه حل دوم، عدم استفاده از خواص و متدهای همزمان با متدهای غیر همزمان است:
        private async void btnGo_Click(object sender, EventArgs e)
        {
            var url =
                "http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo";
            var data = await new Uri(url).GetJsonAsync();
            txtResult.Text = data.ToString();
        }
ابتدا امضای متد رویدادگردان را اندکی تغییر داده و واژه‌ی کلیدی async را به آن اضافه می‌کنیم. سپس از await برای صبر کردن تا پایان عملیات متد GetJsonAsync استفاده خواهیم کرد. صبر کردنی که در اینجا انجام شده، یک asynchronous waits است؛ برخلاف روش همزمان استفاده از خاصیت Result یا متد Wait.


خلاصه‌ی بحث
Await را با متدهای همزمان Wait یا خاصیت Result بلاک نکنید. در غیراینصورت در ترد اجرا کننده‌ی دستورات، یک deadlock رخ‌خواهد داد؛ زیرا نتیجه‌ی await باید به context جاری بازگشت داده شود اما این context توسط خواص یا متدهای همزمان فراخوانی شده بعدی، قفل شده‌است.
مطالب
استفاده از AvalonEdit در WPF
AvalonEdit یکی از زیرساخت‌های برنامه‌ی SharpDevelop است که ویرایشگر متنی به همراه syntax highlighting زبان‌های مختلف را در آن پشتیبانی می‌کند. کیفیت بالایی داشته و بسیاری از برنامه‌های دیگر نیز از آن جهت ارائه ویرایشگر و یا syntax highlighting متون ارائه شده، استفاده می‌کنند. در ادامه نحوه‌ی استفاده از این ویرایشگر را در برنامه‌های WPF خصوصا با دید MVVM بررسی خواهیم کرد.



دریافت و نصب AvalonEdit

برای نصب AvalonEdit می‌توان دستور ذیل را در کنسول پاورشل نیوگت صادر کرد:
 PM> install-package AvalonEdit


استفاده‌ی مقدماتی از AvalonEdit

برای استفاده از این ویرایشگر ابتدا نیاز است فضای نام xmlns:avalonEdit تعریف شود. سپس کنترل avalonEdit:TextEditor در دسترس خواهد بود:
<Window x:Class="SyntaxHighlighter.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
   Title="MainWindow" Height="401" Width="617">
  <Grid>
<avalonEdit:TextEditor  
    Name="txtCode"
    SyntaxHighlighting="C#"
  FontFamily="Consolas"
  FontSize="10pt"/>   
  </Grid>
</Window>
توسط خاصیت SyntaxHighlighting آن می‌توان زبان مشخصی را تعریف کرد. لیست زبان‌های توکار پشتیبانی شده


استفاده از AvalonEdit در برنامه‌های MVVM

خاصیت Text این ویرایشگر به صورت معمولی تعریف شده (DependencyProperty نیست) و امکان binding دو طرفه به آن وجود ندارد. به همین جهت نیاز است یک چنین DependencyProperty را به آن اضافه کرد:
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Windows;
using System.Xml;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Highlighting.Xshd;

namespace AvalonEditWpfTest.Controls
{
    public class BindableAvalonTextEditor : TextEditor
    {
        public static readonly DependencyProperty BoundTextProperty =
            DependencyProperty.Register("BoundText",
                typeof(string),
                typeof(BindableAvalonTextEditor),
                new FrameworkPropertyMetadata(default(string), propertyChangedCallback));
 
        public static string GetBoundText(DependencyObject obj)
        {
            return (string)obj.GetValue(BoundTextProperty);
        }

        public static void SetBoundText(DependencyObject obj, string value)
        {
            obj.SetValue(BoundTextProperty, value);
        }

        protected override void OnTextChanged(EventArgs e)
        {
            SetCurrentValue(BoundTextProperty, Text);
            base.OnTextChanged(e);
        }

        private static void propertyChangedCallback(DependencyObject obj,
                                    DependencyPropertyChangedEventArgs args)
        {
            var target = (BindableAvalonTextEditor)obj;
            var value = args.NewValue;
            if (value == null)
                return;

            if (string.IsNullOrWhiteSpace(target.Text) ||
                !target.Text.Equals(args.NewValue.ToString()))
            {
                target.Text = args.NewValue.ToString();
            }
        }
    }
}
کار با ارث بری از TextEditor (ویرایشگر AvalonEdit) شروع می‌شود. سپس یک DependencyProperty به نام BoundText در اینجا اضافه شده‌است. هر زمان که متن داخل آن تغییر کرد، آن‌را به خاصیت متنی Text این ویرایشگر نسبت می‌دهد. به این ترتیب binding یک طرفه (از کدها به کنترل) کار می‌کند. فعال سازی binding دو طرفه با پشتیبانی از انتقال تغییرات از ویرایشگر به خواص ViewModel در متد بازنویسی شده‌ی OnTextChanged انجام می‌شود.

اکنون برای استفاده از این کنترل جدید که BindableAvalonTextEditor نام دارد، می‌توان به نحو ذیل عمل کرد:
<Window x:Class="AvalonEditWpfTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewModels="clr-namespace:AvalonEditTests.ViewModels"
        xmlns:controls="clr-namespace:AvalonEditWpfTest.Controls"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <viewModels:MainWindowViewModel x:Key="MainWindowViewModel"/>
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource MainWindowViewModel}}">
        <controls:BindableAvalonTextEditor
                BoundText="{Binding SourceCode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                        WordWrap="True"
                        ShowLineNumbers="True"
                        LineNumbersForeground="MediumSlateBlue"
                        FontFamily="Consolas"
                        VerticalScrollBarVisibility="Auto"
                        Margin="3"          
                        HorizontalScrollBarVisibility="Auto"
                        FontSize="10pt"/>
    </Grid>
</Window>
ابتدا فضای نام جدید کنترل BindableAvalonTextEditor مشخص می‌شود و سپس به controls:BindableAvalonTextEditor دسترسی خواهیم داشت. در اینجا نحوه‌ی استفاده از خاصیت جدید BoundText را نیز مشاهده می‌کنید.


افزودن syntax highlighting زبان‌هایی که به صورت رسمی پشتیبانی نمی‌شوند

به خاصیت SyntaxHighlighting این کنترل صرفا مقادیری را می‌توان نسبت داد که به صورت توکار پشتیبانی می‌شوند. برای مثال#XML، C و امثال آن.
فرض کنید نیاز است SyntaxHighlighting زبان SQL را فعال کنیم. برای اینکار نیاز به فایل‌های ویژه‌ای است، با پسوند xshd. برای نمونه فایل sql-ce.xshd را در اینجا می‌توانید مطالعه کنید. در آن یک سری واژه‌های کلیدی و حروفی که باید با رنگی متفاوت نمایش داده شوند، مشخص می‌گردند.
برای استفاده از فایل sql-ce.xshd باید به نحو ذیل عمل کرد:
الف) فایل sql-ce.xshd را به پروژه اضافه کرده و سپس در برگه‌ی خواص آن در VS.NET، مقدار build action آن‌را به embedded resource تغییر دهید.



ب) با استفاده از متد ذیل، این فایل مدفون شده در اسمبلی را گشوده و به متد HighlightingLoader.Load ارسال می‌کنیم:
        private static IHighlightingDefinition getHighlightingDefinition(string resourceName)
        {
            if (string.IsNullOrWhiteSpace(resourceName))
                throw new NullReferenceException("Please specify SyntaxHighlightingResourceName.");

            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
            {
                if (stream == null)
                    throw new NullReferenceException(string.Format("{0} resource is null.", resourceName));

                using (var reader = new XmlTextReader(stream))
                {
                    return HighlightingLoader.Load(reader, HighlightingManager.Instance);
                }
            }
        }
نحوه استفاده از آن نیز به صورت ذیل است:
 txtCode.SyntaxHighlighting = getHighlightingDefinition(resourceName);
به این ترتیب می‌توان یک فایل xhsd را به صورت پویا بارگذاری و به خاصیت SyntaxHighlighting کنترل انتساب داد.

برای سهولت استفاده از این قابلیت شاید بهتر باشد یک DependencyProperty دیگر به نام SyntaxHighlightingResourceName را به کنترل جدید BindableAvalonTextEditor اضافه کنیم:
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Windows;
using System.Xml;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Highlighting.Xshd;

namespace AvalonEditWpfTest.Controls
{
    public class BindableAvalonTextEditor : TextEditor
    {
         public static readonly DependencyProperty SyntaxHighlightingResourceNameProperty =
           DependencyProperty.Register("SyntaxHighlightingResourceName",
               typeof(string),
               typeof(BindableAvalonTextEditor),
               new FrameworkPropertyMetadata(default(string), resourceNamePropertyChangedCallback));
 
        public static string GetSyntaxHighlightingResourceName(DependencyObject obj)
        {
            return (string)obj.GetValue(SyntaxHighlightingResourceNameProperty);
        }

        public static void SetSyntaxHighlightingResourceName(DependencyObject obj, string value)
        {
            obj.SetValue(SyntaxHighlightingResourceNameProperty, value);
        }

        private static void loadHighlighter(TextEditor @this, string resourceName)
        {
            if (@this.SyntaxHighlighting != null)
                return;

            @this.SyntaxHighlighting = getHighlightingDefinition(resourceName);
        }

        private static void resourceNamePropertyChangedCallback(DependencyObject obj,
                                            DependencyPropertyChangedEventArgs args)
        {
            var target = (BindableAvalonTextEditor)obj;
            var value = args.NewValue;
            if (value == null)
                return;

            loadHighlighter(target, value.ToString());
        }
    }
}
کاری که در اینجا انجام شده، افزودن یک خاصیت جدید به نام SyntaxHighlightingResourceName به کنترل BindableAvalonTextEditor است. هر زمانیکه مقدار آن تغییر کند، متد getHighlightingDefinition بحث شده، فراخوانی گردیده و به صورت پویا مقدار خاصیت SyntaxHighlighting این کنترل، مقدار دهی می‌شود.
استفاده از آن نیز به شکل زیر است:
   <controls:BindableAvalonTextEditor
SyntaxHighlightingResourceName = "AvalonEditWpfTest.Controls.sql-ce.xshd"
/>

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
AvalonEditWpfTest.zip 
مطالب
Vue.js - نحوه‌ی ایجاد یک Vue جدید و استفاده از معماری MVVM - قسمت دوم
در قسمت قبلی توضیحاتی جهت معرفی و نصب فریم‌ورک داده شد. در این قسمت قصد داریم کمی بیشتر با ساختار آن آشنا شویم و یک vue جدید را ایجاد کنیم. پس لازم است تا ابتدا درون تگ اسکریپتی که فراخوانی کردیم و آدرس فریم‌ورک در آن قرار داده شده‌است، به صورت زیر یک ویو جدید را ایجاد کنیم:
<html>
    <body>
       <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js">  
 new Vue({ 
               
            }); 
       </script>
    </body>
</html>
ساختار vue.js بدین صورت است که از شما می‌خواهد تعیین کنید این کدها محدود و مربوط به کدام صفحه می‌شوند.
اگر قصد داشته باشید تا درون تگی با یک مشخصه‌ی خاص، دستوری را اجرا کنید، باید تعیین کنید که آن تگ چه خواصی دارد. فرض می‌کنیم که تگ body درون پروژه لازم است مقداری را فراخوانی و اجرا کند. پس لازم است تا یک id، به تگ بادی اختصاص یابد. 
<html>
    <body id="dotnettips">
       <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js">  
 new Vue({ 
               
            }); 
       </script>
    </body>
</html>
در کد بالا یک آیدی با نام dotnettips، به تگ بادی داده شد. حال قصد داریم تا با استفاده از دستور new Vue، رشته یا مقداری مشخص را در خروجی قرار دهیم. پس باید یک مشخصه را برای آن تعریف کرد که ما طبق مستندات فریم‌ورک، مشخصه‌ای را با نام el فراخوانی می‌کنیم:
<html>
    <body id="dotnettips">
       <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js">  
       new Vue({ 
           el: '#dotnettips',
           data: {
                name: 'dotnettips'
               }
            });
       </script>
    </body>
</html>
با استفاده از مشخصه el تعیین کردیم که کدهای برنامه برای کدام id در نظر گرفته شده‌اند. حال با استفاده از مقدار خاصیت name، مقادیر ما در خروجی چاپ خواهند شد.


استفاده از MVVM درون پروژه
به زبان ساده در معماری MVVM، یک مدل، یک view و یک viewmodel داریم که در مدل، کدهای JS و اطلاعاتی که قصد نمایش آن را داریم، قرار می‌دهیم. در واقع viewmodel نقش data building را خواهد داشت.

برای شروع، یک مدل جدید را نیاز داریم که بدین شکل باید فراخوانی کنیم.
 //new model 
var SampleData =( 
        name: 'dotnettips' 
)
در کد بالا ما یک مدل جدید ساخته‌ایم و یک ویژگی را به آن نسبت دادیم. حال لازم است تا ویوی جدیدی را برای نمایش آن ایجاد کنیم.
  • برای نمایش اطلاعات درون ویو جی اس باید از {{ }} استفاده کنید، تا ویژگی ساخته و فراخوانی شده را نمایش دهد.
<div id="dotnettips"> 
 Hello  {{ name }} 
</div>
مثال بالا، ویوی ما خواهد بود که ویژگی مورد نظر را در خروجی نمایش می‌دهد. حال، باید هردو را با viewmodel بهم متصل کنیم.
یک ویوی جدید را ایجاد می‌کنیم و با مشخصه el، ویژگی تعیین شده را به مدل خود متصل می‌کنیم.
new Vue({
  el:'#dotnettips',
  data: SampleData
})
 .یک نمونه از این مثال را از اینجا می‌توانید مشاهده کنید  
.به نمونه کد کامل زیر دقت بفرمائید
<html>  
    <body id="dotnettips">  
          
        <h3 id="dotnettips"> 
            Hello  {{ name }} 
        </h3>  
      
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js">
            
        </script>  
        <script type="text/javascript"> 
            new Vue({  
            el: '#dotnettips',  
                data:{  
                    name: 'dotnettips'  
                }  
            });  
        </script>  
        </body>  
</html>

مطالب
اجرای متد از طریق Reflection
برای اجرای متد درون یک کلاس از طریق Reflection  ابتدا نوع آن کلاس را به دست می‌آوریم و سپس از طریق کلاس Activator.CreateInstance یک نمونه از آن کلاس را ساخته و در متغیری از نوع object  ذخیره کرده و با استفاده از GetMethod اطلاعات متد مورد نظر خود را در متغیری ذخیره کرده و سپس از طریق دستور Invoke آن متد را اجرا می‌کنیم. دستور Invoke دو سربارگذاری دارد که در یک نوع از آن، متغیر حاوی نمونه کلاس و پارامترهای متد مورد نظر، در قالب یک آرایه از نوع object،  به عنوان آرگومان پذیرفته می‌شود. با امضای زیر
public Object Invoke(Object obj, Object[] parameters) 

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

public class TestMath
    {
        public int Squar(int i)
        {
            return i*i;
        }
    }

static void Main(string[] args)
        {            
            Type type = typeof (TestMath);//به دست آوردن نوع کلاس
            object obj = Activator.CreateInstance(type);//ساختن نمونه‌ای از نوع مورد نظر          
            MethodInfo methodInfo = type.GetMethod("Squar");//یافتن اطلاعات متد مورد نظر
            Console.WriteLine(methodInfo.Invoke(obj, new object[] { 100 }));// و نمایش نتیجه object[]   ارسال عدد 100 به صورت 
            Console.Read();
        }     

توجه کنید که دو متد GetMethod و Invoke در فضای نام System.Reflection قرار دارند.

روش دیگر

در شیوه دیگر برای انجام این کار، نیازی به استفاده از GetMethod و Invoke نیست و فراخوانی متد مورد نظر بسیار شبیه فراخوانی عادی متدهاست و نیازی به ساخت متغیر ویژه‌ای از نوع []object برای ارسال پارامترها نیست. برای انجام این کار فقط کافیست نوع متغیری که نوع نمونه‌سازی شده را نگه‌می‌دارد (در اینجا نمونه ای از کلاس را نگه‌می‌دارد)  به صورت dynamic باشد:

static void Main(string[] args)
        {            
            Type type = typeof (TestMath);
            dynamic obj = Activator.CreateInstance(type);
            Console.WriteLine(obj.Square(100));                     
            Console.Read();
        }
 توجه کنید که بعد از تعریف  obj، با درج نقطه در کنار آن، منوی Code Insight متد Square را شامل نمی‌شود اما کامپایلر آن را می‌پذیرد.
مطالب
String.format در جاوا اسکریپت
مقدمه 
با اینکه زبان برنامه نویسی جاوا اسکریپت زبانی بسیار قدرتمند و با امکانات زیاد است، اما فقدان برخی متدهای کمکی پرمصرف در آن در برخی موارد باعث دردسرهایی می‌شود. امکانی برای فرمت‌بندی رشته‌ها یکی از این نیازهای نسبتا پرکاربرد است.
متدی که در این مطلب قصد توضیح پیاده‌سازی آنرا داریم، String.format نام دارد که فرایندی مشابه متد متناظر در دات نت را انجام می‌دهد. هم‌چنین سعی شده است تا نحوه پیاده‌سازی این متد کمکی از ابتدایی‌ترین نمونه‌ها تا نسخه‌های پیشرفته‌تر برای درک بهتر مطلب نشان داده شود.
.
پیاده‌سازی متد String.format
1. در این پیاده‌سازی از اولین فرایندی که ممکن است به ذهن یک برنامه‌نویس خطور کند استفاده شده است. این پیاده‌سازی بسیار ساده به صورت زیر است:
String.format = function () {
  var s = arguments[0];
  for (var i = 0; i < arguments.length - 1; i++) {
    s = s.replace("{" + i + "}", arguments[i + 1]);
  }
  return s;
};

2. پیاده‌سازی مشابهی هم با استفاده از نوع دیگری از حلقه for که تقریبا! مشابه با حلقه foreach در #C است به صورت زیر می‌توان درنظر گرفت:
String.format = function () {
  var s = arguments[0];
  for (var arg in arguments) {
    var i = parseInt(arg);
    s = s.replace("{" + i + "}", arguments[i + 1]);
  }
  return s;
};
در این متدها ابتدا فرمت واردشده توسط کاربر از لیست آرگومان‌های متد خوانده شده و در متغیر s ذخیره می‌شود. سپس درون یک حلقه به ازای هر توکن موجود در رشته فرمت، یک عملیات replace با مقدار متناظر در لیست آرگومان‌های متد انجام می‌شود. نحوه استفاده از این متد نیز به صورت زیر است:
console.log(String.format("{0} is nice!", "donettips.info"));
هر دو متد خروجی یکسانی دارند، به صورت زیر:
donettips.info is nice!
تا اینجا به نظر می‌رسد که عملیات به‌درستی پیش می‌رود. اما اولین و بزرگ‌ترین مشکل در این دو متد نحوه کارکردن متد replace در جاوا اسکریپت است. این متد با این نحوه فراخوانی تنها اولین توکن موجود را یافته و عملیات جایگزینی را برای آن انجام می‌دهد. برای روشن‌تر شدن موضوع به مثال زیر توجه کنید:
console.log(String.format("{0} is {1} nice! {0} is {1} nice!", "donettips.info", "very"));
با اجرای این مثال نتیجه زیر حاصل می‌شود:
donettips.info is very nice! {0} is {1} nice!
همان‌طور که می‌بنید عملیات replace برای سایر توکن‌ها انجام نمی‌شود.

3. برای حل مشکل فوق می‌توان از روش ساده زیر استفاده کرد:
String.format = function () {
  var original = arguments[0],
      replaced;
  for (var i = 0; i < arguments.length - 1; i++) {
    replaced = '';
    while (replaced != original) {
      original = replaced || original;
      replaced = original.replace("{" + i + "}", arguments[i + 1]);
    }
  }
  return replaced;
};
در این روش عملیات replace تا زمانی‌که تغییری در رشته جاری ایجاد نشود ادامه می‌یابد. با استفاده از این متد، خروجی مثال قبل درست و به صورت زیر خواهد بود:
donettips.info is very nice! donettips.info is very nice!

4. راه حل دیگر استفاده از امکانات شی RegExp در دستور replace است. نکته مهم استفاده از modifier کلی یا global (که با حرف g مشخص می‌شود) در شی تولیدی از RegExp است (^ و ^ و ^، برای جلوگیری از دورشدن از بحث اصلی، جستجو برای کسب اطلاعات بیشتر در این زمینه به خوانندگان واگذار می‌شود). برای استفاده از این شی متد ما به صورت زیر تغییر می‌کند:
String.format = function () {
  var s = arguments[0];
  for (var i = 0; i < arguments.length - 1; i++) {
    s = s.replace(new RegExp("\\{" + i + "\\}", "g"), arguments[i + 1]);
  }
  return s;
};
استفاده از این متد هم نتیجه درستی برای مثال آخر ارائه می‌دهد.

5. روش دیگری که کمی از دو متد قبلی سریع‌تر اجرا می‌شود (به دلیل استفاده از حلقه while) به صورت زیر است:
String.format = function () {
  var s = arguments[0],
      i = arguments.length - 1;
  while (i--) {
    s = s.replace(new RegExp('\\{' + i + '\\}', 'g'), arguments[i + 1]);
  }
  return s;
};
این متد نیز نتیجه مشابهی ارائه می‌کند. حال به مثال زیر توجه کنید:
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "{2}", "two"));
خروجی صحیح مثال فوق باید به صورت زیر باشد:
zero:0 {2}:1 two:2
درصورتی‌که رشته‌ای که دو متد از سه متد آخر (3 و 4) به عنوان خروجی ارائه می‌دهند به‌صورت زیر است:
zero:0 two:1 two:2
برای آخرین متد که ازحلقه while (درواقع با اندیس معکوس) استفاده می‌کند (5) مثالی که خطای مورد بحث را نشان می‌دهد به صورت زیر است:
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "one", "{1}"));
که خروجی اشتباه زیر را برمی‌گرداند:
zero:0 one:1 one:2
درصورتی‌که باید مقدار زیر را برگشت دهد:
zero:0 one:1 {1}:2
دلیل رخدادن این خطا اجرای عملیات replace به صورت جداگانه و کامل برای هر توکن، از اول تا آخر برای رشته‌های replace شده جاری است که کار را خراب می‌کند.

6. برای حل مشکل بالا نیز می‌توان از یکی دیگر از امکانات دستور replace استفاده کرد که به صورت زیر است:
String.format = function () {
  var args = arguments;
  return args[0].replace(/{(\d+)}/g, function (match, number) { return args[parseInt(number) + 1]; });
};
در اینجا از قابلیت سفارشی‌سازی عملیات جایگزینی در دستور replace استفاده شده است. با استفاده از این ویژگی عملیات replace برای هر توکن جداگانه انجام می‌شود و بنابراین تغییرات اعمالی در حین عملیات تاثیر مستقیمی برای ادامه روند نخواهد گذاشت.
دقت کنید که برای بکاربردن RegExp درون دستور replace به جای تولید یک نمونه از شی RegExp می‌توان عبارت مربوطه را نیز مستقیما بکار برد. در اینجا از عبارتی کلی برای دریافت تمامی توکن‌های با فرمتی به صورت {عدد} استفاده شده است.
متد سفارشی مربوطه نیز شماره ردیف توکن یافته‌شده به همراه خود عبارت یافته‌شده را به عنوان آرگومان ورودی دریافت کرده و مقدار متناظر را از لیست آرگومان‌های متد اصلی پس از تبدیل شماره ردیف توکن به یک عدد، برگشت می‌دهد (در اینجا نیز برای جلوگیری از دورشدن از بحث اصلی، جستجو برای کسب اطلاعات بیشتر در این زمینه به خوانندگان واگذار می‌شود).
برای جلوگیری از تداخل بین آرگومان‌های متد اصلی و متد تهیه‌شده برای سفارشی‌سازی عملیات جایگزینی، در ایتدای متد اصلی، لیست آرگومان‌های آن درون متغیر جداگانه‌ای (args) ذخیره شده است.
با استفاده از این متد خروجی درست نشان داده می‌شود. حال مثال زیر را درنظر بگیرید:
console.log(String.format("{0} is {1} nice!", "donettips.info"));
خروجی این مثال به‌صورت زیر است:
donettips.info is undefined nice!
پیاده‌سازی زیر برای حل این مشکل استفاده می‌شود.

7. برای کنترل بیشتر و رفع خطاهای احتمالی در متد بالا، می‌توان ابتدا از وجود آرگومان مربوطه در متغیر args اطمینان حاصل کرد تا از جایگزینی مقدار undefined در رشته نهایی جلوگیری کرد. مانند نمونه زیر:
String.format = function () {
  var s = arguments[0],
      args = arguments;
  return s.replace(/{(\d+)}/g, function (match, number) {
    var i = parseInt(number);
    return typeof args[i + 1] != 'undefined' ? args[i + 1] : match;
  });
};
با استفاده از این متد جدید خروجی مثال‌های قبل درست خواهد بود.
در فرمت بندی رشته‌ها برای نمایش خود کاراکتر { یا } از تکرار آن‌ها (یعنی {{ یا }}) استفاده می‌شود. اما متد ما تا این لحظه این امکان را ندارد. برای مثال:
console.log(String.format("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}}  {{{{2}}}}   {2}", "zero", "{2}", "two"));
که خروجی زیر را ارائه می‌دهد:
zero:0 {2}:1 two:2, {zero} {{{2}}}  {{{two}}}   two
.
8. برای پیاده‌سازی امکان اشاره‌شده در بالا می‌توان از کد زیر استفاده کرد:
String.format = function () {
  var s = arguments[0],
      args = arguments;
  return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) {
    if (match == "{{") { return "{"; }
    if (match == "}}") { return "}"; }
    var i = parseInt(number);
    return typeof args[i + 1] != 'undefined'
                              ? args[i + 1]
                              : match;
  });
};
در اینجا با استفاده از یک عبارت RegExp پیچیده‌تر و کنترل تکرار کاراکترهای { و } در متد سفارشی جایگزینی در دستور replace، پیاده‌سازی اولیه این ویژگی ارائه شده است.
این متد خروجی صحیح زیر را برای مثال آخر ارائه می‌دهد:
zero:0 {2}:1 two:2, {0} {{2}}  {{2}}   two

پیاده‌سازی به‌صورت یک خاصیت prototype
تمامی متدهای نشان داده‌شده تا اینجا به‌صورت مستقیم از طریق String.format در دسترس خواهند بود (تعریفی شبیه به متدهای استاتیک در دات نت). درصورتی‌که بخواهیم از این متدها به صورت یک خاصیت prototype شی string استفاده کنیم (چیزی شبیه به متدهای instance در اشیای دات نت) می‌توانیم از تعریف زیر استفاده کنیم:
String.prototype.format = function () {
   ...
}
تنها فرق مهم این پیاده‌سازی این است که رشته مربوط به فرمت وارده در این متد از طریق شی this در دسترس است و بنابراین شماره اندیس آرگومان‌های متد یکی کمتر از متدهای قبلی است که باید مدنظر قرار گیرد. مثلا برای متد آخر خواهیم داشت:
String.prototype.format = function () {
  var s = this.toString(),
      args = arguments;
  return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) {
    if (match == "{{") { return "{"; }
    if (match == "}}") { return "}"; }
    return typeof args[number] != 'undefined'
                              ? args[number]
                              : match;
  });
};

نکته: در تمامی خواص prototype هر شی در جاوا اسکریپت، متغیر this از نوع object است. بنابراین برای جلوگیری از وقوع هر خطا بهتر است ابتدا آن‌را به نوع مناسب تبدیل کرد. مثل استفاده از متد toString در متد فوق که موجب تبدیل آن به رشته می‌شود.

ازآنجاکه نیاز به تغییر اندیس در متد سفارشی عملیات replace وجود ندارد، بنابراین خط مربوط به تبدیل آرگومان number به یک مقدار عددی (با دستور parseInt) حذف شده است و از این متغیر به صورت مستقیم استفاده شده است. در این حالت عملیات تبدیل توسط خود جاوا اسکریپت مدیریت می‌شود که کار را راحت‌تر می‌سازد.
بنابراین متد ما به صورت زیر قابل استفاده است:
console.log("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}}  {{{{2}}}}   {2}".format("zero", "{2}", "two"));

پیاده‌سازی با استفاده از توکن‌های غیرعددی
برای استفاده از توکن‌های غیرعددی می‌توانیم به صورت زیر عمل کنیم:
String.format = function () {
  var s = arguments[0],
      args = arguments[1];
  for (var arg in args) {
    s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]);
  }
  return s;
};
برای حالت prototype نیز داریم:
String.prototype.format = function () {
  var s = this.toString(),
      args = arguments[0];
  for (var arg in args) {
    s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]);
  }
  return s;
};
با استفاده از این دو متد داریم:
console.log(String.format("{site} is {adj}! {site} is {adj}!", { site: "donettips.info", adj: "nice" }));
console.log("{site} is {adj}! {site} is {adj}!".format({ site: "donettips.info", adj: "nice" }));
.
تا اینجا متدهایی نسبتا کامل برای نیازهای عادی برنامه‌نویسی تهیه شده است. البته کار توسعه این متد برای پشتیبانی از امکانات پیشرفته‌تر فرمت‌بندی رشته‌ها می‌تواند ادامه پیدا کند.

کتابخانه‌های موجود
یکی از کامل‌ترین کتابخانه‌های کار با رشته‌ها همان کتابخانه معروف Microsoft Ajax Client Libray است که بیشتر امکانات موجود کار با رشته‌ها در دات نت را در خود دارد. صرفا جهت آشنایی، پیاده‌سازی متد String.format در این کتابخانه در زیر آورده شده است:
String.format = function String$format(format, args) {
  /// <summary locid="M:J#String.format" />
  /// <param name="format" type="String"></param>
  /// <param name="args" parameterArray="true" mayBeNull="true"></param>
  /// <returns type="String"></returns>
//  var e = Function._validateParams(arguments, [
//    { name: "format", type: String },
//    { name: "args", mayBeNull: true, parameterArray: true }
//  ]);
//  if (e) throw e;
  return String._toFormattedString(false, arguments);
};
String._toFormattedString = function String$_toFormattedString(useLocale, args) {
  var result = '';
  var format = args[0];
  for (var i = 0; ; ) {
    var open = format.indexOf('{', i);
    var close = format.indexOf('}', i);
    if ((open < 0) && (close < 0)) {
      result += format.slice(i);
      break;
    }
    if ((close > 0) && ((close < open) || (open < 0))) {
      if (format.charAt(close + 1) !== '}') {
        throw Error.argument('format', Sys.Res.stringFormatBraceMismatch);
      }
      result += format.slice(i, close + 1);
      i = close + 2;
      continue;
    }
    result += format.slice(i, open);
    i = open + 1;
    if (format.charAt(i) === '{') {
      result += '{';
      i++;
      continue;
    }
    if (close < 0) throw Error.argument('format', Sys.Res.stringFormatBraceMismatch);
    var brace = format.substring(i, close);
    var colonIndex = brace.indexOf(':');
    var argNumber = parseInt((colonIndex < 0) ? brace : brace.substring(0, colonIndex), 10) + 1;
    if (isNaN(argNumber)) throw Error.argument('format', Sys.Res.stringFormatInvalid);
    var argFormat = (colonIndex < 0) ? '' : brace.substring(colonIndex + 1);
    var arg = args[argNumber];
    if (typeof (arg) === "undefined" || arg === null) {
      arg = '';
    }
    if (arg.toFormattedString) {
      result += arg.toFormattedString(argFormat);
    }
    else if (useLocale && arg.localeFormat) {
      result += arg.localeFormat(argFormat);
    }
    else if (arg.format) {
      result += arg.format(argFormat);
    }
    else
      result += arg.toString();
    i = close + 1;
  }
  return result;
}
دقت کنید قسمت ابتدایی این متد که برای بررسی اعتبار آرگومان‌های ورودی است، برای سادگی عملیات کامنت شده است. همان‌طور که می‌بینید این متد پیاده‌سازی نسبتا مفصلی دارد و امکانات بیشتری نیز در اختیار برنامه نویسان قرار می‌دهد. البته سایر متدهای مربوطه بدلیل طولانی بودن در اینجا آورده نشده است. برای مثال امکانات پیشرفته‌تری مثل زیر با استفاده از این کتابخانه در دسترس هستند:
console.log(String.format("{0:n}, {0:c}, {0:p}, {0:d}", 100.0001));
// result:   100.00, ¤100.00, 10,000.01 %, 100.0001

console.log(String.format("{0:d}, {0:t}", new Date(2015, 1, 1, 10, 45)));
// result:   02/01/2015, 10:45
آخرین نسخه این کتابخانه از اینجا قابل دریافت است (این متدها درون فایل MicrosoftAjax.debug.js قرار دارند). این کتابخانه دیگر به این صورت و با این نام توسعه داده نمیشود و چند سالی است که تصمیم به توسعه ویژگی‌های جدید آن به صورت پلاگین‌های jQuery گرفته شده است.

کتابخانه دیگری که می‌توان برای عملیات فرمت‌بندی رشته‌ها در جاوا اسکریپت از آن استفاده کرد، کتابخانه معروف jQuery Validation است. این کتابخانه یک متد نسبتا خوب با نام format برای فرمت کردن رشته‌ها دارد. نحوه استفاده از این متد به صورت زیر است:
var template = jQuery.validator.format("{0} is not a valid value");
console.log(template("abc"));
// result: 'abc is not a valid value'

کتابخانه نسبتا کامل دیگری که وجود دارد، با عنوان Stringformat از اینجا قابل دریافت است. برای استفاده از این کتابخانه باید به صورت زیر عمل کرد:
String.format([full format string], [arguments...]);
// or:
[date|number].format([partial format string]);
همان‌طور که می‌بینید این کتابخانه امکانات کامل‌تری نیز دارد. مثال‌های مربوط به این کتابخانه به صورت زیر هستند که توانایی‌های نسبتا کامل آن‌را نشان می‌دهد:
// Object path
String.format("Welcome back, {username}!", 
{ id: 3, username: "JohnDoe" });
// Result: "Welcome back, JohnDoe!"

// Date/time formatting
String.format("The time is now {0:t}.", 
new Date(2009, 5, 1, 13, 22));
// Result: "The time is now 01:22 PM."

// Date/time formatting (without using a full format string)
var d = new Date();
d.format("hh:mm:ss tt");
// Result: "02:28:06 PM"

// Custom number format string
String.format("Please call me at {0:+##0 (0) 000-00 00}.", 4601111111);
// Result: "Please call me at +46 (0) 111-11 11."

// Another custom number format string
String.format("The last year result was {0:+$#,0.00;-$#,0.00;0}.", -5543.346);
// Result: "The last year result was -$5,543.35."

// Alignment
String.format("|{0,10:PI=0.00}|", Math.PI);
// Result: "|   PI=3.14|"

// Rounding
String.format("1/3 ~ {0:0.00}", 1/3);
// Result: "1/3 ~ 0.33"

// Boolean values
String.format("{0:true;;false}", 0);
// Result: "false"

// Explicitly specified localization
// (note that you have to include the .js file for used cultures)
msf.setCulture("en-US");
String.format("{0:#,0.0}", 3641.667);
// Result: "3,641.7"

msf.setCulture("sv-SE");
String.format("{0:#,0.0}", 3641.667);
// Result: "3 641,7"

یک کتابخانه دیگر نیز از این آدرس قابل دریافت است. این کتابخانه با عنوان String.format نام‌گذاری شده است. نحوه استفاده از این کتابخانه نیز به صورت زیر است:
//inline arguments
String.format("some string with {0} and {1} injected using argument {{number}}", 'first value', 'second value');
//returns: 'some string with first value and second value injected argument {number}'

//single array
String.format("some string with {0} and {1} injected using array {{number}}", [ 'first value', 'second value' ]);
//returns: 'some string with first value and second value injected using array {number}'

//single object
String.format("some string with {first} and {second} value injected using {{propertyName}}",{first:'first value',second:'second value'});
//returns: 'some string with first value and second value injected using {propertyName}'
کتابخانه نسبتا معروف و کامل sprintf نیز در اینجا وجود دارد. این کتابخانه امکانات بسیاری همچون متدهای متناظر در زبان C دارد.

منابع

مطالب
مروری بر کاربردهای Action و Func - قسمت اول
delegate‌ها، نوع‌هایی هستند که ارجاعی را به یک متد دارند؛ بسیار شبیه به function pointers در C و CPP هستند، اما برخلاف آن‌ها، delegates شی‌ء‌گرا بوده، به امضای متد اهمیت داده و همچنین کد مدیریت شده و امن به شمار می‌روند.
سیر تکاملی delegates را در مثال ساده زیر می‌توان ملاحظه کرد:
using System;

namespace ActionFuncSamples
{
    public delegate int AddMethodDelegate(int a);
    public class DelegateSample
    {
        public void UseDelegate(AddMethodDelegate addMethod)
        {
            Console.WriteLine(addMethod(5));
        }
    }

    public class Helper
    {
        public int CustomAdd(int a)
        {
            return ++a;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Helper helper = new Helper();

            // .NET 1            
            AddMethodDelegate addMethod = new AddMethodDelegate(helper.CustomAdd);
            new DelegateSample().UseDelegate(addMethod);

            // .NET 2, anonymous delegates
            new DelegateSample().UseDelegate(delegate(int a) { return helper.CustomAdd(a); });

            // .NET 3.5
            new DelegateSample().UseDelegate(a => helper.CustomAdd(a));
        }
    }
}
معنای کلمه delegate، واگذاری مسئولیت است. به این معنا که ما در متد UseDelegate، نمی‌دانیم addMethod به چه نحوی تعریف خواهد شد. فقط می‌دانیم که امضای آن چیست.
در دات نت یک، یک وهله از شیء AddMethodDelegate ساخته شده و سپس متدی که امضایی متناسب و متناظر با آن را داشت، به عنوان متد انجام دهنده مسئولیت معرفی می‌شد. در دات نت دو، اندکی نحوه تعریف delegates با ارائه delegates بی‌نام، ساده‌تر شد و در دات نت سه و نیم با ارائه lambda expressions ، تعریف و استفاده از delegates باز هم ساده‌تر و زیباتر گردید.
به علاوه در دات نت 3 و نیم، دو Generic delegate به نام‌های Action و Func نیز ارائه گردیده‌اند که به طور کامل جایگزین تعریف طولانی delegates در کدهای پس از دات نت سه و نیم شده‌اند. تفاوت‌های این دو نیز بسیار ساده است:
اگر قرار است واگذاری قسمتی از کد را به متدی محول کنید که مقداری را بازگشت می‌دهد، از Func و اگر این متد خروجی ندارد از Action استفاده نمائید:
Action<int> example1 = x => Console.WriteLine("Write {0}", x);
example1(5);

Func<int, string> example2 = x => string.Format("{0:n0}", x);
Console.WriteLine(example2(5000));
در دو مثال فوق، نحوه تعریف inline یک Action و یا Func را ملاحظه می‌کنید. Action به متدی اشاره می‌کند که خروجی ندارد و در اینجا تنها یک ورودی int را می‌پذیرد. Func در اینجا به تابعی اشاره می‌کند که یک ورودی int را دریافت کرده و یک خروجی string را باز می‌گرداند.

پس از این مقدمه، در ادامه قصد داریم مثال‌های دنیای واقعی Action و Func را که در سال‌های اخیر بسیار متداول شده‌اند، بررسی کنیم.


مثال یک) ساده سازی تعاریف API ارائه شده به استفاده کنندگان از کتابخانه‌های ما
عنوان شد که کار delegates، واگذاری مسئولیت انجام کاری به کلاس‌های دیگر است. این مورد شما را به یاد کاربردهای interfaceها نمی‌اندازد؟
در interfaceها نیز یک قرارداد کلی تعریف شده و سپس کدهای یک کتابخانه، تنها با امضای متدها و خواص تعریف شده در آن کار می‌کنند و کتابخانه ما نمی‌داند که این متدها قرار است چه پیاده سازی خاصی را داشته باشند.
برای نمونه طراحی API زیر را درنظر بگیرید که در آن یک interface جدید تعریف شده که تنها حاوی یک متد است. سپس کلاس Runner از این interface استفاده می‌کند:
using System;

namespace ActionFuncSamples
{
    public interface ISchedule
    {
        void Run();
    }

    public class Runner
    {
        public void Exceute(ISchedule schedule)
        {
            schedule.Run();
        }
    }

    public class HelloSchedule : ISchedule
    {
        public void Run()
        {
            Console.WriteLine("Just Run!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            new Runner().Exceute(new HelloSchedule());
        }
    }
}
در اینجا ابتدا باید این interface را در طی یک کلاس جدید (مثلا HelloSchedule) پیاده سازی کرد و سپس حاصل را در کلاس Runner استفاده نمود.
نظر شما در مورد این طراحی ساده شده چیست؟
using System;

namespace ActionFuncSamples
{
    public class Schedule
    {
        public void Exceute(Action run)
        {
            run();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            new Schedule().Exceute(() => Console.WriteLine("Just Run!"));
        }
    }
}
با توجه به اینکه هدف از معرفی interface در طراحی اول، واگذاری مسئولیت نحوه تعریف متد Run به کلاسی دیگر است، به همین طراحی با استفاده از یک Action delegate نیز می‌توان رسید. مهم‌ترین مزیت آن، حجم بسیار کمتر کدنویسی استفاده کننده نهایی از API تعریف شده ما است. به علاوه امکان inline coding نیز فراهم گردیده است و در همان محل تعریف Action، بدنه آن‌را نیز می‌توان تعریف کرد.
بدیهی است delegates نمی‌توانند به طور کامل جای interfaceها را پر کنند. اگر نیاز است قرارداد تهیه شده بین ما و استفاده کنندگان از کتابخانه، حاوی بیش از یک متد باشد، استفاده از interfaceها بهتر هستند.
از دیدگاه بسیاری از طراحان API، اشیاء delegate معادل interface ایی با یک متد هستند و یک وهله از delegate معادل وهله‌ای از کلاسی است که یک interface را پیاده سازی کرده‌است.
علت استفاده بیش از حد interfaceها در سایر زبان‌ها برای ابتدایی‌ترین کارها، کمبود امکانات پایه‌ای آن زبان‌ها مانند نداشتن lambda expressions، anonymous methods و anonymous delegates هستند. به همین دلیل مجبورند همیشه و در همه‌جا از interfaceها استفاده کنند.

ادامه دارد ...

 
مطالب
React 16x - قسمت 18 - کار با فرم‌ها - بخش 1 - دریافت ورودی‌ها از کاربر
تقریبا تمام برنامه‌ها نیاز دارند فرم‌های مخصوصی را داشته باشند. به همین جهت در این قسمت، برنامه‌ی نمایش لیست فیلم‌ها را که تا این مرحله تکمیل کردیم، با افزودن تعدادی فرم بهبود می‌بخشیم؛ مانند فرم لاگین، فرم ثبت نام، فرمی برای ثبت و ویرایش فیلم‌ها و یک فرم جستجوی سریع در لیست فیلم‌های موجود.


ایجاد فرم لاگین

فرم لاگینی را که به برنامه‌ی نمایش لیست فیلم‌های تکمیل شده‌ی تا قسمت 17، اضافه خواهیم کرد، یک فرم بوت استرپی است و می‌توانید جزئیات بیشتر مزین سازی المان‌های این نوع فرم‌ها را با کلاس‌های بوت استرپ، در مطلب «کار با شیوه‌نامه‌های فرم‌ها در بوت استرپ 4» مطالعه کنید.
در ابتدا فایل جدید src\components\loginForm.jsx را ایجاد کرده و سپس توسط میان‌برهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت جدید LoginForm را ایجاد می‌کنیم:
import React, { Component } from "react";


class LoginForm extends Component {
  render() {
    return <h1>Login</h1>;
  }
}

export default LoginForm;
در ادامه یک Route جدید را در فایل app.js برای این فرم، با مسیر login/ و کامپوننت LoginForm، در ابتدای Switch موجود، تعریف می‌کنیم:
import LoginForm from "./components/loginForm";
//...

function App() {
  return (
    <React.Fragment>
      <NavBar />
      <main className="container">
        <Switch>
          <Route path="/login" component={LoginForm} />
          <Route path="/movies/:id" component={MovieForm} />
          // ...
        </Switch>
      </main>
    </React.Fragment>
  );
}
پس از تعریف این مسیریابی، نیاز است لینک آن‌را نیز به منوی راهبری سایت اضافه کنیم. به همین جهت در فایل navBar.jsx که آن‌را در قسمت قبل تکمیل کردیم، در انتهای لیست موجود و پس از Rentals، لینک لاگین را نیز قرار می‌دهیم:
<NavLink className="nav-item nav-link" to="/login">
   Login
</NavLink>
که در نهایت حاصل این تغییرات، به صورت زیر در مرورگر ظاهر می‌شود:


اکنون نوبت به افزودن فرم بوت استرپی لاگین به فایل loginForm.jsx رسیده‌است:
import React, { Component } from "react";


class LoginForm extends Component {
  render() {
    return (
      <form>
        <div className="form-group">
          <label htmlFor="username">Username</label>
          <input id="username" type="text" className="form-control" />
        </div>
        <div className="form-group">
          <label htmlFor="password">Password</label>
          <input id="password" type="password" className="form-control" />
        </div>
        <button className="btn btn-primary">Login</button>
      </form>
    );
  }
}

export default LoginForm;
توضیحات:
- ابتدا المان form به صفحه اضافه می‌شود.
- سپس هر ورودی، داخل یک div با کلاس form-group، محصور می‌شود. کار آن تبدیل یک برچسب و فیلد ورودی، به یک گروه از ورودی‌های بوت استرپ است.
- در اینجا هر برچسب دارای یک ویژگی for است. اما چون قرار است عبارات jsx، به معادل‌های جاوا اسکریپتی ترجمه شوند، نمی‌توان از واژه‌ی کلیدی for در اینجا استفاده کرد. به همین جهت از معادل react ای آن که htmlFor است، در کدهای فوق استفاده کرده‌ایم؛ شبیه به نکته‌ای که در مورد تبدیل ویژگی class به className وجود دارد. مقدار هر ویژگی htmlFor نیز به id فیلد ورودی متناظر با آن تنظیم می‌شود. به این ترتیب اگر کاربر بر روی این برچسب کلیک کرده و آن‌را انتخاب کند، فیلد متناظر با آن، دارای focus می‌شود.
- فیلدهای ورودی نیز دارای کلاس form-control هستند.

با این خروجی نهایی در مرورگر:



مدیریت ارسال فرم‌ها

به صورت پیش فرض و استاندارد، دکمه‌ی افزوده شده‌ی به المان form، سبب ارسال اطلاعات آن به سرور و سپس بارگذاری کامل صفحه می‌شود. این رفتاری نیست که در یک برنامه‌ی SPA مدنظر باشد. برای مدیریت این حالت، می‌توان از رخ‌داد onSubmit هر المان فرم، استفاده کرد:
class LoginForm extends Component {
  handleSubmit = e => {
    console.log("handleSubmit", e);
    e.preventDefault();

    // call the server
  };

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
      //...
در اینجا یک متد رویدادگردان را برای رخ‌داد onSubmit تعریف کرده‌ایم که توسط آن رخ‌داد جاری، دریافت و متد preventDefault آن فراخوانی می‌شود تا دیگر پس از کلیک بر روی دکمه‌ی submit، حالت پیش‌فرض و استاندارد full page reload و post back به سمت سرور، رخ ندهد.


دسترسی مستقیم به المان‌های فرم‌ها

پس از فراخوانی متد preventDefault، کار مدیریت ارسال فرم به سرور را باید خودمان مدیریت کنیم و دیگر رخ‌داد full post back استاندارد به سمت سرور را نخواهیم داشت. در جاوا اسکریپت خالص برای دریافت مقادیر وارد شده‌ی توسط کاربر می‌توان نوشت:
const username = document.getElementById("username").value;
اما در React و کدهای یک کامپوننت، نباید ارجاع مستقیمی را به شیء document و DOM اصلی مرورگر داشته باشیم. در برنامه‌های React هیچگاه نباید با شیء document کار کرد؛ چون کل فلسفه‌ی آن ایجاد یک abstraction بر فراز DOM اصلی مرورگر است که به آن DOM مجازی گفته می‌شود. به این ترتیب مدیریت برنامه و همچنین آزمون نویسی برای آن نیز ساده‌تر می‌شود. اما اگر واقعا نیاز به دسترسی به یک المان DOM در React وجود داشت، چه باید کرد؟
برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت، ایجاد کرده و آن‌را با React.RefObject، مقدار دهی اولیه می‌کنیم:
class LoginForm extends Component {
  username = React.createRef();
سپس ویژگی ref المان مدنظر را به این RefObject تنظیم می‌کنیم:
<input
  ref={this.username}
  id="username"
  type="text"
  className="form-control"
/>
اکنون زمان submit فرم، اگر نیاز به مقدار username وجود داشت، می‌توان توسط خاصیت ارجاعی username تعریف شده، به خاصیت current آن که DOM element مدنظر را بازگشت می‌دهد، دسترسی یافت و مانند مثال زیر، مقدار آن‌را مورد استفاده قرار داد:
  handleSubmit = e => {
    e.preventDefault();

    // call the server
    const username = this.username.current.value;
    console.log("handleSubmit", username);
  };

البته در حالت کلی باید استفاده‌ی از RefObjectها را به حداقل رساند (راه حل بهتری برای دریافت ورودی‌ها وجود دارد) و جاهائی از آن‌ها استفاده کرد که واقعا راه حل دیگری وجود ندارد؛ مانند تنظیم focus بر روی یک المان DOM. در این حالت حتما باید ارجاعی را از آن المان DOM در دسترس داشت و یا برای پویانمایی (animation) نیز مجبور به استفاده‌ی از RefObjectها هستیم.
برای نمونه روش تنظیم focus بر روی یک فیلد ورودی توسط RefObjectها به صورت زیر است:
class LoginForm extends Component {
  username = React.createRef();

  componentDidMount = () => {
    this.username.current.focus();
  };
در life-cycle hook ای به نام componentDidMount که پس از رندر کامپوننت در DOM فراخوانی می‌شود، می‌‌توان توسط RefObject تعریف شده، به شیء current که معادل DOM Element متناظر است، دسترسی یافت و سپس متد focus آن‌را فراخوانی کرد. در این حالت در اولین بار نمایش فرم، یک چنین تصویری حاصل می‌شود:


البته روش بهتری نیز برای انجام اینکار وجود دارد. المان‌های JSX دارای ویژگی autoFocus نیز هستند که دقیقا همین کار را انجام می‌دهد:
<input
  autoFocus
  ref={this.username}
  id="username"
  type="text"
  className="form-control"
/>
برای آزمایش آن، قطعه کد componentDidMount را کامنت کرده و برنامه را اجرا کنید.


تبدیل المان‌های فرم‌ها به Controlled elements

در بسیاری از اوقات، فرم‌های ما state خود را از سرور دریافت می‌کنند. فرض کنید که در حال ایجاد یک فرم ثبت اطلاعات فیلم‌ها هستیم. در این حالت باید بر اساس id فیلم، اطلاعات آن را از سرور دریافت و در state ذخیره کرد؛ سپس فیلدهای فرم را بر اساس آن مقدار دهی اولیه کرد. برای نمونه در فرم لاگین می‌توان state را با شیء account، به صورت زیر مقدار دهی اولیه کرد:
class LoginForm extends Component {
  state = {
    account: { username: "", password: "" }
  };
تا اینجا فیلدهای فرم لاگین، از این state مطلع نبوده و تغییرات داده‌های ورودی در آن‌ها، به شیء account منعکس نمی‌شوند. علت اصلی هم اینجا است که هر کدام از فیلدهای ورودی در React، دارای state خاص خود بوده و مستقل از state کامپوننت جاری هستند. برای رفع این مشکل باید آن‌ها را تبدیل به controlled element هایی کرد که دارای state خاص خود نبوده، تمام اطلاعات مورد نیاز خود را از طریق props دریافت می‌کنند و تغییرات در داده‌های خود را از طریق صدور رخ‌دادهایی اطلاع رسانی می‌کنند. برای اینکار باید مراحل زیر طی شوند:
ابتدا ویژگی value فیلد برای مثال username را به خاصیت username شیء account موجود در state متصل می‌کنیم:
<input 
  value={this.state.account.username}
به این ترتیب دیگر این المان، state خاص خود را نداشته و از طریق props، مقادیر خود را دریافت می‌کند. تا اینجا username، به رشته‌ی خالی دریافتی از شیء state و خاصیت account آن، به صورت یک طرفه متصل شده‌است. یعنی زمانیکه فرم نمایش داده می‌شود، دارای یک مقدار خالی است. برای اینکه تغییرات رخ‌داده‌ی در این المان را به state منعکس کرد، باید رخ‌داد change آن‌را مدیریت نمود. به این ترتیب زمانیکه کاربری اطلاعاتی را در اینجا وارد می‌کند، رخ‌داد change صادر شده و پس از آن می‌توان اطلاعات وارد شده را دریافت و state را به روز رسانی کرد. به روز رسانی state نیز سبب رندر مجدد فرم می‌شود. بنابراین فیلدهای ورودی، با اطلاعات state جدید، به روز رسانی و رندر می‌شوند. به همین جهت ابتدا رویداد onChange را به فیلد username اضافه کرده:
<input 
  value={this.state.account.username}
  onChange={this.handleChange}
و متد مدیریت کننده‌ی آن‌را به صورت زیر تعریف می‌کنیم:
  handleChange = e => {
    const account = { ...this.state.account }; //cloning an object
    account.username = e.currentTarget.value;
    this.setState({ account });
  };
در اینجا، هدف به روز رسانی this.state.account، بر اساس رخ‌داد رسیده (پارامتر e) است و چون نمی‌توان state را مستقیما به روز رسانی کرد، ابتدا یک clone از آن را تهیه می‌کنیم. سپس توسط e.currentTarget به المان در حال به روز رسانی دسترسی یافته و مقدار آن‌را به مقدار خاصیت username انتساب می‌دهیم. در آخر state را بر اساس این تغییرات، به روز رسانی می‌کنیم. این انعکاس در state را توسط افزونه‌ی react developer tools هم می‌توان مشاهده کرد:



مدیریت دریافت اطلاعات چندین فیلد ورودی

تا اینجا موفق شدیم اطلاعات state را به تغییرات فیلد username در فرم لاگین متصل کنیم؛ اما فیلد password را چگونه باید مدیریت کرد؟ برای اینکه تمام این مراحل را مجددا تکرار نکنیم، می‌توان از مقدار دهی پویای خواص در جاوا اسکریپت که توسط [] انجام می‌شود استفاده کرد:
  handleChange = e => {
    const account = { ...this.state.account }; //cloning an object
    account[e.currentTarget.name] = e.currentTarget.value;
    this.setState({ account });
  };
البته برای اینکه این قطعه کد کار کند، نیاز است ویژگی name فیلدهای ورودی را نیز تنظیم کرد تا e.currentTarget.name، به نام یکی از خواص شیء account تعریف شده‌ی در state اشاره کند. برای نمونه فیلد کلمه‌ی عبور، ابتدا دارای ویژگی value متصل به خاصیت password شیء account موجود در state می‌شود. سپس تغییرات آن توسط رویداد onChange، به متد handleChange منتقل شده و خاصیت name آن نیز مقدار دهی شده‌است تا مقدار دهی پویای خواص، در این متد میسر شود:
<input
  id="password"
  name="password"
  value={this.state.account.password}
  onChange={this.handleChange}
  type="password"
  className="form-control"
/>
که در نهایت سبب مقدار دهی صحیح state، با هر دو فیلد تغییر یافته می‌شود:


یک نکته: می‌توان توسط Object Destructuring، تکرار e.currentTarget را حذف کرد:
  handleChange = ({ currentTarget: input }) => {
    const account = { ...this.state.account }; //cloning an object
    account[input.name] = input.value;
    this.setState({ account });
  };
ما از شیء e دریافتی، تنها به خاصیت currentTarget آن نیاز داریم. بنابراین آن‌را از طریق Object Destructuring در همان پارامتر ورودی متد جاری دریافت کرده و سپس آن‌را به نام input، تغییر نام می‌دهیم.


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

فرض کنید خاصیت username را از شیء account موجود در state حذف کرده‌ایم. در زمان نمایش ابتدایی فرم، خطایی را دریافت نخواهیم کرد، اما اگر اطلاعاتی را در آن وارد کنیم، بلافاصله در کنسول توسعه دهندگان مرورگر چنین اخطاری ظاهر می‌شود:
Warning: A component is changing an uncontrolled input of type text to be controlled.
Input elements should not switch from uncontrolled to controlled (or vice versa).
Decide between using a controlled or uncontrolled input element for the lifetime of the component.
More info: https://fb.me/react-controlled-components
چون خاصیت username را حذف کرده‌ایم، اینبار که در textbox مقداری را وارد می‌کنیم، سبب انتساب undefined و یا null به مقدار المان خواهد شد. در این حالت React چنین المانی را به صورت controlled element درنظر نمی‌گیرد و دارای state خاص خودش خواهد بود. به همین جهت عنوان می‌کند که بین یک المان کنترل شده و نشده، یکی را انتخاب کنید.
دقیقا چنین اخطاری را با ورود null/undefined بجای "" در حین مقدار دهی اولیه‌ی username در شیء account نیز دریافت خواهیم کرد:
Warning: `value` prop on `input` should not be null.
Consider using an empty string to clear the component or `undefined` for uncontrolled components.
بنابراین به عنوان یک قاعده در فرم‌های React، المان‌های یک فرم را باید توسط یک "" مقدار دهی اولیه کرد و یا با مقداری که از سمت سرور دریافت می‌شود.


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

هر چند در پیاده سازی فعلی سعی کردیم با بکارگیری مقداردهی پویای خواص اشیاء، تکرار کدها را کاهش دهیم، اما باز هم به ازای هر فیلد ورودی باید این مسایل تکرار شوند:
- ایجاد یک div با کلاس‌های بوت استرپی.
- ایجاد label و همچنین فیلد ورودی.
- در اینجا مقدار htmlFor باید با مقدار id فیلد ورودی یکی باشد.
- مقدار دهی ویژگی‌های value و onChange نیز باید تکرار شوند.

بنابراین بهتر است این تعاریف را استخراج و به یک کامپوننت با قابلیت استفاده‌ی مجدد منتقل کرد. به همین جهت فایل جدید src\components\common\input.jsx را در پوشه‌ی common ایجاد کرده و سپس توسط میانبرهای imrc و sfc، این کامپوننت تابعی بدون حالت را تکمیل می‌کنیم:
import React from "react";

const Input = ({ name, label, value, onChange }) => {
  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <input
        value={value}
        onChange={onChange}
        id={name}
        name={name}
        type="text"
        className="form-control"
      />
    </div>
  );
};

export default Input;
در اینجا کل تگ div مرتبط با username را از کامپوننت فرم لاگین cut کرده و در اینجا در قسمت return، قرار داده‌ایم. سپس شروع به تبدیل مقادیر قبلی به مقادیری که قرار است از props تامین شوند، کرده‌ایم. یا می‌توان props را به عنوان آرگومان این متد تعریف کرد و یا می‌توان توسط Object Destructuring، خواصی را که از props نیاز داریم، در پارامتر متد Input ذکر کنیم که این روش چون به نوعی اینترفیس کامپوننت را نیز مشخص می‌کند و همچنین کدهای تکراری دسترسی به props را به حداقل می‌رساند، تمیزتر و با قابلیت نگهداری بالاتری است. برای مثال هر جائیکه نام username استفاده شده بود، با خاصیت name جایگزین شده و بجای برچسب از label، بجای مقدار username از متغیر value و بجای رخ‌داد تعریف شده نیز onChange قرار گرفته‌است.

سپس به کامپوننت فرم لاگین بازگشته و ابتدا آن‌را import می‌کنیم:
import Input from "./common/input";
اکنون متد رندر ماژول src\components\loginForm.jsx، به صورت زیر با درج دو Input، خلاصه می‌شود که دیگر در آن خبری از تگ‌ها و کدهای تکراری نیست:
  render() {
    const { account } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <Input
          name="username"
          label="Username"
          value={account.username}
          onChange={this.handleChange}
        />
        <Input
          name="password"
          label="Password"
          value={account.password}
          onChange={this.handleChange}
        />
        <button className="btn btn-primary">Login</button>
      </form>
    );


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:  sample-18.zip
مطالب
یافتن خطاهای متداول کدهای جاوا اسکریپتی با غنی سازی تنظیمات کامپایلر TypeScript
یکی از اهداف مهم استفاده‌ی از TypeScript، یافتن خطاهای متداول کدهای جاواسکریپتی، پیش از اجرای آن‌ها در مرورگر است. برای مثال، قطعه کد زیر:
  defaultChecks() {
    const author = { firstName: "Vahid", lastName: "N" };
    console.log(author.lastname);
    author.lastName.trimStart();
    author.firstName.charCodeAt("1");
  }
دارای سه مشکل است که سریعا توسط TypeScript شناسایی می‌شود:


- خاصیت lastname در شیء author وجود خارجی ندارد.
- نوع رشته‌ای، به همراه متد trimStart نیست.
- متد charCodeAt یک عدد را به عنوان پارامتر قبول می‌کند.

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


نصب افزونه‌ی TSLint در VSCode

جهت مشاهده‌ی بهتر خطاهای کامپایلر TypeScript، پیش از کامپایل نهایی کدها، می‌توان از افزونه‌ی TSLint استفاده کرد. برای نصب آن، ابتدا باید بسته‌ی ذیل را نصب کرد:
  > npm install -g tslint typescript
سپس نیاز است افزونه‌ی آن‌را نیز نصب کنید: https://marketplace.visualstudio.com/items?itemName=eg2.tslint
کار TSLint انجام static code analysis است؛ چیزی شبیه به افزونه‌هایی مانند ری‌شارپر در ویژوال استودیو که راهنماهایی را در مورد بهتر کردن کیفیت کدهای نوشته شده ارائه می‌دهد.


فعالسازی بررسی نال و نوع‌های نال پذیر

 strictNullChecks یکی از مهم‌ترین پرچم‌های تنظیمات کامپایلر تایپ‌اسکریپت است. برای افزودن آن، به فایل tsconfig.json مراجعه کرده و پرچم آن‌را به true تنظیم کنید:
{
    "compilerOptions": {
        "strictNullChecks": true
    }
}
به این ترتیب کامپایلر تایپ‌اسکریپت، بین null ،undefined و سایر نوع‌ها، تفاوت قائل خواهد شد و اگر نوعی نال‌پذیر است، باید آن‌را به صورت صریح type | null تعریف کنید.
برای مثال، متد ذیل را در نظر بگیرید:
  getSessionItem(key: string): any {
    const data = window.sessionStorage.getItem(key);
    return JSON.parse(data);
  }
زمانیکه پرچم strictNullChecks فعال شود، قطعه کد فوق با خطای ذیل کامپایل نخواهد شد:
[ts]
Argument of type 'string | null' is not assignable to parameter of type 'string'.
Type 'null' is not assignable to type 'string'.
const data: string | null
از این جهت که خروجی متد getItem به صورت string | null تعریف شده‌است؛ اما پارامتر متد JSON.parse فقط string را قبول می‌کند. یعنی ممکن است data دریافتی نال باشد که متد JSON.parse قادر به پردازش آن نیست.
برای رفع این مشکل تنها کافی است بررسی کنیم که آیا data نال است یا خیر؟ و اگر خیر، آنگاه آن‌را به متد JSON.parse ارسال کنیم:
  getSessionItem(key: string): any {
    const data = window.sessionStorage.getItem(key);
    if (data) {
      return JSON.parse(data);
    } else {
      return null;
    }
  }
همانطور که ملاحظه می‌کنید، فعالسازی strictNullChecks می‌تواند از بروز بسیاری از خطاهای در زمان اجرای برنامه، جلوگیری کند.


گزارش return‌های فراموش شده

در متد ذیل، یک return فراموش شده وجود دارد و تمام شرط‌های برنامه به یک خروجی مشخص، منتهی نمی‌شوند:
  noImplicitReturns(a: number) {
    if (a > 10) {
      return a;
    }
    // No return in this branch
  }
برای تشخیص زود هنگام یک چنین مشکلاتی می‌توان پرچم noImplicitReturns را در فایل tsconfig.json به true تنظیم کرد:
{
    "compilerOptions": {
        "noImplicitReturns": true
    }
}
پس از این تنظیم، کامپایلر در مورد متد noImplicitReturns، خطای ذیل را صادر می‌کند:
 [ts] Not all code paths return a value.


تشخیص کدهای مرده

قطعه کدی که پس از یک return قرار بگیرد، یک کد مرده نامیده می‌شود. با تنظیم پرچم allowUnreachableCode در فایل tsconfig.json به false، می‌توان کامپایلر TypeScript را وادار کرد تا اینگونه موارد را به عنوان خطا گزارش کند:
{
    "compilerOptions": {
        "allowUnreachableCode": false
    }
}
پس از این فعالسازی، کامپایلر TypeScript دو خطای [ts] Unreachable code detected را در مورد قطعه کد ذیل صادر می‌کند:
    allowUnreachableCode() {
      if (false) {
        console.log("Unreachable code");
      }
      const a = 1;
      if (a > 0) {
        return 10; // reachable code
      }
      return 0;
      console.log("Unreachable code");
    }
مورد اول مربوط به بدنه‌ی if ایی است که شرط آن false است و این بدنه هیچگاه فراخوانی نمی‌شود و مورد دوم، به سطر پس از return آخر مرتبط است که آن مورد نیز یک کد مرده محسوب می‌شود.


تشخیص پارامترها و متغیرهای استفاده نشده

دو متد ذیل را درنظر بگیرید:
  unusedLocals() {
    const a = "foo"; // Error: 'a' is declared but its value is never read
    return "bar";
  }

  unusedParameters(n: number) {
    n = 0; // Never read
  }
در اولی متغیر a تعریف شده‌است، اما هیچگاه استفاده نشده‌است. در متد دوم، پارامتر n نیز هیچگاه خوانده نشده‌است و وجود آن بی‌مصرف است؛ حتی ممکن است نشان فراموش شدن استفاده‌ی از آن و مقدار دهی اشتباه آن باشد.
برای فعالسازی بررسی یک چنین مواردی باید دو پرچم ذیل را در فایل tsconfig.json به true تنظیم کرد:
{
    "compilerOptions": {
        "noUnusedLocals": true,
        "noUnusedParameters": true
    }
}
پس از آن، کامپایلر دو خطای ذیل را در مورد متدهای فوق، گزارش می‌کند:
[ts] 'a' is declared but its value is never read.
[ts] 'n' is declared but its value is never read.


یافتن خواصی که نباید در یک شیء وجود داشته باشند

در مثال ذیل، خاصیت baz در تعاریف اصلی نوع‌های x و y وجود ندارد:
  excessPropertyForObjectLiterals() {
    let x: { foo: number };
    x = { foo: 1, baz: 2 };  // Error, excess property 'baz'
    let y: { foo: number, bar?: number };
    y = { foo: 1, baz: 2 };  // Error, excess property 'baz'
  }
برای فعالسازی یافتن یک چنین مواردی که در بسیاری از حالات ممکن است ناشی از اشتباهات تایپی نیز باشند، می‌توان پرچم suppressExcessPropertyErrors را در فایل tsconfig.json به false تنظیم کرد:
{
    "compilerOptions": {
        "suppressExcessPropertyErrors": false
    }
}
پس از آن برای نمونه در مورد انتساب اول، یک چنین پیام خطایی از طرف کامپایلر TypeScript صادر خواهد شد:
[ts]
Type '{ foo: number; baz: number; }' is not assignable to type '{ foo: number; }'.
Object literal may only specify known properties, and 'baz' does not exist in type '{ foo: number; }'.
(property) baz: number


یافتن breakهای فراموش شده در عبارات switch

در مثال زیر، یک break فراموش شده‌است:
  fallthroughCasesInSwitchStatement(a: number) {
    switch (a) {
      case 0:
        break;

      case 1:
        a += 1;

      case 2:
        a += 2;
        break;
    }
  }
برای تشخیص آن توسط کامپایلر TypeScript باید پرچم noFallthroughCasesInSwitch را در فایل tsconfig.json به true تنظیم کرد:
{
    "compilerOptions": {
        "noFallthroughCasesInSwitch": true
    }
}
پس از آن کامپایلر TypeScript خطای «[ts] Fallthrough case in switch» را در مورد متد فوق صادر می‌کند.


یافتن ایندکس‌های تعریف نشده‌ی در اشیاء

در مثال زیر، شیء x دارای خاصیت b نیست؛ اما دقیقا با این ایندکس مورد استفاده قرار گرفته‌است:
  indexingObjectsLackingIndexSignatures() {
    const x = { a: 0 };
    x["a"] = 1; // ok
    x["b"] = 1; // Error, type '{ a: number; }' has no index signature.
  }
برای تشخیص یک چنین خطاهایی می‌توان پرچم suppressImplicitAnyIndexErrors را در فایل tsconfig.json به false تنظیم کرد:
{
    "compilerOptions": {
        "suppressImplicitAnyIndexErrors": false
    }
}
پس از آن کامپایلر TypeScript خطای ذیل را در مورد دسترسی به ایندکسی که با امضای شیء سازگاری ندارد، صادر می‌کند:
[ts] Element implicitly has an 'any' type because type '{ a: number; }' has no index signature.


اجبار به تعریف صریح نوع‌ها در TypeScript

عمده‌ی قابلیت TypeScript در یافتن خطاها به تعاریف نوع‌ها و راهنمایی کامپایلر آن در این زمینه بر می‌گردد. اما چون این زبان سازگاری کاملی را با JavaScript دارد، تعریف نوع‌ها در آن اجباری نیست و در این حالت اگر نوعی تعریف نشده باشد، به any تفسیر می‌شود. جهت اجبار به تعریف نوع‌ها در TypeScript می‌توان پرچم noImplicitAny را در فایل tsconfig.json به true تنظیم کرد:
{
    "compilerOptions": {
        "noImplicitAny": true
    }
}
در این حالت دیگر قطعه کد ذیل کامپایل نخواهد شد:
  noImplicitAny(args) { // Error: Parameter 'args' implicitly has an 'any' type.
    console.log(args);
  }
برای رفع این مشکل می‌توان نوع args را به صورت صریحی مشخص کرد:
  noImplicitAnyArgs(args: string[]) { // ok with the type information
    console.log(args);
  }


یک نکته‌ی تکمیلی
اگر از دستور ng build --watch برای ساخت برنامه‌های Angular استفاده می‌کنید، تغییرات فوق زمانی تاثیر داده خواهند شد که یکبار این برنامه را بسته و مجددا اجرا کنید.
مطالب
تخمین مدت زمان خوانده شدن یک مطلب
پس از انتشار مطلب «Pro Agile .NET Development With Scrum - قسمت اول» شاید این سؤال در ابتدای کار برای خواننده پیش بیاید که ... چقدر باید برای خواندن آن وقت بگذارم؟ برای پاسخ به این سؤال باید درنظر داشت که یک انسان معمولی، می‌تواند بین 200 تا 250 کلمه را در دقیقه، مطالعه کند. بنابراین در ابتدا باید محاسبه کرد که یک متن، چه تعدادی کلمه دارد؟
شاید عنوان کنید که کافی است متن ورودی را بر اساس فاصله‌ی بین کلمات تقسیم بندی کرده و سپس تعداد کلمات بدست آمده را محاسبه کنیم:
 var words = text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
return words.Length;
این روش با آزمون زیر کار نکرده و با شکست مواجه می‌شود:
[TestMethod]
public void TestInvalidChars()
{
    const string data = "To be . ! < > ( ) ! ! , ; : ' ? + -";
    Assert.AreEqual(2, data.WordsCount());
}
در اینجا ! ، و امثال آن نیز یک کلمه درنظر گرفته می‌شوند. برای حل این مشکل کافی است آرایه‌ی split را کمی تکمیل‌تر کنیم تا حروف غیرمجاز را درنظر نگیرد:
 var words = text.Split(
    new[] { ' ', ',', ';', '.', '!', '"', '(', ')', '?', ':', '\'', '«' , '»', '+', '-' },
    StringSplitOptions.RemoveEmptyEntries);
return words.Length;
تا اینجا مشکل !، >< حل شد، اما در مورد متن ذیل چطور؟
[TestMethod]
public void TestSimpleHtmlSpacesWithNewLine()
{
    const string data = "<b>this is&nbsp;a&nbsp;&nbsp;test.</b>\n\r<b>this is&nbsp;a&nbsp;&nbsp;test.</b>";
    Assert.AreEqual(8, data.WordsCount());
}
مطالب ثبت شده، عموما توسط HTML Editorها ثبت می‌شوند. بنابراین دارای انواع و اقسام تگ‌ها بوده و همچنین ممکن است در این بین new line هم وجود داشته باشد که در این حالت، test\n\rtest باید دو کلمه محاسبه شود و نه یک کلمه.
اگر این موارد را در نظر بگیریم، به کلاس ذیل خواهیم رسید:
using System;
using System.Text.RegularExpressions;
 
namespace ReadingTime
{
    public static class CalculateWordsCount
    {
        private static readonly Regex _matchAllTags =
            new Regex(@"<(.|\n)*?>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 
        public static int WordsCount(this string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                return 0;
            }
 
            text = text.cleanTags().Trim();
            text = text.Replace("\t", " ");
            text = text.Replace("\n", " ");
            text = text.Replace("\r", " ");
 
            var words = text.Split(
                new[] { ' ', ',', ';', '.', '!', '"', '(', ')', '?', ':', '\'', '«' , '»', '+', '-' },
                StringSplitOptions.RemoveEmptyEntries);
            return words.Length;
        }
 
        private static string cleanTags(this string data)
        {
            return data.Replace("\n", "\n ").removeHtmlTags();
        }
 
        private static string removeHtmlTags(this string text)
        {
            return string.IsNullOrEmpty(text) ?
                        string.Empty :
                        _matchAllTags.Replace(text, " ").Replace("&nbsp;", " ");
        }
    }
}
در اینجا حذف تگ‌های HTML و همچنین پردازش خطوط جدید و حروف غیرمجاز درنظر گرفته شده‌اند.

پس از اینکه موفق به شمارش تعداد کلمات یک متن HTML ایی شدیم، اکنون می‌توان این تعداد را تقسیم بر 180 (یک عدد معمول و متداول) کرد تا زمان خواندن کل متن بدست آید. سپس با استفاده از متد toReadableString می‌توان آن‌را به شکل قابل خواندن‌تری نمایش داد.
using System;
 
namespace ReadingTime
{
    public static class CalculateReadingTime
    {
        public static string MinReadTime(this string text, int wordsPerMinute = 180)
        {
            var wordsCount = text.WordsCount();
            var minutes = wordsCount / wordsPerMinute;
            return minutes == 0 ? "کمتر از یک دقیقه" : TimeSpan.FromMinutes(minutes).toReadableString();
        }
 
        private static string toReadableString(this TimeSpan span)
        {
            var formatted = string.Format("{0}{1}{2}{3}",
                span.Duration().Days > 0 ? string.Format("{0:0} روز و ", span.Days) : string.Empty,
                span.Duration().Hours > 0 ? string.Format("{0:0} ساعت و ", span.Hours) : string.Empty,
                span.Duration().Minutes > 0 ? string.Format("{0:0} دقیقه و ", span.Minutes) : string.Empty,
                span.Duration().Seconds > 0 ? string.Format("{0:0} ثانیه", span.Seconds) : string.Empty);
 
            if (formatted.EndsWith("و "))
            {
                formatted = formatted.Substring(0, formatted.Length - 2);
            }
 
            if (string.IsNullOrEmpty(formatted))
            {
                formatted = "0 ثانیه";
            }
            return formatted.Trim();
        }
    }
}

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
ReadingTime.zip
 
مطالب
پیاده سازی RabbitMQ
RabbitMq شبیه به یک صف FIFO عمل میکند؛ یعنی داده‌ها به ترتیب وارد queue میشوند و به ترتیب نیز به Consumer‌ها ارسال میشوند. برای شروع، یک سولوشن جدید را به نام RabbitMqExample ایجاد میکنیم و پروژه‌های زیر را به آن اضافه میکنیم.
  • یک پروژه از نوع Asp.Net Core Web Application ایجاد میکنیم به نام RabbiMqExample.Producer که همان ارسال کننده (Producer) میباشد.
  • یک پروژه از نوع Asp.Net Core Web Application به نام RabbitMqExample.Consumer برای دریافت کننده (Consumer).
  • یک پروژه از نوع Class library .Net Core به نام RabbitMqExample.Common که شامل سرویس‌ها و مدل‌های مشترک بین Producer و Consumer میباشد.
ابتدا در لایه Common یک کلاس برای دریافت اطلاعات RabbitMq از appsettings.json ایجاد میکنیم.
public class RabbitMqConfiguration
{
    public string HostName { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}
سپس یک سرویس را برای برقراری ارتباط با RabbitMq ایجاد میکنیم
public interface IRabbitMqService
{
    IConnection CreateChannel();
}

public class RabbitMqService : IRabbitMqService
{
    private readonly RabbitMqConfiguration _configuration;
    public RabbitMqService(IOptions<RabbitMqConfiguration> options)
    {
        _configuration = options.Value;
    }
    public IConnection CreateChannel()
    {
        ConnectionFactory connection = new ConnectionFactory()
        {
            UserName = _configuration.Username,
            Password = _configuration.Password,
            HostName = _configuration.HostName
        };
        connection.DispatchConsumersAsync = true;
        var channel = connection.CreateConnection();
        return channel;
    }
}
در متد CreateChannel، اطلاعات موردنیاز برای ارتباط با RabbitMq را مانند هاست، نام کاربری و کلمه عبور، وارد میکنیم که از appsettings.json خوانده شده‌اند. مقدار پیش‌فرض نام کاربری و کلمه عبور، guest میباشد.
 اگر بخواهید Consumer شما داده‌های queue‌ها را به صورت async دریافت کند، باید مقدار پراپرتی DispatchConsumersAsync مربوط به ConnectionFactory را برابر true کنید. مقدار پیشفرض آن false است.  
در ادامه یک کلاس را برای رجیستر کردن سرویس‌ها ایجاد میکنیم؛ در لایه Common.
public static class StartupExtension
{
    public static void AddCommonService(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<RabbitMqConfiguration>(a => configuration.GetSection(nameof(RabbitMqConfiguration)).Bind(a));
        services.AddSingleton<IRabbitMqService, RabbitMqService>();
    }
}
پکیج‌های مورد نیاز این لایه :
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
    <PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
  </ItemGroup>
تا به اینجا موارد مربوط به لایه Common تمام شده؛ در ادامه باید یک Consumer و یک Producer را ایجاد کنیم.
در لایه Consumer برای دریافت داده‌ها از RabbitMq؛ یک سرویس را به نام ConsumerService ایجاد میکنیم:
public interface IConsumerService
{
    Task ReadMessgaes();
}

public class ConsumerService : IConsumerService, IDisposable
{
    private readonly IModel _model;
    private readonly IConnection _connection;
    public ConsumerService(IRabbitMqService rabbitMqService)
    {
        _connection = rabbitMqService.CreateChannel();
        _model = _connection.CreateModel();
        _model.QueueDeclare(_queueName, durable: true, exclusive: false, autoDelete: false);
        _model.ExchangeDeclare("UserExchange", ExchangeType.Fanout, durable: true, autoDelete: false);
        _model.QueueBind(_queueName, "UserExchange", string.Empty);
    }
    const string _queueName = "User";
    public async Task ReadMessgaes()
    {
        var consumer = new AsyncEventingBasicConsumer(_model);
        consumer.Received += async (ch, ea) =>
        {
            var body = ea.Body.ToArray();
            var text = System.Text.Encoding.UTF8.GetString(body);
            Console.WriteLine(text);
            await Task.CompletedTask;
            _model.BasicAck(ea.DeliveryTag, false);
        };
        _model.BasicConsume(_queueName, false, consumer);
        await Task.CompletedTask;
    }

    public void Dispose()
    {
        if (_model.IsOpen)
            _model.Close();
        if (_connection.IsOpen)
            _connection.Close();
    }
}
ابتدا connection را ایجاد کرده‌ایم؛ توسط متد CreateChannel که آنرا در سرویس قبلی پیاده سازی کردیم.
بعد از ایجاد IModel، باید queue مربوطه را معرفی کنیم که با استفاده از متد QueueDeclare این کار را انجام داده‌ایم. 
پارامترهای متد QueueDeclare:
  • پارامتر اول، اسم queue میباشد 
  • پارامتر durable مشخص میکند که داده‌ها به صورت مانا باشند یا نه. اگر برابر true باشد، دیتاهای مربوط به queue‌ها، در دیسک ذخیره میشوند؛ اما اگر برابر false باشد، بر روی حافظه ذخیره میشوند. در محیط‌هایی که مانایی اطلاعات مهم میباشد، باید مقدار این پارامتر را true کنید.
  • پارامتر سوم: اطلاعات بیشتر
  • پارامتر autoDelete اگر برابر true باشد، زمانی که تمامی Consumer‌ها ارتباطشان با RabbitMq قطع شود، queue هم پاک میشود. اما اگر برابر true باشد، queue باقی میماند؛ حتی اگر هیچ Consumer ای به آن وصل نباشد.
در ادامه باید Exchange مربوط به queue را مشخص کنیم. متد ExchangeDeclare یک Exchange را ایجاد میکند. پارامتر‌های متد ExchangeDeclare:
  • نام Exchange
  • نوع Exchange که میتواند Headers , Topic ,  Fanout یا Direct باشد. اگر برابر Fanout باشد و اگر داده‌ای وارد Exchange شود، آن‌را به تمامی queue هایی که به آن بایند شده‌است، ارسال میکند. اما اگر نوع آن Direct باشد، داده را به یک queue مشخص ارسال میکند؛ با استفاده از پارامتر routeKey.
  • پارامتر‌های بعدی، durable و autoDelete هستند که همانند پارامترهای QueueDeclare عمل میکنند.
سپس در ادامه با استفاده از متد QueueBind میتوانیم queue ایجاد شده را به exchange ایجاد شده، بایند کنیم. پارامتر اول، اسم queue و پارامتر دوم، اسم exchange میباشد و پارامتر سوم، routeKey میباشد و چون نوع Exchange ایجاد شده از نوع Fanout است، آنرا خالی میگذاریم. 

چون هنگام تعریف queue مقدار پارامتر DispatchConsumersAsync مربوط به ConnectionFactory را برابر true کردیم، در اینجا نیز باید بجای EventingBasicConsumer، از AsyncEventingBasicConsumer استفاده کنیم. اگر مقدار DispatchConsumersAsync برابر false باشد، باید از EventingBasicConsumer برای ایجاد Consumer استفاده کنید. 

سپس باید EventHandler مربوط به دریافت داده‌ها از queue را پیاده سازی کنیم. event مربوط به Received، زمانی اجرا میشود که داده‌ای به queue ارسال شود. زمانیکه داده‌ای ارسال میشود، وارد event مربوطه میشود و ابتدا آنرا به صورت byte دریافت میکنیم. سپس رشته‌ی ارسالی آن‌را توسط متد GetString، بدست می‌آوریم و داده‌ی ارسال شده را در صفحه‌ی کنسول نمایش میدهیم.

 در ادامه به RabbitMq اطلاع میدهیم که داده‌ای که ارسال شده برای queue، توسط Consumer دریافت شده؛ با استفاده از متد BasicAck. این کار یک delivery  به RabbitMq ارسال میکند تا دیتای ارسال شده را را پاک کند. اگر این متد را فراخوانی نکنیم، هربار که برنامه اجرا میشود، تمامی دیتاهای قبلی را مجددا دریافت میکنیم و تا زمانیکه delivery را به RabbitMq نفرستیم، داده‌ها را پاک نمیکند.

نکته آخر در Consumer، متد BasicConsume است که عملا Consumer ایجاد شده را به RabbitMq معرفی میکند. برای دریافت داده‌ها و ثبت Consumer، نیازمند آن هستیم تا یکبار متد ReadMessage فراخوانی شود. برای همین یک HostedService ایجاد میکنیم تا یکبار این متد را فراخوانی کند:
public class ConsumerHostedService : BackgroundService
{
    private readonly IConsumerService _consumerService;

    public ConsumerHostedService(IConsumerService consumerService)
    {
        _consumerService = consumerService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _consumerService.ReadMessgaes();
    }
}
در نهایت سرویس‌های ایجاد شده را رجیستر میکنیم؛ در Startup لایه Consumer
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; set; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCommonService(Configuration);
        services.AddSingleton<IConsumerService, ConsumerService>();
        services.AddHostedService<ConsumerHostedService>();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    }
}
تا به اینجا کارهای مربوط به Consumer تمام شده و باید قسمت Producer آنرا پیاده سازی کنیم.
در لایه Producer یک کنترلر به نام RabbitController را ایجاد میکنیم که شامل یک متد میباشد که داده‌ها را به Queue ارسال میکند:
[Route("api/[controller]/[action]")]
[ApiController]
public class RabbitController : ControllerBase
{
    private readonly IRabbitMqService _rabbitMqService;

    public HomeController(IRabbitMqService rabbitMqService)
    {
        _rabbitMqService = rabbitMqService;
    }
    [HttpPost]
    public IActionResult SendMessage()
    {
        using var connection = _rabbitMqService.CreateChannel();
        using var model = connection.CreateModel();
        var body = Encoding.UTF8.GetBytes("Hi");
        model.BasicPublish("UserExchange",
                             string.Empty,
                             basicProperties: null,
                             body: body);

        return Ok();
    }
}
در متد SendMessage، ابتدا ارتباط خود را با RabbitMq برقرار میکنیم و سپس دیتای "Hi" را به صورت byte، به RabbitMq ارسال میکنیم؛ توسط متد BasicPublish.
پارامتر اول، اسم Exchange است و پارامتر دوم، routeKey و body هم دیتای ارسالی میباشد. در نهایت سرویس‌های مربوط به لایه Producer را رجیستر میکنیم؛ در Startup لایه Consumer:
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; set; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddCommonService(Configuration);
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
        });
    }
}
اکنون اگر هر دو پروژه را اجرا کنید و متد SendMessage مربوط به کنترلر Rabbit را فراخوانی کنید، بعد از آنکه پیام شما ارسال شد، در صفحه کنسول مربوط به Consumer، رشته ارسال شده را مشاهده میکنید.
فایل appsetting.json مربوط به پروژه‌های Consumer و Producer:
{
  "RabbitMqConfiguration": {
    "HostName": "localhost",
    "Username": "guest",
    "Password": "guest"
  }
}
فایل docker-compose.yml برای اجرای RabbitMq بر روی داکر:
version: "3.2"
services:
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: 'rabbitmq'
    ports:
        - 5672:5672
        - 15672:15672
کدهای این مقاله را میتوانید از گیت‌هاب دانلود کنید.