نظرات مطالب
EF Code First #1

سلام ممنون از  مطالب مفیدتون

من تازه 1 روزه شروع کردم code first کار کنم

تو یه مقاله  داشتم می‌خوندم  برای تعریف entity ها این association ‌ها رو تو هر دو طرف virtual   تعریف کرده بود

مثلا برای blog این طور نوشته بود

public virtual IList<post> posts { set; get; }

خودم هم این طور نوشتم تا این جا که فرقی ندیدم می‌شه توضیح بدید چه فرقی دارن

باز هم ممنونم

مطالب
مقدمه ای بر AutoMapper
AutoMapper کتابخانه ای ساده و سبک برای نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار هست و...
اگه این پست رو مطالعه کرده باشید یه مشکل امنیتی بنام «Mass Assignment» مطرح شد.برای رفع این مشکل یک روش استفاده از ViewModel بود.
فرض کنید Model ما
 public class User
    {
        public int Id { get; set; }
       
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string UserName { get; set; }

        public string Password { get; set; }

        public bool IsAdmin { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; }
    }
و ViewModel ما
 public class UserViewModel
    {
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string Password { get; set; }
    }
باشه(توجه کنید در واقع من برای View ی مورد نظرم فقط به نام , نام خانوادگی و پسورد نیاز دارم)
برای استفاده UserViewModel بعنوان Model در View ی مورد نظر باید شی UserViewModel رو با اطلاعات شی User مقدار دهی کنیم مثلا با کدی مثل این در کنترلر.
 
public ActionResult Index(int id = 1)
        {
            var user = _userService.GetById(id);
            var userViewModel = new UserViewModel
                {
                    FirstName = user.FirstName,
                    LastName = user.LastName,
                    Password = user.Password
                };

            return View(userViewModel);
        }
رهایی از نوشتن اینجور کدهای تکراری و خسته کننده باعث پیدایش AutoMapper شد...
برای استفاده از AutoMapper از نوگت استفاده میکنیم.
PM> Install-Package AutoMapper
در شروع برنامه نگاشت‌ها رو تعریف میکنم.یک روش ابداعی تعریف نگاشت‌ها در یک کلاس استاتیک و فراخوانی اون تو متد  Application_Start هست.
public static class AutoMapperWebConfiguration
    {

        public static void Configure()
        {
            ConfigureUserMapping();
        }

        private static void ConfigureUserMapping()
        {
            Mapper.CreateMap<User, UserViewModel>();
        }
    }

اولین پارامتر نوع مبدا و دومین پارامتر نوع مقصد هست.
برای انجام نگاشت هم از متد Map استفاده میکنیم.
public ActionResult Index(int id=1)
        {
            var user = _userService.GetById(id);
            var userViewModel=new UserViewModel();
            AutoMapper.Mapper.Map(user, userViewModel);
            
            return View(userViewModel);
        }
همنطور که میبنید با نوشتن چندین خط کد عملیات نگاشت اطلاعات یک شی به شی دیگه انجام شد.
ادامه دارد...
مطالب
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 پذیر شود.

مطالب
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# 8.0 - Async Streams
یک نکته‌ی تکمیلی: استفاده از IAsyncEnumerable در جهت ایجاد وب سرویس‌های REST با قابلیت Stream

 مقدمه
در Net Core 3. نوع‌های جدیدی با عنوان‌های <IA­syncEnumerable<T>,IAsync­Enumerator<T> در فضای نام System.Collections.Gener­ic معرفی شدند. همانطور که مشخص است این نوع‌های جدید کاملا با نوع‌های synchronous خود هم پوشانی دارند و مفاهیم قبلی را به پیاده سازی میکنند.

نوع <IAsync­Enumerable<T متد GetAsyncEnumerator را معرفی میکند تا عملیات enumeration را به صورت async انجام دهد و در خروجی این متد، نوع <IAsyncEnumerator<T را برگشت میدهد؛ به‌طوریکه این نوع disposable و دو عضو MoveNextAsync و Current را در خود دارد. اولی برای رسیدن به مقدار بعدی و دومی برای دریافت مقدار فعلی استفاده می‌شود. این در حالی است که MoveNextAsync بجای برگشت دادن یک bool یک <ValueTask<bool را برگشت می‌دهد. همچنین این متد، مقدار CancelationToken را همانند سایر فرآیندهایی که به صورت async تعریف می‌شوند، به صورت اختیاری از ورودی دریافت میکند، تا در صورت لزوم، عملیات جاری را کنسل کند. از طرفی به دلیل اینکه IAsyncEnumerator اینترفیس IAsyncDisposable را پیاده سازی میکند، متد DisposeAsync را نیز در اختیار دارد به‌طوریکه بجای void یک ValueTask را برگشت میدهد.


نحوه استفاده از IAsyncEnumerable 
static async IAsyncEnumerable<int> RangeAsync(int start, int count)
{
  for (int i = 0; i < count; i++)
  {
    await Task.Delay(i);
    yield return start + i;
  }
}
برای استفاده از این نوع در نهایت باید از عبارت yield return استفاده کرد. تا مقدار برگشتی مشخص شده در IAsyncEnumerable که در این مثال int است برگشت داده شود. در صورت استفاده نشدن از yield، خطای cannot return value from an iterator داده می‌شود.

پیاده سازی سمت سرور  

در قسمت قبل سعی بر این بود تا با این نوع جدید آشنا شویم. در این قسمت تلاش میکنیم تا با استفاده از این نوع یک وب سرویس stream را ایجاد کنیم .

ایجاد یک وب سرویس بدون خروجی IAsyncEnumerable

در مرحله اول، یک وب سرویس REST را بدون استفاده از IAsyncEnumerable ایجاد می‌کنیم تا متوجه مشکلات آن شویم و سپس در مرحله بعدی همین وب سرویس را با نوع IAsyncEnumerable  بازنویسی میکنیم.
    [ApiController]
    [Route("[controller]")]
    public class CustomerController : ControllerBase
    {
        private readonly IDictionary<int, Customer> _customers;
        private void FillCustomerFromMemory(int countOfCustomer)
        {
            for (int CustomerId = 1; CustomerId <= countOfCustomer; CustomerId++)
            {
                _customers.Add(key: CustomerId, new Customer($"name_{CustomerId}", CustomerId));
            }
        }
        public CustomerController()
        {
            _customers = new Dictionary<int, Customer>();
            FillCustomerFromMemory(countOfCustomer : 100);
        }
        [HttpGet]
        public async Task<IEnumerable<Customer>> Get()
        {
            var output = new List<Customer>();
            while (_customers.Any(_ => _.Key % 10 == 0))
            {
                var customer = _customers.First(_ => _.Key % 10 == 0);
                output.Add(new Customer(customer.Value.Name, customer.Key));
                await Task.Delay(500);
                _customers.Remove(customer);
            }
            return output;
        }

        public class Customer
        {
            public int Id { get; private set; }

            public string Name { get; private set; }
            public Customer(string name, int id)
            {
                Name = name;
                Id = id;
            }
        }
    }
در صورت اجرای این تکه کد و فراخوانی وب سرویس موجود بعد از بارگذاری کامل دیتا، خروجی به کاربر برگشت داده می‌شود. این در حالی است که ممکن است کاربر فقط به بخشی از این دیتا نیاز داشته باشد؛ برای مثال شاید صرفا به Id با مقدار ۸۰ نیاز داشته باشد، اما مجبور است تا بارگذاری کل دیتا صبر کند. برای رفع این مشکل وب سرویس موجود را مجدد باز نویسی میکنیم.

ایجاد یک وب سرویس با خروجی IAsyncEnumerable

  [HttpGet]
        public async IAsyncEnumerable<Customer> Get()
        {
            while (_customers.Any(_ => _.Key % 10 == 0))
            {
                var customer = _customers.First(_ => _.Key % 10 == 0);
                yield return new Customer(customer.Value.Name, customer.Key);
                _customers.Remove(customer);
                await Task.Delay(500);
            }
        }
این بار به محض اینکه یک دیتا ساخته شد، برگشت داده می‌شود و منتظر تمام دیتا نیستیم. این برگه برنده استفاده از IAsyncEnumerable , yield return است چرا که با ترکیب این دو میتوان وب سرویسی با قابلیت stream را ایجاد کرد. از طرفی حجم payload نیز کمتر شده‌است، چرا که هر بار صرفا یک بلاک مشخص از دیتا را به کلاینت ارسال میکنیم.

تا اینجا سمت سرور را به صورت stream پیاده سازی کردیم. در قسمت بعدی سمت کلاینت را نیز پیاده سازی میکنیم تا دیتا را همانطور که سرور، قسمت به قسمت ارسال میکند، کلاینت نیز آن را به شکل تک قسمتی دریافت کند.

پیاده سازی سمت کلاینت

در قسمت قبل تلاش کردیم تا یک وب سرویس با قابلیت stream را پیاده سازی کنیم. حال در این بخش کد کلاینت را به صورتی ایجاد میکنیم تا هر سری صرفا یک بلاک ارسال شده توسط سرور را دریافت و آن را Deserialize کند. برای این کار از کتابخانه Newtonsoft.Json استفاده میکنیم.
const int TARGET = 80;
var _httpClient = new HttpClient();
using (var response = await _httpClient.GetAsync(
    "https://localhost:7284/customer",
     HttpCompletionOption.ResponseHeadersRead))
{
    var stream = await response.Content.ReadAsStreamAsync();

    var _jsonSerializerSettings = new JsonSerializerSettings();
    var _serializer = Newtonsoft.Json.JsonSerializer.Create(_jsonSerializerSettings);

    using TextReader textReader = new StreamReader(stream);
    using JsonReader jsonReader = new JsonTextReader(textReader);

    await using (stream.ConfigureAwait(false))
    {
        await jsonReader.ReadAsync().ConfigureAwait(false);
        while (await jsonReader.ReadAsync().ConfigureAwait(false) &&
               jsonReader.TokenType != JsonToken.EndArray)
        {
            Customer customer = _serializer!.Deserialize<Customer>(jsonReader);
            if (customer.Id == TARGET)
            {
                Console.WriteLine(customer.Id + " : " + customer.Name);
                break;
            }
        }
    }
}
همانطورکه در کد بالا مشخص است، ابتدا یک درخواست Get را به آدرس وب سرویس زده و برای اینکه متجوجه شویم به انتهای لیست داده‌ها رسیدیم از jsonReader.TokenType != JsonToken.EndArray استفاده میکنیم. با این کار در صورتی که به ] نرسیده باشیم، باید عملیات خواندن از stream ادامه داشته باشد و هر سری بلاک جاری را Deserialize  میکنیم و در آخر در صورتیکه آیتم مورد نظر را دریافت کردیم، با دستور break از حلقه دریافت بلاک‌ها خارج می‌شویم.

 
استفاده از CancelationToken در جهت استفاده بهینه از منابع

تا اینجا به هدفی که انتظار داشتیم رسیدیم؛ به این شکل که یک وب سرویس را ایجاد کردیم تا اطلاعات را به صورت بخش بخش ارسال کند و کلاینتی ساختیم تا این اطلاعات را دریافت کند و در صورتیکه اطلاعات مورد نظر را دریافت کرد، به کار خواندن از وب سرویس خاتمه دهد. برای اینکه متوجه اهمیت CanclationToken  شویم دو سناریو زیر را با هم بررسی میکنیم :

سناریو اول - قطع کردن ارتباط توسط کلاینت

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

برای برطرف کردن مشکل، این سناریو کد سمت سرور را مجدد باز نویسی میکنیم : 
[HttpGet]
        public async IAsyncEnumerable<Customer> Get(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested && _customers.Any(_ => _.Key % 10 == 0))
            {
                var customer = _customers.First(_ => _.Key % 10 == 0);
                yield return new Customer(customer.Value.Name, customer.Key);
                _customers.Remove(customer);
                await Task.Delay(500,cancellationToken);
            }
        }
در کد بالا صرفا یک CancelationToken به ورودی متد اضافه شده و از آن در جهت اطمینان از اتصال کلاینت استفاده شده، به طوری که در حلقه اصلی ارسال اطلاعات شرط cancellationToken.IsCancellationRequested را چک میکند تا کاربر به دلایل مختلفی از دریافت اطلاعات منصرف نشده باشد و در صورت لغو کاربر، سرور به کار خود خاتمه میدهد

سناریو دوم-دستیابی کلاینت به اطلاعات مورد نظر

کلاینت در صورتیکه به اطلاعات مورد نظر از طریق وب سرویس دسترسی پیدا کرد، دیگر تمایلی به ادامه خواندن از جریان داده یا stream را ندارد و از حلقه خواندن اطلاعات خارج می‌شود. اما سرور همچنان درگیر ارسال اطلاعات است. برای رفع این مشکل کد سمت کلاینت را بازنویسی میکنیم: 
const int TARGET = 80;
var _httpClient = new HttpClient();
var _cancelationTokenSource = new CancellationTokenSource();

using (var response = await _httpClient.GetAsync(
    "https://localhost:7284/customer",
     HttpCompletionOption.ResponseHeadersRead,
     _cancelationTokenSource.Token))
{
    var stream = await response.Content.ReadAsStreamAsync(_cancelationTokenSource.Token);

    var _jsonSerializerSettings = new JsonSerializerSettings();
    var _serializer = Newtonsoft.Json.JsonSerializer.Create(_jsonSerializerSettings);

    using TextReader textReader = new StreamReader(stream);
    using JsonReader jsonReader = new JsonTextReader(textReader);

    await using (stream.ConfigureAwait(false))
    {
        await jsonReader.ReadAsync(_cancelationTokenSource.Token).ConfigureAwait(false);
        while (await jsonReader.ReadAsync(_cancelationTokenSource.Token).ConfigureAwait(false) &&
               jsonReader.TokenType != JsonToken.EndArray)
        {
            Customer customer = _serializer!.Deserialize<Customer>(jsonReader);
            if (customer.Id == TARGET)
            {
                Console.WriteLine(customer.Id + " : " + customer.Name);
                _cancelationTokenSource.Cancel();
                break;
            }
        }
    }
}

منابع :

https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8

https://code-maze.com/csharp-async-enumerable-yield

Github Link : https://github.com/Ershad95/Stream_REST_API
مطالب
اتصال Node.js به SQL Server با استفاده از Edge.js
اگر خواسته باشید که با استفاده از Node.js به SQL Server متصل شوید، احتمالا متوجه شده‌اید ماژولی که مایکروسافت منتشر کرده است، ناقص بوده و به صورت پیش نمایش است که بسیاری از ویژگی‌ها و مسائل مهم، در آن در نظر گرفته نشده است.

یکی دیگر از ماژول‌هایی که امکان اتصال Node.js را به SQL Server ممکن می‌کند، Edge.js است. Edge.js یک ماژول Node.js است که امکان اجرای کدهای دات نت را در همان پروسه توسط Node.js فراهم می‌کند. این مسئله، توسعه دهندگان Node.js را قادر می‌سازد تا از فناوری‌هایی که به صورت سنتی استفاده‌ی از آنها سخت یا غیر ممکن بوده است را به راحتی استفاده کنند. برای نمونه:
  • SQL Server
  • Active Directory
  • Nuget packages
  • استفاده از سخت افزار کامپیوتر (مانند وب کم، میکروفن و چاپگر)


نصب Node.js

اگر Node.js را بر روی سیستم خود نصب ندارید، می‌توانید از اینجا آن را دانلود کنید. بعد از نصب برای اطمینان از کارکرد آن، command prompt را باز کرده و دستور زیر را تایپ کنید:

node -v
شما باید نسخه‌ی نصب شده‌ی Node.js را مشاهده کنید.

ایجاد پوشه پروژه

سپس پوشه‌ای را برای پروژه Node.js خود ایجاد کنید. مثلا با استفاده از command prompt و دستور زیر:

md \projects\node-edge-test1
cd \projects\node-edge-test1

نصب Edge.js

Node با استفاده از package manager خود دانلود و نصب ماژول‌ها را خیلی آسان کرده است. برای نصب، در command prompt عبارت زیر را تایپ کنید:

npm install edge
npm install edge-sql
فرمان اول باعث نصب Edge.js و دومین فرمان سبب نصب پشتیبانی از SQL Server می‌شود.

Hello World

ایجاد یک فایل متنی با نام server.js و نوشتن کد زیر در آن:
var edge = require('edge');

// The text in edge.func() is C# code
var helloWorld = edge.func('async (input) => { return input.ToString(); }');

helloWorld('Hello World!', function (error, result) {
    if (error) throw error;
    console.log(result);
});
حالا برای اجرای این Node.js application از طریق command prompt کافی است به صورت زیر عمل کنید:
node server.js
همانطور که مشاهده می‌کنید "!Hello World" در خروجی چاپ شد.

ایجاد پایگاه داده تست

در مثال‌های بعدی، نیاز به یک پایگاه داده داریم تا query‌ها را اجرا کنیم. در صورتی که SQL Server بر روی سیستم شما نصب نیست، می‌توانید نسخه‌ی رایگان آن را از اینجا دانلود و نصب کنید. همچنین SQL Management Studio Express را نیز نصب کنید.

  1. در SQL Management Studio، یک پایگاه داده را با نام node-test با تنظیمات پیش فرض ایجاد کنید.
  2. بر روی پایگاه داده node-test راست کلیک کرده و New Query را انتخاب کنید.
  3. اسکریپت زیر را copy کرده و در آنجا paste کنید، سپس بر روی Execute کلیک کنید.
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('SampleUsers')) BEGIN; DROP TABLE SampleUsers; END; GO

CREATE TABLE SampleUsers ( Id INTEGER NOT NULL IDENTITY(1, 1), FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Email VARCHAR(255) NOT NULL, CreateDate DATETIME NOT NULL DEFAULT(getdate()), PRIMARY KEY (Id) ); GO

INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Orla','Sweeney','nunc@convallisincursus.ca','Apr 13, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Zia','Pickett','porttitor.tellus.non@Duis.com','Aug 31, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Justina','Ayala','neque.tellus.imperdiet@temporestac.com','Jul 28, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Levi','Parrish','adipiscing.elit@velarcueu.com','Jun 21, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Pearl','Warren','In@dignissimpharetra.org','Mar 3, 2014');
نتیجه‌ی اجرای کد بالا، ایجاد جدولی با نام SampleUsers و درج 5 رکورد در آن می‌شود.

تنظیمات ConnectionString

قبل از استفاده از Edge.js با SQL Server، باید متغیر محیطی (environment variable) با نام EDGE_SQL_CONNECTION_STRING را تعریف کنید.

set EDGE_SQL_CONNECTION_STRING=Data Source=localhost;Initial Catalog=node-test;Integrated Security=True
این متغیر تنها برای command prompt جاری تعریف شده است و با بستن آن از دست می‌رود. در صورتیکه از Node.js Tools for Visual Studio استفاده می‌کنید، نیاز به ایجاد یک متغیر محیطی دائمی و راه اندازی مجدد VS دارید. همچنین در صورتیکه بخواهید متغیر محیطی دائمی ایجاد کنید، فرمان زیر را اجرا کنید:
SETX EDGE_SQL_CONNECTION_STRING "Data Source=localhost;Initial Catalog=node-test;Integrated Security=True"


روش اول: اجرای مستقیم SQL Server Query در Edge.js

فایلی با نام server-sql-query.js را ایجاد کرده و کد زیر را در آن وارد کنید:

var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

var getTopUsers = edge.func('sql', function () {/*
    SELECT TOP 3 * FROM SampleUsers ORDER BY CreateDate DESC
*/});

function logError(err, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.write("Error: " + err);
    res.end("");
}    

http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html' });

    getTopUsers(null, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
        }
    });
}).listen(port);
console.log("Node server listening on port " + port);
سپس با استفاده از command prompt، فرمان زیر را اجرا کنید:
node server-sql-query.js
حال مرورگر خود را باز و سپس آدرس http://localhost:8080 را باز کنید. در صورتی که همه چیز به درستی انجام گرفته باشد لیستی از 3 کاربر را خواهید دید.

روش دوم: اجرای کد دات نت برای SQL Server Query

Edge.js تنها از دستورات Update، Insert، Select و Delete پشتیبانی می‌کند. در حال حاضر از store procedures و مجموعه‌ای از کد SQL پشتیبانی نمی‌کند. بنابراین، اگر چیزی بیشتر از عملیات CRUD می‌خواهید انجام دهید، باید از دات نت برای این کار استفاده کنید.

یادتان باشد، همیشه async

مدل اجرایی Node.js به صورت یک حلقه‌ی رویداد تک نخی است. بنابراین این بسیار مهم است که کد دات نت شما به صورت async باشد. در غیر اینصورت یک فراخوانی به دات نت سبب مسدود شدن و ایجاد خرابی در Node.js می‌شود.

ایجاد یک Class Library

اولین قدم، ایجاد یک پروژه Class Library در Visual Studio که خروجی آن یک فایل DLL است و استفاده از آن در Edge.js است. پروژه Class Library با عنوان EdgeSampleLibrary ایجاد کرده و فایل کلاسی با نام Sample1 را به آن اضافه کنید و سپس کد زیر را در آن وارد کنید:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace EdgeSampleLibrary
{
     public class Sample1
    {
        public async Task<object> Invoke(object input)
        {
            // Edge marshalls data to .NET using an IDictionary<string, object>
            var payload = (IDictionary<string, object>) input;
            var pageNumber = (int) payload["pageNumber"];
            var pageSize = (int) payload["pageSize"];
            return await QueryUsers(pageNumber, pageSize);
        }

        public async Task<List<SampleUser>> QueryUsers(int pageNumber, int pageSize)
        {
            // Use the same connection string env variable
            var connectionString = Environment.GetEnvironmentVariable("EDGE_SQL_CONNECTION_STRING");
            if (connectionString == null)
                throw new ArgumentException("You must set the EDGE_SQL_CONNECTION_STRING environment variable.");

            // Paging the result set using a common table expression (CTE).
            // You may rather do this in a stored procedure or use an 
            // ORM that supports async.
            var sql = @"
DECLARE @RowStart int, @RowEnd int;
SET @RowStart = (@PageNumber - 1) * @PageSize + 1;
SET @RowEnd = @PageNumber * @PageSize;

WITH Paging AS
(
    SELECT  ROW_NUMBER() OVER (ORDER BY CreateDate DESC) AS RowNum,
            Id, FirstName, LastName, Email, CreateDate
    FROM    SampleUsers
)
SELECT  Id, FirstName, LastName, Email, CreateDate
FROM    Paging
WHERE   RowNum BETWEEN @RowStart AND @RowEnd
ORDER BY RowNum;
";
            var users = new List<SampleUser>();

            using (var cnx = new SqlConnection(connectionString))
            {
                using (var cmd = new SqlCommand(sql, cnx))
                {
                    await cnx.OpenAsync();

                    cmd.Parameters.Add(new SqlParameter("@PageNumber", SqlDbType.Int) { Value = pageNumber });
                    cmd.Parameters.Add(new SqlParameter("@PageSize", SqlDbType.Int) { Value = pageSize });

                    using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection))
                    {
                        while (await reader.ReadAsync())
                        {
                            var user = new SampleUser
                            {
                                Id = reader.GetInt32(0), 
                                FirstName = reader.GetString(1), 
                                LastName = reader.GetString(2), 
                                Email = reader.GetString(3), 
                                CreateDate = reader.GetDateTime(4)
                            };
                           users.Add(user);
                        }
                    }
                }
            }
            return users;
        } 
    }

    public class SampleUser
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public DateTime CreateDate { get; set; }
    }
}
سپس ذخیره و کامپایل کنید. فایل DLL خروجی که در مسیر
[project]/bin/Debug/EdgeSampleLibrary.dll
قرار دارد را در پوشه‌ی پروژه Node کپی کنید. فایل جدیدی را با نام server-dotnet-query.js در پروژه Node ایجاد کنید و کد زیر را در آن وارد کنید:
var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

// Set up the assembly to call from Node.js 
var querySample = edge.func({ assemblyFile: 'EdgeSampleLibrary.dll', typeName: 'EdgeSampleLibrary.Sample1', methodName: 'Invoke' });

function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Got error: " + err); res.end(""); }

http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' });

    // This is the data we will pass to .NET
    var data = { pageNumber: 1, pageSize: 3 };

    // Invoke the .NET function
    querySample(data, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
            res.end("No results");
        }
    });

}).listen(port);

console.log("Node server listening on port " + port);
سپس از طریق command prompt آن را اجرا کنید:
node server-dotnet-query.js
حال مرورگر خود را باز کرده و به آدرس http://localhost:8080 بروید. در صورتیکه همه چیز به درستی انجام گرفته باشد، لیستی از 3 کاربر را خواهید دید. مقادیر pageNumber و pageSize را در فایل جاوااسکریپت تغییر دهید و تاثیر آن را بر روی خروجی مشاهده کنید.
 
نکته: برای ایجاد pageNumber و pageSize داینامیک با استفاده از ارسال مقادیر توسط QueryString، می‌توانید از ماژول connect استفاده کنید.
مطالب
فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطلب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال اول اکتفاء خواهد شد.


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

در سمت کلاینت، در حین تعریف ستون‌ها، ابتدا باید توسط مقدار دهی خاصیت search، ستون‌های مشارکت کننده‌ی در حین جستجو را مشخص کرد:
                colModel: [
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 200,
                        search: true, stype: 'text', searchoptions: { sopt: searchOptions }
                    },
                    {
                        name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 200,
                        search: true, stype: 'select', edittype: 'select', searchOperators: true,
                        searchoptions:
                        {
                            sopt: searchOptions, dataUrl: '@Url.Action("SuppliersSelect","Home")'
                        }
                    }
                ],
- برای نمونه در اینجا search: true، جهت دو ستون نام محصول و نام تولید کننده، تنظیم شده‌اند.
- stype، روش مقایسه‌ی مقادیر را مشخص می‌کند. مقدار پیش فرض آن text است و مقادیری مانند int، integer، float، number، numeric، date و datetime را می‌پذیرد.
- searchoptions برای تنظیم جزئیات نحو‌ه‌ی جستجوی بر روی فیلدها بکار می‌رود. توسط sopt می‌توان آرایه‌ای با مقادیر ذیل را مقدار دهی کرد:
 var searchOptions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge', 'bw', 'bn', 'in', 'ni', 'ew', 'en', 'cn', 'nc'];
eq به معنای مساوی است، ne، مساوی نیست، lt کمتر و به همین ترتیب.
البته باید دقت داشت که آرایه فوق کاملترین حالت ممکن است و ضرورتی ندارد تمام حالات را برای یک فیلد تعریف کرد. چون برای مثال جستجو cn یا contains برای مقادیر رشته‌ای معنا دارد و نه سایر حالات.
- searchoptions گزینه‌های دیگری را نیز می‌تواند شامل شود. برای مثال در حین نمایش جستجوی داخل ردیفی یا صفحه‌ی دیالوگ مخصوص آن، قصد داریم فیلد نام تولید کننده را توسط یک drop down نمایش دهیم و نه یک text box پیش فرض. برای این منظور dataUrl مقدار دهی شده‌است.
SuppliersSelect آن به اکشن متد ذیل اشاره می‌کند که لیست تولید کنندگان را با فرمت لیستی از SelectListItemها به یک partial view تولید کننده‌ی دراپ داون ارسال می‌کند:
        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);
        }
و محتوای فایل _SelectPartial نیز به صورت ذیل است:
 @model IList<SelectListItem>

@Html.DropDownList("srch", Model)
در این حالت با نمایش صفحه‌ی جستجو و انتخاب فیلد نام تولید کننده، به صورت خودکار یک dorp down پویا نمایش داده خواهد شد.

- اگر دقت کرده باشید، نام فیلد تولید کننده با Supplier.Id مقدار دهی شده‌است. علت اینجا است که در زمان استفاده از drop down، مقدار Id آیتم انتخابی، به سرور ارسال می‌شود. به همین جهت کار کردن پویا با Supplier.Id ساده‌تر خواهد بود.




- برای نمایش دیالوگ و یا نوار ابزار توکار جستجو، می‌توان دکمه‌هایی را به فوتر گرید اضافه کرد:


        $(document).ready(function () {
            $('#list').jqGrid({
              // ... مانند قبل و توضیحات فوق
            })
            .jqGrid('navGrid', '#pager',
                { add: false, edit: false, del: false },
                {},  // default settings for edit
                {},  // default settings for add
                {},  // delete instead that del:false we need this
                {
                    // search options
                    multipleSearch: true,
                    closeOnEscape: true,
                    closeAfterSearch: true,
                    ignoreCase: true
                })
            .jqGrid('navButtonAdd', "#pager", {
                caption: "نوار ابزار جستجو", title: "Search Toolbar", buttonicon: 'ui-icon-search',
                onClickButton: function () {
                    toolbarSearching();
                }
            });
        });
        

        function toolbarSearching() {
            $('#list').filterToolbar({
                groupOp: 'OR',
                defaultSearch: "cn",
                autosearch: true,
                searchOnEnter: true,
                searchOperators: true, // فعال سازی منوی اپراتورها
                stringResult : true // وجود این سطر سبب می‌شود تا اپراتورها به سرور ارسال شوند
            });
        };

        function singleSearching() {
            $('#list').searchGrid({
                closeAfterSearch: true
            });
        };

        function advancedSearching() {
            $('#list').searchGrid({
                multipleSearch: true,
                closeAfterSearch: true
            });
        };
در اینجا نکات ذیل قابل توجه هستند:
- با استفاده از متد jqGrid و پارامتر navGrid، در ناحیه‌ی pager گرید، تنظیمات جستجو را فعال کرده‌ایم.
multipleSearch به معنای امکان جستجوی همزمان بر روی بیش از یک فیلد است. closeOnEscape سبب بسته شدن صفحه‌ی دیالوگ جستجو با فشردن دکمه‌ی ESC می‌شود. اگر closeAfterSearch به true تنظیم نشود، صفحه‌ی دیالوگ جستجو پس از جستجو، در صفحه باقی مانده و بسته نخواهد شد.
- این دکمه‌ی جستجو، جزو موارد توکار jqGrid است. اگر قصد داشته باشیم یک دکمه‌ی سفارشی دیگر را نیز اضافه کنیم، مجددا از متد jqGrid با پارامتر navButtonAdd در ناحیه‌ی pager استفاده خواهیم کرد. کلیک بر روی آن سبب اجرای متد toolbarSearching می‌شود.

در اینجا حداقل سه نوع جستجو را می‌توان فعال کرد:
- filterToolbar که سبب نمایش نوار ابزار جستجو، دقیقا بالای ستون‌های جدول می‌شود.
- searchGrid که صفحه‌ی دیالوگ مستقلی را جهت جستجو به صورت پویا تولید می‌کند. اگر خاصیت multipleSearch آن به true تنظیم نشود، این جستجو هربار تنها بر روی یک فیلد قابل انجام خواهد بود و برعکس.
- در حالت جستجوی نوار ابزاری، اگر خواص searchOperators و stringResult به true تنظیم شوند، مانند تصویر ذیل، به ازای هر ستون می‌توان از عملگرهای مختلفی استفاده کرد. در غیراینصورت جستجوی انجام شده بر اساس groupOp و defaultSearch پیش فرض انجام می‌شود. یعنی And یا Or تمام موارد تنها در حالت مثلا contains یا تساوی و امثال آن و نه حالت پیشرفته‌ی انتخاب عملگرها توسط کاربر.


یک نکته
اگر می‌خواهید صفحه‌ی جستجو در وسط صفحه ظاهر شود، می‌توانید از تنظیمات CSS ذیل استفاده کنید:
/* align center search popup in jqgrid */
.ui-jqdialog {
    display: none;
    width: 300px;
    position: absolute;
    padding: .2em;
    font-size: 11px;
    overflow: visible;
    left: 30% !important;
    top: 40% !important;
}


پردازش سمت سرور جستجوی پویای jqGrid

کدهای سمت سرور، با کدهای استفاده از dynamic LINQ مایکروسافت یکی است. با این تفاوت که اینبار قسمت where این کوئری نیز پویا می‌باشد. پیشتر قسمت order by را پویا پردازش کرده بودیم. برای ساخت where پویا که در dynamic LINQ به خوبی پشتیبانی می‌شود، باید ابتدا ساختار اطلاعات ارسالی به سرور را آنالیز کنیم:
 //single field search
//_search=true&nd=1403935889318&rows=10&page=1&sidx=Id&sord=asc&searchField=Id&searchString=4444&searchOper=eq&filters=

//multi-field search
//_search=true&nd=1403935941367&rows=10&page=1&sidx=Id&sord=asc&filters=%7B%22groupOp%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22field%22%3A%22Id%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%2244%22%7D%2C%7B%22field%22%3A%22SupplierID%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%221%22%7D%5D%7D&searchField=&searchString=&searchOper=
// filters -> {"groupOp":"AND","rules":[{"field":"All","op":"cn","data":"fffff"},{"field":"Price","op":"bn","data":"ffff"}]}

//toolbar search
//_search=true&nd=1403935593036&rows=10&page=1&sidx=Id&sord=asc&Id=2&Name=333&SupplierID=1&CategoryID=1&Price=44
در اینجا ساختار ارسالی به سرور را در سه حالت مختلف جستجوی پویای jqGrid، ملاحظه می‌کنید:
- در تمام این حالات پارامتر _search مساوی true است (تفاوت آن با درخواست اطلاعات معمولی).
- در حالت جستجوی نوار ابزاری، اگر گزینه‌های searchOperators و stringResult به true تنظیم نشوند، حالت toolbar search فوق را شاهد خواهیم بود. در غیراینصورت به حالت multi-field search سوئیچ می‌شود.
- در حالت جستجوی تک فیلدی توسط صفحه دیالوگ جستجوی jqGrid، فیلد در حال جستجو توسط searchField و مقدار آن توسط searchString به سرور ارسال شده‌اند. مابقی پارامترها نال هستند.
- در حالت جستجوی چند فیلدی توسط صفحه دیالوگ جستجوی jqGrid، اینبار filters مقدار دهی شده‌است و سایر پارامترها نال هستند. مقدار filters ارسالی، در حقیقت یک شیء JSON است با ساختار کلی ذیل:
        { "groupOp": "AND",
              "groups" : [ 
                { "groupOp": "OR",
                    "rules": [
                        { "field": "name", "op": "eq", "data": "England" }, 
                        { "field": "id", "op": "le", "data": "5"}
                     ]
                } 
              ],
              "rules": [
                { "field": "name", "op": "eq", "data": "Romania" }, 
                { "field": "id", "op": "le", "data": "1"}
              ]
        }
که می‌توان چنین ساختاری را برای آن متصور شد:
    public class SearchFilter
    {
        public string groupOp { set; get; }
        public List<SearchGroup> groups { set; get; }
        public List<SearchRule> rules { set; get; }
    }

    public class SearchRule
    {
        public string field { set; get; }
        public string op { set; get; }
        public string data { set; get; }

        public override string ToString()
        {
            return string.Format("'{0}' {1} '{2}'", field, op, data);
        }
    }

    public class SearchGroup
    {
        public string groupOp { set; get; }
        public List<SearchRule> rules { set; get; }
    }
در اینجا AND و Or کلی مشخص می‌شود، به همراه فیلدهای ارسالی به سرور، عملگرهای اعمالی بر روی آن‌ها و مقادیر مرتبط.
اگر این موارد را کنار هم قرار دهیم، به متدی عمومی ApplyFilter با امضای ذیل خواهیم رسید.
   public IQueryable<T> ApplyFilter<T>(IQueryable<T> query, bool _search, string searchField, string searchString,
string searchOper, string filters, NameValueCollection form)
کدهای کامل آن‌را به علت طولانی بودن پردازش سه حالت ذکر شده‌ی فوق، از پروژه‌ی پیوست می‌توانید دریافت کنید.
پس از آن، تغییراتی که در کدهای متد GetProducts باید اعمال شوند به صورت ذیل است:
        [HttpPost]
        public ActionResult GetProducts(string sidx, string sord, int page, int rows,
                                        bool _search, string searchField, string searchString,
                                        string searchOper, string filters)
        {
            var list = ProductDataSource.LatestProducts;            

            var pageIndex = page - 1;
            var pageSize = rows;
            var totalRecords = list.Count;
            var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize);

            var productsQuery = list.AsQueryable();

            productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString,
                                                           searchOper, filters, this.Request.Form);
            var productsList = productsQuery.OrderBy(sidx + " " + sord)
                                            .Skip(pageIndex * pageSize)
                                            .Take(pageSize)
                                            .ToList();

            var productsData = new JqGridData
            {
                Total = totalPages,
                Page = page,
                Records = totalRecords,
                Rows = (productsList.Select(product => new JqGridRowData
                {
                    Id = product.Id,
                    RowCells = new List<string>
                               {
                                     product.Id.ToString(CultureInfo.InvariantCulture),
                                     product.Name,
                                     product.Supplier.CompanyName,
                                     product.Category.Name,
                                     product.Price.ToString(CultureInfo.InvariantCulture)
                                }
                })).ToArray()
            };
            return Json(productsData, JsonRequestBehavior.AllowGet);
        }
- ابتدا چند پارامتر اضافه‌تر به امضای متد اضافه شده‌اند، تا فیلدهای جستجو را نیز دریافت کنند.
- نوع متد به HttpPost تغییر کرده‌است. این مورد برای ارسال اطلاعات حجیم جستجوها به سرور ضروری است و بهتر است از حالت Get استفاده نشود.
این حالت در سمت کلاینت نیز باید تنظیم شود:
 $('#list').jqGrid({
//url access method type
mtype: 'POST',
- سایر سطرها مانند قبل است؛ فقط یک سطر ذیل جهت اعمال where پویا به عبارت LINQ ساخته شده، اضافه شده‌است:
 productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString,
  searchOper, filters, this.Request.Form);

برای مطالعه بیشتر
جستجوی تک فیلدی
جستجوی نوار ابزاری
جستجوی چند فیلدی


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid03.zip
 
مطالب
پیاده سازی Option یا Maybe در #C

Options یا Maybe در یک زبان تابعی مثل #F، نشان دهنده‌ی این است که شیء (Object) ممکن است وجود نداشته باشد(Null Reference) که یکی از مهمترین ویژگی‌های یک زبان شیءگرا مثل #C و یا Java محسوب می‌شودما برنامه نویس‌ها (اغلب) از هرچیزی که باعث کرش برنامه می‌شود، بیزاریم و برای اینکه برنامه کرش نکند، مجبور میشویم تمام کد‌های خود  را از Null Reference محافظت کنیم. تمام این مشکلات توسط Tony Hoare مخترع ALOGL است که تنها دلیل وجود Null References را سادگی پیاده سازی آن می‌داند و او این مورد را یک «خطای  میلیون دلاری» نامیده‌است. 

به این مثال توجه بفرمایید: 

public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

public class UserService : IUserService
    {
        private IList<User> _userData;

        public UserService()
        {
            _userData = new List<User>
            {
                new User {Id = 1,Name = "ali"},
                new User {Id = 2,Name = "Karim"}
            };
        }

        public User GetById(int id)
        {
            return _userData.FirstOrDefault(x => x.Id == id);
        }
    }  

public class UserController : Controller
    {
        private readonly IUserService _userService;

        public UserController(IUserService  userService)
        {
            _userService = userService;
        }
        public ActionResult Details(int id)
        {
            var user=_userService.GetById(3); // این متد ممکن است مقداری برگرداند و یا مقدار نال برگرداند                           
            if( user == null)
                 return HttpNotFound();    
            return View(user);  
        }
    }

این کدی است که ما برنامه نویسان به صورت متداولی با آن سروکار داریم. اما چه چیزی درباره این کد اشکال دارد؟

مشکل از آن جایی هست که ما نمی‌دانیم متد GetById مقداری را برمیگرداند و یا Null را بر می‌گرداند. این متد هرگاه که امکان برگرداندن Null وجود داشته باشد، خطای  NullReferenceException را در زمان اجرا بر می‌گرداند و همان طور که میدانید، به ازای هر شرطی که به برنامه اضافه میکنیم، پیچیدگی برنامه هم افزایش می‌یابد و کد خوانایی خود را از دست می‌دهد. تصور کنید دنیایی بدون NullReferenceException چه دنیایی زیبایی می‌بود؛ ولی متاسفانه این مورد از ویژگی‌های زبان #C است. خوشبختانه راه‌حل‌های برای حل NRE ارائه شده‌اند که در ادامه به آن‌ها می‌پردازیم.

ما می‌خواهیم متد GetById همیشه چیزی غیر از نال را برگرداند و یکی از راه‌هایی که ما را به این هدف می‌رساند این است که این متد یک توالی را برگرداند.

به نگاری جدید کد توجه بفرمایید:
public class UserService : IUserService
    {
        private IList<User> _userData;

        public UserService()
        {
            _userData = new List<User>
            {
                new User {Id = 1,Name = "ali"},
                new User {Id = 2,Name = "Karim"}
            };
        }

        public IEnumerable<User> GetById(int id)
        {
            var user = _userData.FirstOrDefault(x => x.Id == id);
            if (user == null) return new User[0];
            return new[] { user };
        }
    } 

اگر به امضای متد GetById توجه کنید، به جای اینکه User را برگرداند، این متد یک توالی از User را بر می‌گرداند و اگر در اینجا کاربری یافت شد، این توالی دارای یک المان خواهد بود و در غیر این صورت اگر User یافت نشد، این متد یک توالی را بر می‌گرداند که دارای هیچ المانی نیست. در ادامه اگر کلاینت بخواهد از متد GetById استفاده کند، به صورت زیر خواهد بود:

 public ActionResult Details(int id)
        {
            var user = _userService
                            .GetById(3)
                            .DefaultIfEmpty(new User())
                            .Single();
            return View(user);
        }

 متد GetById دارای دو وجه است و وجه مثبت آن این است که اگر مجموعه دارای مقداری باشد، هیچ مشکلی نیست؛ ولی اگر مجموعه دارای المانی نباشد، باید یک شیء را به صورت پیش فرض به آن اختصاص دهیم که این کار را با استفاده از متد DefualtIfEmpty انجام داده‌ایم. 

 در اول مقاله هم اشاره کردیم که  Maybe یا Options، مجموعه‌ای است که دارای یک المان و یا هیچ المانی است. اگر به امضای متد GetById توجه کنید، متوجه خواهید شد که این متد می‌تواند مجموعه‌ای را برگرداند و نمی‌تواند گارانتی کند که حتما مجموعه‌ای را بر می‌گرداند که دارای یک المان و یا هیچ باشد. برای حل این مشکل می‌توانیم از کلاس Option استفاده کنیم:

public class Option<T> : IEnumerable<T>
    {
        private readonly T[] _data;

        private Option(T[] data)
        {
            _data = data;
        }

        public static Option<T> Create(T element) => new Option<T>(new[] { element });

        public static Option<T> CreateEmpty() => new Option<T>(new T[0]);

        public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) _data).GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
    }

تنها دلیل استفاده از متد‌های Create و CreateEmpty این است که به خوانایی برنامه کمک کنیم؛ نه بیشتر. در ادامه اگر بخواهیم از کلاس option استفاده کنیم، به صورت زیر خواهد بود:

 public class UserService : IUserService
    {
       ...
       ...
       public Option<User> GetById(int id)
        {
            var user = _userData.FirstOrDefault(x => x.Id == id);
            return user == null ? Option<User>.CreateEmpty() : Option<User>.Create(user);
        }
    }

 public class UserController : Controller
    {
       ...
       ...
       public ActionResult Details(int id)
        {
            var user = _userService
                            .GetById(3)
                            .DefaultIfEmpty(new User())
                            .Single();
            return View(user);
        }
    }


چکیده:

مدیریت کردن References کار بسیار پیچیده‌ای است. قبل از آن که تلاش کنیم مقداری را برگردانیم و یا عملیاتی را بر روی آن انجام دهیم، اول باید مطمئن شویم که این شیء به جایی اشاره می‌کند. نمونه‌های متفاوتی از Option و یا Maybe را می‌توانید در اینترنت پیدا کنید که هدف نهایی آن‌ها، حذف NullReferenceException است و آشنایی با این ایده، شما را به دنیای برنامه نویسی تابعی در#C هدایت می‌کند.

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

صورت مساله

می‌خواهیم اطلاعات نمایش داده شده در گرید را به نحوی فرمت کنیم که
الف) اگر id ردیف مساوی 5 بود، رنگ و پس زمینه‌ی آن تغییر کند.
ب) نام محصول، به جزئیات آن لینک شود و این اطلاعات توسط یک jQuery UI Dialog نمایش داده شود.
ج) عدد قیمت با سه رقم جدا کننده همراه باشد.




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

مدل قسمت اول صرفا یک محصول بود. مدل قسمت جاری، اطلاعات تولید/تامین کننده آن‌را توسط کلاس Supplier نیز به همراه دارد:
namespace jqGrid02.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public Supplier Supplier { set; get; }
    }

    public class Supplier
    {
public int Id { set; get; }
        public string CompanyName { set; get; }
        public string Address { set; get; }
        public string PostalCode { set; get; }
        public string City { set; get; }
        public string Country { set; get; }
        public string Phone { set; get; }
        public string HomePage { set; get; }
    }
}

کدهای سمت سرور

کدهای سمت سرور مانند متد GetProducts به همراه صفحه بندی و مرتب سازی پویای آن دقیقا مانند قسمت قبل است.
در اینجا فقط یک اکشن متد جدید جهت بازگشت اطلاعات تولید کننده‌ای مشخص با فرمت JSON، اضافه شده‌است:
        public ActionResult GetGetSupplierData(int id)
        {
            var list = ProductDataSource.LatestProducts;
            var product = list.FirstOrDefault(x => x.Id == id);
            if (product == null)
                return Json(null, JsonRequestBehavior.AllowGet);

            return Json(new
                          {
                              product.Supplier.CompanyName,
                              product.Supplier.Address,
                              product.Supplier.PostalCode,
                              product.Supplier.City,
                              product.Supplier.Country,
                              product.Supplier.Phone,
                              product.Supplier.HomePage
                          }, JsonRequestBehavior.AllowGet);
        }

کدهای سمت کلاینت

صفحه دیالوگی که قرار است اطلاعات تولید کننده را نمایش دهد، یک چنین ساختاری دارد:
<div dir="rtl" id="supplierDialog">
    <span id="CompanyName"></span><br /><br />
    <span id="Address"></span><br />
    <span id="PostalCode"></span>, <span id="City"></span><br />
    <span id="Country"></span><br /><br />
    <span id="Phone"></span><br />
    <span id="HomePage"></span>
</div>
و تغییرات گرید برنامه به شرح زیر است:
    <script type="text/javascript">
        function showSupplierDialog(linkElement, supplierId) {
            //request json data
            $.getJSON('@Url.Action("GetGetSupplierData","Home")', { id: supplierId }, function (data) {
                //set values in dialog
                for (var property in data) {
                    if (data.hasOwnProperty(property)) {
                        $('#' + property).text(data[property]);
                    }
                }
                
                //get link position
                var linkPosition = $(linkElement).offset();
                $('#supplierDialog').dialog('option', 'position', [linkPosition.left, linkPosition.top]);
                //open dialog
                $('#supplierDialog').dialog('open');
            });
        }

        $(document).ready(function () {

            $('#supplierDialog').dialog({
                 autoOpen: false, bgiframe: true, resizable: false, title: 'تولید کننده'
            });

            $('#list').jqGrid({
                // .... مانند قبل
                colNames: ['شماره', 'نام محصول', 'قیمت'],
                //columns model
                colModel: [
                    {
                        name: 'Id', index: 'Id', align: 'right', width: 20,
                        formatter: function (cellvalue, options, rowObject) {
                            var cellValueInt = parseInt(cellvalue);
                            if (cellValueInt == 5) {
                                return "<span style='background: brown; color: yellow'>" + cellvalue + "</span>";
                            }
                            return cellvalue;
                        }
                    },
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 300,
                        formatter: function (cellvalue, options, rowObject) {
                            return "<a href='#' onclick='showSupplierDialog(this, " + rowObject[0] + ");'>" + cellvalue + "</a>";
                        }
                    },
                    {
                        name: 'Price', index: 'Price', align: 'center', width: 50,
                        formatter: 'currency',
                        formatoptions:
                        {
                            decimalSeparator: '.', thousandsSeparator: ',', decimalPlaces: 2, prefix: '$'
                        }
                    }
                ],
                // .... مانند قبل
            });
        });
    </script>
- همانطور که ملاحظه می‌کنید، توسط خاصیت formatter می‌توان عناصر در حال نمایش را فرمت کرد و بر روی نحوه‌ی نمایش نهایی آن‌ها تاثیرگذار بود.
در حالت ستون Id، از یک formatter سفارشی استفاده شده‌است. در اینجا این فرمت کننده به صورت یک callback عمل کرده و پیش از رندر نهایی اطلاعات، مقدار سلول جاری را توسط cellvalue در اختیار ما قرار می‌دهد. در این بین هر نوع فرمتی را که نیاز است می‌توان اعمال کرد و سپس یک رشته را بازگشت می‌دهیم. این رشته در سلول جاری درج خواهد شد.
- اگر مانند ستون Name، نیاز به مقادیر سایر سلول‌ها نیز وجود داشت، می‌توان از آرایه‌ی rowObject استفاده کرد. برای مثال در این حالت، یک لینک که کلیک بر روی آن سبب فراخوانی تابع showSupplierDialog می‌شود، در سلول‌های ستون Name درج خواهند شد. اولین rowObject که در اینجا مورد استفاده است، به ستون اول یا همان Id محصول اشاره می‌کند.
- در ستون Price از یک سری formatter از پیش تعریف شده استفاده شده‌است. نمونه‌ای از آن را در قسمت اول در ستون نمایش وضعیت موجود بودن محصول با تنظیم formatter: checkbox مشاهده کرده‌اید. در اینجا از یک formatter توکار دیگر به نام currency برای کار با مقادیر پولی استفاده شده‌است به همراه تنظیمات خاص آن.
- متد showSupplierDialog طوری تنظیم شده‌است که پس از دریافت Id یک محصول، آن‌را به سرور ارسال کرده و مشخصات تولید کننده‌ی آن‌را با فرمت JSON دریافت می‌کند. سپس در حلقه‌ای که مشاهده می‌کنید، خواص شیء جاوا اسکریپتی دریافتی استخراج و به spanهای supplierDialog انتساب داده می‌شوند. جهت سهولت کار، Id این spanها دقیقا مساوی Id خواص شیء دریافتی از سرور، درنظر گرفته شده‌اند.
- در مورد راست به چپ نمایش داده شدن عنوان دیالوگ، تغییرات CSS ایی لازم است که در قسمت اول بیان شدند.


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


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid02.zip
 
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 10 - بررسی تغییرات Viewها
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 11 - بررسی بهبودهای Razor» قسمت «نحوه‌ی سفارشی سازی کلاس پایه‌ی تمام Viewهای برنامه و معرفی inherits@ »، این مورد را توضیح دادم. برای نمونه اگر به فایل Views\_ViewStart.cshtml مراجعه کنید، یک چنین مقدار دهی عمومی در آن هست:
@{
    Layout = "_Layout";
}
این خاصیت عمومی Layout در کلاس پایه‌ی تمام ویووها تعریف شده‌است. اگر می‌خواهید معادل آن‌را داشته باشید، باید یک کلاس سفارشی پایه را با ارث بری از RazorPage ایجاد کنید:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Internal;

namespace Core1RtmTestResources.StartupCustomizations
{
    public abstract class MyCustomBaseView<TModel> : RazorPage<TModel>
    {
        //روش خاص تزریق وابستگی‌ها در فایل ویژه‌ی جاری
        [RazorInject]
        public IHtmlLocalizerFactory MyHtmlLocalizerFactory { get; set; }

        public IHtmlLocalizer MySharedLocalizer => MyHtmlLocalizerFactory.Create(
            baseName: "SharedResource" /*مشخصات*/,
            location: "Core1RtmTestResources.ExternalResources" /*نام اسمبلی ثالث*/);

        public bool IsAuthenticated()
        {
            return Context.User.Identity.IsAuthenticated;
        }

#pragma warning disable 1998
        public override async Task ExecuteAsync()
        {
        }
#pragma warning restore 1998
    }
}
در اینجا چند نکته مهم هستند:
- در کلاس پایه‌ی سفارشی، امکان تزریق وابستگی‌ها در سازنده‌ی کلاس وجود ندارد. اما از طریق ویژگی RazorInject می‌توان این‌کار را انجام داد.
- RazorInject نیاز به نصب وابستگی ذیل را دارد:
{
    "dependencies": {
        //same as before
        "Microsoft.AspNetCore.Mvc.Razor": "1.0.0"
    }
}
- پس از تعریف این کلاس پایه، برای معرفی آن به فایل Views\_ViewImports.cshtml مراجعه کنید:
@using Core1RtmTestResources
@using Microsoft.AspNetCore.Mvc.Localization
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@inherits Core1RtmTestResources.StartupCustomizations.MyCustomBaseView<TModel>
اکنون در تمام ویووهای برنامه به خاصیت عمومی MySharedLocalizer واقع در کلاس پایه‌ی سفارشی برنامه، دسترسی خواهید داشت:
MySharedLocalizer from MyCustomBaseView: @MySharedLocalizer["About Title"]