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

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

مشاهده مفاهیم شیء گرایی در پیرامون خود

حتماً در دنیای برنامه نویسی شیء گرا بارها با کلمات کلاس و شیء روبرو شده اید. درک صحیح از این مفاهیم بسیار مهم و البته بسیار ساده است. کار را با یک مثال شروع می‌کنیم. به تصویر زیر نگاه کنید.
 



در سمت راست بخشی از نقشه یک ساختمان و در سمت چپ ساختمان ساخته شده بر اساس این نقشه را می‌بینید. ساختمان همان شیء است. و نقشه ساختمان کلاس آن است چراکه امکان ایجاد اشیائی که تحت عنوان ساختمان طبقه بندی (کلاس بندی) می‌شوند را فراهم می‌کند. به همین سادگی. کلاس‌ها طرح اولیه، نقشه یا قالبی هستند که جزییات یک شی را توصیف می‌کنند.
حتماً با من موافق هستید اگر بگویم:
  • در نقشه ساختمان نمی‌توانید زندگی کنید اما در خود ساختمان می‌توانید.
  • از روی یک نقشه می‌توان به تعداد دلخواه ساختمان ساخت.
  • هنگامی که در یک ساختمان زندگی می‌کنید نیازی نیست تا دقیقاً بدانید چگونه ساخته شده و مثلاً سیم کشی یا لوله کشی‌های آن چگونه است! تنها کافیست بدانید برای روشن شدن لامپ باید کلید آن را بزنید.
  • ساختمان دارای ویژگی هایی مانند متراژ، ضخامت دیوار، تعداد پنجره و ابعاد هر یک و ... است که در هنگام ساخت و بر اساس اطلاعات موجود در نقشه تعیین شده اند.
  • ساختمان دارای کارکرد هایی است. مانند بالا و پایین رفتن آسانسور و یا باز و بسته شدن درب پارکینگ. هر یک از این کارکرد‌ها نیز بر اساس اطلاعات موجود در نقشه پیاده سازی و ساخته شده اند.
  • ساختمان تمام اجزای لازم برای اینکه از آن بتوانیم استفاده کنیم و به عبارتی در آن بتوانیم زندگی کنیم را در خود دارد.
در محیط پیرامون ما تقریباً هر چیزی را می‌توان در یک دیدگاه شیء تصور کرد. به عبارتی هر چیزی که بتوانید به صورت مستقل در ذهن بیاورید و سپس برخی ویژگی‌ها و رفتارها یا کارکردهای آن‌را برشمارید تا آن چیز را قابل شناسایی کند شیء است. مثلاً من به شما می‌گویم موجودی چهار پا دارد، مو... مو... می‌کند و شیر می‌دهد و ... . شما خواهید گفت گاو! و نمی‌گویید گربه. چرا؟ چون توانستید در ذهن خود موجودیتی را به صورت مستقل تصور کنید و از روی ویژگی‌ها و رفتارش آن‌را دقیقاً شناسایی کنید.
سوال: کلاس یا نقشه ایجاد گاو چیست؟ اگر از من بپرسید خواهم گفت طرح اولیه گاو هم ممکن است وجود داشته باشد البته در اختیار خداوند و با سطح دسترسی ملکوت!
اتومبیل، تلویزیون و ... همگی مثال هایی از اشیاء پیرامون ما در دنیای واقعی هستند که حتماً می‌توانید کلاس یا نقشه ایجاد آن‌ها را نیز بدست آورید و یا ویژگی‌ها و کارکرد‌های آن‌ها را برشمارید.

مفاهیم شیء گرایی در مهندسی نرم افزار

مفاهیمی که تاکنون در مورد دنیای واقعی مرور کردیم همان چیزی است که در دنیای برنامه نویسی ـ به عقیده من دنیای واقعی‌تر از دنیای واقعی ـ با آن سر و کار داریم. علت این امر آن است که اصولاً ایده روش برنامه نویسی شیء گرا با مشاهده محیط پیرامون ما به وجود آمده است.
برای نوشتن برنامه جهت حل یک مسئله بزرگ باید بتوان آن مسئله را به بخش‌های کوچکتری تقسیم نمود. در این رابطه مفهوم شیء و کلاس با همان کیفیتی که در محیط پیرامون ما وجود دارد به صورت مناسبی امکان تقسیم یه مسئله بزرگ به بخش‌های کوچکتر را فراهم می‌کند. و سبب می‌شود هماهنگی و تقارن و تناظر خاصی بین اشیاء برنامه و دنیای واقعی بوجود آید که یکی از مزایای اصلی روش شیء گراست.
از آنجا که در یک برنامه اصولاً همه چیز و همه مفاهیم در قالب کدها و دستورات برنامه معنا دارد، کلاس و شیء نیز چیزی بیش از قطعاتی کد نیستند. قطعه کد هایی که بسته بندی شده اند تا تمام کار مربوط به هدفی که برای آن‌ها در نظر گرفته شده است را انجام دهند.
همان طور که در هر زبان برنامه نویسی دستوراتی برای کارهای مختلف مانند تعریف یک متغیر یا ایجاد یک حلقه و ... در نظر گرفته شده است، در زبان‌های برنامه نویسی شیء گرا نیز دستوراتی وجود دارد تا بتوان قطعه کدی را بر اساس مفهوم کلاس بسته بندی کرد.
به طور مثال قطعه کد زیر را در زبان برنامه نویسی سی شارپ در نظر بگیرید.
class Player
{
   public string Name;
   public int Age;
   public void Walk()
   {
      // کدهای مربوط به پیاده سازی راه رفتن
   }
   public void Run()
   {
      // کدهای مربوط به پیاده سازی دویدن
   }
}
در این قطعه کد با استفاده از کلمه کلیدی class در زبان سی شارپ کلاسی ایجاد شده است که دارای دو ویژگی نام و سن و دو رفتار راه رفتن و دویدن است.
این کلاس به چه دردی می‌خورد؟ کجا می‌توانیم از این کلاس استفاده کنیم؟
پاسخ این است که این کلاس ممکن است برای ما هیچ سودی نداشته باشد و هیچ کجا نتوانیم از آن استفاده کنیم. اما بیایید فرض کنیم برنامه نویسی هستیم که قصد داریم یک بازی فوتبال بنویسیم. به جای آنکه قطعات کد مجزایی برای هر یک از بازیکنان و کنترل رفتار و ویژگی‌های آنان بنویسیم با اندکی تفکر به این نکته پی می‌بریم که همه بازیکنان مشترکات بسیاری دارند و به عبارتی در یک گروه یا کلاس قابل دسته بندی هستند. پس سعی می‌کنیم نقشه یا قالبی برای بازیکن‌ها ایجاد کنیم که دربردارنده ویژگی‌ها و رفتارهای آن‌ها باشد.
همان طور که در نقشه ساختمان نمی‌توانیم زندگی کنیم این کلاس هم هنوز آماده انجام کارهای واقعی نیست. چراکه برخی مقادیر هنوز برای آن تنظیم نشده است. مانند نام بازیکن و سن و ....
و همان طور که برای سکونت لازم است ابتدا یک ساختمان از روی نقشه ساختمان بسازیم برای استفاده واقعی از کلاس یاد شده نیز باید از روی آن شیء بسازیم. به این فرآیند وهله سازی یا نمونه سازی نیز می‌گویند. یک زبان برنامه نویسی شیء گرا دستوراتی را برای وهله سازی نیز در نظر گرفته است. در C# کلمه کلیدی new این وظیفه را به عهده دارد.
Player objPlayer = new Player();
objPlayer.Name = “Ali Karimi”;
objPlayer.Age = 30;
objPlayer.Run();
وقتی فرآیند وهله سازی صورت می‌گیرد یک نمونه یا شیء از آن کلاس در حافظه ساخته می‌شود که در حقیقت می‌توانید آنرا همان کدهای کلاس تصور کنید با این تفاوت که مقداردهی‌های لازم صورت گرفته است. به دلیل تعیین مقادیر لازم، حال شیء تولید شده می‌تواند به خوبی اعمال پیش بینی شده را انجام دهد. توجه نمایید در اینجا پیاده سازی داخلی رفتار دویدن و اینکه مثلاً در هنگام فراخوانی آن چه کدی باید اجرا شود تا تصویر یک بازیکن در حال دویدن در بازی نمایش یابد مد نظر و موضوع بحث ما نیست. بحث ما چگونگی سازماندهی کد‌ها توسط مفهوم کلاس و شیء است. همان طور که مشاهده می‌کنید ما تمام جزییات بازیکن‌ها را یکبار در کلاس پیاده سازی کرده ایم اما به تعداد دلخواه می‌توانیم از روی آن بازیکن‌های مختلف را ایجاد کنیم. همچنین به راحتی رفتار دویدن یک بازیکن را فراخوانی میکنیم بدون آنکه پیاده سازی کامل آن در اختیار و جلوی چشم ما باشد.
تمام آنچه که بازیکن برای انجام امور مربوط به خود نیاز دارد در کلاس بازیکن کپسوله می‌شود. بدیهی است در یک برنامه واقعی ویژگی‌ها و رفتارهای بسیار بیشتری باید برای کلاس بازیکن در نظر گرفته شود. مانند پاس دادن، شوت زدن و غیره.
به این ترتیب ما برای هر برنامه می‌توانیم مسئله اصلی را به تعدادی مسئله کوچکتر تقسیم کنیم و وظیفه حل هر یک از مسائل کوچک را به یک شیء واگذار کنیم. و بر اساس اشیاء تشخیص داده شده کلاس‌های مربوطه را بنویسیم. برنامه نویسی شیء گرا سبب می‌شود تا مسئله توسط تعدادی شیء که دارای نمونه‌های متناظری در دنیای واقعی هستند حل شود که این امر زیبایی و خوانایی و قابلیت نگهداری و توسعه برنامه را بهبود می‌دهد.
احتمالاً تاکنون متوجه شده اید که برای نگهداری ویژگی‌های اشیاء از متغیر‌ها و برای پیاده سازی رفتارها یا کارکرد‌های اشیاء از توابع استفاده میکنیم.
با توجه به این که هدف این مطلب بررسی مفهوم شیء گرائی بود و نه جزییات برنامه نویسی، بنابراین بیان برخی مفاهیم در این رابطه را که بیشتر در مهندسی نرم افزار معنا دارند تا در دنیای واقعی در مطالب بعدی بررسی می‌کنیم.
مطالب
آموزش فایرباگ - #7 - CSS Panel
پنل CSS مانند پنل جانبیِ Style که در مقاله‌ی قبل بررسی کردیم است با این تفاوت که امکانات بیشتری برای افرادی که قصد تصریف استایل دارند محیا کرده است.
در این پنل می‌توان به اضافه ، ویرایش و حذف استایل هایی که به صفحه‌ی جاری توسط فایل‌های مختلف اضافه شده اند و یا داخل خود صفحه تعریف شده اند پرداخت.



همچنین در این پنل امکان ویرایش یک فایل css بصورت کامل وجود دارد ، به این صورت که می‌توانید تمام محتویات فایل مورد نظر را در یک Text area ویرایش کنید.

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


Options Menu
با راست کلیک کردن بروی تب CSS و یا کلیک کردن بروی فلش کوچک روی تب CSS قابل دسترس است و امکانات زیر را محیا می‌کند:

  • Expand Shorthand Properties : نمایش دستورات css بصورت کامل ، به این معنی که دستوراتی مانند margin که هم بصورت خلاصه و هم بصورت کامل تعریف می‌شوند را بصورت کامل نمایش می‌دهد. مثلا margin را چهار مقدار margin-top , margin-right , margin-bottom , margin-left نمایش می‌دهد.

  • Color As Hex , Color As RGB , Color As HSL : با انتخاب یکی از سه مقدار ذکر شده ، فرمت نمایش رنگ‌ها به حالت انتخاب شده تغییر می‌کند.
    مثلا دستور color : #000000 به این صورت نمایش داده می‌شود :‭ color : rgb(0, 0, 0) 

  • Refresh : این گزینه محتویات پنل را بروز می‌کند.

Panel Toolbar
ابزاری موجود در این پنل مشابه پنل‌های دیگر در بالای پنل و در زیر تب پنل‌ها قرار دارد و شامل ابزارهای زیر می‌شود:

  • Edit Button : این ابزار به دو صورت Live Edit و Source Edit در دسترس هست که با کلیک بروی فلش کوچکی که در سمت راست دکمه قرار دارد می‌توان نوع آن را تغییر داد.
    انتخاب حالت Source Edit ، باعث می‌شود سورس فایل به همان صورتی که به مرورگر ارسال شده نمایش داده شود. ( کامنت‌ها ، فرمت فایل و ... حفظ می‌شود. )
    حالت Live Edit هم باعث نمایش محتویات فایل بصورت مرتب شده می‌شود. ( کامنت‌ها و دستوراتی که توسط مرورگر تفسیر نشده اند نمایش داده نمی‌شوند. )



  • CSS Location Menu : نام فایل جاری که در پنل در حال نمایش است را نمایش می‌دهد و همچنین با کلیک بروی آن ، فایل‌های استایل دیگری که در صفحه بارگزاری شده اند را نمایش می‌دهد. با کلیک بروی هرکدام از فایل‌های نمایش داده شده ، همان فایل در پنل باز می‌شود.

    استایل هایی که در خود صفحه تعریف شده اند با نام خود صفحه در این قسمت نمایش داده می‌شوند و اگر استایل‌های در چندین تگ style تعریف شده باشند ، دومین تگ با #2 ، سومین با #3 و ... مشخص می‌شوند.

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



    2- با تایپ کردن عبارت دلخواه می‌توان نتایج را محدود کرد:



Panel
منظور از پنل ، قسمتی هست که استایل بصورت فرمت شده و نمایش داده شده است.


 Infotips
توسط این قابلیت ، زمانی که موس را بروی آدرس تصاویر ، نوع فونت و ... ببرید ، پاپ آپ کوچکی باز می‌شود و اطلاعاتی در مورد مقادیر می‌دهد. مثلا اینکه آیا تصویر مورد نظر به درستی بارگذاری شده یا نه ، نمای کوچکی از تصویر ، شکل فونت و ...

 Editing rules
برای ویرایش تعاریف CSS شامل Selector‌ها ، دستورات و مقادیر ، این پنل ابزارهای مفیدی ارائه می‌کند.
برای ویرایش یک selector ، دستور یا مقدار آن بروی آن عبارت کلیک کنید ، در همین حال یک text box ظاهر می‌شود و می‌توانید مقدار جدید را وارد کنید. پس از انجام ویرایش مورد نظر بروی قسمتی از صفحه کلیک کنید یا کلید Tab سپس Esc را بزنید. برای ویرایش دستورات بعدی ، کلید Tab را بزنید و برای لغو تغییر جاری ، کلید Esc را بزنید.

برای ایجاد یک rule جدید ، بروی قسمتی از پنل راست کلیک کرده و سپس گزینه‌ی "New Rule..." را برگزینید.
برای ایجاد یک property هم چند راه وجود دارد:

  • بروی قسمت از فضای خالی تعریف یک استایل دوبار کلیک کنید.
  • در قسمتی از تعریف یک استایل راست کلیک کرده و گزینه‌ی "New Property..." را انتخاب کنید.
  • بروی مقدار آخرین property کلیک کرده و کلید Tab را بزنید.


هنگام ویرایش یا ایجاد یک Rule , Propery یا مقدار بصورت inline ، حاشیه‌ی text box متناسب با مقدار وارد شده رنگی را که نمایانگر حالت ذخیره‌ی مقدار وارد شده است نمایش می‌دهد. برای مثال اگر مقداری که برای یک selector وارد شده است نامعتبر باشد ، رنگ حاشیه‌ی text box قرمز می‌شود.
جدول زیر حالت‌های مختلف را شرح می‌دهد:

رنگ حاشیه
Selectorها
نام Properyها
مقادیر Propertyها و Ruleها
 خاکستری تغییری ایجاد نشده استتغییری ایجاد نشده است
تغییری ایجاد نشده است
 قرمز selector نامعتبر است
نام نامعتبر است
مقدار نامعتبر است
 زرد selector صحیح است اما بروی Element فعلی تاثیر ندارد
نام صحیح است اما مقدار Property غیر مجاز است یا وارد نشده است
 
 سبز selector صحیح است نام و مقدار صحیح هستند
مقدار صحیح است


Auto-completion
زمانی که در حال ایجاد/ویراش کردن یک Rule, Propery یا مقدار آنها هستید ، میتوانید از این قابلیت استفاده کنید. مثلا با وارد کردن # تمام Selector هایی که می‌توانند با # شروع شوند در دسترس شما هستند و با دکمه‌های Up/Down می‌توانید مقادیر ممکن را مرور کنید.
اگر در حال ویرایش یک مقدار عددی هستید ، می‌توانید با دکمه‌های Up/Down مقادیر را یک واحد یک واحد افزایش دهید. با نگه داشتن کلید Shift و فشردن Up/Down می‌توانید مقادیر را 10تا 10تا تغییر دهید و با نگه داشتن Ctrl بجای Sihft ، یک دهم یک دهم.

Toggling styles
با این امکان می‌توانید یک Property را بطور موقت فعال/غیرفعال کنید. با حرکت موس از یک Property یک آیکون قرمز رنگ در کنار آن نمایش داده می‌شود که با کلیک بروی آن ، Property و مقدار آن کمرنگ شده و آیکون قرمز رنگ کنار آن ثابت می‌شود. با کلیک مجدد بروی آن ، Property فعال می‌شود.



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

 گزینهContext
توضیحات
Copy Location
CSS Location Menuآدرس فایل استایل را در حافظه کپی می‌کند.
Open in New TabCSS Location Menuفایل استایل را در یک تب جدید باز می‌کند.
Copy Image Location
image valuesآدرس تصویر را در حافظه کپی می‌کند.
Open Image in New Tabimage valuesتصویر را در یک تب جدید باز می‌کند.
Copy Colorcolor valuesمقدار رنگ را در حافظه کپی می‌کند.
Copy Rule DeclarationCSS selectorSelector و Propertyها را در حافظه کپی می‌کند.
Copy Style DeclarationCSS selectorفقط Propertyها را در حافظه کپی می‌کند.
New Rule...
همه جای پنل
یک Rule جدید بالای قسمتی که راست کلیک شده ایجاد می‌کند.
Delete "<selector>"
CSS selectorRule را حذف می‌کند.
New Property...
CSS rule
یک Property جدید در Ruleی که در آن راست کلیک شده ایجاد می‌کند.
Edit "<property name>"...
CSS property
Property فعلی به حالت ویرایش درمی آید. ( راه ساده‌تر ، کلیک بروی Property است. )
Delete "<property name>"...CSS propertyProperty فعلی را حذف می‌کند.
Disable "<property name>"...
CSS property
Property فعلی را غیرفعال می‌کند.
Refreshهمه جای پنل
محتویات پنل را بروز رسانی می‌کند.
Inspect in DOM panel
CSS Location Menu, CSS rule
فایل استایل یا Rule را در پنل DOM باز می‌کند.


Elements Side Panel
در این پنل که سمت راست پنل CSS قرار دارد ، با وارد کردن یک CSS Selector می‌توانید Elementهایی که در صفحه با آن مطابقت دارند را مشاهده کنید.
برای وارد کردن CSS Selector هم می‌توان مقدار مورد نظر را در قسمت Try a selector... وارد کرد هم می‌توان بروی یکی از Selectorهای پنل راست کلیک کرد و گزینه‌ی Get Matching Elements را برگزید.
Context Menu این قسمت هم مشابه Context Menu پنل HTML هست و فقط در ورژن 1.11 گزینه‌ی Paste HTML اضافه شده که در این کامنت از مقاله‌ی آموزش فایرباگ - #5 - HTML Panel توضیح داده شده است.

مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 5#
در ادامه مطلب پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 4# به تشریح مابقی کلاس‌های برنامه می‌پردازیم.  

در این پست به شرح کلاس Rectangle جهت رسم مستطیل و Square جهت رسم مربع می‌پردازیم

using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Rectangle
    /// </summary>
    public class Rectangle : Shape
    {
        #region Constructors (2)

        /// <summary>
        /// Initializes a new instance of the <see cref="Rectangle" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        public Rectangle(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
            : base(startPoint, endPoint, zIndex, foreColor, thickness, isFill, backgroundColor)
        {
            ShapeType = ShapeType.Rectangle;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Rectangle" /> class.
        /// </summary>
        public Rectangle()
        {
            ShapeType = ShapeType.Rectangle;
        }

        #endregion Constructors

        #region Methods (1)

        // Public Methods (1) 

        /// <summary>
        /// Draws the specified g.
        /// </summary>
        /// <param name="g">The g.</param>
        public override void Draw(Graphics g)
        {
            if (IsFill)
                g.FillRectangle(BackgroundBrush, StartPoint.X, StartPoint.Y, Width, Height);
            g.DrawRectangle(Pen, StartPoint.X, StartPoint.Y, Width, Height);
            base.Draw(g);
        }

        #endregion Methods
    }
}
کلاس Rectangle از کلاس پایه طراحی شده در ^ ارث بری دارد. این کلاس ساده بوده و تنها شامل یک سازنده و متد ترسیم شی مستطیل می‌باشد.

کلاس بعدی کلاس Square می‌باشد، که از کلاس بالا (Rectangle) ارث بری داشته است، کد‌های این کلاس را در زیر مشاهده می‌کنید.
using System;
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Square
    /// </summary>
    public class Square : Rectangle
    {
#region Constructors (2) 

        /// <summary>
        /// Initializes a new instance of the <see cref="Square" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        public Square(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
        {
            float x = 0, y = 0;
            float width = Math.Abs(endPoint.X - startPoint.X);
            float height = Math.Abs(endPoint.Y - startPoint.Y);
            if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = startPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = endPoint.X;
                y = endPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = endPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = startPoint.X;
                y = endPoint.Y;
            }
            StartPoint = new PointF(x, y);
            var side = Math.Max(width, height);
            EndPoint = new PointF(x+side, y+side);
            ShapeType = ShapeType.Square;
            Zindex = zIndex;
            ForeColor = foreColor;
            Thickness = thickness;
            BackgroundColor = backgroundColor;
            IsFill = isFill;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Square" /> class.
        /// </summary>
        public Square()
        {
            ShapeType = ShapeType.Square;
        }

#endregion Constructors 
    }
}
این کلاس شامل دو سازنده می‌باشد که سازنده دوم فقط نوع شی را تعیین می‌کند و بقیه کارهای آن مانند مستطیل است، در واقع می‌توان از یک دیدگاه گفت که مربع یک مستطیل است که اندازه طول و عرض آن یکسان است. در سازنده اول (نحوه ترسیم شکل) ابتدا نقاط ابتدا و انتهای رسم شکل تعیین شده و سپس با توجه به پارامترهای محاسبه شده نوع شی جهت ترسیم و دیگر خصوصیات کلاس مقدار دهی می‌شود، با این تفاوت که در نقطه EndPoint طول و عرض مربع برابر با بزرگترین مقدار طول و عرض وارد شده در سازنده کلاس تعیین شده و مربع شکل می‌گیرد. مابقی متدهای ترسیم و ... طبق کلاس پایه مستطیل و Shape تعیین می‌شود.
مطالب قبل:
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 4# 
مطالب
نحوه ایجاد یک گزارش فاکتور فروش توسط PdfReport
شکل زیر را که شبیه به یک فاکتور فروش است درنظر بگیرید:



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

در ادامه کدهای کامل این مثال را مشاهده می‌کنید. همچنین این کد و کلاس‌های وابسته به آن مانند User و TransparentTemplate به سورس‌های کتابخانه PdfReport نیز اضافه شده‌اند.
using System;
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfReportSamples.Models;
using PdfReportSamples.Templates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.Tax
{
    public class TaxPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.CustomTemplate(new TransparentTemplate());
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 7; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PreviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");

                events.MainTableAdded(args =>
                {
                    var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
                    var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);

                    var others = Math.Round(balance * 1.8 / 100);
                    var tax = Math.Round(balance * 2.2 / 100);
                    var total = balance + tax + others;

                    var taxTable = new PdfPTable(args.Table); // Create a clone of the MainTable's structure                   

                    taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "عوارض";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", others);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "جمع کل";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", total);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "قابل پرداخت";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = total.NumberToText(Language.Persian) + " ریال";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                            cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                        });

                    args.PdfDoc.Add(taxTable);
                });
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\TaxReportSample.pdf"));
        }
    }
}
توضیحات:
تنها تفاوت این مثال با مثال‌های قبلی، کدهای مرتبط با متد events.MainTableAdded می‌باشند.
توسط متد args.LastOverallAggregateValueOf، می‌توان به مقدار نهایی متد تجمعی تعریف شده برای یک ستون خاص دسترسی یافت:
var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);
سپس بر این اساس، امکان محاسبه مالیات و عوارض میسر می‌شود:
var others = Math.Round(balance * 1.8 / 100);
var tax = Math.Round(balance * 2.2 / 100);
var total = balance + tax + others;
در ادامه نیاز داریم تا یک جدول جدید را ذیل جدول اصلی ایجاد کنیم. نکته مهم این جدول جدید، هماهنگی عرض ستون‌های آن با ستون‌های جدول اصلی است. به همین منظور می‌توان از خاصیت args.Table جهت دسترسی به خواص جدول اصلی استفاده کرد و جدول جدیدی را ایجاد نمود:
var taxTable = new PdfPTable(args.Table);
از اینجا به بعد دیگر به عهده خودتان است. می‌توانید از دانش iTextSharp خود استفاده کرده و ردیف‌های این جدول جدید را پر کنید. یا اینکه می‌توانید از متد کمکی توکار AddSimpleRow به نحو زیر استفاده نمائید:
taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });
با توجه به اینکه قصد نداریم در سه ستون اول این جدول جدید، عنصری را نمایش دهیم، آن‌ها را با null مقدار دهی کرده و سپس ستون برچسب و ستون مقدار را اضافه می‌کنیم (آرگومان‌های این متد به صورت params تعریف شده‌اند. بنابراین هر تعداد ستون که نیاز باشد قابل تعریف است).
با مقدار دهی data، مقدار مورد نظر در آن سلول ثبت می‌گردد. با مقدار دهی خواص cellProperties، نوع قلم، جهت قرارگیری و سایر تنظیماتی را که ملاحظه می‌کنید، می‌توان اعمال کرد.
و در آخر لازم است که این جدول جدید را به شیء Document اضافه کنیم تا نمایش داده شود:
args.PdfDoc.Add(taxTable);

یک نکته:
متد NumberToText جزئی از کتابخانه PdfReport (تعریف شده در فضای نام PdfRpt.Core.Helper) است و برای نمایش رقم به حروف می‌تواند مورد استفاده قرار گیرد:
total.NumberToText(Language.Persian)

 
مطالب
کار با بانک‌های اطلاعاتی مختلف در PdfReport
تعدادی از منابع داده پیش فرض PdfReport جهت کار مستقیم با بانک‌های اطلاعاتی مختلف، کوئری نوشتن و نمایش نتایج آن‌ها طراحی شده‌اند.
در این بین با توجه به اینکه دات نت پشتیبانی توکاری از SQL Server دارد، اتصال و استفاده از توانمندی‌های آن نیاز به کتابخانه جانبی خاصی ندارد. اما برای کار با بانک‌های اطلاعاتی دیگر نیاز خواهد بود تا پروایدر ADO.NET آن‌ها را تهیه و به برنامه اضافه کنیم.
چهار نمونه از منابع داده پیش فرضی که در متد MainTableDataSource قابل تعریف هستند به شرح زیر می‌باشند:
public void SqlDataReader(string connectionString, string sql, params object[] parametersValues)

//.mdb or .accdb files
public void AccessDataReader(string filePath, string password, string sql, params object[] parametersValues)

public void OdbcDataReader(string connectionString, string sql, params object[] parametersValues)
SqlDataReader برای کار با بانک‌های اطلاعاتی SQL Server بهینه سازی شده است.
AccessDataReader قابلیت اتصال به بانک‌های اطلاعاتی اکسس جدید (فایل‌های accdb) و اکسس قدیم (فایل‌های mdb) را دارد.
OdbcDataReader یک پروایدر عمومی است که از روز اول دات نت به همراه آن بوده است. برای مثال جهت اتصال به بانک‌های اطلاعاتی فاکس‌پرو می‌تواند مورد استفاده قرار گیرد.
اما ... برای مابقی بانک‌های اطلاعاتی چطور؟
برای سایر بانک‌های اطلاعاتی، منبع داده عمومی زیر تدارک دیده شده است:
public void GenericDataReader(string providerName, string connectionString, string sql, params object[] parametersValues)
تنها تفاوت آن با نمونه‌های قبل، ذکر providerName آن است. برای مثال جهت اتصال به SQLite ابتدا پروایدر مخصوص ADO.NET آن‌را دریافت و به پروژه خود اضافه نمائید. سپس پارامتر providerName فوق را با "System.Data.SQLite" مقدار دهی کنید.

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

در ادامه نحوه تهیه گزارش از یک بانک اطلاعاتی SQLite را توسط PdfReport بررسی خواهیم کرد:

using System;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.SQLiteDataReader
{
    public class SQLiteDataReaderPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.RunDirection(PdfRunDirection.RightToLeft);
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SilverTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(5);
            })
            .MainTableDataSource(dataSource =>
            {
                dataSource.GenericDataReader(
                    providerName: "System.Data.SQLite",
                    connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                    sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                    parametersValues: new object[] { 10 }
                );
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PreviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("url");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("آدرس");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.Hyperlink(foreColor: System.Drawing.Color.Blue, fontUnderline: true);
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("name");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(2);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("NumberOfPosts");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("تعداد مطلب");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("AddDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("تاریخ ثبت");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : PersianDate.ToPersianDateTime((DateTime)obj) /*((DateTime)obj).ToString("dd/MM/yyyy HH:mm")*/);
                    });
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptSqlDataReaderSample.pdf"));
        }
    }
}

توضیحات:

- در مثال فوق نحوه استفاده از یک بانک اطلاعاتی SQLite را ملاحظه می‌کنید. این بانک اطلاعاتی نمونه در پوشه bin\data سورس به روز شده پروژه موجود است.
                dataSource.GenericDataReader(
                    providerName: "System.Data.SQLite",
                    connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                    sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                    parametersValues: new object[] { 10 }
                );
فرض بر این است که فایل‌های System.Data.SQLite.dll و SQLite.Interop.dll را از سایت SQLite دریافت کرده و سپس ارجاعی را به اسمبلی System.Data.SQLite.dll به پروژه خود افزوده‌اید.
در مرحله بعد به کمک GenericDataReader می‌توان به این پروایدر دسترسی یافت. همانطور که ملاحظه می‌کنید یک کوئری پارامتری با مقدار پارامتر مساوی 10 جهت تهیه گزارش، تعریف شده است.
همچنین باید دقت داشت که اگر پروژه جاری شما مبتنی بر دات نت 4 است، نیاز خواهید داشت چند سطر زیر را به فایل config برنامه اضافه نمائید تا با SQLite مشکلی نداشته باشد:
<?xml version="1.0"?>
   <configuration>
      <startup useLegacyV2RuntimeActivationPolicy="true">
           <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
      </startup>
   </configuration>                  
- مرحله بعد نوبت به معرفی ستون‌های گزارش است. هر ستون، معادل یک فیلد معرفی شده در کوئری SQL ارسال شده به GenericDataReader خواهد بود (کوچکی و بزرگی حروف باید در اینجا رعایت شوند).
- در حین معرفی ستون AddDate، نحوه نمایش و تبدیل تاریخ دریافتی که با فرمت DateTime است را به تاریخ شمسی ملاحظه می‌کنید. متد PersianDate.ToPersianDateTime در فضای نام PdfRpt.Core.Helper قرار دارد. توسط DisplayFormatFormula، فرصت خواهید داشت مقدار متناظر با سلول در حال رندر را پیش از نمایش، به هر نحو دلخواهی فرمت کنید.
- در ستون url از قالب نمایشی پیش فرض Hyperlink، برای نمایش اطلاعات فیلد جاری به صورت یک لینک قابل کلیک استفاده شده است.

یک نکته:
ذکر قسمت MainTableColumns و تمام تعاریف مرتبط با آن در PdfReports اختیاری است. به این معنا که می‌توانید قسمت گزارش سازی و تعاریف گزارشات برنامه خود را پویا کنید (شبیه به حالت auto generate columns در گرید‌های معروف). کوئری‌های SQL متناظر با گزارشات را در بانک اطلاعاتی ذخیره کنید و به گزارش ساز فوق ارسال نمائید. حاصل یک گزارش جدید است.

مطالب
تزریق وابستگی (Dependency Injection) و توسعه پذیری
دانستن اینکه چگونه یک نرم افزار با قابلیت نگهداری بالا بنویسیم مهم است ، برای اکثر سیستم‌های سازمانی زمانی که در فاز نگهداری صرف می‌شود بیشتر از زمان فاز توسعه می‌باشد. به عنوان مثال تصور کنید در حال توسعه یک سیستم مالی هستید ، این سیستم احتمالا بین شش ماه تا یک زمان برای توسعه نیاز دارد و بقیه‌ی دوره‌ی پنج ساله صرف نگهداری سیستم خواهد شد. در فاز نگهداری زمان صرف رفع باگ ، افزودن امکانات جدید و یا تغییر عملکرد ویژگی‌های فعلی می‌شود. مهم است که این تغییرات راحت و سریع صورت پذیرد.
 اطمینان از اینکه کدها قابلیت نگهداری دارند به توسعه دهندگان احتمالی که در آینده به پروژه اضافه می‌شوند کمک می‌کند سریع کد‌های فعلی را درک کنند و مشغول کار شوند. روش‌های زیادی برای افزایش قابلیت نگهداری کد‌ها وجود دارد ، مانند نوشتن آزمون‌های واحد ، شکستن قسمت‌های بزرگ سیستم به قسمت‌های کوچک‌تر و ... در این مورد که ما از یکی از زبان‌های شئ گرا مانند C# استفاده می‌کنیم در حالت معمول کلاس‌ها باید با مسئولیت‌های مستقل و منحصر به فرد طراحی شوند به جای آنکه تمام مسئولیت‌ها از قبیل پردازش ورودی‌های کاربر ، رندر کردن HTML و حتی Query زدن به دیتابیس را به یک کلاس سپرد (مثلا Controller در MVC ) باید برای هر مقصود کلاسی مجزا طراحی کرد. با این روش نتیجه اینگونه خواهد بود که می‌توان هر قسمت از عملکرد را بدون نیاز به تغییر بقیه‌ی قسمت‌های Codebase تغییر داد.
در این مطلب قصد داریم به کمک تزریق وابستگی (ِDependency Injection) قسمت‌های مستقلتری توسعه دهیم. تکنیک تزریق وابستگی را نمی‌توان در یک مطلب وبلاگ و حتی یک فصل کامل از یک کتاب کامل تشریح کرد ، اگر جستجو کنید کتاب‌ها و آموزش‌های ویدویی زیادی هستند که فقط روی این تکنیک بحث و آموزش دارند. برای بیان مفهوم DI مثالی از یک سیستم ساده‌ی "چاپ اسناد" ارائه می‌کنیم ، این سیستم ممکن است کار‌های متفاوتی انجام دهد :
 این سیستم ابتدا باید یک سند را تحویل بگیرد ، سپس باید آن را به فرمت قابل چاپ در آورد و در انتها باید عمل اصلی چاپ را انجام دهد. برای اینکه سیستم ما ساختار خوبی داشته باشد می‌توان هر وظیفه را به کلاسی مجزا سپرد :
 کلاس Document : این کلاس اطلاعات سندی که قرار است چاپ شود را نگه می‌دارد.
کلاس DocumentRepository : این کلاس وظیفه‌ی بازیابی سند از فایل سیستم (یا هر منبع دیگری) را دارد.
 کلاس DocumentFormatter : یک وهله از سند را جهت چاپ آماده می‌کند.
کلاس Printer : مسئولیت ارتباط با سخت افزار Printer را دارد.
کلاس DocumentPrinter : مسئولیت سازماندهی اجزا سیستم را بر عهده دارد.
 در این مطلب پیاده سازی بدنه‌ی کلاس‌های بالا اهمیتی ندارد :
public class DocumentPrinter
{
  public void PrintDocument(string documentName)
  {
    var repository = new DocumentRepository();
    var formatter = new DocumentFormatter();         
    var printer = new Printer();              
    var document = repository                        
      .GetDocumentByName(documentName);               
    var formattedDocument = formatter.Format(document);    
    printer.Print(formattedDocument); 
  }
}
همانطور که مشاهده می‌کنید در بدنه‌ی کلاس DocumentPrinter ابتدا وابستگی‌ها نمونه سازی شده اند ، سپس یک سند بر اساس نام دریافت شده و سند پس از آماده شدن به فرمت چاپ به چاپگر ارسال شده است.  کلاس DocumentPrinter به تنهایی قادر به چاپ سند نیست و برای انجام این کار نیاز به نمونه سازی همه‌ی وابستگی‌ها دارد .
 استفاده از این API اینگونه خواهد بود :
var documentPrinter = new DocumentPrinter();
documentPrinter.PrintDocument(@"c:\doc.doc");
در حال حاضر کلاس DocumentPrinter از DI استفاده نمی‌کند این کلاس Loosely coupled نیست. به طور مثال لازم است که API سیستم به گونه ای تغییر پیدا کند که سند به جای فایل سیستم از دیتابیس بازیابی شود ، باید کلاس جدیدی به نام DatabaseDocumentRepository تعریف شود و به جای DocumentRepository اصلی در بدنه‌ی DocumentPrinter استفاده شود ، در نتیجه با تغییر با تغییر دادن یک قسمت از برنامه مجبور به تغییر در قسمت دیگر شده ایم.(tightly coupled است یعنی به دیگر قسمت‌ها چفت شده است.)
  DI به ما کمک می‌کند که این چفت شدگی (coupling) را از بین ببریم.
استفاده از constructor injection:
 اولین قدم برای از بین بردن این چفت شدگی Refactor کردن کلاس DocumentPrinter هست ، پس از این Refactoring وظیفه‌ی وهله سازی مستقیم اشیاء از این کلاس گرفته می‌شود و نیازمندی‌های این کلاس از طریق سازنده به این کلاس تزریق می‌شود و فیلد‌های کلاس نگهداری می‌شود . به کد زیر توجه کنید :
public class DocumentPrinter
{
  private DocumentRepository _repository;
  private DocumentFormatter _formatter;       
  private Printer _printer;              
  public DocumentPrinter(             
    DocumentRepository repository,               
    DocumentFormatter formatter,      
    Printer printer)                  
  {                                   
    _repository = repository;         
    _formatter = formatter;           
    _printer = printer;               
  }
  public void PrintDocument(string documentName)
  {
    var document = _repository.GetDocumentByName(documentName);
    var formattedDocument = _formatter.Format(document);
    _printer.Print(formattedDocument);
  }
}
 اکنون برای استفاده از این کلاس باید نیازمندی هایش را قبل از ارسال به سازنده نمونه سازی کرد :
var repository = new DocumentRepository();
var formatter = new DocumentFormatter();
var printer = new Printer();
var documentPrinter = new DocumentPrinter(repository, formatter, printer);
documentPrinter.PrintDocument(@"c:\doc.doc");
بله هنوز طراحی خوبی نیست اما این یک مثال ساده از DI می‌باشد. هنوز مشکلاتی در این طراحی هست ، به طور مثال کلاس DocumentPrinter به یک پیاده سازی مشخص از وابستگی هایش چفت شده است. (هنوز برای استفاده از  DatabaseDocumentRepository باید DocumentPrinter را تغییر داد) پس این طراحی هنوز انعطاف پذیر نیست و نمی‌توان به سادگی برای آن آزمون واحد نوشت.
برای حل این مشکلات از Interface‌ها کمک می‌گیریم. اگر به مثال قبلی بازگردیم نگرانی هر دو کلاس DocumentRepository و DatabaseDocumentRepository دریافت سند می‌باشد ، تنها پیاده سازی تفاوت دارد ، پس می‌توان یک Interface تعریف کرد
public interface IDocumentRepository
{
  Document GetDocumentByName(string documentName);
}
 حال ما 2 کلاس داریم که هر دو یک Interface را پیاده سازی کرده اند می‌توان این کار را برای بقیه‌ی وابستگی‌های کلاس DocumentPrinter نیز انجام داد ، حالا باید DocumentPrinter را به گونه ای Refactor کنیم که وابستگی‌ها را بر اساس Interface دریافت کند :
public class DocumentPrinter
{
  private IDocumentRepository _repository;                        
  private IDocumentFormatter _formatter;                          
  private IPrinter _printer;                                      
  public DocumentPrinter(
    IDocumentRepository repository,
    IDocumentFormatter formatter,
    IPrinter printer)
  {
    _repository = repository;
    _formatter = formatter;
    _printer = printer;
  }
  public void PrintDocument(string documentName)
  {
    var document = _repository.GetDocumentByName(documentName);
    var formattedDocument = _formatter.Format(document);
    _printer.Print(formattedDocument);
  }
}
حالا به سادگی می‌توان پیاده سازی‌های متفاوتی را از وابستگی‌های DocumentPrinter انجام داد و به آن تزریق کرد. همچنین اکنون نوشتن آزمون واحد هم ممکن شده است ، می‌توان یک پیاده سازی جعلی از هر کدام از Interface‌ها انجام داد و جهت اهداف Unit testing از آن استفاده کرد. به طور مثال می‌توان یک پیاده سازی جعلی از IPrinter انجام داد و بدون نیاز به ارسال صفحه به پرینتر عملکرد سیستم را تست کرد.
با وجودی که موفق شدیم چفت شدگی میان DocumentPrinter و وابستگی هایش را از بین ببریم اما اکنون استفاده از آن پیچیده شده است ، هربار که قصد نمونه سازی شیء را داریم باید به یاد آوریم کدام پیاده سازی از Interface مورد نیاز است ؟ این پروسه را می‌توان به کمک یک DI Container اتوماسیون کرد.
DI Container یک Factory هوشمند است ، مانند بقیه‌ی کلاس‌های Factory وظیفه‌ی نمونه سازی اشیاء را بر عهده دارد. هوشمندی آن در اینجا هست که می‌داند چطور وابستگی‌ها را نمونه سازی کند . DI Container‌های زیادی برای .NET وجود دارند یکی از محبوب‌ترین آنها StructureMap می‌باشد که قبلا در سایت درباره آن صحبت شده است .
برای مثال جاری پس از افزودن StructureMap به پروژه کافی است در ابتدای شروع برنامه به آن بگوییم برای هر Interface کدام شیء را وهله سازی کند : 
ObjectFactory.Configure(cfg =>
{
  cfg.For<IDocumentRepository>().Use<FilesystemDocumentRepository>();
  cfg.For<IDocumentFormatter>().Use<DocumentFormatter>();
  cfg.For<IPrinter>().Use<Printer>();
});
مطالب
ASP.NET Web API - قسمت سوم
در قسمت اول به دلایل ایجاد Web API پرداخته شد و در قسمت دوم مثالی ساده از Web API را بررسی کردیم. در این قسمت، مثال قبل را تست کرده و نحوه‌ی تعامل jQuery با آن را بررسی می‌کنیم.

فراخوانی Web API از طریق مرورگر

با فشردن کلید F5، پروژه را اجرا کنید. شکل ذیل ظاهر می‌شود.


صفحه ای که ظاهر می‌شود، یک View است که توسط HomeController و متد Index آن برگشت داده شده است. برای فراخوانی متدهای موجود در کلاس Controller مثال قسمت قبل که مربوط به Web API است، باید به یکی از آدرس‌های اشاره شده در قسمت قبل برویم. به عنوان مثال، برای به دست آوردن لیست تمامی محصولات، به آدرس http://localhost:xxxx/api/products بروید. xxxx، شماره‌ی پورتی است که Web Server داخلی Visual Studio در هنگام اجرای پروژه به آن اختصاص می‌دهد. آن را نسبت به پروژه‌ی خود تغییر دهید.
نتیجه‌ی دریافتی بستگی به نوع مرورگری دارد که استفاده می‌کنید. Internet Explorer از شما در مورد باز کردن یا ذخیره‌ی فایلی با نام products پرسش می‌کند (شکل ذیل).


محتوای فایل، بدنه‌ی پاسخ دریافتی است. اگر این فایل را باز کنید، خواهید دید که که محتوای آن، لیستی از محصولات با فرمت JSON مانند ذیل است.

[{"Id":1,"Name":"Tomato soup","Category":"Groceries","Price":1.39},{"Id":2,"Name":
"Yo-yo","Category":"Toys","Price":3.75},{"Id":3,"Name":"Hammer","Category":
"Hardware","Price":16.99}]
اما مرورگر Firefox، محصولات را در قالب XML نشان می‌دهد (شکل ذیل).


دلیل تفاوت در نتیجه‌ی دریافتی این است که مرورگر Internet Explorer و Firefox، هر یک مقدار متفاوتی را در هدر Accept درخواست، ارسال می‌کنند. بنابراین، Web API نیز مقدار متفاوتی را در پاسخ برگشت می‌دهد.

حال به آدرس‌های ذیل بروید: 

http://localhost:xxxx/api/products/1
http://localhost:xxxx/api/products?category=hardware

اولین آدرس، باید محصولی با مشخصه‌ی 1 را برگشت دهد و دومین آدرس، لیستی از تمامی محصولاتی که در دسته‌ی hardware قرار دارند را برگشت می‌دهد (در مثال ما فقط یک آیتم این شرط را دارد).

نکته: در صورتی که در هنگام فراخوانی هر یک از متدهای Web API با خطای ذیل مواجه شدید، دستور [("AcceptVerbs("GET", "POST] را به ابتدای متدها اضافه کنید.

The requested resource does not support http method 'GET'
 

فراخوانی Web API با استفاده از کتابخانه‌ی jQuery

در قسمت قبل، متدهای Web API را مستقیماً از طریق وارد کردن آدرس آنها در نوار آدرس مرورگر فراخوانی کردیم. اما در اکثر اوقات، این متدها با روش‌های برنامه نویسی توسط یک Client فراخوانی می‌شوند. اجازه بدهید Clientیی ایجاد کنیم که با استفاده از jQuery، متدهای ما را فراخوانی می‌کند.
در Solution Explorer، از پوشه‌ی Views و سپس Home، فایل Index.cshtml را باز کنید.

تمامی محتویات این View را حذف و کدهای ذیل را در آن قرار دهید.  

<!DOCTYPE html>
<html>
<head>
    <title>ASP.NET Web API</title>
    <script src="../../Scripts/jquery-1.7.2.min.js" 
        type="text/javascript"></script>
</head>
<body>
    <div>
        <h1>All Products</h1>
        <ul id='products' />
    </div>
    <div>
        <label for="prodId">ID:</label>
        <input type="text" id="prodId" size="5"/>
        <input type="button" value="Search" onclick="find();" />
        <p id="product" />
    </div>
</body>
</html>


بازیابی لیستی از محصولات

برای بازیابی لیستی از محصولات، فقط کافی است تا یک درخواست از نوع GET به آدرس "/api/products" بفرستید. این کار با jQuery به صورت ذیل انجام می‌شود. 

<script type="text/javascript">
    $(document).ready(function () {
        // Send an AJAX request
        $.getJSON("api/products/",
        function (data) {
            // On success, 'data' contains a list of products.
            $.each(data, function (key, val) {

                // Format the text to display.
                var str = val.Name + ': $' + val.Price;

                // Add a list item for the product.
                $('<li/>', { html: str })    
                .appendTo($('#products'));   
            });
        });
    });
</script>

متد getJSON، یک درخواست AJAX از نوع GET را ارسال می‌کند و پاسخ دریافتی آن نیز با فرمت JSON خواهد بود. دومین پارامتر متد getJSON، یک callback است که پس از دریافت موفقیت آمیز پاسخ اجرا می‌شود.


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

برای بازیابی یک محصول با استفاده از مشخصه‌ی آن، یک درخواست از نوع GET به آدرس "api/products/id/" ارسال کنید. id، مشخصه‌ی محصول است. کد ذیل را در ادامه‌ی کد قبل و پیش از تگ <script/> قرار دهید.

function find() {
    var id = $('#prodId').val();
    $.getJSON("api/products/" + id,
        function (data) {
            var str = data.Name + ': $' + data.Price;
            $('#product').html(str);
        })
    .fail(
        function (jqXHR, textStatus, err) {
            $('#product').html('Error: ' + err); 
        });
}


باز هم از متد getJSON استفاده کردیم، اما این بار مقدار id برای آدرس از یک Text Box خوانده و آدرس ایجاد می‌شود. پاسخ دریافتی، یک محصول در قالب JSON است.


اجرای پروژه

پروژه را با فشردن کلید F5 اجرا کنید. پس از نمایش فرم، تمامی محصولات بر روی صفحه نمایش داده می‌شوند. عدد 1 را وارد و بر روی دکمه‌ی Search کلیک کنید، محصولی که مشخصه‌ی آن 1 است نمایش داده می‌شود (شکل ذیل).

اگر مشخصه ای را وارد کنید که وجود ندارد، خطای 404 با مضمون "Error: Not Found" بر روی صفحه نمایش داده می‌شود و در صورتی که به جای عدد، عبارتی غیر عددی وارد کنید، خطای 400 با مضمون: "Error: Bad Request" نمایش داده می‌شود. در Web API، تمامی پاسخ‌ها باید در قالب کدهای وضعیت HTTP باشند (شکل ذیل). این یکی از اصول اساسی کار با وب سرویس‌ها است. وفادار ماندن به مفاهیم پایه‌ی وب، دید بهتری در مورد اتفاقاتی که می‌افتد به شما می‌دهد. 

در قسمت بعد با مفهوم مسیریابی در ASP.NET Web API آشنا می‌شوید.

مطالب
ASP.NET MVC #15

فیلترها در ASP.NET MVC

پایه قسمت‌های بعدی مانند مباحث امنیت، اعتبار سنجی کاربران، caching و غیره، مبحثی است به نام فیلترها در ASP.NET MVC. تابحال با سه فیلتر به نام‌های ActionName، NonAction و AcceptVerbs آشنا شده‌ایم. به این‌ها Action selector filters هم گفته می‌شود. زمانیکه قرار است یک درخواست رسیده به متدی در یک کنترلر خاص نگاشت شود،‌ فریم ورک ابتدا به متادیتای اعمالی به متدها توجه کرده و بر این اساس درخواست را به متدی صحیح هدایت خواهد کرد. ActionName، نام پیش فرض یک متد را بازنویسی می‌کند و توسط AcceptVerbs اجرای یک متد، به افعالی مانند POST، GET، DELETE و امثال آن محدود می‌شود که در قسمت‌های قبل در مورد آن‌ها بحث شد.
علاوه بر این‌ها یک سری فیلتر دیگر نیز در ASP.NET MVC وجود دارند که آن‌ها نیز به شکل متادیتا به متدهای کنترلرها اعمال شده و کار نهایی‌اشان تزریق کدهایی است که باید پیش و پس از اجرای یک اکشن متد،‌ اجرا شوند. 4 نوع فیلتر در ASP.NET MVC وجود دارند:
الف) IAuthorizationFilter
این نوع فیلترها پیش از اجرای هر متد یا فیلتر دیگری در کنترلر جاری اجرا شده و امکان لغو اجرای آن‌را فراهم می‌کنند. پیاده سازی پیش‌فرض آن توسط کلاس AuthorizeAttribute در فریم ورک وجود دارد.
بدیهی است این نوع اعمال را مستقیما داخل متدهای کنترلرها نیز می‌توان انجام داد (بدون نیاز به هیچگونه فیلتری). اما به این ترتیب حجم کدهای تکراری در سراسر برنامه به شدت افزایش می‌یابد و نگهداری آن‌را در طول زمان مشکل خواهد ساخت.

ب) IActionFilter
ActionFilterها پیش (OnActionExecuting) و پس از (OnActionExecuted) اجرای متدهای کنترلر جاری اجرا می‌شوند و همچنین پیش از ارائه خروجی نهایی متدها. به این ترتیب برای مثال می‌توان نحوه رندر یک View را تحت کنترل گرفت. این اینترفیس توسط کلاس ActionFilterAttribute در فریم ورک پیاده سازی شده است.

ج) IResultFilter
ResultFilter بسیار شبیه به ActionFilter است با این تفاوت که تنها پیش از (OnResultExecuting) بازگرداندن نتیجه متد و همچنین پس از (OnResultExecuted) اجرای متد، فراخوانی می‌گردد. کلاس ActionFilterAttribute موجود در فریم ورک، پیاده سازی پیش فرضی از آن‌‌را ارائه می‌دهد.

د) IExceptionFilter
ExceptionFilterها پس از اجرای تمامی فیلترهای دیگر، همواره اجرا خواهند شد؛ صرفنظر از اینکه آیا در این بین استثنایی رخ داده است یا خیر. بنابراین یکی از کاربردهای آن‌ها می‌تواند ثبت وقایع مرتبط با استثناهای رخ‌داده باشد. پیاده سازی پیش فرض آن توسط کلاس HandleErrorAttribute در فریم ورک موجود است.

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

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

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس فیلتر سفارشی زیر را به برنامه اضافه نمائید:

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class LogAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Log("OnActionExecuting", filterContext);
}

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
Log("OnActionExecuted", filterContext);
}

public override void OnResultExecuting(ResultExecutingContext filterContext)
{
Log("OnResultExecuting", filterContext);
}

public override void OnResultExecuted(ResultExecutedContext filterContext)
{
Log("OnResultExecuted", filterContext);
}

private void Log(string stage, ControllerContext ctx)
{
ctx.HttpContext.Response.Write(
string.Format("{0}:{1} - {2} < br/> ",
ctx.RouteData.Values["controller"], ctx.RouteData.Values["action"], stage));
}
}
}

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

using System.Web.Mvc;
using MvcApplication12.CustomFilters;

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

[Log]
public ActionResult Test()
{
return View();
}
}
}

سپس ویژگی Log را از متدها حذف کرده و به خود کنترلر اعمال کنید:
[Log]
public class HomeController : Controller

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


تقدم و تاخر اجرای فیلترهای هم‌خانواده

همانطور که عنوان شد، همیشه ابتدا AuthorizationFilter اجرا می‌شود و در آخر ExceptionFilter. سؤال: اگر در این بین مثلا دو نوع ActionFilter متفاوت به یک متد اعمال شدند، کدامیک ابتدا اجرا می‌شود؟
تمام فیلترها از کلاسی به نام FilterAttribute مشتق می‌شوند که دارای خاصیتی است به نام Order. بنابراین جهت مشخص سازی ترتیب اجرای فیلترها تنها کافی است این خاصیت مقدار دهی شود. برای مثال جهت اعمال دو فیلتر سفارشی زیر:

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class AuthorizationFilterA : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
Debug.WriteLine("OnAuthorization : AuthorizationFilterA");
}
}
}

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class AuthorizationFilterB : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
Debug.WriteLine("OnAuthorization : AuthorizationFilterB");
}
}
}

خواهیم داشت:
using System.Web.Mvc;
using MvcApplication12.CustomFilters;

namespace MvcApplication12.Controllers
{
public class HomeController : Controller
{
[AuthorizationFilterA(Order = 2)]
[AuthorizationFilterB(Order = 1)]
public ActionResult Index()
{
return View();
}
}
}

در اینجا با توجه به مقادیر order، ابتدا AuthorizationFilterB اجرا می‌گردد و سپس AuthorizationFilterA.
علاوه بر این‌ها محدوده اجرای فیلترها نیز بر بر این حق تقدم اجرایی تاثیر گذار هستند. برای مثال در پشت صحنه زمانیکه قرار است یک فیلتر جدید اجرا شود، وهله سازی آن به نحوه زیر است که بر اساس مقادیر order و FilterScope صورت می‌گیرد:
var filter = new Filter(actionFilter, FilterScope, order);

مقادیر FilterScope را در ادامه ملاحظه می‌نمائید:
namespace System.Web.Mvc { 
public enum FilterScope {
First = 0,
Global = 10,
Controller = 20,
Action = 30,
Last = 100,
}
}

به صورت پیش فرض، ابتدا فیلتری با محدوده اجرای کمتر، اجرا خواهد شد. در اینجا Global به معنای اجرای شدن در تمام کنترلرها است.


تعریف فیلترهای سراسری

برای اینکه فیلتری را عمومی و سراسری تعریف کنیم، تنها کافی است آن‌را در متد Application_Start فایل Global.asax.cs به نحو زیر معرفی نمائیم:

GobalFilters.Filters.Add(new AuthorizationFilterA() { Order = 2});

به این ترتیب AuthorizationFilterA، به تمام کنترلرها و متدهای قابل دسترسی آن‌ها در برنامه به صورت خودکار اعمال خواهد شد.
یکی از کاربردهای فیلترهای سراسری، نوشتن برنامه‌های پروفایلر است. برنامه‌هایی که برای مثال مدت زمان اجرای متدها را ثبت کرده و بر این اساس بهتر می‌توان کارآیی قسمت‌های مختلف برنامه را دقیقا زیرنظر قرار داد.


یک نکته
کلاس کنترلر در ASP.NET MVC نیز یک فیلتر است:
public abstract class Controller : ControllerBase, IActionFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter

به همین دلیل، امکان تحریف متدهای OnActionExecuting، OnActionExecuted و امثال آن که پیشتر ذکر شد، در یک کنترلر نیز وجود دارد.
کلاس کنترلر دارای محدوده اجرایی First و Order ایی مساوی Int32.MinValue است. به این ترتیب کنترلرها پیش از اجرای هر فیلتر دیگری اجرا خواهند شد.


ASP.NET MVC دارای یک سری فیلتر و متادیتای توکار مانند OutputCache، HandleError، RequireHttps، ValidateInpute و غیره است که توضیحات بیشتر آن‌ها به قسمت‌های بعد موکول می‌گردد.

مطالب
MVVM و الگوی ViewModel Locator

اگر ViewModel را همان فایل code behind عاری از ارجاعاتی به اشیاء بصری بدانیم، یک تفاوت مهم را علاوه بر مورد ذکر شده نسبت به Code behind متداول خواهد داشت: وهله سازی آن باید دستی انجام شود و خودکار نیست.
اگر به ابتدای کلاس‌های code behind‌ دقت کنید همیشه واژه‌ی partial قابل رویت است، به این معنا که این کلاس در حقیقت جزئی از همان کلاس متناظر با XAML ایی است که مشاهده می‌کنید؛ یا به عبارتی با آن یکی است. فقط جهت زیبایی یا مدیریت بهتر، در دو کلاس قرار گرفته‌اند اما واژه کلیدی partial این‌ها را نهایتا به صورت یکسان و یکپارچه‌ای به کامپایلر معرفی خواهد کرد. بنابراین وهله سازی code behind هم خودکار خواهد بود و به محض نمایش رابط کاربری،‌ فایل code behind آن هم وهله سازی می‌شود؛ چون اساسا و در پشت صحنه، از دیدگاه کامپایلر تفاوتی بین این دو وجود ندارد.

اکنون سؤال اینجا است که آیا می‌توان با ViewModel ها هم همین وهله سازی خودکار را به محض نمایش یک View متناظر، پیاده سازی کرد؟
البته صحیح آن این است که عنوان شود ViewModel متناظر با یک View و نه برعکس. چون روابط در الگوی MVVM از View به ViewModel به Model است و نه حالت عکس؛ مدل نمی‌داند که ViewModel ایی وجود دارد. ViewModel هم از وجود View ها در برنامه بی‌خبر است و این «بی‌خبری‌ها» اساس الگوهایی مانند MVC ، MVVM ، MVP‌ و غیره هستند. به همین جهت شاعر در وصف ViewModel فرموده‌اند که:

ای در درون برنامه‌ام و View از تو بی خبر_________وز تو برنامه‌ام پر است و برنامه از تو بی خبر :)

پاسخ:
بله. برای این منظور الگوی دیگری به نام ViewModel Locator طراحی شده است؛ روش‌های زیادی برای پیاده سازی این الگو وجود دارند که ساده‌ترین آن‌ها مورد زیر است:
فرض کنید ViewModel ساده زیر را قصد داریم به کمک الگوی ViewModel Locator به View ایی تزریق کنیم:

namespace WpfViewModelLocator.ViewModels
{
public class MainWindowViewModel
{
public string SomeText { set; get; }
public MainWindowViewModel()
{
SomeText = "Data ...";
}
}
}

برای این منظور ابتدا کلاس ViewModelLocatorBase زیر را تدارک خواهیم دید:

using WpfViewModelLocator.ViewModels;

namespace WpfViewModelLocator.ViewModelLocator
{
public class ViewModelLocatorBase
{
public MainWindowViewModel MainWindowVm
{
get { return new MainWindowViewModel(); }
}
}
}

در اینجا یک وهله از کلاس MainWindowViewModel توسط خاصیتی به نام MainWindowVm در دسترس قرار خواهد گرفت. برای اینکه بتوان این کلاس را در تمام Viewهای برنامه قابل دسترسی کنیم، آن‌را در App.Xaml تعریف خواهیم کرد:

<Application x:Class="WpfViewModelLocator.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vml="clr-namespace:WpfViewModelLocator.ViewModelLocator"
StartupUri="MainWindow.xaml">
<Application.Resources>
<vml:ViewModelLocatorBase x:Key="ViewModelLocatorBase" />
</Application.Resources>
</Application>

اکنون فقط کافی است در View خود DataContext را به نحو زیر مقدار دهی کنیم تا در زمان اجرا به صورت خودکار بتوان به خاصیت MainWindowVm یاد شده دسترسی یافت:

<Window x:Class="WpfViewModelLocator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid DataContext="{Binding Path=MainWindowVm, Source={StaticResource ViewModelLocatorBase}}">
<TextBlock Text="{Binding SomeText}" VerticalAlignment="Top" Margin="5" />
</Grid>
</Window>

در مورد ViewModel ها و Viewهای دیگر هم به همین ترتیب خواهد بود. یک وهله از آن‌ها به کلاس ViewModelLocatorBase اضافه می‌شود. سپس Binding Path مرتبط به DataContext به نام خاصیتی که در کلاس ViewModelLocatorBase مشخص خواهیم کرد، Bind خواهد شد.

روش دوم:
اگر در اینجا بخواهیم Path را حذف کنیم و فقط دسترسی عمومی به ViewModelLocatorBase را ذکر کنیم، باید یک Converter نوشت (چون به این ترتیب می‌توان به اطلاعات Binding در متد Convert دسترسی یافت). سپس یک قرار داد را هم تعریف خواهیم کرد؛ به این صورت که ما در Converter به نام View دسترسی پیدا می‌کنیم (از طریق ریفلکشن). سپس نام viewModel ایی را که باید به دنبال آن گشت مثلا ViewName به علاوه کلمه ViewModel در نظر خواهیم گرفت. در حقیقت یک نوع Convection over configuration است:

using System;
using System.Globalization;
using System.Linq;
using System.Windows.Data;

namespace WpfViewModelLocator.ViewModelLocator
{
public class ViewModelLocatorBaseConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
//مقدار در اینجا همان مشخصات ویوو است
if (value == null) return null;
string viewTypeName = value.GetType().Name;

//قرار داد ما است
//ViewModel Name = ViewName + "ViewModel"
string viewModelName = string.Concat(viewTypeName, "ViewModel");

//یافتن اسمبلی که حاوی ویوو مدل ما است
var asms = AppDomain.CurrentDomain.GetAssemblies();
var viewModelAsmName = "WpfViewModelLocator"; //نام پروژه مرتبط
var viewModelAsm = asms.Where(x => x.FullName.Contains(viewModelAsmName)).First();

//یافتن این کلاس ویوو مدل مرتبط
var viewModelType = viewModelAsm.GetTypes().Where(x => x.FullName.Contains(viewModelName)).FirstOrDefault();
if (viewModelType == null)
throw new InvalidOperationException(string.Format("Could not find view model '{0}'", viewModelName));

//وهله سازی خودکار آن
return Activator.CreateInstance(viewModelType);
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

کار این تبدیلگر بسیار ساده و واضح است. Value‌ دریافتی، وهله‌ای از view است. پس به این ترتیب می‌توان نام آن‌را یافت. سپس قرارداد ویژه خودمان را اعمال می‌کنیم به این ترتیب که ViewModel Name = ViewName + "ViewModel" و سپس به دنبال اسمبلی که حاوی این نام است خواهیم گشت. آن‌را یافته، کلاس مرتبط را در آن پیدا می‌کنیم و در آخر، به صورت خودکار آن‌را وهله سازی خواهیم کرد.
اینبار تعریف عمومی این Conveter در فایل App.Xaml به صورت زیر خواهد بود:

<Application x:Class="WpfViewModelLocator.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vml="clr-namespace:WpfViewModelLocator.ViewModelLocator"
StartupUri="MainWindow.xaml">
<Application.Resources>
<vml:ViewModelLocatorBaseConverter x:Key="ViewModelLocatorBaseConverter" />
</Application.Resources>
</Application>

و استفاده‌ی آن در تمام View های برنامه به شکل زیر می‌باشد (بدون نیاز به ذکر هیچ نام خاصی و بدون نیاز به کلاس ViewModelLocatorBase یاد شده در ابتدای مطلب):

<Window x:Class="WpfViewModelLocator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource ViewModelLocatorBaseConverter}}"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBlock Text="{Binding SomeText}" VerticalAlignment="Top" Margin="5" />
</Grid>
</Window>


مطالب
MVVM و رویدادگردانی

در دو قسمت قبل به اینجا رسیدیم که بجای شروع به کدنویسی مستقیم در code behind یک View (یک پنجره، یک user control ...)، کلاس مجزای دیگری را به نام ViewModel به برنامه اضافه خواهیم کرد و این کلاس از وجود هیچ فرمی در برنامه مطلع نیست.
بنابراین جهت انتقال رخدادها به ViewModel، بجای روش متداول تعریف روال‌های رخدادگردان در Code behind:
<Button  Click="btnClick_Event">Last</Button>

آن‌ها را با Commands به ViewModel ارسال خواهیم کرد:
<Button Command="{Binding GoLast}">Last</Button>  


همچنین بجای اینکه مستقیما بخواهیم از طریق نام یک شیء به مثلا خاصیت متنی آن دسترسی پیدا کنیم:
<TextBox Name="txtName" />  

از طریق Binding، اطلاعات مثلا متنی آن‌را به ViewModel منتقل خواهیم کرد:
<TextBox Text="{Binding Name}" />  


و همینجا است که 99 درصد آموزش‌های MVVM موجود در وب به پایان می‌رسند؛ البته پس از مشاهده 10 تا 20 ویدیو و خواندن بیشتر از 30 تا مقاله! و اینجا است که خواهید گفت: فقط همین؟! با این‌ها میشه یک برنامه رو مدیریت کرد؟!
البته همین‌ها برای مدیریت قسمت عمده‌ای از اکثر برنامه‌ها کفایت می‌کنند؛ اما خیلی از ریزه‌ کاری‌ها وجود دارند که به این سادگی‌ها قابل حل نیستند و در طی چند مقاله به آن‌ها خواهیم پرداخت.

سؤال: در همین مثال فوق، اگر متن ورودی در TextBox تغییر کرد، چگونه می‌توان بلافاصله از تغییرات آن در ViewModel مطلع شد؟ قدیم‌ترها می‌شد نوشت:
<TextBox TextChanged="TextBox_TextChanged" />


اما الان که قرار نیست در code behind کد بنویسیم (تا حد امکان البته)، باید چکار کرد؟
پاسخ: امکان Binding به TextChanged وجود ندارد، پس آن‌را فراموش می‌کنیم. اما همان Binding معمولی را به این صورت هم می‌شود نوشت (همان مثال قسمت قبل):
<TextBox Text="{Binding 
MainPageModelData.Name,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />


و نکته مهم آن UpdateSourceTrigger است. اگر روی حالت پیش فرض باشد، ViewModel پس از تغییر focus از این TextBox به کنترلی دیگر، از تغییرات آگاه خواهد شد. اگر آن‌را صریحا ذکر کرده و مساوی PropertyChanged قرار دهیم (این مورد در سیلورلایت 5 جدید است؛ هر چند از روز نخست WPF وجود داشته است)، با هر تغییری در محتوای TextBox، خاصیت MainPageModelData.Name به روز رسانی خواهد شد.
اگر هم بخواهیم این تغییرات آنی‌را در ViewModel تحت نظر قرار دهیم، می‌توان نوشت:

using System.ComponentModel;

namespace SL5Tests
{
public class MainPageViewModel
{
public MainPageModel MainPageModelData { set; get; }
public MainPageViewModel()
{
MainPageModelData = new MainPageModel();
MainPageModelData.Name = "Test1";
MainPageModelData.PropertyChanged += MainPageModelDataPropertyChanged;
}

void MainPageModelDataPropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "Name":
//do something
break;
}
}
}
}

تعریف MainPageModel را در قسمت قبل مشاهده کرده‌اید و این کلاس اینترفیس INotifyPropertyChanged را پیاده سازی می‌کند. بنابراین می‌توان از رویدادگردان PropertyChanged آن در ViewModel هم استفاده کرد.
به این ترتیب همان کار رودیدادگردان TextChanged را اینطرف هم می‌توان شبیه سازی کرد و تفاوتی نمی‌کند. البته با این تفاوت که در ViewModel فقط به اطلاعات به روز موجود در MainPageModelData.Name دسترسی داریم، اما نمی‌دانیم و نمی‌خواهیم هم بدانیم که منبع آن دقیقا کدام شیء رابط کاربری برنامه است.

سؤال: ما قبلا مثلا می‌توانستیم بررسی کنیم که اگر کاربر حین تایپ در یک TextBox بر روی دکمه‌ی Enter کلیک کرد، آن‌گاه برای نمونه، جستجویی بر اساس اطلاعات وارد شده صورت گیرد. الان این فشرده شدن دکمه‌ی Enter را چگونه دریافت و چگونه به ViewModel ارسال کنیم؟
این مورد کمی پیشرفته‌تر از حالت‌های قبلی است. برای حل این مساله ابتدا باید UpdateSourceTrigger یاد شده را مساوی Explicit قرار داد. یعنی اینبار می‌خواهیم نحوه ی به روز رسانی خاصیت MainPageModelData.Name را از طریق Binding خودمان مدیریت کنیم. این مدیریت کردن هم با استفاده از امکاناتی به نام Attached properties قابل انجام است که به آن‌ها Behaviors هم می‌گویند. مثلا:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace SL5Tests
{
public static class InputBindingsManager
{
public static readonly DependencyProperty UpdatePropertySourceWhenEnterPressedProperty
= DependencyProperty.RegisterAttached(
"UpdatePropertySourceWhenEnterPressed",
typeof(bool),
typeof(InputBindingsManager),
new PropertyMetadata(false, OnUpdatePropertySourceWhenEnterPressedPropertyChanged));

static InputBindingsManager()
{ }

public static void SetUpdatePropertySourceWhenEnterPressed(DependencyObject dp, bool value)
{
dp.SetValue(UpdatePropertySourceWhenEnterPressedProperty, value);
}

public static bool GetUpdatePropertySourceWhenEnterPressed(DependencyObject dp)
{
return (bool)dp.GetValue(UpdatePropertySourceWhenEnterPressedProperty);
}

private static void OnUpdatePropertySourceWhenEnterPressedPropertyChanged(DependencyObject dp,
DependencyPropertyChangedEventArgs e)
{
var txt = dp as TextBox;
if (txt == null)
return;

if ((bool)e.NewValue)
{
txt.KeyDown += HandlePreviewKeyDown;
}
else
{
txt.KeyDown -= HandlePreviewKeyDown;
}
}

static void HandlePreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;

var txt = sender as TextBox;
if (txt == null)
return;

var binding = txt.GetBindingExpression(TextBox.TextProperty);
if (binding == null) return;
binding.UpdateSource();
}
}
}

تعریف Attached properties یک قالب استاندارد دارد که آن را در کد فوق ملاحظه می‌کنید. یک تعریف به صورت static و سپس تعریف متدهای Get و Set آن. با تغییر مقدار آن که اینجا از نوع bool تعریف شده، متد OnUpdatePropertySourceWhenEnterPressedPropertyChanged به صورت خودکار فراخوانی می‌شود. اینجا است که ما از طریق آرگومان dp به textBox جاری دسترسی کاملی پیدا می‌کنیم. مثلا در اینجا بررسی شده که آیا کلید فشرده شده enter است یا خیر. اگر بله، یک سری فرامین را انجام بده. به عبارتی ما توانستیم، قطعه کدی را به درون شیءایی موجود تزریق کنیم. Txt تعریف شده در اینجا، واقعا همان کنترل TextBox ایی است که به آن متصل شده‌ایم.

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

<UserControl x:Class="SL5Tests.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:VM="clr-namespace:SL5Tests"
mc:Ignorable="d" Language="fa"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<VM:MainPageViewModel x:Name="vmMainPageViewModel" />
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainPageViewModel}}"
x:Name="LayoutRoot"
Background="White">
<TextBox Text="{Binding
MainPageModelData.Name,
Mode=TwoWay,
UpdateSourceTrigger=Explicit}"
VerticalAlignment="Top"
VM:InputBindingsManager.UpdatePropertySourceWhenEnterPressed="True" />
</Grid>
</UserControl>

همانطور که مشاهده می‌کنید، UpdateSourceTrigger به Explicit تنظیم شده و سپس InputBindingsManager.UpdatePropertySourceWhenEnterPressed به این کنترل متصل گردیده است. یعنی تنها زمانیکه در متد HandlePreviewKeyDown ذکر شده، متد UpdateSource فراخوانی گردد، خاصیت MainPageModelData.Name به روز رسانی خواهد شد (کنترل آن‌را خودمان در دست گرفته‌ایم نه حالت‌های از پیش تعریف شده).

این روش، روش متداولی است برای تبدیل اکثر حالاتی که Binding و Commanding متداول در مورد آن‌ها وجود ندارد. مثلا نیاز است focus را به آخرین سطر یک ListView از داخل ViewModel انتقال داد. در حالت متداول چنین امری میسر نیست، اما با تعریف یک Attached properties می‌توان به امکانات شیء ListView مورد نظر دسترسی یافت (به آن متصل شد، یا نوعی تزریق)، آخرین عنصر آن‌را یافته و سپس focus را به آن منتقل کرد یا به هر اندیسی مشخص که بعدا در ViewModel به این Behavior از طریق Binding ارسال خواهد شد.