مطالب دوره‌ها
استفاده از 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
مسیرراه‌ها
ASP.NET MVC
              مطالب
              Blazor 5x - قسمت 14 - کار با فرم‌ها - بخش 2 - تعریف فرم‌ها و اعتبارسنجی آن‌ها
              در ادامه قصد داریم از سرویس زیر که در قسمت قبل تکمیل شد، در یک برنامه‌ی Blazor Server استفاده کنیم:
              namespace BlazorServer.Services
              {
                  public interface IHotelRoomService
                  {
                      Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO);
              
                      Task<int> DeleteHotelRoomAsync(int roomId);
              
                      IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync();
              
                      Task<HotelRoomDTO> GetHotelRoomAsync(int roomId);
              
                      Task<HotelRoomDTO> IsRoomUniqueAsync(string name);
              
                      Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO);
                  }
              }


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


              در ابتدا کامپوننت‌های خالی نمایش لیست اتاق‌ها و همچنین فرم خالی ثبت و ویرایش آن‌ها را به همراه مسیریابی‌های مرتبط، ایجاد می‌کنیم. به همین جهت ابتدا داخل پوشه‌ی Pages، پوشه‌ی جدید HotelRoom را ایجاد کرده و فایل جدید HotelRoomList.razor را با محتوای ابتدایی زیر، به آن اضافه می‌کنیم.
              @page "/hotel-room"
              
              <div class="row mt-4">
                  <div class="col-8">
                      <h4 class="card-title text-info">Hotel Rooms</h4>
                  </div>
                  <div class="col-3 offset-1">
                      <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink>
                  </div>
              </div>
              
              @code {
              
              }
              این کامپوننت در مسیر hotel-room/ قابل دسترسی خواهد بود. بر این اساس، به کامپوننت Shared\NavMenu.razor مراجعه کرده و مدخل منوی آن‌را تعریف می‌کنیم:
              <li class="nav-item px-3">
                  <NavLink class="nav-link" href="hotel-room">
                      <span class="oi oi-list-rich" aria-hidden="true"></span> Hotel Rooms
                  </NavLink>
              </li>

              تا اینجا صفحه‌ی ابتدایی نمایش لیست اتاق‌ها، به همراه یک دکمه‌ی افزودن اتاق جدید نیز هست. به همین جهت فایل جدید Pages\HotelRoom\HotelRoomUpsert.razor را به همراه مسیریابی hotel-room/create/ برای تعریف کامپوننت ابتدایی ثبت و ویرایش اطلاعات اتاق‌ها، اضافه می‌کنیم:
              @page "/hotel-room/create"
              
              <h3>HotelRoomUpsert</h3>
              
              @code {
              
              }
              - واژه‌ی Upsert در مورد فرمی بکاربرده می‌شود که هم برای ثبت اطلاعات و هم برای ویرایش اطلاعات از آن استفاده می‌شود.
              - NavLink تعریف شده‌ی در کامپوننت نمایش لیست اتاق‌ها، به مسیریابی کامپوننت فوق اشاره می‌کند.


              ایجاد فرم ثبت یک اتاق جدید

              برای ثبت یک اتاق جدید نیاز است به مدل UI آن که همان HotelRoomDTO تعریف شده‌ی در قسمت قبل است، دسترسی داشت. به همین جهت در پروژه‌ی BlazorServer.App، ارجاعی را به پروژه‌ی BlazorServer.Models.csproj اضافه می‌کنیم:
              <Project Sdk="Microsoft.NET.Sdk.Web">
                <ItemGroup>
                  <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" />
                </ItemGroup>
              </Project>
              سپس جهت سراسری اعلام کردن فضای نام آن، یک سطر زیر را به انتهای فایل BlazorServer.App\_Imports.razor اضافه می‌کنیم:
              @using BlazorServer.Models
              اکنون می‌توانیم کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor را به صورت زیر تکمیل کنیم:
              @page "/hotel-room/create"
              
              <div class="row mt-2 mb-5">
                  <h3 class="card-title text-info mb-3 ml-3">@Title Hotel Room</h3>
                  <div class="col-md-12">
                      <div class="card">
                          <div class="card-body">
                              <EditForm Model="HotelRoomModel">
                                  <div class="form-group">
                                      <label>Name</label>
                                      <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText>
                                  </div>
                              </EditForm>
                          </div>
                      </div>
                  </div>
              </div>
              
              @code
              {
                  private HotelRoomDTO HotelRoomModel = new HotelRoomDTO();
                  private string Title = "Create";
              }
              توضیحات:
              - در برنامه‌های Blazor، کامپوننت ویژه‌ی EditForm را بجای تگ استاندارد form، مورد استفاده قرار می‌دهیم.
              - این کامپوننت، مدل فرم را از فیلد HotelRoomModel که در قسمت کدها تعریف کردیم، دریافت می‌کند. کار آن تامین اطلاعات فیلدهای فرم است.
              - سپس در EditForm تعریف شده، بجای المان استاندارد input، از کامپوننت InputText برای دریافت اطلاعات متنی استفاده می‌شود. با bind-value@ در قسمت چهارم این سری بیشتر آشنا شدیم و کار آن two-way data binding است. در اینجا هر اطلاعاتی که وارد می‌شود، سبب به روز رسانی خودکار مقدار خاصیت HotelRoomModel.Name می‌شود و برعکس.

              یک نکته: در قسمت قبل، مدل UI را از نوع رکورد C# 9.0 و init only تعریف کردیم. رکوردها، با EditForm و two-way databinding آن سازگاری ندارند (bind-value@ در اینجا) و بیشتر برای کنترلرهای برنامه‌های Web API که یکبار قرار است کار وهله سازی آن‌ها در زمان دریافت اطلاعات از کاربر صورت گیرد، مناسب هستند و نه با فرم‌های پویای Blazor. به همین جهت به پروژه‌ی BlazorServer.Models مراجعه کرده و نوع آن‌ها را به کلاس و init‌ها را به set معمولی تغییر می‌دهیم تا در فرم‌های Blazor هم قابل استفاده شوند.

              تا اینجا کامپوننت ثبت اطلاعات یک اتاق جدید، چنین شکلی را پیدا کرده‌است:



              تکمیل سایر فیلدهای فرم ورود اطلاعات اتاق

              پس از تعریف فیلد ورود اطلاعات نام اتاق، سایر فیلدهای متناظر با HotelRoomDTO را نیز به صورت زیر به EditForm تعریف شده اضافه می‌کنیم که در اینجا از InputNumber برای دریافت اطلاعات عددی و از InputTextArea، برای دریافت اطلاعات متنی چندسطری استفاده شده‌است:
              <EditForm Model="HotelRoomModel">
                  <div class="form-group">
                      <label>Name</label>
                      <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText>
                  </div>
                  <div class="form-group">
                      <label>Occupancy</label>
                      <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber>
                  </div>
                  <div class="form-group">
                      <label>Rate</label>
                      <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber>
                  </div>
                  <div class="form-group">
                      <label>Sq ft.</label>
                      <InputText @bind-Value="HotelRoomModel.SqFt" class="form-control"></InputText>
                  </div>
                  <div class="form-group">
                      <label>Details</label>
                      <InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea>
                  </div>
                  <div class="form-group">
                      <button class="btn btn-primary">@Title Room</button>
                      <NavLink href="hotel-room" class="btn btn-secondary">Back to Index</NavLink>
                  </div>
              </EditForm>
              با این خروجی:



              تعریف اعتبارسنجی‌های فیلدهای یک فرم Blazor

              در حین تعریف یک فرم، برای واکنش نشان دادن به دکمه‌ی submit، می‌توان رویداد OnSubmit را به کامپوننت EditForm اضافه کرد که سبب فراخوانی متدی در قسمت کدهای کامپوننت جاری خواهد شد؛ مانند فراخوانی متد HandleHotelRoomUpsert در مثال زیر:
              <EditForm Model="HotelRoomModel" OnSubmit="HandleHotelRoomUpsert">
              </EditForm>
              
              @code
              {
                  private HotelRoomDTO HotelRoomModel = new HotelRoomDTO();
              
                  private async Task HandleHotelRoomUpsert()
                  {
              
                  }
              }
              هرچند HotelRoomDTO تعریف شده به همراه تعریف اعتبارسنجی‌هایی مانند Required است، اما اگر بر روی دکمه‌ی submit کلیک کنیم، متد HandleHotelRoomUpsert فراخوانی می‌شود. یعنی روال رویدادگردان OnSubmit، صرفنظر از وضعیت اعتبارسنجی مدل فرم، همواره با submit فرم، اجرا می‌شود.
              اگر این مورد، مدنظر نیست، می‌توان بجای OnSubmit، از رویداد OnValidSubmit استفاده کرد. در این حالت اگر اعتبارسنجی مدل فرم با شکست مواجه شود، دیگر متد HandleHotelRoomUpsert فراخوانی نخواهد شد. همچنین در این حالت می‌توان خطاهای اعتبارسنجی را نیز در فرم نمایش داد:
              <EditForm Model="HotelRoomModel" OnValidSubmit="HandleHotelRoomUpsert">
                  <DataAnnotationsValidator />
                  @*<ValidationSummary />*@
                  <div class="form-group">
                      <label>Name</label>
                      <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText>
                      <ValidationMessage For="()=>HotelRoomModel.Name"></ValidationMessage>
                  </div>
                  <div class="form-group">
                      <label>Occupancy</label>
                      <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber>
                      <ValidationMessage For="()=>HotelRoomModel.Occupancy"></ValidationMessage>
                  </div>
                  <div class="form-group">
                      <label>Rate</label>
                      <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber>
                      <ValidationMessage For="()=>HotelRoomModel.RegularRate"></ValidationMessage>
                  </div>
              - در اینجا قسمت‌های تغییر کرده را مشاهده می‌کنید که به همراه درج DataAnnotationsValidator و ValidationMessage‌ها است.
              - کامپوننت DataAnnotationsValidator، اعتبارسنجی مبتنی بر data annotations را مانند [Required]، در دامنه‌ی دید یک EditForm فعال می‌کند.
              - اگر خواستیم تمام خطاهای اعتبارسنجی را به صورت خلاصه‌ای در بالای فرم نمایش دهیم، می‌توان از کامپوننت ValidationSummary استفاده کرد.
              - و یا اگر خواستیم خطاها را به صورت اختصاصی‌تری ذیل هر تکست‌باکس نمایش دهیم، می‌توان از کامپوننت ValidationMessage کمک گرفت. خاصیت For آن از نوع <Expression<System.Func تعریف شده‌است که اجازه‌ی تعریف strongly typed نام خاصیت در حال اعتبارسنجی را به صورتی که مشاهده می‌کنید، میسر می‌کند.



              ثبت اولین اتاق هتل

              در ادامه می‌خواهیم روال رویدادگردان HandleHotelRoomUpsert را مدیریت کنیم. به همین جهت نیاز به کار با سرویس IHotelRoomService ابتدای بحث خواهد بود. بنابراین در ابتدا به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضای نام سرویس‌های برنامه را اضافه می‌کنیم:
              @using BlazorServer.Services
              اکنون امکان تزریق IHotelRoomService را که در قسمت قبل پیاده سازی و به سیستم تزریق وابستگی‌های برنامه معرفی کردیم، پیدا می‌کنیم:
              @page "/hotel-room/create"
              
              @inject IHotelRoomService HotelRoomService
              @inject NavigationManager NavigationManager
              
              
              @code
              {
                  private HotelRoomDTO HotelRoomModel = new HotelRoomDTO();
                  private string Title = "Create";
              
                  private async Task HandleHotelRoomUpsert()
                  {
                      var roomDetailsByName = await HotelRoomService.IsRoomUniqueAsync(HotelRoomModel.Name);
                      if (roomDetailsByName != null)
                      {
                          //there is a duplicate room. show an error msg.
                          return;
                      }
              
                      var createdResult = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
                      NavigationManager.NavigateTo("hotel-room");
                  }
              }
              در اینجا در ابتدا، سرویس IHotelRoomService به کامپوننت جاری تزریق شده و سپس از متدهای IsRoomUniqueAsync و CreateHotelRoomAsync آن، جهت بررسی منحصربفرد بودن نام اتاق و ثبت نهایی اطلاعات مدل برنامه که به فرم جاری به صورت دو طرفه‌ای متصل است، استفاده کرده‌ایم. در نهایت پس از ثبت اطلاعات، کاربر به صفحه‌ی نمایش لیست اتاق‌ها، توسط سرویس توکار NavigationManager، هدایت می‌شود.

              اگر پیشتر با ASP.NET Web Forms کار کرده باشید (اولین روش توسعه‌ی برنامه‌های وب در دنیای دات نت)، مدل برنامه نویسی Blazor Server، بسیار شبیه به کار با وب فرم‌ها است؛ البته بر اساس آخرین تغییرات دنیای دانت نت مانند برنامه نویسی async، کار با سرویس‌ها، تزریق وابستگی‌های توکار و غیره.


              نمایش لیست اتاق‌های ثبت شده


              تا اینجا موفق شدیم اطلاعات یک مدل اعتبارسنجی شده را در بانک اطلاعاتی ثبت کنیم. مرحله‌ی بعد، نمایش لیست اطلاعات ثبت شده‌ی در بانک اطلاعاتی است. بنابراین به کامپوننت HotelRoomList.razor مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
              @page "/hotel-room"
              
              @inject IHotelRoomService HotelRoomService
              
              <div class="row mt-4">
                  <div class="col-8">
                      <h4 class="card-title text-info">Hotel Rooms</h4>
                  </div>
                  <div class="col-3 offset-1">
                      <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink>
                  </div>
              </div>
              
              <div class="row mt-4">
                  <div class="col-12">
                      <table class="table table-bordered table-hover">
                          <thead>
                              <tr>
                                  <th>Name</th>
                                  <th>Occupancy</th>
                                  <th>Rate</th>
                                  <th>
                                      Sqft
                                  </th>
                                  <th>
              
                                  </th>
                              </tr>
                          </thead>
                          <tbody>
                              @if (HotelRooms.Any())
                              {
                                  foreach (var room in HotelRooms)
                                  {
                                      <tr>
                                          <td>@room.Name</td>
                                          <td>@room.Occupancy</td>
                                          <td>@room.RegularRate.ToString("c")</td>
                                          <td>@room.SqFt</td>
                                          <td></td>
                                      </tr>
                                  }
                              }
                              else
                              {
                                  <tr>
                                      <td colspan="5">No records found</td>
                                  </tr>
                              }
                          </tbody>
                      </table>
                  </div>
              </div>
              
              @code
              {
                  private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>();
              
                  protected override async Task OnInitializedAsync()
                  {
                      await foreach(var room in HotelRoomService.GetAllHotelRoomsAsync())
                      {
                          HotelRooms.Add(room);
                      }
                  }
              }
              توضیحات:
              - متد GetAllHotelRoomsAsync، لیست اتاق‌های ثبت شده را بازگشت می‌دهد. البته خروجی آن از نوع <IAsyncEnumerable<HotelRoomDTO است که از زمان C# 8.0 ارائه شد و روش کار با آن اندکی متفاوت است. IAsyncEnumerable‌ها را باید توسط await foreach پردازش کرد.
              - همانطور که در مطلب بررسی چرخه‌ی حیات کامپوننت‌ها نیز عنوان شد، متدهای رویدادگران OnInitialized و نمونه‌ی async آن برای دریافت اطلاعات از سرویس‌ها طراحی شده‌اند که در اینجا نمونه‌ای از آن‌را مشاهده می‌کنید.
              - پس از تشکیل لیست اتاق‌ها، حلقه‌ی foreach (var room in HotelRooms) تعریف شده، ردیف‌های آن‌را در UI نمایش می‌دهد.


              کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-14.zip
              مطالب
              اعتبارسنجی سایتهای چند زبانه در ASP.NET MVC - قسمت اول

              اگر در حال تهیه یک سایت چند زبانه هستید و همچنین سری مقالات Globalization در ASP.NET MVC رو دنبال کرده باشید میدانید که با تغییر Culture فایلهای Resource مورد نظر بارگذاری و نوشته‌های سایت تغییر میابند ولی با تغییر Culture رفتار اعتبارسنجی در سمت سرور نیز تغییر و اعتبارسنجی بر اساس Culture فعلی سایت انجام میگیرد. بررسی این موضوع را با یک مثال شروع میکنیم.

              یک پروژه وب بسازید سپس به پوشه Models یک کلاس با نام ValueModel اضافه کنید. تعریف کلاس به شکل زیر هست: 

              public class ValueModel
              {
                  [Required]
                  [Display(Name = "Decimal Value")]
                  public decimal DecimalValue { get; set; }
              
                  [Required]
                  [Display(Name = "Double Value")]
                  public double DoubleValue { get; set; }
              
                  [Required]
                  [Display(Name = "Integer Value")]
                  public int IntegerValue { get; set; }
              
                  [Required]
                  [Display(Name = "Date Value")]
                  public DateTime DateValue { get; set; }
              }

              به سراغ کلاس HomeController بروید و کدهای زیر را اضافه کنید: 

              [HttpPost]
              public ActionResult Index(ValueModel valueModel)
              {
                  if (ModelState.IsValid)
                  {
                      return Redirect("Index");
                  }
              
                  return View(valueModel);
              }

              Culture را به fa-IR تغییر میدهیم، برای اینکار در فایل web.config در بخش system.web کد زیر اضافه نمایید: 

              <globalization culture="fa-IR" uiCulture="fa-IR" />

              و در نهایت به سراغ فایل Index.cshtml بروید کدهای زیر رو اضافه کنید:

              @using (Html.BeginForm())
              {
                  <ol>
                      <li>
                          @Html.LabelFor(m => m.DecimalValue)
                          @Html.TextBoxFor(m => m.DecimalValue)
                          @Html.ValidationMessageFor(m => m.DecimalValue)
                      </li>
                      <li>
                          @Html.LabelFor(m => m.DoubleValue)
                          @Html.TextBoxFor(m => m.DoubleValue)
                          @Html.ValidationMessageFor(m => m.DoubleValue)
                      </li>
                      <li>
                          @Html.LabelFor(m => m.IntegerValue)
                          @Html.TextBoxFor(m => m.IntegerValue)
                          @Html.ValidationMessageFor(m => m.IntegerValue)
                      </li>
                      <li>
                          @Html.LabelFor(m => m.DateValue)
                          @Html.TextBoxFor(m => m.DateValue)
                          @Html.ValidationMessageFor(m => m.DateValue)
                      </li>
                      <li>
                          <input type="submit" value="Submit"/>
                      </li>
                  </ol>
              }

              پرژه را اجرا نمایید و در ٢ تکست باکس اول ٢ عدد اعشاری را و در ٢ تکست باکس آخر یک عدد صحیح و یک تاریخ وارد نمایید و سپس دکمه Submit را بزنید. پس از بازگشت صفحه از سمت سرور در در ٢ تکست باکس اول با این پیامها روبرو میشوید که مقادیر وارد شده نامعتبر میباشند. 

              اگر پروژه رو در حالت دیباگ اجرا کنیم و نگاهی به داخل ModelState بیاندازیم، میبینیم که کاراکتر جدا کننده قسمت اعشاری برای fa-IR '/' میباشد که در اینجا برای اعداد مورد نظر کاراکتر '.' وارد شده است. 

              برای فایق شدن بر این مشکل یا باید سمت سرور اقدام کرد یا در سمت کلاینت. در بخش اول راه حل سمت کلاینت را بررسی مینماییم. 

              در سمت کلاینت برای اینکه کاربر را مجبور به وارد کردن کاراکترهای مربوط به Culture فعلی سایت نماییم باید مقادیر وارد شده را اعتبارسنجی و در صورت معتبر نبودن مقادیر پیام مناسب نشان داده شود. برای اینکار از کتابخانه jQuery Globalize استفاده میکنیم. برای اضافه کردن jQuery Globalize از طریق کنسول nuget فرمان زیر اجرا نمایید: 

              PM> Install-Package jquery-globalize

               پس از نصب کتابخانه  اگر به پوشه Scripts نگاهی بیاندازید میبینید که پوشەای با نام jquery.globalize اضافه شده است. درداخل پوشه زیر پوشەی دیگری با نام cultures وجود دارد که در آن Cultureهای مختلف وجود دارد و بسته به نیاز میتوان از آنها استفاده کرد. دوباره به سراغ فایل Index.cshtm بروید و فایلهای جاوا اسکریپتی زیر را به صفحه اضافه کنید:

              <script src="~/Scripts/jquery.validate.js"> </script>
              <script src="~/Scripts/jquery.validate.unobtrusive.js"> </script>
              <script src="~/Scripts/jquery.globalize/globalize.js"> </script>
              <script src="~/Scripts/jquery.globalize/cultures/globalize.culture.fa-IR.js"> </script>

              در فایل globalize.culture.fa-IR.js کاراکتر جدا کننده اعشاری '.' در نظر گرفته شده است که مجبور به تغییر آن هسیتم. برای اینکار فایل را باز کرده و numberFormat را پیدا کنید و آن را به شکل زیر تغییر دهید: 

              numberFormat: {
                  pattern: ["n-"],
                  ".": "/",
                  currency: {
                      pattern: ["$n-", "$ n"],
                      ".": "/",
                      symbol: "ریال"
                  }
              },

              و در نهایت کدهای زیر را به فایل Index.cshtml اضافه کنید و برنامه را دوباره اجرا نمایید:

              Globalize.culture('fa-IR');
              $.validator.methods.number = function(value, element) {
                  if (value.indexOf('.') > 0) {
                      return false;
                  }
                  var splitedValue = value.split('/');
                  if (splitedValue.length === 1) {
                      return !isNaN(Globalize.parseInt(value));
                  } else if (splitedValue.length === 2 && $.trim(splitedValue[1]).length === 0) {
                      return false;
                  }
                  return !isNaN(Globalize.parseFloat(value));
              };
              };

              در خط اول Culture را ست مینمایم و در ادامه نحوه اعتبارسنجی را در unobtrusive validation تغییر میدهیم. از آنجایی که برای اعتبارسنجی عدد وارد شده از تابع parseFloat استفاده میشود، کاراکتر جدا کننده قسمت اعشاری قابل قبول برای این تابع '.' است پس در داخل تابع دوباره '/' به '.' تبدیل میشود و سپس اعتبارسنجی انجام میشود از اینرو اگر کاربر '.' را نیز وارد نماید قابل قبول است به همین دلیل با این خط کد if (value.indexOf('.') > 0) وجود نقطه را بررسی میکنیم تا در صورت وجود '.' پیغام خطا نشان داده شود.در خط بعدی بررسی مینماییم که اگر عدد وارد شده اعشاری نباشد از تابع parseInt  استفاده نماییم. در خط بعدی این حالت را بررسی مینماییم که اگر کاربر عددی همچون /١٢ وارد کرد پیغام خطا صادر شود. 

              برای اعتبارسنجی تاریخ شمسی متاسفانه توابع کمکی برای تبدیل تاریخ در فایل globalize.culture.fa-IR.js وجود ندارد ولی اگر نگاهی به فایلهای Culture عربی بیاندازید همه دارای توابع کمکی برای تبدیل تاریج هجری به میلادی هستند به همین دلیل امکان اعتبارسنجی تاریخ شمسی با استفاده از jQuery Globalize میسر نمیباشد. من خودم تعدادی توابع کمکی را به globalize.culture.fa-IR.js اضافه کردەام که از تقویم فارسی آقای علی فرهادی برداشت شده است و با آنها کار اعتبارسنجی را انجام میدهیم. لازم به ذکر است این روش ١٠٠% تست نشده است و شاید راه کاملا اصولی نباشد ولی به هر حال در اینجا توضیح میدهم. در فایل globalize.culture.fa-IR.js قسمت Gregorian_Localized را پیدا کنید و آن را با کدهای زیر جایگزین کنید: 

              Gregorian_Localized: {
                  firstDay: 6,
                  days: {
                      names: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"],
                      namesAbbr: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"],
                      namesShort: ["ی", "د", "س", "چ", "پ", "ج", "ش"]
                  },
                  months: {
                      names: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""],
                      namesAbbr: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""]
                  },
                  AM: ["ق.ظ", "ق.ظ", "ق.ظ"],
                  PM: ["ب.ظ", "ب.ظ", "ب.ظ"],
                  patterns: {
                      d: "yyyy/MM/dd",
                      D: "yyyy/MM/dd",
                      t: "hh:mm tt",
                      T: "hh:mm:ss tt",
                      f: "yyyy/MM/dd hh:mm tt",
                      F: "yyyy/MM/dd hh:mm:ss tt",
                      M: "dd MMMM"
                  },
                  JalaliDate: {
                      g_days_in_month: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
                      j_days_in_month: [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]
                  },
                  gregorianToJalali: function (gY, gM, gD) {
                      gY = parseInt(gY);
                      gM = parseInt(gM);
                      gD = parseInt(gD);
                      var gy = gY - 1600;
                      var gm = gM - 1;
                      var gd = gD - 1;
              
                      var gDayNo = 365 * gy + parseInt((gy + 3) / 4) - parseInt((gy + 99) / 100) + parseInt((gy + 399) / 400);
              
                      for (var i = 0; i < gm; ++i)
                          gDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i];
                      if (gm > 1 && ((gy % 4 == 0 && gy % 100 != 0) || (gy % 400 == 0)))
                          /* leap and after Feb */
                          ++gDayNo;
                      gDayNo += gd;
              
                      var jDayNo = gDayNo - 79;
              
                      var jNp = parseInt(jDayNo / 12053);
                      jDayNo %= 12053;
              
                      var jy = 979 + 33 * jNp + 4 * parseInt(jDayNo / 1461);
              
                      jDayNo %= 1461;
              
                      if (jDayNo >= 366) {
                          jy += parseInt((jDayNo - 1) / 365);
                          jDayNo = (jDayNo - 1) % 365;
                      }
              
                      for (var i = 0; i < 11 && jDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; ++i) {
                          jDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i];
                      }
                      var jm = i + 1;
                      var jd = jDayNo + 1;
              
                      return [jy, jm, jd];
                  },
                  jalaliToGregorian: function (jY, jM, jD) {
                      jY = parseInt(jY);
                      jM = parseInt(jM);
                      jD = parseInt(jD);
                      var jy = jY - 979;
                      var jm = jM - 1;
                      var jd = jD - 1;
              
                      var jDayNo = 365 * jy + parseInt(jy / 33) * 8 + parseInt((jy % 33 + 3) / 4);
                      for (var i = 0; i < jm; ++i) jDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i];
              
                      jDayNo += jd;
              
                      var gDayNo = jDayNo + 79;
              
                      var gy = 1600 + 400 * parseInt(gDayNo / 146097); /* 146097 = 365*400 + 400/4 - 400/100 + 400/400 */
                      gDayNo = gDayNo % 146097;
              
                      var leap = true;
                      if (gDayNo >= 36525) /* 36525 = 365*100 + 100/4 */ {
                          gDayNo--;
                          gy += 100 * parseInt(gDayNo / 36524); /* 36524 = 365*100 + 100/4 - 100/100 */
                          gDayNo = gDayNo % 36524;
              
                          if (gDayNo >= 365)
                              gDayNo++;
                          else
                              leap = false;
                      }
              
                      gy += 4 * parseInt(gDayNo / 1461); /* 1461 = 365*4 + 4/4 */
                      gDayNo %= 1461;
              
                      if (gDayNo >= 366) {
                          leap = false;
              
                          gDayNo--;
                          gy += parseInt(gDayNo / 365);
                          gDayNo = gDayNo % 365;
                      }
              
                      for (var i = 0; gDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap) ; i++)
                          gDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap);
                      var gm = i + 1;
                      var gd = gDayNo + 1;
              
                      return [gy, gm, gd];
                  },
                  checkDate: function (jY, jM, jD) {
                      return !(jY < 0 || jY > 32767 || jM < 1 || jM > 12 || jD < 1 || jD >
                          (Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[jM - 1] + (jM == 12 && !((jY - 979) % 33 % 4))));
                  },
                  convert: function (value, format) {
                      var day, month, year;
              
                      var formatParts = format.split('/');
                      var dateParts = value.split('/');
                      if (formatParts.length !== 3 || dateParts.length !== 3) {
                          return false;
                      }
              
                      for (var j = 0; j < formatParts.length; j++) {
                          var currentFormat = formatParts[j];
                          var currentDate = dateParts[j];
                          switch (currentFormat) {
                              case 'dd':
                                  if (currentDate.length === 2 || currentDate.length === 1) {
                                      day = currentDate;
                                  } else {
                                      year = currentDate;
                                  }
                                  break;
                              case 'MM':
                                  month = currentDate;
                                  break;
                              case 'yyyy':
                                  if (currentDate.length === 4) {
                                      year = currentDate;
                                  } else {
                                      day = currentDate;
                                  }
                                  break;
                              default:
                                  return false;
                          }
                      }
              
                      year = parseInt(year);
                      month = parseInt(month);
                      day = parseInt(day);
                      var isValidDate = Globalize.culture().calendars.Gregorian_Localized.checkDate(year, month, day);
                      if (!isValidDate) {
                          return false;
                      }
              
                      var grDate = Globalize.culture().calendars.Gregorian_Localized.jalaliToGregorian(year, month, day);
                      var shDate = Globalize.culture().calendars.Gregorian_Localized.gregorianToJalali(grDate[0], grDate[1], grDate[2]);
              
                      if (year === shDate[0] && month === shDate[1] && day === shDate[2]) {
                          return true;
                      }
              
                      return false;
                  }
              },

              روال کار در تابع convert به اینصورت است که ابتدا تاریخ وارد شده را بررسی مینماید تا معتبر بودن آن معلوم شود به عنوان مثال اگر تاریخی مثل 1392/12/31 وارد شده باشد و در ادامه برای بررسی بیشتر تاریخ یک بار به میلادی و تاریخ میلادی دوباره به شمسی تبدیل میشود و با تاریخ وارد شده مقایسه میشود و در صورت برابری تاریخ معتبر اعلام میشود. در فایل Index.cshtml کدهای زیر اضافی نمایید:

              $.validator.methods.date = function (value, element) {
                  return Globalize.culture().calendars.Gregorian_Localized.convert(value, 'yyyy/MM/dd');
              };

              برای اعتبارسنجی تاریخ میتوانید از ٢ فرمت استفاده کنید:

              ١ – yyyy/MM/dd

              ٢ – dd/MM/yyyy

              البته از توابع اعتبارسنجی تاریخ میتوانید به صورت جدا استفاده نمایید و لزومی ندارد آنها را همراه با jQuery Globalize بکار ببرید. در آخر خروجی کار به این شکل است:

              در کل استفاده از jQuery Globalize برای اعتبارسنجی در سایتهای چند زبانه به نسبت خوب میباشد و برای هر زبان میتوانید از culture مورد نظر استفاده نمایید. در قسمت دوم این مطلب به بررسی بخش سمت سرور میپردازیم.

              مطالب
              انجام پی در پی اعمال Async به کمک Iterators - قسمت دوم

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

              چندین کتابخانه و کلاس جهت مدیریت Coroutines در دات نت تهیه شده که لیست آن‌ها به شرح زیر است:
              1) Using C# 2.0 iterators to simplify writing asynchronous code
              2) Wintellect's Jeffrey Richter's PowerThreading Library
              3) Rob Eisenberg's Build your own MVVM Framework codes

              و ...

              مورد سوم که توسط خالق اصلی کتابخانه‌ی Caliburn (یکی از فریم ورک‌های مشهور MVVM برای WPF و Silverlight) در کنفرانس MIX 2010 ارائه شد، این روزها در وبلاگ‌های مرتبط بیشتر مورد توجه قرار گرفته و تقریبا به یک روش استاندارد تبدیل شده است. این روش از یک اینترفیس و یک کلاس به شرح زیر تشکیل می‌شود:

              using System;

              namespace SLAsyncTest.Helper
              {
              public interface IResult
              {
              void Execute();
              event EventHandler Completed;
              }
              }

              using System;
              using System.Collections.Generic;

              namespace SLAsyncTest.Helper
              {
              public class ResultEnumerator
              {
              private readonly IEnumerator<IResult> _enumerator;

              public ResultEnumerator(IEnumerable<IResult> children)
              {
              _enumerator = children.GetEnumerator();
              }

              public void Enumerate()
              {
              childCompleted(null, EventArgs.Empty);
              }

              private void childCompleted(object sender, EventArgs args)
              {
              var previous = sender as IResult;

              if (previous != null)
              previous.Completed -= childCompleted;

              if (!_enumerator.MoveNext())
              return;

              var next = _enumerator.Current;
              next.Completed += childCompleted;
              next.Execute();
              }
              }
              }

              توضیحات:
              مطابق توضیحات قسمت قبل، برای مدیریت اعمال همزمان به شکلی پی در پی، نیاز است تا یک IEnumerable را به همراه yield return در پایان هر مرحله از کار ایجاد کنیم. در اینجا این IEnumerable را از نوع اینترفیس IResult تعریف خواهیم کرد. متد Execute آن شامل کدهای عملیات Async خواهند شد و پس از پایان کار رخداد Completed صدا زده می‌شود. به این صورت کلاس ResultEnumerator به سادگی می‌تواند یکی پس از دیگری اعمال Async مورد نظر ما را به صورت متوالی فراخوانی نمائید. با هر بار فراخوانی رخداد Completed، متد MoveNext صدا زده شده و یک مرحله به جلو خواهیم رفت.
              برای مثال کدهای ساده WCF Service زیر را در نظر بگیرید.

              using System.ServiceModel;
              using System.ServiceModel.Activation;
              using System.Threading;

              namespace SLAsyncTest.Web
              {
              [ServiceContract(Namespace = "")]
              [AspNetCompatibilityRequirements(RequirementsMode
              = AspNetCompatibilityRequirementsMode.Allowed)]
              public class TestService
              {
              [OperationContract]
              public int GetNumber(int number)
              {
              Thread.Sleep(2000);//Simulating a log running operation
              return number * 2;
              }
              }
              }

              قصد داریم در طی دو مرحله متوالی این WCF Service را در یک برنامه‌ی Silverlight فراخوانی کنیم. کدهای قسمت فراخوانی این سرویس بر اساس پیاده سازی اینترفیس IResult به صورت زیر درخواهند آمد:

              using System;
              using SLAsyncTest.Helper;

              namespace SLAsyncTest.Model
              {
              public class GetNumber : IResult
              {
              public int Result { set; get; }
              public bool HasError { set; get; }

              private int _num;
              public GetNumber(int num)
              {
              _num = num;
              }

              #region IResult Members
              public void Execute()
              {
              var srv = new TestServiceReference.TestServiceClient();
              srv.GetNumberCompleted += (sender, e) =>
              {
              if (e.Error == null)
              Result = e.Result;
              else
              HasError = true;

              Completed(this, EventArgs.Empty); //run the next IResult
              };
              srv.GetNumberAsync(_num);
              }

              public event EventHandler Completed;
              #endregion
              }
              }
              در متد Execute کار فراخوانی غیرهمزمان WCF Service به صورتی متداول انجام شده و در پایان متد Completed صدا زده می‌شود. همانطور که توضیح داده شد، این فراخوانی در کلاس ResultEnumerator یاد شده مورد استفاده قرار می‌گیرد.
              اکنون قسمت‌های اصلی کدهای View Model برنامه به شکل زیر خواهند بود:

              private void doFetch(object obj)
              {
              new ResultEnumerator(executeAsyncOps()).Enumerate();
              }

              private IEnumerable<IResult> executeAsyncOps()
              {
              FinalResult = 0;
              IsBusy = true; //Show BusyIndicator

              //Sequential Async Operations
              var asyncOp1 = new GetNumber(10);
              yield return asyncOp1;

              //using the result of the previous step
              if(asyncOp1.HasError)
              {
              IsBusy = false; //Hide BusyIndicator
              yield break;
              }

              var asyncOp2 = new GetNumber(asyncOp1.Result);
              yield return asyncOp2;

              FinalResult = asyncOp2.Result; //Bind it to the UI

              IsBusy = false; //Hide BusyIndicator
              }
              در اینجا یک IEnumerable از نوع IResult تعریف شده است و در طی دو مرحله‌ی متوالی اما غیرهمزمان کار دریافت اطلاعات از WCF Service صورت می‌گیرد. ابتدا عدد 10 به WCF Service ارسال می‌شود و خروجی 20 خواهد بود. سپس این عدد در مرحله‌ی بعد مجددا به WCF Service ارسال گردیده و حاصل نهایی که عدد 40 می‌باشد در اختیار سیستم Binding قرار خواهد گرفت.
              اگر از این روش استفاده نمی‌شد ممکن بود به این جواب برسیم یا خیر. ممکن بود مرحله‌ی دوم ابتدا شروع شود و سپس مرحله‌ی اول رخ دهد. اما با کمک Iterators و yield return به همراه کلاس ResultEnumerator موفق شدیم تا عملیات دوم همزمان را در حالت تعلیق قرار داده و پس از پایان اولین عملیات غیر همزمان، مرحله‌ی بعدی فراخوانی را بر اساس مقدار حاصل شده از WCF Service آغاز کنیم.
              این روش برای برنامه‌ نویس‌ها آشناتر است و همان سیستم فراخوانی A->B->C را تداعی می‌کند اما کلیه اعمال غیرهمزمان هستند و ترد اصلی برنامه قفل نخواهد شد.

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

              نظرات مطالب
              توزیع پروژه‌های ASP.NET Core 1.1 بدون ارائه فایل‌های View آن
              برای کامپایل مجدد فایل‌های ویو (cshtml) در هنگام اجرای برنامه (runtime compilationو مشاهده تغییرات اعمال شده بر روی آن‌ها به صورت زیر عمل می‌کنیم:

              • ASP.NET Core 2.2
              services.AddMvc()
                  .AddRazorOptions(options => options.AllowRecompilingViewsOnFileChange = true);

              • ASP.NET Core 3.0 , 3.1
              ابتدا بسته  Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation به پروژه اضافه کرده و سپس از کد زیر استفاده می‌کنیم:
              public void ConfigureServices(IServiceCollection services)
              {
                  services.AddControllersWithViews()
                      .AddRazorRuntimeCompilation();
                  //...
              }
               
              مطالب
              ایجاد «خواص الحاقی»
              حتما با متدهای الحاقی یا Extension methods آشنایی دارید؛ می‌توان به یک شیء، که حتی منبع آن در دسترس ما نیست، متدی را اضافه کرد. سؤال: در مورد خواص چطور؟ آیا می‌شود به وهله‌ای از یک شیء موجود از پیش طراحی شده، یک خاصیت جدید را اضافه کرد؟
              احتمالا شاید عنوان کنید که با اشیاء dynamic می‌توان چنین کاری را انجام داد. اما سؤال در مورد اشیاء غیر dynamic است.
              یا نمونه‌ی دیگر آن Attached Properties در برنامه‌های مبتنی بر Xaml هستند. می‌توان به یک شیء از پیش موجود Xaml، خاصیتی را افزود که البته پیاده سازی آن منحصر است به همان نوع برنامه‌ها.


              راه حل پیشنهادی

              یک Dictionary را ایجاد کنیم تا ارجاعی از اشیاء، به عنوان کلید، در آن ذخیره شده و سپس key/valueهایی به عنوان value هر شیء، در آن ذخیره شوند. این key/valueها همان خواص و مقادیر آن‌ها خواهند بود. هر چند این راه حل به خوبی کار می‌کند اما ... مشکل نشتی حافظه دارد.
              شیء Dictionary یک ارجاع قوی را از اشیاء، درون خودش نگه داری می‌کند و تا زمانیکه در حافظه باقی است، سیستم GC مجوز رهاسازی منابع آن‌ها را نخواهد یافت؛ چون عموما این نوع Dictionaryها باید استاتیک تعریف شوند تا طول عمر آن‌ها با طول عمر برنامه یکی گردد. بنابراین اساسا اشیایی که به این نحو قرار است پردازش شوند، هیچگاه dispose نخواهند شد. راه حلی برای این مساله در دات نت 4 به صورت توکار به دات نت فریم ورک اضافه شده‌است؛ به نام ساختار داده‌ای ConditionalWeakTable.


              معرفی ConditionalWeakTable

              ConditionalWeakTable جزو ساختارهای داده‌ای کمتر شناخته شده‌ی دات نت است. این ساختار داده، اشاره‌گرهایی را به ارجاعات اشیاء، درون خود ذخیره می‌کند. بنابراین چون ارجاعاتی قوی را به اشیاء ایجاد نمی‌کند، مانع عملکرد GC نیز نشده و برنامه در دراز مدت دچار مشکل نشتی حافظه نخواهد شد. هدف اصلی آن ایجاد ارتباطی بین CLR و DLR است. توسط آن می‌توان به اشیاء دلخواه، خواصی را افزود. به علاوه طراحی آن به نحوی است که thread safe است و مباحث قفل گذاری بر روی اطلاعات، به صورت توکار در آن پیاده سازی شده‌است. کار DLR فراهم آوردن امکان پیاده سازی زبان‌های پویایی مانند Ruby و Python برفراز CLR است. در این نوع زبان‌ها می‌توان به وهله‌هایی از اشیاء موجود، خاصیت‌های جدیدی را متصل کرد.
              به صورت خلاصه کار ConditionalWeakTable ایجاد نگاشتی است بین وهله‌هایی از اشیاء CLR (اشیایی غیرپویا) و خواصی که به آن‌ها می‌توان به صورت پویا انتساب داد. در کار GC اخلال ایجاد نمی‌کند و همچنین می‌توان به صورت همزمان از طریق تردهای مختلف، بدون مشکل با آن کار کرد.


              پیاده سازی خواص الحاقی به کمک ConditionalWeakTable

              در اینجا نحوه‌ی استفاده از ConditionalWeakTable را جهت اتصال خواصی جدید به وهله‌های موجود اشیاء مشاهده می‌کنید:
              using System.Collections.Generic;
              using System.Runtime.CompilerServices;
              
              namespace ConditionalWeakTableSamples
              {
                  public static class AttachedProperties
                  {
                      public static ConditionalWeakTable<object,
                          Dictionary<string, object>> ObjectCache = new ConditionalWeakTable<object,
                              Dictionary<string, object>>();
              
                      public static void SetValue<T>(this T obj, string name, object value) where T : class
                      {
                          var properties = ObjectCache.GetOrCreateValue(obj);
              
                          if (properties.ContainsKey(name))
                              properties[name] = value;
                          else
                              properties.Add(name, value);
                      }
              
                      public static T GetValue<T>(this object obj, string name)
                      {
                          Dictionary<string, object> properties;
                          if (ObjectCache.TryGetValue(obj, out properties) && properties.ContainsKey(name))
                              return (T)properties[name];
                          return default(T);
                      }
              
                      public static object GetValue(this object obj, string name)
                      {
                          return obj.GetValue<object>(name);
                      }
                  }
              }
              ObjectCache تعریف شده از نوع استاتیک است؛ بنابراین در طول عمر برنامه زنده نگه داشته خواهد شد، اما اشیایی که به آن منتسب می‌شوند، خیر. هرچند به ظاهر در متد GetOrCreateValue، یک وهله از شیءایی موجود را دریافت می‌کند، اما در پشت صحنه صرفا IntPtr یا اشاره‌گری به این شیء را ذخیره سازی خواهد کرد. به این ترتیب در کار GC اخلالی صورت نخواهد گرفت و شیء مورد نظر، تا پایان کار برنامه به اجبار زنده نگه داشته نخواهد شد.


              کاربرد اول

              اگر با ASP.NET کار کرده باشید حتما با IPrincipal آشنایی دارید. خواصی مانند Identity یک کاربر در آن ذخیره می‌شوند.
              سؤال: چگونه می‌توان یک خاصیت جدید به نام مثلا Disclaimer را به وهله‌ای از این شیء افزود:
                  public static class ISecurityPrincipalExtension
                  {
                      public static bool Disclaimer(this IPrincipal principal)
                      {
                          return principal.GetValue<bool>("Disclaimer");
                      }
              
                      public static void SetDisclaimer(this IPrincipal principal, bool value)
                      {
                          principal.SetValue("Disclaimer", value);
                      }
                  }
              در اینجا مثالی را از کاربرد کلاس AttachedProperties فوق مشاهده می‌کنید. توسط متد SetDisclaimer یک خاصیت جدید به نام Disclaimer به وهله‌ای از شیءایی از نوع  IPrincipal  قابل اتصال است. سپس توسط متد  Disclaimer قابل دستیابی خواهد بود.

              اگر صرفا قرار است یک خاصیت به شیءایی متصل شود، روش ذیل نیز قابل استفاده می‌باشد (بجای استفاده از دیکشنری از یک کلاس جهت تعریف خاصیت اضافی جدید استفاده شده‌است):
              using System.Runtime.CompilerServices;
              
              namespace ConditionalWeakTableSamples
              {
                  public static class PropertyExtensions
                  {
                      private class ExtraPropertyHolder
                      {
                          public bool IsDirty { get; set; }
                      }
              
                      private static readonly ConditionalWeakTable<object, ExtraPropertyHolder> _isDirtyTable
                              = new ConditionalWeakTable<object, ExtraPropertyHolder>();
              
                      public static bool IsDirty(this object @this)
                      {
                          return _isDirtyTable.GetOrCreateValue(@this).IsDirty;
                      }
              
                      public static void SetIsDirty(this object @this, bool isDirty)
                      {
                          _isDirtyTable.GetOrCreateValue(@this).IsDirty = isDirty;
                      }
                  }
              }


              کاربرد دوم

              ایجاد Id منحصربفرد برای اشیاء برنامه.
              فرض کنید در حال نوشتن یک Entity framework profiler هستید. طراحی فعلی سیستم Interception آن به نحو زیر است:
              public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
              {
              }
              سؤال: اینجا رویداد بسته شدن یک اتصال را دریافت می‌کنیم؛ اما ... دقیقا کدام اتصال؟ رویداد Opened را هم داریم اما چگونه این اشیاء را به هم مرتبط کنیم؟ شیء DbConnection دارای Id نیست. متد GetHashCode هم الزامی ندارد که اصلا پیاده سازی شده باشد یا حتی یک Id منحصربفرد را تولید کند. این متد با تغییر مقادیر خواص یک شیء می‌تواند مقادیر متفاوتی را ارائه دهد. در اینجا می‌خواهیم به ازای ارجاعی از یک شیء، یک Id منحصربفرد داشته باشیم تا بتوانیم تشخیص دهیم که این اتصال بسته شده، دقیقا کدام اتصال باز شده‌است؟
              راه حل: خوب ... یک خاصیت Id را به اشیاء موجود متصل کنید!
              using System;
              using System.Runtime.CompilerServices;
              
              namespace ConditionalWeakTableSamples
              {
                  public static class UniqueIdExtensions
                  {
                      static readonly ConditionalWeakTable<object, string> _idTable = 
                                                  new ConditionalWeakTable<object, string>();
              
                      public static string GetUniqueId(this object obj)
                      {
                          return _idTable.GetValue(obj, o => Guid.NewGuid().ToString());
                      }
              
                      public static string GetUniqueId(this object obj, string key)
                      {
                          return _idTable.GetValue(obj, o => key);
                      }
                  }
              }
              در اینجا مثالی دیگر از پیاده سازی و استفاده از ConditionalWeakTable را ملاحظه می‌کنید. اگر در کش آن ارجاعی به شیء مورد نظر وجود داشته باشد، مقدار Guid آن بازگشت داده می‌شود؛ اگر خیر، یک Guid به ارجاعی از شیء، انتساب داده شده و سپس بازگشت داده می‌شود. به عبارتی به صورت پویا یک خاصیت UniqueId به وهله‌هایی از اشیاء اضافه می‌شوند. به این ترتیب به سادگی می‌توان آن‌ها را ردیابی کرد و تشخیص داد که اگر این Guid پیشتر جایی به اتصال باز شده‌ای منتسب شده‌است، در چه زمانی و در کجا بسته شده است یا اصلا ... خیر. جایی بسته نشده‌است.


              برای مطالعه بیشتر
              The Conditional Weak Table: Enabling Dynamic Object Properties
              How to create mixin using C# 4.0
              Disclaimer Page using MVC
              Extension Properties Revised
              Easy Modeling
              Providing unique ID on managed object using ConditionalWeakTable
              مطالب
              کارهایی جهت بالابردن کارآیی Entity Framework #3

              در قسمت‌های قبلی (^ و ^) راهکارهایی جهت بالا بردن کارآیی، ارائه شد. در ادامه، به آخرین قسمت این سری اشاره خواهم کرد.

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

              یادآوری: قبل از هر چیز با توجه به این مقاله دانستن این نکته الزامی است که فراخوانی برخی متدها مانند DbSet.Add سبب فراخوانی DataContext.ChangeTracker.DetectChanges خواهند شد.

              فرض کنید قصد افزودن 2000 موجودیت دانش آموز را دارید:

              for (int i = 0; i < 2000; i++)
              {
                  Pupil pupil = GetNewPupil();
                  db.Pupils.Add(pupil);
              }
              db.SaveChanges();
              در کد بالا بدلیل فراخوانی متد DbSet.Add و به دنبال آن فراخوانی متد DetectChanges در هر بار اجرای حلقه (2000بار) مدت زمان اجرای کد بالا بسیار زیاد است و اگر به پروفایلر نگاهی بیاندازید، اشغال CPU توسط کوئری بالا، بیش از حد متعارف است.

              اگر به تصویر بالا دقت کنید بیش از 34 ثانیه (خط 193 قسمت سوم شکل) جهت افزودن 2000 موجودیت به کانتکست سپری شده است. در حالی که درج این 2000 موجودیت کمی بیش از 1 ثانیه (خط 195 قسمت سوم شکل) که 379 میلی ثانیه (قسمت دوم شکل) آن مربوط به اجرای کوئری اختصاص یافته  طول کشیده است.
              بیشترین زمان صرف شده‌ی برای درج 2000 موجودیت، در کد برنامه سپری شده است که با بررسی بیشتر در پروفایلر، متوجه زمان بر بودن فراخوانی متد ()DetectChanges که در فضای نام Data.Entity.Core وجود دارد خواهید شد. این متد 2000 بار به تعداد موجودیت هایی که قصد داریم به بانک اطلاعاتی اضافه نماییم، فراخوانی شده است.

              همانطور که در شکل بالا مشخص است همان 34 ثانیه جهت ردیابی تغییرات صرف شده است. EF ردیابی تغییرات را بصورت پیش فرض هر زمانی که قصد افزودن یا ویرایش موجودیتی را داشته باشید، انجام خواهد داد و هر چه موجودیت‌های بیشتری را بخواهید ویرایش یا اضافه نمایید، این زمان نیز بیشتر خواهد شد. در حقیقت زمان لازم برای الگوریتم ردیابی تغییرات بصورت نمایی با رشد موجودیت‌ها افزوده می‌شود. به عبارت دیگر اگر این تعداد موجودیت‌ها را به 4000 عدد برسانید، مدت زمان لازم بیش از 2 برابر افزوده خواهد شد.

              راه حل اول:
              استفاده از متد ()AddRange ارائه شده در EF6 که جهت درج دسته‌ای (Bulk Insert) ارائه شده است:
              var list = new List<Pupil>();
              
              for (int i = 0; i < 2000; i++)
              {
                  Pupil pupil = GetNewPupil();
                  list.Add(pupil);
              }
              
              db.Pupils.AddRange(list);
              db.SaveChanges();

              راه حل دوم:

              در سناریوهای پیچیده، مانند درج دسته‌ای چندین موجودیت، شاید مجبور به خاموش نمودن این قابلیت شوید:
              db.Configuration.AutoDetectChangesEnabled = false;
              توجه داشته باشید اگر قصد دارید از امکان ردیابی تغییرات مجددا بهره‌مند شوید، باید این قابلیت را نیز فعال نمایید. با خاموش نمودن ردیابی تغییرات بار دیگر کوئری ابتدایی را اجرا نمایید. مدت زمان لازم جهت افزودن 2000 موجودیت به کانتکست از بیش از 34 ثانیه به 85 میلی ثانیه کاهش یافته است؛ ولی اعمال تغییرات به بانک اطلاعاتی همانند مرتبه اول بیش از 1 ثانیه طول خواهد کشید.


              ردیابی تغییرات

              هنگامی که موجودیتی را از بانک اطلاعاتی دریافت نمایید، می‌توانید آن را ویرایش نمایید و مجددا به بانک اطلاعاتی اعمال نمایید. چون EF اطلاعی از قصد شما برای موجودیت نمی‌داند، مجبور است تغییرات شما را زیر نظر بگیرد که این زیر نظر گرفتن، هزینه و سربار دارد و این سربار و هزینه برای داده‌های زیاد، بیشتر خواهد شد. بنابراین اگر قصد دارید اطلاعاتی فقط خواندنی را از بانک اطلاعاتی دریافت نمایید، بهتر است صراحتا به EF بگویید این موجودیت را تحت ردیابی قرار ندهد.
              string city = "New York";
              
              List<School> schools = db.Schools
                  .AsNoTracking()
                  .Where(s => s.City == city)
                  .Take(100)
                  .ToList();

              استفاده از متد AsNoTracking  در کد بالا سبب خواهد شد 100 مدرسه که در شهر نیویورک وجود دارد توسط EF، بدون تحت نظر گرفتن آن‌ها از بانک اطلاعاتی دریافت شوند و سرباری نیز تحمیل نشود.

              ویوهای از قبل کامپایل شده

              معمولا، هنگامی که از EF برای اولین بار استفاده می‌نمایید، ویوهایی ایجاد می‌گردد که برای ایجاد کوئری‌ها مورد استفاده قرار می‌گیرند. این ویوها در طول حیات برنامه فقط یکبار ایجاد می‌شوند. ولی همین یکبار هم زمانبر هستند. خوشبختانه راه‌هایی وجود دارد که ایجاد این ویوها را در زمان runtime انجام نداد و آن راه، استفاده از ویوهای از پیش کامپایل شده است. یکی از راههای ایجاد این ویوها استفاده از Entity Framework Power Tools است. بعد از نصب اکستنشن، بر روی فایل کانتکست راست کلیک کرده و سپس گزینه‌ی Generate Views را از منوی Entity Framework انتخاب کنید.

              توجه داشته باشید که هر تغییری را بعد از ایجاد این ویوها بر روی کانتکست اعمال نمایید، باید آن‌ها را مجددا تولید کنید. برای آشنایی بیشتر با این ویوها به این لینک مراجعه کنید. هم چنین پکیج نیوگتی بنام EFInteractiveViews نیز برای این منظور تهیه و توزیع شده است.


              حذف کوئری‌های ابتدایی غیر ضروری

              در هنگام شروع به کار با EF، چندین کوئری آغازین بر روی دیتابیس اجرا می‌شوند. یکی از کوئری‌های آغازین جهت تشخیص نسخه‌ی دیتابیس است که همانطور در تصویر زیر مشاهده می‌کنید، در حدود چند میلی ثانیه می‌باشد.

              با توجه به توضیحات، در صورتیکه اطلاعی از نسخه‌ی دیتابیس دارید، می‌توانید این کوئری ابتدایی را تحریف نمایید. برای اینکار می‌توان توسط متد ()ResolveManifestToken کلاسی که اینترفیس IManifestTokenResolver را پیاده سازی کرده است، نسخه‌ی دیتابیس را برگردانیم و از یک رفت و برگشت به دیتابیس جلوگیری نماییم.

               public class CustomManifestTokenResolver : IManifestTokenResolver
              {
                  public string ResolveManifestToken(DbConnection connection)
                  {
                      return "2014";
                  }
              }
              و توسط کلاسی که از کلاس DbConfiguration ارث بری کرده است آن را استفاده نماییم.
               public class CustomDbConfiguration : DbConfiguration
              {
                  public CustomDbConfiguration()
                  {
                      SetManifestTokenResolver(new CustomManifestTokenResolver());
                  }
              }


              تخریب کانتکست

              تخریب و از بین بردن کانتکست هنگامی که به آن نیاز نداریم بسیار ضروری است. یکی از روشهای اصولی برای Disposing کانتکست، محصور کردن آن بوسیله دستور Using است (البته فرض بر این است که قرار نیست از الگوهای اشاره شده استفاده نماییم). در صورت عدم تخریب صحیح کانتکست باید منتظر آسیب جدی به کارایی Garbage Collector جهت آزاد سازی منابع مورد استفاده کانتکست و هم چنین باز نمودن اتصالات جدید به دیتابیس باشید.


              پاسخگویی به چندین درخواست بر روی یک کانکشن

              EF از قابلیتی بنام Multiple Result Sets می‌تواند بهره ببرد که این قابلیت باعث می‌شود بر روی یک کانکشن ایجاد شده، یک یا چند درخواست از دیتابیس ارسال و یا دریافت شود که سبب کاهش تعداد رفت و برگشت به دیتابیس می‌شود. کاربرد دیگر این قابلیت، زمانی است که تاخیر زیادی (latency) بین اپلیکیشن و دیتابیس وجود دارد.

              برای فعالسازی کافی است مقدار زیر را در کانکشن استرینگ اضافه نمایید:

              MultipleActiveResultSets=True;


              استفاده از متدهای ناهمگام

              در C#5 و EF6 پشتیبانی خوبی از متدهای ناهمگام فراهم شده است و اکثر متدهایی مانند ToListAsync, CountAsync, FirstAsync, SaveChangesAsync و غیره که باعث رفت و برگشت به دیتابیس می‌شوند امکان پشتیبانی ناهمگام را نیز دارند. البته این قابلیت برای برنامه‌های یک درخواست در یک زمان شاید مفید نباشد؛ ولی برای برنامه‌های وبی برعکس. در برنامه وب جهت پشتیبانی از بارگذاری همزمان (concurrent) قابلیت ناهمگام (Async) سبب خواهد شد منابع تا زمان اجرای کوئری به ThreadPool بازگردانده شود و برای سرویس دهی مهیا باشند که باعث افزایش scalability خواهد شد.

              بررسی و آزمایش با داده‌های واقعی

              در اکثر مواقع کارآیی با حجیم شدن داده‌ها کاهش پیدا می‌کند (البته در صورت عدم رعایت اصول استاندارد). بنابراین بررسی کارآیی در محیط هایی با حجم داده‌های بالا ضروری است. هیچ چیز بدتر از آن نیست که همه چیز در محیط توسعه خوب و بی نقص باشد ولی در محیط عملیاتی به شکست بیانجامد. به همین جهت سعی کنید از ابزارهای تولید داده (^ و ^ و ^) برای ایجاد داده‌های آزمایشی استفاده نمایید. سپس کارآیی کوئری خود را مورد بررسی و آزمایش قرار دهید.

              مطالب
              یکسان سازی "ی" و "ک" دریافتی در حین استفاده از WCF RIA Services

              یکی از مواردی که در تمام برنامه‌های فارسی "باید" رعایت شود (مهم نیست به چه زبانی یا چه سکویی باشد یا چه بانک اطلاعاتی مورد استفاده است)، بحث اصلاح "ی" و "ک" دریافتی از کاربر و یکسان سازی آن‌ها می‌باشد. به عبارتی برنامه‌ی فارسی که اصلاح خودکار این دو مورد را لحاظ نکرده باشد دیر یا زود به مشکلات حادی برخورد خواهد کرد و "ناقص" است : اطلاعات بیشتر ؛ برای مثال شاید دوست نداشته باشید که دو کامران در سایت شما ثبت نام کرده باشند؛ یکی با ک فارسی و یکی با ک عربی! به علاوه همین کامران امروز می‌تواند لاگین کند و فردا با یک کامپیوتر دیگر و صفحه کلیدی دیگر پشت درب خواهد ماند. در حالیکه از دید این کامران، کلمه کامران همان کامران است!
              بنابراین در دو قسمت "باید" این یکسان سازی صورت گیرد:
              الف) پیش از ثبت اطلاعات در بانک اطلاعاتی (تا با دو کامران ثبت شده در بانک اطلاعاتی مواجه نشوید)
              ب) پیش از جستجو (تا کامران روزی دیگر با صفحه کلیدی دیگر بتواند به برنامه وارد شود)

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

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

              در مورد ‌مقدمات WCF RIA Services که درSilverlight و ASP.NET کاربرد دارد می‌توانید به این مطلب مراجعه کنید: +

              جهت تکمیل این بحث متدی تهیه شده که کار یکسان سازی ی و ک دریافتی از کاربر را حین ثبت توسط امکانات WCF RIA Services انجام می‌دهد (دقیقا پیش از فراخوانی متد SubmitChanges باید بکارگرفته شود):


              namespace SilverlightTests.RiaYeKe
              {
              public static class PersianHelper
              {
              public static string ApplyUnifiedYeKe(this string data)
              {
              if (string.IsNullOrEmpty(data)) return data;
              return data.Replace("ی", "ی").Replace("ک", "ک");
              }
              }
              }

              using System.Linq;
              using System.Windows.Controls;
              using System.Reflection;
              using System.ServiceModel.DomainServices.Client;

              namespace SilverlightTests.RiaYeKe
              {
              public class RIAHelper
              {
              /// <summary>
              /// یک دست سازی ی و ک در عبارات ثبت شده در بانک اطلاعاتی پیش از ورود به آن
              /// این متد باید پیش از فراخوانی متد
              /// SubmitChanges
              /// استفاده شود
              /// </summary>
              /// <param name="dds"></param>
              public static void ApplyCorrectYeKe(DomainDataSource dds)
              {
              if (dds == null)
              return;

              if (dds.DataView.TotalItemCount <= 0)
              return;

              //پیدا کردن موجودیت‌های تغییر کرده
              var changedEntities = dds.DomainContext.EntityContainer.GetChanges().Where(
              c => c.EntityState == EntityState.Modified ||
              c.EntityState == EntityState.New);

              foreach (var entity in changedEntities)
              {
              //یافتن خواص این موجودیت‌ها
              var propertyInfos = entity.GetType().GetProperties(
              BindingFlags.Public | BindingFlags.Instance
              );

              foreach (var propertyInfo in propertyInfos)
              {
              //اگر این خاصیت رشته‌ای است ی و ک آن را استاندارد کن
              if (propertyInfo.PropertyType != typeof (string)) continue;
              var propName = propertyInfo.Name;
              var val = new PropertyReflector().GetValue(entity, propName);
              if (val == null) continue;
              new PropertyReflector().SetValue(
              entity,
              propName,
              val.ToString().ApplyUnifiedYeKe());
              }
              }
              }
              }
              }

              توضیحات:
              از آنجائیکه حین فراخوانی متد SubmitChanges فقط موجودیت‌های تغییر کرده جهت ثبت ارسال می‌شوند، ابتدا این موارد یافت شده و سپس خواص عمومی تک تک این اشیاء توسط عملیات Reflection بررسی می‌گردند. اگر خاصیت مورد بررسی از نوع رشته‌ای بود، یکبار این یک دست سازی اطلاعات ی و ک دریافتی صورت خواهد گرفت (و از آنجائیکه این تعداد همیشه محدود است عملیات Reflection سربار خاصی نخواهد داشت).
              اگر در کدهای خود از DomainDataSource استفاده نمی‌کنید باز هم تفاوتی نمی‌کند. متد ApplyCorrectYeKe را از قسمت DomainContext.EntityContainer به بعد دنبال کنید.
              اکنون تنها مورد باقیمانده بحث جستجو است که با اعمال متد ApplyUnifiedYeKe به مقدار ورودی متد جستجوی خود، مشکل حل خواهد شد.

              کلاس PropertyReflector بکارگرفته شده هم از اینجا به عاریت گرفته شد.
              دریافت کدهای این بحث

              مطالب
              MVC vs 3-Tier Pattern

              من تا به حال برنامه نویس‌های زیادی را دیده‌ام که می‌پرسند «چه تفاوتی بین الگوهای معماری MVC و Three-Tier وجود دارد؟» قصد من روشن کردن این سردرگمی، بوسیله مقایسه هردو، با کنار هم قرار دادن آنها می‌باشد. حداقل در این بخش، من اعتقاد دارم، منبع بیشتر این سردرگمی‌ها در این است که هر دو‌ی آنها، دارای سه لایه متمایز و گره، در دیاگرام مربوطه‌اشان هستند.

              اگر شما به دقت به دیاگرام آنها نگاه کنید، پیوستگی را خواهید دید. بین گره‌ها و راه اندازی آنها، کمی تفاوت است.


              معماری سه لایه

              سیستم‌های سه لایه، واقعاً لایه‌ها را می‌سازند: لایه UI به لایه Business logic دسترسی دارد و لایه Business logic به لایه Data دسترسی دارد. اما لایه UI دسترسی مستقیمی به لایه Data ندارد و باید از طریق لایه Business logic و روابط آنها عمل کند. بنابراین می‌توانید فکر کنید که هر لایه، بعنوان یک جزء، آزاد است؛ همراه با قوانین محکم طراحی دسترسی بین لایه ها.

              MVC

              در مقابل، اینPattern ، لایه‌های سیستم را نگهداری نمی‌کند. کنترلر به مدل و View (برای انتخاب یا ارسال مقادیر) دسترسی دارد. View نیز دسترسی دارد به مدل . دقیقاً چطور کار می‌کند؟ کنترلر در نهایت نقطه تصمیم گیری منطقی است. چه نوع منطقی؟ نوعاً، کنترلر، ساخت و تغییر مدل را در اکشن‌های مربوطه، کنترل خواهد کرد. کنترلر سپس تصمیم گیری می‌کند که برای منطق داخلیش، کدام View مناسب است. در آن نقطه، کنترلر مدل را به View   ارسال می‌کند. من در اینجا چون هدف بحث مورد دیگه‌ای می‌باشد، مختصر توضیح دادم.

              چه موقع و چه طراحی را انتخاب کنم؟

              اول از همه، هر دو طراحی قطعاً و متقابلاً منحصر بفرد نیستند. در واقع طبق تجربه‌ی من، هر دو آنها کاملاً هماهنگ هستند. اغلب ما از معماری چند لایه استفاده می‌کنیم مانند معماری سه لایه، برای یک ساختار معماری کلی. سپس من در داخل لایه UI، از MVC   استفاده می‌کنم، که در زیر دیاگرام آن را آورده ام.