مطالب
آشنایی با mocking frameworks (چارچوب‌های تقلید) - قسمت اول

این مطلب در ادامه‌ی مطالب آزمو‌ن‌های واحد یا unit testing است.
نوشتن آزمون واحد برای کلاس‌هایی که با یک سری از الگوریتم‌ها ، مسایل ریاضی و امثال آن سر و کار دارند، ساده است. عموما این نوع کلاس‌ها وابستگی خارجی آنچنانی ندارند؛ اما در عمل کلاس‌های ما ممکن است وابستگی‌های خارجی بسیاری پیدا کنند؛ برای مثال کار با دیتابیس، اتصال به یک وب سرویس، دریافت فایل از اینترنت، خواندن اطلاعات از انواع فایل‌ها و غیره.
مطابق اصول آزمایشات واحد، یک آزمون واحد خوب باید ایزوله باشد. نباید به مرزهای سیستم‌های دیگر وارد شده و عملکرد سیستم‌های خارج از کلاس را بررسی کند.
این مثال ساده را در نظر بگیرید:
فرض کنید برنامه شما قرار است از یک وب سرویس لیستی از آدرس‌های IP یک کشور خاص را دریافت کند و در یک دیتابیس محلی آن‌ها را ذخیره نماید. به صورت متداول این کلاس باید اتصالی را به وب سرویس گشوده و اطلاعات را دریافت کند و همچنین آن‌ها را خارج از مرز کلاس در یک دیتابیس ثبت کند. نوشتن آزمون واحد برای این کلاس مطابق اصول مربوطه غیر ممکن است. اگر کلاس آزمون واحد آن‌را تهیه نمائید، این آزمون، integration test نام خواهد گرفت زیرا از مرزهای سیستم باید عبور نماید.

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

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

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

public static string GetComparisonText(IComparable a, IComparable b)
{
if (a.CompareTo(b) == 1)
return "a is bigger";
if (a.CompareTo(b) == -1)
return "b is bigger";
return "same";
}
در این مثال چون از IComparable استفاده شده، متد ما از هر نوع داده‌ای جهت مقایسه می‌تواند استفاده کند. تنها موردی که برای آن مهم خواهد بود این است که a راهی را برای مقایسه با b ارائه دهد.

اکنون با توجه به این توضیحات، برای ایزوله کردن ارتباط با دیتابیس و وب سرویس در مثال فوق، می‌توان اینترفیس‌های زیر را تدارک دید:
    public interface IEmailSource
{
IEnumerable<string> GetEmailAddresses();
}

public interface IEmailDataStore
{
void SaveEmailAddresses(IEnumerable<string> emailAddresses);

}
در اینجا استفاده و تعریف اینترفیس‌ها چندین خاصیت را به همراه خواهد داشت :
الف) به این صورت تنها مشخص می‌شود که چه کاری را قصد داریم انجام دهیم و کاری به پیاده سازی آن نداریم.
ب) ساخت کلاس بدون وجود یا دسترسی به یک دیتابیس میسر می‌شود. این مورد خصوصا در یک کار تیمی که قسمت‌های مختلف کار به صورت همزمان در حالت پیشرفت و تهیه است حائز اهمیت می‌شود.
ج) با توجه به اینکه در اینجا به پیاده سازی توجهی نداریم، می‌توان از این اینترفیس‌ها جهت تقلید دنیای واقعی استفاده کنیم. (که در اینجا mocking نام گرفته است)

جهت تقلید رفتار و عملکرد این دو اینترفیس، به کلاس‌های تقلید زیر خواهیم رسید:

public class MockEmailSource : IEmailSource
{
public IEnumerable<string> EmailAddressesToReturn { get; set; }
public IEnumerable<string> GetEmailAddresses()
{
return EmailAddressesToReturn;
}
}

public class MockEmailDataStore : IEmailDataStore
{
public IEnumerable<string> SavedEmailAddresses { get; set; }
public void SaveEmailAddresses(IEnumerable<string> emailAddresses)
{
SavedEmailAddresses = emailAddresses;
}

}
تا اینجا اولین قدم در مورد ایزوله سازی کلاس‌هایی که به مرز سیستم‌های دیگر وارد می‌شوند، برداشته شد. اما به مرور زمان مدیریت این اینترفیس‌ها و افزودن رفتارهای جدید به کلاس‌های مشتق شده از آن‌ها مشکل می‌شود. به همین جهت تا حد ممکن از پیاده سازی دستی آن‌ها خودداری شده و روش پیشنهادی استفاده از mocking frameworks است.

ادامه دارد ....

مطالب
آشنایی با NHibernate - قسمت دوم

آزمون واحد کلاس نگاشت تهیه شده

در مورد آشنایی با آزمون‌های واحد لطفا به برچسب مربوطه در سمت راست سایت مراجعه بفرمائید. همچنین در مورد اینکه چرا به این نوع API کلمه Fluent اطلاق می‌شود، می‌توان به تعریف آن جهت مطالعه بیشتر مراجعه نمود.

در این قسمت قصد داریم برای بررسی وضعیت کلاس نگاشت تهیه شده یک آزمون واحد تهیه کنیم. برای این منظور ارجاعی را به اسمبلی nunit.framework.dll به پروژه UnitTests که در ابتدای کار به solution جاری در VS.Net افزوده بودیم، اضافه نمائید (همچنین ارجاع‌هایی به اسمبلی‌های پروژه NHSample1 ، FluentNHibernate ، System.Data.SQLite ، NHibernate.ByteCode.Castle و Nhibernate نیز نیاز هستند). تمام اسمبلی‌های این فریم ورک‌ها از پروژه FluentNHibernate قابل استخراج هستند.

سپس سه کلاس زیر را به پروژه آزمون واحد اضافه خواهیم کرد.
کلاس TestModel : (جهت مشخص سازی محل دریافت اطلاعات نگاشت)

using FluentNHibernate;
using NHSample1.Domain;

namespace UnitTests
{
public class TestModel : PersistenceModel
{
public TestModel()
{
AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
}
}
}

کلاس FixtureBase : (جهت ایجاد سشن NHibernate در ابتدای آزمون واحد و سپس پاکسازی اشیاء در پایان کار)

using NUnit.Framework;
using NHibernate;
using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;

namespace UnitTests
{
public class FixtureBase
{
protected SessionSource SessionSource { get; set; }
protected ISession Session { get; private set; }

[SetUp]
public void SetupContext()
{
var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.InMemory);

SessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
new TestModel());

Session = SessionSource.CreateSession();
SessionSource.BuildSchema(Session);
}

[TearDown]
public void TearDownContext()
{
Session.Close();
Session.Dispose();
}
}
}

و کلاس CustomerMapping_Fixture.cs : (جهت بررسی صحت نگاشت تهیه شده با کمک دو کلاس قبل)

using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;

namespace UnitTests
{
[TestFixture]
public class CustomerMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_customer()
{
new PersistenceSpecification<Customer>(Session)
.CheckProperty(c => c.Id, 1001)
.CheckProperty(c => c.FirstName, "Vahid")
.CheckProperty(c => c.LastName, "Nasiri")
.CheckProperty(c => c.AddressLine1, "Addr1")
.CheckProperty(c => c.AddressLine2, "Addr2")
.CheckProperty(c => c.PostalCode, "1234")
.CheckProperty(c => c.City, "Tehran")
.CheckProperty(c => c.CountryCode, "IR")
.VerifyTheMappings();
}
}
}

توضیحات:
اکنون به عنوان یک برنامه نویس متعهد نیاز است تا کار صورت گرفته در قسمت قبل را آزمایش کنیم.
کار بررسی صحت نگاشت تعریف شده در قسمت قبل توسط کلاس استاندارد PersistenceSpecification فریم ورک FluentNHibernate انجام خواهد شد (در کلاس CustomerMapping_Fixture). این کلاس برای انجام عملیات آزمون واحد نیاز به کلاس پایه دیگری به نام FixtureBase دارد که در آن کار ایجاد سشن NHibernate (در قسمت استاندارد SetUp آزمون واحد) و سپس آزاد سازی آن را در هنگام خاتمه کار ، انجام می‌دهد (در قسمت TearDown آزمون واحد).
این ویژگی‌ها که در مباحث آزمون واحد نیز به آن‌ها اشاره شده است، سبب اجرای متدهایی پیش از اجرا و بررسی هر آزمون واحد و سپس آزاد سازی خودکار منابع خواهند شد.
برای ایجاد یک سشن NHibernate نیاز است تا نوع دیتابیس و همچنین رشته اتصالی به آن (کانکشن استرینگ) مشخص شوند. فریم ورک Fluent NHibernate با ایجاد کلاس‌های کمکی برای این امر، به شدت سبب ساده‌ سازی انجام آن شده است. در این مثال، نوع دیتابیس به SQLite و در حالت دیتابیس در حافظه (in memory)، تنظیم شده است (برای انجام امور آزمون واحد با سرعت بالا).
جهت اجرای هر دستوری در NHibernate نیاز به یک سشن می‌باشد. برای تعریف شیء سشن، نه تنها نیاز به مشخص سازی نوع و حالت دیتابیس مورد استفاده داریم، بلکه نیاز است تا وهله‌ای از کلاس استاندارد PersistanceModel را نیز جهت مشخص سازی کلاس نگاشت مورد استفاده مشخص نمائیم. برای این منظور کلاس TestModel فوق تعریف شده است تا این نگاشت را از اسمبلی مربوطه بخواند و مورد استفاده قرار دهد (بر پایی اولیه این مراحل شاید در ابتدای امر کمی زمانبر باشد اما در نهایت یک پروسه استاندارد است). توسط این کلاس به سیستم اعلام خواهیم کرد که اطلاعات نگاشت را باید از کدام کلاس دریافت کند.
تا اینجای کار شیء SessionSource را با معرفی نوع دیتابیس و همچنین محل دریافت اطلاعات نگاشت اشیاء معرفی کردیم. در دو سطر بعدی متد SetupContext کلاس FixtureBase ، ابتدا یک سشن را از این منبع سشن تهیه می‌کنیم. شیء منبع سشن در این فریم ورک در حقیقت یک factory object است (الگوهای طراحی برنامه نویسی شیءگرا) که امکان دسترسی به انواع و اقسام دیتابیس‌ها را فراهم می‌سازد. برای مثال اگر روزی نیاز بود از دیتابیس اس کیوال سرور استفاده شود، می‌توان از کلاس MsSqlConfiguration بجای SQLiteConfiguration استفاده کرد و همینطور الی آخر.
در ادامه توسط شیء SessionSource کار ساخت database schema را نیز به صورت پویا انجام خواهیم داد. بله، همانطور که متوجه شده‌اید، کار ساخت database schema نیز به صورت پویا توسط فریم ورک NHibernate با توجه به اطلاعات کلاس‌های نگاشت، صورت خواهد گرفت.

این مراحل، نحوه ایجاد و بر پایی یک آزمایشگاه آزمون واحد فریم ورک Fluent NHibernate را مشخص ساخته و در پروژه‌های شما می‌توانند به کرات مورد استفاده قرار گیرند.

در ادامه اگر آزمون واحد را اجرا نمائیم (متد can_correctly_map_customer در کلاس CustomerMapping_Fixture)، نتیجه باید شبیه به شکل زیر باشد:



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

نکته:
شاید سؤال بپرسید که در تابع can_correctly_map_customer عملا چه اتفاقاتی رخ داده است؟ برای بررسی آن در متد SetupContext کلاس FixtureBase ، اولین سطر آن‌را به صورت زیر تغییر دهید تا عبارات SQL نهایی تولید شده را نیز بتوانیم در حین عملیات تست مشاهده نمائیم:

var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.ShowSql().InMemory);




مطابق متد تست فوق، عبارات تولید شده به شرح زیر هستند:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 1001
NHibernate: SELECT customer0_.Id as Id0_0_, customer0_.FirstName as FirstName0_0_, customer0_.LastName as LastName0_0_, customer0_.AddressLine1 as AddressL4_0_0_, customer0_.AddressLine2 as AddressL5_0_0_, customer0_.PostalCode as PostalCode0_0_, customer0_.City as City0_0_, customer0_.CountryCode as CountryC8_0_0_ FROM "Customer" customer0_ WHERE customer0_.Id=@p0;@p0 = 1001

نکته جالب این عبارات، استفاده از کوئری‌های پارامتری است به صورت پیش فرض که در نهایت سبب بالا رفتن امنیت بیشتر برنامه (یکی از راه‌های جلوگیری از تزریق اس کیوال در ADO.Net که در نهایت توسط تمامی این فریم ورک‌ها در پشت صحنه مورد استفاده قرار خواهند گرفت) و همچنین سبب بکار افتادن سیستم‌های کش دیتابیس‌های پیشرفته مانند اس کیوال سرور می‌شوند (execution plan کوئری‌های پارامتری در اس کیوال سرور جهت بالا رفتن کارآیی سیستم کش می‌شوند و اهمیتی هم ندارد که حتما رویه ذخیره شده باشند یا خیر).

ادامه دارد ...

اشتراک‌ها
معرفی کتابخانه FastMember برای کار با Reflection

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

برای سرعت بیشتر و استفاده راحت‌تر از Reflection میتونین از کتابخانه FastMember استفاده کنین. (بین تست‌های که شخصا داشتم تو سناریو‌های مختلف، FastMember بین 6 تا 96 برابر سریعتر از Reflection معمولی سریع‌تر بود.)

مثلا برای دسترسی به property‌های یک object در runtime با کمک FastMember، خواهیم داشت:

var accessor = ObjectAccessor.Create(obj);
var propName = "MyProperty"
var value = accessor[propName];

نکته :

این کتابخانه کد‌ها IL برنامه رو در runtime تغییر میده، در نتیجه در محیط‌های AOT (مخفف Ahead of Time) مانند Xamarin iOS کار نخواهد کرد.


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

سپس به روش‌های افزایش پرفرمنس رفلکشن اشاره میکنه و در آخر بنچمارکی از نتیجه اونها رو نشون میده

Why Reflection Is Slow  

معرفی کتابخانه FastMember برای کار با Reflection
مطالب
بررسی واژه کلیدی static

تفاوت بین یک کلاس استاتیک، متدی استاتیک و یا متغیر عضو استاتیک چیست؟ چه زمانی باید از آن‌ها‌ استفاده کرد و لزوم بودن آن‌ها‌ چیست؟
برای پاسخ دادن به این سؤالات باید از نحوه‌ی تقسیم بندی حافظه شروع کرد.
RAM برای هر نوع پروسه‌ای که در آن بارگذاری می‌شود به سه قسمت تقسیم می‌گردد: Stack ، Heap و Static (استاتیک در دات نت در حقیقت قسمتی از Heap است که به آن High Frequency Heap نیز گفته می‌شود).
این قسمت استاتیک حافظه، محل نگهداری متدها و متغیرهای استاتیک است. آن متدها و یا متغیرهایی که نیاز به وهله‌ای از کلاس برای ایجاد ندارند، به صورت استاتیک ایجاد می‌گردند. در سی شارپ از واژه کلیدی static برای معرفی آن‌ها کمک گرفته می‌شود. برای مثال:

class MyClass
{
public static int a;
public static void DoSomething();
}
در این مثال برای فراخوانی متد DoSomething نیازی به ایجاد یک وهله جدید از کلاس MyClass نمی‌باشد و تنها کافی است بنویسیم:

MyClass.DoSomething(); // and not -> new MyClass().DoSomething();
نکته‌ی مهمی که در اینجا وجود دارد این است که متدهای استاتیک تنها قادر به استفاده از متغیرهای استاتیک تعریف شده در سطح کلاس هستند. علت چیست؟
به مثال زیر دقت نمائید:

class MyClass
{
// non-static instance member variable
private int a;
//static member variable
private static int b;
//static method
public static void DoSomething()
{
//this will result in compilation error as “a” has no memory
a = a + 1;
//this works fine since “b” is static
b = b + 1;
}
}
در این مثال اگر متد DoSomething را فراخوانی کنیم، تنها متغیر b تعریف شده، در حافظه حضور داشته (به دلیل استاتیک معرفی شدن) و چون با روش فراخوانی MyClass.DoSomething هنوز وهله‌ای از کلاس مذکور ایجاد نشده، به متغیر a نیز حافظه‌ای اختصاص داده نشده است و نامعین می‌باشد.
بر این اساس کامپایلر نیز از کامپایل شدن این کد جلوگیری کرده و خطای لازم را گوشزد خواهد کرد.

اکنون تعریف یک کلاس به صورت استاتیک چه اثری را خواهد داشت؟
با تعریف یک کلاس به صورت استاتیک مشخص خواهیم کرد که این کلاس تنها حاوی متدها و متغیرهای استاتیک می‌باشد. امکان ایجاد یک وهله از آن‌ها وجود نداشته و نیازی نیز به این امر ندارند. این کلاس‌ها امکان داشتن instance variables را نداشته و به صورت پیش فرض از نوع sealed به حساب خواهند آمد و امکان ارث بری از آن‌ها نیز وجود ندارد. علت این امر هم این است که یک کلاس static هیچ نوع رفتاری را تعریف نمی‌کند.

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

با کلاس‌های استاتیک نیز همانند سایر کلاس‌های یک برنامه توسط JIT compiler رفتار می‌شود، اما با یک تفاوت. کلاس‌های استاتیک فقط یکبار هنگام اولین دسترسی به آن‌ها ساخته شده و در قسمت High Frequency Heap حافظه قرار می‌گیرند. این قسمت از حافظه تا پایان کار برنامه از دست garbage collector‌ در امان است (بر خلاف garbage-collected heap‌ یا object heap که جهت instance classes مورد استفاده قرار می‌گیرد)


نکته:
در برنامه‌های ASP.Net از بکارگیری متغیرهای عمومی استاتیک برحذر باشید (از static fields و نه static methods). این متغیرها بین تمامی کاربران همزمان یک برنامه به اشتراک گذاشته شده و همچنین باید مباحث قفل‌گذاری و امثال آن‌را در محیط‌های چند ریسمانی هنگام کار با آن‌ها رعایت کرد (thread safe نیستند).

نظرات مطالب
T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
نمونه‌های شبیه این باز هم بودند که مسئله‌ای که سالهاست دات نت با Reflection قابل حل کرده. ولی بازم تو جدیدترین تکنولوژی‌ها از مقادیر رشته‌ای استفاده میکنه. آیا هدف خاصی پشت این منطق هست؟ یا صرفا بی‌توجهی و قصور تیم توسعه هست؟
مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور
یکی از نیازهای مهم هر برنامه‌ای، امکانات گزارشگیری و نمایش لیستی از اطلاعات است. به همین جهت در طی چند قسمت، قصد داریم یک گرید ساده را به همراه امکانات نمایش، صفحه بندی و مرتب سازی اطلاعات، تنها به کمک امکانات توکار Angular و ASP.NET Core تهیه کنیم.




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

مدلی که در تصویر فوق نمایش داده شده‌است، در سمت سرور چنین ساختاری را دارد:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Product
    {
        public int ProductId { set; get; }
        public string ProductName { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}

همچنین یک منبع ساده درون حافظه‌ای را نیز جهت بازگشت 1500 محصول تهیه کرده‌ایم. علت اینجا است که ساختار نهایی اطلاعات آن شبیه به ساختار اطلاعات حاصل از ORMها باشد و همچنین به سادگی قابلیت اجرا و بررسی را داشته باشد:
using System.Collections.Generic;

namespace AngularTemplateDrivenFormsLab.Models
{
    public static class ProductDataSource
    {
        private static readonly IList<Product> _cachedItems;
        static ProductDataSource()
        {
            _cachedItems = createProductsDataSource();
        }

        public static IList<Product> LatestProducts
        {
            get { return _cachedItems; }
        }

        private static IList<Product> createProductsDataSource()
        {
            var list = new List<Product>();
            for (var i = 0; i < 1500; i++)
            {
                list.Add(new Product
                {
                    ProductId = i + 1,
                    ProductName = "نام " + (i + 1),
                    IsAvailable = (i % 2 == 0),
                    Price = 1000 + i
                });
            }
            return list;
        }
    }
}


مشخص کردن قرارداد اطلاعات دریافتی از سمت کلاینت

زمانیکه کلاینت Angular برنامه، اطلاعاتی را به سمت سرور ارسال می‌کند، یک چنین ساختاری را دریافت خواهیم کرد:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
درخواست، به یک اکشن متد مشخص ارسال شده‌است و حاوی یک سری کوئری استرینگ مشخص کننده‌ی نام خاصیت یا فیلدی که قرار است مرتب سازی بر اساس آن صورت گیرد، صعودی و نزولی بودن این مرتب سازی، شماره صفحه‌ی درخواستی و تعداد آیتم‌های در هر صفحه، می‌باشد.
بنابراین اینترفیسی را دقیقا بر اساس نام کلیدهای همین کوئری استرینگ‌ها تهیه می‌کنیم:
    public interface IPagedQueryModel
    {
        string SortBy { get; set; }
        bool IsAscending { get; set; }
        int Page { get; set; }
        int PageSize { get; set; }
    }


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

با تعریف این اینترفیس چند هدف را دنبال خواهیم کرد:
الف) استاندارد سازی نام خواصی که مدنظر هستند و اعمال یک دست آن‌ها به ViewModelهایی که قرار است از سمت کلاینت دریافت شوند:
    public class ProductQueryViewModel : IPagedQueryModel
    {
        // ... other properties ...

        public string SortBy { get; set; }
        public bool IsAscending { get; set; }
        public int Page { get; set; }
        public int PageSize { get; set; }
    }
برای مثال در اینجا یک ViewModel مخصوص Product را ایجاد کرده‌ایم که می‌تواند شامل یک سری فیلد دیگر نیز باشد. اما یک سری خواص مرتب سازی و صفحه بندی آن، یک دست و مشخص هستند.

ب) امکان استفاده‌ی از این قرارداد در متدهای کمکی که نوشته خواهند شد:
    public static class IQueryableExtensions
    {
        public static IQueryable<T> ApplyPaging<T>(
          this IQueryable<T> query, IPagedQueryModel model)
        {
            if (model.Page <= 0)
            {
                model.Page = 1;
            }

            if (model.PageSize <= 0)
            {
                model.PageSize = 10;
            }

            return query.Skip((model.Page - 1) * model.PageSize).Take(model.PageSize);
        }
    }
در حین ارائه‌ی اطلاعات نهایی صفحه بندی شده به کلاینت، همیشه یک قسمت Skip و Take وجود خواهند داشت. این متدها نیز باید بر اساس یک سری خاصیت و مقدار مشخص، مانند صفحه شماره صفحه‌ی جاری و تعداد ردیف‌های در هر صفحه کار کنند. اکنون که قرارداد IPagedQueryModel را تهیه کرده‌ایم و ViewModel ما نیز آن‌را پیاده سازی می‌کند، مطمئن خواهیم بود که می‌توان به سادگی به این خواص دسترسی یافت و همچنین این کد تکراری صفحه بندی را توانسته‌ایم به یک متد الحاقی کمکی منتقل و حجم کدهای نهایی را کاهش دهیم.
همچنین دراینجا بجای صدور استثناء در حین دریافت مقادیر غیرمعتبر شماره صفحه یا تعداد ردیف‌های هر صفحه، از حالت «بخشنده» بجای حالت «تدافعی» استفاده شده‌است. برای مثال در حالت «بخشنده» اگر شماره صفحه منفی بود، همان صفحه‌ی اول اطلاعات نمایش داده می‌شود؛ بجای صدور یک استثناء (یا حالت «تدافعی و defensive programming»).


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

همانطور که عنوان شد، از سمت کلاینت، چنین لینکی را دریافت خواهیم کرد:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
در اینجا، هربار sortBy و isAscending می‌توانند متفاوت باشند و در نهایت به یک چنین کدهایی خواهیم رسید:
if(model.SortBy == "f1")
{
   query = !model.IsAscending ? query.OrderByDescending(x => x.F1) : query.OrderBy(x => x.F1);
}
امکان نوشتن این نوع کوئری‌ها توسط قابلیت تعریف زنجیره‌وار کوئری‌های LINQ میسر است و در نهایت زمانیکه ToList نهایی فراخوانی می‌شود، آنگاه است که کوئری SQL معادل این‌ها تولید خواهد شد.
اما در این حالت نیاز است به ازای تک تک فیلدها، یکبار if/else یافتن فیلد و سپس بررسی صعودی و نزولی بودن آن‌ها صورت گیرد که در نهایت ظاهر خوشایندی را نخواهند داشت.

یک نمونه از مزیت‌های تهیه‌ی قرارداد IPagedQueryModel را در حین نوشتن متد ApplyPaging مشاهده کردید. نمونه‌ی دیگر آن کاهش کدهای تکراری مرتب سازی اطلاعات است:
namespace AngularTemplateDrivenFormsLab.Utils
{
    public static class IQueryableExtensions
    {
        public static IQueryable<T> ApplyOrdering<T>(
          this IQueryable<T> query,
          IPagedQueryModel model,
          IDictionary<string, Expression<Func<T, object>>> columnsMap)
        {
            if (string.IsNullOrWhiteSpace(model.SortBy) || !columnsMap.ContainsKey(model.SortBy))
            {
                return query;
            }

            if (model.IsAscending)
            {
                return query.OrderBy(columnsMap[model.SortBy]);
            }
            else
            {
                return query.OrderByDescending(columnsMap[model.SortBy]);
            }
        }
    }
}
در اینجا متد الحاقی ApplyOrdering، کار دریافت و بررسی خواص مدنظر را از طریق یک دیکشنری انجام می‌دهد و مابقی کدهای تکراری نوشته شده، حذف خواهند شد. برای نمونه، مثالی از نحوه‌ی استفاده‌ی از این متد الحاقی را در ذیل مشاهده می‌کنید:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
query = query.ApplyOrdering(queryModel, columnsMap);
ابتدا نگاشتی بین خواص رشته‌ای دریافتی از سمت کلاینت، با خواص شیء Product برقرار شده‌است. سپس این نگاشت به متد ApplyOrdering ارسال شده‌است. به این ترتیب از نوشتن تعداد زیادی if/else یا switch بر اساس خاصیت SortBy بی‌نیاز شده‌ایم، حجم کدهای نهایی تولیدی کاهش پیدا می‌کنند و برنامه نیز خواناتر می‌شود.


تهیه قرارداد ساختار اطلاعات بازگشتی از سمت سرور به سمت کلاینت

تا اینجا قرارداد اطلاعات دریافتی از سمت کلاینت را مشخص کردیم. همچنین از آن برای ساده سازی عملیات مرتب سازی و صفحه بندی اطلاعات کمک گرفتیم. در ادامه نیاز است مشخص کنیم چگونه می‌خواهیم این اطلاعات را به سمت کلاینت ارسال کنیم:
using System.Collections.Generic;

namespace AngularTemplateDrivenFormsLab.Models
{
    public class PagedQueryResult<T>
    {
        public int TotalItems { get; set; }
        public IEnumerable<T> Items { get; set; }
    }
}
عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتم‌های تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیف‌های صفحه‌ی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی می‌تواند باشد، بهتر است آن‌را جنریک تعریف کنیم.


پایان کار بازگشت اطلاعات سمت سرور با تهیه اکشن متد GetPagedProducts

در اینجا اکشن متدی را مشاهده می‌کنید که اطلاعات نهایی مرتب سازی شده و صفحه بندی شده را بازگشت می‌دهد:
    [Route("api/[controller]")]
    public class ProductController : Controller
    {
        [HttpGet("[action]")]
        public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
        {
            var pagedResult = new PagedQueryResult<Product>();

            var query = ProductDataSource.LatestProducts
                                         .AsQueryable();

            //TODO: Apply Filtering ... .where(p => p....) ...

            var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
            query = query.ApplyOrdering(queryModel, columnsMap);

            pagedResult.TotalItems = query.Count();
            query = query.ApplyPaging(queryModel);
            pagedResult.Items = query.ToList();
            return pagedResult;
        }
    }
توضیحات تکمیلی

امضای این اکشن متد، شامل دو مورد مهم است:
 public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
الف) ViewModel ایی که پیاده سازی کننده‌ی IPagedQueryModel است. به این ترتیب می‌توان به ساختار استانداردی از مقادیر مورد نیاز برای صفحه بندی و مرتب سازی رسید و همچنین این ViewModel می‌تواند حاوی خواص اضافی ویژه‌ی خود نیز باشد.
ب) خروجی آن از نوع PagedQueryResult است که در مورد آن توضیح داده شد. بنابراین باید به همراه تعداد کل رکوردهای جدول محصولات و همچنین تنها آیتم‌های صفحه‌ی جاری درخواستی باشد.

در ابتدای کار، دسترسی به منبع داده‌ی درون حافظه‌ای ابتدای برنامه را مشاهده می‌کنید. برای اینکه کارکرد آن‌را شبیه به کوئری‌های ORMها کنیم، یک AsQueryable نیز به انتهای آن اضافه شده‌است.
 var query = ProductDataSource.LatestProducts
  .AsQueryable();

//TODO: Apply Filtering ... .where(p => p....) ...
اینجا دقیقا جائی است که در صورت نیاز می‌توان کار فیلتر اطلاعات و اعمال متد where را انجام داد.

پس از مشخص شدن منبع داده و فیلتر آن در صورت نیاز، اکنون نوبت به مرتب سازی اطلاعات است:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
query = query.ApplyOrdering(queryModel, columnsMap);
توضیحات این مورد را پیشتر مطالعه کردید و هدف از آن، تهیه یک نگاشت ساده‌ی بین خواص رشته‌ای رسیده‌ی از سمت کلاینت به خواص مدل متناظر با آن است و سپس ارسال آن‌ها به همراه queryModel دریافتی از کاربر، برای اعمال مرتب سازی نهایی.

در آخر مطابق ساختار PagedQueryResult بازگشتی، ابتدا تعداد کل آیتم‌های منبع داده محاسبه شده‌است و سپس صفحه بندی به آن اعمال گردیده‌است. این ترتیب نیز مهم است و گرنه TotalItems دقیقا به همان تعداد ردیف‌های صفحه‌ی جاری محاسبه می‌شود:
var pagedResult = new PagedQueryResult<Product>();
pagedResult.TotalItems = query.Count();
query = query.ApplyPaging(queryModel);
pagedResult.Items = query.ToList();
return pagedResult;


در قسمت بعد، نحوه‌ی نمایش این اطلاعات را در سمت Angular بررسی خواهیم کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
استفاده ازExpressionها جهت ایجاد Strongly typed view در ASP.NET MVC
مدل زیر را در نظر بگیرید:
/// <summary>
    /// 
    /// </summary>
    public class CompanyModel
    {
        /// <summary>
        /// Table Identity
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// Company Name
        /// </summary>
        [DisplayName("نام شرکت")]
        public string CompanyName { get; set; }

        /// <summary>
        /// Company Abbreviation
        /// </summary>
        [DisplayName("نام اختصاری شرکت")]
        public string CompanyAbbr { get; set; }
    
    }
از View زیر جهت نمایش لیستی از شرکت‌ها متناظر با مدل جاری استفاده میشود:
@{
    const string viewTitle = "شرکت ها";
    ViewBag.Title = viewTitle;
    const string gridName = "companies-grid";
}
<div class="col-md-12">
    <div class="form-panel">
        <header>
            <div class="title">
                <i class="fa fa-book"></i>
                @viewTitle
            </div>
        </header>
        <div class="panel-body">
            <div id="@gridName">
            </div>
        </div>
    </div>
</div>
</div>
@section scripts
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#@gridName").kendoGrid({
                dataSource: {
                    type: "json",
                    transport: {
                        read: {
                            url: "@Html.Raw(Url.Action(MVC.Company.CompanyList()))",
                            type: "POST",
                            dataType: "json",
                            contentType: "application/json"
                        }
                    },
                    schema: {
                        data: "Data",
                        total: "Total",
                        errors: "Errors"
                        }
                    },
                    pageSize: 10,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true
                },
                pageable: {
                    refresh: true
                },
                sortable: {
                    mode: "multiple",
                    allowUnsort: true
                },
                editable: false,
                filterable: false,
                scrollable: false,
                columns: [ {
                    field: "CompanyName",
                    title: "نام شرکت",
                    sortable: true,
                }, {
                    field: "CompanyAbbr",
                    title: "مخفف نام شرکت",
                    sortable: true
                }]
            });
        });
    </script>
}


مشکلی که در کد بالا وجود دارد این است که با تغییر نام هر یک از متغییر هایمان ، اطلاعات گرید در ستون مربوطه نمایش داده نمیشود.همچنین عناوین ستونها نیز از DisplayName مدل پیروی نمیکنند.توسط متدهای الحاقی زیر این مشکل برطرف شده است.

 /// <summary>
    /// 
    /// </summary>
    public static class PropertyExtensions
    {
        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static MemberInfo GetMember<T>(this Expression<Func<T, object>> expression)
        {
            var mbody = expression.Body as MemberExpression;

            if (mbody != null) return mbody.Member;
            //This will handle Nullable<T> properties.
            var ubody = expression.Body as UnaryExpression;
            if (ubody != null)
            {
                mbody = ubody.Operand as MemberExpression;
            }
            if (mbody == null)
            {
                throw new ArgumentException("Expression is not a MemberExpression", "expression");
            }
            return mbody.Member;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string PropertyName<T>(this Expression<Func<T, object>> expression)
        {
            return GetMember(expression).Name;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string PropertyDisplay<T>(this Expression<Func<T, object>> expression)
        {
            var propertyMember = GetMember(expression);
            var displayAttributes = propertyMember.GetCustomAttributes(typeof(DisplayNameAttribute), true);
            return displayAttributes.Length == 1 ? ((DisplayNameAttribute)displayAttributes[0]).DisplayName : propertyMember.Name;
        }
    }



public static string PropertyName<T>(this Expression<Func<T, object>> expression)
جهت بدست آوردن نام متغییر هایمان استفاده مینماییم.


public static string PropertyDisplay<T>(this Expression<Func<T, object>> expression)
جهت بدست آوردن DisplayNameAttribute استفده میشود. درصورتیکه این DisplayNameAttribute یافت نشود نام متغییر بازگشت داده میشود.

بنابراین View مربوطه را اینگونه بازنویسی میکنیم:

@using Models
@{
    const string viewTitle = "شرکت ها";
    ViewBag.Title = viewTitle;
    const string gridName = "companies-grid";
}
<div class="col-md-12">
    <div class="form-panel">
        <header>
            <div class="title">
                <i class="fa fa-book"></i>
                @viewTitle
            </div>
        </header>
        <div class="panel-body">
            <div id="@gridName">
            </div>
        </div>
    </div>
</div>
</div>
@section scripts
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#@gridName").kendoGrid({
                dataSource: {
                    type: "json",
                    transport: {
                        read: {
                            url: "@Html.Raw(Url.Action(MVC.Company.CompanyList()))",
                            type: "POST",
                            dataType: "json",
                            contentType: "application/json"
                        }
                    },
                    schema: {
                        data: "Data",
                        total: "Total",
                        errors: "Errors"
                        }
                    },
                    pageSize: 10,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true
                },
                pageable: {
                    refresh: true
                },
                sortable: {
                    mode: "multiple",
                    allowUnsort: true
                },
                editable: false,
                filterable: false,
                scrollable: false,
                columns: [ {
                    field: "@(PropertyExtensions.PropertyName<CompanyModel>(a => a.CompanyName))",
                    title: "@(PropertyExtensions.PropertyDisplay<CompanyModel>(a => a.CompanyName))",
                    sortable: true,
                }, {
                    field: "@(PropertyExtensions.PropertyName<CompanyModel>(a => a.CompanyAbbr))",
                    title: "@(PropertyExtensions.PropertyDisplay<CompanyModel>(a => a.CompanyAbbr))",
                    sortable: true
                }]
            });
        });
    </script>
}


مطالب
نمایش ساختارهای درختی در Blazor
یکی از نکات جالب رندر کامپوننت‌ها در Blazor، امکان فراخوانی بازگشتی آن‌ها است؛ یعنی یک کامپوننت می‌تواند خودش را نیز فراخوانی کند. از همین قابلیت می‌توان جهت نمایش ساختارهای درختی، مانند مدل‌های خود ارجاع دهنده‌ی EF استفاده کرد.


مدل برنامه، جهت تامین داده‌های خود ارجاع دهنده و درختی

فرض کنید قصد داریم لیستی از کامنت‌های تو در تو را مدل سازی کنیم که در آن هر کامنت، می‌تواند چندین کامنت تا بی‌نهایت سطح تو در تو را داشته باشد:
namespace BlazorTreeView.ViewModels;

public class Comment
{
    public IList<Comment> Comments = new List<Comment>();
    public string? Text { set; get; }
}
برای نمونه بر اساس این مدل، منبع داده‌ی فرضی زیر را تهیه می‌کنیم:
using BlazorTreeView.ViewModels;

namespace BlazorTreeView.Pages;

public partial class TreeView
{
    private IReadOnlyDictionary<string, object> ChildrenHtmlAttributes { get; } =
        new Dictionary<string, object>(StringComparer.Ordinal)
        {
            { "style", "list-style: none;" },
        };

    private IList<Comment> Comments { get; } =
        new List<Comment>
        {
            new()
            {
                Text = "پاسخ یک",
            },
            new()
            {
                Text = "پاسخ دو",
                Comments =
                    new List<Comment>
                    {
                        new()
                        {
                            Text = "پاسخ اول به پاسخ دو",
                            Comments =
                                new List<Comment>
                                {
                                    new()
                                    {
                                        Text = "پاسخی به پاسخ اول پاسخ دو",
                                    },
                                },
                        },
                        new()
                        {
                            Text = "پاسخ دوم به پاسخ دو",
                        },
                    },
            },
            new()
            {
                Text = "پاسخ سوم",
            },
        };
}
این قطعه کد partial class که مربوط به فایل TreeView.razor.cs برنامه‌است، در حقیقت کدهای پشت صحنه‌ی کامپوننت مثال TreeView.razor است که در ادامه آن‌را توسعه خواهیم داد. در نهایت قرار است بتوانیم آن‌را به صورت زیر رندر کنیم:



طراحی کامپوننت DntTreeView

برای اینکه بتوانیم به یک کامپوننت با قابلیت استفاده‌ی مجدد بررسیم، کدهای نمایش اطلاعات تو در تو و درختی را توسط کامپوننت سفارشی DntTreeView پیاده سازی خواهیم کرد. پیشنیازهای آن نیز به صورت زیر است:
- این کامپوننت باید جنریک باشد؛ یعنی باید به صورت زیر شروع شود:
/// <summary>
///   A custom DntTreeView
/// </summary>
public partial class DntTreeView<TRecord>
{
چون باید بتوان یک لیست جنریک <IEnumerable<TRecord را به آن، جهت رندر ارسال کرد و قرار نیست این کامپوننت، تنها به شیء سفارشی Comment مثال جاری ما وابسته باشد. بنابراین اولین خاصیت آن، شیء جنریک Items است که لیست کامنت‌ها/عناصر را دریافت می‌کند:
    /// <summary>
    ///     The treeview's self-referencing items
    /// </summary>
    [Parameter]
    public IEnumerable<TRecord>? Items { set; get; }
- هنگام رندر هر آیتم کامنت باید بتوان یک قالب سفارشی را از کاربر دریافت کرد. نمی‌خواهیم صرفا برای مثال Text شیء Comment فوق را به صورت متنی و ساده نمایش دهیم. می‌خواهیم در حین رندر، کل شیء TRecord جاری را به مصرف کننده ارسال و یک قالب سفارشی را از آن دریافت کنیم. یعنی باید یک RenderFragment جنریک را به صورت زیر نیز داشته باشیم تا مصرف کننده بتواند TRecord در حال رندر را دریافت و قالب Htmlای خودش را بازگشت دهد:
    /// <summary>
    ///     The treeview item's template
    /// </summary>
    [Parameter]
    public RenderFragment<TRecord>? ItemTemplate { set; get; }
- همچنین همیشه باید به فکر عدم وجود اطلاعاتی برای نمایش نیز بود. به همین جهت بهتر است قالب دیگری را نیز از مصرف کننده برای اینکار درخواست کنیم و نحوه‌ی رندر سفارشی این قسمت را نیز به مصرف کننده واگذار کنیم:
    /// <summary>
    ///     The content displayed if the list is empty
    /// </summary>
    [Parameter]
    public RenderFragment? EmptyContentTemplate { set; get; }
- زمانیکه با شیء از پیش تعریف شده‌ی Comment این مثال کار می‌کنیم، کاملا مشخص است که خاصیت Comments آن تو در تو است:
public class Comment
{
    public IList<Comment> Comments = new List<Comment>();
    public string? Text { set; get; }
}
اما زمانیکه با یک کامپوننت جنریک کار می‌کنیم، نیاز است از مصرف کننده، نام این خاصیت تو در تو را به نحو واضحی دریافت کنیم؛ به صورت زیر:
    /// <summary>
    ///     The property which returns the children items
    /// </summary>
    [Parameter]
    public Expression<Func<TRecord, IEnumerable<TRecord>>>? ChildrenSelector { set; get; }
دلیل استفاده از Expression Funcها را در مطلب «static reflection» می‌توانید مطالعه کنید. زمانیکه قرار است از کامپوننت DntTreeView استفاده کنیم، ابتدا نوع جنریک آن‌را مشخص می‌کنیم، سپس لیست اشیاء ارسالی به آن‌را و در ادامه با استفاده از ChildrenSelector به صورت زیر، مشخص می‌کنیم که خاصیت Comments است که به همراه Children می‌باشد و تو در تو است:
        <DntTreeView
            TRecord="Comment"
            Items="Comments"
            ChildrenSelector="m => m.Comments"
و مرسوم است جهت بالابردن کارآیی Expression Funcها، آن‌ها را کامپایل و کش کنیم که نمونه‌ای از روش آن‌را به صورت زیر مشاهده می‌کنید:
public partial class DntTreeView<TRecord>
{
    private Expression? _lastCompiledExpression;
    internal Func<TRecord, IEnumerable<TRecord>>? CompiledChildrenSelector { private set; get; }

    // ...

    protected override void OnParametersSet()
    {
        if (_lastCompiledExpression != ChildrenSelector)
        {
            CompiledChildrenSelector = ChildrenSelector?.Compile();
            _lastCompiledExpression = ChildrenSelector;
        }
    }
}
تا اینجا ساختار کدهای پشت صحنه‌ی DntTreeView.razor.cs مشخص شد. اکنون UI این کامپوننت را به صورت زیر تکمیل می‌کنیم:
@namespace BlazorTreeView.Pages.Components
@typeparam TRecord

@if (Items is null || !Items.Any())
{
    @EmptyContentTemplate
}
else
{
    <CascadingValue Value="this">
        <ul @attributes="AdditionalAttributes">
            @foreach (var item in Items)
            {
                <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/>
            }
        </ul>
    </CascadingValue>
}
در ابتدای کار، اگر آیتمی برای نمایش وجود نداشته باشد، EmptyContentTemplate دریافتی از استفاده کننده را رندر می‌کنیم. در غیراینصورت، حلقه‌ای را بر روی لیست Items ایجاد کرده و آن‌‌ها را یکی نمایش می‌دهیم. این نمایش، نکات زیر را به همراه دارد:
- نمایش توسط کامپوننت دومی به نام DntTreeViewChildrenItem انجام می‌شود که آن‌‌هم جنریک است و شیء item جاری را توسط خاصیت ParentItem دریافت می‌کند.
- در اینجا یک CascadingValue اشاره کننده به شیء this را هم مشاهده می‌کنید. این روش، یکی از روش‌های اجازه دادن دسترسی به خواص و امکانات یک کامپوننت والد، در کامپوننت‌های فرزند است که در ادامه از آن استفاده خواهیم کرد.


تکمیل کامپوننت بازگشتی DntTreeViewChildrenItem.razor

اگر به حلقه‌ی foreach (var item in Items) در کامپوننت DntTreeView.razor دقت کنید، یک سطح را بیشتر پوشش نمی‌دهد؛ اما کامنت‌های ما چندسطحی و تو در تو هستند و عمق آن‌ها هم مشخص نیست. به همین جهت نیاز است به نحوی بتوان یک طراحی recursive و بازگشتی را در کامپوننت‌های Blazor داشت که خوشبختانه این مورد پیش‌بینی شده‌است و هر کامپوننت Blazor، می‌تواند خودش را نیز فراخوانی کند:
@namespace BlazorTreeView.Pages.Components
@typeparam TRecord

<li @attributes="@SafeOwnerTreeView.ChildrenHtmlAttributes" @key="ParentItem?.GetHashCode()">
    @if (SafeOwnerTreeView.ItemTemplate is not null && ParentItem is not null)
    {
        @SafeOwnerTreeView.ItemTemplate(ParentItem)
    }
    @if (Children is not null)
    {
        <ul>
            @foreach (var item in Children)
            {
                <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/>
            }
        </ul>
    }
</li>
این‌ها کدهای DntTreeViewChildrenItem.razor هستند که در آن، ابتدا ItemTemplate دریافتی از والد یا همان DntTreeView.razor رندر می‌شود. سپس به کمک CompiledChildrenSelector ای که عنوان شد، یک شیء Children را تشکیل داده و آن‌را به خودش (فراخوانی مجدد DntTreeViewChildrenItem در اینجا)، ارسال می‌کند. این فراخوانی بازگشتی، سبب رندر تمام سطوح تو در توی شیء جاری می‌شود.

کدهای پشت صحنه‌ی این کامپوننت یعنی فایل DntTreeViewChildrenItem.razor.cs به صورت زیر است:
/// <summary>
///     A custom DntTreeView
/// </summary>
public partial class DntTreeViewChildrenItem<TRecord>
{
    /// <summary>
    ///     Defines the owner of this component.
    /// </summary>
    [CascadingParameter]
    public DntTreeView<TRecord>? OwnerTreeView { get; set; }

    private DntTreeView<TRecord> SafeOwnerTreeView =>
        OwnerTreeView ??
        throw new InvalidOperationException("`DntTreeViewChildrenItem` should be placed inside of a `DntTreeView`.");

    /// <summary>
    ///     Nested parent item to display
    /// </summary>
    [Parameter]
    public TRecord? ParentItem { set; get; }

    private IEnumerable<TRecord>? Children =>
        ParentItem is null || SafeOwnerTreeView.CompiledChildrenSelector is null
            ? null
            : SafeOwnerTreeView.CompiledChildrenSelector(ParentItem);
}
با استفاده از یک پارامتر از نوع CascadingParameter، می‌توان به اطلاعات شیء CascadingValue ای که در کامپوننت والد DntTreeView.razor قرا دادیم، دسترسی پیدا کنیم. سپس یکبار هم بررسی می‌کنیم که آیا نال هست یا خیر. یعنی قرار نیست که این کامپوننت فرزند، درجائی به صورت مستقیم استفاده شود. فقط قرار است داخل کامپوننت والد فراخوانی شود. به همین جهت اگر این CascadingParameter نال بود، یعنی این کامپوننت فرزند، به اشتباه فراخوانی شده و با صدور استثنائی این مساله را گوشزد می‌کنیم. اکنون که به SafeOwnerTreeView یا همان نمونه‌ای از شیء والد دسترسی پیدا کردیم، می‌توانیم پارامتر CompiledChildrenSelector آن‌را نیز فراخوانی کرده و توسط آن، به شیء تو در توی جدیدی در صورت وجود، جهت رندر بازگشتی آن رسید.
یعنی این کامپوننت ابتدا ParentItem، یا اولین سطح ممکن و در دسترس را رندر می‌کند. سپس با استفاده از Expression Func مهیای در کامپوننت والد، شیء فرزند را در صورت وجود یافته و سپس به صورت بازگشتی آن‌را با فراخوانی مجدد خودش ، رندر می‌کند.


روش استفاده از کامپوننت DntTreeView

اکنون که کار توسعه‌ی کامپوننت جنریک DntTreeView پایان یافت، روش استفاده‌ی از آن به صورت زیر است:
<div class="card" dir="rtl">
    <div class="card-header">
        DntTreeView
    </div>
    <div class="card-body">
        <DntTreeView
            TRecord="Comment"
            Items="Comments"
            ChildrenSelector="m => m.Comments"
            style="list-style: none;"
            ChildrenHtmlAttributes="ChildrenHtmlAttributes">
            <ItemTemplate Context="record">
                <div class="card mb-1">
                    <div class="card-body">
                        <span>@record.Text</span>
                    </div>
                </div>
            </ItemTemplate>
            <EmptyContentTemplate>
                <div class="alert alert-warning">
                    There is no item to display!
                </div>
            </EmptyContentTemplate>
        </DntTreeView>
    </div>
</div>
همانطور که مشاهده می‌کنید، چون کامپوننت جنریک است، باید نوع TRecord را که در مثال ما، شیء Comment است، مشخص کرد. سپس لیست نظرات، خاصیت تو در تو، قالب سفارشی نمایش Text نظرات (با توجه به Context دریافتی که امکان دسترسی به شیء جاری در حال رندر را میسر می‌کند) و همچنین قالب سفارشی نبود اطلاعاتی برای نمایش را تعریف می‌کنیم.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorTreeView.zip
کامپوننت توسعه یافته‌ی در اینجا در هر دو حالت Blazor WASM و Blazor Server کار می‌کند.
مطالب
ساخت Attribute های دلخواه یا خصوصی سازی شده
در قسمت‌های مختلفی از منابع آموزشی این سایت از متادیتاها attributes استفاده شده و در برخی آموزش هایی چون EF و MVC حداقل یک قسمت کامل را به خود اختصاص داده‌اند. متادیتاها کلاس‌هایی هستند که به روشی سریع و کوتاه در بالای یک Type معرفی شده و ویژگی‌هایی را به آن اضافه می‌کنند. به عنوان مثال متادیتای زیر را ببینید. این متادیتا در بالای یک متد در یک کلاس تعریف شده است و این متد را منسوخ شده اعلام می‌کند و به برنامه نویس می‌گوید که در نسخه‌ی جاری کتابخانه، این متد که احتمال میرود در نسخه‌های پیشین کاربرد داشته است، الان کارآیی خوبی برای استفاده نداشته و بهتر است طبق مستندات آن کلاس، از یک متد جایگزین که برای آن فراهم شده است استفاده کند.
 public static class  MyAttributes
    {
        [Obsolete]
        public static void MyMethod1()
        {

        }

        public static void MyMetho2()
        {

        }
    }
همانطور که ملاحظه می‌کنید می‌توانید اخطار آن را مشاهده کنید:

البته توصیه می‌کنم از ابزارهایی چون Resharper در کارهایتان استفاده کنید، تا طعم کدنویسی را بهتر بچشید. نحوه‌ی نمایش آن در Resharper به مراتب واضح‌تر و گویاتر است:



 حال در این بین این سؤال پیش می‌آید که چگونه ما هم می‌توانیم متادیتاهایی را با سلیقه‌ی خود ایجاد کنیم.
برای تهیه‌ی یک متادیتا از کلاس system.attribute استفاده می‌کنیم:
public  class  MyMaxLength:Attribute
    {
   
    }
در چنین حالتی شما یک متادیتا ساخته‌اید که می‌توان از آن به شکل زیر استفاده کرد:
[MyMaxLength]
    public class GetCustomProperties
    {
//...
     }

ولی اگر بخواهید توسط این متادیتا اطلاعاتی را دریافت کنید، می‌توانید به روش زیر عمل کنید. در اینجا من دوست دارم یک متادیتا به اسم MyMaxLength را ایجاد کرده تا جایگزین MaxLength دات نت کنم، تا طبق میل من رفتار کند.
    public  class  MyMaxLength:Attribute
    {
        private int max;
        public string ErrorText = "";

        public MyMaxLength(int max)
        {
            this.max = max;
            ErrorText = string.Format("max Length is {0} chars", max);
        }
    }

در کد بالا، یک متادیتا با یک پارامتر اجباری در سازنده تعریف شده است. این کلاس هم می‌تواند مثل سایر کلاس‌ها سازنده‌های مختلفی داشته باشد تا چندین شکل تعریف متادیتا داشته باشیم. متغیر ErrorText به عنوان یک پارامتر معرفی نشده، ولی از آن جا که public تعریف شده است می‌تواند مورد استفاده‌ی مستقیم قرار بگیرد و استفاده‌ی از آن نیز اختیاری است. نحوه‌ی معرفی این متادیتا نیز به صورت زیر است:
 [MyMaxLength(30)]
    public class GetCustomProperties
    {
//...
     }

//or 
 [MyMaxLength(30,ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")]
    public class GetCustomProperties
    {
//...
     }
در حالت اول از آنجا که متغیر ErrorText اختیاری است، تعریف نشده‌است. پس در نتیجه با مقدار Max length is (x=max) chars پر خواهد شد ولی در حالت دوم برنامه نویس متن خطا را به خود کلاس واگذار نکرده است و آن را طبق میل خود تغییر داده است.


اجباری کردن Type
هر متادیتا می‌تواند مختص  یک نوع Type باشد که این نوع می‌تواند یک کلاس، متد، پراپرتی یا ساختار و ... باشد. نحوه‌ی محدود سازی آن توسط یک متادیتا مشخص می‌شود:
    [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
    public  class  MyMaxLength:Attribute
    {
        private int max;
        public string ErrorText = "";

        public MyMaxLength(int max)
        {
            this.max = max;
            ErrorText = string.Format("max Length is {0} chars", max);
        }
    }
الان این کلاس توسط متادیتای AttributeUsage که پارامتر ورودی آن Enum است محدود به دو ساختار کلاس و Struct شده است. البته در ویژوال بیسیک با نام Structure معرفی شده است. اگر ساختار شمارشی AttributeTarget را مشاهده کنید، لیستی از نوع‌ها را چون All (همه موارد) ، دلیگیت، سازنده، متد و ... را مشاهده خواهید کرد و از آن جا که این متادیتای ما کاربردش در پراپرتی‌ها خلاصه می‌شود، از متادیتای زیر بر روی آن استفاده می‌کنیم:
[AttributeUsage(AttributeTargets.Property)]

  public class User
    {
        [MyMaxLength(30, ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")]
        public string Name { get; set; }
}

یکی دیگر از ویژگی‌های AttributeUsage خصوصیتی به اسم AllowMultiple است که اجازه می‌دهد بیش از یک بار این متادیتا، بر روی یک نوع استفاده شود:
 [AttributeUsage(AttributeTargets.Property,AllowMultiple = true)]
    public  class  MyMaxLength:Attribute
    {
        //....
     }

که تعریف چندگانه آن به شکل زیر می‌شود:
[MyMaxLength(40, ErrorText = "شما اجازه ندارید بیش از 40 کاراکتر وارد نمایید")]
        [MyMaxLength(50, ErrorText = "شما اجازه ندارید بیش از 50 کاراکتر وارد نمایید")]
        [MyMaxLength(30, ErrorText = "شما اجازه ندارید بیش از 30 کاراکتر وارد نمایید")]
        public string Name { get; set; }
در این مثال ما فقط اجازه‌ی یکبار استفاده را خواهیم داد؛ پس مقدار این ویژگی را false قرار می‌دهم.

آخرین ویژگی که این متادیتا در دسترس ما قرار میدهد، استفاده از خصوصیت ارث بری است که به طور پیش فرض با True مقداردهی شده است. موقعی که شما یک متادیتا را به ویژگی ارث بری مزین کنید، در صورتی که آن کلاس که برایش متادیتا تعریف می‌کنید به عنوان والد مورد استفاده قرار بگیرد، فرزند آن هم به طور خودکار این متادیتا برایش منظور می‌گردد. به مثال‌های زیر دقت کنید:
دو عدد متادیتا تعریف شده که یکی از آن‌ها ارث بری در آن فعال شده و دیگری خیر.
public class MyAttribute : Attribute
{
    //...
}

[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class YourAttribute : Attribute
{
    //...
}

هر دو متادیتا بر سر یک متد در یک کلاسی که بعدا از آن ارث بری می‌شود تعریف شده اند.
public class MyClass
{
    [MyAttribute]
    [YourAttribute]
    public virtual void MyMethod()
    {
        //...
    }
}

در کد زیر کلاس بالا به عنوان والد معرفی شده و متد کلاس فرزند الان شامل متادیتایی به اسم MyAttribute است، ولی متادیتای YourAttribute بر روی آن تعریف نشده است.
public class YourClass : MyClass
{
    public override void MyMethod()
    {
        //...
    }

}

الان که با نحوه‌ی تعریف یکی از متادیتاها آشنا شدیم، این بحث پیش می‌آید که چگونه Type مورد نظر را تحت تاثیر این متادیتا قرار دهیم. الان چگونه میتوانم حداکثر متنی که یک پراپرتی می‌گیرد را کنترل کنم. در اینجا ما از مفهومی به نام Reflection  استفاده می‌کنیم. با استفاده از این مفهوم ما میتوانیم به تمامی قسمت‌های یک Type دسترسی داشته باشیم. متاسفانه دسترسی مستقیمی از داخل کلاس متادیتا به نوع مورد نظر نداریم. کد زیر تمامی پراپرتی‌های یک کلاس را چک میکند و سپس ویژگی‌های هر پراپرتی را دنبال کرده و در صورتیکه متادیتای مورد نظر به آن پراپرتی  ضمیمه شده باشد، حالا می‌توانید عملیات را انجام دهید. کد زیر میتواند در هر جایی نوشته شود. داخل کلاسی که که به آن متادیتا ضمیمه می‌کنید یا داخل تابع Main در اپلیکشین‌ها و هر جای دیگر. مقدار True که به متد GetCustomAttributes پاس می‌شود باعث می‌شود تا متادیتاهای ارث بری شده هم لحاظ گردند.
   Type type = typeof (User);

                foreach (PropertyInfo property in type.GetProperties())
                {
                    foreach (Attribute attribute in property.GetCustomAttributes(true))
                    {
                        MyMaxLength max = attribute as MyMaxLength;
                        if (max != null)
                        {
                            string Max = max.ErrorText;
                            //انجام عملیات
                        }
                    }
                }
البته یک ترفند جهت دسترسی به کلاس‌ها از داخل کلاس متادیتا وجود دارد و آن هم این هست که نوع را از طریق پارامتر به سمت متادیتا ارسال کنید. هر چند این کار زیبایی ندارد ولی به هر حال روش خوبی برای کنترل از داخل کلاس متادیتا و هچنین منظم سازی و دسته بندی و کم کردن کد دارد.
[MyMaxLength(30, typeof(User))]
مطالب
ASP.NET MVC #2

MVC‌ چیست و اساس کار آن چگونه است؟

الگوی MVC در سال‌های اول دهه 70 میلادی در شرکت زیراکس توسط خالقین زبان اسمال‌تاک که جزو اولین زبان‌های شیءگرا محسوب می‌شود، ارائه گردید. نام MVC از الگوی Model-View-Controller گرفته شده و چندین دهه است که در صنعت تولید نرم افزار مورد استفاده می‌باشد. هدف اصلی آن جدا سازی مسئولیت‌های اجزای تشکیل دهنده «لایه نمایشی» برنامه است.
این الگو در سال 2004 برای اولین بار در سکویی به نام Rails به کمک زبان روبی جهت ساخت یک فریم ورک وب MVC مورد استفاده قرار گرفت و پس از آن به سایر سکوها مانند جاوا، دات نت (در سال 2007)، PHP و غیره راه یافت.
تصاویری را از این تاریخچه در ادامه ملاحظه می‌کنید؛ از دکتر Trygve Reenskaug تا شرکت زیراکس و معرفی آن در Rails به عنوان اولین فریم ورک وب MVC.


C در MVC معادل Controller است. کنترلر قسمتی است که کار دریافت ورودی‌های دنیای خارج را به عهده دارد؛ مانند پردازش یک درخواست HTTP ورودی.


زمانیکه کنترلر این درخواست را دریافت می‌کند، کار وهله سازی Model را عهده دار خواهد شد و حاوی اطلاعاتی است که نهایتا در اختیار کاربر قرار خواهد گرفت تا فرآیند پردازش درخواست رسیده را تکمیل نماید. برای مثال اگر کاربری جهت دریافت آخرین اخبار به سایت شما مراجعه کرده است،‌ در اینجا کار تهیه لیست اخبار بر اساس مدل مرتبط به آن صورت خواهد گرفت. بنابراین کنترلرها، پایه اصلی و مدیر ارکستر الگوی MVC محسوب می‌شوند.
در ادامه، کنترلر یک View را جهت نمایش Model انتخاب خواهد کرد. View در الگوی MVC یک شیء ساده است. به آن می‌توان به شکل یک قالب که اطلاعاتی را از Model دریافت نموده و سپس آن‌ها را در مکان‌های مناسبی در صفحه قرار می‌دهد، نگاه کرد.
نتیجه استفاده از این الگو، ایزوله سازی سه جزء یاد شده از یکدیگر است. برای مثال View نمی‌داند و نیازی ندارد که بداند چگونه باید از لایه دسترسی به اطلاعات کوئری بگیرد. یا برای مثال کنترلر نیازی ندارد بداند که چگونه و در کجا باید خطایی را با رنگی مشخص نمایش دهد. به این ترتیب انجام تغییرات در لایه رابط کاربری برنامه در طول توسعه کلی سیستم، ساده‌تر خواهد شد.
همچنین در اینجا باید اشاره کرد که این الگو مشخص نمی‌کند که از چه نوع فناوری دسترسی به اطلاعاتی باید استفاده شود. می‌توان از بانک‌های اطلاعاتی، وب سرویس‌ها، صف‌ها و یا هر نوع دیگری از اطلاعات استفاده کرد. به علاوه در اینجا در مورد نحوه طراحی Model نیز قیدی قرار داده نشده است. این الگو تنها جهت ساخت بهتر و اصولی «رابط کاربری» طراحی شده است و بس.



تفاوت مهم پردازشی ASP.NET MVC با ASP.NET Web forms

اگر پیشتر با ASP.NET Web forms کار کرده باشید اکنون شاید این سؤال برایتان وجود داشته باشد که این سیستم جدید در مقایسه با نمونه قبلی، چگونه درخواست‌ها را پردازش می‌کند.


همانطور که مشاهده می‌کنید، در وب فرم‌ها زمانیکه درخواستی دریافت می‌شود، این درخواست به یک فایل موجود در سیستم مثلا default.aspx ارسال می‌گردد. سپس ASP.NET یک کلاس وهله سازی شده معرف آن صفحه را ایجاد کرده و آن‌را اجرا می‌کند. در اینجا چرخه طول عمر صفحه مانند page_load و غیره رخ خواهد داد. جهت انجام این وهله سازی، View به فایل code behind خود گره خورده است و جدا سازی خاصی بین این دو وجود ندارد. منطق صفحه به markup آن که معادل است با یک فایل فیزیکی بر روی سیستم، کاملا مقید است. در ادامه، این پردازش صورت گرفته و HTML نهایی تولیدی به مرورگر کاربر ارسال خواهد شد.
در ASP.NET MVC این نحوه پردازش تغییر کرده است. در اینجا ابتدا درخواست رسیده به یک کنترلر هدایت می‌شود و این کنترلر چیزی نیست جز یک کلاس مجزا و مستقل از هر نوع فایل ASPX ایی در سیستم. سپس این کنترلر کار پردازش درخواست رسیده را شروع کرده، اطلاعات مورد نیاز را جمع آوری و سپس به View ایی که انتخاب می‌کند، جهت نمایش نهایی ارسال خواهد کرد. در اینجا View این اطلاعات را دریافت کرده و نهایتا در اختیار کاربر قرار خواهد داد.



آشنایی با قرارداد یافتن کنترلرهای مرتبط

تا اینجا دریافتیم که نحوه پردازش درخواست‌ها در ASP.NET MVC بر مبنای کلاس‌ها و متدها است و نه بر مبنای فایل‌های فیزیکی موجود در سیستم. اگر درخواستی به سیستم ارسال می‌شود، در ابتدا، این درخواست جهت پردازش، به یک متد عمومی موجود در یک کلاس کنترلر هدایت خواهد شد و نه به یک فایل فیزیکی ASPX (برخلاف وب فرم‌ها).


همانطور که در تصویر مشاهده می‌کنید، در ابتدای پردازش یک درخواست، آدرسی به سیستم ارسال خواهد شد. بر مبنای این آدرس، نام کنترلر که در اینجا زیر آن خط قرمز کشیده شده است، استخراج می‌گردد (برای مثال در اینجا نام این کنترلرProducts است). سپس فریم ورک به دنبال کلاس این کنترلر خواهد گشت. اگر آن‌را در اسمبلی پروژه بیابد، از آن خواهد خواست تا درخواست رسیده را پردازش کند؛ در غیراینصورت پیغام 404 یا یافت نشد، به کاربر نمایش داده می‌شود.
اما فریم ورک چگونه این کلاس کنترلر درخواستی را پیدا می‌کند؟
در زمان اجرا، اسمبلی اصلی پروژه به همراه تمام اسمبلی‌هایی که به آن ارجاعی دارند جهت یافتن کلاسی با این مشخصات اسکن خواهند شد:
1- این کلاس باید عمومی باشد.
2- این کلاس نباید abstract باشد (تا بتوان آن‌را به صورت خودکار وهله سازی کرد).
3- این کلاس باید اینترفیس استاندارد IController را پیاده سازی کرده باشد.
4- و نام آن باید مختوم به کلمه Controller باشد (همان مبحث Convention over configuration یا کار کردن با یک سری قرار داد از پیش تعیین شده).

برای مثال در اینجا فریم ورک به دنبال کلاسی به نام ProductsController خواهد گشت.
شاید تعدادی از برنامه نویس‌های ASP.NET MVC تصور ‌کنند که فریم ورک در پوشه‌ی استانداردی به نام Controllers به دنبال این کلاس خواهد گشت؛ اما در عمل زمانیکه برنامه کامپایل می‌شود، پوشه‌ای در این اسمبلی وجود نخواهد داشت و همه چیز از طریق Reflection مدیریت خواهد شد.