نظرات مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور
خطای 400، صرفا یک خطای عددی نیست. ظاهر آن یک عدد است، اما می‌تواند شامل ریز جزئیات خطاهای ارسالی از سمت سرور هم باشد:

bad requestها (یا خطای 400) همان return BadRequest‌های اعتبارسنجی اطلاعات (و یا سایر خطاهایی که خود فریم ورک ارسال می‌کند و نه اعتبارسنجی کاربران با خطای 401) هستند. مطلب « نمایش خطاهای اعتبارسنجی سمت سرور ASP.NET Core در برنامه‌های Angular » را مطالعه کنید تا با روش استخراج ریز جزئیات ارسالی این نوع خطاها از سمت سرور و نمایش آن‌ها به کاربر آشنا شوید. به علاوه لاگ‌های سمت سرور را هم مدنظر داشته باشید.
نظرات مطالب
EF Code First #4
این مورد احتمالا یک باگ هست که اگر بهشون گزارش کنید بهتره تا برای همه اعمال شود. عنوان کنید بسته نیوگت دریافتی دات نت 4.5 فایل init.ps1 مشکل داری داره و اگر اون رو با یک نمونه از بسته نیوگت دات نت 4 اندکی قدیمی‌تر جایگزین کنم مشکلی نیست. تمام خطاها رو هم دقیقا گزارش بدید.
پاسخ به بازخورد‌های پروژه‌ها
توضیحی در مورد T4
سلام. تمام فایلهایی که خودکار  generate میشن رو حذف کردم 


میخواستم نحوه ایجادشون رو ببینم (قبلش کتابخانه‌ها Restore  شدن). فقط این چند فایل مربوط به T4  رو گذاشتم

حالا وقتی Run Custom Tool میکنم این خطاها رو میگیریم


لطفا راهنمایی کنید.

ممنونم



بازخوردهای پروژه‌ها
کامپایل نشدن فایل T4

وقتی روی فایل T4 راست کلیک میکنم و Run Custom Tool رو میزنم چند تا خطا صادر میشه، ظاهرا این خطاها وقتیه که پروژه روی net4 به پایین باشه در حالیکه این پروژه رو 4.5 .net هست، شما دلیلش رو میدونید؟
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 19 - بومی سازی
روش انتقال منابع مرتبط با data annotations و ViewModelها به یک اسمبلی دیگر
- فرض کنید یک class library مخصوص NET Core. را به نام Core1RtmTestResources.ExternalResources جهت درج منابع تهیه کرده‌اید و پوشه‌ی Resources را از پروژه‌ی اصلی به آن انتقال داده‌اید (بدون هیچ تغییر نامی).
- نیازی نیست تا قسمت options.ResourcesPath کلاس آغازین برنامه را تغییر دهید و همان مقدار Resources در این حالت هم کار می‌کند.
- در اینجا دو مورد باید تغییر کنند:
الف) باید مشخص کنید که این اطلاعات قرار است از کدام اسمبلی خوانده شود:
public void ConfigureServices(IServiceCollection services)
{
  services.AddLocalization(options =>
  {
   options.ResourcesPath = "Resources";
  });
 
  services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization(options=>
    {
      options.DataAnnotationLocalizerProvider = (type, factory) =>
      {
       return factory.Create(
        baseName: type.FullName /* بر این اساس نام فایل منبع متناظر باید به همراه ذکر فضای نام پایه آن هم باشد */,
        location: "Core1RtmTestResources.ExternalResources" /*نام اسمبلی ثالث*/);
      };
    });
در اینجا location به نام اسمبلی اشاره می‌کند که حاوی فایل resx مرتبط است. حالت پیش فرض آن (بدون این تنظیمات)، به اسمبلی اشاره می‌کند که کلاس ViewModel در آن قرار گرفته‌است.
ب) چون در این تنظیم baseName به FullName تنظیم شده‌است، نام فایل منبع باید کامل باشد؛ یعنی باید به همراه فضای نام اصلی اسمبلی هم باشد. مثلا اگر قبلا چنین نامی را داشته
ViewModels.Account.RegisterViewModel.fa.resx 
الان باید فضای نام مرتبط را هم داشته باشد (نام کامل نوع آن کلاس):
Core1RtmTestResources.ViewModels.Account.RegisterViewModel.fa.resx 
مطالب
نوشتن آزمون‌های واحد به کمک کتابخانه‌ی Moq - قسمت پنجم - نکات و مباحث تکمیلی
پس از بررسی مباحث و نکات پایه‌ای کار با کتابخانه‌ی Moq، در این قسمت تعدادی از نکات تکمیلی آن‌را بررسی خواهیم کرد.


حالت‌های عملکرد کتابخانه‌ی Moq

کتابخانه‌ی Moq، دو حالت عملکرد را دارد: Strict Mode و Loose mode. زمانیکه یک Mock object را نمونه سازی می‌کنیم، به صورت پیش‌فرض کتابخانه‌ی Moq، یک Loose mock را ایجاد می‌کند. در این حالت این شیء، مقادیر پیش‌فرض خواص و اشیاء را بازگشت می‌دهد و استثنائی را صادر نمی‌کند. اگر این موارد مدنظر نیستند، می‌توان به حالت Strict آن رجوع کرد که روش تنظیم آن به صورت زیر است:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(MockBehavior.Strict);
در این حالت اگر متد آزمون واحد را اجرا کنیم، با پیام زیر، با شکست مواجه خواهد شد:
Test method Loans.Tests.LoanApplicationProcessorShould.Accept threw exception:
Moq.MockException: IIdentityVerifier.Initialize() invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.
در حالت Strict، تمام فراخوانی‌های شیء Mock شده باید دارای Setup باشند (نیازی به Setup تمام موارد نیست؛ فقط مواردی که در فراخوانی‌های آزمون واحد، مورد استفاده قرار می‌گیرند، حتما باید تنظیم شوند). برای نمونه در اینجا عنوان کرده‌است که در این آزمایش، تنظیمات متد Initialize انجام نشده‌است که با تعریف سطر زیر، این مشکل برطرف می‌شود:
mockIdentityVerifier.Setup(x => x.Initialize());

بنابراین هرچند کارکردن با حالت پیش‌فرض کتابخانه‌ی Moq ساده‌است، اما تنظیم حالت Strict سبب می‌شود تا تنظیمی را فراموش نکنیم و در نتیجه کیفیت آزمون واحد تهیه شده افزایش می‌یابد.


صدور استثناءها از طریق Mock objects

اگر در سیستم در حال آزمایش، قسمتی به بررسی خطاها اختصاص دارد، می‌توان توسط Mock objects استثناءهایی را تولید و به این ترتیب منطق بررسی خطاها را آزمایش کرد.
برای نمونه در متد Process کلاس LoanApplicationProcessor، یک try/catch را به قسمت CalculateScore اضافه می‌کنیم:
try
{
    _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address);
}
catch
{
    return application.IsAccepted;
}
زمانیکه کار فراخوانی متد CalculateScore صورت می‌گیرد، برای تنظیم آزمون واحد آن می‌توان از متد Throws، برای صدور یک استثناء استفاده کرد:
mockCreditScorer.Setup(x =>
                    x.CalculateScore(It.IsAny<string>(), It.IsAny<string>()))
                .Throws(new InvalidOperationException("Test Exception"));
صدور این استثناء سبب خواهد شد تا درخواست شخص، رد شود. بنابراین در آزمایش آن می‌توان این مساله را بررسی کرد و از رسیدن به این قسمت (رد شدن درخواست) اطمینان حاصل نمود:
Assert.IsFalse(application.IsAccepted);


صدور رخدادها از طریق Mock objects

فرض کنید یک EventArgs سفارشی را به صورت زیر تعریف:
using System;

namespace Loans.Models
{
    public class CreditScoreResultArgs : EventArgs
    {
        public int Score { get; set; }
    }
}
و سپس رخدادی را به نحو زیر به ICreditScorer اضافه کرده‌ایم:
public interface ICreditScorer
{
   event EventHandler<CreditScoreResultArgs> ResultAvailable;
برای اینکه یک Mock object سبب بروز رخداد ResultAvailable شود (به صورت دستی و دقیقا در سطری که مشخص می‌کنیم)، می‌توان به صورت زیر عمل کرد:
mockCreditScorer.Raise(x => x.ResultAvailable += null, new CreditScoreResultArgs());
ابتدا توسط متد Raise، رخ‌داد مدنظر را ذکر می‌کنیم و سپس یک نمونه‌ی EventArgs را به آن ارسال خواهیم کرد.
روش دیگر انجام اینکار به صورت زیر است:
mockCreditScorer.Setup(x =>
                x.CalculateScore(It.IsAny<string>(), It.IsAny<string>()))
                .Raises(x => x.ResultAvailable += null, new CreditScoreResultArgs());
در این حالت با فراخوانی متد CalculateScore، رخداد ResultAvailable به صورت خودکار صادر می‌شود.


معرفی Partial Mocks

در اغلب آزمون‌های واحدی که تا اینجا بررسی شدند، ابتدا یک Mock object را ایجاد و سپس وهله‌ای از سرویس مدنظر را توسط آن تهیه می‌کنیم. در ادامه تعدادی از متدهای این سرویس را مانند متد Process کلاس LoanApplicationProcessor، فراخوانی می‌کنیم. اینکار سبب اجرای فعالیتی در این سیستم شده و به همراه آن تعاملی با اشیاء Mock شده نیز صورت می‌گیرد. در نهایت حالت و یا نتیجه‌ای را دریافت می‌کنیم و آن‌را با حالت یا نتیجه‌ای که انتظار داریم، مقایسه خواهیم کرد. در این روش پس از پایان اجرای سیستم در حال اجرا، حالت و نتیجه‌ی نهایی حاصل از عملکرد آن، مورد بررسی قرار می‌گیرد. این بررسی‌ها را نیز بر روی اینترفیس‌ها انجام دادیم. اگر بجای اینترفیس‌ها از یک class استفاده شود، به آن partial mock گفته می‌شود. عموما مواردی را که آزمایش آن‌ها سخت است، با Partial mocks پیاده سازی می‌کنند؛ مانند کار با فایل سیستم، کار با قطعه کدهای نامعین مانند DateTime.Now، اعداد اتفاقی و یا Guidها.

در مثال زیر، شبیه به متد آزمون واحد Accept که تاکنون آن‌را بررسی کردیم، از اشیاء Mock شده استفاده شده‌است؛ با یک تفاوت: بجای اینترفیس IIdentityVerifier، از کلاس پیاده سازی کننده‌ی آن که در اینجا IdentityVerifierServiceGateway است، استفاده شده:
namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {        
        [TestMethod]
        public void AcceptUsingPartialMock()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};

            var mockIdentityVerifier = new Mock<IdentityVerifierServiceGateway>();

            mockIdentityVerifier.Setup(x => x.CallService(applicant.Name, applicant.Age, applicant.Address))
                .Returns(true);

            var mockCreditScorer = new Mock<ICreditScorer>();
            mockCreditScorer.Setup(x => x.ScoreResult.ScoreValue.Score).Returns(110_000);

            var sut = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object);
            sut.Process(application);

            Assert.IsTrue(application.IsAccepted);
        }
    }
}
در اینجا برای اینکه بتوانیم متد CallService را که private بوده، بررسی و تنظیم کنیم، آن‌را به public virtual تبدیل کرده‌ایم تا توسط Moq قابل دسترسی و همچنین قابل بازنویسی شود:
public virtual bool CallService(string applicantName, int applicantAge, string applicantAddress)


تبدیل DateTime.Now به یک مقدار ثابت قابل آزمایش توسط Partial Mocks

در کلاس IdentityVerifierServiceGateway، یک چنین کدی را داریم که از DateTime.Now نامشخص استفاده می‌کند و آزمون واحد نوشتن برای آن مشکل است؛ چون DateTime.Now در هربار که آزمایش اجرا می‌شود، تغییر می‌کند:
public bool Validate(string applicantName, int applicantAge, string applicantAddress)
{
    Connect();
    var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
    LastCheckTime = DateTime.Now;
    Disconnect();

    return isValidIdentity;
}
برای بالابردن قابلیت آزمون نویسی این کلاس، آن‌را به صورت زیر Refactor می‌کنیم تا DateTime.Now را به صورت یک متد public virtual دریافت کند:
public bool Validate(string applicantName, int applicantAge, string applicantAddress)
{
    Connect();
    var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
    LastCheckTime = GetCurrentTime();
    Disconnect();

    return isValidIdentity;
}

public virtual DateTime GetCurrentTime()
{
    return DateTime.Now;
}
اکنون آزمون واحد نویسی برای این کلاس توسط Mock objects بسیار ساده‌است:
var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Setup(x => x.GetCurrentTime())
    .Returns(expectedTime);
// ...
Assert.AreEqual(expectedTime, mockIdentityVerifier.Object.LastCheckTime);
در اینجا خروجی متد GetCurrentTime بر روی Mock object تهیه شده، به یک مقدار ثابت تنظیم شده‌است که با هر بار اجرای آزمایش در زمان‌های مختلف، تغییری نمی‌کند و وابسته‌ی به DateTime.Now نامشخص، نیست.


استفاده از متدهای protected بجای استفاده از متدهای public virtual در Partial Mocks

همانطور که مشاهده کردید، برای کار با Partial Mocks نیاز است متدهای معرفی شده، از نوع public virtual باشند. برای نمونه حتی مجبور شدیم یک متد private را نیز public کنیم. اگر علاقمند به این نوع تغییرات نیستید، می‌توان بجای public کردن متدهای private، آن‌ها را protected تعریف کرد. به همین جهت دو متدی را که تاکنون public virtual تعریف کردیم، تبدیل به protected virtual می‌کنیم.
پس از آن در کلاسی که آزمون‌های واحد را تهیه کردیم، ابتدا using Moq.Protected را ذکر می‌کنیم تا بتوانیم به قابلیت‌های ویژه‌ی کار با متدهای Protected دسترسی پیدا کنیم.
سپس روش تنظیم این نوع متدهای protected، چون دسترسی مستقیمی به آن‌ها وجود ندارد، به صورت زیر، با ذکر نام رشته‌ای آن‌ها تغییر می‌کند:
mockIdentityVerifier.Protected().Setup<bool>(
        "CallService",applicant.Name, applicant.Age, applicant.Address)
    .Returns(true);

var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Protected().Setup<DateTime>("GetCurrentTime")
    .Returns(expectedTime);
ابتدا متد Protected شیء Mock شده ذکر می‌شود و پس از آن متد Setup باید دقیقا نوع بازگشتی متد در حال تنظیم را ذکر کند؛ چون دیگر دسترسی strongly typed ای به آن نداریم. پس ا‌ز آن، لیست پارامترهای متد، ذکر می‌شوند.

روش دیگری نیز برای تعریف متدهای protected وجود دارد که اینبار strongly typed است. بالای متد آزمون واحد، اینترفیس private زیر را تعریف می‌کنیم:
interface IIdentityVerifierServiceGatewayProtectedMembers
{
   DateTime GetCurrentTime();
   bool CallService(string applicantName, int applicantAge, string applicantAddress);
}
که در آن متدهای تعریف شده، با متدهای protected در حال بررسی، امضای یکسانی دارند (و همواره با هر تغییری در برنامه نیز باید این وضعیت حفظ شود). در ادامه تعاریف تنظیمات این متدها به صورت strongly typed زیر قابل انجام است:
mockIdentityVerifier.Protected()
    .As<IIdentityVerifierServiceGatewayProtectedMembers>()
    .Setup(x => x.CallService(It.IsAny<string>(),
        It.IsAny<int>(),
        It.IsAny<string>()))
    .Returns(true);

var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Protected()
    .As<IIdentityVerifierServiceGatewayProtectedMembers>()
    .Setup(x => x.GetCurrentTime())
    .Returns(expectedTime);


معرفی روش دیگری بجای استفاده از متدهای protected

اگر در کدهای خود نیاز به استفاده‌ی بیش از حد از متدهای protected را مشاهده کردید، این مورد می‌توان نشانه‌ی امکان Refactoring این قسمت از کدها به سرویس‌هایی مجزا باشند. برای مثال می‌توان یک اینترفیس INowProvider را به صورت زیر تعریف کرد:
using System;

namespace Loans.Services.Contracts
{
    public interface INowProvider
    {
        DateTime GetNow();
    }
}
و سپس آن‌را به سازنده‌ی کلاس IdentityVerifierServiceGateway تزریق کرد:
    public class IdentityVerifierServiceGateway : IIdentityVerifier
    {
        private readonly INowProvider _nowProvider;
        
        public DateTime LastCheckTime { get; private set; }

        public IdentityVerifierServiceGateway(INowProvider nowProvider)
        {
            _nowProvider = nowProvider;
        }
 و متد GetCurrentTime را حذف و آن‌را با متد GetNow این سرویس جایگزین نمود:
        public bool Validate(string applicantName, int applicantAge, string applicantAddress)
        {
            Connect();
            var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
            LastCheckTime = _nowProvider.GetNow();
            // ...
 به این ترتیب نیاز به تنظیم متد protected بازگشت زمان، حذف شده و می‌توان از این سرویس جدید استفاده کرد:
var mockNowProvider = new Mock<INowProvider>();
mockNowProvider.Setup(x => x.GetNow()).Returns(expectedTime);

var mockIdentityVerifier =  new Mock<IdentityVerifierServiceGateway>(mockNowProvider.Object);


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید: MoqSeries-05.zip
نظرات مطالب
انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
«هر ثانیه؟» خیر. تنظیم شده هر دقیقه یکبار آدرس root سایت شما را فراخوانی کند. 400 Bad Request هم یعنی این آدرس root سایت قابل دسترسی نیست و خطا می‌دهد. AddPingTask هم اگر false باشد، این job اصلا اضافه نخواهد شد. بنابراین این تنظیم در زمان راه اندازی اولیه‌ی برنامه تاثیر دارد و نه پس از آن.  
مطالب
Blazor 5x - قسمت 13 - کار با فرم‌ها - بخش 1 - کار با EF Core در برنامه‌های Blazor Server
در ادامه قصد داریم یک پروژه‌ی مدیریت هتل را پیاده سازی کنیم. این پروژه، دو قسمتی است. قسمت اول آن یک پروژه‌ی Blazor Server، برای مدیریت هتل مانند تعاریف اتاق‌ها است و پروژه‌ی دوم آن از نوع Blazor WASM، برای مراجعه‌ی کاربران عمومی و رزرو اتاق‌ها است. هدف، بررسی نحوه‌ی کار با هر دو نوع فناوری است. وگرنه می‌توان کل پروژه را با Blazor Server و یا کل آن‌را با Blazor WASM هم پیاده سازی کرد. در مورد نحوه‌ی انتخاب و مزایا و معایب هرکدام از این فناوری‌ها، در قسمت‌های اول و دوم این سری بیشتر بحث شده‌است.


ساختار پوشه‌ها و پروژه‌های قسمت Blazor Server


قسمت Blazor Server مدیریت هتل ما از 7 پروژه و پوشه‌ی زیر تشکیل می‌شود:
- BlazorServer.App: پروژه‌ی اصلی Blazor Server است که با اجرای دستور dotnet new blazorserver در پوشه‌ی خالی آن آغاز می‌شود.
- BlazorServer.Common: پروژه‌ای از نوع classlib، جهت قرارگیری کدهای مشترک بین پروژه‌ها است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.DataAccess: پروژه‌ای از نوع classlib، برای تعریف DbContext برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Entities: پروژه‌ای از نوع classlib، جهت تعریف کلاس‌های متناظر با جداول بانک اطلاعاتی برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Models: پروژه‌ای از نوع classlib، برای تعریف کلاس‌های data transfer objects برنامه (DTO's) است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Models.Mappings: پروژه‌ای از نوع classlib، برای تعریف نگاشت‌های بین DTO's و مجودیت‌های برنامه و برعکس است که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.
- BlazorServer.Services: پروژه‌ای از نوع classlib، جهت تعریف کدهایی که منطق تجاری تعامل با بانک اطلاعاتی را از طریق BlazorServer.DataAccess میسر می‌کند که با اجرای دستور dotnet new classlib در این پوشه آغاز می‌شود.


اصلاح پروژه‌ی BlazorServer.App جهت استفاده از LibMan

قالب پیش‌فرض BlazorServer.App، به همراه پوشه‌ی wwwroot\css است که در آن بوت استرپ و open-iconic به همراه فایل site.css قرار دارند. چون این پروژه به همراه هیچ نوع روشی برای مدیریت نگهداری بسته‌های سمت کلاینت خود نیست، دو پوشه‌ی بوت استرپ و open-iconic آن‌را حذف کرده و از روش مطرح شده‌ی در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» استفاده خواهیم کرد:
dotnet tool update -g Microsoft.Web.LibraryManager.Cli
libman init
libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap
libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic
در پوشه‌ی ریشه‌ی پروژه‌ی BlazorServer.App، دستورات فوق را اجرا می‌کنیم تا بسته‌های bootstrap و open-iconic را در پوشه‌ی wwwroot/lib نصب کند و همچنین فایل libman.json متناظری را نیز جهت اجرای دستور libman restore برای دفعات آتی، تولید کند.

بعد از نصب بسته‌های ذکر شده، ابتدا سطر زیر را از ابتدای فایل پیش‌فرض wwwroot\css\site.css حذف می‌کنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
سپس فایل Pages\_Host.cshtml را به صورت زیر به روز رسانی می‌کنیم تا به مسیرهای جدید بسته‌های CSS، اشاره کند:
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorServer.App</title>
    <base href="~/" />
    <link href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet" />
    <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="BlazorServer.App.styles.css" rel="stylesheet" />
</head>

برای bundling & minification این فایل‌ها می‌توان از «Bundler Minifier» استفاده کرد.


پروژه‌ی موجودیت‌های مدیریت هتل

فایل BlazorServer.Entities.csproj وابستگی خاصی را نداشته و به صورت زیر تعریف شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
</Project>
در این پروژه، کلاس جدید HotelRoom را که بیانگر ساختار جدول متناظری در بانک اطلاعاتی است، به صورت زیر تعریف می‌کنیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Entities
{
    public class HotelRoom
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public int Occupancy { get; set; }

        [Required]
        public decimal RegularRate { get; set; }

        public string Details { get; set; }

        public string SqFt { get; set; }

        public string CreatedBy { get; set; }

        public DateTime CreatedDate { get; set; } = DateTime.Now;

        public string UpdatedBy { get; set; }

        public DateTime UpdatedDate { get; set; }
    }
}
که شامل فیلدهایی مانند نام، ظرفیت، مساحت و ... یک اتاق هتل است.


پروژه‌ی تعریف DbContext برنامه‌ی مدیریت هتل

فایل BlazorServer.DataAccess.csproj به این صورت تعریف شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
- در اینجا چون نیاز است موجودیت HotelRoom را به صورت یک DbSet معرفی کنیم، ارجاعی را به پروژه‌ی BlazorServer.Entities.csproj تعریف کرده‌ایم.
- همچنین دو وابستگی مورد نیاز جهت کار با EntityFrameworkCore و اجرای مهاجرت‌ها را نیز به آن افزوده‌ایم.

پس از تامین این وابستگی‌ها، اکنون می‌توان DbContext ابتدایی برنامه را به صورت زیر تعریف کرد که کار آن، در معرض دید قرار دادن HotelRoom به صورت یک DbSet است:
using BlazorServer.Entities;
using Microsoft.EntityFrameworkCore;

namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<HotelRoom> HotelRooms { get; set; }

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        { }
    }
}
پس از این مرحله، نیاز است این DbContext را به سیستم تزریق وابستگی‌های برنامه‌ی اصلی معرفی کرد. بنابراین فایل BlazorServer.App.csproj پروژه‌ی اصلی Blazor Server را گشوده و تغییرات زیر را اعمال می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
- چون می‌خواهیم ApplicationDbContext را به سیستم تزریق وابستگی‌ها معرفی کنیم، بنابراین باید بتوان به کلاس آن نیز دسترسی داشت که اینکار، با تعریف ارجاعی به BlazorServer.DataAccess.csproj میسر شده‌است.
- سپس چون می‌خواهیم از تامین کننده‌ی بانک اطلاعاتی SQL Server نیز استفاده کنیم، وابستگی‌های آن‌را نیز افزوده‌ایم.

با این تنظیمات، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و کار افزودن AddDbContext و UseSqlServer را انجام می‌دهیم تا DbContext برنامه از طریق تزریق وابستگی‌ها قابل دسترسی شود و همچنین رشته‌ی اتصالی مشخص شده نیز به تامین کننده‌ی SQL Server ارسال شود:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...
 
        public void ConfigureServices(IServiceCollection services)
        {
            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));

            // ...
این رشته‌ی اتصالی را به صورت زیر در فایل BlazorServer\BlazorServer.App\appsettings.json تعریف کرده‌ایم:
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
که در حقیقت یک رشته‌ی اتصالی جهت کار با LocalDB است.


اجرای مهاجرت‌ها و تشکیل ساختار بانک اطلاعاتی

پس از این تنظیمات، اکنون می‌توانیم به پوشه‌ی BlazorServer\BlazorServer.DataAccess مراجعه کرده و از طریق خط فرمان، دستورات زیر را صادر کنیم:
dotnet tool update --global dotnet-ef --version 5.0.3
dotnet build
dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext
dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
- در ابتدا نیاز است ابزارهای مهاجرت EF-Core را نصب کنیم که سطر اول، اینکار را انجام می‌دهد.
- همیشه بهتر است پیش از اجرای عملیات Migration، یکبار dotnet build را اجرا کرد؛ تا اگر خطایی وجود دارد، بتوان جزئیات دقیق آن‌را مشاهده کرد. چون عموما این جزئیات در حین اجرای دستورات بعدی، با پیام مختصر «عملیات شکست خورد»، نمایش داده نمی‌شوند.
- دستور سوم، کار تشکیل پوشه‌ی BlazorServer\BlazorServer.DataAccess\Migrations و تولید خودکار دستورات تشکیل بانک اطلاعاتی را بر اساس ساختار DbContext برنامه انجام می‌دهد.
- دستور چهارم، بر اساس اطلاعات موجود در پوشه‌ی BlazorServer\BlazorServer.DataAccess\Migrations، بانک اطلاعاتی واقعی را تولید می‌کند.
در این دستورات ذکر پروژه‌ی آغازین برنامه جهت یافتن وابستگی‌های پروژه ضروری است.



تکمیل پروژه‌ی DTO‌های برنامه

همواره توصیه شده‌است که موجودیت‌های برنامه را مستقیما در معرض دید UI قرار ندهید. حداقل مشکلی را که در اینجا ممکن است مشاهده کنید، حملات از نوع mass assignment هستند. برای مثال قرار است از کاربر، کلمه‌ی عبور جدید آن‌را دریافت کنید، ولی چون اطلاعات دریافتی، به اصل موجودیت متناظر با بانک اطلاعاتی نگاشت می‌شود، کاربر می‌تواند فیلد IsAdmin را هم خودش مقدار دهی کند! و چون سیستم Binding بسیار پیشرفته عمل می‌کند، این ورودی را معتبر یافته و در اینجا علاوه بر به روز رسانی کلمه‌ی عبور، خواص دیگری را هم که نباید به روز رسانی شوند، به روز رسانی می‌کند و یا در بسیاری از موارد نیاز است data annotations خاصی را برای فیلدها تعریف کرد که ربطی به موجودیت اصلی ندارند و یا نیاز است فیلدهایی را در UI قرار داد که باز هم تناظر یک به یکی با موجودیت اصلی ندارند (گاهی کمتر و گاهی بیشتر هستند و باید بر روی آن‌ها محاسباتی صورت گیرد تا قابلیت ذخیره سازی در بانک اطلاعاتی را پیدا کنند). به همین جهت کار مدل سازی UI و یا بازگشت اطلاعات نهایی از سرویس‌ها را توسط DTO‌ها که یک سری کلاس ساده‌ی C# 9.0 از نوع record هستند، انجام می‌دهیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public record HotelRoomDTO
    {
        public int Id { get; init; }

        [Required(ErrorMessage = "Please enter the room's name")]
        public string Name { get; init; }

        [Required(ErrorMessage = "Please enter the occupancy")]
        public int Occupancy { get; init; }

        [Range(1, 3000, ErrorMessage = "Regular rate must be between 1 and 3000")]
        public decimal RegularRate { get; init; }

        public string Details { get; init; }

        public string SqFt { get; init; }
    }
}
Record‌های C# 9.0، انتخاب بسیار مناسبی برای تعریف DTO‌ها هستند. از این لحاظ که قرار نیست اطلاعات دریافتی از کاربر، در این بین و پس از مقدار دهی اولیه، تغییر کنند.
در اینجا فیلدهای UI برنامه را که در قسمت بعد تکمیل خواهیم کرد، مشاهده می‌کنید؛ به همراه یک سری data annotation برای تعریف اجباری و یا بازه‌ی مورد قبول، به همراه پیام‌های خطای مرتبط.


نگاشت DTO‌های برنامه به موجودیت‌ها و بر عکس

یا می‌توان خواص DTO تعریف شده را یکی یکی به موجودیتی متناظر با آن انتساب داد و یا می‌توان از AutoMapper برای اینکار استفاده کرد. به همین جهت به BlazorServer.Models.Mappings.csproj مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" />
    <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
  </ItemGroup>
</Project>
- پروژه‌ای که کار تعریف نگاشت‌ها را انجام می‌دهد، نیاز به اطلاعات موجودیت‌ها و مدل‌ها (DTO ها)ی متناظر را دارد. به همین جهت ارجاعاتی را به این دو پروژه، تعریف کرده‌ایم.
- همچنین بسته‌ی مخصوص AutoMapper را که به همراه امکانات تزریق وابستگی‌های آن نیز هست، در اینجا افزوده‌ایم.

پس از افزودن این ارجاعات، نگاشت دو طرفه‌ی بین مدل و موجودیت تعریف شده را به صورت زیر تعریف می‌کنیم:
using AutoMapper;
using BlazorServer.Entities;

namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            CreateMap<HotelRoomDTO, HotelRoom>().ReverseMap(); // two-way mapping
        }
    }
}
اکنون برای شناسایی پروفایل فوق و معرفی آن به AutoMapper، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و تزریق وابستگی و ردیابی خودکار آن‌را اضافه می‌کنیم که شامل اسکن تمام اسمبلی‌های موجود، جهت یافتن Profile‌های AutoMapper است:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

            // ...


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

پس از راه اندازی برنامه و تعریف موجودیت‌ها، DbContext و غیره، اکنون می‌توانیم از آن‌ها جهت ارائه‌ی منطق مدیریتی برنامه استفاده کنیم:
using System.Collections.Generic;
using System.Threading.Tasks;
using BlazorServer.Models;

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);
    }
}

که پیاده سازی ابتدایی آن به صورت زیر است:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using BlazorServer.DataAccess;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.EntityFrameworkCore;

namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public HotelRoomService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO)
        {
            var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
            hotelRoom.CreatedDate = DateTime.Now;
            hotelRoom.CreatedBy = "";
            var addedHotelRoom = await _dbContext.HotelRooms.AddAsync(hotelRoom);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<HotelRoomDTO>(addedHotelRoom.Entity);
        }

        public async Task<int> DeleteHotelRoomAsync(int roomId)
        {
            var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId);
            if (roomDetails == null)
            {
                return 0;
            }

            _dbContext.HotelRooms.Remove(roomDetails);
            return await _dbContext.SaveChangesAsync();
        }

        public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync()
        {
            return _dbContext.HotelRooms
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .AsAsyncEnumerable();
        }

        public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId)
        {
            return _dbContext.HotelRooms
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
        }

        public Task<HotelRoomDTO> IsRoomUniqueAsync(string name)
        {
            return _dbContext.HotelRooms
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Name == name);
        }

        public async Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO)
        {
            if (roomId != hotelRoomDTO.Id)
            {
                return null;
            }

            var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId);
            var room = _mapper.Map(hotelRoomDTO, roomDetails);
            room.UpdatedBy = "";
            room.UpdatedDate = DateTime.Now;
            var updatedRoom = _dbContext.HotelRooms.Update(room);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<HotelRoomDTO>(updatedRoom.Entity);
        }
    }
}
- در اینجا DbContext برنامه و همچنین نگاشت‌گر AutoMapper، به سازنده‌ی سرویس، تزریق شده و توسط آن‌ها، ابتدا اطلاعات DTOها به موجودیت‌ها تبدیل شده‌اند (و یا برعکس) و سپس با استفاده از dbContext برنامه، کوئری‌هایی را بر روی بانک اطلاعاتی اجرا کرده‌ایم.
- در این کدها استفاده از متد ProjectTo را هم مشاهده می‌کنید. استفاده از این متد، بسیار بهینه‌تر از کار با متد Map درون حافظه‌ای است. از این جهت که بر روی SQL نهایی ارسالی به سمت سرور تاثیرگذار است و تعداد فیلدهای بازگشت داده شده را بر اساس DTO تعیین شده، کاهش می‌دهد. درغیراینصورت باید تمام ستون‌های جدول را بازگشت داد و سپس با استفاده از متد Map درون حافظه‌‌ای، کار نگاشت نهایی را انجام داد که آنچنان بهینه نیست.

در آخر نیاز است این سرویس را نیز به سیستم تزریق وابستگی‌های برنامه معرفی کنیم. به همین جهت در فایل BlazorServer\BlazorServer.App\Startup.cs، تغییر زیر را اعمال خواهیم کرد:
namespace BlazorServer.App
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IHotelRoomService, HotelRoomService>();

         // ...


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-13.zip
مطالب
نمایش فرم‌های مودال Ajax ایی در ASP.NET MVC به کمک Twitter Bootstrap
اصول نمایش اطلاعات مودال به کمک bootstrap در مطلب «استفاده از modal dialogs مجموعه Twitter Bootstrap برای گرفتن تائید از کاربر» بررسی شدند.
در این قسمت قصد داریم یک فرم Ajaxایی را در ASP.NET MVC به همراه تمام مسایل اعتبارسنجی، پردازش اطلاعات و غیره را به کمک Twitter Bootstrap و jQuery Ajax پیاده سازی کنیم.


تهیه افزونه jquery.bootstrap-modal-ajax-form.js

از این جهت که مباحث مرتبط با نمایش و پردازش فرم‌های مودال Ajaxایی به کمک Twitter Bootstrap اندکی نکته دار و طولانی هستند، بهتر است این موارد را به شکل یک افزونه، کپسوله کنیم. کدهای کامل افزونه jquery.bootstrap-modal-ajax-form.js را در ادامه ملاحظه می‌کنید:
// <![CDATA[
(function ($) {
    $.bootstrapModalAjaxForm = function (options) {
        var defaults = {
            renderModalPartialViewUrl: null,
            renderModalPartialViewData: null,
            postUrl: '/',
            loginUrl: '/login',
            beforePostHandler: null,
            completeHandler: null,
            errorHandler: null
        };
        var options = $.extend(defaults, options);

        var validateForm = function (form) {
            //فعال سازی دستی اعتبار سنجی جی‌کوئری
            var val = form.validate();
            val.form();
            return val.valid();
        };

        var enableBootstrapStyleValidation = function () {
            $.validator.setDefaults({
                highlight: function (element, errorClass, validClass) {
                    if (element.type === 'radio') {
                        this.findByName(element.name).addClass(errorClass).removeClass(validClass);
                    } else {
                        $(element).addClass(errorClass).removeClass(validClass);
                        $(element).closest('.control-group').removeClass('success').addClass('error');
                    }
                    $(element).trigger('highlited');
                },
                unhighlight: function (element, errorClass, validClass) {
                    if (element.type === 'radio') {
                        this.findByName(element.name).removeClass(errorClass).addClass(validClass);
                    } else {
                        $(element).removeClass(errorClass).addClass(validClass);
                        $(element).closest('.control-group').removeClass('error').addClass('success');
                    }
                    $(element).trigger('unhighlited');
                }
            });
        }

        var enablePostbackValidation = function () {
            $('form').each(function () {
                $(this).find('div.control-group').each(function () {
                    if ($(this).find('span.field-validation-error').length > 0) {
                        $(this).addClass('error');
                    }
                });
            });
        }

        var processAjaxForm = function (dialog) {
            $('form', dialog).submit(function (e) {
                e.preventDefault();

                if (!validateForm($(this))) {
                    //اگر فرم اعتبار سنجی نشده، اطلاعات آن ارسال نشود
                    return false;
                }

                //در اینجا می‌توان مثلا دکمه‌ای را غیرفعال کرد
                if (options.beforePostHandler)
                    options.beforePostHandler();

                //اطلاعات نباید کش شوند
                $.ajaxSetup({ cache: false });
                $.ajax({
                    url: options.postUrl,
                    type: "POST",
                    data: $(this).serialize(),
                    success: function (result) {
                        if (result.success) {
                            $('#dialogDiv').modal('hide');
                            if (options.completeHandler)
                                options.completeHandler();
                        } else {
                            $('#dialogContent').html(result);
                            if (options.errorHandler)
                                options.errorHandler();
                        }
                    }
                });
                return false;
            });
        };

        var mainContainer = "<div id='dialogDiv' class='modal hide fade in'><div id='dialogContent'></div></div>";
        enableBootstrapStyleValidation(); //اعمال نکات خاص بوت استرپ جهت اعتبارسنجی یکپارچه با آن
        $.ajaxSetup({ cache: false });
        $.ajax({
            type: "POST",
            url: options.renderModalPartialViewUrl,
            data: options.renderModalPartialViewData,
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            complete: function (xhr, status) {
                var data = xhr.responseText;
                var data = xhr.responseText;
                if (xhr.status == 403) {
                    window.location = options.loginUrl; //در حالت لاگین نبودن شخص اجرا می‌شود
                }
                else if (status === 'error' || !data) {
                    if (options.errorHandler)
                        options.errorHandler();
                }
                else {
                    var dialogContainer = "#dialogDiv";
                    $(dialogContainer).remove();
                    $(mainContainer).appendTo('body');

                    $('#dialogContent').html(data); // دریافت پویای اطلاعات مودال دیالوگ
                    $.validator.unobtrusive.parse("#dialogContent"); // فعال سازی اعتبارسنجی فرمی که با ایجکس بارگذاری شده                            
                    enablePostbackValidation();
                    // و سپس نمایش آن به صورت مودال
                    $('#dialogDiv').modal({
                        backdrop: 'static', //با کلیک کاربر روی صفحه، صفحه مودال بسته نمی‌شود
                        keyboard: true
                    }, 'show');
                    // تحت نظر قرار دادن این فرم اضافه شده
                    processAjaxForm('#dialogContent');
                }
            }
        });
    };
})(jQuery);
// ]]>
توضیحات:
- توابع enableBootstrapStyleValidation و enablePostbackValidation در مطلب «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» بررسی شدند.
- این افزونه با توجه به مقدار renderModalPartialViewUrl، یک partial view را از برنامه ASP.NET MVC درخواست می‌کند.
- سپس این partial view را به صورت خودکار به صفحه اضافه کرده و آن‌را به صورت modal نمایش می‌دهد.
- پس از افزودن فرم Ajaxایی دریافتی، مسایل اعتبارسنجی را به آن اعمال کرده و سپس دکمه submit آن‌را تحت کنترل قرار می‌دهد.
- در زمان submit، ابتدا بررسی می‌کند که آیا فرم معتبر است و اعتبارسنجی آن بدون مشکل است؟ اگر اینچنین است، اطلاعات فرم را به آدرس postUrl به صورت Ajaxایی ارسال می‌کند.


کدهای مدل برنامه
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Mvc4TwitterBootStrapTest.Models
{
    public class User
    {
        public int Id { set; get; }

        [DisplayName("نام")]
        [Required(ErrorMessage="لطفا نام را تکمیل کنید")]
        public string Name { set; get; }

        [DisplayName("نام خانوادگی")]
        [Required(ErrorMessage = "لطفا نام خانوادگی را تکمیل کنید")]
        public string LastName { set; get; }
    }
}
در اینجا یک مدل ساده‌را به همراه ویژگی‌های اعتبارسنجی و نام‌های نمایشی خواص ملاحظه می‌کنید.


کدهای کنترلر برنامه

using System.Web.Mvc;
using Mvc4TwitterBootStrapTest.Models;

namespace Mvc4TwitterBootStrapTest.Controllers
{
    public class ModalFormAjaxController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش صفحه اولیه
        }

        [HttpPost] //برای این حالت امن‌تر است
        //[AjaxOnly]
        public ActionResult RenderModalPartialView()
        {
            //رندر پارشال ویوو صفحه مودال به همراه اطلاعات مورد نیاز آن
            return PartialView(viewName: "_ModalPartialView", model: new User { Name = "", LastName = "" });
        }

        [HttpPost]
        //[AjaxOnly]
        public ActionResult Index(User user) //ذخیره سازی اطلاعات
        {
            if (this.ModelState.IsValid)
            {
                //todo: SaveChanges;
                return Json(new { success = true });
            }

            this.ModelState.AddModelError("", "خطایی رخ داده است");
            return PartialView("_ModalPartialView", user);
        }
    }
}
کدهای کنترلر برنامه در این حالت از سه قسمت تشکیل می‌شود:
الف) متد Index حالت HttpGet که صفحه ابتدایی را نمایش خواهد داد.
ب) متد RenderModalPartialView یک partial view اضافه شده به برنامه به نام _ModalPartialView.cshtml را بازگشت می‌دهد. این partial view در حقیقت همان فرمی است که قرار است به صورت مودال نمایش داده شود و پردازش آن نیز Ajaxایی باشد.
ج) متد Index حالت HttpPost که نهایتا اطلاعات فرم مودال را دریافت خواهد کرد. اگر پردازش موفقیت آمیز بود، نیاز است همانند کدهای فوق return Json صورت گیرد. در غیراینصورت مجددا همان partial view را بازگشت دهید.


کدهای Index.cshtml

@{
    ViewBag.Title = "Index";
    var renderModalPartialViewUrl = Url.Action("RenderModalPartialView", "ModalFormAjax");
    var postDataUrl = Url.Action("Index", "ModalFormAjax");
}
<h2>
    Index</h2>
<a href="#" class="btn btn-primary" id="btnCreate">ثبت اطلاعات</a>

@section JavaScript
{
    <script type="text/javascript">
        $(function () {           
            $('#btnCreate').click(function (e) {
                e.preventDefault(); //می‌خواهیم لینک به صورت معمول عمل نکند

                $.bootstrapModalAjaxForm({
                    postUrl: '@postDataUrl',
                    renderModalPartialViewUrl: '@renderModalPartialViewUrl',
                    renderModalPartialViewData: {},
                    loginUrl: '/login',
                    beforePostHandler: function () {                       
                    },
                    completeHandler: function () {
                        // Refresh: برای حالتیکه نیاز به به روز رسانی کامل صفحه زیرین باشد
                        // location.reload();
                    },
                    errorHandler: function () {
                    }
                });
            });
        });             
    </script>
}
این کدها متناظر هستند با کدهای view اکشن متد Index در حالت Get.
- در اینجا یک لینک ساده در صفحه قرار گرفته و به کمک کلاس btn مجموعه bootstrap به شکل یک دکمه مزین شده است.
- در ادامه نحوه استفاده از افزونه‌ای را که در ابتدای بحث طراحی کردیم، ملاحظه می‌کنید. کار با آن بسیار ساده است و تنها باید مسیرهای ارسال اطلاعات نهایی به سرور یا postDataUrl و مسیر دریافت partial view رندر شده یا renderModalPartialViewUrl به آن معرفی شود. سایر مسایل آن خودکار است.


کدهای _ModalPartialView.cshtml یا همان فرم مودال برنامه

@model Mvc4TwitterBootStrapTest.Models.User
<div class="modal-header">
    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">
        &times;</button>
    <h5>
        افزودن کاربر جدید</h5>
</div>
@using (Html.BeginForm("Index", " ModalFormAjax", FormMethod.Post, new { @class = "modal-form" }))
{
    <div class="modal-body">
        @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" })
        <fieldset class="form-horizontal">
            <legend>مشخصات کاربر</legend>
            <div class="control-group">
                @Html.LabelFor(model => model.Name, new { @class = "control-label" })
                <div class="controls">
                    @Html.EditorFor(model => model.Name)
                    @Html.ValidationMessageFor(model => model.Name, null, new { @class = "help-inline" })
                </div>
            </div>
            <div class="control-group">
                @Html.LabelFor(model => model.LastName, new { @class = "control-label" })
                <div class="controls">
                    @Html.EditorFor(model => model.LastName)
                    @Html.ValidationMessageFor(model => model.LastName, null, new { @class = "help-inline" })
                </div>
            </div>
        </fieldset>
    </div>
        
    <div class="modal-footer">
        <button class="btn btn-primary" type="submit">
            ارسال</button>
        <button class="btn" data-dismiss="modal" aria-hidden="true">
            انصراف</button>
    </div>
}
در اینجا اطلاعات فرمی را ملاحظه می‌کنید که قرار است به صورت مودال نمایش داده شود. نحوه طراحی آن بر اساس نکات form-horizontal است. همچنین divهای modal-header، modal-body و modal-footer نیز به این فرم ویژه اضافه شده‌اند تا به خوبی توسط bootstrap پردازش گردد.
حاصل نهایی این مبحث را در دو شکل ذیل ملاحظه می‌کنید. صفحه index نمایش دهنده یک دکمه و در ادامه باز شدن یک فرم مودال، پس از کلیک بر روی دکمه ثبت اطلاعات.