مطالب
معماری لایه بندی نرم افزار #1

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

ضد الگو (Antipattern) – رابط کاربری هوشمند (Smart UI)

با استفاده از Visual Studio یا به اختصار VS، می‌توانید برنامه‌های کاربردی را به راحتی تولید نمایید. طراحی رابط کاربری به آسانی عمل کشیدن و رها کردن (Drag & Drop) کنترل‌ها بر روی رابط کاربری قابل انجام است. همچنین در پشت رابط کاربری (Code Behind) تمامی عملیات مربوط به مدیریت رویدادها، دسترسی به داده ها، منطق تجاری و سایر نیازهای برنامه کاربردی، کد نویسی خواهند شد. مشکل این نوع کدنویسی بدین شرح است که تمامی نیازهای برنامه در پشت رابط کاربری قرار می‌گیرند و موجب تولید کدهای تکراری، غیر قابل تست، پیچیدگی کدنویسی و کاهش قابلیت استفاده مجدد از کد می‌گردد.

به این روش کد نویسی Smart UI می‌گویند که موجب تسهیل تولید برنامه‌های کاربردی می‌گردد. اما یکی از مشکلات عمده‌ی این روش، کاهش قابلیت نگهداری و پشتیبانی و عمر کوتاه برنامه‌های کاربردی می‌باشد که در برنامه‌های بزرگ به خوبی این مشکلات را حس خواهید کرد.

از آنجایی که تمامی برنامه نویسان مبتدی و تازه کار، از جمله من و شما در روزهای اول برنامه نویسی، به همین روش کدنویسی می‌کردیم، لزومی به ارائه مثال در رابطه با این نوع کدنویسی نمی‌بینم.

تفکیک و جدا سازی اجزای برنامه کاربردی (Separating Your Concern)

راه حل رفع مشکل Smart UI، لایه بندی یا تفکیک اجزای برنامه از یکدیگر می‌باشد. لایه بندی برنامه می‌تواند به شکل‌های مختلفی صورت بگیرد. این کار می‌تواند توسط تفکیک کدها از طریق فضای نام (Namespace)، پوشه بندی فایلهای حاوی کد و یا جداسازی کدها در پروژه‌های متفاوت انجام شود. در شکل زیر نمونه ای از معماری لایه بندی را برای یک برنامه کاربردی بزرگ می‌بینید.

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

1- Visual Studio را باز کنید و یک Solution خالی با نام SoCPatterns.Layered ایجاد نمایید.

· جهت ایجاد Solution خالی، پس از انتخاب New Project، از سمت چپ گزینه Other Project Types و سپس Visual Studio Solutions را انتخاب نمایید. از سمت راست گزینه Blank Solution را انتخاب کنید.

2- بر روی Solution کلیک راست نموده و از گزینه Add > New Project یک پروژه Class Library با نام SoCPatterns.Layered.Repository ایجاد کنید.

3- با استفاده از روش فوق سه پروژه Class Library دیگر با نامهای زیر را به Solution اضافه کنید:

  • SoCPatterns.Layered.Model
  • SoCPatterns.Layered.Service
  • SoCPatterns.Layered.Presentation

4- با توجه به نیاز خود یک پروژه دیگر را باید به Solution اضافه نمایید. نوع و نام پروژه در زیر لیست شده است که شما باید با توجه به نیاز خود یکی از پروژه‌های موجود در لیست را به Solution اضافه کنید.

  • Windows Forms Application (SoCPatterns.Layered.WinUI)
  • WPF Application (SoCPatterns.Layered.WpfUI)
  • ASP.NET Empty Web Application (SoCPatterns.Layered.WebUI)
  • ASP.NET MVC 4 Web Application (SoCPatterns.Layered.MvcUI)

5- بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و با انتخاب گزینه Add Reference به پروژه‌ی SoCPatterns.Layered.Model ارجاع دهید.

6- بر روی پروژه SoCPatterns.Layered.Service کلیک راست نمایید و با انتخاب گزینه Add Reference به پروژه‌های SoCPatterns.Layered.Model و SoCPatterns.Layered.Repository ارجاع دهید.

7- بر روی پروژه SoCPatterns.Layered.Presentation کلیک راست نمایید و با انتخاب گزینه Add Reference به پروژه‌های SoCPatterns.Layered.Model و SoCPatterns.Layered.Service ارجاع دهید.

8- بر روی پروژه‌ی UI خود به عنوان مثال SoCPatterns.Layered.WebUI کلیک راست نمایید و با انتخاب گزینه Add Reference به پروژه‌های SoCPatterns.Layered.Model، SoCPatterns.Layered.Repository، SoCPatterns.Layered.Service و SoCPatterns.Layered.Presentation ارجاع دهید.

9- بر روی پروژه‌ی UI خود به عنوان مثال SoCPatterns.Layered.WebUI کلیک راست نمایید و با انتخاب گزینه Set as StartUp Project پروژه‌ی اجرایی را مشخص کنید.

10-  بر روی Solution کلیک راست نمایید و با انتخاب گزینه Add > New Solution Folder پوشه‌های زیر را اضافه نموده و پروژه‌های مرتبط را با عمل Drag & Drop در داخل پوشه‌ی مورد نظر قرار دهید.

  • 1. UI
    • § SoCPatterns.Layered.WebUI

  • 2. Presentation Layer
    • § SoCPatterns.Layered.Presentation

  • 3. Service Layer
    • § SoCPatterns.Layered.Service

  • 4. Domain Layer
    • § SoCPatterns.Layered.Model

  • 5. Data Layer
    • § SoCPatterns.Layered.Repository
  • توجه داشته باشید که پوشه بندی برای مرتب سازی لایه‌ها و دسترسی راحت‌تر به آنها می‌باشد.

پیاده سازی ساختار لایه بندی برنامه به صورت کامل انجام شد. حال به پیاده سازی کدهای مربوط به هر یک از لایه‌ها و بخش‌ها می‌پردازیم و از لایه Domain شروع خواهیم کرد. 

مطالب
معرفی و راهنمایی جهت انتخاب پلتفرم‌های توسعه‌ی رابط کاربری جدید مایکروسافت
اگر به تکنولوژی‌های شرکت مایکروسافت علاقمند باشید و اخبار آن را دنبال کرده باشید قطعا در جریان هستید که علاوه بر تکنولوژی‌های قدیمی (WPF, UWP, Xamarin) تکنولوژی‌های جدیدی (Project Reunion, Maui, WinUI, Uno, Xaml Island) نیز بصورت همزمان در حال توسعه هستند. اکثر این تکنولوژی‌ها شبیه و نزدیک به هم هستند و برای کسی که تازه کار باشد ممکن است دچار سردرگمی شود و چون بصورت همزمان در حال توسعه می‌باشند ممکن سوالاتی برای شما پیش بیاید. در این مطلب هر کدام از این تکنولوژی‌ها را معرفی کرده و در انتخاب صحیح به شما کمک خواهیم کرد.

ساخت برنامه با WPF
به کمک تکنولوژی WPF میتوانیم نرم افزارهای دسکتاپ را توسعه دهیم. WPF همچنان پشتیبانی میشود و در سال‌های اخیر بصورت متن باز نیز منتشر شده‌است. اگر نیاز دارید که برنامه شما در ویندوز‌های 7 تا ویندوز 11 اجرا شود، میتوانید از WPF استفاده کنید. لازم به ذکر است که برنامه‌های WPF به عنوان Win32 یا Desktop نیز شناخته میشوند.

ساخت برنامه با UWP
UWP بعد از WPF و با انتشار ویندوز 10 معرفی شد. علت انتشار، هماهنگی برنامه‌ها با سیستم عامل ویندوز 10 و امنیت بیشتر بود. به‌طور فنی برنامه‌ای که بصورت UWP ساخته میشود، همان WPF است؛ با این تفاوت که داخل SandBox اجرا میشود و با محیط خارج ارتباطی ندارد. بدلیل مسائل امنیتی، بسیاری از کارهای ساده و مهم در UWP غیرممکن (البته راه حل‌هایی نیز وجود دارد) می‌باشد و نیاز به دسترسی کاربر دارد. به عنوان مثال، API‌های system.Io.File یا Process قابل استفاده نمی‌باشند.
نرم افزارهایی که با UWP ساخته میشوند فقط بر روی ویندوز 10 به بالا قابلیت اجرایی دارند و توزیع آن از طریق استور مایکروسافت امکان پذیر است. در صورت نیاز به توزیع دستی (فایل نصبی)، توسعه دهنده باید فایل نصبی را بصورت دیجیتال، امضاء کند که دردسر‌های خودش را دارد.

ساخت برنامه با Xamarin
اگر نیاز دارید که برای سیستم عامل اندروید و مک برنامه بنویسید، زامارین میتواند به شما کمک کند.

ساخت برنامه با WinUI
بعد از معرفی UWP نیاز به یک فریمورک رابط کاربری قوی جهت جذب کاربران به سمت UWP احساس شد. در نتیجه مایکروسافت فریمورک WinUI را ایجاد کرد. WinUI در 2 نسخه در حال توسعه می‌باشد:

WinUI 2.X
این نسخه از WinUI فقط قابلیت استفاده در برنامه‌های مبتنی بر UWP را دارد. اخیرا نسخه 2.6 آن منتشر شده که شامل تغییرات بصری عظیمی می‌باشد. لازم به ذکر است که ویندوز 11 که اخیرا معرفی شد، بر پایه WinUI 2.6 ایجاد شده است.

WinUI 3.X
این نسخه از WinUI قابلیت استفاده در پلتفرم‌های دیگر را محیا می‌کند و هم اکنون بصورت پیش نمایش است و بر پایه WinUI 2.5 می‌باشد. در 3 ماهه آخر سال 2021 تمامی استایل‌ها بر پایه نسخه 2.6 خواهد بود.
 

پلتفرم Uno
پلتفرم اونو توسط مایکروسافت ایجاد نشده، اما توسط آن پشتیبانی میشود. شما به کمک پلتفرم اونو میتوانید به کمک WinUI 3، برنامه‌های خود را در ویندوز 7 (به کمک موتور رندر Skia ) تا ویندوز 11، لینوکس (به کمک Skia)، مک و حتی موبایل اجرا کنید.

پلتفرم Maui
مائویی در واقع نسل بعدی زامارین می‌باشد و بصورت تک پروژه‌ای ایجاد شده‌است. در زامارین شما برای هر پلتفرم (ویندوز، اندروید، مک و...) یک پروژه جداگانه داشتید، اما در مائویی فقط یک پروژه واحد وجود دارد. پس اگر نیاز دارید که برای گوشی‌های همراه برنامه نویسی کنید، میتوانید از مائویی استفاده کنید. لازم به ذکر است به کمک مائویی میتوانید برای لینوکس و مک هم برنامه ایجاد کنید. اما بدلیل وجود WinUI در سایر پلتفرم‌ها، بهتر است از مائویی فقط برای ایجاد برنامه‌های موبایل استفاده کنید.
 

پلتفرم Project Reunion
اخیرا نام این پروژه به Windows App SDK تغییر یافته‌است. به کمک این پروژه میتوانید از WinUI 3 در برنامه‌های WPF و سایر تکنولوژی‌های Desktop استفاده کنید و کل برنامه خود را مدرن کنید. لازم به ذکر است که برنامه‌های ساخته شده توسط Reunion فقط در ویندوز 10 به بالا اجرا میشوند. در حال حاضر جهت اجرای برنامه نیاز هست که برنامه بصورت MSIX پکیج بشود. در نسخه 1.0 که تا چند ماه آینده منتشر خواهد شد، نیازی به پکیج کردن نخواهد بود.

پلتفرم Xaml Island
این پلتفرم در واقع پلی است که میتوانید از کنترل‌های UWP یا WinUI در برنامه‌های دسکتاپ (WPF) استفاده کنید. تفاوت این پلتفرم با Reunion در این است که شما فقط میتوانید بخشی از برنامه خود را مدرن کنید و قسمت‌های مدرن شده در ویندوز‌های پایین‌تر از ویندوز 10، کار نخواهند کرد. اما در Reunion تمام بخش‌های برنامه شما مدرن خواهد شد.

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

اگر نیاز به اجرای برنامه بصورت کراس پلتفرم دارید (ویندوز/لینوکس/مک) بهتر است از Uno استفاده کنید بدلیل اینکه:
  • مخصوص کراس پلتفرم طراحی شده
  • از WinUI 3 استفاده می‌کند
  • برای ویندوز 10 به بالا از تکنولوژی UWP و برای ویندوز 7 و لینوکس از Skia استفاده می‌کند

اگر نیاز دارید برنامه شما فقط در ویندوز 10 به بالا اجرا شود بهتر است از Project Reunion استفاده کنید بدلیل اینکه:
  • از WinUI 3 استفاده می‌کند
  • تمام ویژگی‌های UWP را دارد
  • محدودیت‌های UWP را ندارد
  • بصورت Full Trust اجرا میشود
  • پیچیدگی‌های UWP را ندارد
  • از پلتفرم WPF برای اجرا استفاده می‌کند

اگر نیاز دارید که برنامه شما در ویندوز 7 به بالا اجرا شود و در ویندوز 10 ظاهر مدرن‌تری به خود بگیرد بهتر است از Xaml Island استفاده کنید بدلیل اینکه:
  • فقط بخشی از برنامه را مدرن می‌کند
  • قسمت‌های مدرن شده در نسخه‌های قبل ویندوز 10 اجرا نمیشود

نظرات مطالب
آغاز به کار با Twitter Bootstrap در ASP.NET MVC
فایل bootstrap.css  بصورت ناقص فشرده می‌شود.
بطور دقیق‌تر تا رسیدن به کاراکترهای :: بدون مشکل کار پیش می‌رود.
در حالت اجرا وقتی فایل را css را باز میکنیم فقط کلاس‌ها و شناسه‌های قبل از :: وجود دارند. 
مطالب
ارتباط بین کامپوننت ها در Vue.js - قسمت چهارم کاربرد Vuex - بخش اول
در قسمت‌های قبلی (^ ,^ ,^ ) نحوه‌ی ارتباط بین کامپوننت‌ها بررسی شد؛ روش دیگری هم برای به اشتراک گذاری داده‌ها بین کامپوننت‌ها وجود دارد که با استفاده از کتابخانه‌ای بنام Vuex پیاده سازی میشود. وقتی برنامه‌ی شما وسعت پیدا میکند و ارتباط بین کامپوننت‌ها بیشتر و پیچیده‌تر می‌شود، روشهای قبلی (^ ,^ ,^ ) کارایی لازم را ندارند و یا اینکه به سختی میشود داده‌های به اشتراک گذاشته شده‌ی بین کامپوننت‌ها را مدیریت نمود. در اینجا میتوان از Vuex  استفاده کرد و به‌راحتی ارتباط‌های پیچیده‌ی بین کامپوننت‌ها را مدیریت کرد.


کد زیر را در نظر بگیرید:
new Vue({
  // state
  // داده‌ها در اینجا قرار میگیرند
  data () {
    return {
      count: 0
    }
  },
  // view
  // ویوها برای نمایش داده‌ها مورد استفاده قرار میگیرند
  template: `<div>{{ count }}</div>`,
  // actions
  // برای تغییر داده‌ها از متدها استفاده میکنیم
  methods: {
    increment () {
      this.count++
    }
  }
})


در یک کامپوننت ساده، از طریق Actionها، داده‌ها (State) تغییر داده میشوند و سپس این تغییر در view مشاهده میشود. اما فرض کنید بیش از صد کامپوننت در برنامه دارید که بسیاری از آنها از داده‌های واحدی استفاده میکنند. روشهای قبلی (^ ,^ ,^) برای چنین سناریویی جوابگو نخواهند بود (آن‌را به سختی میتوان مدیریت کرد و بسیار طاقت فرسا خواهد بود).

راه حل Vuex:
با استفاده از Vuex میتوان برای داده‌ها (State)، یک منبع در نظر گرفت تا کامپوننت‌ها قادر باشند از داده‌های واحدی استفاده کنند و اشتراک گذاری داده‌ها ساده شود.


یک برنامه ساده با استفاده از Vuex:

یک پروژه Vuejs را ایجاد کنید و مطابق تصویر زیر، گزینه دوم را انتخاب و Enter را فشار دهید:


سپس گزینه Vuex را طبق تصویر زیر با دکمه‌ی space انتخاب کنید و برای مابقی گزینه‌های بعدی با زدن Enter، پیش فرض‌ها را بپذیرید تا پروژه ساخته شود:


دو کامپوننت را به برنامه اضافه میکنیم.

کامپوننت اول با نام increase-counter-component.vue  

<template>
  <div>
    <!--  نمایش شمارشگر  -->
    <h1>{{count}}</h1>
    <!--  افزودن یک واحد به شمارشگر  -->
    <button @click="add">Add 1</button>
    <!--  افزودن مقداری دلخواه به شمارشگر  -->
    <button @click="add2">Add 2</button>
  </div>
</template>

<script>
// یا همان منبع ذخیره داده‌ها store کردن  import
import store from "../store";

export default {
  // You can consider computed properties another view into your data.
  // https://css-tricks.com/methods-computed-and-watchers-in-vue-js/
  computed: { count: () => store.state.count },

  // به دو طریق فراخوانی شده  add تابع
  methods: {
    // بدون پارامتر
    add: () => store.commit("add"),
    // با  پارامتر
    // برای مقدار مورد نظر استفاده کنیم input میتوانیم بجای مقدار ثابت از یک
    add2: () => store.commit("add", 2)
  }
};
</script>


کامپوننت دوم با نام decrease-counter-component.vue  

<template>
  <div>
    <h1>{{count}}</h1>
    <button @click="subtract">Subtract 1</button>
    <button @click="subtract(3)">Subtract 3</button>
  </div>
</template>

<script>
import store from "../store";

export default {
  computed: { count: () => store.state.count },

  methods: {
    subtract: payload => store.commit("subtract", +payload)
  }
};
</script>
درون کامپوننت اصلی برنامه App.vue، هر دو کامپوننت را فراخوانی میکنیم:
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <counter-plus></counter-plus>
    <hr />
    <hr />
    <counter-minus></counter-minus>
  </div>
</template>

<script>
import counterPlus from "./components/increase-counter-component";
import counterMinus from "./components/decrease-counter-component";
export default {
  name: "app",
  components: {
    "counter-plus": counterPlus,
    "counter-minus": counterMinus
  }
};
</script>
محتویات فایل  store.js  که تنظیمات Vuex در آن لحاظ شده‌است به شکل زیر می‌باشد:
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  //  داده‌های به اشتراک گذاشته شده
  state: {
    count: 0
  },
  // تعریف متدها
  mutations: {
    add(state, payload) {
      // If we get a payload, add it to count
      // Else, just add one to count
      payload ? (state.count += payload) : state.count++;
    },
    subtract(state, payload) {
      payload ? (state.count -= payload) : state.count--;
    }
  }
});
در Terminal دستور زیر را تایپ و اجرا کنید تا نتیجه رویت گردد:
npm run serve
در این برنامه از دو کامپوننت مجزا با داده‌ی واحد، استفاده میکنیم و دیگر خبری از emit$ و on$ و EventBus و تزریق وابستگی نخواهد بود.

چگونه کار میکند؟

در Vuex، متدها در قسمت mutation در فایل store.js نوشته میشوند و در methods  درون کامپوننت‌ها فراخوانی میشوند. اگر با سی شارپ آشنا باشید، این فراخوانی تقریبا  شبیه delegate می‌باشد. داده‌ها در store.js تعریف میشوند و در سراسر برنامه در تمام کامپوننت‌ها قابل دسترس می‌باشند. بدین ترتیب اشتراک گذاری داده‌ها بین کامپوننت‌ها بسیار ساده می‌باشد.


نکته:    برای دریافت پکیج‌های مورد استفاده در مثال جاری، نیاز است دستور زیر را اجرا کنید:  
 
npm install
سپس برنامه را با دستور زیر اجرا کنید: 
npm run serve

مطالب
کار با Docker بر روی ویندوز - قسمت دوم - نصب Docker
پس از آشنایی با مفهوم Containers، در این قسمت قصد داریم برنامه‌ی تقریبا 500 مگابایتی Docker for Windows Installer.exe را نصب کنیم.
 
پیش‌نیازهای نصب Docker بر روی ویندوز

مطابق مستندات آن، برای نصب داکر بر روی ویندوز به حداقل‌های زیر نیاز است:
- استفاده از ویندوز 10 نگارش enterprise، که شماره نگارش آن حداقل 1607 باشد (حداقل Anniversary Update باشد).
- مجازی سازی در BIOS فعال شده باشد.
البته مجازی سازی عموما به صورت پیش‌فرض فعال است. برای بررسی آن، taskmanager ویندوز را اجرا کرده و در برگه‌ی Performance آن، جائیکه مشخصات CPU را نمایش می‌دهد، یک سطر به Virtualization اختصاص دارد که مقدار آن باید enabled باشد (تصویر زیر) و اگر نیست، برای فعال کردن آن باید به تنظیمات BIOS سیستم خود مراجعه کنید:


روش دیگر دریافت این اطلاعات، اجرای دستور systeminfo در خط فرمان، با دسترسی مدیریتی است. در خروجی آن، عبارت «Virtualization Enabled In Firmware» را جستجو کنید که باید مقدار آن yes باشد.

- داشتن CPU با قابلیت SLAT یا Second Level Address Translation.
برای یافتن این موضوع، برنامه‌ی coreinfo را دریافت کرده و آن‌را به صورت coreinfo -v اجرا کنید. خروجی آن سه سطر مرتبط با مجازی سازی را به همراه دارد. اگر قابلیتی موجود نباشد، جلوی آن یک خط تیره و اگر قابلیتی موجود باشد، روبروی آن یک ستاره را مشاهده خواهید کرد.

روش دیگر بررسی آن، اجرای دستور msinfo32 در قسمت run ویندوز و سپس enter است. در قسمت system summary، اطلاعات Second Level Address Translation قابل مشاهده هستند (اگر No باشد، امکان اجرای containerهای لینوکسی را بر روی ویندوز نخواهید داشت):


- داشتن حداقل 4 گیگابایت RAM.
- فعال بودن Hyper-V نیز برای اجرای Linux Containers بر روی ویندوز، ضروری است (نصاب Docker، این‌کار را به صورت خودکار انجام می‌دهد).


دریافت نصاب Docker for Windows

برای دریافت نصاب داکر مخصوص ویندوز، به آدرس زیر مراجعه کنید:
https://store.docker.com/editions/community/docker-ce-desktop-windows
که بلافاصله با تصویر کریه زیر مواجه خواهید شد:


برای رفع این مشکل، می‌توان از روش مطرح شده‌ی در مطلب «یک روش ساده برای دور زدن تحریم‌ها!» استفاده کرد؛ یعنی تنظیم DNS به 178.22.122.100 به صورت زیر:


پس از این تغییر، چون IP قابل مشاهده‌ی سیستم شما توسط سایت داکر تغییر می‌کند، اینبار صفحه‌ی دریافت Docker Community Edition for Windows به صورت زیر ظاهر می‌شود:


همانطور که مشاهده می‌کنید، عنوان کرده‌است که لطفا لاگین کنید تا بتوانید این برنامه را دریافت کنید. به همین جهت بر روی لینک آن کلیک کرده، یک اکانت جدید را در سایت docker ایجاد کنید (با یک ایمیل واقعی که تائیدیه آن‌را دریافت خواهید کرد). پس از آن، با این اکانت جدید به سایت داکر وارد شوید تا لینک دریافت فایل exe نصاب آن‌را دریافت کنید.
در این حالت مرورگر و یا حتی دانلودمنیجر شما بدون مشکل می‌توانند این فایل را دریافت کنند و همان تنظیم DNS فوق، مشکل عدم دسترسی را برطرف می‌کند.


نصب Docker for Windows

پس از اجرای نصاب آن و پایان عملیات نصب (که تنها کافی است در صفحه‌ی ابتدایی آن تیک مربوط به Windows Containers را نیز قرار دهید)، نیاز دارد تا شما را یکبار از سیستم Logout و login کند. پس از ورود به سیستم، تنظیمات ابتدایی آن به صورت خودکار صورت گرفته و در صورت فعال نبودن Hyper-V، پیام زیر را مشاهده خواهید کرد:


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

یک نکته: کاری که در قسمت فعالسازی Hyper-V به صورت خودکار انجام می‌شود، شامل اجرای سه دستور زیر، در کنسول پاور شل، با دسترسی مدیریتی و سپس ری استارت سیستم است:
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -Verbose
Enable-WindowsOptionalFeature -Online -FeatureName Containers -All -Verbose
bcdedit /set hypervisorlaunchtype Auto
پس از آن، خط فرمان را باز کرده و با ستفاده از docker CLI نصب شده، دستور docker info را صادر کنید، تا بتوانید مشخصات نگارش نصب شده را مشاهده نمائید.
C:\Users\Vahid>docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 18.06.1-ce
OSType: windows
OSType را در صورتیکه سیستم شما توانمندی‌های سخت افزاری لازم را داشته باشد، می‌توان به Linux نیز تغییر داد.


بررسی تنظیمات سوئیچ کردن بین Linux Containers و Windows Containers

پس از اتمام ری‌استارت‌ها، برای آزمایش فعال بودن Hyper-V، در قسمت Run ویندوز، عبارت Virtmgmt.msc را نوشته و enter کنید. اگر تصویر زیر را مشاهده نمی‌کنید:


یکبار بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینه‌ی switch to Linux containers را انتخاب کنید تا پس از مدتی، آیکن MobyLinuxVM در قسمت virtual machines (تصویر فوق) ظاهر شود.


اگر پس از انتخاب این گزینه، پیام زیر را دریافت کردید:


و یا اگر بر روی این ماشین مجازی کلیک راست کردید و گزینه‌ی Start آن‌را انتخاب کردید و پیام زیر ظاهر شد:


قسمت «پیش‌نیازهای نصب Docker بر روی ویندوز» را با دقت بررسی کنید (خصوصا قسمت BIOS و SLAT). نبود یکی از موارد ذکر شده، سبب بروز این مشکل می‌شود.
برای مثال اجرای دستور coreinfo -v بر روی سیستم من چنین خروجی را به همراه دارد:
E:\>coreinfo -v

AuthenticAMD
Microcode signature: 00000000
HYPERVISOR      -       Hypervisor is present
SVM             *       Supports AMD hardware-assisted virtualization
NP              -       Supports AMD nested page tables (SLAT)
روبروی HYPERVISOR و همچنین SLAT یک - را قرار داده‌است. یعنی این موارد یا پشتیبانی نمی‌شوند و یا در BIOS فعال نشده‌اند.
همانطور که مشاهده می‌کنید، قابلیت SLAT در CPU این سیستم وجود ندارد. به همین جهت نمی‌توان به Linux containers سوئیچ کرد. هرچند windows containers آن کار می‌کند.

روش دیگر مشاهده‌ی این خطا، مراجعه‌ی به event viewer ویندوز است. در قسمت خطاهای سیستم، ممکن است خطای زیر را مشاهده کنید:
Hypervisor launch failed; Second Level Address Translation is required to launch the hypervisor.


آزمایش Docker نصب شده

پس از نصب docker، خط فرمان ویندوز را گشوده و دستور زیر را صادر کنید:
docker run hello-world
اگر از قسمت قبل به‌خاطر داشته باشید، hello-world در اینجا نام یک image است. البته چون این image بر روی سیستم ما موجود نیست، این دستور، ابتدا آن‌را از docker hub دریافت کرده و سپس اجرا می‌کند. بنابراین می‌شد ابتدا دستور pull را صادر کرد و سپس run. اما دستور run قادر است هر دو عمل را با هم انجام دهد.

یک نکته: این image، یک image لینوکسی است. به همین جهت پیش از اجرای این دستور، همانطور که پیشتر نیز عنوان شد، یکبار بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینه‌ی switch to Linux containers را انتخاب کنید. سپس دستور docker run hello-world را اجرا نمائید.

و یا در همین حال دستور docker run -p 80:80 nginx را صادر کنید تا وب سرور لینوکسی nginx را بتوانید تحت ویندوز اجرا کنید. پس از خاتمه‌ی عملیات دریافت و اجرای وب سرور، با توجه به تنظیم  p 80:80-، پورت 80 میزبان (اولین عدد)، به پورت 80 کانتینر نگاشت شده‌است. به همین جهت تنها با اجرای دستور http://localhost، خروجی این وب سرور را می‌توانید در مرورگر سیستم خود مشاهده کنید.
همانطور که مشاهده می‌کنید، با استفاده از داکر، پیش از آنکه بدانیم چگونه باید یک نرم افزار را نصب کرد، می‌توان از آن استفاده کرد!


روش متوقف کردن Containers در حال اجرا

اگر دستور docker ps را در خط فرمان ویندوز اجرا کنید، لیست پروسه‌های اجرا شده‌ی توسط آن قابل مشاهده هستند. در این لیست container id در حال اجرا نیز مشخص است. برای خاتمه‌ی کار آن، تنها کافی است دستور docker stop id را اجرا کنید.
یک نکته: ضرورتی به ذکر کامل id نیست. برای مثال ذکر سه حرف اول آن نیز کفایت می‌کند.


روش اجرای مجدد یک Container

فرض کنید می‌خواهیم سرور nginx را مجددا اجرا کنیم. یک روش آن، اجرای مجدد دستور docker run -p 80:80 nginx است که پیشتر آن‌را انجام دادیم. در این حالت این image تبدیل به container شده و همانند روش‌های متداول نصب نرم افزار، اکنون به عنوان یک نرم افزار نصب شده در دسترس است. برای مشاهده‌ی لیست آن‌ها، دستور docker ps -a را اجرا کنید. این لیست تا این لحظه باید شامل containerهای nginx و hello-world باشد. متوقف کردن یک container، سبب تخریب یا حذف آن نمی‌شود. در این حالت در لیستی که توسط دستور docker ps -a نمایش داده شده‌است، باز هم container idها قابل مشاهده هستند. فقط کافی است برای اجرای یکی از آن‌ها، دستور docker start id را اجرا کرد. به این صورت دیگر نیازی به ذکر دستور کامل docker run با تمام پارامترهای آن نیست. این id نیز همانطور که ذکر شد، می‌تواند سه حرف ابتدایی این id باشد تا حدی که نسبت به سایر idهای موجود، منحصربفرد شناخته شود. یا بجای container id می‌توان container name نمایش داده شده‌ی در این لیست را استفاده کرد.
پس از اجرای nginx توسط دستور docker start id، دو روش برای بررسی در حال اجرا بودن آن وجود دارد:
الف) مرورگر را باز کنیم و آدرس http://localhost را بررسی کنیم.
ب) دستور docker ps را در خط فرمان اجرا کنیم، تا مشخص شود که آیا پروسه‌ی nginx در حال اجرا است یا خیر؟

بنابراین دستور docker ps -a لیست تمام containers در حال اجرا و همچنین متوقف شده را نمایش می‌دهد. اما دستور docker ps تنها لیست containers در حال اجرا را نمایش خواهد داد.


روش حذف Containers از Docker

همانطور که در قسمت قبل نیز بحث شد، معادل نصب نرم افزار در اینجا، ایجاد یک container از یک image دریافتی از docker hub است. روش عکس آن، یعنی تخریب یک container، دقیقا معادل عزل نرم افزار از سیستم، در حالت‌های متداول است. برای اینکار مجددا دستور docker ps -a را اجرا می‌کنیم تا لیست تمام containerهای در حال اجرا و همچنین متوقف شده نمایش داده شوند. لیستی که در اینجا نمایش داده می‌شود، شبیه به لیستی است که در قسمت add/remove programs ویندوز مشاهده می‌کنید. این لیست معادل لیست نرم افزارهای نصب شده‌ی بر روی سیستم است و یا برای مشاهده‌ی لیست imageهای دریافتی از docker hub می‌توان دستور docker images را صادر کرد.
قبل از حذف یک container نیاز است آن‌را متوقف کنیم. برای این منظور از دستور docker stop id استفاده می‌شود. سپس اجرای دستور docker rm id، سبب حذف کامل این container خواهد شد. برای آزمایش آن، مجددا دستور docker ps -a را اجرا کنید.
دستور docker rm چندین id را نیز می‌پذیرد. می‌توان این idها و یا حتی سه حرف ابتدایی آن‌ها را با فاصله در اینجا ذکر کرد. علاوه بر id، ذکر نام containers نیز مجاز است.


روش حذف Imageهای دریافتی از Docker Hub

دستور docker rm، فقط containers را از سیستم حذف می‌کند (نرم افزارهای نصب شده). اما خود imageهای اصلی دریافت شده‌ی از docker hub را حذف نمی‌کند (معادل همان فایل‌های zip دریافت نرم افزار یا برنامه‌های نصاب، در حالت متداول و سنتی نصب نرم افزار). برای آزمایش آن دستور docker images را اجرا کنید. هنوز هم در لیست آن، تمام موارد دریافتی موجود هستند.
برای حذف یک image می‌توان از دستور docker rmi id استفاده کرد (rmi بجای rm). این id نیز در لیست docker images ظاهر می‌شود و ذکر قسمتی از آن، تا حدی که نسبت به سایر idهای لیست شده منحصربفرد باشد، کافی است. در اینجا بجای id، از نام image نیز می‌توان استفاده کرد. همچنین ذکر چندین id و یا نام نیز پس از دستور docker rmi، میسر است.


روش جستجوی imageها در Docker Hub توسط Docker CLI

فرض کنید می‌خواهیم image مربوط به راهنمای Docker را از Docker Hub دریافت کنیم. یک روش آن مراجعه‌ی مستقیم به سایت آن است و استفاده از امکانات جستجوی فراهم شده‌ی در آن سایت. روش دیگر، استفاده از Docker CLI است. اگر دستور docker search docs را در خط فرمان اجرا کنیم، لیست تمام مخازن کدی که در آن‌ها واژه‌ی docs قرار دارد، نمایش داده می‌شود. البته پیش از نصب image آن بهتر است به برگه‌ی tags مخزن کد آن نیز مراجعه کنید تا بتوانید حجم آن‌را نیز مشاهده نمائید که حدود یک گیگابایت است. مخازن docker hub، حاوی imageهای نصاب containerهای متناظر هستند. برای دریافت و اجرای آن می‌توان دستور docker run -p 4000:4000 docs/docker.github.io را اجرا کرد.
پس از دریافت یک گیگابایت مستندات، container آن بر روی پورت 4000 در سیستم ما (http://localhost:4000)، به صورت یک وب سایت استاتیک، قابل دسترسی خواهد بود. به این صورت می‌توان به مستندات کامل داکر به صورت آفلاین دسترسی داشت.


مفهوم Interactive Terminal در Docker

زمانیکه دستور اجرای مستندات آفلاین را صادر می‌کنید، در انتهای آن عنوان می‌کند که وب سایت محلی آن بر روی پورت 4000 قابل دسترسی است. سپس در ذیل آن ذکر شده‌است که اگر ctrl+c را فشار دهید، اجرای آن به پایان می‌رسد. اما عملا اینطور نیست و اگر دستور docker ps را صادر کنید، هنوز container در حال اجرای آن را می‌توان مشاهده کرد.
اما اگر اینبار دستور اجرای docker run را به همراه یک interactive terminal با سوئیچ it و نام docs صادر کنیم:
 docker run -p 4000:4000 -it --name docs docs/docker.github.io
اکنون اگر ctrl+c را فشار دهیم و پس از آن دستور docker ps را صادر کنیم، دیگر در لیست آن، container در حال اجرای docs مشاهده نمی‌شود.
سوئیچ it یا interactive terminal سبب می‌شود تا یک container در foreground، بجای background اجرا شود. به این ترتیب دستور ctrl+c، سبب خاتمه‌ی واقعی پروسه‌ی درحال اجرای در container می‌شود.
روش دیگر خاتمه‌ی این container، استفاده از نام ذکر شده‌است؛ یعنی اجرای دستور docker stop docs.

یک نکته: اگر می‌خواهید از terminal باز شده قطع شوید (مجددا به command prompt باز گردید) اما سبب خاتمه‌ی container آن نشوید، از ترکیب ctrl+p+q استفاده کنید.


اجرای containerهای ویندوزی

در مورد نحوه‌ی سوئیچ بین نوع‌های مختلف containerهای ویندوزی و لینوکسی پیشتر توضیح دادیم. برای این منظور می‌توان بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینه‌ی switch to Windows/Linux containers را انتخاب کرد. باید دقت داشت که پشتیبانی از containerهای ویندوزی، از ویندوز 10، نگارش  1607، یا همان Anniversary Update آن به بعد، به ویژگی‌های ویندوز اضافه شده‌اند که به صورت خودکار توسط docker فعالسازی می‌شوند:



اجرای IIS به عنوان یک Windows Container

تا اینجا imageهای دریافتی، لینوکسی بودند. اگر گزینه‌ی Windows Containers را به روشی که گفته شد، فعال کنید، اینبار با اجرای دستورات docker ps و یا docker images، هیچ خروجی را دریافت نخواهید کرد. از این جهت که کانتینرهای ویندوزی و لینوکسی، به صورت کاملا ایزوله‌ای از هم اجرا و مدیریت می‌شوند. علت آن‌را هم در MobyLinuxVM که پیشتر با اجرای دستور Virtmgmt.msc بررسی کردیم، می‌توان یافت. Containerهای لینوکسی، در داخل MobyLinuxVM اجرا می‌شوند.
در اینجا به عنوان مثال می‌توان image رسمی مربوط به IIS را از docker hub دریافت و به صورت یک کانتینر ویندوزی اجرا کرد. البته پیش از اجرای دستورات آن بهتر است به برگه‌ی tags آن مراجعه کرده و حجم‌های نگارش‌های مختلف آن‌را بررسی کرد. اجرای دستور docker pull microsoft/iis به معنای دریافت tag ای به نام latest است (به حجم 6 گیگابایت!)؛ یعنی با دستور docker pull microsoft/iis:latest یکی است. بنابراین در اینجا بر اساس tagهای مختلف، می‌توان دستور pull متفاوتی را صادر کرد. برای مثال اگر دستور docker pull microsoft/iis:nanoserver را صادر کردید، نگارش مخصوص nano server آن‌را که فقط 449 مگابایت است، دریافت می‌کند. بنابراین از این پس به tagهای هر مخزن docker hub خوب دقت کنید و نگارش مختص به سیستم عامل خود را دریافت نمائید. عدم ذکر tag ای، همواره tag ویژه‌ای را به نام latest، دریافت می‌کند.
با اجرای دستور زیر
 docker run -p 81:80 -d --name iis microsoft/iis:nanoserver
داکر، ابتدا image مخصوص nanoserver آن‌را دریافت و سپس اجرا می‌کند. چون وب سرور است، نیاز به تنظیمات پورت آن‌را داریم. p 81:80- به این معنا است که پورت 80 کانتینر را (پورتی که IIS درون آن بر روی آن اجرا می‌شود)، به پورت 81 بر روی سیستم میزبان (یا همین ویندوز فعلی که داکر را اجرا می‌کند)، نگاشت کن. پارامتر d در اینجا به معنای detach است. یعنی به محض اجرای این دستور، کار اجرای این کانتینر در background انجام شده و سپس به خط فرمان، بازگشت داده می‌شویم. همچنین نامی نیز به این container انتساب داده شده‌است تا ساده‌تر بتوان با آن کار کرد.

یک نکته: مشکلی با اجرای IIS مخصوص نانوسرور بر روی ویندوز 10 به این صورت و توسط داکر نیست. بنابراین پس از اجرای دستور فوق، کار دریافت image و ساخت container و سپس اجرای آن به صورت خودکار انجام شده و بلافاصله به command prompt بازگشت داده می‌شویم (به علت استفاده‌ی از پارامتر d). اکنون اگر دستور docker ps را صادر کنیم، مشاهده می‌کنیم که کانتینر IIS مخصوص نانوسرور، هم اکنون بر روی ویندوز 10 در حال اجرا است و در آدرس http://localhost:81 قابل دسترسی است.

جهت تکمیل این بحث، بهتر است image مخصوص nanoserver را نیز از docker hub دریافت و اجرا کنیم:
 docker run microsoft/windowsservercore
حجم image آن 6GB است.


تنظیمات کارت شبکه‌ی Containers

هنگامیکه پروسه‌ای درون یک container اجرا می‌شود، ایزوله سازی‌های بسیاری نیز در مورد آن اعمال خواهد شد؛ به همین جهت گاهی از اوقات عده‌ای containerها را با ماشین‌های مجازی نیز مقایسه می‌کنند. برای مثال کانتینرها به همراه network adapter خاص خود نیز هستند؛ درست مانند اینکه یک کامپیوتر مجزای از سیستم جاری می‌باشند و اگر این network adapter را ping کنیم، می‌توان به این صورت نیز به آن کانتینر، دسترسی داشته باشیم.
برای یافتن آن، دستور docker inspect iis را صادر می‌کنیم. خروجی آن به همراه یک قسمت network نیز هست که داخل آن یک IP Address قابل مشاهده است. این IP است که مختص و منحصربفرد این container است. در ابتدا برای آزمایش آن، می‌توان آن‌را ping کرد؛ مانند ping 172.27.49.47. همچنین به تمام برنامه‌های داخل این container توسط این IP نیز می‌توان دسترسی یافت. برای مثال فراخوانی http://172.27.49.47:81 در مرورگر، سبب نمایش صفحه‌ی اول IIS می‌شود. البته اگر اینکار را انجام دهیم، کار نمی‌کند. علت اینجا است، نگاشت پورتی را که تعریف کرده‌ایم (پورت 81)، به پورتی در کامپیوتر میزبان است و نه این IP ویژه. برنامه‌ی اصلی IIS در داخل container، به پورت 80 بر روی این آدرس IP گوش فرا می‌دهد. اکنون اگر آدرس http://172.27.49.47:80 را در کامپیوتر میزبان فراخوانی کنیم، کار می‌کند.
بنابراین هرچند containerها به معنای نرم افزارهای از پیش نصب شده‌ی در حال اجرا هستند، اما ... به همراه ایزوله سازی‌های قابل توجهی بر روی کامپیوتر میزبان اجرا می‌شوند؛ درست مانند یک کامپیوتر مجزای از آن.
مطالب
کار با شاخه‌ها و انشعابات Git در Visual Studio
در مطلب «آشنایی با ساختار یک Pull Request خوب» عنوان شد که قابلیت‌های جدید و یا رفع مشکلات را در شاخه‌ی اصلی کار نکنید. اما ... چگونه؟


ایجاد یک شاخه‌ی جدید در Visual Studio و انتشار آن

به برگه‌ی Team explorer مراجعه کرده و سپس گزینه‌ی Branches آن‌را انتخاب کنید:


در برگه‌ی باز شده، انشعاب و شاخه‌ی جاری با فونت ضخیم نمایش داده می‌شود. برای مثال در اینجا، انشعاب کاری همان master است:


برای ایجاد یک شاخه‌ی جدید، بر روی لینک new branch کلیک کنید تا بتوان نامی را برای این منظور وارد کرد. بهتر است از نام‌های با مفهومی مانند feature-X و یا fix-Y استفاده کنید (افزودن قابلیت X و یا رفع مشکل Y) و در آخر بر روی دکمه‌ی Create branch کلیک کنید.
در اینجا می‌توان مشخص کرد که انشعاب ایجاد شده باید بر اساس کدام انشعاب فعلی نیز تهیه شود (دراپ داون ذیل قسمتی که می‌توان نام انشعاب را وارد کرد). برای مثال پروژه‌‌های مایکروسافت در GitHub، دارای سه شاخه‌ی master، dev و release هستند. شاخه‌ی dev (یا توسعه) جایی است که انشعابات pull requests را قبول خواهند کرد. بنابراین بر اساس ساختار و طراحی پروژه‌ی جاری به این موضوع نیز باید دقت داشت.

پس از ایجاد شاخه‌ی جدید، تصویر ذیل نمایان خواهد شد:


همانطور که ملاحظه می‌کنید، اینبار شاخه‌ی جدید ایجاد شده به صورت bold و ضخیم نمایش داده شده‌است. این bold بودن به معنای شاخه‌ی کاری جاری بودن است. همچنین این شاخه در قسمت unpublished branches قرار دارد. بنابراین کلیه‌ی تغییرات واقع شده‌ی در آن، محلی بوده و هنوز با سرور هماهنگ نشده‌اند.

برای انتشار و publish این شاخه، تنها کافی است تا بر روی آن کلیک راست کرده و گزینه‌ی publish branch را انتخاب کنیم:



این انتشار سبب نمایش لیستی از تغییرات جدید در برگه‌ی branches پروژه، در GitHub خواهد شد:



یک نکته: برای تغییر branch فعال جاری، فقط کافی است در برگه‌ی branches در ویژوال استودیو، دوبار بر روی لینک نام آن شاخه کلیک کنید تا به صورت bold ظاهر شود.


ارسال تغییرات انجام شده‌ی در Branch به سرور

پس از کار بر روی شاخه‌ی جدید ایجاد شده، اکنون نوبت به ارسال و هماهنگ سازی این تغییرات با سرور است. این مورد نیز همانند قبل بوده و ابتدا باید به برگه‌ی Home و گزینه‌ی changes آن مراجعه کرد:



و سپس تغییرات را به همراه توضیحی commit کرد:




اینکار سبب sync محلی می‌شود. سپس بر روی لینک sync کلیک نمائید و تغییرات را با سرور هماهنگ کنید.







یکی کردن تغییرات شاخه‌ی جدید با شاخه‌ی اصلی

هرچند این تغییرات به سرور ارسال شده‌اند، اما چون در یک انشعاب کاری دیگر قرار دارند، با انشعاب اصلی یکی نخواهند شد. برای انجام عملیات merge، ابتدا به برگه‌ی Home و سپس گزینه‌ی branches مراجعه کنید. در ادامه بر روی لینک merge کلیک نمائید (تصاویر اول و دوم بحث).



در اینجا می‌خواهیم اطلاعات موجود در شاخه‌ی افزودن توضیحات را با شاخه‌ی اصلی یکی کنیم (انتخاب منبع و مقصد). سپس بر روی دکمه‌ی merge کلیک نمائید.
اکنون برای ارسال این تغییرات به سرور، به برگه‌ی Home و سپس گزینه‌ی unsynced commits مراجعه کرده و بر روی دکمه‌ی sync کلیک نمائید تا تغییرات یکی شده به سرور ارسال شوند.

مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت پنجم - تعیین نوع هوک useReducer
هوک استاندارد useReducer، یک نمونه‌ی پیچیده‌تر هوک useState، برای مدیریت حالت است؛ با ساختاری شبیه به Redux، اما تنها مخصوص یک کامپوننت. هوک useReducer شبیه به یک نسخه‌ی کوچک و محلی Redux است. در این قسمت، نحوه‌ی تعیین نوع‌های قسمت‌های مختلف آن‌را با TypeScript بررسی می‌کنیم.


ایجاد کامپوننت جدید ReducerButtons

برای توضیح مفاهیم این قسمت، فایل جدید src\components\ReducerButtons.tsx را به همراه محتوای زیر ایجاد می‌کنیم:
import React, { useReducer } from "react";

const initialState = { rValue: true };
type State = {  rValue: boolean; };
type Action = {   type: string; };

function reducer(state: State, action: Action) {
  switch (action.type) {
    case "one":
      return { rValue: true };
    case "two":
      return { rValue: false };
    default:
      return state;
  }
}

export const ReducerButtons = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {state?.rValue && <h1>Visible</h1>}
      <button onClick={() => dispatch({ type: "one" })}>Action One</button>
      <button onClick={() => dispatch({ type: "two" })}>Action Two</button>
    </div>
  );
};
توضیحات:
- این کامپوننت دو دکمه را رندر می‌کند تا توسط آن‌ها بتوان چندین Action مختلف را جهت مدیریت حالت، سبب شد؛ مانند Action One و Two.
- initialState را در این مثال، یک شیء ساده در نظر گرفته‌ایم که تنها دارای یک مقدار boolean است.
- سپس نیاز به یک تابع ویژه را به نام reducer، داریم که مقدار state جاری و یک action را دریافت می‌کند. این تابع بر اساس نوع Action ای که به آن می‌رسد، state جدیدی را بازگشت می‌دهد و در قسمت رندر آن، با بررسی state?.rValue، سبب نمایش عبارتی خواهیم شد.
- در حین تعریف هوک useReducer، قسمت dispatch آن، تابعی است که یک Action را اجرا می‌کند. فراخوانی آن‌را در رویداد onClick دو دکمه‌ی تعریف شده مشاهده می‌کنید. این تابع یک شیء را دریافت می‌کند که type اکشن ارسالی به تابع reducer را مقدار دهی می‌کند.

توسط useReducer برای ایجاد تغییرات در شیء state کامپوننت جاری، از مفهومی به نام dispatch actions استفاده می‌شود. action در اینجا به معنای رخ‌دادن چیزی است؛ مانند کلیک بر روی یک دکمه و یا دریافت اطلاعاتی از یک API. در این حالت مقایسه‌ای بین وضعیت قبلی state و وضعیت فعلی آن صورت می‌گیرد و تغییرات مورد نیاز جهت اعمال به UI، محاسبه خواهند شد. اصلی‌ترین جزء آن، تابعی است به نام Reducer. این تابع، یک تابع خالص است و دو آرگومان را دریافت می‌کند. تابع Reducer، بر اساس action و یا رخ‌دادی، ابتدا کل state برنامه را دریافت می‌کند و سپس خروجی آن بر اساس منطق این تابع، یک state جدید خواهد بود. اکنون که این state جدید را داریم، برنامه‌ی React ما می‌تواند به تغییرات آن گوش فرا داده و بر اساس آن، UI را به روز رسانی کند.

اولین قسمتی را که در حین کار با useReducer توسط TypeScript به آن برخورد خواهیم کرد، نمایش خطاهایی در مورد نوع‌های پارامترهای state و action تابع reducer است. در حالت متداول جاوا اسکریپتی آن، این پارامترها، بدون نوع ذکر می‌شوند که در اینجا به any تفسیر خواهند شد و یک چنین تعاریفی با توجه به strict بودن تنظیمات tsconfig.json برنامه، مجاز نیست. به همین جهت نیاز به تعریف دو type جدید به نام‌های State و Action در اینجا وجود دارد تا خطاهای بدون نوع بودن پارامترهای تابع reducer برطرف شوند. نوع Action به همراه حداقل یک خاصیت به نام action رشته‌ای است و نوع State بر اساس initialState ای که تعریف کردیم، دارای یک خاصیت متناظر boolean است.

نکته‌ی جالب دومی که در اینجا توسط TypeScript گوشزد می‌شود، الزام به ذکر حالت default مربوط به switch ای است که در تابع reducer تعریف کرده‌ایم. اگر این حالت را حذف کنیم، بلافاصله خطای زیر را در قسمت تعریف useReducer نمایش می‌دهد:


عنوان می‌کند که خروجی تابع reducer، یک state جدید است؛ اما ممکن است از نوع never هم باشد که قابلیت ترجمه‌ی به نوع state را ندارد.


بهبود تعاریف نوع‌های useReducer

تا اینجا مهم‌ترین تغییرات تایپ‌اسکریپتی صورت گرفته، تعریف نوع‌های پارامترهای تابع reducer است. اکنون فرض کنید می‌خواهیم، سومین Action را هم به کامپوننت جاری اضافه کنیم:
<button onClick={() => dispatch({ type: "three" })}>Action Three</button>
با این تغییر، برنامه بدون مشکل کامپایل می‌شود، اما ... در عمل کار خاصی را هم انجام نمی‌دهد؛ چون switch تعریف شده‌ی در تابع reducer، یک case مخصوص به آن‌را تعریف نکرده‌است. یا حتی ممکن است در حین تعریف همان دو تابع dispatch، مقدار type را به اشتباه وارد کنیم و با حالت‌های تعریف شده‌ی در تابع reducer تطابقی نداشته باشد. به همین جهت علاقمندیم تا TypeScript بتواند جلوی یک چنین اشتباهاتی را نیز بگیرد؛ برای این منظور می‌توان از union types استفاده کرد:
type Action = {
   type: "one" | "two" | "three";
};
در اینجا تمام اکشن‌های ممکن و مدنظر را لیست کرده‌ایم که سبب خواهد شد تا اگر مقدار نوع اکشنی به درستی وارد نشود، بلافاصله خطایی را دریافت کنیم:


و یا حتی اگر مقدار action.type ای را در تابع reducer به اشتباه وارد کردیم، باز هم برنامه کامپایل نمی‌شود:



تا اینجا موفق شدیم جلوی ورود actionهای پیش بینی نشده را بگیریم. اما اگر در switch تعریف شده، "case "three را هم قید نکنیم، باز هم برنامه کامپایل می‌شود و خطایی گزارش نخواهد شد. برای این منظور فقط کافی است case default را حذف کنیم. چون action.type دیگر نمی‌تواند مقدار دیگری را بجز موارد ذکر شده‌ی در نوع Action داشته باشد. با حذف case default، اینبار ذکر "case "three یا هر کدام از مقادیر سه‌گانه‌ی مجازی که در اینجا تعریف کردیم، الزامی خواهد بود. در غیراینصورت، همان خطای دریافت خروجی never را از تابع reducer دریافت می‌کنیم که با ذکر هر سه case مجاز، برطرف می‌شود.
با این تغییرات، کدهای نهایی این قسمت به صورت زیر در خواهند آمد:
import React, { useReducer } from "react";

const initialState = { rValue: true };

type State = {
  rValue: boolean;
};

type Action = {
  type: "one" | "two" | "three";
};

function reducer(state: State, action: Action) {
  switch (action.type) {
    case "one":
      return { rValue: true };
    case "two":
      return { rValue: false };
    case "three":
      return { rValue: false };
  }
}

export const ReducerButtons = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {state?.rValue && <h1>Visible</h1>}
      <button onClick={() => dispatch({ type: "one" })}>Action One</button>
      <button onClick={() => dispatch({ type: "two" })}>Action Two</button>
      <button onClick={() => dispatch({ type: "three" })}>Action Three</button>
    </div>
  );
};
مطالب
آشنایی و بررسی ابزار Glimpse
در مطلب MiniProfiler ابزار مانیتور کارآیی وب سایت‌ها را بررسی کردیم. اما ابزار Glimpse هم جزو ابزار‌های حرفه‌ای است که در مطلبی آقای هانسلمن در سایت خود به آن پرداخته بودند. اما دیدم جای یک مطلب فارسی در این رابطه خالی است.


Glimpse چیست؟
glimpse یک ابزار حرفه‌ای برای نمایش زمان اجرای کدها، پیکربندی سرور، درخواست‌های وب، اشکال زدایی و بررسی کارآیی وب سایت‌های MVC و Web Forms می‌باشد. البنه بدون آنکه در کد‌های پروژه شما تغییری ایجاد نماید.
ابتدا در پنجره Nuget عبارت glimpse را جستجو و آن را نصب نمایید:


کتابخانه‌های زیادی برای این ابزار آماده شده‌اند:

  • کتابخانه Glimpse Core
    که هسته اصلی ابزار است، حتما باید نصب شود.
  • کتابخانه Glimpse ASP.NET
    برای بررسی وب سایت‌های نوشته شده با ASP.NET Web Forms استفاده می‌شود. البته بری Mvc هم لازم است.
  • کتابخانه Glimpse Mvc2, Glimpse Mvc3، Glimpse Mvc4
    برای بررسی وب سایت‌های نوشته شده با ASP.NET Mvc
  • کتابخانه Glimpse Ado
    برای بررسی و نمایش زمان کوئری بر روی پایگاه داده
  • کتاخانه Glimpse EF4.3، Glimpse EF5، Glimpse EF6
    برای زمانیکه از نگارش‌های مختلف Entity Framework استفاده می‌نماییم
پس از نصب کتابخانه‌های مورد نیاز، پروژه را rebuild و سپس اجرا نمایید. برای فعال کردن glimpse آدرس http://{your-site}/Glimpse.axd را اجرا کنید تا صفحه تنظیمات آن فعال شوند و سپس بر روی گزینه Turn Glimpse on، کلیک کنید. همچنین با گزینه Turn Glimpse off می‌توانید آن را غیر فعال نمایید.

علاوه بر این، تنظیمات استاندارد این ابزار قابل تغییر است.
به صفحه اصلی سایت برگشته و صفحه را بروز رسانی کنید. ابزار glimpse در پایین مرورگر نمایش داده می‌شود.


این ابزار شامل سه قسمت است:
  • HTTP
    اطلاعات Request و زمان پاسخ و اطلاعات سرور نمایش داده می‌شود
  • HOST
    اطلاعات صفحه اجرا شده، زمان پاسخ و تعداد کوئری‌های اجرا شده و زمان آن نمایش داده می‌شوند
  • AJAX
    اطلاعات درخواست‌های اجکسی این صفحه و تعداد آن نمایش داده می‌شوند
بر روی هر یک از این قسمت‌ها با حرکت ماوس، جزئیات آن قسمت نمایش داده می‌شود.

اگر بر روی آیکون g ابزار کلیک کنید، همچون developer tools مرورگر‌ها باز شده و دارای زبانه‌های متعددی می‌باشد. مثلا اگر پلاگین ado و ef5 نصب باشند، در زبانه SQL می‌توانید کوئری‌های اجرا شده و زمان مصرف شده آن‌ها را مشاهده نمایید

زبانه دیگر Timeline است که زمان انقیاد اشیاء و رویدادها را بصورت گرافیکی نمایش می‌دهد.

در مطلب بعدی به جزئیات بیشتری از این ابزار می‌پردازم.
مطالب
استفاده از Razor در فایل‌های JavaScript و CSS
یکی از مشکلات سینتکس Razor سمت سرور، این است که در فایل‌های JavaScript و CSS سمت کاربر نمی‌توانیم از آن استفاده کنیم، به عنوان مثال فرض کنید در یک فایل JavaScript نیاز به مشخص سازی آدرس یک اکشن متد دارید؛ مثلاً انجام یک عملیات ای‌جکسی. در این حالت به عنوان یک Best Practice بهتر است از Url.Action استفاده کنید. اما همانطور که عنوان شد، این امکان یعنی استفاده از سینتکس Razor در فایل‌های JS و CSS مهیا نیست.
ساده‌ترین راه‌حل، تولید ویوهای سمت سرور JavaScript ایی است. برای اینکار تنها کاری که باید انجام دهیم، تغییر مقدار Content-Type صفحه به مقدار موردنظر می‌باشد؛ مثلاً text/javascript برای فایل‌های JS و text/css برای فایل‌های CSS. به عنوان مثال برای فایل‌های CSS به این صورت عمل خواهیم کرد:
public ActionResult Style()
{
            Response.ContentType = "text/css";
            var model = new Style
            {
                Color = "red",
                Background = "blue"
            };
            return View(model);
}
برای ویوی آن نیز خواهیم داشت:
@model ExternalJavaScript.Models.Style
@{
    Layout = null;
}
body {
    color : @Model.Color;
    background-color : @Model.Background;
}
در نهایت ویوی فوق را به عنوان فایل CSS در فایل Layout استفاده خواهیم کرد:
<link rel="stylesheet" href="@Url.Action("Style","Home")" />
برای حالت فوق می‌توانیم یک اکشن فیلتر به صورت زیر تهیه کنیم:
public class ContentType : ActionFilterAttribute
    {
        private string _contentType;
        public ContentType(string ct)
        {
            this._contentType = ct;
        }

        public override void OnActionExecuted(ActionExecutedContext context) { /* nada */ }
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            context.HttpContext.Response.ContentType = this._contentType;
        }
    }
و برای استفاده از آن خواهیم داشت:
[ContentType("text/css")]
public ActionResult Style()
 {
     var model = new Style
      {
                Color = "red",
                Background = "blue"
      };
     return View(model);
}

برای فایل‌های JS نیز می‌توانیم از یک View به عنوان محل قرارگیری کدهای جاوا اسکریپت استفاده کنیم:
public class JavaScriptSettingsController : Controller
{
        public ActionResult Index()
        {
            return PartialView();
        }
}
در این حالت در داخل فایل Index.cshtml کدهای جاوا اسکریپت را همراه با سینتکس Razor می‌توانیم بنویسیم:
$(function(){
    $.post('@Url.Action("GetData", "Home")', function (data) {
        $('.notificationList').html(data);
            if ($(data).filter("li").length != 0) {
                $('#notificationCounter').html($(data).filter("li").length);
            }
    });
});
سپس در داخل فایل Layout.cshtml_ می‌توانیم به ویوی فوق ارجاعی داشته باشیم:
<script src="/JavaScriptSettings"></script>
این روش به خوبی برای ویوهای JS و CSS کار خواهد کرد؛ اما از آنجائیکه ویوی ما توسط ویژوال استودیو به عنوان یک فایل JS و یا CSS معتبر شناخته نمی‌شود، Intellisense برای آن مهیا نیست. برای فعال سازی Intellisense و همچنین معتبر شناخته شدن ویوی فوق، بهترین راه‌حل قرار دادن کدهای JS درون بلاک script است (برای فایل‌های CSS نیز همینطور):
<script>
    $(function () {
        $.post('@Url.Action("Index", "Home")', function (data) {
            $('.notificationList').html(data);
            if ($(data).filter("li").length != 0) {
                $('#notificationCounter').html($(data).filter("li").length);
            }
        });
    });
</script>
اما با اجرای برنامه، در کنسول مرورگر بلافاصله خطای Uncaught SyntaxError: Unexpected token < را دریافت خواهید کرد. در این حالت به روشی نیاز داریم که در زمان اجرا بلاک script را حذف نمائید. بنابراین از یک اکشن فیلتر سفارشی برای اینکار استفاده خواهیم کرد. کار این اکشن فیلتر، تغییر مقدار Content-Type و همچنین حذف بلاک مورد نظر می‌باشد:
public class ExternalFileAttribute : ActionFilterAttribute
    {
        private readonly string _contentType;
        private readonly string _tag;
        public ExternalFileAttribute(string ct, string tag)
        {
            this._contentType = ct;
            _tag = tag;
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var response = filterContext.HttpContext.Response;
            response.Filter = new StripEnclosingTagsFilter(response.Filter, _tag);
            response.ContentType = _contentType;
        }

        private class StripEnclosingTagsFilter : MemoryStream
        {
            private static Regex _leadingOpeningScriptTag;
            private static Regex _trailingClosingScriptTag;

            //private static string Tag;

            private readonly StringBuilder _output;
            private readonly Stream _responseStream;

            /*static StripEnclosingTagsFilter()
            {
                LeadingOpeningScriptTag = new Regex(string.Format(@"^\s*<{0}[^>]*>", Tag), RegexOptions.Compiled);
                TrailingClosingScriptTag = new Regex(string.Format(@"</{0}>\s*$", Tag), RegexOptions.Compiled);
            }*/

            public StripEnclosingTagsFilter(Stream responseStream, string tag)
            {
                _leadingOpeningScriptTag = new Regex(string.Format(@"^\s*<{0}[^>]*>", tag), RegexOptions.Compiled);
                _trailingClosingScriptTag = new Regex(string.Format(@"</{0}>\s*$", tag), RegexOptions.Compiled);

                _responseStream = responseStream;
                _output = new StringBuilder();
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                string response = GetStringResponse(buffer, offset, count);
                _output.Append(response);
            }

            public override void Flush()
            {
                string response = _output.ToString();

                if (_leadingOpeningScriptTag.IsMatch(response) && _trailingClosingScriptTag.IsMatch(response))
                {
                    response = _leadingOpeningScriptTag.Replace(response, string.Empty);
                    response = _trailingClosingScriptTag.Replace(response, string.Empty);
                }

                WriteStringResponse(response);
                _output.Clear();
            }

            private static string GetStringResponse(byte[] buffer, int offset, int count)
            {
                byte[] responseData = new byte[count];
                Buffer.BlockCopy(buffer, offset, responseData, 0, count);

                return Encoding.Default.GetString(responseData);
            }

            private void WriteStringResponse(string response)
            {
                byte[] outdata = Encoding.Default.GetBytes(response);
                _responseStream.Write(outdata, 0, outdata.GetLength(0));
            }
        }
    }
 در نهایت می‌توانیم اکشن‌متد موردنظرمان را با فیلتر سفارشی مزین کنیم:
[ExternalFile("text/javascript", "script")]
public ActionResult Index()
{
      return PartialView();
}
برای تولید ویوهای CSS نیز کافی است مقادیر فیلتر را تغییر دهیم:
[ExternalFile("text/css", "style")]
public ActionResult Style()
{
            var model = new Style
            {
                Color = "red",
                Background = "blue"
            };
            return View(model);
}
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت دهم - کار با فرم‌ها - قسمت اول
هر برنامه‌ی وبی، نیاز به کار با فرم‌های وب را دارد و به همین جهت، AngularJS 2.0 به همراه دو نوع از فرم‌ها است: فرم‌های مبتنی بر قالب‌ها و فرم‌های مبتنی بر مدل‌ها.
کار با فرم‌های مبتنی بر قالب‌ها ساده‌تر است؛ اما کنترل کمتری را بر روی مباحث اعتبارسنجی داده‌های ورودی توسط کاربر، در اختیار ما قرار می‌دهند. اما فرم‌های مبتنی بر مدل‌ها هر چند به همراه اندکی کدنویسی بیشتر هستند، اما کنترل کاملی را جهت اعتبارسنجی ورودی‌های کاربران، ارائه می‌دهند. در این قسمت فرم‌های مبتنی بر قالب‌ها (Template-driven forms) را بررسی می‌کنیم.


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

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

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

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

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

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

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

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


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

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


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

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

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

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



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

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

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


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



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

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

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

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


بررسی ngForm

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

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



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

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



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

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



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

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

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

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

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

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



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


خلاصه‌ی بحث

فرم‌های template driven در AngularJS 2.0 به این نحو طراحی می‌شوند:
 1) ابتدا فرم HTML را به حالت معمولی آن طراحی می‌کنیم؛ با تمام المان‌های آن.
 2) به تمام المان‌های فرم، دیراکتیو ngControl را متصل می‌کنیم، تا AngularJS 2.0 آن‌را تبدیل به یک کنترل خاص خودش کند. کنترلی که دارای خواصی مانند valid و touched است.
 3) سپس برای دسترسی به این کنترل ایجاد شده‌ی به صورت ضمنی، یک متغیر محلی آغاز شده‌ی با # را به تمام المان‌ها اضافه می‌کنیم.
 4) اعتبارسنجی‌هایی را مانند required  به المان‌های فرم اضافه می‌کنیم.
 5) از متغیر محلی تعریف شده و ngIf* برای بررسی خواصی مانند valid و touched برای نمایش خطاهای اعتبارسنجی کمک گرفته می‌شود.
 6) پس از تعریف فرم، تعریف ngControlها، تعریف متغیر محلی شروع شده‌ی با # و افزودن خطاهای اعتبارسنجی، اکنون نوبت به ارسال این اطلاعات به سرور است. بنابراین رخداد ngSubmit را باید به متدی در کلاس کامپوننت جاری متصل کرد.
 7) اکنون که با کلیک بر روی دکمه‌ی submit فرم، متد onSubmit متصل به ngSubmit فراخوانی می‌شود، نیاز است بین المان‌های فرم HTML و کلاس کامپوننت، ارتباط برقرار کرد. این‌کار را توسط two-way data binding و تعریف ngModel بر روی تمام المان‌های فرم، انجام می‌دهیم. این ngModelها، به یک خاصیت عمومی که متناظر است با وهله‌ای از شیء مدل فرم، متصل هستند. بنابراین این مدل، در هر لحظه، بیانگر آخرین تغییرات کاربر است و از آن می‌توان برای ارسال اطلاعات به سرور استفاده کرد.
 8) پس از اتصال فرم به کلاس متناظر با آن، اکنون سرویس محصولات را تکمیل کرده و به آن متد HTTP Post را جهت ارسال اطلاعات سمت کاربر، به سرور، اضافه می‌کنیم. در اینجا نکته‌ی مهم، تنظیم content type ارسالی به سمت سرور است. در غیراینصورت فریم ورک سمت سرور قادر به تشخیص JSON بودن این اطلاعات نخواهد شد.