نظرات مطالب
ASP.NET MVC #18
- از کلاس AuthorizeAttribute ارث بری کنید. بعد داخل آن یک خاصیت به نام مثلا public string AreaName تعریف کنید. این ویژگی سفارشی اکنون می‌تواند از پارامتر AreaName هم استفاده کند و استفاده داخلی از آن با تحریف متد AuthorizeCore میسر خواهد شد.
- اگر متد OnAuthorization را تحریف کنید، به filterContext.Controller دسترسی خواهید داشت.
- ضمن اینکه شما در سازنده این کلاس فیلتر سفارشی، فرصت مقدار دهی خواصی مانند Roles را بر اساس اطلاعات بانک اطلاعاتی خواهید داشت. یعنی به این شکل هم می‌شود آن‌را پویا تعریف کرد.
- توسط HttpContextBase httpContext متدهای تحریف شده به اطلاعات کاربر جاری می‌شود دسترسی یافت (httpContext.User.Identity.Name).
مطالب
ASP.NET MVC #24

مروری بر نمونه سؤالات ASP.NET MVC امتحانات مایکروسافت در چند سال اخیر

در قسمت آخر سری ASP.NET MVC بد نیست مروری داشته باشیم بر نمونه سؤالات امتحانات مایکروسافت؛ امتحانات 70-515 و 70-519 که در آن‌ها تعدادی از سؤالات به ASP.NET MVC اختصاص دارند. در این سؤالات امکان انتخاب بیش از یک گزینه نیز وجود دارد.


1) شما در حال توسعه یک برنامه‌ی ASP.NET MVC ‌هستید. باید درخواست Ajax ایی از صفحه‌ای صادر شده و خروجی زیر را از اکشن متدی دریافت کند:
["Adventure Works","Contoso"] 

کدام نوع خروجی اکشن متد زیر را برای اینکار مناسب می‌دانید؟
a) AjaxHelper 
b) XDocument
c) JsonResult
d) DataContractJsonSerializer



2) شما در حال طراحی یک برنامه ASP.NET MVC ‌هستید. محتوای یک View باید بر اساس نیازمندی‌های زیر تشکیل شود:
الف) ارائه محتوای رندر شده user controls/partial views به مرورگر
ب) کار انتخاب user controls/partial views مناسب در اکشن متد کنترلر باید انجام شود
استفاده از کدام روش زیر را توصیه می‌کنید؟
a) Use the Html.RenderPartial extension method
b) Use the Html.RenderAction extension method
c) Use the PartialViewResult class
d) Use the ContentResult class


3) در حین طراحی یک برنامه ASP.NET MVC، نیاز است منطق مدیریت استثناهای رخ داده و همچنین ثبت وقایع مرتبط را در یک مکان یا کلاس مرکزی مدیریت کنید. کدام روش زیر را پیشنهاد می‌دهید؟
a) استفاده از try/catch در تمام متدها
b) تحریف متد OnException در کنترلرها
c) مزین سازی تمام کنترلرها به ویژگی HandleError سفارشی شده
d) مزین سازی تمام کنترلرها به ویژگی HandleError پیش فرض


4) شما در حال توزیع برنامه‌ی ASP.NET MVC خود جهت اجرا بر روی IIS 6.x هستید. چه ملاحظاتی را باید مدنظر داشته باشید تا برنامه به درستی کار کند؟
a) تنظیم IIS به نحوی‌که تمام درخواست‌ها را بر اساس wildcard‌ خاصی به aspnet_isapi.dll هدایت کند.
b) تنظیم IIS به نحوی‌که تمام درخواست‌ها را بر اساس wildcard‌ خاصی به aspnet_wp.exe هدایت کند.
c) تغییر برنامه به نحوی‌که تمام درخواست‌ها را به یک HttpHandler خاص هدایت کند.
d) تغییر برنامه به نحوی‌که تمام درخواست‌ها را به یک HttpModule خاص هدایت کند.


5) شما در حال توسعه برنامه‌ی ASP.NET MVC هستید که در پوشه Views/Shared/DisplayTemplates آن، فایلی به نام score.cshtml به عنوان یک templated helper نمایش سفارشی اعداد صحیح تعریف شده است. مدل برنامه هم مطابق تعاریف زیر است:
public class Player
{
public String Name { get; set; }
public int LastScore { get; set; }
public int HighScore { get; set; }
}

در اینجا اگر نیاز باشد تا فایل score.cshtml یاد شده به صورت خودکار به خاصیت LastScore در حین فراخوانی متد HtmlHelper.DisplayForModel اعمال شود، چه روشی را پیشنهاد می‌دهید؟
a) فایل score.cshtml باید به LastScore.cshtml تغییر نام یابد.
b) فایل یاد شده باید از پوشه Views/Shared/DisplayTemplates به پوشه Views/Player/DisplayTemplates منتقل شود.
c) باید از ویژگی UIHint به همراه مقدار score جهت مزین سازی خاصیت LastScore استفاده کرد.
[UIHint("Score")]
d) باید از ویژگی زیر برای مزین سازی خاصیت مورد نظر استفاده کرد:
[Display(Name="LastScore", ShortName="Score")]


6) شما در حال طراحی برنامه‌ی ASP.NET MVC هستید که در آن متد Edit کنترلری باید تنها توسط کاربران اعتبارسنجی شده قابل دسترسی باشد. استفاده از کدام دو گزینه زیر را برای این منظور توصیه می‌کنید؟
a) [Authorize(Users = "")]
b) [Authorize(Roles = "")]
c) [Authorize(Users = "*")]
d) [Authorize(Roles = "*")]

7) قطعه کد HTML زیر را درنظر بگیرید:
<span id="ref">
<a name=Reference>Check out</a>
the FAQ on
<a href="http://www.contoso.com">
Contoso</a>'s web site for more information:
<a href="http://www.contoso.com/faq">FAQ</a>.
</span>
<a href="http://www.contoso.com/home">Home</a>

قصد داریم به کمک jQuery در span ایی با id مساوی ref، متن تمام لینک‌ها را ضخیم کنیم. کدام گزینه زیر را پیشنهاد می‌دهید؟

a) $("#ref").filter("a[href]").bold();
b) $("ref").filter("a").css("bold");
c) $("a").css({fontWeight:"bold"});
d) $("#ref a[href]").css({fontWeight:"bold"});


نظرات مطالب
ترفندهای یونیکد برای زبان‌های راست به چپ

یک نکته‌ی تکمیلی: اگر می‌خواهید کاربران موبایل به سادگی بتوانند اعداد صحیح را وارد کنند، از یک ورودی با ویژگی‌های type=tel و inputmode=numeric استفاده کنید:

<input type="tel" inputmode="numeric">

مزیت اینکار، نمایش خودکار صفحه کلید تمام عددی تنظیم شده‌ی بر روی حالت انگلیسی است؛ به این ترتیب مشکل «... در دستگاه‌های موبایل، زمانیکه صفحه کلید در حالت فارسی قرار دارد، اعداد را هم فارسی وارد می‌کند ...» اصلا رخ نمی‌دهد.

نظرات مطالب
سازماندهی برنامه‌های Angular توسط ماژول‌ها
توضیحات تکمیلی در مورد Core Module

اگر به CoreModule تعریف شده دقت کنید، یک چنین تعریفی در آن ذکر شده‌است:
   providers: [ UserRepositoryService ]
برای درک یک چنین تعریفی، نیاز است با نحوه‌ی عملکرد Angular Injector آشنا شد. برای این منظور فرض کنید برنامه‌ای را با سه ماژول طراحی کرده‌اید: یک App Module و دو ماژول معمولی و دیگری Lazy loaded. به ازای هر برنامه، Angular یک Root Injector را ایجاد می‌کند و کار آن تزریق سرویس‌ها، در مکان‌هایی است که به آن‌ها نیاز هست. این Root Injector نیز در App Module و در بالاترین سطح برنامه تشکیل می‌شود.
اکنون فرض کنید سرویسی را در ماژول معمولی غیر Lazy loaded تعریف کرده‌اید. Angular این سرویس را در Root Injector ثبت می‌کند. به این ترتیب این سرویس در کل برنامه قابل دسترسی می‌شود. اما اگر سرویسی را در یک ماژول Lazy loaded تعریف کنید، Angular کار ایجاد یک Injector جداگانه را برای آن ماژول و در داخل آن انجام می‌دهد. این Injector مجزا است از Root Injector قرار گرفته‌ی در App Module. سپس این سرویس در این Injector جدید ثبت می‌شود. به این ترتیب، وهله‌ی تهیه شده‌ی از این سرویس، تنها درون این ماژول Lazy loaded قابل دسترسی است. حتی اگر این دو سرویس ثبت شده‌ی در Root Injector و Injector مخصوص ماژول Lazy loaded از یک کلاس تهیه شوند، باز هم دو وهله‌ی مختلف از آن ارائه می‌شوند. برای مثال اگر UserRepositoryService را در ماژول معمولی و ماژول Lazy loaded مورد استفاده قرار دهیم، دو وهله‌ی مجزای از آن‌ها تشکیل خواهد شد که دیگر با هم همگام نبوده و کار ردیابی اطلاعات کاربر جاری سیستم را با مشکل مواجه می‌کنند.
ایجاد وهله‌ی دوم از یک سرویس، تنها منحصر است به ماژول‌های Lazy loaded. اما اگر این سرویس در ماژول‌های معمولی مختلفی مورد استفاده قرار گیرد، Angular تنها یک وهله از آن‌را ایجاد خواهد کرد. به همین جهت است که در این‌جا CoreModule را تعریف کردیم. این ماژول مکانی است که قرار است سرویس‌های اشتراکی در کل برنامه در آن قرار گیرند. چون این ماژول هیچگاه Lazy loaded نخواهد شد، هرگاه سرویسی را توسط آن ارائه دهیم، در Root Injector مربوط به App Module ثبت می‌شود. به این ترتیب تبدیل به یک Singleton Service قابل دسترسی در کل برنامه می‌شود.
باید دقت داشت که از لحاظ فنی، تفاوتی بین Core Module و یک ماژول معمولی غیر Lazy loaded نیست و بیشتر هدف از آن نظم بخشیدن به تعریف سرویس‌هایی است که قرار است در طول عمر برنامه و در تمام انواع ماژول‌های آن، توسط یک وهله قابل دسترسی شوند.
بنابراین تفاوتی نمی‌کند که یک سرویس را درون Core Module تعریف کنید و یا یک ماژول معمولی. این سرویس همواره در Root Injecror ثبت خواهد شد؛ مگر اینکه آن ماژول Lazy loaded باشد. در این حالت اگر سرویسی تنها قرار است در یک ماژول خاص استفاده شود، بهتر است آن‌را جهت مدیریت بهتر برنامه، درون همان پوشه تعریف کرد. هرچند از لحاظ فنی، این سرویس‌ها نهایتا در Root Injector مربوط به App Module ثبت و ارائه می‌شوند (از این لحاظ فرقی بین یک Core Module و Feature Modules نیست).
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت پنجم - تکمیل مستندات نوع و فرمت‌های مجاز خروجی و دریافتی API
زمانیکه کنترلر یک API را توسط قالب‌های پیش‌فرض آن ایجاد می‌کنیم، یک سری اکشن متد پیش‌فرض Get/Post/Put/Delete در آن قابل مشاهده هستند. می‌توان این نوع خروجی این نوع متدها را به نحو ساده‌تری نیز مستند کرد:
namespace OpenAPISwaggerDoc.Web.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ConventionTestsController : ControllerBase
    {
        // GET: api/ConventionTests/5
        [HttpGet("{id}", Name = "Get")]
        [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
        public string Get(int id)
        {
            return "value";
        }
در اینجا با ذکر ویژگی ApiConventionMethod، از نوع DefaultApiConventions، برای تولید مستندات خروجی متدی از نوع Get استفاده شده‌است. اگر به تعریف کلاس توکار  DefaultApiConventions مراجعه کنیم، در مورد متد Get، یک چنین ویژگی‌هایی را به صورت خودکار اعمال می‌کند:
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace Microsoft.AspNetCore.Mvc
{
    public static class DefaultApiConventions
    {
        [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
        [ProducesDefaultResponseType]
        [ProducesResponseType(200)]
        [ProducesResponseType(404)]
        public static void Get(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)] 
object id);
    }
}
البته باید دقت داشت که DefaultApiConventions برای قالب پیش‌فرض کنترلرهای API طراحی شده‌است و همچنین اگر فیلترهای سراسری را مانند قسمت قبل فعال کرده باشیم، اعمال نخواهند شد و از همان فیلترهای سراسری استفاده می‌شود.
امکان اعمال DefaultApiConventions به تمام متدهای یک کنترلر API نیز به صورت زیر با استفاده از ویژگی ApiConventionType اعمال شده‌ی به کلاس کنترلر میسر است:
namespace OpenAPISwaggerDoc.Web.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [ApiConventionType(typeof(DefaultApiConventions))]
    public class ConventionTestsController : ControllerBase
یا حتی می‌توان بجای اعمال دستی ApiConventionType به تمام کنترلرهای API، آن‌را به کل پروژه و اسمبلی جاری اعمال کرد:
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
اینکار را در کلاس Startup و پیش‌از تعریف فضای نام آن به نحو فوق می‌توان انجام داد. به این ترتیب DefaultApiConventions، به تمام کنترلرهای موجود در این اسمبلی اعمال می‌شوند. بنابراین با اعمال سراسری آن می‌توان ApiConventionType اعمالی بر کلاس ConventionTestsController را حذف کرد.


ایجاد ApiConventions سفارشی

همانطور که عنوان شد، اگر متدهای API شما دقیقا همان نام‌های پیش‌فرض Get/Post/Put/Delete را داشته باشند، توسط DefaultApiConventions مدیریت خواهند شد. در سایر حالات، مثلا اگر بجای نام Post، از نام Insert استفاده شد، باید ApiConventions سفارشی را ایجاد کرد:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace OpenAPISwaggerDoc.Web.AppConventions
{
    public static class CustomConventions
    {
        [ProducesDefaultResponseType]
        [ProducesResponseType(StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
        public static void Insert(
            [ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
            [ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
            object model)
        { }
    }
}
همانطور که ملاحظه می‌کنید، نحوه‌ی تشکیل این کلاس، با public static class DefaultApiConventions توکاری که پیشتر در مورد آن بحث شد، یکی است. نوع کلاس آن static است و با نام متدی که قصد اعمال به آن‌را داریم، سازگاری دارد. سپس تعدادی ویژگی خاص، به این متد اعمال شده‌اند.
پس از آن برای اعمال این ApiConventions جدید می‌توان به صورت زیر عمل کرد:
namespace OpenAPISwaggerDoc.Web.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ConventionTestsController : ControllerBase
    {
        [HttpPost]
        [ApiConventionMethod(typeof(CustomConventions), nameof(CustomConventions.Insert))]
        public void Insert([FromBody] string value)
        {
        }
در اینجا حالت نوع ApiConventionMethod به کلاس جدید CustomConventions اشاره می‌کند و نام متد آن نیز Insert درنظر گرفته شده‌است. در این حالت حتی اگر نام این اکشن متد را به InsertTest تغییر دهیم، باز هم کار می‌کند؛ چون بر اساس پارامتر دوم ویژگی ApiConventionMethod عمل کرده و متد متناظر را پیدا می‌کند. اما اگر آن‌را توسط ApiConventionType به خود کنترلر اعمال کنیم، فقط بر اساس ApiConventionNameMatch است که باز هم به متد InsertTest اعمال خواهد شد؛ چون در اینجا Prefix همان معنای StartsWith را می‌دهد. به علاوه در اینجا object model به عنوان پارامتر تعریف شده‌است و در سمت اکشن متد کنترلر، string value را داریم. در این مورد نیز ویژگی‌های اعمال شده به معنای صرفنظر از نوع و نام پارامتر تعریف شده‌ی در ApiConvention ما هستند (Any در اینجا به معنای صرفنظر از تطابق دقیق است).


سؤال: آیا استفاده‌ی از این ApiConventions ایده‌ی خوبی است؟

همانطور که در ابتدای بحث نیز عنوان شد، اگر فیلترهای سراسری را مانند قسمت قبل فعال کرده باشیم، از اعمال ApiConventions صرفنظر می‌شود. همچنین حالت پیش‌فرض آن‌ها برای حالت‌های متداول و ساده مفید هستند و برای سایر حالات باید کدهای زیادی را نوشت. به همین جهت خود مایکروسافت هم استفاده‌ی از ApiConventions را صرفا برای کنترلرهای API ای که دقیقا مطابق با قالب پیش‌فرض آن‌ها تهیه شده‌اند، توصیه می‌کند. بنابراین استفاده‌ی از Attributes که در قسمت قبل آن‌ها را بررسی کردیم، مقدم هستند بر استفاده‌ی از ApiConventions و تعدادی از بهترین تجربه‌های کاربری در این زمینه به شرح زیر می‌باشند:
- از API Analyzers که در قسمت قبل معرفی شد، برای یافتن کمبودهای نقایص مستندات استفاده کنید.
- از ویژگی ProducesDefaultResponseType استفاده کنید؛ اما تا جائیکه می‌توانید، جزئیات ممکن را به صورت صریحی مستند نمائید.
- Attributes را به صورت سراسری معرفی کنید.


بهبود مستندات Content negotiation

فرض کنید می‌خواهید لیست کتاب‌های یک نویسنده را دریافت کنید. در اینجا خروجی ارائه شده، با فرمت JSON تولید می‌شود؛ اما ممکن است XML ای نیز باشد و یا حالت‌های دیگر، بسته به تنظیمات برنامه. کار Content negotiation این است که مصرف کننده‌ی یک API، دقیقا مشخص کند، چه نوع فرمت خروجی را مدنظر دارد. هدری که برای این منظور استفاده می‌شود، accept header نام‌دارد و ذکر آن اجباری است؛ هر چند تعدادی از APIها بدون وجود آن نیز سعی می‌کنند حالت پیش‌فرضی را ارائه دهند.


Swagger-UI به نحوی که در تصویر فوق ملاحظه می‌کنید، امکان انتخاب Accept header را مسیر می‌کند. در این حالت اگر application/json را انتخاب کنیم، خروجی JSON ای را دریافت می‌کنیم. اما اگر text/plain را انتخاب کنیم، چون توسط API ما پشتیبانی نمی‌شود، خروجی از نوع 406 یا همان Status406NotAcceptable را دریافت خواهیم کرد. بنابراین وجود گزینه‌ی text/plain در اینجا غیرضروری و گمراه کننده‌است و نیاز است این مشکل را برطرف کرد:
namespace OpenAPISwaggerDoc.Web.Controllers
{
    [Produces("application/json")]
    [Route("api/authors/{authorId}/books")]
    [ApiController]
    public class BooksController : ControllerBase
در اینجا ویژگی جدیدی را به نام Produces مشاهده می‌کنید که به کل اکشن متدهای یک کنترلر API اضافه شده‌است. کار آن محدود کردن فرمت خروجی اکشن متدها با ذکر media-types مورد نظر است.
پس از اعمال این ویژگی، تاثیر آن‌را بر روی Swagger-UI در شکل زیر مشاهده می‌کنید که اینبار تنها به یک مورد مشخص، محدود شده‌است:


در اینجا اگر قصد داشته باشیم خروجی XML را نیز پشتیبانی کنیم، می‌توان به صورت زیر عمل کرد:
- ابتدا در کلاس Startup، نیاز است OutputFormatter متناظری را به سیستم معرفی نمود:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(setupAction =>
            {
                setupAction.OutputFormatters.Add(new XmlSerializerOutputFormatter());
- سپس ویژگی Produces را نیز تکمیل می‌کنیم تا این نوع خروجی را پشتیبانی کند:
namespace OpenAPISwaggerDoc.Web.Controllers
{
    [Produces("application/json", "application/xml")]
    [Route("api/authors/{authorId}/books")]
    [ApiController]
    public class BooksController : ControllerBase
با این خروجی:

نکته‌ی مهم: اگر Produces را اصلاح نکنیم، تعریف XmlSerializerOutputFormatter و ارسال یک درخواست با هدر Accept از نوع application/xml، هیچ تاثیری نداشته و باز هم JSON بازگشت داده می‌شود.


در این حالت اگر Controls Accept header را در UI از نوع xml انتخاب کنیم و سپس با کلیک بر روی دکمه‌ی try it out و ذکر id یک نویسنده، لیست کتاب‌های او را درخواست کنیم، خروجی نهایی XML ای آن قابل مشاهده خواهد بود:


البته تا اینجا فقط Swagger-UI را جهت محدود کردن به دو نوع خروجی با فرمت JSON و XML، اصلاح کرده‌ایم؛ اما این مورد به معنای محدود کردن سایر ابزارهای آزمایش یک API مانند postman نیست. در این نوع موارد، تمام مدیاتایپ‌های ارسالی پشتیبانی نشده، سبب تولید خروجی با فرمت JSON می‌شوند. برای محدود کردن آن‌ها به خروجی از نوع 406 می‌توان تنظیم ReturnHttpNotAcceptable را به true انجام داد تا اگر برای مثال درخواست application/xyz ارسال شد، صرفا یک استثناء بازگشت داده شود و نه خروجی JSON:

namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(setupAction =>
            {
                setupAction.ReturnHttpNotAcceptable = true; // Status406NotAcceptable


بهبود مستندات نوع بدنه‌ی درخواست


تا اینجا فرمت accept header را دقیقا مشخص و مستند کردیم؛ اما اگر به تصویر فوق دقت کنید، در حین ارسال اطلاعاتی از نوع POST به سرور، چندین نوع Request body را می‌توان انتخاب کرد که الزاما تمام آن‌ها توسط API ما پشتیبانی نمی‌شود. برای رفع این مشکل می‌توان از ویژگی Consumes استفاده کرد که نوع مدیتاتایپ‌های مجاز ورودی را مشخص می‌کند:
namespace OpenAPISwaggerDoc.Web.Controllers
{
    [Produces("application/json", "application/xml")]
    [Route("api/authors/{authorId}/books")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        /// <summary>
        /// Create a book for a specific author
        /// </summary>
        /// <param name="authorId">The id of the book author</param>
        /// <param name="bookForCreation">The book to create</param>
        /// <returns>An ActionResult of type Book</returns>
        [HttpPost()]
        [Consumes("application/json")]
        [ProducesResponseType(StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<Book>> CreateBook(
            Guid authorId,
            [FromBody] BookForCreation bookForCreation)
        {
بعد از این تغییر، نوع بدنه‌ی درخواست نیز محدود می‌شود:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-05.zip
مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت سوم
پس از بررسی ساختار یک پروژه‌ی افزونه پذیر و همچنین بهبود توزیع فایل‌های استاتیک آن، اکنون نوبت به کار با داده‌ها است. هدف اصلی آن نیز داشتن مدل‌های اختصاصی و مستقل Entity framework code-first به ازای هر افزونه است و سپس بارگذاری و تشخیص خودکار آن‌ها در Context مرکزی برنامه.

پیشنیازها
- آشنایی با مباحث Migrations در EF Code first
- آشنایی با مباحث الگوی واحد کار
- چگونه مدل‌های EF را به صورت خودکار به Context اضافه کنیم؟
- چگونه تنظیمات مدل‌های EF را به صورت خودکار به Context اضافه کنیم؟


کدهایی را که در این قسمت مشاهده خواهید کرد، در حقیقت همان برنامه‌ی توسعه یافته «آشنایی با مباحث الگوی واحد کار» است و از ذکر قسمت‌های تکراری آن جهت طولانی نشدن مبحث، صرفنظر خواهد شد. برای مثال Context و مدل‌های محصولات و گروه‌های آن‌ها به همراه کلاس‌های لایه سرویس برنامه‌ی اصلی، دقیقا همان کدهای مطلب «آشنایی با مباحث الگوی واحد کار» است.


تعریف domain classes مخصوص افزونه‌ها

در ادامه‌ی پروژه‌ی افزونه پذیر فعلی، پروژه‌ی class library جدیدی را به نام MvcPluginMasterApp.Plugin1.DomainClasses اضافه خواهیم کرد. از آن جهت تعریف کلاس‌های مدل افزونه‌ی یک استفاده می‌کنیم. برای مثال کلاس News را به همراه تنظیمات Fluent آن به این پروژه‌ی جدید اضافه کنید:
using System.Data.Entity.ModelConfiguration;
 
namespace MvcPluginMasterApp.Plugin1.DomainClasses
{
    public class News
    {
        public int Id { set; get; }
 
        public string Title { set; get; }
 
        public string Body { set; get; }
    }
 
    public class NewsConfig : EntityTypeConfiguration<News>
    {
        public NewsConfig()
        {
            this.ToTable("Plugin1_News");
            this.HasKey(news => news.Id);
            this.Property(news => news.Title).IsRequired().HasMaxLength(500);
            this.Property(news => news.Body).IsOptional().IsMaxLength();
        }
    }
}
این پروژه برای کامپایل شدن نیاز به بسته‌ی نیوگت ذیل دارد:
 PM> install-package EntityFramework

مشکل! برنامه‌ی اصلی، همانند مطلب «آشنایی با مباحث الگوی واحد کار» دارای domain classes خاص خودش است به همراه تنظیمات Context ایی که صریحا در آن مدل‌های متناظر با این پروژه در معرض دید EF قرار گرفته‌اند:
public class MvcPluginMasterAppContext : DbContext, IUnitOfWork
{
    public DbSet<Category> Categories { set; get; }
    public DbSet<Product> Products { set; get; }
اکنون برنامه‌ی اصلی چگونه باید مدل‌ها و تنظیمات سایر افزونه‌ها را یافته و به صورت خودکار به این Context اضافه کند؟ با توجه به اینکه این برنامه هیچ ارجاع مستقیمی را به افزونه‌ها ندارد.


تغییرات اینترفیس Unit of work جهت افزونه پذیری

در ادامه، اینترفیس بهبود یافته‌ی IUnitOfWork را جهت پذیرش DbSetهای پویا و همچنین EntityTypeConfigurationهای پویا، ملاحظه می‌کنید:
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IUnitOfWork : IDisposable
    {
        IDbSet<TEntity> Set<TEntity>() where TEntity : class;
        int SaveAllChanges();
        void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class;
        IList<T> GetRows<T>(string sql, params object[] parameters) where T : class;
        IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class;
        void SetDynamicEntities(Type[] dynamicTypes);
        void ForceDatabaseInitialize();
        void SetConfigurationsAssemblies(Assembly[] assembly);
    }
}
متدهای جدید آن:
SetDynamicEntities : توسط این متد در ابتدای برنامه، نوع‌های مدل‌های جدید افزونه‌ها به صورت خودکار به Context اضافه خواهند شد.
SetConfigurationsAssemblies : کار افزودن اسمبلی‌های حاوی تعاریف EntityTypeConfigurationهای جدید و پویا را به عهده دارد.
ForceDatabaseInitialize: سبب خواهد شد تا مباحث migrations، پیش از شروع به کار برنامه، اعمال شوند.

در کلاس Context ذیل، نحوه‌ی پیاده سازی این متدهای جدید را ملاحظه می‌کنید:
namespace MvcPluginMasterApp.DataLayer.Context
{
    public class MvcPluginMasterAppContext : DbContext, IUnitOfWork
    {
        private readonly IList<Assembly> _configurationsAssemblies = new List<Assembly>();
        private readonly IList<Type[]> _dynamicTypes = new List<Type[]>(); 
 
        public void ForceDatabaseInitialize()
        {
            Database.Initialize(force: true);
        }
 
        public void SetConfigurationsAssemblies(Assembly[] assemblies)
        {
            if (assemblies == null) return;
 
            foreach (var assembly in assemblies)
            {
                _configurationsAssemblies.Add(assembly);
            }
        }
 
        public void SetDynamicEntities(Type[] dynamicTypes)
        {
            if (dynamicTypes == null) return;
            _dynamicTypes.Add(dynamicTypes);
        }
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            addConfigurationsFromAssemblies(modelBuilder);
            addPluginsEntitiesDynamically(modelBuilder);
            base.OnModelCreating(modelBuilder);
        }
 
        private void addConfigurationsFromAssemblies(DbModelBuilder modelBuilder)
        {
            foreach (var assembly in _configurationsAssemblies)
            {
                modelBuilder.Configurations.AddFromAssembly(assembly);
            }
        }
 
        private void addPluginsEntitiesDynamically(DbModelBuilder modelBuilder)
        {
            foreach (var types in _dynamicTypes)
            {
                foreach (var type in types)
                {
                    modelBuilder.RegisterEntityType(type);
                }
            }
        }
    }
}
در متد استاندارد OnModelCreating، فرصت افزودن نوع‌های پویا و همچنین تنظیمات پویای آن‌ها وجود دارد. برای این منظور می‌توان از متدهای modelBuilder.RegisterEntityType و modelBuilder.Configurations.AddFromAssembly کمک گرفت.


بهبود اینترفیس IPlugin جهت پذیرش نوع‌های پویای EF

در قسمت اول، با اینترفیس IPlugin آشنا شدیم. هر افزونه باید دارای کلاسی باشد که این اینترفیس را پیاده سازی می‌کند. از آن جهت دریافت تنظیمات و یا ثبت تنظیمات مسیریابی و امثال آن استفاده می‌شود.
در اینجا متد GetEfBootstrapper آن کار دریافت تنظیمات EF هر افزونه را به عهد دارد.
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IPlugin
    {
        EfBootstrapper GetEfBootstrapper();
        //...به همراه سایر متدهای مورد نیاز
    }
 
    public class EfBootstrapper
    {
        /// <summary>
        /// Assemblies containing EntityTypeConfiguration classes.
        /// </summary>
        public Assembly[] ConfigurationsAssemblies { get; set; }
 
        /// <summary>
        /// Domain classes.
        /// </summary>
        public Type[] DomainEntities { get; set; }
 
        /// <summary>
        /// Custom Seed method.
        /// </summary>
        public Action<IUnitOfWork> DatabaseSeeder { get; set; }
    } 
}
ConfigurationsAssemblies مشخص کننده‌ی اسمبلی‌هایی است که حاوی تعاریف EntityTypeConfigurationهای افزونه‌ی جاری هستند.
DomainEntities بیانگر لیست مدل‌ها و موجودیت‌های هر افزونه است.
DatabaseSeeder کار دریافت منطق متد Seed را بر عهده دارد. برای مثال اگر افزونه‌ای نیاز است در آغاز کار تشکیل جداول آن، دیتای پیش فرض و خاصی را در بانک اطلاعاتی ثبت کند، می‌توان از این متد استفاده کرد. اگر دقت کنید این Action یک وهله از IUnitOfWork را به افزونه ارسال می‌کند. بنابراین در این طراحی جدید، اینترفیس IUnitOfWork به پروژه‌ی MvcPluginMasterApp.PluginsBase منتقل می‌شود. به این ترتیب دیگر نیازی نیست تا تک تک افزونه‌ها ارجاع مستقیمی را به DataLayer پروژه‌ی اصلی پیدا کنند.


تکمیل متد GetEfBootstrapper در افزونه‌ها

اکنون جهت معرفی مدل‌ها و تنظیمات EF آن‌ها، تنها کافی است متد GetEfBootstrapper هر افزونه را تکمیل کنیم:
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return new EfBootstrapper
            {
                DomainEntities = new[] { typeof(News) },
                ConfigurationsAssemblies = new[] { typeof(NewsConfig).Assembly },
                DatabaseSeeder = uow =>
                {
                    var news = uow.Set<News>();
                    if (news.Any())
                    {
                        return;
                    }
 
                    news.Add(new News
                    {
                        Title = "News 1",
                        Body = "news 1 news 1 news 1 ...."
                    });
 
                    news.Add(new News
                    {
                        Title = "News 2",
                        Body = "news 2 news 2 news 2 ...."
                    });
                }
            };
        }
در اینجا نحوه‌ی معرفی مدل‌های جدید را توسط خاصیت DomainEntities و تنظیمات متناظر را به کمک خاصیت ConfigurationsAssemblies مشاهده می‌کنید. باید دقت داشت که هر اسمبلی فقط باید یکبار معرفی شود و مهم نیست که چه تعداد تنظیمی در آن وجود دارند. کار یافتن کلیه‌ی تنظیمات از نوع EntityTypeConfigurationها به صورت خودکار توسط EF صورت می‌گیرد.
همچنین توسط delegate ایی به نام DatabaseSeeder، نحوه‌ی دسترسی به متد Set واحد کار و سپس استفاده‌ی از آن، برای تعریف متد Seed سفارشی نیز تکمیل شده‌است.


تدارک یک راه انداز EF، پیش از شروع به کار برنامه

در پوشه‌ی App_Start پروژه‌ی اصلی یا همان MvcPluginMasterApp، کلاس جدید EFBootstrapperStart را با کدهای ذیل اضافه کنید:
[assembly: PreApplicationStartMethod(typeof(EFBootstrapperStart), "Start")]
namespace MvcPluginMasterApp
{
    public static class EFBootstrapperStart
    {
        public static void Start()
        {
            var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
            using (var uow = SmObjectFactory.Container.GetInstance<IUnitOfWork>())
            {
                initDatabase(uow, plugins);
                runDatabaseSeeders(uow, plugins);
            }
        }
 
        private static void initDatabase(IUnitOfWork uow, IEnumerable<IPlugin> plugins)
        {
            foreach (var plugin in plugins)
            {
                var efBootstrapper = plugin.GetEfBootstrapper();
                if (efBootstrapper == null) continue;
 
                uow.SetDynamicEntities(efBootstrapper.DomainEntities);
                uow.SetConfigurationsAssemblies(efBootstrapper.ConfigurationsAssemblies);
            }
 
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MvcPluginMasterAppContext, Configuration>());
            uow.ForceDatabaseInitialize();
        }
 
        private static void runDatabaseSeeders(IUnitOfWork uow, IEnumerable<IPlugin> plugins)
        {
            foreach (var plugin in plugins)
            {
                var efBootstrapper = plugin.GetEfBootstrapper();
                if (efBootstrapper == null || efBootstrapper.DatabaseSeeder == null) continue;
 
                efBootstrapper.DatabaseSeeder(uow);
                uow.SaveAllChanges();
            }
        }
    }
}
در اینجا یک راه انداز سفارشی از نوع PreApplicationStartMethod تهیه شده‌است. Pre بودن آن به معنای اجرای کدهای متد Start این کلاس، پیش از آغاز به کار برنامه و پیش از فراخوانی متد Application_Start فایل Global.asax.cs است.
همانطور که ملاحظه می‌کنید، ابتدا لیست تمام افزونه‌های موجود، به کمک StructureMap دریافت می‌شوند. سپس می‌توان در متد initDatabase به متد GetEfBootstrapper هر افزونه دسترسی یافت و توسط آن تنظیمات مدل‌ها را یافته و به Context اصلی برنامه اضافه کرد. سپس با فراخوانی ForceDatabaseInitialize تمام این موارد به صورت خودکار به بانک اطلاعاتی اعمال خواهند شد.
کار متد runDatabaseSeeders، یافتن DatabaseSeeder هر افزونه، اجرای آن‌ها و سپس فراخوانی متد SaveAllChanges در آخر کار است.



کدهای کامل این سری را از اینجا می‌توانید دریافت کنید:
MvcPlugin
نظرات مطالب
پیاده سازی الگوی طراحی Memento

چنین پیاده سازی برای سازنده کلاس ConcreteOriginator و پیاده سازی برای متد  SetData  در کلاس ConcreteOriginator  انجام نشده است. 
ConcreteOriginator myOriginator = new ConcreteOriginator("Hello World!", StateType.ONE);

myOriginator.SetData("Goodbye!", StateType.TWO);

مطالب
سفارشی کردن صفحه بندی WebGrid در ASP.NET MVC
بعد از استفاده از گرید‌های Grid.mvc , JQGrid, Kendo و مشکلاتی که با هر کدام از آنها داشتم، در نهایت به WebGrid که به صورت توکار وجود دارد، برای استفاده جهت نمایش اطلاعات رسیدم؛ از این جهت که به کتابخانه‌ی جانبی نیازی ندارد و از نظر سرعت و لود شدن بهینه می‌باشد، البته با اضافه کردن یکسری کدهای css.
برای آشنایی بیشتر با این helper توصیه میکنم ابتدا این مقاله را مطالعه نمایید.
به صورت پیش فرش WbebGrid صفحه بندی را به صورت خیلی ساده فقط با نمایش اعداد و جهت نماهای جلو و عقب، نشان می‌دهد که برای پروژه‌های رسمی تا حدودی جالب نیست.

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


برای اینکار، ابتدا قبل از هر چیزی به یک متد الحاقی برای انجام صفحه بندی سفارشی سازی شده، نیاز داریم که کدهای این متد به صورت زیر خواهد بود:

public static class WebGridExtensions
{
    public static HelperResult PagerList(
        this WebGrid webGrid,
        WebGridPagerModes mode = WebGridPagerModes.NextPrevious | WebGridPagerModes.Numeric,
        string firstText = null,
        string previousText = null,
        string nextText = null,
        string lastText = null,
        int numericLinksCount = 5)
    {
        return PagerList(webGrid, mode, firstText, previousText, nextText, lastText, numericLinksCount, explicitlyCalled: true);
    }

    private static HelperResult PagerList(
        WebGrid webGrid,
        WebGridPagerModes mode,
        string firstText,
        string previousText,
        string nextText,
        string lastText,
        int numericLinksCount,
        bool explicitlyCalled)
    {
        
        int currentPage = webGrid.PageIndex;
        int totalPages = webGrid.PageCount;
        int lastPage = totalPages - 1;

        var ul = new TagBuilder("ul");
        var li = new List<TagBuilder>();

        
        if (ModeEnabled(mode, WebGridPagerModes.FirstLast)) {
            if (String.IsNullOrEmpty(firstText)) {
                firstText = "اولین";
            }

            var part = new TagBuilder("li") {
                InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(0), firstText)
            };

            if (currentPage == 0) {
                part.MergeAttribute("class", "disabled");
            }

            li.Add(part);

        }
        
        if (ModeEnabled(mode, WebGridPagerModes.NextPrevious)) {
            if (String.IsNullOrEmpty(previousText)) {
                previousText = "قبلی";
            }

            int page = currentPage == 0 ? 0: currentPage - 1;

            var part = new TagBuilder("li") {
                InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(page), previousText)
            };
            
            if (currentPage == 0) {
                part.MergeAttribute("class", "disabled");
            }

            li.Add(part);

        }


       if (ModeEnabled(mode, WebGridPagerModes.Numeric) && (totalPages > 1)) {
            int last = currentPage + (numericLinksCount / 2);
            int first = last - numericLinksCount + 1;
            if (last > lastPage) {
                first -= last - lastPage;
                last = lastPage;
            }
            if (first < 0) {
                last = Math.Min(last + (0 - first), lastPage);
                first = 0;
            }
            for (int i = first; i <= last; i++) {

                var pageText = (i + 1).ToString(CultureInfo.InvariantCulture);
                var part = new TagBuilder("li") {
                    InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(i), pageText)
                };

                if (i == currentPage) {
                    part.MergeAttribute("class", "active");
                }
                
                li.Add(part);

            }
        }
        
        if (ModeEnabled(mode, WebGridPagerModes.NextPrevious)) {
            if (String.IsNullOrEmpty(nextText)) {
                nextText = "بعدی";
            }
            
            int page = currentPage == lastPage ? lastPage: currentPage + 1;

            var part = new TagBuilder("li") {
                InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(page), nextText)
            };
            
            if (currentPage == lastPage) {
                part.MergeAttribute("class", "disabled");
            }

            li.Add(part);

        }
        
        if (ModeEnabled(mode, WebGridPagerModes.FirstLast)) {
            if (String.IsNullOrEmpty(lastText)) {
                lastText = "آخرین";
            }

            var part = new TagBuilder("li") {
                InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(lastPage), lastText)
            };
            
            if (currentPage == lastPage) {
                part.MergeAttribute("class", "disabled");
            }

            li.Add(part);

        }

        ul.InnerHtml = string.Join("", li);

        var html = "";
        if (explicitlyCalled && webGrid.IsAjaxEnabled) {
            var span = new TagBuilder("span");
            span.MergeAttribute("data-swhgajax", "true");
            span.MergeAttribute("data-swhgcontainer", webGrid.AjaxUpdateContainerId);
            span.MergeAttribute("data-swhgcallback", webGrid.AjaxUpdateCallback);

            span.InnerHtml = ul.ToString();
            html = span.ToString();

        } else {
            html = ul.ToString();
        }

        return new HelperResult(writer => {
            writer.Write(html);         
        });
    }

    private static String GridLink(WebGrid webGrid, string url, string text) 
    {
        TagBuilder builder = new TagBuilder("a");
        builder.SetInnerText(text);
        builder.MergeAttribute("href", url);
        if (webGrid.IsAjaxEnabled) {
            builder.MergeAttribute("data-swhglnk", "true");
        }
        return builder.ToString(TagRenderMode.Normal);
    }


    private static bool ModeEnabled(WebGridPagerModes mode, WebGridPagerModes modeCheck)
    {
        return (mode & modeCheck) == modeCheck;
    }

}
کلاس فوق باید در پوشه‌ی App_Code قرار گیرد.
پس از آن در View یی که اطلاعات را نمایش می‌دهید، فقط لازم است کد زیر را اضافه نمایید:
<div>
@grid.PagerList(mode: WebGridPagerModes.All)
</div>
تا اینجا متد مورد نظر برای انجام صفحه بندی گرید پیاده سازی شد. ادامه‌ی کار هم مشخص است؛ داشتن یک PartialView جهت نمایش لیست اطلاعات، پاس دادن دیتا به Partial و تمام.

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

PartialView لازم برای نمایش اطلاعات
تنظیمات لازم گرید :
@{  
    WebGrid grid = new WebGrid(Model,
                               rowsPerPage: 10,
                               ajaxUpdateContainerId: "grid");
    
    var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
تعیین فیلد‌های گرید :
    @grid.Table(
                            tableStyle: "table table-striped table-hover",
                    headerStyle: "webgrid-header",
          
            alternatingRowStyle: "webgrid-alternating-row",
            selectedRowStyle: "webgrid-selected-row",
            rowStyle: "webgrid-row-style",
            columns: grid.Columns(
                
                  grid.Column(columnName: "Name", header: "نام استان", style: "myfont"),
                  grid.Column(columnName: "NameEn", header: "نام استان ( انگلیسی )", style: "myfont"),
                  grid.Column(header: "", format: item => @Html.ActionLink("مدیریت شهرها", actionName: MVC.Admin.City.ActionNames.Index, controllerName: MVC.Admin.City.Name, routeValues: new {Code=item.Code },htmlAttributes:null)),
                 grid.Column(header: "",
                 style: "text-align-center-col smallcell",
                             format: item => @Html.ActionLink(linkText: "ویرایش", actionName: "Edit",
                             controllerName: "Province", routeValues: new { area = "Admin", code = item.Code },
                             htmlAttributes: new { @class = "btn-sm btn-info vertical-center" }))
                 ) )

<div>
    @grid.PagerList(mode: WebGridPagerModes.All)
</div>

خروجی حاصل به صورت زیر خواهد بود :


اگر طبق توضیحات بالا عمل کرده باشید، در نهایت صفحه بندی شما به صورت عمودی نمایش داده می‌شود؛ یعنی هر کدام از شماره صفحات در یک سطر. دلیل آن هم این است که تگ ul، کلاس .pagination  را ندارد. در کدهای بوت استراپ تعریف شده است که تمام li هایی که به صورت مستقیم داخل کلاس .pagination هستند خصوصیات مورد نظر را بگیرند.

برای این کار دو راه حل وجود دارد :

راه حل اول: تغییر کدهای css

کدهای  نوشته شده برای صفحه بندی در بوت استراپ را از حالت زیر:

.pagination > li
به حالت زیر تغییر دهید:
.pagination li

یادآوری : علامت < در CSS یعنی به صورت مستقیم و در شاخه‌ی اول.

راه حل دوم - افزودن کلاس .pagination  به تگ ul:
ابتدا کلاس .pagination  را از تگ div حذف نمایید:
<div >
    @grid.PagerList(mode: WebGridPagerModes.All)
</div>

و در کدهایی کلاس WebGridExtensions،  در قسمتی که تگ ul اصافه می‌شود، کلاس مورد نظر را به آن اضافه می‌کنیم:
  var ul = new TagBuilder("ul");
        ul.AddCssClass("pagination");


دانلود کدهای این مثال


نکته‌ای در مورد Webgrid
اگر نیاز داشتید به یکباره تمام اطلاعات را در گرید لود نکنید و به صورت n تاn تا رکورد‌ها را نمایش دهید، در این حالت پس از پاس دادن لیستی از اطلاعات به View مورد نظر لازم است تعداد کل رکورد‌ها را در یک متغییر به سمت View بفرستید. این کار به این دلیل می‌باشد که بتوان صفحه بندی را تولید کرد. برای این کار در بخش تنظیمات Webgrid مقدار source را برابر null قرار دهید و از قطعه کد زیر جهت بایند کردن گرید، بعد از کدهای تنظیمات WebGrid استفاده نمایید:
grid.Bind(Model, rowCount: (int)ViewBag.PageCount);
مطالب
ASP.NET MVC #9

مروری بر HTML Helpers استاندارد مهیا در ASP.NET MVC

یکی از اهداف وجودی Server controls در ASP.NET Web forms، رندر خودکار HTML است. برای مثال Menu control، TreeView control، GridView و امثال آن کار تولید تگ‌های table، tr و بسیاری موارد دیگر را در پشت صحنه برای ما انجام می‌دهند. اما در ASP.NET MVC، هدف رسیدن به یک markup ساده و تمیز است که 100 درصد بر روی اجزای آن کنترل داشته باشیم و این مورد به صورت ضمنی به این معنا است که در اینجا تمام این HTMLها را باید خودمان تولید کنیم. البته در عمل خیر. یک نمونه از آن‌را در قسمت قبل مشاهده کردیم که چطور می‌توان منطق تولید تگ‌های HTML را کپسوله سازی کرد و بارها مورد استفاده قرار داد. به علاوه فریم ورک ASP.NET MVC نیز به همراه تعدادی HTML helper توکار ارائه شده است مانند CheckBox، ActionLink، RenderPartial و غیره که کار تولید تگ‌های HTML ضروری و پایه را برای ما ساده می‌کنند.
یک مثال:
@Html.ActionLink("About us", "Index", "About")

در اینجا از متدی به نام ActionLink استفاده شده است. شیء Html هم وهله‌ای از کلاس HtmlHelper است که در تمام Viewها قابل دسترسی می‌باشد.
در این متد،‌ اولین پارامتر، متن نمایش داده شده به کاربر را مشخص می‌کند، پارامتر سوم، نام کنترلری است که مورد استفاده قرار می‌گیرد و پارامتر دوم، نام متد یا اکشنی در آن است که فراخوانی خواهد شد (البته هر کدام از این HtmlHelperها به همراه تعداد قابل توجهی overload هم هستند).
زمانیکه این صفحه را رندر کنیم، به خروجی زیر خواهیم رسید:
<a href="/About">About us</a>

در این لینک نهایی خبری از متد Index ایی که معرفی کردیم، نیست. چرا؟
متد ActionLink بر اساس تعاریف پیش فرض مسیریابی برنامه، سعی می‌کند بهترین خروجی را ارائه دهد. مطابق تعاریف پیش فرض برنامه، متد Index، اکشن پیش فرض کنترلرهای برنامه است. بنابراین ضرورتی به ذکر آن ندیده است.

مثالی دیگر:
همان کلاس‌های Product و Products قسمت هفتم را در نظر بگیرید (قسمت بررسی «ساختار پروژه مثال جاری» در آن مثال). همچنین به اطلاعات «نوشتن HTML Helpers ویژه، به کمک امکانات Razor» قسمت هشتم هم نیاز داریم.
اینبار می‌خواهیم بجای نمایش لیست ساده‌ای از محصولات،‌ ابتدا نام آن‌ها را به صورت لینک‌هایی در صفحه نمایش دهیم. در ادامه پس از کلیک کاربر روی یک نام، توضیحات بیشتری از محصول انتخابی را در صفحه‌ای دیگر ارائه نمائیم. کدهای View ما اینبار به شکل زیر تغییر می‌کنند:

@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
<h2>Index</h2>

@GetProductsList(@Model)

توضیحات:
ابتدا یک helper method را تعریف کرده‌ایم و به کمک Html.ActionLink، از نام و شماره محصول، جهت تولید لینک‌های نمایش جزئیات هر یک از محصولات کمک گرفته‌ایم. بنابراین در کنترلر خود نیاز به متد جدیدی به نام Details خواهیم داشت که پارامتری از نوع ProductNumber را دریافت می‌کند. سپس جزئیات این محصول را یافته و در View متناظر با خودش ارائه خواهد داد. پارامتر سومی که در متد ActionLink بکارگرفته شده در اینجا مشاهده می‌کنید، یک anonymously typed object است و توسط آن خواصی را تعریف خواهیم کرد که توسط تعاریف مسیریابی تعریف شده در فایل Global.asax.cs،‌ قابل تفسیر و تبدیل به لینک‌های مرتبط و صحیحی باشد.
اکنون اگر این مثال را اجرا کنیم، اولین لینک تولیدی آن به این شکل خواهد بود:
http://localhost/Home/Details/D123

در اینجا به یک نکته مهم هم باید دقت داشت؛ نام کنترلر به صورت خودکار به این لینک اضافه شده است. بنابراین بهتر است از ایجاد دستی این نوع لینک‌ها خودداری کرده و کار را به متدهای استاندارد فریم ورک واگذار نمود تا بهترین خروجی را دریافت کنیم.
البته اگر الان بر روی این لینک کلیک نمائیم، با پیغام 404 مواجه خواهیم شد. برای تکمیل این مثال، متد Details را به کنترلر تعریف شده اضافه خواهیم کرد:

using System.Linq;
using System.Web.Mvc;
using MvcApplication5.Models;

namespace MvcApplication5.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}

public ActionResult Details(string id)
{
var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
if (product == null)
return View("Error");
return View(product);
}
}
}

در متد Details، ابتدا ProductNumber دریافت شده و سپس شیء محصول متناظر با آن، به View این متد، بازگشت داده می‌شود. اگر بر اساس ورودی دریافتی، محصولی یافت نشد، کاربر را به View ایی به نام Error که در پوشه Views/Shared قرار گرفته است، هدایت می‌کنیم.
برای اضافه کردن این View هم بر روی متد کلیک راست کرده و گزینه Add view را انتخاب کنید. چون یک شیء strongly typed از نوع Product را قرار است به View ارسال کنیم (مانند مثال قسمت پنجم)، می‌توان در صفحه باز شده تیک Create a strongly typed view را گذاشت و سپس Model class را از نوع Product انتخاب کرد و در قسمت Scaffold template هم Details را انتخاب نمود. به این ترتیب Code generator توکار VS.NET قسمتی از کار تولید View را برای ما انجام داده و بدیهی است اکنون سفارشی سازی این View تولیدی که قسمت عمده‌ای از آن تولید شده است، کار ساده‌ای می‌باشد:

@model MvcApplication5.Models.Product

@{
ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
<legend>Product</legend>

<div class="display-label">ProductNumber</div>
<div class="display-field">@Model.ProductNumber</div>

<div class="display-label">Name</div>
<div class="display-field">@Model.Name</div>

<div class="display-label">Price</div>
<div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
@Html.ActionLink("Back to List", "Index")
</p>

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


یک نکته:
اگر سعی کنیم متد @helper GetProductsList فوق را در پوشه App_Code، همانند قسمت قبل قرار دهیم، به متد Html.ActionLink دسترسی نخواهیم داشت. چرا؟
پیغام خطایی که ارائه می‌شود این است:
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink' 

به این معنا که در وهله‌ای از شیء System.Web.WebPages.Html.HtmlHelper، به دنبال متد ActionLink می‌گردد. در حالیکه ActionLink مورد نظر به کلاس System.Web.Mvc.HtmlHelper مرتبط می‌شود.
یک راه حل آن به صورت زیر است. به هر متد helper یک آرگومان WebViewPage page را اضافه می‌کنیم (به همراه دو فضای نامی که به ابتدای فایل اضافه می‌شوند)

@using System.Web.Mvc
@using System.Web.Mvc.Html

@using MvcApplication5.Models

@helper GetProductsList(WebViewPage page, List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
سپس برای استفاده از آن در یک View خواهیم داشت:
@MyHelpers.GetProductsList(this, @Model)


متد ActionLink و عبارات فارسی

متد ActionLink آدرس‌های وبی را که تولید می‌کند، URL encoded هستند. برای نمونه اگر رشته‌ای که قرار است به عنوان پارامتر به اکشن متد ما ارسال شود، مساوی Hello World است، آن‌را به صورت Hello%20World در صفحه درج می‌کند. البته این مورد مشکلی را در سمت متدهای کنترلرها ایجاد نمی‌کند، چون کار URL decoding خودکار است. اما ... اگر مقداری که قرار است ارسال شود مثلا «مقدار یک» باشد، آدرس تولیدی این شکل را خواهد داشت:

http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83

و اگر این URL encoding انجام نشود، فقط اولین قسمت قبل از فاصله به متد ارسال می‌گردد.
مرورگرهایی مثل فایرفاکس و کروم، مشکلی با نمایش این لینک به شکل اصلی فارسی آن ندارند (حین نمایش، URL decoding را اعمال می‌کنند). اما اگر مرورگر مثلا IE8 باشد، کاربر دقیقا به همین شکل آدرس‌ها را در نوار آدرس مرورگر خود مشاهده خواهد کرد که آنچنان زیبا نیستند.
حل این مشکل، یک نکته کوچک را به همراه دارد. اگر href تولیدی به شکل زیر باشد:

<li><a href="/Home/Details/مقدار یک">Super Fast Bike</a></li>

IE حین نمایش نهایی آن، آن‌را فارسی نشان خواهد داد. حتی زمانیکه کاربر بر روی آن کلیک کند، به صورت خودکار کاراکترهایی را که لازم است encode نماید، به نحو صحیحی در URL نهایی قابل مشاهده در نوار آدرس‌ها ظاهر خواهد کرد. برای مثال %20 را به صورت خودکار اضافه می‌کند و نگرانی از این لحاظ وجود نخواهد داشت که الان بین دو کلمه فاصله‌ای وجود دارد یا خیر (مرورگرهای دیگر هم دقیقا همین رفتار را در مورد لینک‌های داخل صفحه دارند).
خلاصه این توضیحات متد کمکی زیر است:

@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
@Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}

و برای نمونه نحوه استفاده از آن به شکل زیر خواهد بود:

@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
}
</ul>
}

ضمن اینکه باید درنظر داشت کلا این نوع طراحی مشکل دارد! برای مثال فرض کنید که در این مثال، جزئیات، نمایش دهنده مطلب ارسالی در یک بلاگ است. یعنی یک سری عنوان و جزئیات متناظر با آن‌ها در دیتابیس وجود دارند. اگر آدرس مطالب به این شکل باشد http://site/blog/details/text، به این معنا است که این text مساوی است با primary key جدول بانک اطلاعاتی. یعنی وبلاگ نویس سایت شما فقط یکبار در طول عمر این برنامه می‌تواند بگوید «سال نو مبارک!». دفعه‌ی بعد به علت تکراری بودن، مجاز به ارسال پیام تبریک دیگری نخواهد بود! به همین جهت بهتر است طراحی را به این شکل تغییر دهید http://site/blog/details/id/text. در اینجا id همان primary key خواهد بود. Text هم عنوان مطلب. Id به جهت خوشایند بانک اطلاعاتی و Text هم برای خوشایند موتورهای جستجو در این URL قرار دارند. مطابق تعاریف مسیریابی برنامه، Text فقط حالت تزئینی داشته و پردازش نخواهد شد.
از این نوع ترفندها زیاد به کار برده می‌شوند. برای نمونه به URL مطالب انجمن‌های معروف اینترنتی دقت کنید. عموما یک عدد را به همراه text مشاهده می‌کنید. عدد در برنامه پردازش می‌شود، متن هم برای موتورهای جستجو درنظر گرفته شده است.



مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 1 - مقدمه
مقدمه

زمانیکه یک برنامه را بر پایه‌ی شیء گرایی طراحی می‌کنید و می‌نویسید، به صورت معمول جریان وابستگی‌ها در برنامه‌ی شما به صورت زیر است:


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

به عبارت دیگر در طراحی ساخت یافته، کلاس‌های سطح بالا، به کلاس‌های سطح پایین وابسته‌اند. این مسئله دو مشکل را ایجاد می‌کند:
  1. هر تغییری در کلاس‌های سطح پایین ممکن است باعث ایجاد اشکالی در کلاس‌های سطح بالا گردد.
  2. استفاده‌ی مجدد از کلاس‌های سطح بالا در جاهای دیگر مشکل است؛ زیرا وابستگی مستقیمی به کلاس‌های سطح پایین دارند.

اصل معکوس سازی / وارونگی وابستگی‌ها Dependency Inversion Principle
 
یکی از اصول پنجگانه‌ی طراحی برنامه‌های شیء گرا  که با نام اصول SOLID  شناخته می‌شوند، اصل «وارونگی وابستگی‌ها» است که روشی برای مشکل جفت شدگی و وابستگی کلاس‌ها به یکدیگر را به صورت تئوری ارائه می‌دهد.

 اصل وارونگی وابستگی‌ها بیان می‌کند:
  •   ماژول‌های (کلاس‌های) سطح بالا نباید به ماژولهای (کلاس‌های) سطح پایین وابسته باشند و هر دو باید به انتزاعات وابسته باشند (برای مثال interface‌ها).
  •   انتزاعات نباید وابسته به جزئیات باشند؛ بلکه جزئیات (پیاده سازی) باید وابسته به انتزاعات باشند.

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

در شکل زیر، حالت عادی جریان کنترل را می‌بینید .
 


همانطور که می‌بینید، کلاس M برای اجرا، وابسته به کلاس N و متد F در درون آن است. در اینجا ما با استفاده از اینترفیس‌ها می‌توانیم جریان کنترل را معکوس یا وارونه کنیم که به این عمل «وارونگی کنترل یا Inversion of Control» می‌گویند.


 
شیء گرایی در واقع در مورد نحوه‌ی جریان کنترل است. در اینجا اینترفیس‌ها به ما کنترل کاملی را بر جریان کنترل (Flow of control) می‌دهند که با استفاده از این امکان می‌توانیم از نوشتن کدهای جفت شده، شکننده و کلاس‌هایی یکبار مصرف، بپرهیزیم.


الگوی Dependency Injection 
تزریق وابستگی یا Dependency Injection، یک الگوی طراحی است که از آن برای طراحی و پیاده سازی IoC Container‌ها استفاده می‌شود. این الگو به ما اجازه می‌دهد که اشیاء وابسته را خارج از کلاس بسازیم و آنها را به طریقی دیگر به کلاس، جهت استفاده ارائه دهیم. به‌وسیله‌ی DI ما ساخت و اتصال اشیاء وابسته به کلاس را از تعریف آن خارج می‌کنیم.

الگوی تزریق وابستگی 3 نوع کلاس را درگیر می‌کند:
  •  کلاس کلاینت / Client Class : کلاس کلاینت (کلاس وابسته) کلاسی است که به کلاس سرویس وابسته است .
  • کلاس سرویس  /  Service Class :  کلاس سرویس (وابستگی) کلاسی است که یک سرویس را به کلاس کلاینت ارائه می‌دهد.
  •  کلاس تزریق کننده / Injector Class  :  کلاس تزریق کننده، نمونه‌ای از کلاس سرویس را ساخته و به کلاس کلاینت، تزریق می‌کند.

شکل زیر وابستگی بین کلاس‌ها را شرح می‌دهد:

 
همانطور که می‌بینید، کلاس Injector، نمونه‌ای از کلاس سرویس را می‌سازد و آن را به نمونه‌ای از کلاس Client تزریق می‌کند. با این کار، DI، وظیفه‌ی ساخت یک نمونه از کلاس Service را از درون کلاس Client جدا می‌کند.

انواع تزریقات وابستگی‌ها:
به صورت کلی به سه روش و در سه مکان، امکان تزریق وابستگی کلاس سرویس، درون کلاس کلاینت وجود دارد:
  •   تزریق درون سازنده / Constructor Injection  : در تزریق درون سازنده، در سازنده‌ی کلاس کلاینت، لیستی از سرویس‌های مورد نیاز کلاس، که کلاس، برای عملکرد خود به آن‌ها «وابسته» است، ثبت می‌شوند و کلاس Injector، سرویس (وابستگی) مورد نظر را درون سازنده‌ی کلاس Client ارائه می‌دهد.
  •   تزریق درون Property  کلاس / Property Injection : در این حالت که همچنین با نام (Setter Injection) هم شناخته می‌شود، تزریق کننده، وابستگی را به وسیله‌ی یک Property عمومی کلاس کلاینت ارائه می‌دهد.
  •  تزریق درون متد / Method Injection  : در این حالت، خود کلاس کلاینت، یک پیاده سازی از یک interface را ارائه می‌کند که درون آن متدهایی برای ارائه‌ی وابستگی‌ها به کلاینت تعریف شده‌اند. در این وضعیت، تزریق کننده از این اینترفیس برای ارائه‌ی وابستگی‌ها به کلاینت درون متدها، استفاده می‌کند. 

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

Inversion of Control Container
در واژگان فنی مهندسی نرم افزار، Container (محفظه)  به جزیی از برنامه گفته می‌شود که می‌تواند اجزای دیگر برنامه را در بر بگیرد.  IoC Container ‌ها در واقع فریم ورک‌ها/چارچوب‌ها یا کتابخانه‌های برنامه نویسی هستند که ما در آنها می‌توانیم اشیاء مختلف را به سبک‌های خاصی تعریف و ثبت کنیم و در مواقع لزوم آنها را واکشی و به کلاس‌ها تزریق کنیم. معمولا  IoC Container‌ها لیستی از اشیاء هستند که در آن اینترفیس‌ها و پیاده سازی‌های مربوط به هر کدام از آن‌ها ثبت می‌شوند. درون IoC Container برای پیاده سازی اصل وارونگی وابستگی‌ها، معمولا از یکی از دو الگوی زیر استفاده می‌کنند (گاها هم دو الگو را با هم پیاده سازی می‌کنند) :
  •  Dependency Injection 
  • Service Locator

تمرکز اصلی ما در این نوشتار بر روی DI Container هاست. فرق Dependency Injection و Service Locator در این است که در DI، وابستگی‌ها توسط IoC Container از درون محفظه واکشی می‌شوند و به درون کد تزریق می‌شوند ولی در Service Locator در هر جایی از برنامه می‌توان با استفاده‌ی مستقیم از Container و با استفاده از متدهایی که به ما ارائه می‌دهد،  پرس و جو کرد (کوئری زد) و وابستگی مورد نظر را واکشی کرد.
در تزریق وابستگی، کلاس استفاده کننده از سرویس‌ها، درگیر نحوه‌ی واکشی و نمونه سازی از سرویس مورد نظر خود نمی‌شود و همه‌ی کار توسط DI Container انجام می‌گیرد. ولی در Service Locator باید سرویس مورد نظر، درون خود کلاس، مستقیما از Container دریافت و ساخته شود.
برای استفاده از Service Locator، تنها پیش نیاز، دسترسی به شیء Service Locator است.

به صورت کلی IoC Container ‌ها سه وظیفه‌ی اساسی را برعهده دارند:
  • ثبت سرویس درون خود
  •   ساخت نمونه‌های مورد نظر از سرویس‌ها و ارائه دادن آن‌ها به کلاس‌هایی که نیاز دارند.
  •  از بین بردن نمونه سرویس‌های ساخته شده (Dispose) کردن آن‌ها .

در ادامه با ساخت پروژه‌ای، اولین سرویس خودمان را درون Microsoft Dependency Injection Container یا به اختصار DI Container، ثبت کرده و آن را واکشی می‌کنیم.