مطالب
Globalization در ASP.NET MVC - قسمت پنجم
در قسمت قبل راجع به مدل پیش‌فرض پرووایدر منابع در ASP.NET بحث نسبتا مفصلی شد. در این قسمت تولید یک پرووایدر سفارشی برای استفاده از دیتابیس به جای فایل‌های resx. به عنوان منبع نگهداری داده‌ها بحث می‌شود.
قبلا هم اشاره شده بود که در پروژه‌های بزرگ ذخیره تمام ورودی‌های منابع درون فایل‌های resx. بازدهی مناسبی نخواهد داشت. هم‌چنین به مرور زمان و با افزایش تعداد این فایل‌ها، کار مدیریت آن‌ها بسیار دشوار و طاقت‌فرسا خواهد شد. درضمن به‌دلیل رفتار سیستم کشینگ این منابع در ASP.NET، که محتویات کل یک فایل را بلافاصله پس از اولین درخواست یکی از ورودی‌های آن در حافظه سرور کش می‌کند، در صورت وجود تعداد زیادی فایل منبع و با ورودی‌های بسیار، با گذشت زمان بازدهی کلی سایت به شدت تحت تاثیر قرار خواهد گرفت.
بنابراین استفاده از یک منبع مثل دیتابیس برای چنین شرایطی و نیز کنترل مدیریت دسترسی به ورودی‌های آن به صورت سفارشی، می‌تواند به بازدهی بهتر برنامه کمک زیادی کند. درضمن فرایند به‌روزرسانی مقادیر این ورودی‌ها در صورت استفاده از یک دیتابیس می‌تواند ساده‌تر از حالت استفاده از فایل‌های resx. انجام شود.
 
تولید یک پرووایدر منابع دیتابیسی - بخش اول
در بخش اول این مطلب با نحوه پیاده‌سازی کلاس‌های اصلی و اولیه موردنیاز آشنا خواهیم شد. مفاهیم پیشرفته‌تر (مثل کش‌کردن ورودی‌ها و عملیات fallback) و نیز ساختار مناسب جدول یا جداول موردنیاز در دیتابیس و نحوه ذخیره ورودی‌ها برای انواع منابع در دیتابیس در مطلب بعدی آورده می‌شود.
با توجه به توضیحاتی که در قسمت قبل داده شد، می‌توان از طرح اولیه‌ای به صورت زیر برای سفارشی‌سازی یک پرووایدر منابع دیتابیسی استفاده کرد:


اگر مطالب قسمت قبل را خوب مطالعه کرده باشید، پیاده سازی اولیه طرح بالا نباید کار سختی باشد. در ادامه یک نمونه از پیاده‌سازی‌های ممکن نشان داده شده است.
برای آغاز کار ابتدا یک پروژه ClassLibrary جدید مثلا با نام DbResourceProvider ایجاد کنید و ریفرنسی از اسمبلی System.Web به این پروژه اضافه کنید. سپس کلاس‌هایی که در ادامه شرح داده شده‌اند را به آن اضافه کنید.

کلاس DbResourceProviderFactory
همه چیز از یک ResourceProviderFactory شروع می‌شود. نسخه سفارشی نشان داده شده در زیر برای منابع محلی و کلی از کلاس‌های پرووایدر سفارشی استفاده می‌کند که در ادامه آورده شده‌اند.
using System.Web.Compilation;
namespace DbResourceProvider
{
  public class DbResourceProviderFactory : ResourceProviderFactory
  {
    #region Overrides of ResourceProviderFactory
    public override IResourceProvider CreateGlobalResourceProvider(string classKey)
    {
      return new GlobalDbResourceProvider(classKey);
    }
    public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
    {
      return new LocalDbResourceProvider(virtualPath);
    }
    #endregion
  }
}
درباره اعضای کلاس ResourceProviderFactory در قسمت قبل توضیحاتی داده شد. در نمونه سفارشی بالا دو متد این کلاس برای برگرداندن پرووایدرهای سفارشی منابع محلی و کلی بازنویسی شده‌اند. سعی شده است تا نمونه‌های سفارشی در اینجا رفتاری همانند نمونه‌های پیش‌فرض در ASP.NET داشته باشند، بنابراین برای پرووایدر منابع کلی (GlobalDbResourceProvider) نام منبع درخواستی (className) و برای پرووایدر منابع محلی (LocalDbResourceProvider) مسیر مجازی درخواستی (virtualPath) به عنوان پارامتر کانستراکتور ارسال می‌شود.
 
نکته: برای استفاده از این کلاس به جای کلاس پیش‌فرض ASP.NET باید یکسری تنظیمات در فایل کانفیگ برنامه مقصد اعمال کرد که در ادامه آورده شده است.

کلاس BaseDbResourceProvider
برای پیاده‌سازی راحت‌تر کلاس‌های موردنظر، بخش‌های مشترک بین دو پرووایدر محلی و کلی در یک کلاس پایه به صورت زیر قرار داده شده است. این طرح دقیقا مشابه نمونه پیش‌فرض ASP.NET است.
using System.Globalization;
using System.Resources;
using System.Web.Compilation;
namespace DbResourceProvider
{
  public abstract class BaseDbResourceProvider : IResourceProvider
  {
    private DbResourceManager _resourceManager;
    protected abstract DbResourceManager CreateResourceManager();
    private void EnsureResourceManager()
    {
      if (_resourceManager != null) return;
      _resourceManager = CreateResourceManager();
    }
    #region Implementation of IResourceProvider
    public object GetObject(string resourceKey, CultureInfo culture)
    {
      EnsureResourceManager();
      if (_resourceManager == null) return null;
      if (culture == null) culture = CultureInfo.CurrentUICulture;
      return _resourceManager.GetObject(resourceKey, culture);
    }
    public virtual IResourceReader ResourceReader { get { return null; } }
    #endregion
  }
}
کلاس بالا چون یک کلاس صرفا پایه است بنابراین به صورت abstract تعریف شده است. در این کلاس، از نمونه سفارشی DbResourceManager برای بازیابی داده‌ها از دیتابیس استفاده شده است که در ادامه شرح داده شده است.
در اینجا، از متد CreateResourceManager برای تولید نمونه مناسب از کلاس DbResourceManager استفاده می‌شود. این متد به صورت abstract و protected تعریف شده است بنابراین پیاده‌سازی آن باید در کلاس‌های مشتق شده که در ادامه آورده شده‌اند انجام شود.
در متد EnsureResourceManager کار بررسی نال نبودن resouceManager_ انجام می‌شود تا درصورت نال بودن آن، بلافاصله نمونه‌ای تولید شود.

نکته: ازآنجاکه نقطه آغازین فرایند یعنی تولید نمونه‌ای از کلاس DbResourceProviderFactory توسط خود ASP.NET انجام خواهد شد، بنابراین مدیریت تمام نمونه‌های ساخته شده از کلاس‌هایی که در این مطلب شرح داده می‌شوند درنهایت عملا برعهده ASP.NET است. در ASP.NET درطول عمر یک برنامه تنها یک نمونه از کلاس Factory تولید خواهد شد، و متدهای موجود در آن در حالت عادی تنها یکبار به ازای هر منبع درخواستی (کلی یا محلی) فراخوانی می‌شوند. درنتیجه به ازای هر منبع درخواستی (کلی یا محلی) هر یک از کلاس‌های پرووایدر منابع تنها یک‌بار نمونه‌سازی خواهد شد. بنابراین بررسی نال نبودن این متغیر و تولید نمونه‌ای جدید تنها در صورت نال بودن آن، کاری منطقی است. این نمونه بعدا توسط ASP.NET به ازای هر منبع یا صفحه درخواستی کش می‌شود تا در درخواست‌های بعدی تنها از این نسخه کش‌شده استفاده شود.

در متد GetObject نیز کار استخراج ورودی منابع انجام می‌شود. ابتدا با استفاده از متد EnsureResourceManager از وجود نمونه‌ای از کلاس DbResourceManager اطمینان حاصل می‌شود. سپس درصورتی‌که مقدار این کلاس همچنان نال باشد مقدار نال برگشت داده می‌شود. این حالت وقتی پیش می‌آید که نتوان با استفاده از داده‌های موجود نمونه‌ای مناسب از کلاس DbResourceManager تولید کرد.
سپس مقدار کالچر ورودی بررسی می‌شود و درصورتی‌که نال باشد مقدار کالچر UI ثرد جاری که در CultureInfo.CurrentUICulture قرار دارد برای آن درنظر گرفته می‌شود. درنهایت با فراخوانی متد GetObject از DbResourceManager تولیدی برای کلید و کالچر مربوطه کار استخراج ورودی درخواستی پایان می‌پذیرد.
پراپرتی ResourceReader در این کلاس به صورت virtual تعریف شده است تا بتوان پیاده‌سازی مناسب آن را در هر یک از کلاس‌های مشتق‌شده اعمال کرد. فعلا برای این کلاس پایه مقدار نال برگشت داده می‌شود.

کلاس GlobalDbResourceProvider
برای پرووایدر منابع کلی از این کلاس استفاده می‌شود. نحوه پیاده‌سازی آن نیز دقیقا همانند طرح نمونه پیش‌فرض ASP.NET است.
using System;
using System.Resources;
namespace DbResourceProvider
{
  public class GlobalDbResourceProvider : BaseDbResourceProvider
  {
    private readonly string _classKey;
    public GlobalDbResourceProvider(string classKey)
    {
      _classKey = classKey;
    }
    #region Implementation of BaseDbResourceProvider
    protected override DbResourceManager CreateResourceManager()
    {
      return new DbResourceManager(_classKey);
    }
    public override IResourceReader ResourceReader
    {
      get { throw new NotSupportedException(); }
    }
    #endregion
  }
}
GlobalDbResourceProvider از کلاس پایه‌ای که در بالا شرح داده شد مشتق شده است. بنابراین تنها بخش‌های موردنیاز یعنی متد CreateResourceManager و پراپرتی ResourceReader در این کلاس پیاده‌سازی شده است.
در اینجا نمونه مخصوص کلاس ResourceManager (همان DbResourceManager) با توجه به نام فایل مربوط به منبع کلی تولید می‌شود. نام فایل در اینجا همان چیزی است که در دیتابیس برای نام منبع مربوطه ذخیره می‌شود. ساختار آن بعدا بحث می‌شود.
همان‌طور که می‌بینید برای پراپرتی ResourceReader خطای عدم پشتیبانی صادر می‌شود. دلیل آن در قسمت قبل و نیز به‌صورت کمی دقیق‌تر در ادامه آورده شده است.

کلاس LocalDbResourceProvider
برای منابع محلی نیز از طرحی مشابه نمونه پیش‌فرض ASP.NET که در قسمت قبل نشان داده شد، استفاده شده است.
using System.Resources;
namespace DbResourceProvider
{
  public class LocalDbResourceProvider : BaseDbResourceProvider
  {
    private readonly string _virtualPath;
    public LocalDbResourceProvider(string virtualPath)
    {
      _virtualPath = virtualPath;
    }
    #region Implementation of BaseDbResourceProvider
    protected override DbResourceManager CreateResourceManager()
    {
      return new DbResourceManager(_virtualPath);
    }
    public override IResourceReader ResourceReader
    {
      get { return new DbResourceReader(_virtualPath); }
    }
    #endregion
  }
}
این کلاس نیز از کلاس پایه‌ای BaseDbResourceProvider مشتق شده و پیاده‌سازی‌های مخصوص منابع محلی برای متد CreateResourceManager و پراپرتی ResourceReader در آن انجام شده است.
در متد CreateResourceManager کار تولید نمونه‌ای از DbResourceManager با استفاده از مسیر مجازی صفحه درخواستی انجام می‌شود. این فرایند شبیه به پیاده‌سازی پیش‌فرض ASP.NET است. در واقع در پیاده‌سازی جاری، نام منابع محلی همنام با مسیر مجازی متناظر آن‌ها در دیتابیس ذخیره می‌شود. درباره ساختار جدول دیتابیس بعدا بحث می‌شود.
در این کلاس کار بازخوانی کلیدهای موجود برای پراپرتی‌های موجود در یک صفحه از طریق نمونه‌ای از کلاس DbResourceReader انجام شده است. شرح این کلاس در ادامه آمده است. 

نکته: همانطور که در قسمت قبل هم اشاره کوتاهی شده بود، از خاصیت ResourceReader در پرووایدر منابع برای تعیین تمام پراپرتی‌های موجود در منبع استفاده می‌شود تا کار جستجوی کلیدهای موردنیاز در عبارات بومی‌سازی ضمنی برای رندر صفحه وب راحت‌تر انجام شود. بنابراین از این پراپرتی تنها در پرووایدر منابع محلی استفاده می‌شود. ازآنجاکه در عبارات بومی‌سازی ضمنی تنها قسمت اول نام کلید ورودی منبع آورده می‌شود، بنابراین قسمت دوم (و یا قسمت‌های بعدی) کلید موردنظر که همان نام پراپرتی کنترل متناظر است از جستجو میان ورودی‌های یافته شده توسط این پراپرتی بدست می‌آید تا ASP.NET بداند که برای رندر صفحه چه پراپرتی‌هایی نیاز به رجوع به پرووایدر منبع محلی مربوطه دارد (برای آشنایی بیشتر با عبارت بومی‌سازی ضمنی رجوع شود به قسمت قبل).

نکته: دقت کنید که پس از اولین درخواست، خروجی حاصل از enumerator این ResourceReader کش می‌شود تا در درخواست‌های بعدی از آن استفاده شود. بنابراین در حالت عادی، به ازای هر صفحه تنها یکبار این پراپرتی فراخوانده می‌شود. درباره این enumerator در ادامه بحث شده است.

کلاس DbResourceManager
کار اصلی مدیریت و بازیابی ورودی‌های منابع از دیتابیس از طریق کلاس DbResourceManager انجام می‌شود. نمونه‌ای بسیار ساده و اولیه از این کلاس را در زیر مشاهده می‌کنید:
using System.Globalization;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  public class DbResourceManager
  {
    private readonly string _resourceName;
    public DbResourceManager(string resourceName)
    {
      _resourceName = resourceName;
    }
    public object GetObject(string resourceKey, CultureInfo culture)
    {
      var data = new ResourceData();
      return data.GetResource(_resourceName, resourceKey, culture.Name).Value;
    }
  }
}
کار استخراج ورودی‌های منابع با استفاده از نام منبع درخواستی در این کلاس مدیریت خواهد شد. این کلاس با استفاده نام منیع درخواستی به عنوان پارامتر کانستراکتور ساخته می‌شود. با استفاده از متد GetObject که نام کلید ورودی موردنظر و کالچر مربوطه را به عنوان پارامتر ورودی دریافت می‌کند فرایند استخراج انجام می‌شود.
برای کپسوله‌سازی عملیات از کلاس جداگانه‌ای (ResourceData) برای تبادل با دیتابیس استفاده شده است. شرح بیشتر درباره این کلاس و نیز پیاده سازی کامل‌تر کلاس DbResourceManager به همراه مدیریت کش ورودی‌های منابع و نیز عملیات fallback در مطلب بعدی آورده می‌شود.

کلاس DbResourceReader
این کلاس که درواقع پیاده‌سازی اینترفیس IResourceReader است برای یافتن تمام کلیدهای تعریف شده برای یک منبع به‌کار می‌رود، پیاده‌سازی آن نیز به صورت زیر است:
using System.Collections;
using System.Resources;
using System.Security;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  public class DbResourceReader : IResourceReader
  {
    private readonly string _resourceName;
    private readonly string _culture;
    public DbResourceReader(string resourceName, string culture = "")
    {
      _resourceName = resourceName;
      _culture = culture;
    }
    #region Implementation of IResourceReader
    public void Close() { }
    public IDictionaryEnumerator GetEnumerator()
    {
      return new DbResourceEnumerator(new ResourceData().GetResources(_resourceName, _culture));
    }
    #endregion
    #region Implementation of IEnumerable
    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
    #endregion
    #region Implementation of IDisposable
    public void Dispose()
    {
      Close();
    }
    #endregion
  }
}
این کلاس تنها با استفاده از نام منبع و عنوان کالچر موردنظر کار بازخوانی ورودی‌های موجود را انجام می‌دهد.
تنها نکته مهم در کد بالا متد GetEnumerator است که نمونه‌ای از اینترفیس IDictionaryEnumerator را برمی‌گرداند. در اینجا از کلاس DbResourceEnumerator که برای کار با دیتابیس طراحی شده، استفاده شده است. همانطور که قبلا هم اشاره شده بود، هر یک از اعضای این enumerator از نوع DictionaryEntry هستند که یک struct است. این کلاس در ادامه شرح داده شده است.
متد Close برای بستن و از بین بردن منابعی است که در تهیه enumerator موردبحث نقش داشته‌اند. مثل منایع شبکه‌ای یا فایلی که باید قبل از اتمام کار با این کلاس به صورت کامل بسته شوند. هرچند در نمونه جاری چنین موردی وجود ندارد و بنابراین این متد بلااستفاده است.
در کلاس فوق نیز برای دریافت اطلاعات از ResourceData استفاده شده است که بعدا به همراه ساختار مناسب جدول دیتابیس شرح داده می‌شود.
 
نکته: دقت کنید که در پیاده‌سازی نشان داده شده برای کلاس LocalDbResourceProvider برای یافتن ورودی‌های موجود از مقدار پیش‌فرض (یعنی رشته خالی) برای کالچر استفاده شده است تا از ورودی‌های پیش‌فرض که در حالت عادی باید شامل تمام موارد تعریف شده موجود هستند استفاده شود (قبلا هم شرح داده شد که منبع اصلی و پیش‌فرض یعنی همانی که برای زبان پیش‌فرض برنامه درنظر گرفته می‌شود و بدون نام کالچر مربوطه است، باید شامل حداکثر ورودی‌های تعریف شده باشد. منابع مربوطه به سایر کالچرها می‌توانند همه این ورودی‌های تعریف‌شده در منبع اصلی و یا قسمتی از آن را شامل شوند. عملیات fallback تضمین می‌دهد که درنهایت نزدیک‌ترین گزینه متناظر با درخواست جاری را برگشت دهد).
 
کلاس DbResourceEnumerator
کلاس دیگری که در اینجا استفاده شده است، DbResourceEnumerator است. این کلاس در واقع پیاده سازی اینترفیس IDictionaryEnumerator است. محتوای این کلاس در زیر آورده شده است:
using System.Collections;
using System.Collections.Generic;
using DbResourceProvider.Models;
namespace DbResourceProvider
{
  public sealed class DbResourceEnumerator : IDictionaryEnumerator
  {
    private readonly List<Resource> _resources;
    private int _dataPosition;
    public DbResourceEnumerator(List<Resource> resources)
    {
      _resources = resources;
      Reset();
    }
    public DictionaryEntry Entry
    {
      get
      {
        var resource = _resources[_dataPosition];
        return new DictionaryEntry(resource.Key, resource.Value);
      }
    }
    public object Key { get { return Entry.Key; } }
    public object Value { get { return Entry.Value; } }
    public object Current { get { return Entry; } }
    public bool MoveNext()
    {
      if (_dataPosition >= _resources.Count - 1) return false;
      ++_dataPosition;
      return true;
    }
    public void Reset()
    {
      _dataPosition = -1;
    }
  }
}
تفاوت این اینترفیس با اینترفیس IEnumerable در سه عضو اضافی است که برای استفاده در سیستم مدیریت منابع ASP.NET نیاز است. همان‌طور که در کد بالا مشاهده می‌کنید این سه عضو عبارتند از پراپرتی‌های Entry و Key و Value. پراپرتی Entry که ورودی جاری در enumerator را مشخص می‌کند از نوع DictionaryEntry است. پراپرتی‌های Key و Value هم که از نوع object تعریف شده‌اند برای کلید و مقدار ورودی جاری استفاده می‌شوند.
این کلاس لیستی از Resource به عنوان پارامتر کانستراکتور برای تولید enumerator دریافت می‌کند. کلاس Resource مدل تولیدی از ساختار جدول دیتابیس برای ذخیره ورودی‌های منابع است که در مطلب بعدی شرح داده می‌شود. بقیه قسمت‌های کد فوق هم پیاده‌سازی معمولی یک enumerator است.

نکته: به جای تعریف کلاس جداگانه‌ای برای enumerator اینترفیس IResourceProvider می‌توان از enumerator کلاس‌هایی که IDictionary را پیاده‌سازی کرده‌اند نیز استفاده کرد، مانند کلاس <Dictionary<object,object یا ListDictionary.
 
تنظیمات فایل کانفیگ
برای اجبار کردن ASP.NET به استفاده از Factory موردنظر باید تنظیمات زیر را در فایل web.config اعمال کرد:
<system.web>
    ...
    <globalization resourceProviderFactoryType=" نام کامل اسمبلی مربوطه ,نام پرووایدر فکتوری به همراه فضای نام آن " />
    ...
</system.web>
روش نشان داده شده در بالا حالت کلی تعریف و تنظیم یک نوع داده در فایل کانفیگ را نشان می‌دهد. درباره نام کامل اسمبلی در اینجا شرح داده شده است.
مثلا برای پیاده‌سازی نشان داده شده در این مطلب خواهیم داشت:
<globalization resourceProviderFactoryType="DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />

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

منابع:
مطالب
شروع کار با Dart - قسمت 3
لطفا قسمت دوم را در اینجا مطالعه بفرمایید

خدمت دوستان عزیز مطلبی را عرض کنم که البته باید در ابتدای این سری مقالات متذکر می‌شدم. این سری مقالات Dart مرجع کاملی برای یادگیری Dart نمی‌باشد. فقط یک Quick Start یا Get Started محسوب می‌شود برای آشنایی مقدماتی با ساختار Dart. از عنوان مقاله هم این موضوع قابل درک و تشخیص می‌باشد. همچنین فرض شده است که دوستان آشنایی مقدماتی با جاوااسکریپت و مباحث شی گرایی را نیز دارند. البته اگر مشغله کاری به بنده این اجازه را بدهد، مطالب جامع‌تری را در این زمینه آماده و منتشر می‌کنم.

گام پنجم: ذخیره سازی اطلاعات در فضای محلی یا Local
در این گام، تغییرات badge را در فضای ذخیره سازی سیستم Local نگهداری می‌نماییم؛ بطوری که اگر دوباره برنامه را راه اندازی نمودید، badge با داده‌های ذخیره شده در سیستم Local مقداردهی اولیه می‌گردد.
کتابخانه dart:convert را به منظور استفاده از کلاس مبدل JSON به فایل piratebadge.dart اضافه نمایید.
import 'dart:html';
import 'dart:math' show Random;

import 'dart:convert' show JSON;
همچنین یک Named Constructor یا سازنده‌ی با نام را به کلاس PirateName بصورت زیر اضافه کنید.
class PirateName {
  ...
  PirateName.fromJSON(String jsonString) {
    Map storedName = JSON.decode(jsonString);
    _firstName = storedName['f'];
    _appellation = storedName['a'];
  }
}
توضیحات
- جهت کسب اطلاعات بیشتر در مورد Json به این لینک مراجعه نمایید
- کلاس JSON جهت کار با داده هایی به فرمت Json استفاده می‌شود که امکاناتی را جهت دسترسی سریعتر و راحت‌تر به این داده‌ها فراهم می‌کند.
- سازنده‌ی PirateName.fromJSON، از یک رشته حاوی داده‌ی Json، یک نمونه از کلاس PirateName ایجاد می‌کند.
- سازنده‌ی PirateName.fromJSON، یک Named Constructor می‌باشد. این نوع سازنده‌ها دارای نامی متفاوت از نام سازنده‌های معمول هستند و بصورت خودکار نمونه ای از کلاس مورد نظر را ایجاد نموده و به عنوان خروجی بر می‌گردانند.
- تابع JSON.decode یک رشته‌ی حاوی داده‌ی Json را تفسیر نموده و اشیاء Dart را از آن ایجاد می‌کند.
یک Getter به کلاس PirateName اضافه کنید که مقادیر ویژگی‌های آن را به یک رشته Json تبدیل می‌کند
class PirateName {
  ...
  String get jsonString => JSON.encode({"f": _firstName, "a": _appellation});
}
جهت ذخیره سازی آخرین تغییرات کلاس PirateName در فضای ذخیره سازی Local، از یک کلید استفاده می‌کنیم که مقدار آن محتوای PirateName می‌باشد. در واقع فضای ذخیره سازی Local داده‌ها را به صورت جفت کلید-مقدار یا Key-Value Pairs نگهداری می‌نماید. جهت تعریف کلید، یک متغیر رشته ای را بصورت top-level و به شکل زیر تعریف کنید.
final String TREASURE_KEY = 'pirateName';

void main() {
  ...
}
زمانیکه تغییری در badge name صورت گرفت، این تغییرات را در فضای ذخیره سازی Local، توسط ویژگی window.localStorage ذخیره می‌نماییم. تغییرات زیر را در تابع setBadgeName اعمال نمایید
void setBadgeName(PirateName newName) {
  if (newName == null) {
    return;
  }
  querySelector('#badgeName').text = newName.pirateName;
  window.localStorage[TREASURE_KEY] = newName.jsonString;
}
تابع getBadgeNameFromStorage را بصورت top-level تعریف نمایید. این تابع داده‌های ذخیره شده را از Local Storage بازیابی نموده و یک شی از نوع کلاس PirateName ایجاد می‌نماید.
void setBadgeName(PirateName newName) {
  ...
}

PirateName getBadgeNameFromStorage() {
  String storedName = window.localStorage[TREASURE_KEY];
  if (storedName != null) {
    return new PirateName.fromJSON(storedName);
  } else {
    return null;
  }
}
در پایان نیز تابع setBadgeName را به منظور مقدار دهی اولیه به badge name، در تابع main، فراخوانی می‌نماییم.
void main() {
  ...
  setBadgeName(getBadgeNameFromStorage());
}
حال به مانند گامهای قبل برنامه را اجرا و بررسی نمایید.

گام ششم: خواندن نام‌ها از فایل‌های ذخیره شده به فرمت Json
در این گام کلاس PirateName را به گونه‌ای تغییر می‌دهیم که نام‌ها را از فایل Json بخواند. این عمل موجب می‌شود تا به راحتی اسامی مورد نظر را به فایل اضافه نمایید تا توسط کلاس خوانده شوند، بدون آنکه نیاز باشد کد کلاس را مجددا دستکاری کنید.
به منوی File > New File... مراجعه نموده و فایل piratenames.json را با محتوای زیر ایجاد نمایید. این فایل را در پوشه 1-blankbadge و در کنار فایلهای HTML و Dart ایجاد کنید.
{ "names": [ "Anne", "Bette", "Cate", "Dawn",
        "Elise", "Faye", "Ginger", "Harriot",
        "Izzy", "Jane", "Kaye", "Liz",
        "Maria", "Nell", "Olive", "Pat",
        "Queenie", "Rae", "Sal", "Tam",
        "Uma", "Violet", "Wilma", "Xana",
        "Yvonne", "Zelda",
        "Abe", "Billy", "Caleb", "Davie",
        "Eb", "Frank", "Gabe", "House",
        "Icarus", "Jack", "Kurt", "Larry",
        "Mike", "Nolan", "Oliver", "Pat",
        "Quib", "Roy", "Sal", "Tom",
        "Ube", "Val", "Walt", "Xavier",
        "Yvan", "Zeb"],
  "appellations": [ "Awesome", "Captain",
        "Even", "Fighter", "Great", "Hearty",
        "Jackal", "King", "Lord",
        "Mighty", "Noble", "Old", "Powerful",
        "Quick", "Red", "Stalwart", "Tank",
        "Ultimate", "Vicious", "Wily", "aXe", "Young",
        "Brave", "Eager",
        "Kind", "Sandy",
        "Xeric", "Yellow", "Zesty"]}
این فایل شامل یک شی Json با دو لیست رشته ای می‌باشد.
به فایل piratebadge.html مراجعه نمایید و فیلد input و المنت button را غیر فعال نمایید.
...
  <div>
    <input type="text" id="inputName" maxlength="15" disabled>
  </div>
  <div>
    <button id="generateButton" disabled>Aye! Gimme a name!</button>
  </div>
...
این دو المنت پس از اینکه تمامی نام‌ها از فایل Json با موفقیت خوانده شدند فعال می‌گردند.
کتابخانه dart:async را در ابتدای فایل دارت import نمایید
import 'dart:html';
import 'dart:math' show Random;
import 'dart:convert' show JSON;

import 'dart:async' show Future;
توضیحات
- کتابخانه dart:async برنامه نویسی غیر همزمان یا asynchronous را فراهم می‌کند
- کلاس Future روشی را ارئه می‌کند که در آن مقادیر مورد نیاز در آینده ای نزدیک و به صورت غیر همزمان واکشی خواهند شد.
در مرحله بعد لیست‌های names و appellations را با کد زیر بصورت یک لیست خالی جایگزین نمایید.
class PirateName {
  ...
  static List<String> names = [];
  static List<String> appellations = [];
  ...
}
توضیحات
- مطمئن شوید که کلمه کلیدی final را از تعاریف فوق حذف نموده اید
- [] معادل new List() می‌باشد
- کلاس List یک نوع Generic می‌باشد که می‌تواند شامل هر نوع شی ای باشد. اگر می‌خواهید که لیست شما فقط شامل داده هایی از نوع String باشد، آن را بصورت List<String> تعریف نمایید.
دو تابع static را بصورت زیر به کلاس PirateName اضافه نمایید
class PirateName {
  ...

  static Future readyThePirates() {
    var path = 'piratenames.json';
    return HttpRequest.getString(path)
        .then(_parsePirateNamesFromJSON);
  }
  
  static _parsePirateNamesFromJSON(String jsonString) {
    Map pirateNames = JSON.decode(jsonString);
    names = pirateNames['names'];
    appellations = pirateNames['appellations'];
  }
}
توضیحات
- کلاس HttpRequest یک Utility می‌باشد که داده‌ها را از یک آدرس یا URL خاص واکشی می‌نماید.
- تابع getString یک درخواست را به صورت GET ارسال می‌نماید و رشته ای را بر می‌گرداند
- در کد فوق از کلاس Future استفاده شده است که موجب می‌شود درخواست GET بصورت غیر همزمان ارسال گردد.
- زمانیکه Future با موفقیت خاتمه یافت، تابع then فراخوانی می‌شود. پارامتر ورودی این تابع، یک تابع می‌باشد که پس از خاتمه درخواست GET اجرا خواهد شد. به این نوع توابع که پس از انجام یک عملیات خاص بصورت خودکار اجرا می‌شوند توابع CallBack می‌گویند.
- زمانیکه Future با موفقیت خاتمه یافت، اسامی از فایل Json خوانده خواهند شد
- تابع readyThePirates دارای نوع خروجی Future می‌باشد بطوری که برنامه اصلی در زمانی که فایلها در حال خوانده شدن هستند، به کار خود ادامه میدهد و متوقف نخواهد شد
یک متغیر top-level از نوع SpanElement در کلاس PirateName ایجاد کنید.
SpanElement badgeNameElement;

void main() {
  ...
}
تغییرات زیر را در تابع main ایجاد کنید.
void main() {
  InputElement inputField = querySelector('#inputName');
  inputField.onInput.listen(updateBadge);
  genButton = querySelector('#generateButton');
  genButton.onClick.listen(generateBadge);
  
  badgeNameElement = querySelector('#badgeName');
  ...
}
کد زیر را نیز به منظور خواندن نام‌ها از فایل Json اضافه کنید. در این کد اجرای موفقیت آمیز درخواست و عدم اجرای درخواست، هر دو به شکلی مناسب مدیریت شده اند. 
void main() {
  ...
  
  PirateName.readyThePirates()
    .then((_) {
      //on success
      inputField.disabled = false; //enable
      genButton.disabled = false;  //enable
      setBadgeName(getBadgeNameFromStorage());
    })
    .catchError((arrr) {
      print('Error initializing pirate names: $arrr');
      badgeNameElement.text = 'Arrr! No names.';
    });
}
توضیحات
- تابع readyThePirates فراخوانی شده است که یک Future بر می‌گرداند.
- زمانی که Future با موفقیت خاتمه یافت تابع CallBack موجود در تابع then فراخوانی می‌شود.
- (_) به عنوان پارامتر ورودی تابع then ارسال شده است، به این معنا که از پارامتر ورودی صرف نظر شود.
- تابع then المنت‌های صفحه را فعال می‌کند و داده‌های ذخیره شده را بازیابی می‌نماید
- اگر Future با خطا مواجه شود، توسط تابع catchError که یک تابع CallBack می‌باشد، پیغام خطایی را نمایش می‌دهیم.
برنامه را به مانند گامهای قبل اجرا نموده و نتیجه را مشاهده نمایید
مطالب
آموزش MDX Query - قسمت دهم – ادامه کار برروی ساختار های سلسله مراتبی و کار با تابع Cousin و ایجاد Range

در این قسمت در خصوص توابع مرتبط با ساختار سلسله مراتبی صحبت خواهد شد.

Select
{
  [Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
  cousin(
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
[Date].[Calendar].[Calendar Year].[CY 2007]
)
} on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

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

خروجی به صورت زیر می‌باشد:

خوب حالا به ساختار زیر دقت کنید (ساختار سلسله مراتبی Date  )

همانطور که مشخص می‌باشد تاریخ‌ها از 2005 تا 2008 و سال 2010 می‌باشند و فصول عبارتند از دو فصل پایانی سال 2005 و تمامی فصول سال 2006 و 2007 و سه فصل اول سال 2008 و فصل چهارم سال 2010 . حال دوباره به کوئری نوشته شده دقت کنید. در کوئری بالا فصل همسطح فصل اول  سال 2006 در سال 2007 مورد واکشی قرار گرفته است که همان فصل اول در سال 2007 می‌باشد.

حال به بررس کوئری زیر خواهیم پرداخت:

Select
{
  [Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
  cousin(
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
[Date].[Calendar].[Calendar Semester].[H2 CY 2006]
)
} on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

در این کوئری ما ابتدا ستون فصل اول سال 2006 را بر می گردانیم . سپس در تابع پسر عمو در نیم فصل دوم سال 2006 به دنبال هم سطح فصل اول 2006 می گردیم .

نمودار درختی زیر توضیح کاملی به ما خواهد داد:

حال برای ادامه‌ی مطلب کار بر روی ساختار‌های سلسله مراتبی، ابتدا باید در خصوص نحوه‌ی ایجاد Range توضیحاتی ارایه گردد. دو کوئری زیر را در نظر گرفته و خروجی آنها را با هم مقایسه نمایید

Select
{
  [Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
  [Date].[Calendar].[Calendar Quarter].[Q2 CY 2006],
  [Date].[Calendar].[Calendar Quarter].[Q3 CY 2006]
} on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

و

Select
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006]:
[Date].[Calendar].[Calendar Quarter].[Q3 CY 2006]
on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

خروجی‌ها به صورت زیر می‌باشد :

و

مشخص می‌باشد که از علامت <:> برای ایجاد یک محدوده و جلوگیری از تولید کد‌های بلند و طولانی استفاده می‌شود.

حال کوئری زیر را اجرا کنید:

Select
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006]
:
cousin(
[Date].[Calendar].[Calendar Quarter].[Q1 CY 2006],
[Date].[Calendar].[Calendar Semester].[H2 CY 2006]
    ) on columns,
[Measures].[Reseller Sales Amount] on rows
From [Adventure Works]

در این کوئری در ابتدا تابع پسر عمو اجرا می گردد، سپس تابع رنج اجرا می گردد و در نتیجه، فاصله ی بین  Q1 CY 2006 تا Q3 CY 2006 را بدست می‌آورد.

 نمودار درختی زیر توضیح کاملی به ما خواهد داد :

خروجی به صورت زیر می‌باشد

در قسمت‌های بعدی دیگر توابع MDX Query‌ها را بررسی میکنیم.

مطالب
بررسی روش تعریف انقیاد دو طرفه‌ی سفارشی در کامپوننت‌های Angular
برخلاف AngularJS، در برنامه‌های Angular امکانات two way data binding به صورت پیش‌فرض ارائه نمی‌شوند تا از تمام مشکلات آن مانند digest cycle ،watchers و غیره خبری نباشد. اما گاهی از اوقات نیاز است انقیاد دو طرفه‌ی سفارشی را بین دو کامپوننت ایجاد کنیم. در این مطلب روش ایجاد یک چنین انقیادهایی را بررسی خواهیم کرد و در اینجا در ابتدا نیاز است دو پیشنیاز Property Binding و Event Binding را بررسی کنیم که از جمع آن‌ها two way data binding حاصل می‌شود:


البته Angular به همراه دایرکتیو ویژه‌ای به نام ngModel است که two-way data binding را با import ماژول ویژه‌ی فرم‌ها میسر می‌کند:


که آن نیز در اصل از جمع Property Binding و Event Binding تشکیل شده‌است:
<input [ngModel]="username" (ngModelChange)="username = $event">
و یا به صورت خلاصه:
<input [(ngModel)]='username' />
در اینجا می‌خواهیم یک چنین امکانی را بدون استفاده از ngModel و ماژول فرم‌ها پیاده سازی کنیم.


انقیاد به خواص یا Property binding

فرض کنید دو کامپوننت والد و فرزند را ایجاد کرده‌ایم:


در کامپوننت والد، مقداری را توسط متد deposit هربار 100 آیتم افزایش می‌دهیم:
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-parent",
  templateUrl: "./parent.component.html",
  styleUrls: ["./parent.component.css"]
})
export class ParentComponent implements OnInit {

  amount = 500;

  constructor() { }

  ngOnInit() {
  }

  deposit() {
    this.amount += 100;
  }
}
با این قالب:
<h2>Custom two way data binding</h2>

<div class="panel panel-primary">
  <div class="panel-heading">
    <h2 class="panel-title">Parnet Component</h2>
  </div>
  <div class="panel-body">
    <label>Available amount:</label> {{amount}}
    <button (click)="deposit()" class="btn btn-success">Deposit 100</button>
    <div>
      <app-child [amount]="amount"> </app-child>
    </div>
  </div>
</div>
که در آن مقدار amount کامپوننت والد نمایش داده شده‌است و همچنین این مقدار به خاصیت ورودی کامپوننتی به نام app-child نیز نسبت داده شده‌است.

کامپوننت فرزند به صورت ذیل تعریف می‌شود:
import { Component, OnInit, Input } from "@angular/core";

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent implements OnInit {

  @Input() amount: number;

  constructor() { }

  ngOnInit() {
  }

  withdraw() {
    this.amount -= 100;
  }
}
که در آن خاصیت amount، از والد آن، توسط ویژگی Input دریافت می‌شود. سپس در متد withdraw هربار می‌توان 100 آیتم را از آن کسر کرد.
با این قالب:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">Child Component</h2>
  </div>
  <div class="panel-body">
    <label>Amount available: </label> {{amount}}

    <button (click)="withdraw()" class="btn btn-danger">Withdraw 100</button>
  </div>
</div>
که در آن مقدار amount فرزند نمایش داده شده‌است و همچنین امکان فراخوانی متد withdraw وجود دارد.

در اینجا زمانیکه data binding را به صورت ذیل تعریف می‌کنیم:
<app-child [amount]="amount"> </app-child>
روش مقدار دهی خاصیت amount داخل [] ، انقیاد به خواص نامیده می‌شود و سمت راست آن نیز یک خاصیت درنظر گرفته می‌شود. یعنی مقدار خاصیت amount والد (درون "") به مقدار خاصیت amount فرزند (درون []) نسبت داده خواهد شد.
این ارتباط نیز یک طرفه‌است. برای مثال اگر بر روی دکمه‌ی Deposit والد کلیک کنیم:


مقدار افزایش یافته‌ی در والد، به فرزند نیز منتقل می‌شود و نمایش داده خواهد شد. اما اگر بر روی دکمه‌ی withdraw فرزند کلیک کنیم:


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


انقیاد به رخ‌دادها یا Event binding

یک کامپوننت می‌تواند به رخ‌دادهای صادر شده‌ی توسط کامپوننتی دیگر گوش فرا دهد:
import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core";

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent implements OnInit {

  @Input() amount: number;
  @Output() amountChange = new EventEmitter();

  constructor() { }

  ngOnInit() {
  }

  withdraw() {
    this.amount -= 100;
    this.amountChange.emit(this.amount);
  }
}
برای این منظور در کامپوننت فرزند، یک خاصیت Output را به نام amountChange از نوع EventEmitter تعریف می‌کنیم. سپس جایی که قرار است کار کاهش amount صورت گیرد، با صدور رخ‌دادی (this.amountChange.emit)، این مقدار را به والد اعلام می‌کنیم.
اکنون در قالب کامپوننت والد، این رخ‌داد را درون یک () معرفی خواهیم کرد:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
به این ترتیب زمانیکه کامپوننت فرزند، مقدار amount را تغییر می‌دهد، این مقدار توسط this.amountChange.emit به والد منتشر خواهد شد و می‌توان در سمت والد توسط event$ به آن دسترسی یافته و آن‌را به خاصیت this.amount کامپوننت والد نسبت دهیم.
اکنون اگر برنامه را آزمایش کنیم، با کلیک بر روی دکمه‌ی withdraw فرزند، مقدار کاهش یافته به والد نیز منعکس می‌شود:



پیاده سازی syntax ویژه‌ی Banana in a box

تا اینجا پیاده سازی two way data-binding سفارشی به پایان می‌رسد. اما تعریف طولانی:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
به صورت ذیل هم قابل نوشتن و ساده سازی است:
<app-child [(amount)]="amount"> </app-child>
که به آن syntax ویژه Banana in a box نیز گفته می‌شود.
نکته‌ی ویژه‌ی آن، وجود پسوند Change در نام رخ‌داد تعریف شده‌است:
  @Input() amount: number;
  @Output() amountChange = new EventEmitter();
 اگر نام خاصیت Input مساوی x باشد، باید جهت فعالسازی syntax ویژه Banana in a box، نام رخ‌داد متناظر با آن دقیقا مساوی xChange انتخاب شود. مانند amount ورودی در اینجا و amountChange خروجی تعریف شده.

بنابراین به صورت خلاصه جهت تعریف یک انقیاد دو طرفه سفارشی:
- ابتدا باید انقیاد به یک خاصیت ورودی x را تعریف کرد.
- سپس نیاز است انقیاد به یک رخ‌داد خروجی هم‌نام، که نام آن، پسوند Change را اضافه‌تر دارد، یعنی xChange را تعریف کرد.
- اکنون می‌توان two-way data binding syntax ویژه‌ای را به نام banana in a box بر روی این‌دو تعریف کرد[(x)].


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت چهارم
دو برنامه Mono Shared Runtime و Xamarin Android Support هستن. این دو، آیکون ای در Home Screen گوشی/امولاتور شما اضافه نمی‌کنند، بلکه فقط در قسمت Settings سیستم عامل، در جایی که تمامی App‌ها لیست شده اند، دیده می‌شوند. مطمئنا هر دو رو دارید که الآن تونستید دیباگ کنید. این که تغییر آنی Xaml روی امولاتور کار می‌کنه، ولی روی گوشی نه، احتمالا به خاطر این هست که گوشی و کامپیوتر تو یه شبکه نیستن، یا Firewall ای چیزی در بین هست. اعمال آنی تغییرات CSharp هم مختص UWP هست و در Android/iOS کار نمی‌کنه.
نظرات مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی
- سازنده‌ی کلاس سرویس Auth هست که کار اطلاع رسانی به کامپوننت هدر را انجام می‌دهد. علت قرار گرفتن این قطعه کد، در هدر هم دقیقا اجرای آن با ریفرش صفحه است. بنابراین این قسمت و همچنین مشترکین به BehaviorSubject آن‌را بررسی کنید:
 constructor() {
    this.updateStatusOnPageRefresh();
  }
- اگر با وجود مقادیر token در کش مرورگر، این مقادیر کار نمی‌کنند، نیاز به پیاده سازی «نکته‌ی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور» را دارید. ری استارت برنامه = تولید کلیدهای رمزنگاری جدید = غیرمعتبر شدن اطلاعات سمت کاربر و برگشت خوردن آن‌ها در سمت سرور
نظرات مطالب
افزودن یک DataType جدید برای نگه‌داری تاریخ خورشیدی - 3
public DateTime AddDate { set; get; }  

[NotMapped]  // فیلد محاسباتی سمت برنامه که در بانک اطلاعاتی قرار نخواهد گرفت
public string PersianDate
{  
    get { return MyDateConverter(AddDate); }  
}

در code first از ویژگی NotMapped استفاده کنید تا بتونید با استفاده از تابع کمکی تبدیل تاریخ خودتون راحت این تبدیلات رو انجام بدید. در بانک اطلاعاتی DateTime استاندارد رو ذخیره کنید، در سمت کدها برای مسایل نمایشی از خاصیت PersianDate که NotMapped تعریف شده، میشه استفاده کرد. به علاوه روی DateTime استاندارد راحت میشه کوئری‌های LINQ رو اجرا کرد بدون محدودیت. زمانیکه مثلا یک List تهیه شد، قسمت بعدی، کار نمایشی است که از خاصیت NotMapped میشه کمک گرفت. این روش با تمام بانک‌های اطلاعاتی کار می‌کنه.

نظرات مطالب
ASP.NET MVC #6
- به قسمت کار با Ajax در ASP.NET MVC که برسیم، کتابخانه برگزیده، jQuery است. بنابراین لازم است از همین الان اطلاعاتی را در این مورد داشته باشید (^).
- برای طراحی CSS بین برنامه نویس‌های دات نت، فریم ورکی به نام LESS خیلی محبوبیت دارد (^).
- کتابخانه‌های جاوا اسکریپتی سمت کلاینت معنا پیدا می‌کنند. بنابراین آنچنان وارد بحث ASP.NET که سمت سرور است نمی‌شوند مگر اینکه کمک حالی در این رابطه باشند مانند jQuery. یا البته جاوا اسکریپت سمت سرور هم به نام نودجی‌اس وجود دارد که بحث دیگری است. در کل حین کار با جاوا اسکریپت دست بازتر است چون داخل مرورگر کاربر اجرا می‌شود که stateful است (مثل برنامه‌های سیلورلایت). به همین جهت کتابخانه MVVM هم برای جاوا اسکریپت وجود دارد (^). جالب اینجا است که این کتابخانه MVVM توسط یکی از اعضای تیم ASP.NET MVC طراحی شده.
بازخوردهای پروژه‌ها
مدیریت پروژه
قبل از هر چیز سپاس گذارم از وب سایت بسیار قوی شما 
صحبتم را با یک مثال پیش می‌برم 
فرض کنیم یک پروژه کوچک مثل فروشگاه اینترنتی یا برنامه مدیرت املاک را یک تیم 4 نفره می‌خواهند انجام دهند پروژه باید چگونه تقسیم بندی بشه . حالا تقسیم بندی شد هر کسی در سیستم خودش داره قسمت مورد نظر را توسعه میده پایان کار چگونه باید پروژه اسمبل بشه و مباحثی در مورد مدیرت پروژه به صورت کاربردی مثل مطالب سایتتون البته بازم تکرار می‌کنم من مباحثی مانند Scrum و Uml نیاز ندارم . مدیرت پروژه تیمی ونحوه کار هر یک از اعضا - و نهایتا چگونگی اسمبل کردن پروژه در یک vs . با تشکر 
پروژه‌ها
مشاهده گر سایت dotnettips
پروژه انجام شده، برنامه ای برای خواندن مطالب همین سایت با قابلیت هایی مانند جستجو و تم‌های مختلف مخصوصا Dark می‌باشد.
از این پروژه می‌توانید هم به صورت آفلاین و یا آنلاین از آدرس زیر استفاده کنید.
توضیحات و عکس از پروژه را می‌توانید در آدرس مخزن پروژه ببینید.
موارد قابل بهبود :
  • قسمت (بر اساس تگ) به درستی کار نمی‌کند.
  • پروژه بر اساس فایل json سایت کار می‌کند که می‌توان آن را به xml تغییر داد.
  • سرعت لود مطالب به علت سنگین بودن فایل سایت جاری کمی در نسخه آنلاین طول می‌کشد.