نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS
اگر پروژه به صورت InProcess بر روی IIS اجرا شود باید از UseIIS استفاده کنیم. در Asp.Net Core 2.2
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseApplicationInsights()
        .UseStartup<Startup>()
        .UseIIS();
اما اگر پروژه به صورت OutOfProcess باشد باید از UseIISIntegration استفاده کنیم.
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseApplicationInsights()
        .UseStartup<Startup>()
        .UseIISIntegration();

مطالب
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطلب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال اول اکتفاء خواهد شد.


تغییرات مورد نیاز جهت فعال سازی ویرایش، حذف و افزودن رکوردهای jqGrid

می‌خواهیم در بدو نمایش گرید، یک ستون خاص دارای دکمه‌های ویرایش و حذف ظاهر شوند:


برای اینکار تنها کافی است در انتهای ستون‌های تعریف شده، یک ستون خاص را با formatter مساوی actions ایجاد کنیم:
                colModel: [
                    {
                        // سایر ستون‌ها
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false, formatter: 'actions',
                        formatoptions: {
                            keys: true
                        }
                    }
                ],
برای اینکه دکمه‌های ویرایش و حذف ردیف‌های آن عمل کنند:


نیاز است تعاریف سایر ستون‌هایی را که باید قابلیت ویرایش داشته باشند، به نحو ذیل تغییر دهیم:
                colModel: [
                    {
                        name: 'Id', index: 'Id', align: 'right', width: 70,
                        editable: false
                    },
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 100,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 110,
                        editable: true, edittype: 'select',
                        editoptions: {
                            dataUrl: '@Url.Action("SuppliersSelect","Home")'
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Category.Id', index: 'Category.Id', align: 'right', width: 110,
                        editable: true, edittype: 'select',
                        editoptions: {
                            dataUrl: '@Url.Action("CategoriesSelect","Home")'
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Price', index: 'Price', align: 'center', width: 100,
                        formatter: 'currency',
                        formatoptions:
                        {
                            decimalSeparator: '.',
                            thousandsSeparator: ',',
                            decimalPlaces: 2,
                            prefix: '$'
                        },
                        editable: true, edittype: 'text',
                        editrules: {
                            required: true,
                            number: true,
                            minValue: 0
                        }
                    },
                    {
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false, formatter: 'actions',
                        formatoptions: {
                            keys: true
                        }
                    }
                ],
- در اینجا هر ستونی که دارای خاصیت editable مساوی true است، قابلیت ویرایش پیدا می‌کند.
- edittype آن بیانگر کنترلی است که باید حین ویرایش آن سلول خاص ظاهر شود. برای مثال اگر text باشد، یک text box و اگر مانند حالت Supplier.Id مساوی select تعریف شود، یک drop down را ظاهر خواهد کرد. برای مقدار دهی این drop down می‌توان editoptions و سپس dataUrl آن‌را مقدار دهی نمود.
        public ActionResult SuppliersSelect()
        {
            var list = ProductDataSource.LatestProducts;
            var suppliers = list.Select(x => new SelectListItem
            {
                Text = x.Supplier.CompanyName,
                Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture)
            }).ToList();
            return PartialView("_SelectPartial", suppliers);
        }
در مثال فوق، این dataUrl به اکشن متد SuppliersSelect اشاره می‌کند که نهایتا لیستی از تولید کننده‌ها را توسط partial view ذیل بازگشت می‌دهد:
 @model IList<SelectListItem>

@Html.DropDownList("srch", Model)
در کل مقادیر قابل تنظیم در اینجا شامل  text، textarea، select، checkbox، password، button، image، file و custom هستند.
- خاصیت editrules، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شده‌است. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool، خاصیت‌های minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است (مثال‌های حالت custom را در منابع انتهای بحث می‌توانید مطالعه کنید).
- پس از اینکه مشخص شدند کدامیک از ستون‌ها باید قابلیت ویرایش داشته باشند، مسیری که باید اطلاعات نهایی را به سرور ارسال کند، توسط خاصیت editurl مشخص می‌شود:
$('#list').jqGrid({
   caption: "آزمایش چهارم",
   //url from wich data should be requested
   url: '@Url.Action("GetProducts","Home")',
   //url for edit operation
   editurl: '@Url.Action("EditProduct","Home")',
اکشن متد متناظر با این آدرس یک چنین شکلی را می‌تواند داشته باشد:
        [HttpPost]
        public ActionResult EditProduct(Product postData)
        {
            //todo: Edit product based on postData

            return Json(true);
        }
- تعاریف مسیرهای ارسال اطلاعات Add و Delete، در قسمت تنظیمات navGrid باید ذکر شوند:
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: false, search: false },
                //edit options
                {},
                //add options
                { width: 'auto', url: '@Url.Action("AddProduct","Home")' },
                //delete options
                { url: '@Url.Action("DeleteProduct","Home")' }
                );
امضای این اکشن متدها نیز بسیار شبیه به اکشن متد ویرایش است:
        [HttpPost]
        public ActionResult DeleteProduct(string id)
        {
            //todo: Delete product
            return Json(true);
        }

        [HttpPost]
        public ActionResult AddProduct(Product postData)
        {
            //todo: Add product to repository
            return Json(true);
        }
- حالت ویرایش و حذفی که تا اینجا بررسی شد (ستون actions)، جزو خواص توکار این گرید است. اگر بخواهیم آن‌ها را دستی فعال کنیم (جهت اطلاعات عمومی) می‌توان از فراخوانی متد ذیل نیز کمک گرفت:
        var lastSel;
        function inlineEdit() {
            $('input[name=rdEditApproach]').attr('disabled', true);
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: false, search: false },
                //edit options
                {},
                //add options
                { width: 'auto', url: '@Url.Action("AddProduct","Home")' },
                //delete options
                { url: '@Url.Action("DeleteProduct","Home")' }
                );
            //add onSelectRow event to support inline edit
            $('#list').setGridParam({
                onSelectRow: function (id) {
                    if (id && id != lastSel) {
                        //save changes in row
                        $('#list').saveRow(lastSel, false);
                        lastSel = id;
                    }
                    //trigger inline edit for row
                    $('#list').editRow(id, true);
                }
            });
        };
در اینجا ابتدا همان تنظیمات مسیرهای Add و Delete انجام شده‌است. سپس با فراخوانی دستی متد editRow در زمان کلیک بر روی یک ردیف، همان کاری را که ستون actions در جهت فعال سازی خودکار حالت ویرایش سلول‌ها انجام می‌دهد، می‌توان شبیه سازی کرد. متد saveRow نیز کار ارسال اطلاعات تغییر کرده را به سرور انجام می‌دهد.
- برای فعال سازی خودکار فرم‌های افزودن رکوردها و یا ویرایش ردیف‌های موجود می‌توان از فراخوانی متد formEdit ذیل کمک گرفت:
        function formEdit() {
            $('input[name=rdEditApproach]').attr('disabled', true);
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    width: 'auto', checkOnUpdate: true, checkOnSubmit: true,
                    beforeShowForm: function (form) {
                        centerDialog(form, $('#list'));
                    }
                },
                //add options
                {
                    width: 'auto', url: '@Url.Action("AddProduct","Home")',
                    reloadAfterSubmit: false, checkOnUpdate: true, checkOnSubmit: true,
                    beforeShowForm: function (form) {
                        centerDialog(form, $('#list'));
                    }
                },
                //delete options
                {
                    url: '@Url.Action("DeleteProduct","Home")', reloadAfterSubmit: false
                })
            .jqGrid('navButtonAdd', "#pager", {
                caption: "حذف ردیف‌های انتخابی", title: "Delete Toolbar", buttonicon: 'ui-icon ui-icon-trash',
                onClickButton: function () {
                    var idsList = jQuery("#list").jqGrid('getGridParam', 'selarrrow');
                    alert(idsList);
                    //jQuery("#list").jqGrid('delGridRow',idsList,{reloadAfterSubmit:false});
                }
            });
        };

        function centerDialog(form, grid) {
            var dlgDiv = $("#editmod" + grid[0].id);
            var parentDiv = dlgDiv.parent(); // div#gbox_list
            var dlgWidth = dlgDiv.width();
            var parentWidth = parentDiv.width();
            var dlgHeight = dlgDiv.height();
            var parentHeight = parentDiv.height();
            var parentTop = parentDiv.offset().top;
            var parentLeft = parentDiv.offset().left;
            dlgDiv[0].style.top =  Math.round(  parentTop  + (parentHeight-dlgHeight)/2  ) + "px";
            dlgDiv[0].style.left = Math.round(  parentLeft + (parentWidth-dlgWidth  )/2 )  + "px";
        }
ابتدای تنظیمات آن، شاهد add: true, del: true, edit: true هستید. این مورد سبب می‌شود تا در فوتر گرید، سه دکمه‌ی افزودن، ویرایش و حذف ردیف‌ها ظاهر شوند:


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


و با کلیک بر روی دکمه‌ی ویرایش ردیفی انتخاب شده، صفحه‌ی ویرایش آن ردیف به همراه مقادیر سلول‌های آن ظاهر خواهند شد:


تنظیمات قسمت‌های Add و Delete ویرایش توسط فرم‌ها، با حالت ویرایش داخل ردیفی آنچنان تفاوتی ندارد. فقط در اینجا پیش از نمایش فرم، از متد centerDialog برای نمایش صفحات افزودن و ویرایش رکوردها در وسط صفحه، استفاده شده‌است. توسط checkOnUpdate: true, checkOnSubmit: true سبب خواهیم شد تا اگر کاربر مقادیر موجود فرمی را تغییر داده‌است و سعی در بستن فرم، بدون ذخیره سازی اطلاعات کند، پیغام هشدار دهنده‌ای به او نمایش داده شود که آیا می‌خواهید تغییرات را ذخیره کنید یا خیر؟


- در انتهای متد formEdit، به کمک متد jqGrid و پارامتر navButtonAdd یک دکمه‌ی سفارشی را نیز اضافه کرده‌ایم. اگر به ستون پس از شماره‌های خودکار ردیف‌ها، در سمت راست گرید دقت کنید، یک سری chekbox قابل مشاهده هستند. برای فعال سازی خودکار آن‌ها کافی است خاصیت multiselect گرید به true تنظیم شود. اکنون برای دسترسی به این ستون‌های انتخاب شده، می‌توان از متد jqGrid به همراه پارامترهای getGridParam و selarrrow استفاده کرد. خروجی آن، لیست idهای ستون‌ها است.


برای مطالعه بیشتر

Common Editing Properties
Inline Editing
Form Editing
Cell Editing


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid04.zip
مطالب
معماری لایه بندی نرم افزار #2

Domain Model یا Business Layer

پیاده سازی را از منطق تجاری یا Business Logic آغاز می‌کنیم. در روش کد نویسی Smart UI، منطق تجاری در Code Behind قرار می‌گرفت اما در روش لایه بندی، منطق تجاری و روابط بین داده‌ها در Domain Model طراحی و پیاده سازی می‌شوند. در مطالب بعدی راجع به Domain Model و الگوهای پیاده سازی آن بیشتر صحبت خواهم کرد اما بصورت خلاصه این لایه یک مدل مفهومی از سیستم می‌باشد که شامل تمامی موجودیت‌ها و روابط بین آنهاست.

الگوی Domain Model جهت سازماندهی پیچیدگی‌های موجود در منطق تجاری و روابط بین موجودیت‌ها طراحی شده است.

شکل زیر مدلی را نشان می‌دهد که می‌خواهیم آن را پیاده سازی نماییم. کلاس Product موجودیتی برای ارائه محصولات یک فروشگاه می‌باشد. کلاس Price جهت تشخیص قیمت محصول، میزان سود و تخفیف محصول و همچنین استراتژی‌های تخفیف با توجه به منطق تجاری سیستم می‌باشد. در این استراتژی همکاران تجاری از مشتریان عادی تفکیک شده اند.

Domain Model را در پروژه SoCPatterns.Layered.Model پیاده سازی می‌کنیم. بنابراین به این پروژه یک Interface به نام IDiscountStrategy را با کد زیر اضافه نمایید:

public interface IDiscountStrategy
{
    decimal ApplyExtraDiscountsTo(decimal originalSalePrice);
}

علت این نوع نامگذاری Interface فوق، انطباق آن با الگوی Strategy Design Pattern می‌باشد که در مطالب بعدی در مورد این الگو بیشتر صحبت خواهم کرد. استفاده از این الگو نیز به این دلیل بود که این الگو مختص الگوریتم هایی است که در زمان اجرا قابل انتخاب و تغییر خواهند بود.

توجه داشته باشید که معمولا نام Design Pattern انتخاب شده برای پیاده سازی کلاس را بصورت پسوند در انتهای نام کلاس ذکر می‌کنند تا با یک نگاه، برنامه نویس بتواند الگوی مورد نظر را تشخیص دهد و مجبور به بررسی کد نباشد. البته به دلیل تشابه برخی از الگوها، امکان تشخیص الگو، در پاره ای از موارد وجود ندارد و یا به سختی امکان پذیر است.

الگوی Strategy یک الگوریتم را قادر می‌سازد تا در داخل یک کلاس کپسوله شود و در زمان اجرا به منظور تغییر رفتار شی، بین رفتارهای مختلف سوئیچ شود.

حال باید دو کلاس به منظور پیاده سازی روال تخفیف ایجاد کنیم. ابتدا کلاسی با نام TradeDiscountStrategy را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:

public class TradeDiscountStrategy : IDiscountStrategy
{
    public decimal ApplyExtraDiscountsTo(decimal originalSalePrice)
    {
        return originalSalePrice * 0.95M;
    }
}

سپس با توجه به الگوی Null Object کلاسی با نام NullDiscountStrategy را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:

public class NullDiscountStrategy : IDiscountStrategy
{
    public decimal ApplyExtraDiscountsTo(decimal originalSalePrice)
    {
        return originalSalePrice;
    }
}

از الگوی Null Object زمانی استفاده می‌شود که نمی‌خواهید و یا در برخی مواقع نمی‌توانید یک نمونه (Instance) معتبر را برای یک کلاس ایجاد نمایید و همچنین مایل نیستید که مقدار Null را برای یک نمونه از کلاس برگردانید. در مباحث بعدی با جزئیات بیشتری در مورد الگوها صحبت خواهم کرد.

با توجه به استراتژی‌های تخفیف کلاس Price را ایجاد کنید. کلاسی با نام Price را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:

public class Price
{
    private IDiscountStrategy _discountStrategy = new NullDiscountStrategy();
    private decimal _rrp;
    private decimal _sellingPrice;
    public Price(decimal rrp, decimal sellingPrice)
    {
        _rrp = rrp;
        _sellingPrice = sellingPrice;
    }
    public void SetDiscountStrategyTo(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy;
    }
    public decimal SellingPrice
    {
        get { return _discountStrategy.ApplyExtraDiscountsTo(_sellingPrice); }
    }
    public decimal Rrp
    {
        get { return _rrp; }
    }
    public decimal Discount
    {
        get {
            if (Rrp > SellingPrice)
                return (Rrp - SellingPrice);
            else
                return 0;
        }
    }
    public decimal Savings
    {
        get{
            if (Rrp > SellingPrice)
                return 1 - (SellingPrice / Rrp);
            else
                return 0;
        }
    }
}

کلاس Price از نوعی Dependency Injection به نام Setter Injection در متد SetDiscountStrategyTo استفاده نموده است که استراتژی تخفیف را برای کالا مشخص می‌نماید. نوع دیگری از Dependency Injection با نام Constructor Injection وجود دارد که در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.

جهت تکمیل لایه Model، کلاس Product را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:

public class Product
{
    public int Id {get; set;}
    public string Name { get; set; }
    public Price Price { get; set; }
}

موجودیت‌های تجاری ایجاد شدند اما باید روشی اتخاذ نمایید تا لایه Model نسبت به منبع داده ای بصورت مستقل عمل نماید. به سرویسی نیاز دارید که به کلاینت‌ها اجازه بدهد تا با لایه مدل در اتباط باشند و محصولات مورد نظر خود را با توجه به تخفیف اعمال شده برای رابط کاربری برگردانند. برای اینکه کلاینت‌ها قادر باشند تا نوع تخفیف را مشخص نمایند، باید یک نوع شمارشی ایجاد کنید که به عنوان پارامتر ورودی متد سرویس استفاده شود. بنابراین نوع شمارشی CustomerType را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:

public enum CustomerType
{
    Standard = 0,
    Trade = 1
}

برای اینکه تشخیص دهیم کدام یک از استراتژی‌های تخفیف باید بر روی قیمت محصول اعمال گردد، نیاز داریم کلاسی را ایجاد کنیم تا با توجه به CustomerType تخفیف مورد نظر را اعمال نماید. کلاسی با نام DiscountFactory را با کد زیر ایجاد نمایید:

public static class DiscountFactory
{
    public static IDiscountStrategy GetDiscountStrategyFor
        (CustomerType customerType)
    {
        switch (customerType)
        {
            case CustomerType.Trade:
                return new TradeDiscountStrategy();
            default:
                return new NullDiscountStrategy();
        }
    }
}

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

لایه‌ی سرویس با برقراری ارتباط با منبع داده ای، داده‌های مورد نیاز خود را بر می‌گرداند. برای این منظور از الگوی Repository استفاده می‌کنیم. از آنجایی که لایه Model باید مستقل از منبع داده ای عمل کند و نیازی به شناسایی نوع منبع داده ای ندارد، جهت پیاده سازی الگوی Repository از Interface استفاده می‌شود. یک Interface به نام IProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:

public interface IProductRepository
{
    IList<Product> FindAll();
}

الگوی Repository به عنوان یک مجموعه‌ی در حافظه (In-Memory Collection) یا انباره ای از موجودیت‌های تجاری عمل می‌کند که نسبت به زیر بنای ساختاری منبع داده ای کاملا مستقل می‌باشد.

کلاس سرویس باید بتواند استراتژی تخفیف را بر روی مجموعه ای از محصولات اعمال نماید. برای این منظور باید یک Collection سفارشی ایجاد نماییم. اما من ترجیح می‌دهم از Extension Methods برای اعمال تخفیف بر روی محصولات استفاده کنم. بنابراین کلاسی به نام ProductListExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:

public static class ProductListExtensionMethods
{
    public static void Apply(this IList<Product> products,
                                        IDiscountStrategy discountStrategy)
    {
        foreach (Product p in products)
        {
            p.Price.SetDiscountStrategyTo(discountStrategy);
        }
    }
}

الگوی Separated Interface تضمین می‌کند که کلاینت از پیاده سازی واقعی کاملا نامطلع می‌باشد و می‌تواند برنامه نویس را به سمت Abstraction و Dependency Inversion به جای پیاده سازی واقعی سوق دهد.

حال باید کلاس Service را ایجاد کنیم تا از طریق این کلاس، کلاینت با لایه Model در ارتباط باشد. کلاسی به نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:

public class ProductService
{
    private IProductRepository _productRepository;
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public IList<Product> GetAllProductsFor(CustomerType customerType)
    {
        IDiscountStrategy discountStrategy =
                                DiscountFactory.GetDiscountStrategyFor(customerType);
        IList<Product> products = _productRepository.FindAll();
        products.Apply(discountStrategy);
        return products;
    }
}

در اینجا کدنویسی منطق تجاری در Domain Model به پایان رسیده است. همانطور که گفته شد، لایه‌ی Business یا همان Domain Model به هیچ منبع داده ای خاصی وابسته نیست و به جای پیاده سازی کدهای منبع داده ای، از Interface‌ها به منظور برقراری ارتباط با پایگاه داده استفاده شده است. پیاده سازی کدهای منبع داده ای را به لایه‌ی Repository واگذار نمودیم که در بخش‌های بعدی نحوه پیاده سازی آن را مشاهده خواهید کرد. این امر موجب می‌شود تا لایه Model درگیر پیچیدگی‌ها و کد نویسی‌های منبع داده ای نشود و بتواند به صورت مستقل و فارغ از بخشهای مختلف برنامه تست شود. لایه بعدی که می‌خواهیم کد نویسی آن را آغاز کنیم، لایه‌ی Service می‌باشد.

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

مطالب
معرفی سایت ExtensionMethod.NET

سایت ExtensionMethod.NET بانک اطلاعاتی است از قابلیتی تحت عنوان extension methods معرفی شده در C# 3.0 و Visual Basic 2008 . هدف اصلی از extension methods ، بسط کلاس‌های دات نت فریم ورک توسط جامعه‌ی برنامه نویس‌ها می‌باشد.



مثلا در کلاس پایه string ، متدی به نام Right وجود ندارد. برای اضافه کردن آن می‌توان به صورت زیر عمل کرد:

public static string Right(this string s, int length)
{
length = Math.Max(length, 0);
if (s.Length > length)
{
return s.Substring(s.Length - length, length);
}
else
{
return s;
}
}

و پس از آن هم استفاده از این متد که کلاس پایه string را بسط داده است به شکل زیر خواهد بود (همانند یکی از متدهای کلاس string می‌توان از آن استفاده کرد):
string s = "abcde";
s = s.Right(3); //s becomes "cde"

برای پیگیری سایت هم می‌توان از فید آن استفاده نمود.

مطالب
ASP.NET MVC #21

آشنایی با تکنیک‌های Ajax در ASP.NET MVC

اهمیت آشنایی با Ajax، ارائه تجربه‌ کاربری بهتری از برنامه‌های وب، به مصرف کنندگان نهایی آن می‌باشد. به این ترتیب می‌توان درخواست‌های غیرهمزمانی (asynchronous) را با فرمت XML یا Json به سرور ارسال کرد و سپس نتیجه نهایی را که حجم آن نسبت به یک صفحه کامل بسیار کمتر است، به کاربر ارائه داد. غیرهمزمان بودن درخواست‌ها سبب می‌شود تا ترد اصلی رابط کاربری برنامه قفل نشده و کاربر در این بین می‌تواند به سایر امور خود بپردازد. به این ترتیب می‌توان برنامه‌های وبی را که شبیه به برنامه‌های دسکتاپ هستند تولید نمود؛ کل صفحه مرتبا به سرور ارسال نمی‌شود، flickering و چشمک زدن صفحه کاهش خواهد یافت (چون نیازی به ترسیم مجدد کل صفحه نخواهد بود و عموما قسمتی جزئی از یک صفحه به روز می‌شود) یا بدون نیاز به ارسال کل صفحه به سرور، به کاربری خواهیم گفت که آیا اطلاعاتی که وارد کرده است معتبر می‌باشد یا نه (نمونه‌ای از آن‌ را در قسمت Remote validation اعتبار سنجی اطلاعات ملاحظه نمودید).


مروری بر محتویات پوشه Scripts یک پروژه جدید ASP.NET MVC در ویژوال استودیو

با ایجاد هر پروژه ASP.NET MVC‌ جدیدی در ویژوال استودیو، یک سری اسکریپت‌ هم به صورت خودکار در پوشه Scripts آن اضافه می‌شوند. تعدادی از این فایل‌ها توسط مایکروسافت پیاده سازی شده‌اند. برای مثال:
MicrosoftAjax.debug.js
MicrosoftAjax.js
MicrosoftMvcAjax.debug.js
MicrosoftMvcAjax.js
MicrosoftMvcValidation.debug.js
MicrosoftMvcValidation.js

این فایل‌ها از ASP.NET MVC 3 به بعد، صرفا جهت سازگاری با نگارش‌های قبلی قرار دارند و استفاده از آن‌ها اختیاری است. بنابراین با خیال راحت آن‌ها را delete کنید! روش توصیه شده جهت پیاده سازی ویژگی‌های Ajax ایی، استفاده از کتابخانه‌های مرتبط با jQuery می‌باشد؛ از این جهت که 100ها افزونه برای کار با آن توسط گروه وسیعی از برنامه نویس‌ها در سراسر دنیا تاکنون تهیه شده است. به علاوه فریم ورک jQuery تنها منحصر به اعمال Ajax ایی نیست و از آن جهت دستکاری DOM (document object model) و CSS صفحه نیز می‌توان استفاده کرد. همچنین حجم کمی نیز داشته،‌ با انواع و اقسام مرورگرها سازگار است و مرتبا هم به روز می‌شود.

در این پوشه سه فایل دیگر پایه کتابخانه jQuery نیز قرار دارند:
jquery-xyz-vsdoc.js
jquery-xyz.js
jquery-xyz.min.js

فایل vsdoc برای ارائه نهایی برنامه طراحی نشده است. هدف از آن ارائه Intellisense بهتری از jQuery در VS.NET می‌باشد. فایلی که باید به کلاینت ارائه شود، فایل min یا فشرده شده آن است. اگر به آن نگاهی بیندازیم به نظر obfuscated مشاهده می‌شود. علت آن هم حذف فواصل، توضیحات و همچنین کاهش طول متغیرها است تا اندازه فایل نهایی به حداقل خود کاهش پیدا کند. البته این فایل از دیدگاه مفسر جاوا اسکریپت یک مرورگر، فایل بی‌نقصی است!
اگر علاقمند هستید که سورس اصلی jQuery را مطالعه کنید، به فایل jquery-xyz.js مراجعه نمائید.
محل الحاق اسکریپت‌های عمومی مورد نیاز برنامه نیز بهتر است در فایل master page یا layout برنامه باشد که به صورت پیش فرض اینکار انجام شده است.
سایر فایل‌های اسکریپتی که در این پوشه مشاهده می‌شوند، یک سری افزونه عمومی یا نوشته شده توسط تیم ASP.NET MVC برفراز jQuery هستند.

به چهار نکته نیز حین استفاده از اسکریپت‌های موجود باید دقت داشت:
الف) همیشه از متد Url.Content همانند تعاریفی که در فایل Views\Shared\_Layout.cshtml مشاهده می‌کنید،‌ برای مشخص سازی مسیر ریشه سایت، استفاده نمائید. به این ترتیب صرفنظر از آدرس جاری صفحه، همواره آدرس صحیح قرارگیری پوشه اسکریپت‌ها در صفحه ذکر خواهد شد.
ب) ترتیب فایل‌های js مهم هستند. ابتدا باید کتابخانه اصلی jQuery ذکر شود و سپس افزونه‌های آن‌ها.
ج) اگر اسکریپت‌های jQuery در فایل layout سایت تعریف شده‌اند؛ نیازی به تعریف مجدد آن‌ها در View‌های سایت نیست.
د) اگر View ایی به اسکریپت ویژه‌ای جهت اجرا نیاز دارد، بهتر است آن‌را به شکل یک section داخل view تعریف کرد و سپس به کمک متد RenderSection این قسمت را در layout سایت مقدار دهی نمود. مثالی از آن‌را در قسمت 20 این سری مشاهده نمودید (افزودن نمایش جمع هر ستون گزارش).


یک نکته
اگر آخرین به روز رسانی‌های ASP.NET MVC را نیز نصب کرده باشید، فایلی به نام packages.config به صورت پیش فرض به هر پروژه جدید ASP.NET MVC اضافه می‌شود. به این ترتیب VS.NET به کمک NuGet این امکان را خواهد یافت تا شما را از آخرین به روز رسانی‌های این کتابخانه‌ها مطلع کند.


آشنایی با Ajax Helpers توکار ASP.NET MVC

اگر به تعاریف خواص و متدهای کلاس WebViewPage دقت کنیم:

using System;

namespace System.Web.Mvc
{
public abstract class WebViewPage<TModel> : WebViewPage
{
protected WebViewPage();
public AjaxHelper<TModel> Ajax { get; set; }
public HtmlHelper<TModel> Html { get; set; }
public TModel Model { get; }
public ViewDataDictionary<TModel> ViewData { get; set; }
public override void InitHelpers();
protected override void SetViewData(ViewDataDictionary viewData);
}
}

علاوه بر خاصیت Html که وهله‌ای از آن امکان دسترسی به Html helpers توکار ASP.NET MVC را در یک View فراهم می‌کند، خاصیتی به نام Ajax نیز وجود دارد که توسط آن می‌توان به تعدادی متد AjaxHelper توکار دسترسی داشت. برای مثال توسط متد Ajax.ActionLink می‌توان قسمتی از صفحه را به کمک ویژگی‌های Ajax، به روز رسانی کرد.


مثالی در مورد به روز رسانی قسمتی از صفحه به کمک متد Ajax.ActionLink

ابتدا نیاز است فایل Views\Shared\_Layout.cshtml را اندکی ویرایش کرد. برای این منظور سطر الحاق jquery.unobtrusive-ajax.min.js را به فایل layout برنامه اضافه نمائید (اگر این سطر اضافه نشود، متد Ajax.ActionLink همانند یک لینک معمولی رفتار خواهد کرد):

<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
</head>

سپس مدل ساده و منبع داده زیر را نیز به پروژه اضافه کنید:

namespace MvcApplication18.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}

using System.Collections.Generic;

namespace MvcApplication18.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
for (int i = 0; i < 1000; i++)
{
list.Add(new Employee { Id = i + 1, Name = "name " + i });
}
return list;
}
}
}

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

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

namespace MvcApplication18.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}

[HttpPost] //for IE-8
public ActionResult EmployeeInfo(int? id)
{
if (!Request.IsAjaxRequest())
return View("Error");

if (!id.HasValue)
return View("Error");

var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return View("Error");

return PartialView(viewName: "_EmployeeInfo", model: data);
}
}
}

بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. یک View خالی را به آن اضافه نمائید. همچنین بر روی متد EmployeeInfo کلیک راست کرده و با انتخاب گزینه Add view در صفحه ظاهر شده یک partial view را اضافه نمائید. جهت تمایز بین partial view و view هم بهتر است نام partial view با یک underline شروع شود.
کدهای partial view مورد نظر را به نحو زیر تغییر دهید:

@model MvcApplication18.Models.Employee

<strong>Name:</strong> @Model.Name

سپس کدهای View متناظر با متد Index را نیز به صورت زیر اعمال کنید:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>

<div id="EmployeeInfo">
@Ajax.ActionLink(
linkText: "Get Employee-1 info",
actionName: "EmployeeInfo",
controllerName: "Home",
routeValues: new { id = 1 },
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
})
</div>

<div id="Progress" style="display: none">
<img src="@Url.Content("~/Content/images/loading.gif")" alt="loading..." />
</div>

توضیحات جزئیات کدهای فوق

متد Ajax.ActionLink لینکی را تولید می‌کند که با کلیک کاربر بر روی آن، اطلاعات اکشن متد واقع در کنترلری مشخص، به کمک ویژگی‌های jQuery Ajax دریافت شده و سپس در مقصدی که توسط UpdateTargetId مشخص می‌گردد، بر اساس مقدار InsertionMode،‌ درج خواهد شد (می‌تواند قبل از آن درج شود یا پس از آن و یا اینکه کل محتوای مقصد را بازنویسی کند). HttpMethod آن هم به POST تنظیم شده تا با IE‌ مشکلی نباشد. از این جهت که IE پیغام‌های GET را کش می‌کند و مساله ساز خواهد شد. توسط پارامتر routeValues، آرگومان مورد نظر به متد EmployeeInfo ارسال خواهد شد.
به علاوه یکی دیگر از خواص کلاس AjaxOptions، برای معرفی حالت بروز خطایی در سمت سرور به نام OnFailure در نظر گرفته شده است. در اینجا می‌توان نام یک متد JavaScript ایی را مشخص کرده و پیغام خطای عمومی را در صورت فراخوانی آن به کاربر نمایش داد. یا توسط خاصیت Confirm آن می‌توان یک پیغام را پیش از ارسال اطلاعات به سرور به کاربر نمایش داد.
به این ترتیب در مثال فوق، id=1 به متد EmployeeInfo به صورت غیرهمزمان ارسال می‌گردد. سپس کارمندی بر این اساس یافت شده و در ادامه partial view مورد نظر بر اساس اطلاعات کاربر مذکور، رندر خواهد شد. نتیجه کار، در یک div با id مساوی EmployeeInfo درج می‌گردد (InsertionMode.Replace). متد Ajax.ActionLink از این جهت داخل div تعریف شده‌است که پس از کلیک کاربر و جایگزینی محتوا، محو شود. اگر نیازی به محو آن نبود، آن‌را خارج از div تعریف کنید.
عملیات دریافت اطلاعات از سرور ممکن است مدتی طول بکشد (برای مثال دریافت اطلاعات از بانک اطلاعاتی). به همین جهت بهتر است در این بین از تصاویری که نمایش دهنده انجام عملیات است، استفاده شود. برای این منظور یک div با id مساوی Progress تعریف شده و id آن به LoadingElementId انتساب داده شده است. این div با توجه به display: none آن، در ابتدای امر به کاربر نمایش داده نخواهد شد؛ در آغاز کار دریافت اطلاعات از سرور توسط متد Ajax.ActionLink نمایان شده و پس از خاتمه کار مجددا مخفی خواهد شد.
به علاوه اگر به کدهای فوق دقت کرده باشید، از متد Request.IsAjaxRequest نیز استفاده شده است. به این ترتیب می‌توان تشخیص داد که آیا درخواست رسیده از طرف jQuery Ajax صادر شده است یا خیر. البته آنچنان روش قابل ملاحظه‌ای نیست؛ چون امکان دستکاری Http Headers همیشه وجود دارد؛ اما بررسی آن ضرری ندارد. البته این نوع بررسی‌ها را در ASP.NET MVC بهتر است تبدیل به یک فیلتر سفارشی نمود؛ به این ترتیب حجم if و else نویسی در متدهای کنترلرها به حداقل خواهد رسید. برای مثال:

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    if (filterContext.HttpContext.Request.IsAjaxRequest())
    {
      base.OnActionExecuting(filterContext);
    }
    else
    {
      throw new InvalidOperationException("This operation can only be accessed via Ajax requests");
    }
  }
}

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

[AjaxOnly]
public ActionResult SomeAjaxAction()
{
    return Content("Hello!");
}


در مورد کلمه unobtrusive در قسمت بررسی نحوه اعتبار سنجی اطلاعات، توضیحاتی را ملاحظه نموده‌اید. در اینجا نیز از ویژگی‌های data-* برای معرفی پارامترهای مورد نیاز حین ارسال اطلاعات به سرور، استفاده می‌گردد. برای مثال خروجی متد Ajax.ActionLink به شکل زیر است. به این ترتیب امکان حذف کدهای جاوا اسکریپت از صفحه فراهم می‌شود و توسط یک فایل jquery.unobtrusive-ajax.min.js که توسط تیم ASP.NET MVC تهیه شده، اطلاعات مورد نیاز به سرور ارسال خواهد گردید:
<a data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST" 
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
href="/Home/EmployeeInfo/1">Get Employee-1 info</a>

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


به روز رسانی اطلاعات قسمتی از صفحه بدون استفاده از متد Ajax.ActionLink

الزامی به استفاده از متد Ajax.ActionLink و فایل jquery.unobtrusive-ajax.min.js وجود ندارد. اینکار را مستقیما به کمک jQuery نیز می‌توان به نحو زیر انجام داد:

<a href="#" onclick="LoadEmployeeInfo()">Get Employee-1 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfo() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfo",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning a simple text, not json
complete: function (xhr, status) {
var data = xhr.responseText;
if (status === 'error' || !data) {
//handleError
}
else {
$('#EmployeeInfo').html(data);
}
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}

توضیحات:
توسط متد jQuery.Ajax نیز می‌توان درخواست‌های Ajax ایی خود را به سرور ارسال کرد. در اینجا type نوع http verb مورد نظر را مشخص می‌کند که به POST تنظیم شده است. Url آدرس کنترلر را دریافت می‌کند. البته حین استفاده از متد توکار Ajax.ActionLink،‌ این لینک به صورت خودکار بر اساس تعاریف مسیریابی برنامه تنظیم می‌شود. اما در صورت استفاده مستقیم از jQuery.Ajax باید دقت داشت که با تغییر تعاریف مسیریابی برنامه نیاز است تا این Url نیز به روز شود.
سه سطر بعدی نوع اطلاعاتی را که باید به سرور POST شوند مشخص می‌کند. نوع json است و همچنین contentType آن برای ارسال اطلاعات یونیکد ضروری است. از متد JSON.stringify برای تبدیل اشیاء به رشته کمک گرفته‌ایم. این متد در تمام مرورگرهای امروزی به صورت توکار پشتیبانی می‌شود و استفاده از آن سبب خواهد شد تا اطلاعات به نحو صحیحی encode شده و به سرور ارسال شوند. بنابراین این رشته ارسالی اطلاعات را به صورت دستی تهیه نکنید؛ چون کاراکترهای زیادی هستند که ممکن است مشکل ساز شده و باید پیش از ارسال به سرور اصطلاحا escape یا encode شوند.
متداول است از پارامتر success برای دریافت نتیجه عملیات متد jQuery.Ajax استفاده شود. اما در اینجا از پارامتر complete آن استفاده شده است. علت هم اینجا است که return PartialView یک رشته را بر می‌گرداند. پارامتر success انتظار دریافت خروجی از نوع json را دارد. به همین جهت در این مثال خاص باید از پارامتر complete استفاده کرد تا بتوان به رشته بدون فرمت خروجی بدون مشکل دسترسی پیدا کرد.
به علاوه چون از یک section برای تعریف اسکریپت‌های مورد نیاز استفاده کرده‌ایم، برای درج خودکار آن در هدر صفحه باید قسمت هدر فایل layout برنامه را به صورت زیر مقدار دهی کرد:

@RenderSection("javascript", required: false)



دسترسی به اطلاعات یک مدل در View، به کمک jQuery Ajax

اگر جزئی از صفحه که قرار است به روز شود، پیچیده است، روش استفاده از partial viewها توصیه می‌شود؛ برای مثال می‌توان اطلاعات یک مدل را به همراه یک گرید کامل از اطلاعات، رندر کرد و سپس در صفحه درج نمود. اما اگر تنها به اطلاعات چند خاصیت از مدلی نیاز داشتیم، می‌توان از روش‌هایی با سربار کمتر نیز استفاده کرد. برای مثال متد جدید زیر را به کنترلر Home اضافه کنید:

[HttpPost] //for IE-8        
public ActionResult EmployeeInfoData(int? id)
{
if (!Request.IsAjaxRequest())
return Json(false);

if (!id.HasValue)
return Json(false);

var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return Json(false);

return Json(data);
}

سپس View برنامه را نیز به نحو زیر تغییر دهید:

<a href="#" onclick="LoadEmployeeInfoData()">Get Employee-2 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfoData() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfoData",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning the json data
success: function (result) {
if (result) {
alert(result.Id + ' - ' + result.Name);
}
hideProgress();
},
error: function (result) {
alert(result.status + ' ' + result.statusText);
hideProgress();
}
});
}

function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}

در این مثال، کنترلر برنامه، اطلاعات مدل را تبدیل به Json کرده و بازگشت خواهد داد. سپس می‌توان به اطلاعات این مدل و خواص آن در View برنامه، در پارامتر success متد jQuery.Ajax، مطابق کدهای فوق دسترسی یافت. اینبار چون خروجی کنترلر تعریف شده از نوع Json است، امکان استفاده از پارامتر success فراهم شده است. همه چیز هم در اینجا خودکار است؛ تبدیل یک شیء به Json و برعکس.
یک نکته: اگر نوع متد کنترلر، HttpGet باشد، نیاز خواهد بود تا پارامتر دوم متد بازگشت Json، مساوی JsonRequestBehavior.AllowGet قرار داده شود.


ارسال اطلاعات فرم‌ها به سرور، به کمک ویژگی‌های Ajax

متد کمکی توکار دیگری به نام Ajax.BeginForm در ASP.NET MVC وجود دارد که کار ارسال غیرهمزمان اطلاعات یک فرم را به سرور انجام داده و سپس اطلاعاتی را از سرور دریافت و قسمتی از صفحه را به روز خواهد کرد. مکانیزم کاری کلی آن بسیار شبیه به متد Ajax.ActionLink می‌باشد. در ادامه با تکمیل مثال قسمت جاری، به بررسی این ویژگی خواهیم پرداخت.
ابتدا متد جستجوی زیر را به کنترلر برنامه اضافه کنید:

[HttpPost] //for IE-8        
public ActionResult SearchEmployeeInfo(string data)
{
if (!Request.IsAjaxRequest())
return Content(string.Empty);

if (string.IsNullOrWhiteSpace(data))
return Content(string.Empty);

var employeesList = EmployeeDataSource.CreateEmployees();
var list = employeesList.Where(x => x.Name.Contains(data)).ToList();
if (list == null || !list.Any())
return Content(string.Empty);

return PartialView(viewName: "_SearchEmployeeInfo", model: list);
}

سپس بر روی نام متد کلیک راست کرده و گزینه add view را انتخاب کنید. در صفحه باز شده، گزینه create a stronlgly typed view را انتخاب کرده و قالب scaffolding را هم بر روی list قرار دهید. سپس گزینه ایجاد partial view را نیز انتخاب کنید. نام آن‌را هم _SearchEmployeeInfo وارد نمائید. برای نمونه خروجی حاصل به نحو زیر خواهد بود:

@model IEnumerable<MvcApplication18.Models.Employee>

<table>
<tr>
<th>
Name
</th>
</tr>

@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}

</table>

تا اینجا یک متد جستجو را ایجاد کرده‌ایم که می‌تواند لیستی از رکوردهای کارمندان را بر اساس قسمتی از نام آن‌ها که توسط کاربری جستجو شده است، بازگشت دهد. سپس این اطلاعات را به partial view مورد نظر ارسال کرده و یک جدول را بر اساس آن تولید خواهیم نمود.
اکنون به فایل Index.cshtml مراجعه کرده و فرم Ajax ایی زیر را اضافه نمائید:

@using (Ajax.BeginForm(actionName: "SearchEmployeeInfo",
controllerName: "Home",
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
}))
{
@Html.TextBox("data")
<input type="submit" value="Search" />
}

اینبار بجای استفاده از متد Html.BeginForm از متد Ajax.BeginForm استفاده شده است. به کمک آن اطلاعات Html.TextBox تعریف شده، به کنترلر Home و متد SearchEmployeeInfo آن، بر اساس HttpMethod تعریف شده، ارسال گردیده و نتیجه آن در یک div با id مساوی EmployeeInfo درج می‌گردد. همچنین اگر اطلاعاتی یافت نشد، به کمک متد return Content یک رشته خالی بازگشت داده می‌شود.
متد Ajax.BeginForm نیز از ویژگی‌های data-* برای تعریف اطلاعات مورد نیاز ارسالی به سرور استفاده می‌کند. بنابراین نیاز به سطر الحاق jquery.unobtrusive-ajax.min.js در فایل layout برنامه جهت وفق دادن این اطلاعات unobtrusive به اطلاعات مورد نیاز متد jQuery.Ajax وجود دارد.

<form action="/Home/SearchEmployeeInfo" data-ajax="true" 
data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
id="form0" method="post">
<input id="data" name="data" type="text" value="" />
<input type="submit" value="Search" />
</form>


کتابخانه کمکی «ASP.net MVC Awesome - jQuery Ajax Helpers»
علاوه بر متدهای توکار Ajax همراه با ASP.NET MVC، سایر علاقمندان نیز یک سری Ajax helper را بر اساس افزونه‌های jQuery تدارک دیده‌اند که از آدرس زیر قابل دریافت هستند:
http://awesome.codeplex.com/


افزودن فرم‌ها به کمک jQuery.Ajax و فعال سازی اعتبار سنجی سمت کلاینت

در ASP.NET MVC چون ViewState حذف شده است، امکان تزریق فرم‌های جدید به صفحه یا به روز رسانی قسمتی از صفحه توسط jQuery Ajax به سهولت و بدون دریافت پیغام «viewstate is corrupted» در حین ارسال اطلاعات به سرور، میسر است.
در این حالت باید به یک نکته مهم نیز دقت داشت: «اعتبار سنجی سمت کلاینت دیگر کار نمی‌کند». علت اینجا است که در حین بارگذاری متداول یک صفحه، متد زیر به صورت خودکار فراخوانی می‌گردد:
$.validator.unobtrusive.parse("#{form-id}");

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

$.ajax
({
url: "/{controller}/{action}/{id}",
type: "get",
success: function(data)
{
$.validator.unobtrusive.parse("#{form-id}");
}
});

//or
$.get('/{controller}/{action}/{id}', function (data) { $.validator.unobtrusive.parse("#{form-id}"); });

شبیه به همین مساله را حین استفاده از Ajax.BeginForm نیز باید مد نظر داشت:

@using (Ajax.BeginForm(
    "Action1",
    "Controller",
    null,
    new AjaxOptions {
        OnSuccess = "onSuccess",
        UpdateTargetId = "result"
    },
    null)
)
{
    <input type="submit" value="Save" />
}

var onSuccess = function(result) {
    // enable unobtrusive validation for the contents
    // that was injected into the <div id="result"></div> node
    $.validator.unobtrusive.parse("#result");
};

در این مثال در پارامتر UpdateTargetId، مجددا یک فرم رندر می‌شود. بنابراین اعتبار سنجی سمت کلاینت آن دیگر کار نخواهد کرد مگر اینکه با مقدار دهی خاصیت OnSuccess، مجددا متد unobtrusive.parse را فراخوانی کنیم.


نظرات مطالب
ساخت ربات تلگرامی با #C
سلام  . دو تا مشکل دارم :
1. اینکه نمیدونم چرا برخی از پیامهای دریافتی null هستش و بخاطر همین نال بودن rs.message برنامه میفته تو catch  . چطوری نال رو رد کنم ؟
2. وقتی از متد آپدیت استفاده میکنم وقتی داخل یک حلقه قرارش میدم همینطوری هر بار که حلقه شروع میشه میاد هرچی پیام قبلا توسط کاربر به بات فرستاده شده رو میگیره در آرایه ش میریزه و باز پاسخ میده و وقتی هم که پیام جدید واسش میاد نمیتونه به لیستش اضافه کنه .  
protected void Page_Load(object sender, EventArgs e)
        {
            long offset = 0;
            var Bot = new Telegram.Bot.TelegramBotClient("Token");
            int whilecount = 0;
            while (true)
            {
               
                WebRequest req = WebRequest.Create("https://api.telegram.org/bot" + "Token" + "/getUpdates");
                req.UseDefaultCredentials = true;
                WebResponse resp = req.GetResponse();
                Stream stream = resp.GetResponseStream();
                StreamReader sr = new StreamReader(stream);
                string s = sr.ReadToEnd();
                sr.Close();
                var jobject = Newtonsoft.Json.Linq.JObject.Parse(s);
                mydata gg = JsonConvert.DeserializeObject<mydata>(jobject.ToString());
                List<result> results = new List<result>();
                foreach (result rs in gg.result)
                {
                    try
                    {
                         
                        //Debug.Assert(message != null, "message != null");
                        //if ((BotTelegramWeb.TaktopBot.message.Equals(rs, null))!= true)
                        if (rs.update_id != 547758883 && rs.update_id !=547758886)
                        {
                            results.Add(rs);
                            SendMessage(rs.update_id.ToString(), "hello      " + rs.message.chat.first_name); 
                        }
                        else
                        {
                            continue;
                        }
                    }
                    catch (Exception ex)
                    {
                        throw;
                    }
                }
            }    
        }
       
    }

مطالب
رفرنس تایپ‌ها چگونه به ورودی متدها ارسال می‌شوند
اگر شما یک تایپ از نوع reference type را در ورودی یک متد قرار دهید و در داخل متد، پراپرتی‌های این تایپ را ویرایش کنید، بعد از آنکه از متد خارج می‌شود، تغییرات خود را مشاهده خواهید کرد. به طور مثال کد زیر را در نظر بگیرید که در داخل متد EditUserName، مقدار پراپرتی Name را تغییر داده‌ایم:
public class User
{
    public string Name { get; set; }
}
class Program
{
    static void Main(string[] args)
    {
        User user = new User
        {
            Name = "Farhad"
        };
        EditUserName(user);
        Console.WriteLine(user.Name);
        //Print Zamani in  console 
    }
    public static void EditUserName(User userCopy)
    {
        userCopy.Name = "Zamani";
    }
}
اگر کد بالا را اجرا کنید، مقدار "Zamani" را در صفحه کنسول مشاهده میکنید.
 اگر یک متغیر از نوع Value type مانند int را به ورودی متدی ارسال کنید و آن را در داخل متد تغییر دهید، تغییرات خود را بعد از آنکه از متد خارج میشود، مشاهده نمیکنید.
اما اگر در داخل متد (EditUserName) بالا که کلاس User را پاس داده‌ایم، مقدار پارامتر userCopy را برابر null کنیم چه اتفاقی میافتد؟ (خودم اول فکر کردم بعد از اینکه از متد EditUserName خارج میشه و میخواد user.Name رو چاپ کنه، Null reference exception رخ میده؛ ولی نه).
اگر تغییرات زیر را اعمال کنیم و مجددا برنامه را اجرا کنیم، همان نتیجه‌ی قبلی را مشاهده میکنیم:
static void Main(string[] args)
{
    User user = new User
    {
        Name = "Farhad"
    };
    EditUserName(user);
    Console.WriteLine(user.Name);
    //Print Zamani in console
}
public static void EditUserName(User userCopy)
{
    userCopy.Name = "Zamani";
    userCopy = null;
}
اگرچه نوع userCopy از نوع reference type میباشد، اما بعد از آنکه از متد خارج میشود، مقدار قبلی خود را دارد، چرا؟ 
زیرا زمانیکه شما مقدار user را به متد EditUserName پاس میدهید، یک کپی از آبجکت user به متد ارسال میشود، یعنی یک کپی از متغیر user ایجاد میشود و به ورودی متد ارسال میشود (خود متغیر user ارسال نمیشود) . بر این اساس میتوان گفت که user و userCopy هر دو به یک مکان از حافظه اشاره دارند که userCopy به متد EditUserName ارسال شده است.

 و زمانی که مقدار userCopy را برابر null میکنید، رفرنسی که به آن اشاره دارد، از بین میرود.

به همین دلیل اگر در داخل متد، پارامتری که از نوع reference type است را برابر null کنید و یا new کنید، بعد از آنکه از متد خارج شود، تغییرات را مشاهده نمی‌کنید. همین عمل برای نمونه سازی داخل متد نیز صدق میکند.
 برای مثال در کد زیر در داخل متد EditUserName، پارامتر userCopy را new میکنیم و سپس مقدار نام آن را تغییر میدهیم و بعد از آنکه از متد خارج میشود، همان مقداری را نشان میدهد که قبل از new شدن اعمال شده بود.
static void Main(string[] args)
{
    User user = new User
    {
        Name = "Farhad"
    };
    EditUserName(user);
    Console.WriteLine(user.Name);
    //Print Zamani in console
}
public static void EditUserName(User userCopy)
{
    userCopy.Name = "Zamani";
    userCopy = new User();
    userCopy.Name = "Farhad";
}
اگر کد بالا را اجرا کنید مجددا "Zamani" را در صفحه کنسول مشاهده میکنید؛ زیرا زمانیکه userCopy را new میکنید، رفرنسی که userCopy به آن اشاره دارد، تغییر میکند و به مکانی دیگر از حافظه اشاره میکند و تغییرات بر روی user که در متد Main قرار دارد اعمال نمیشود. 
در متغیر از نوع رفرنس، آدرس آبجکت اصلی کپی می‌شود و در واقع پارامتر، یک متغیر جدید است که آدرس ابجکت اصلی را دارد؛ بنابراین هنگامیکه به اعضای آبجکت دسترسی صورت گیرد، از طریق آدرس، به همان عضو آبجکت اصلی اشاره شده و درنتیجه تغییر، ماندگار می‌ماند. اما هنگامیکه خود پارامتر کلاس مقدار دهی شود، یک فضای جدید در حافظه ایجاد شده و آدرس آن در محتوای پارامتر کپی می‌شود. اینگونه پس از پایان متد، تغییر پایا نبوده، چرا که آدرس پارامتر، با آبجکت اصلی متفاوت خواهد بود. 
مطالب
مدیریت خطا ها در ASP.NET MVC
احتمال بوجود اومدن خطا در اجرای یک برنامه همیشه هست. خطاهایی که برنامه نویس ممکنه هیچ وقت فکر نکنه چنین خطایی در اجرای برنامه ای که نوشته بوجود بیاد و هیچ وقت هم از اون خطا اطلاع پیدا نکنه. ثبت و بررسی خطاهایی که در برنامه بوجود میاد میتونه مفید باشه اما آیا باید همه‌ی خطا‌ها رو ثبت کرد؟
خیر. ممکنه برخی از این خطا‌ها سلامت سیستم رو به خطر نندازه و اصلا ارتباطی هم به برنامه نداشته باشه . به طور مثال زمانی که کاربر یک صفحه ای و یا فایلی رو درخواست میده که وجود نداره.
توسط رویداد OnException میتونیم به خطاهای بوجود اومده در سطح یک Controller دسترسی داشته باشیم و اون رو مدیریت کنیم.
protected override void OnException(ExceptionContext filterContext)
{
    // Bail if we can't do anything
    if (filterContext == null)
        return;

    // log
    var ex = filterContext.Exception ??
            new Exception("No further information exists.");
    // save log ex.Message
    
    filterContext.ExceptionHandled = true;

    filterContext.Result = View("ViewName");
    base.OnException(filterContext);
}

ما تمام خطا هایی که در سطح یک کنترل اتفاق می‌افته رو با دوباره نویسی(override) متد OnException مدیریت میکنیم. اما اگه بخواهیم محدوده‌ی این مدیریت رو بیشتر کنیم و کاری کنیم که روی تمام نرم افزار اعمال بشه باید چه کار کنیم؟
رویداد Application_Error در Global.asax محل مناسبی برای انجام این کار هست اما باید چند مسئله رو در نظر بگیریم:

  • Server.Transfer : این متد یک فایل (میتونه یک صفحه باشه)رو به خروجی میفرسته بدون اینکه آدرس تغییر کنه(کاربر به صفحه دیگه ای منتقل بشه) نکته ای که وجود داره اینه که برای انتقال نیاز به وجود یک فایل فیزیکی بر روی سیستم داره تا اون فایل رو به خروجی منتقل کنه ، در پروژه‌های MVC فایل فیزیکی (به شکلی که در ASP.NET وجود داره) وجود نداره و بوسیله route‌ها ، controllerها و  view ‌ها یک صفحه(خروجی) تولید میشه بنابراین ما نمیتونیم  در ASP.NET MVC از این متد استفاده کنیم و باید به دنبال راه حلی برای فرستادن پاسخ مناسب باشیم.
  • Response.Redirect : این متد کاربر رو به یک صفحه‌ی دیگه منتقل میکنه و StatusCode رو برابر با 301 مقدار دهی میکنه که معنای انتقال صفحه هست و برای مشخص کردن "خطای داخلی سرور" (Internal Server Error) با کد 500 و پیدا نشدن فایل با کد 404 مناسب نیست .این کد(StatusCode) برای موتور‌های جستجو اهمیت زیادی داره لیست کامل این کدها و توضیحاتشون رو میتونید در این آدرس مطالعه کنید.
  • Response.Clear : این متد باید فراخوانی بشه تا خروجی تولید شده تا این لحظه رو پاک کنیم.
  • Server.ClearError :  این متد باید فراخوانی بشه تا از ایجاد صفحه زرد رنگ خطا جلوگیری کنه.
با توجه به نکات بالا، با طی کردن مراحل زیر میتونیم خطا‌های ایجاد شده رو مدیریت کنیم.
  1. بدست آوردن آخرین خطای ایجاد شده.
  2. بدست آوردن کد خطای ایجاد شده.
  3. ثبت خطا برای بررسی (ممکن هست شما برخی خطاها مثل پیدا نشدن فایل رو ثبت نکنید).
  4. پاک کردن خروجی ایجاد شده تا این لحظه.
  5. پاک کردن خطای ایجاد شده در سرور.
  6. فرستادن یک خروجی مناسب بدون تغییر مسیر.
protected void Application_Error(object sender, EventArgs e)
{
    var error = Server.GetLastError();
    var code = (error is HttpException) ? (error as HttpException).GetHttpCode() : 500;

    if (code != 404)
    {
        // save log error.Message
    }

    Response.Clear();
    Server.ClearError();

    string path = Request.Path;
    Context.RewritePath(string.Format("~/Errors/Http{0}", code), false);
    IHttpHandler httpHandler = new MvcHttpHandler();
    httpHandler.ProcessRequest(Context);
    Context.RewritePath(path, false);
}
  خروجی مناسب به کاربر از طریق از یکی از Action هایی انجام میشه که در ErrorsController هست. باید توجه داشته باشید که اگه در خود این ErrorsController خطایی رخ بده ، یک حلقه‌ی بی پایان بین این کنترلر و رویداد Application_Error اتفاق خواهد افتاد.
public class ErrorsController : Controller
{
    [HttpGet]
    public ActionResult Http404()
    {
        Response.StatusCode = 404;
        return View();
    }

    [HttpGet]
    public ActionResult Http500()
    {
        Response.StatusCode = 500;
        return View();
    }
}

مطالب
EF Code First #7

مدیریت روابط بین جداول در EF Code first به کمک Fluent API

EF Code first بجای اتلاف وقت شما با نوشتن فایل‌های XML تهیه نگاشت‌ها یا تنظیم آن‌ها با کد، رویه Convention over configuration را پیشنهاد می‌دهد. همین رویه، جهت مدیریت روابط بین جداول نیز برقرار است. روابط one-to-one، one-to-many، many-to-many و موارد دیگر را بدون یک سطر تنظیم اضافی، صرفا بر اساس یک سری قراردادهای توکار می‌تواند تشخیص داده و اعمال کند. عموما زمانی نیاز به تنظیمات دستی وجود خواهد داشت که قراردادهای توکار رعایت نشوند و یا برای مثال قرار است با یک بانک اطلاعاتی قدیمی از پیش موجود کار کنیم.


مفاهیمی به نام‌های Principal و Dependent

در EF Code first از یک سری واژه‌های خاص جهت بیان ابتدا و انتهای روابط استفاده شده است که عدم آشنایی با آن‌ها درک خطاهای حاصل را مشکل می‌کند:
الف) Principal : طرفی از رابطه است که ابتدا در بانک اطلاعاتی ذخیره خواهد شد.
ب) Dependent : طرفی از رابطه است که پس از ثبت Principal در بانک اطلاعاتی ذخیره می‌شود.
Principal می‌تواند بدون نیاز به Dependent وجود داشته باشد. وجود Dependent بدون Principal ممکن نیست زیرا ارتباط بین این دو توسط یک کلید خارجی تعریف می‌شود.


کدهای مثال مدیریت روابط بین جداول

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

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
public int Id { set; get; }
public string FirstName { set; get; }
public string LastName { set; get; }

public virtual AlimentaryHabits AlimentaryHabits { set; get; }
public virtual ICollection<CustomerAlias> Aliases { get; set; }
public virtual ICollection<Role> Roles { get; set; }
public virtual Address Address { get; set; }
}
}

namespace EF_Sample35.Models
{
public class CustomerAlias
{
public int Id { get; set; }
public string Aka { get; set; }

public virtual Customer Customer { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Role
{
public int Id { set; get; }
public string Name { set; get; }

public virtual ICollection<Customer> Customers { set; get; }
}
}

namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
public int Id { get; set; }
public bool LikesPasta { get; set; }
public bool LikesPizza { get; set; }
public int AverageDailyCalories { get; set; }

public virtual Customer Customer { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }

public virtual ICollection<Customer> Customers { set; get; }
}
}



همچنین تعاریف نگاشت‌های برنامه نیز مطابق کد‌های زیر است:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();

// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});

// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}


به همراه Context زیر:

using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample35.Mappings;
using EF_Sample35.Models;

namespace EF_Sample35.DataLayer
{
public class Sample35Context : DbContext
{
public DbSet<AlimentaryHabits> AlimentaryHabits { set; get; }
public DbSet<Customer> Customers { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CustomerConfig());
modelBuilder.Configurations.Add(new CustomerAliasConfig());

base.OnModelCreating(modelBuilder);
}
}

public class Configuration : DbMigrationsConfiguration<Sample35Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}

protected override void Seed(Sample35Context context)
{
base.Seed(context);
}
}
}


که نهایتا منجر به تولید چنین ساختاری در بانک اطلاعاتی می‌گردد:



توضیحات کامل کدهای فوق:

تنظیمات روابط one-to-one و یا one-to-zero

زمانیکه رابطه‌ای 0..1 و یا 1..1 است، مطابق قراردادهای توکار EF Code first تنها کافی است یک navigation property را که بیانگر ارجاعی است به شیء دیگر، تعریف کنیم (در هر دو طرف رابطه).
برای مثال در مدل‌های فوق یک مشتری که در حین ثبت اطلاعات اصلی او، «ممکن است» اطلاعات جانبی دیگری (AlimentaryHabits) نیز از او تنها در طی یک رکورد، دریافت شود. قصد هم نداریم یک ComplexType را تعریف کنیم. نیاز است جدول AlimentaryHabits جداگانه وجود داشته باشد.

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
}
}

namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
// ...
public virtual Customer Customer { get; set; }
}
}

در اینجا خواص virtual تعریف شده در دو طرف رابطه، به EF خواهد گفت که رابطه‌ای، 1:1 برقرار است. در این حالت اگر برنامه را اجرا کنیم، به خطای زیر برخواهیم خورد:

Unable to determine the principal end of an association between 
the types 'EF_Sample35.Models.Customer' and 'EF_Sample35.Models.AlimentaryHabits'.
The principal end of this association must be explicitly configured using either
the relationship fluent API or data annotations.

EF تشخیص داده است که رابطه 1:1 برقرار است؛ اما با قاطعیت نمی‌تواند طرف Principal را تعیین کند. بنابراین باید اندکی به او کمک کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
}
}
}


همانطور که ملاحظه می‌کنید در اینجا توسط متد WithRequired طرف Principal و توسط متد HasOptional، طرف Dependent تعیین شده است. به این ترتیب EF می‌توان یک رابطه 1:1 را تشکیل دهید.
توسط متد WillCascadeOnDelete هم مشخص می‌کنیم که اگر Principal حذف شد، لطفا Dependent را به صورت خودکار حذف کن.

توضیحات ساختار جداول تشکیل شده:
هر دو جدول با همان خواص اصلی که در دو کلاس وجود دارند، تشکیل شده‌اند.
فیلد Id جدول AlimentaryHabits اینبار دیگر Identity نیست. اگر به تعریف قید FK_AlimentaryHabits_Customers_Id دقت کنیم، در اینجا مشخص است که فیلد Id جدول AlimentaryHabits، به فیلد Id جدول مشتری‌ها متصل شده است (یعنی در آن واحد هم primary key است و هم foreign key). به همین جهت به این روش one-to-one association with shared primary key هم گفته می‌شود (کلید اصلی جدول مشتری با جدول AlimentaryHabits به اشتراک گذاشته شده است).


تنظیمات روابط one-to-many

برای مثال همان مشتری فوق را درنظر بگیرید که دارای تعدادی نام مستعار است:

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<CustomerAlias> Aliases { get; set; }
}
}

namespace EF_Sample35.Models
{
public class CustomerAlias
{
// ...
public virtual Customer Customer { get; set; }
}
}

همین میزان تنظیم کفایت می‌کند و نیازی به استفاده از Fluent API برای معرفی روابط نیست.
در طرف Principal، یک مجموعه یا لیستی از Dependent وجود دارد. در Dependent هم یک navigation property معرف طرف Principal اضافه شده است.
جدول CustomerAlias اضافه شده، توسط یک کلید خارجی به جدول مشتری مرتبط می‌شود.

سؤال: اگر در اینجا نیز بخواهیم CascadeOnDelete را اعمال کنیم، چه باید کرد؟
پاسخ: جهت سفارشی سازی نحوه تعاریف روابط حتما نیاز به استفاده از Fluent API به نحو زیر می‌باشد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}

اینکار را باید در کلاس تنظیمات CustomerAlias انجام داد تا بتوان Principal را توسط متد HasRequired به Customer و سپس dependent را به کمک متد WithMany مشخص کرد. در ادامه می‌توان متد WillCascadeOnDelete یا هر تنظیم سفارشی دیگری را نیز اعمال نمود.
متد HasRequired سبب خواهد شد فیلد Customer_Id، به صورت not null در سمت بانک اطلاعاتی تعریف شود؛ متد HasOptional عکس آن است.


تنظیمات روابط many-to-many

برای تنظیم روابط many-to-many تنها کافی است دو سر رابطه ارجاعاتی را به یکدیگر توسط یک لیست یا مجموعه داشته باشند:

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Role
{
// ...
public virtual ICollection<Customer> Customers { set; get; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<Role> Roles { get; set; }
}
}

همانطور که مشاهده می‌کنید، یک مشتری می‌تواند چندین نقش داشته باشد و هر نقش می‌تواند به چندین مشتری منتسب شود.
اگر برنامه را به این ترتیب اجرا کنیم، به صورت خودکار یک رابطه many-to-many تشکیل خواهد شد (بدون نیاز به تنظیمات نگاشت‌های آن). نکته جالب آن تشکیل خودکار جدول ارتباط دهنده واسط یا اصطلاحا join-table می‌باشد:

CREATE TABLE [dbo].[RolesJoinCustomers](
[RoleId] [int] NOT NULL,
[CustomerId] [int] NOT NULL,
)

سؤال: نام‌های خودکار استفاده شده را می‌خواهیم تغییر دهیم. چکار باید کرد؟
پاسخ: اگر بانک اطلاعاتی برای بار اول است که توسط این روش تولید می‌شود شاید این پیش فرض‌ها اهمیتی نداشته باشد و نسبتا هم مناسب هستند. اما اگر قرار باشد از یک بانک اطلاعاتی موجود که امکان تغییر نام فیلدها و جداول آن وجود ندارد استفاده کنیم، نیاز به سفارشی سازی تعاریف نگاشت‌ها به کمک Fluent API خواهیم داشت:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
}
}
}


تنظیمات روابط many-to-one

در تکمیل مدل‌های مثال جاری، به دو کلاس زیر خواهیم رسید. در اینجا تنها در کلاس مشتری است که ارجاعی به کلاس آدرس او وجود دارد. در کلاس آدرس، یک navigation property همانند حالت 1:1 تعریف نشده است:

namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
}
}

using System.Collections.Generic;

namespace EF_Sample35.Models
{
public class Customer
{
// …
public virtual Address Address { get; set; }
}
}

این رابطه توسط EF Code first به صورت خودکار به یک رابطه many-to-one تفسیر خواهد شد و نیازی به تنظیمات خاصی ندارد.
زمانیکه جداول برنامه تشکیل شوند، جدول Addresses موجودیتی مستقل خواهد داشت و جدول مشتری با یک فیلد به نام Address_Id به جدول آدرس‌ها متصل می‌گردد. این فیلد نال پذیر است؛ به عبارتی ذکر آدرس مشتری الزامی نیست.
اگر نیاز بود این تعاریف نیز توسط Fluent API سفارشی شوند، باید خاصیت public virtual ICollection<Customer> Customers به کلاس Address نیز اضافه شود تا بتوان رابطه زیر را توسط کدهای برنامه تعریف کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;

namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}

متد HasOptional سبب می‌شود تا فیلد Address_Id اضافه شده به جدول مشتری‌ها، null پذیر شود.