مطالب دوره‌ها
تزریق وابستگی‌ها و سناریوهای بسیار متعدد موجود
تعدادی از پرکاربردترین حالت‌های تزریق وابستگی‌ها را در دوره جاری بررسی کردیم. برای مثال چگونه می‌توان تزریق وابستگی‌های یک کنترلر ASP.NET MVC را خودکار کرد، یا در وب فرم‌ها وضعیت چگونه است. اما در حین حل مسایلی از این دست، به سناریوهای بسیار متعددی برخواهید خورد. برای مثال تزریق وابستگی‌های خودکار در یک کنترلر را آموختیم؛ در مورد فیلترها و Action Resultهای سفارشی چطور باید رفتار کرد؟ در WCF چطور؟ در هندلرهای وب فرم‌ها چطور؟ و بسیاری از حالات دیگر. البته تمام این موارد را توسط الگوی Service locator که شامل استفاده مستقیم از امکانات وهله سازی یک IoC Container، در کلاس مدنظر است، می‌توان حل کرد؛ اما باید تا حد امکان از این روش با توجه به اینکه خود IoC Container را تبدیل به یک وابستگی مدفون شده در کلاس‌های ما می‌کند، پرهیز نمود.
اگر به دنبال کتابخانه‌ای هستید که بسیاری از این سناریوها را پیاده سازی کرده است، کتابخانه AutoFac پیشنهاد می‌شود. حتی اگر علاقمند به استفاده از آن نباشید، می‌توان از نحوه پیاده سازی‌های مختلف آن در مورد حالت‌های مختلف خودکار سازی تزریق وابستگی‌ها، ایده گرفت و سپس این کدها را با IoC Container مورد علاقه خود پیاده سازی کرد.

صفحه خانگی AutoFac
http://code.google.com/p/autofac
http://autofac.org

بسته نیوگت
http://www.nuget.org/packages/Autofac

محلی برای ایده گرفتن مثلا در مورد فیلترهای ASP.NET MVC
و در مورد نحوه استفاده از آن‌ها، نیاز است آزمون‌های واحد این پروژه را بررسی کنید و یا مستندات پروژه را مطالعه کنید.
همچنین بررسی لیست مستندات کلی آن نیز بسیار مفید است

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


پ.ن.
سایت bitbucket امکان import کامل مخازن کد Google code را نیز دارد (در صورتیکه دسترسی شما به گوگل کد محدود است).
مطالب
بازسازی کد: جابجایی متد (Move method)
معمولا زمانیکه متدی از امکانات کلاس دیگری غیر از کلاسی که در آن تعریف شده است استفاده می‌کند، نیاز به چنین بازسازی کدی داریم. روش کلی این بازسازی کد، انتقال متد به کلاسی است که بیشترین تعلق را به آن دارد! 
جابجایی متد یکی از موارد پر تکرار و مهم در امر بازسازی کد است. این بازسازی در مراحل انجام دیگر بازسازی‌های کد، مانند شکستن کلاس نیز استفاده می‌شود. با این روش ساده می‌توان کلاس‌هایی با مسئولیت‌های محدود و مشخص را توسعه داد.  

مراحل انجام این بازسازی کد  

  1. تمامی امکانات کلاس مبدا را که متد مورد نظر از آنها استفاده می‌کند، بررسی نمایید که آیا آنها نیز نیاز به انتقال دارند یا خیر. 
  2. کلاس‌های پدر و فرزند کلاس مبدا را برای یافتن تعاریف دیگری از متد مورد نظر بررسی نمایید. اگر تعاریف دیگری وجود داشتند به راحتی نمی‌توان متد را جابجا کرد. در این صورت اگر قصد جابجایی داشتید، باید به فکر جابجایی رابطه چند ریختی موجود نیز باشید. 
  3. متد را در کلاس مقصد ایجاد نمایید. 
  4. بدنه متد را به متد مقصد منتقل نمایید و تمامی امکانات استفاده شده در آن را طوری تغییر دهید که در کلاس جدید کار کند. اگر متد، نیاز به اشاره‌ای به کلاس مبدا داشت، باید تعیین نمایید که به چه صورت این اشاره انجام شود. اگر مکانیزم مدیریت خطایی (exception handling) در متد مبدا پیاده سازی شده بود، تعیین کنید که آیا متد مبدا نیز کماکان امر مدیریت خطا را انجام خواهد داد، یا به متد مقصد انتقال خواهد یافت. 
  5. کد کلاس مقصد را کامپایل و تست نمایید. 
  6. اگر متد مبدا را به عنوان فراخوان متد مقصد نگه داشتید، باید تصمیم بگیرید که کلاس مقصد در آن متد به چه صورت استفاده خواهد شد. 
  7. فراخوانی متد مقصد را به بدنه متد مبدا اضافه کنید. 
  8. کد را کامپایل و تست نمایید.
  9. در مورد سرنوشت متد مبدا تصمیم گیری نمایید که آیا نیازی به وجود آن هست یا خیر. در صورتیکه از متد مبدا در مکان‌های زیادی استفاده شده یا متد در کتابخانه یا فریم ورکی است که کنترلی بر روی استفاده کنندگان آن وجود ندارد، احتمالا باقی ماندن متد به عنوان صرفا فراخوان، ایده خوبی باشد. 
  10. اگر متد مبدا را حذف کردید تمامی استفاده از آن را باید به متد مقصد تغییر دهید. توجه داشته باشید ممکن است سناریو ساختن کلاس جدید با کلاس قدیمی متفاوت باشد. 
  11. مجددا کد را کامپایل و تست نمایید.  

مثال: فرض کنید نرم افزاری برای مدیریت رویدادها و شرکت کنندگان آن‌ها تهیه کرده‌ایم. در این نرم افزار، کلاسی با نام Event وجود دارد و کلاسی نیز با نام Person که نام آنها کاملا نمایانگر استفاده آنها است.
بخشی از بدنه این کلاس‌ها به صورت زیر است:  
public class Event 
{ 
    public List<Person> Participants { get; internal set; } 
} 
public class Person 
{ 
    public int Id { get; private set; } 
    public void Participate(Event ev) 
    { 
        var isParticipatedAlready = ev.Participants.Any(ff => ff.Id == Id); 
        if (isParticipatedAlready) 
            return; 
        ev.Participants.Add(this); 
    } 
}
در کد مربوط به کلاس Person، شاهد متدی هستیم که عمل ثبت‌نام فرد را در یک رویداد انجام می‌دهد. اما با دقت به این متد مشاهده می‌کنیم که بدنه این متد بیشتر از اعضای کلاس Event استفاده می‌کند. حتی این استفاده باعث شده است که خصوصیت Participants از کلاس Event به صورت public تعریف شود که خود مشکل دیگری در این طراحی است.
در چنین شرایطی، بازسازی کد جابجایی متد می‌تواند در راستای انتقال مسئولیت‌های مناسب هر کلاس به بدنه آن و بهبود طراحی کمک کند. بعد از بازسازی کد شاهد چنین طراحی‌ای هستیم:  
public class Event 
{ 
    protected List<Person> Participants { get; set; } 
    public void Participate(Person person) 
    { 
        var isParticipatedAlready = Participants.Any(ff => ff.Id == person.Id); 
        if (isParticipatedAlready) 
            return; 
        Participants.Add(person); 
    } 
} 
public class Person 
{ 
    public int Id { get; private set; } 
}
بازسازی‌ای که انجام شد، دو تاثیر را بر روی طراحی این کلاس‌ها داشته است:
   اول: جایگذاری بهتر و منطقی‌تر مسئولیت‌های یک کلاس 
   دوم: کپسوله سازی آسان‌تر کلاس ها 

بازسازی کد جابجایی متد، سنگ بنای بیشتر بازسازی‌های مورد نیاز در فعالیت‌های روزمره تولید یا نگهداری نرم افزار است. علارغم این که این بازسازی ساده به نظر می‌رسد، در مجموعه کدهای پیچیده، انجام این بازسازی ممکن است امری طاقت فرسا شود.  
مطالب
روش نامگذاری Smurf ایی!
اگر به یک سری از کتابخانه‌ها دقت کنید، تمام کلاس‌های آن‌ها دارای یک پیشوند تکراری هستند؛ مثلا SmurfXMLDataRow، SmurfXMLElement و الی آخر در مورد تمام کلاس‌های موجود در پروژه. به این رویه «Smurf Naming Convention» گفته می‌شود!
در این نوع کتابخانه‌ها زمانیکه کاربری بر روی دکمه‌ای کلیک می‌کند، SmurfAccountView اطلاعات SmurfAccountDTO را به SmurfAccountController منتقل می‌کند. در ادامه از خاصیت SmurfID دریافتی، مقدار SmurfOrderHistory دریافت شده و به SmurfHistoryReportingView جهت نمایش ارسال خواهد شد. اگر استثنای SmurfErrorEvent رخ دهد، توسط SmurfErrorLogger در فایلی به نام log/smurf/smurflog.log ثبت خواهد شد.

کلمه Smurf هم از شخصیتی کارتونی به همین نام اخذ شده است که در زبان مخصوص آن‌ها اکثر افعال و نام‌ها از کلمه Smurf مشتق می‌شود! برای مثال در مورد ماهیگیری کردن در یک رودخانه عنوان می‌کنند «We're going smurfing on the River Smurf today».


خوب، چکار باید کرد؟ روش صحیح معرفی نام یک شرکت در حین طراحی و نامگذاری کلاس‌های یک کتابخانه چیست؟
در مطلب بسیار جامع و عالی «اصول و قراردادهای نام‌گذاری در دات‌نت» عنوان شده است که اساس نام‌گذاری فضاهای نام باید از قاعده زیر پیروی کند:
<Company>.<Technology|Produt|Project>[.<Feature>][.<SubNamespace>]
مثلا مایکروسافت یکبار فضای نام Microsoft.Reporting.WebForms را تعریف کرده است و ... همین! دیگر به ابتدای هر کلاسی در این کتابخانه، پیشوند Microsoft یا MS و امثال آن اضافه نشده است تا بر روی اعصاب و روان استفاده کننده تاثیر منفی داشته باشد.

 
مطالب دوره‌ها
استفاده از Factories برای حذف Service locators در برنامه‌های WinForms
یک برنامه‌ی WinForms را درنظر بگیرید که از دو فرم تشکیل شده است.
فرم اول کار نمایش فرم 2 را به عهده دارد.
فرم دوم کار ارسال ایمیل را انجام می‌دهد. این ایمیل نیز از طریق سرویس ذیل فراهم می‌شود:
namespace WinFormsIoc.Services
{
    public interface IEmailsService
    {
        void SendEmail(string from, string to, string title, string message);
    }
}

namespace WinFormsIoc.Services
{
    public class EmailsService : IEmailsService
    {
        public void SendEmail(string from, string to, string title, string message)
        {
            //todo: ...
        }
    }
}
پیاده سازی متد SendEmail در اینجا مدنظر نیست. نکته‌ی مهم، مدیریت تامین و تزریق وابستگی‌های تعریف شده در سازنده‌ی آن است:
    public partial class Form2 : Form
    {
        private readonly IEmailsService _emailsService;
        public Form2(IEmailsService emailsService)
        {
            _emailsService = emailsService;
            InitializeComponent();
        }
احتمالا شاید عنوان کنید که در فرم اول، زمانیکه نیاز است فرم دوم نمایش داده شود، می‌نویسیم new Form2 و در پارامتر آن با استفاده از متد ObjectFactory.GetInstance سازنده‌ی آن‌را فراهم می‌کنیم:
 var form2 = new Form2(ObjectFactory.GetInstance<IEmailsService>());
form2.Show();
و یا اگر مدتی با IoC Containers کار کرده باشید، شاید پیشنهاد دهید که فقط بنویسید:
 var form2 = ObjectFactory.GetInstance<Form2>();
form2.Show();
و همین! به صورت خودکار اگر n پارامتر تزریق شده هم در سازنده‌ی فرم دوم وجود داشته باشند، بر اساس تنظیمات اولیه‌ی IoC Container مورد استفاده، نمونه سازی شده و برنامه بدون مشکل کار خواهد کرد.

مشکل! این دو راه حل هیچکدام به عنوان تزریق وابستگی‌ها شناخته نمی‌شوند و به الگوی Service locator معروف هستند. مشکل آن‌ها این است که کدهای ما در حال حاضر وابستگی مستقیمی به IoC container مورد استفاده پیدا کرده‌اند. در حالت اول ما خودمان دستی درخواست داده‌ایم که کدام وابستگی باید وهله سازی شود و در حالت دوم همانند حالت اول، کدهای ObjectFactory.GetInstance، مختص به یک IoC Container خاص است. نحوه‌ی صحیح کار با IoC Container‌ها باید به این نحو باشد که یکبار در آغاز برنامه تنظیم شوند و در ادامه سایر کلاس‌های برنامه طوری کار کنند که انگار IoC Container ایی وجود خارجی ندارد.


راه حل: ObjectFactory.GetInstance را کپسوله کنید.

using System.Windows.Forms;

namespace WinFormsIoc.IoC
{
    public interface IFormFactory
    {
        T Create<T>() where T : Form;
    }
}

using System.Windows.Forms;
using StructureMap;

namespace WinFormsIoc.IoC
{
    public class FormFactory : IFormFactory
    {
        public T Create<T>() where T : Form
        {
            return ObjectFactory.GetInstance<T>();
        }
    }
}
در اینجا یک اینترفیس را تعریف کرده‌ایم که متد ایجاد وهله‌ا‌ی از یک فرم را ارائه می‌دهد. پیاده سازی آن در برنامه‌‌ای که از StructureMap استفاده می‌کند، مطابق کلاس FormFactory است. اگر IoC Container دیگری باشد، فقط باید این پیاده سازی را تغییر دهید و نه کل برنامه را. اکنون برای استفاده از آن، IFormFactory را در سازنده‌ی کلاسی که نیاز دارد فرم‌های دیگر را نمایش دهد، تزریق می‌کنیم:
using System;
using System.Windows.Forms;
using WinFormsIoc.IoC;

namespace WinFormsIoc
{
    public partial class Form1 : Form
    {
        private readonly IFormFactory _formFactory;
        public Form1(IFormFactory formFactory)
        {
            _formFactory = formFactory;
            InitializeComponent();
        }

        private void btnShowForm2_Click(object sender, EventArgs e)
        {
            var form2 = _formFactory.Create<Form2>();
            form2.Show();
        }
    }
}
در کدهای فوق، فرم اول برنامه را ملاحظه می‌کنید که قرار است فرم دوم را نمایش دهد. IFormFactory در سازنده‌ی آن تزریق شده‌است. با فراخوانی متد Create آن، فرم دوم برنامه به همراه تمام وابستگی‌های تزریق شده‌ی در سازنده‌ی آن وهله سازی می‌شوند.
نکته‌ی مهم این کدها عدم وابستگی مستقیم آن به هیچ نوع IoC Container خاصی است. این فرم اصلا نمی‌داند که IoC Container ایی در برنامه وجود دارد یا خیر.


مشکل! با تغییر سازنده‌ی Form1 برنامه دیگر کامپایل نمی‌شود!
اگر فایل Program.cs را باز کنید، یک چنین سطری را دارد:
 Application.Run(new Form1());
چون سازنده‌ی فرم یک، اکنون پارامتر جدیدی پیدا کرده‌است، در اینجا می‌توان ObjectFactory.GetInstance را مستقیما بکار برد (در این حالت خاص که مرتبط است به کلاس آغازین برنامه، با توجه به اینکه وهله سازی آن مستقیما و خارج از کنترل ما انجام می‌شود، دیگر چاره‌ای نداریم و مجبور هستیم از الگوی Service locator استفاده کنیم).
 Application.Run(ObjectFactory.GetInstance<Form1>());

مثال کامل این بحث را از اینجا می‌توانید دریافت کنید
WinFormsIoc.zip
مطالب دوره‌ها
مراحل Refactoring یک قطعه کد برای اعمال تزریق وابستگی‌ها
برای رسیدن به الگوی معکوس سازی وابستگی‌ها عموما مراحل زیر طی می‌شوند:

الف) در متدهای لایه جاری خود واژه‌های کلیدی new و همچنین کلیه فراخوانی‌های استاتیک را بیابید.
ب) وهله سازی این‌ها را به یک سطح بالاتر (نقطه آغازین برنامه) منتقل کنید. اینکار باید بر اساس اتکای به Abstraction و برای مثال استفاده از اینترفیس‌ها صورت گیرد.
ج) اینکار را آنقدر تکرار کنید تا دیگر در کدهای لایه جاری خود واژه کلیدی new یا فراخوانی متدهای استاتیک مشاهده نشود.
د) در آخر وهله سازی object graphهای مورد نیاز را به یک IoC Container محول کنید.


یک مثال: ابتدا بررسی یک قطعه کد متداول

using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Mvc;

namespace DI06.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            string result = string.Empty;
            using (var client = new WebClient { Encoding = Encoding.UTF8 })
            {
                result = client.DownloadString("https://www.dntips.ir/");
            }
            var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(result);
            var title = match.Groups[1].Value.Trim();

            ViewBag.PageTitle = title;
            return View();
        }
    }
}
فرض کنید یک برنامه ASP.NET MVC را به نحو فوق تهیه کرده‌ایم. در کدهای کنترلر آن قصد داریم محتویات Html یک صفحه خاص را دریافت و سپس عنوان آن‌را استخراج کرده و نمایش دهیم.
مشکلات کد فوق:
الف) قرار گرفتن منطق تجاری پیاده سازی کدها مستقیما داخل کدهای یک اکشن متد؛ این مساله در دراز مدت به تکرار شدید کدها منجر خواهد شد که نهایتا قابلیت نگهداری آن‌را کاهش می‌دهند.
ب) در این کد حداقل دو بار واژه کلیدی new ذکر شده است. مورد اول یا new WebClient، از همه مهم‌تر است؛ از این جهت که نوشتن آزمون واحد را برای این کنترلر بسیار مشکل می‌کند. آزمون‌های واحد باید سریع و بدون نیاز به منابع خارجی، قابل اجرا باشند. تعویض آن هم مطابق کدهای تدارک دیده شده کار ساده‌ای نیست.


بهبود کیفیت قطعه کد متداول فوق با استفاده از الگوی معکوس سازی وابستگی‌ها

در اصل معکوس سازی وابستگی‌ها عنوان کردیم لایه بالایی سیستم نباید مستقیما به لایه‌های زیرین در حال استفاده از آن، وابسته باشد. این وابستگی باید معکوس شده و همچنین بر اساس Abstraction یا برای مثال استفاده از اینترفیس‌ها صورت گیرد.
به همین منظور یک پروژه دیگر را از نوع Class library، مثلا به نام DI06.Services به Solution جاری اضافه می‌کنیم.
namespace DI06.Services
{
    public interface IWebClientServices
    {
        string FetchUrl(string url);
        string GetWebPageTitle(string url);
    }
}

using System.Net;
using System.Text;
using System.Text.RegularExpressions;

namespace DI06.Services
{
    public class WebClientServices : IWebClientServices
    {
        public string FetchUrl(string url)
        {
            using (var client = new WebClient { Encoding = Encoding.UTF8 })
            {
                return client.DownloadString(url);
            }
        }

        public string GetWebPageTitle(string url)
        {
            var html = FetchUrl(url);
            var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(html);
            return match.Groups[1].Value.Trim();
        }
    }
}
در این لایه، سرویس WebClient ایی را تدارک دیده‌ایم. این سرویس می‌تواند محتوای Html یک آدرس را برگرداند و یا عنوان آن آدرس خاص را استخراج نماید.
هنوز کار معکوس سازی وابستگی‌ها رخ نداده است. صرفا اندکی تمیزکاری و انتقال پیاده سازی منطق تجاری به یک سری کلاس‌هایی با قابلیت استفاده مجدد صورت گرفته است. به این ترتیب اگر باگی در این کدها وجود داشته باشد و همچنین از آن در چندین نقطه برنامه استفاده شده باشد، اصلاح این کلاس مرکزی، به یکباره تمامی قسمت‌های مختلف برنامه را تحت تاثیر مثبت قرار داده و از تکرار کدها و فراموشی احتمالی بهبود قسمت‌های مشابه جلوگیری می‌کند.
کار معکوس سازی وابستگی‌ها در یک لایه بالاتر صورت خواهد گرفت:
using System.Web.Mvc;
using DI06.Services;

namespace DI06.Controllers
{
    public class HomeController : Controller
    {
        readonly IWebClientServices _webClientServices;
        public HomeController(IWebClientServices webClientServices)
        {
            _webClientServices = webClientServices;
        }

        public ActionResult Index()
        {
            ViewBag.PageTitle = _webClientServices.GetWebPageTitle("https://www.dntips.ir/");
            return View();
        }
    }
}
اینبار کنترلر Home را توسط تزریق وابستگی‌ها در سازنده کنترلر، بازنویسی کرده‌ایم. کد نهایی بسیار تمیزتر از حالت قبل است. دیگر پیاده سازی متد GetWebPageTitle مستقیما داخل یک اکشن متد قرار نگرفته است. همچنین این کنترلر اصلا نمی‌داند که قرار است از کدام پیاده سازی اینترفیس IWebClientServices استفاده کند. اگر در تنظیمات اولیه IoC Container مورد استفاده، کلاس WebClientServices ذکر شده باشد، از آن استفاده خواهد کرد؛ یا اگر حتی کلاس WebClientServicesFake نیز معرفی گردد (جهت انجام آزمون غیر وابسته به new WebClient)، باز هم بدون کوچکترین تغییری در کدهای آن قابل استفاده خواهد بود.

در مورد نحوه تنظیمات اولیه یک IoC Container و یا پیشنیازهای ASP.NET MVC جهت آماده شدن برای تزریق خودکار وابستگی‌ها در سازنده کنترلرها، پیشتر مطالبی را در این سری مطالعه کرده‌اید؛ در اینجا نیز اصول مورد استفاده یکی است و تفاوتی نمی‌کند.
مطالب
API Versioning
فرض کنید امروز یک API را برای استفاده عموم ارائه میدهید. آیا با یک breaking change در منابع شما که باعث تغییر در داده‌های ورودی یا خروجی API شود، باید استفاده کنندگان این API در سیستمی که از آن استفاده کرده‌اند، تغییراتی را اعمال کنند یا خیر؟ جواب خیر می‌باشد؛ اصلی‌ترین استفاده از API Versioning دقیقا برای این منظور است که بدون نگرانی از توسعه‌های بعدی، از ورژن‌های قدیمی API بتوانیم استفاده کنیم. 
در این مقاله با روش‌های مختلف ورژن بندی API آشنا خواهید شد.
سه روش اصلی زیر را میتوان برای این منظور در نظر گرفت:
  1.  URI-based versioning 
  2.  Header-based versioning 
  3.  Media type-based versioning 

پیاده سازی URI-based versioning
حداقل به 3 طریق میتوان این مکانیسم را پیاده کرد:
راه حل اول: اگر از Attribute Routing استفاده نمی‌کنید، با تغییر جزئی در تعاریف مسیریابی خود میتوانید به نتیجه مورد نظر برسید. برای ادامه کار دو ویوومدل و دو کنترلر را که هر کدام مربوط به ورژن خاصی از API ما هستند، پیاده سازی میکنیم:
public class ItemViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Country { get; set; }
}
public class ItemV2ViewModel : ItemViewModel
{
    public double Price { get; set; }
}
ItemViewModel مربوط به ورژن 1 میباشد.
 public class ItemsController : ApiController   
 {
        [ResponseType(typeof(ItemViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
 public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
ItemsController مربوط به ورژن 1 میباشد.
اگر قرار باشد از مسیرهای متمرکز استفاده کنیم، کافی است که تغییرات زیر را اعمال کنیم:
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new
{
    controller = "ItemsV2",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute("Items", "api/items/{id}", new
{
    controller = "Items",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
در کد بالا به صراحت برای تک تک کنترلرها مسیریابی متناسب با ورژنی که آن را پشتیبانی میکند، معرفی کرده‌ایم.
نکته: این تنظیمات خاص باید قبل از تنظیمات پیش فرض قرار گیرند.

روش بالا به خوبی کار خواهد کرد ولی آنچنان مطلوب نمیباشد. به همین دلیل یک مسیریابی عمومی و کلی را در نظر میگیریم که شامل ورژن مورد نظر، در قالب یک پارامتر میباشد. در روش جایگزین باید به شکل زیر عمل کنید:
config.Routes.MapHttpRoute(
"defaultVersioned",
"api/v{version}/{controller}/{id}",
new { id = RouteParameter.Optional }, new { version = @"\d+" });

config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional }
);

با این تنظیمات فعلا به مسیریابی ورژن بندی شده‌ای دست نیافته‌ایم. زیرا فعلا به هیچ طریق به Web API اشاره نکرده‌ایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.

IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده می‌باشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:

 به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.

public class VersionFinder
    {
        private static bool NeedsUriVersioning(HttpRequestMessage request, out string version)
        {
            var routeData = request.GetRouteData();
            if (routeData != null)
            {
                object versionFromRoute;
                if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute))
                {
                    version = versionFromRoute as string;
                    if (!string.IsNullOrWhiteSpace(version))
                    {
                        return true;
                    }
                }
            }
            version = null;
            return false;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
        public static int GetVersionFromRequest(HttpRequestMessage request)
        {
            string version;

            return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0;
        }
    }

کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری  مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت می‌گیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار می‌گیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.

 public class VersionAwareControllerSelector : DefaultHttpControllerSelector
    {
        public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { }
        public override string GetControllerName(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);
            var version = VersionFinder.GetVersionFromRequest(request);
                
        return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName;
        }
        private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName,
        int version)
        {
            var versionControllerName = $"{baseControllerName}v{version}";
            HttpControllerDescriptor descriptor;
            if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor))
            {
                return versionControllerName;
            }

            throw new HttpResponseException(request.CreateErrorResponse(
            HttpStatusCode.NotFound,
                $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}"));
        }
    }

متد  GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از  IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند. 

config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));

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

راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالب‌های مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.

[RoutePrefix("api/v1/Items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country =        "Japan" };
            return Ok(viewModel);
        }
    }


 [RoutePrefix("api/V2/Items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

اگر توجه کرده باشید در مثال ما، نام‌های کنترلر‌ها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نام‌های مختلف برای یک کنترلر در ورژن‌های مختلف، آن را با یک نام یکسان درون namespace‌های مختلف احاطه کنیم یا حتی آنها را درون Class Library‌های جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید. 

راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر می‌باشد:

کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:

public class VersionConstraint : IHttpRouteConstraint
{
    public VersionConstraint(string allowedVersion)
    {
        AllowedVersion = allowedVersion.ToLowerInvariant();
    }
    public string AllowedVersion { get; private set; }

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
    IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
        }
        return false;
    }
}

کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.

متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کرده‌ایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.

 به روش استفاده از این Constraint توجه کنید:

namespace UriBasedVersioning.Namespace.Controllers.V1
{
    using Models.V1;

RoutePrefix("api/{version:VersionConstraint(v1)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
}
namespace UriBasedVersioning.Namespace.Controllers.V2
{
    using Models.V2;


RoutePrefix("api/{version:VersionConstraint(v2)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
}

با توجه به کد بالا می‌توان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Route‌ها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از API‌های بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:

var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));
config.MapHttpAttributeRoutes(constraintsResolver);

مرحله بعدی کار، پیاده سازی IHttpControllerSelector می‌باشد:

  public class NamespaceControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
        public NamespaceControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string,
                HttpControllerDescriptor>>(InitializeControllerDictionary);
        }
        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var version = GetVersion(routeData);
            if (version == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                version, controllerName);
            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }
        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string,
                HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);
                var controllerName =
                    controllerType.Name.Remove(controllerType.Name.Length -
                                               DefaultHttpControllerSelector.ControllerSuffix.Length);
                var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                    segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration,
                        controllerType.Name,
                        controllerType);
                }
            }
            return dictionary;
        }
        private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
        private static string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            var dataTokenValue = subroute?.Route.DataTokens.First().Value;
            var controllerName =
                ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
            return controllerName;
        }
        private static string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();
            return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version");
        }
    }

سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکته‌ی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing می‌باشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامتر‌های controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلر‌های موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.

کارکرد متد InitializeControllerDictionary :

همانطور که میدانید به صورت پیش‌فرض Web API توجهی به فضای نام مرتبط با کنترلر‌ها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:

public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()

لذا در کلاس پیاده سازی شده، خصوصیتی به نام ‎‎_controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا می‌باشد، تعریف کرده‌ایم. متد InitializeControllerDictionary کار آماده سازی داده‌های مورد نیاز خصوصیت ‎‎_controllers می‌باشد. به این صورت که تمام کنترلر‌های موجود در برنامه را واکشی کرده و این بار کلید‌های مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کرده‌ایم.

حال تنظیمات زیر را اعمال کنید:

 public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));

            config.MapHttpAttributeRoutes(constraintsResolver);

            config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config));

        }
    }

این بار برنامه را اجرا کرده و API‌های مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.

نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم  فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.

پیاده سازی Header-based versioning  

در این روش کافی است هدری را برای دریافت ورژن API درخواستی مشخص کرده باشید. برای مثال هدری به نام api-version، که استفاده کننده از این طریق، ورژن درخواستی خود را اعلام میکند. همانطور که قبلا اشاره کردیم، با استفاده از Constraint‌ها دست شما خیلی باز خواهد بود. برای مثال این بار به جای واکشی پارامتر version از RouteData، کد همان قسمت را برای واکشی آن از هدر درخواستی تغییر خواهیم داد:
public class HeaderVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionHeaderName = "api-version";

        public HeaderVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
            IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Contains(VersionHeaderName)) return false;

            var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault();

            return VersionToInt(version) == AllowedVersion;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
    }
این بار در متد Match، به دنبال هدر خاصی به نام api-version می‌گردیم و مقدار آن را با AllowdVersion مقایسه میکنیم تا مشخص کنیم که آیا این Route نوشته شده برای ادامه کار واکشی کنترلر مورد نظر استفاده شود یا خیر. در ادامه یک RouteFactoryAttribute شخصی سازی شده را به منظور معرفی این محدودیت تعریف شده، در نظر میگیریم:
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute
    {
        public HeaderVersionedRouteAttribute(string template) : base(template)
        {
            Order = -1;
        }

        public int Version { get; set; }

        public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary
        {
            {"", new HeaderVersionConstraint(Version)}
        };
    }

با استفاده از خصوصیت Constraints، توانستیم محدودیت پیاده سازی شده خود را به سیستم مسیریابی معرفی کنیم. توجه داشته باشید که این بار هیچ پارامتری در Uri تحت عنوان version را نداریم و از این جهت به صراحت کلیدی برای آن مشخص نکرده ایم (‎‎‎‎‏‏‎‎{"", new HeaderVersionConstraint(Version)} ‎‎ ).
کنترلر‌های ما به شکل زیر خواهند بود:
    [RoutePrefix("api/items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [HeaderVersionedRoute("{id}", Version = 1)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
  [RoutePrefix("api/items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [HeaderVersionedRoute("{id}", Version = 2)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

این بار از VersionedRoute برای اعمال مسیر یابی Attribute-based استفاده کرده ایم و به صراحت ورژن هر کدام را با استفاده از خصوصیت Version آن مشخص کرده‌ایم. نتیجه:


پیاده سازی Media type-based versioning
قرار است ورژن API مورد نظر را که استفاده کننده درخواست میدهد، از دل درخواست‌هایی به فرم زیر واکشی کنیم:
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v1+json
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v2+json
در این روش دست ما کمی بازتر است چرا که میتوانیم بر اساس فرمت درخواستی نیز تصمیماتی را برای ارائه ورژن خاصی از API  داشته باشیم. روال کار شبیه به روش قبلی با پیاده سازی یک Constraint و یک RouteFactoryAttribute انجام خواهد گرفت.
 public class MediaTypeVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionMediaType = "vnd.mediatype.versioning";

        public MediaTypeVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Accept.Any()) return false;

            var acceptHeaderVersion =
                request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType));

            //Accept: application/vnd.mediatype.versioning-v1+json
            if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") ||
                !acceptHeaderVersion.MediaType.Contains("+"))
                return false;

            var version = acceptHeaderVersion.MediaType.Between("-v", "+");
            return VersionToInt(version)==AllowedVersion;
        }
}
در متد Match کلاس بالا به دنبال مدیا تایپ مشخصی هستیم که با هدر Accept برای ما ارسال میشود. برای آشنایی با فرمت ارسال Media Type‌ها میتوانید به اینجا مراجعه کنید. کاری که در اینجا انجام شده‌است، یافتن ورژن ارسالی توسط استفاده کننده می‌باشد که آن را با فرمت خاصی که نشان داده شد و مابین (v-) و (+) قرار داده، ارسال میشود. ادامه کار به مانند روش Header-based میباشد و از مطرح کردن آن در مقاله خودداری میکنیم.
نتیجه به شکل زیر خواهد بود:


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


مطالب دوره‌ها
مدیریت استثناهای رخ داده در Application_Start یک برنامه‌ی ASP.NET
اگر در برنامه‌ی وب خود از ELMAH استفاده می‌کنید و استثنایی در روال رخدادگردان Application_Start واقع در فایل global.asax.cs رخ دهد، این استثناء در لاگ‌های ELMAH  ظاهر نمی‌شود و صرفا در لاگ‌های خود ویندوز قابل مطالعه خواهد بود؛ از این جهت که HttpContext.Current در این روال هنوز ایجاد نشده‌است. همچنین اگر از IoC Containers استفاده می‌کنید یا آغاز بانک اطلاعاتی ORM خود را در روال Application_Start قرار داده‌اید، تمام این‌ها نیز آغاز نشده باقی خواهند ماند و عملا تا ری استارت بعدی برنامه یا IIS در سرور به صورت دستی، برنامه و سایت قابل استفاده نخواهد بود.

سؤال: آیا می‌توان از ری‌استارت دستی IIS، به ری‌استارت خودکار رسید؟

پاسخ: بلی. فراخوانی متد HttpRuntime.UnloadAppDomain در یک برنامه‌ی ASP.NET سبب ری استارت آن خواهد شد. بنابراین می‌توان این متد را در Application_Start برنامه، جهت ری‌استارت خودکار آن در صورت شکست اولیه Application_Start قرار داد:
void Application_Start(object sender, EventArgs e)
{
   try
   {
      // startup stuff
   }
   catch
   {
     HttpRuntime.UnloadAppDomain(); // سبب ری استارت برنامه و آغاز مجدد آن با درخواست بعدی می‌شود
     throw;
   }
}
اهمیت این مساله از آنجا ناشی می‌شود که ممکن است در آن لحظه‌ی خاص، بار سرور زیاد بوده باشد یا حتی میزان حافظه مهیای جهت آغاز برنامه کافی نبوده باشد؛ و نه الزاما مشکل منطقی در کدهای برنامه. فراخوانی HttpRuntime.UnloadAppDomain سبب خواهد شد تا برنامه شانس مجدد اجرای دیگری را بدون نیازی به ری استارت دستی IIS بیابد.
مطالب
ساخت تم سفارشی در انگیولار متریال ۲ - بخش اول

در  قسمت قبل  بیان شد همراه با نصب Angular Material، تعدادی تم از قبل ساخته شده نیز نصب خواهند شد که شامل یکسری استایل با رنگهای مشخصی هستند. این تم‌ها عبارتند از:

  • indigo-pink
  • deeppurple-amber
  • purple-green
  • pink-bluegrey 

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


مقدمه

تم در انگیولار متریال، از ترکیب چند پالت رنگی، ساخته می‌‌شود. پالت‌های رنگ را در طراحی متریال ( Material Design ) در  اینجا می‌توانید مشاهده کنید. انگیولار متریال رنگهای مورد استفاده خود را در گروه‌های زیر دسته بندی کرده است. 

  • Primary : این پالت رنگی به صورت گسترده در بخشهای مختلف صفحه و کامپوننت‌ها مورد استفاده قرار می‌گیرد.
  • Accent : این پالت رنگی برای دکمه‌های شناور و همچنین المنتهای تعاملی مورد استفاده قرار می‌گیرد.
  • Warn : این پالت رنگی برای مشخص کردن حالت‌های خطا، مورد استفاده قرار می‌گیرد.
  • Foreground : این پالت رنگی برای متون و آیکونها مورد استفاده قرار می‌گیرد.
  • Background : این پالت رنگی برای المنت‌های پس زمینه مورد استفاده قرار می‌گیرد.

در انگیولار متریال تمامی تم‌ها در زمان build به صورت استاتیک تولید می‌شوند. این قابلیت با خارج کردن چرخه تولید تم از چرخه راه‌اندازی برنامه، باعث بهبود در راه‌اندازی خواهد شد. 


تعریف تم سفارشی

برای ساخت تم سفارشی نیاز به یک فایل Sass خواهیم داشت. پس در مسیر /src یک فایل Sass را  با نام my-custom-theme.scss ایجاد می‌کنیم (شما می‌توانید از هر نام دیگری برای فایل Sass استفاده کنید). اگر از AngularCLI برای برنامه‌های خود استفاده می‌کنید، بایستی فایل Sass ایجاد شده را به لیست استایل‌ها در فایل angular-cli.json اضافه کنید. این کار باعث می‌شود AngularCLI این فایل Sass را در زمان build به css کامپایل کند. 

نکته: استفاده از فایل Sass برای ساختن تم سفارشی به این معنی نیست که شما از Sass برای سایر Style های برنامه خود استفاده کنید.

"styles": [
  "styles.css",
  "my-custom-theme.scss"
],

اگر از AngularCLI استفاده نمی‌کنید، شما نیاز به ابزاری برای کامپایل فایل Sass به css خواهید داشت.  ابزارهای بسیاری در این زمینه وجود دارند از جمله: gulp-sass و grunt-sass . ولی ساده‌ترین ابزار برای این کار node-sass می‌باشد. کافی است بعد از نصب، دستور زیر را اجرا کنید تا فایل sass به css کامپایل شود. فایل css تولید شده را مستقیما در صفحه index.html خود می‌توانید استفاده کنید.

node-sass src/my-custom-theme.scss dist/my-custom-theme.css

در فایل تم ایجاد شده ( my-custom-theme.scss ) ابتدا بایستی فایل Sass اصلی انگیولار متریال را وارد کنید.

@import '~@angular/material/theming';

در قدم بعدی mixin تعریف شده با نام mat-core  را در فایل Sass  انگیولار متریال، وارد می‌کنیم. این mixin شامل تمامی Styleهای مشترکی است که توسط کامپوننت‌های مختلف استفاده می‌شود. 

@include mat-core();

نکته: مطمئن شوید فقط یک بار این mixin را در سرتاسر برنامه خود وارد کرده باشید. در غیر این صورت، فایل css تولید شده شامل یکسری Style تکراری خواهد بود و این باعث بزرگ و حجیم شدن فایل css نهایی خواهد شد. 


تا اینجا فایل تم ایجاد شده اینگونه خواهد بود:  

@import '~@angular/material/theming';
@include mat-core();

حالا نوبت تعریف تم سفارشی است. ولی قبل از آن باید با سیستم رنگها در طراحی متریال ( Material Design ) آشنایی داشته باشیم. در طراحی متریال ۱۹ پالت رنگی با نام‌های مختلف وجود دارند. برای ۱۶ پالت رنگی، ۱۴ طیف رنگی و برای ۳ پالت رنگی دیگر، ۱۰ طیف رنگی در نظر گرفته شده است. هر کدام از این طیف‌های رنگی، دارای یک مقدار عددی است. یعنی یک رنگ در سیستم طراحی متریال متشکل از یک نام رنگ و یک شماره طیف رنگ است که مقدار پیش فرض طیف رنگ، عدد ۵۰۰ می‌باشد. 


حالا با استفاده از تابع mat-palette تعریف شده در فایل Sass انگیولار متریال، سه متغیر را برای رنگهای Primary ، Accent و Warn در فایل my-custom-theme.scss ، به شکل زیر تعریف می‌کنیم. 

$my-app-primary: mat-palette($mat-indigo);
$my-app-accent:  mat-palette($mat-pink, 500, A100, A400);
$my-app-warn:    mat-palette($mat-deep-orange);

تابع mat-palette در فایل Sass اصلی انگیولار متریال، به شکل زیر تعریف شده است. 

@function mat-palette($base-palette, $default: 500, $lighter: 100, $darker: 700)

این تابع یک پارامتر اجباری دارد و بقیه پارامترها اختیاری هستند.

  • base-palette $: نام رنگ را دریافت می‌کند. این پارامتر اجباری است و باید مشخص شود. 
  • default$: با این پارامتر، طیف پیش‌فرض رنگ انتخاب شده را مشخص می‌کنیم. این پارامتر اختیاری است و مقدار پیش فرض آن 500 است.
  • lighter$: با این پارامتر، طیف روشن رنگ انتخاب شده را مشخص می‌کنیم. این پارامتر اختیاری است و مقدار پیش فرض آن 100 است.
  • darker$: با این پارامتر، طیف تیره رنگ انتخاب شده را مشخص می‌کنیم. این پارامتر اختیاری است و مقدار پیش فرض آن 700 است.

در قدم آخر با استفاده از تابع mat-light-theme یا mat-dark-theme، رنگهای تعریف شده در مرحله قبل را ترکیب کرده و نتیجه را به عنوان ورودی به mixin به نام angular-material-theme  ارسال و بارگذاری می‌کنیم. 

تابع mat-light-theme و mat-dark-theme سه پارامتر را دریافت می‌کند. پارارمتر اول پالت رنگ ایجاد شده توسط تابع mat-palette برای گروه Primary ، پارامتر دوم پالت رنگ ایجاد شده برای گروه Accent و پارامتر سوم پالت رنگ ایجاد شده برای گروه Warn را دریافت می‌کند. دو پارامتر اول اجباری و پارامتر سوم اختیاری با مقدار پیش فرض mat-palette($mat-red) می‌باشد. 

شکل کلی فایل Sass در نهایت به شکل زیر خواهد بود. 

@import '~@angular/material/theming';
@include mat-core();
$my-app-primary: mat-palette($mat-teal);
$my-app-accent:  mat-palette($mat-amber, 500, A100, A400);
$my-app-warn:    mat-palette($mat-deep-orange);

$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);

@include angular-material-theme($my-app-theme);

برای استفاده از پالت رنگ‌های ایجاد شده، از خصوصیت color در المنت‌های انگولار متریال استفاده می‌کنیم. برای نمونه بعد از تغییر فایل Sass به شکل بالا و حذف لینک تم از پیش ساخته شده که در پست قبلی به Style.cs اضافه کرده بودیم، می‌توانیم کار خود را به صورت زیر آزمایش کنیم. در فایل app.component.html در تگ main کدهای زیر را اضافه کنید. 

<md-card>
  <button md-raised-button color="primary">
    Primary
  </button>
  <button md-raised-button color="accent">
    Accent
  </button>
  <button md-raised-button color="warn">
    Warning
  </button>
</md-card>

خروجی زیر را مشاهده خواهید کرد. 

همچنین می‌توانید به جای استفاده از تابع mat-light-theme از تابع mat-dark-theme استفاده کنید. دراین صورت خروجی زیر را خواهید دید. 

در بخش بعدی نحوه ساخت چند تم دیگر را در کنار تم اصلی، ساخت تم به ازای هر کامپوننت و نحوه تعویض تم از طریق کد را دنبال خواهیم کرد. 

کدهای این قسمت را از اینجا دریافت کنید:  ساخت-تم-سفارشی-در-انگولار-متریال-۲---بخش-اول.rar 

مطالب
مروری مقدماتی بر ساخت برنامه‌های موبایل در MVC4
برای شروع، پروژه‌ای از نوع InternetApplication را انتخاب نموده مطابق شکل زیر:


 سپس اگر صفحه Layout.cshtml_ باز کنید، با متا تگی به فرم زیر روبرو می‌شوید که توسط آن می‌توانیم اندازه صفحه نمایش را مشخص کنیم:


زمانیکه Request ای صادر می‌شود، موارد ذیل تشکیل خواهند شد:


اگر دقت کنید در قسمت User-Agent یکسری مشخصات از سیستم نمایان است که ما طبق آن میتوانیم صفحه مربوطه را بارگذاری کنیم.
سپس می‌بایست به فایل Global.asax  رفته و تنظیماتی را در Application Start اعمال کنیم:
       protected void Application_Start() 
            {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth(); 
            InitializeDisplayModeProvider(); 
        }
        protected void InitializeDisplayModeProvider()
        {
            var phone = new DefaultDisplayMode("Phone")
                {
                    ContextCondition = ctx => ctx.GetOverriddenUserAgent() != null &&
                                                           ctx.GetOverriddenUserAgent().Contains("iPhone")
                };
            var mobile = new DefaultDisplayMode("Tablet")
                {
                    ContextCondition = ctx => ctx.GetOverriddenUserAgent() != null && 
                                                           ctx.GetOverriddenUserAgent().Contains("iPad")
                };

            DisplayModeProvider.Instance.Modes.Insert(0, phone);
            DisplayModeProvider.Instance.Modes.Insert(1, mobile);
        }
توسط متد InitialDisplayModeProvider  دو متغیر را تعریف می‌کنیم که بواسط آنها می‌توان مثلا UserAgent ای را که می‌خواهیم، مشخص کنیم و سپس در DisplayModelProvider آن را بعنوان Instance ای درج و سپس تابع InitialDisplayModeProvider را در Application_start تنظیم کنیم.
حال می‌بایست Page‌های مربوط به هر کدام را طراحی کرده و با پسوند مثلا (Phone-Tablet)ذخیره کنیم.

اگر Extension ای را برای تست شبیه سازی محیط موبایل یا تبلت، نصب کنید، می‌توانید آن را مشاهده کنید.
حال می‌توان برای شکیل‌تر کردن آن از نیوگت jquerymobile را گرفته و طبق مطالب گفته شده از صفحه Layout خود هم چند نمونه برای مرورگرهای مختلف درست کرده وبا jquerymobile روی آن کار کنید.