مطالب
نوشتن افزونه برای مرورگرها: قسمت اول : کروم
افزونه چیست؟
افزونه‌ها جزء مهمترین قسمت‌های یک مرورگر توسعه پذیر به شمار می‌آیند. افزونه‌ها سعی دارند تا قابلیت هایی را به مرورگر شما اضافه کنند. افزونه‌ها از آخرین فناوری‌های html,CSS و جاوااسکریپت تا به آنجایی که مرورگر آن‌ها را پشتیبانی کند، استفاده می‌کنند.
در این سری سعی خواهیم کرد برای هر مرورگر شناخته شده، یک افزونه ایجاد کنیم و ابتدا از آنجا که خودم از کروم استفاده می‌کنم، اولین افزونه را برای کروم خواهم نوشت.

این افزونه قرار است چه کاری انجام دهد؟
کاری که برای این افزونه تدارک دیده‌ام این است: موقعی‌که سایت dotnettips.info به روز شد مرا آگاه کند. این آگاه سازی را از طریق یک نوتیفیکیشن به اطلاع کاربر میرسانیم. صفحه تنظیمات این افزونه شامل گزینه‌های "آخرین مطالب"،"نظرات آخرین مطالب"،"آخرین اشتراک ها"و"آخرین نظرات اشتراک ها" خواهد بود که به طور پیش فرض تنها گزینه اول فعال خواهد بود و همچنین یک گزینه نیز برای وارد کردن یک عدد صحیح جهت اینکه به افزونه بگوییم هر چند دقیقه یکبار سایت را چک کن. چک کردن سایت هم از طریق فید RSS صورت می‌گیرد.

فایل manifest.json
این فایل برای ذخیره سازی اطلاعاتی در مورد افزونه به کار می‌رود که شامل نام افزونه، توضیح کوتاه در مورد افزونه و ورژن و ... به کار می‌رود که همه این اطلاعات در قالب یا فرمت json نوشته می‌شوند و در بالاترین حد استفاده برای تعریف اهداف افزونه و اعطای مجوز به افزونه از آن استفاده می‌کنیم. این فایل بخش‌های زیر را در یک افزونه تعریف می‌کند که به مرور با آن آشنا می‌شویم.


کد زیر را در فایل manifest.json می‌نویسیم:
{
  "manifest_version": 2,

  "name": "Dotnettips Updater",
  "description": "This extension keeps you updated on current activities on dotnettips.info",
  "version": "1.0",
  "icons": { "16": "icon.png",
           "48": "icon.png",
          "128": "icon.png" },

  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "activeTab",
    "https://www.dntips.ir"
  ]
}
اطلاعات اولیه شامل نام و توضیح و ورژن افزونه است. ورژن برنامه برای به روزآوری افزونه بسیار مهم است. موقعی که ورژن جدیدی از افزونه ارائه شود، گوگل وب استور اعلان آپدیت جدیدی را برای افزونه میکند. آیکن قسمت‌های مختلف افزونه هم با icons مشخص می‌شود که در سه اندازه باید ارائه شوند و البته اگه اندازه آن نباشد scale می‌شود. قسمت بعدی تعریف UI برنامه هست که گوگل کروم، به آن Browser Action می‌گوید. در اینجا یک آیکن و همچنین یک صفحه اختصاصی برای تنظیمات افزونه معرفی می‌کنیم. این آیکن کنار نوار آدرس نمایش داده می‌شود و صفحه popup موقعی نشان داده می‌شود که کاربر روی آن کلیک می‌کند. آیکن‌ها برای browser action در دو اندازه 19 و 38 پیکسلی هستند و در صورتی که تنها یک آیکن تعریف شود، به صورت خودکار عمل scale و تغییر اندازه صورت می‌گیرد. برای تعیین عکس برای هر اندازه می‌توانید کد را به صورت زیر بنویسید:
"default_icon": {                    // optional
            "19": "images/icon19.png",           // optional
            "38": "images/icon38.png"            // optional
          }
قسمت popup برای نمایش تنظیمات به کار می‌رود و درست کردن این صفحه همانند صفحه همیشگی html هست و خروجی آن روی پنجره popup افزونه رندر خواهد شد.
گزینه default_title نیز یکی از دیگر خصیصه‌های مهم و پرکاربرد این قسمت هست که متن tooltip می‌باشد و موقعی که که کاربر، اشاره‌گر را روی آیکن ببرد نمایش داده می‌شود و در صورتی که نوشته نشود، کروم نام افزونه را نمایش می‌دهد؛ برای همین ما هم چیزی ننوشتیم.

صفحات پس‌زمینه
اگر بخواهید برای صفحه popup کد جاوااسکریت بنویسید یا از jquery استفاده کنید، مانند هر صفحه‌ی وبی که درست می‌کنید آن را کنار فایل popup قرار داده و در popup آنها را صدا کرده و از آن‌ها استفاده کنید. ولی برای پردازش هایی که نیاز به UI وجود ندارد، می‌توان از صفحات پس زمینه استفاده کرد. در این حالت ما دو نوع صفحه داریم:
  1. صفحات مصر یا Persistent Page
  2. صفحات رویدادگرا یا Events Pages
اولین نوع صفحه، همواره فعال و در حال اجراست و دومی موقعی فعال می‌شود که به استفاده از آن نیاز است. گوگل توصیه می‌کند که  تا جای ممکن از نوع دوم استفاده شود تا  مقدار حافظه مصرفی  حفظ شود  و کارآیی مروگر بهبود بخشیده شود. کد زیر یک صفحه پس زمینه را از نوع رویدادگرا می‌سازد. به وضوح روشن است در صورتی که خاصیت Persistent با true مقداردهی شود، این صفحه مصرانه در تمام وقت باز بودن مرورگر، فعال خواهد بود:
"background": {
    "scripts": ["background.js"],
    "persistent": false
}

Content Script یا اسکریپت محتوا
در صورتی که بخواهید با هر صفحه‌ای که باز یا رفرش می‌شود، به DOM آن دسترسی پیدا کنید، از این خصوصیت استفاده کنید. در کد زیر برای پردازش اطلاعات DOM از فایل جاوااسکریپت بهره برده و در قسمت matches می‌گویید که چه صفحاتی باید از این کد استفاده کنند که در اینجا از پروتکل‌های HTTP استفاده میشود و اگر مثلا نوع FTP یا file صدا زده شود کد مورد نظر اجرا نخواهد شد. در مورد اینکه matches چگونه کار می‌کند و چگونه می‌توان آن را نوشت، از این صفحه استفاده کنید.
"content_scripts": [
    {
        "matches": ["http://*/*", "https://*/*"],
        "js": ["content.js"]
    }
]

آغاز کدنویسی (رابط‌های کاربری)


اجازه دهید بقیه موارد را در حین کدنویسی تجربه کنیم و هر آنچه ماند را بعدا توضیح خواهیم داد. در اینجا من از یک صفحه با کد HTML زیر بهره برده ام که یک فرم دارد به همراه چهار چک باکس و در نهایت یک دکمه جهت ذخیره مقادیر. نام صفحه را popup.htm گذاشته ام و یک فایل popup.js هم دارم که در آن کد jquery نوشتم. قصد من این است که بتوان یک action browser به شکل زیر درست کنم:


کد html آن به شرح زیر است:
<html>
<head>
<meta charset="utf-8"/>

<script src="jquery.min.js"></script> <!-- Including jQuery -->
<script type="text/javascript" src="popup.js"></script>
</head>
<body style="direction:rtl;width:250px;">
<form >
<input type="checkbox" id="chkarticles" value="" checked="true">آخرین مطالب سایت</input><br/>
<input type="checkbox" id="chkarticlescomments" value="" >آخرین نظرات مطالب سایت</input><br/>
<input type="checkbox" id="chkshares" value="" >آخرین اشتراک‌های سایت</input><br/>
<input type="checkbox"  id="chksharescomments" value="" >آخرین نظرات اشتراک‌های سایت</input><br/>
<input id="btnsave" type="button" value="ذخیره تغییرات" />
    <div id="messageboard" style="color:green;"></div>
</form>

</body>
</html>
کد popup.js هم به شرح زیر است:
$(document).ready(function () {
    $("#btnsave").click(function() {
        var articles = $("#chkarticles").is(':checked');
        var articlesComments = $("#chkarticlescomments").is(':checked');
        var shares = $("#chkshares").is(':checked');
        var sharesComments = $("#chksharescomments").is(':checked');

        chrome.storage.local.set({ 'articles': articles, 'articlesComments': articlesComments, 'shares': shares, 'sharesComments': sharesComments }, function() {
            $("#messageboard").text( 'تنظیمات جدید اعمال شد');
        });
    });
});
در کد بالا موقعی که کاربر بر روی دکمه ذخیره، کلیک کند رویداد کلیک jquery فعال شده و مقادیر چک باکس‌ها را در متغیرهای مربوطه نگهداری می‌کند. نهایتا با استفاده از کلمه کلیدی کروم به ناحیه ذخیره سازی داده‌های کروم دست پیدا کرده و درخواست ذخیره مقادیر چک باکس را بر اساس ساختار نام و مقدار، ذخیره می‌کنیم و بعد از اعمال، توسط یک تابع callback به کاربر اعلام می‌کنیم که اطلاعات ذخیره شده است.
اولین مورد جدیدی که در بالا دیدیم، کلمه‌ی کلیدی chrome است. کروم برای توسعه دهندگانی که قصد نوشتن افزونه دارند api هایی را تدارک دیده است که میتوانید با استفاده از آنها به قسمت‌های مختلف مرورگر مثل بوک مارک یا تاریخچه فعالیت‌های مرورگر و ... دست پیدا کنید. البته برای اینکار باید در فایل manifest.json هم مجوز اینکار را درخواست نماییم. این ویژگی باید برای برنامه نویسان اندروید آشنا باشد. برای آشنایی هر چه بیشتر با مجوزها این صفحه را ببینید.
برای دریافت مجوز، کد زیر را به manifest اضافه می‌کنیم:
"permissions": [
    "storage"
  ]
مجوزی که در بالا درخواست کرده‌ایم مجوز دسترسی به ناحیه ذخیره سازی است. بعد از کلمه کلیدی chrome، کلمه‌ی local آمده است و می‌گوید که باید داده‌ها به صورت محلی و لوکال ذخیره شوند ولی اگر میخواهید داده‌ها در گوگل سینک شوند، باید به جای لوکال از کلمه کلیدی sync استفاده کنید یعنی:
chrome.storage.sync.set
فایل manifest نهایی:
{
  "manifest_version": 2,

  "name": "Dotnettips Updater",
  "description": "This extension keeps you updated on current activities on dotnettips.info",
  "version": "1.0",

  "browser_action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },
  "permissions": [
    "storage"
  ]
}
الان باید 4 فایل داشته باشید: فایل آیکن، popup.htm,popup.js و manifest.json. همه را داخل یک دایرکتوری قرار داده و در مروگر کروم به قسمت extensions بروید و گزینه Developer mode را فعال کنید تا یک تستی از کد نوشته شده بگیریم. گزینه Load Unpacked Extension را بزنید و آدرس دایرکتوری ایجاد شده را به آن بدهید.
chrome://extensions

الان باید مانند تصویر بالا یک آیکن کنار نوار آدرس یا به قول گوگل، Omni box ببینید. گزینه‌ها را تیک بزنید و روی دکمه ذخیره کلیک کنید. باید پیام مقادیر ذخیره شدند، نمایش پیدا کند. الان یک مشکل وجود دارد؛ داده‌ها ذخیره می‌شوند ولی موقعی که دوباره تنظیمات افزونه را باز کنید حالت اولیه نمایش داده میشود. پس باید تنظیمات ذخیره شده را خوانده و به آن‌ها اعمال کنیم. کد زیر را جهت دریافت مقادیر ذخیره شده می‌نویسیم. اینبار به جای استفاده از متد set از متد get استفاده می‌کنیم. به صورت آرایه، رشته نام مقادیر را درخواست می‌کنیم و در تابع callback، مقادیر به صورت آرایه برای ما برگشت داده می‌شوند.
    chrome.storage.local.get(['articles', 'articlesComments', 'shares', 'sharesComments'], function ( items) {
        console.log(items[0]);
        $("#chkarticles").attr("checked", items["articles"]);
        $("#chkarticlescomments").attr("checked", items["articlesComments"]);
        $("#chkshares").attr("checked", items["shares"]);
        $("#chksharescomments").attr("checked", items["sharesComments"]);

    });
حالا برای اینکه افزونه‌ی شما متوجه تغییرات شود، به تب extensions رفته و در لیست افزونه‌ها به دنبال افزونه خود بگردید و گزینه Reload را انتخاب نمایید تا افزونه تغییرات را متوجه شود و صفحه را تست کنید.

Page Action
روش دیگر برای ارائه یک رابط کاربری، page action هست. این روش دقیقا مانند روش قبلی است، ولی جای آیکن عوض می‌شود. قبلا بیرون از نوار آدرس بود، ولی الان داخل نوار آدرس قرار می‌گیرد. جالب‌ترین نکته در این مورد این است که این آیکن در ابتدا مخفی شده است و شما تصمیم می‌گیرید که این آیکن چه موقع نمایش داده شود. مثلا آیکن RSS تنها موقعی نمایش داده می‌شود که وب سایتی که باز شده است، دارای محتوای RSS باشد یا بوک مارک کردن یک آدرس برای همه‌ی سایت‌ها باز باشد و سایر موارد.
کد زیر نحوه‌ی تعریف یک page action را در manifest نشان می‌دهد. ما در این مثال یک page action را به طور موقت اضافه می‌کنیم و موقعی هم آن را نشان میدهیم که سایت dotnettips.info باز باشد. دلیل اینکه موقت اضافه می‌کنیم این است که باید یکی از دو گزینه رابط کاربری که تا به حال گفتیم، استفاده شود. در غیر این صورت کروم در هنگام خواندن فایل manifest در هنگام افزودن افزونه به مرورگر، پیام خطا خواهد داد و این مطلب را به شما گوشزد می‌کند. پس نمی‌توان دو گزینه را همزمان داشت و من میخواهم افزونه را در حالت browser action ارائه کنم. پس در پروژه نهایی، این مطلب page action نخواهد بود. برای داشتن یک page action کد زیر را در manifest بنویسید.
  "page_action": {
    "default_icon": {
        "19": "images/icon19.png",
        "38": "images/icon38.png"
    },
    "default_popup": "popup.html"
گزینه page action تعریف شد حالا باید کاری کنیم تا هر موقع صفحه‌ای باز می‌شود چک کند آیا سایت مورد نظر است یا خیر، اینکار را توسط صفحه‌ی پردازشی انجام می‌دهیم. پس تکه کد زیر را هم به manifest اضافه می‌کنیم:
"background": {
    "scripts": ["page_action_validator.js"]
}

تا اینجا فایل جاوااسکریپت معرفی شد که کد زیر را دارد و در پس زمینه شروع به اجرا می‌کند.
function UrlValidation(tabId, changeInfo, tab) {
if (tab.url.indexOf('dotnettips.info') >-1) {
chrome.pageAction.show(tabId);
}
};
chrome.tabs.onUpdated.addListener(UrlValidation);
چون از api در این کد بهره برده‌ایم و آن هم مدیریت بر روی تب هاست، پس باید مجوز آن هم گرفته شود. کلمه "tabs" را در قسمت permissions اضافه کنید.
یک listener برای tabها ایجاد کرده‌ایم که اگر تب جدید ایجاد شد، یا تب قبلی به آدرس جدیدی تغییر پیدا کرد تابع UrlValidation را اجرا کند و در این تابع چک می‌کنیم که اگر url این تب شامل نام وب سایت می‌شود، page action روی این تب ظاهر شود. پس از انجام تغییرات، مجددا افزونه را بارگذاری می‌کنیم و تغییرات اعمال شده را می‌بینیم. سایت dotnettips را باز کنید یا صفحه را مجددا رفرش کنید تا تغییر اعمال شده را ببینید.

تغییرات موقت را حذف و کدها را به حالت قبلی یعنی browser action بر میگردانم.

OmniBox
omnibox یک کلمه کلیدی است که در نوار آدرس مرورگر وارد می‌شود و در واقع می‌توانیم آن را نوع دیگری از رابط کاربری بنامیم. موقعی که شما کلمه کلیدی رزرو شده را وارد می‌کنید، در نوار آدرس کلماتی نشان داده میشود که کاربر میتواند یکی از آن‌ها را انتخاب کند تا عملی انجام شود. ما هم قرار است این کار را انجام دهیم. به این مثال دقت کنید:
میخواهیم موقعی که کاربرکلمه net. را تایپ می‌کند، 5 عبارت آخرین مطالب و آخرین اشتراک‌ها و آخرین نظرات مطالب و آخرین نظرات اشتراک‌ها و صفحه اصلی سایت نمایش داده شود و با انتخاب هر کدام، کاربر به سمت آن صفحه هدایت شود.
برای افزودن کلمه کلیدی در manifest خطوط زیر را اضافه کنید:
"omnibox": { "keyword" : ".net" }
با نوشتن خط بالا کلمه net. در مرورگر یک کلمه‌ی کلیدی به حساب خواهد آمد و موقعی که کاربر این کلمه را وارد کند، در سمت راست نوشته خواهد شد. در این حالت باید کلید تب را بزند تا به محیط دستوری آن برود.

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

 
 برای اینکار باید کدنویسی کنیم ، پس یک فایل پس زمینه را به manifest معرفی کنید:
"background": {
    "scripts": ["omnibox.js"]
در فایل ominbox.js دستوراتی که مرتبط با omnibox است را می‌نویسیم و کد زیر را به آن اضافه می‌کنیم:
chrome.omnibox.onInputChanged.addListener(function(text, suggest) {
    suggest([
  {content: ".net tips Home Page", description: "صفحه اصلی"},
      {content: ".net tips Posts", description: "آخرین مطالب"},
      {content: ".net tips News", description: "آخرین نظرات مطالب"},
      {content: ".net tips Post Comments", description: "آخرین اشتراک ها"},
      {content: ".net tips News Comments", description: "آخرین نظرات اشتراک ها"}
    ]);
});
chrome.omnibox شامل 4 رویداد می‌شود:
 onInputStarted   بعد از اینکه کاربر کلمه کلیدی را وارد کرد اجرا می‌شود
onInputChanged 
  بعد از وارد کردن کلمه کلیدی هربار که کاربر تغییری در ورودی نوارد آدرس می‌دهد اجرا می‌شود.
 onInputEntered   کاربر ورودی خود را تایید می‌کند. مثلا بعد از وارد کردن، کلید enter را می‌فشارد
 onInputCancelled  کاربر از وارد کردن ورودی منصرف شده است؛ مثلا کلید ESC را فشرده است. 
با نوشتن chrome.omnibox.onInputChanged.addListener ما یک listener ساخته‌ایم تا هر بار کاربر ورودی را تغییر داد، یک تابع callback که دو آرگومان را دارد، صدا بزند. این آرگومان‌ها یکی متن ورودی‌است و دیگری آرایه‌ی suggest که شما با تغییر آرایه می‌توانید عباراتی که همزمان با تایپ به کاربر پیشنهاد می‌شود را نشان دهید. البته می‌توانید با تغییر کد کاری کنید تا بر اساس حروفی که تا به حال تایپ کرده‌اید، دستورات را نشان دهد؛ ولی من به دلیل اینکه 5 دستور بیشتر نبود و کاربر راحت باشد، چنین کاری نکردم. همچنین وقتی شما برای هر یک description تعریف کنید، به جای نام پیشنهادی، توضیح آن را نمایش می‌دهد.
حالا وقت این است که کد زیر را جهت اینکه اگر کاربر یکی از کلمات پیشنهادی را انتخاب کرد، به صفحه‌ی مورد نظر هدایت شود، اضافه کنیم:
chrome.omnibox.onInputEntered.addListener(function (text) {

var location="";
    switch(text)
{
case ".net tips Posts":
location="https://www.dntips.ir/postsarchive";
break;
case ".net tips News":
location="https://www.dntips.ir/newsarchive";
break;
case ".net tips Post Comments":
location="https://www.dntips.ir/commentsarchive";
break;
case".net tips News Comments":
location="https://www.dntips.ir/newsarchive/comments";
break;
default:
location="https://www.dntips.ir/";
}

    chrome.tabs.getSelected(null, function (tab) {
        chrome.tabs.update(tab.id, { url: location });
    });
});
ابتدا یک listener برای روی رویداد onInputEntered قرار داده تا وقتی کاربر عبارت وارد شده را تایید کرد، اجرا شود. در مرحله بعد چک می‌کنیم که عبارت وارد شده چیست و به ازای هر عبارت مشخص شده، آدرس آن صفحه را در متغیر location قرار می‌دهیم. در نهایت با استفاده از عبارت chrome.tabs.getSelected تب انتخابی را به یک تابع callback بر میگردانیم. اولین آرگومان windowId است، برای زمانی که چند پنجره کروم باز است که می‌توانید وارد نکنید تا پنجره فعلی و تب فعلی محسوب شود. برای همین نال رد کردیم. در تابع برگشتی، شیء tab شامل اطلاعات کاملی از آن تب مانند url و id و title می‌باشد و در نهایت با استفاده از دستور chrome.tabs.update اطلاعات تب را به روز می‌کنیم. آرگومان اول id تب را میدهیم تا بداند کدام تب باید تغییر کند و آرگومان بعدی می‌توانید هر یک از ویژگی‌های تب از قبیل آدرس فعلی یا عنوان آن و ... را تغییر دهید که ما آدرس آن را تغییر داده ایم.

Context Menu
یکی دیگر از رابط‌های کاربری، منوی کانتکست هست که توسط chrome.contextmenus ارائه می‌شود و به مجوز "contextmenus" نیاز دارد. فعال سازی منوی کانتکست در قسمت‌های زیر ممکن است:
all, page, frame, selection, link, editable, image, video, audio 
من گزینه‌ی dotenettips.info را برای باز کردن سایت، به Contextmenus اضافه می‌کنم. کد را در فایلی به اسم contextmenus.js ایجاد می‌کنم و در قسمت background آن را معرفی می‌کنم. برای باز کردن یک تب جدید برای سایت، نیاز به chrome.tabs داریم که البته  نیاز به مجوز tabs هم داریم.
محتوای فایل contextmenus.js
var root = chrome.contextMenus.create({
    title: 'Open .net tips',
    contexts: ['page']
}, function () {
    var Home= chrome.contextMenus.create({
        title: 'Home',
        contexts: ['page'],
        parentId: root,
        onclick: function (evt) {
            chrome.tabs.create({ url: 'https://www.dntips.ir' })
        }
    });
var Posts = chrome.contextMenus.create({
        title: 'Posts',
        contexts: ['page'],
        parentId: root,
        onclick: function (evt) {
            chrome.tabs.create({ url: 'https://www.dntips.ir/postsarchive/' })
        }
    });
});
در کد بالا یک گزینه به context menu اضافه میشود و دو زیر منو هم دارد که یکی صفحه‌ی اصلی سایت را باز میکند و دیگری هم صفحه‌ی مطالب سایت را باز می‌کند.

تا به اینجا ما قسمت ظاهری کار را آماده کرده ایم و به دلیل اینکه مطلب طولانی نشود، این مطلب را در دو قسمت ارائه خواهیم کرد. در قسمت بعدی نحوه خواندن RSS و اطلاع رسانی و دیگر موارد را بررسی خواهیم کرد.
مطالب
Markup Extensions در XAML
Markup Extension‌ها برای مواردی استفاده می‌شوند که قرار است مقداری غیر از یک مقدار ثابت و یک نوع قابل شناسایی در XAML برای یک value تنظیم شود. تمام مواردی در XAML که درون {} قرا می‌گیرند همان Markup Extension‌ها هستند. مانند Binding و یا StaticResoiurces.
علاوه بر Markup Extension‌های از پیش تعریف شده در XAML، می‌توان Markup Extension‌های شخصی را نیز تولید کرد. در واقع به زبان ساده‌تر Markup Extension برای تولید ساده‌ی داده‌های پیچیده در XAML استفاده می‌شوند.

لازم به ذکر است کهMarkup Extension ‌ها می‌توانند به دو صورت Attribute Usage ،درون  {} :
 "{Binding path=something,Mode=TwoWay}”
و یا Property Element Usage (همانند سایر Element هایWPF) درون <> استفاده شوند:
 <Binding Path="Something" Mode="TwoWay"></Binding>
برای تعریف یک Markup Extension، یک کلاس ایجاد می‌کنیم که از Markup Extensions ارث بری می‌کند. این کلاس یک Abstract Method به نام  ProvideValue دارد که باید پیاده سازی  شود. این متد مقدار خصوصیتی که Markup Extensions را فراخوانی کرده به صورت یک Object بر می‌گرداند که یکبار در زمان Load برای خصوصیت مربوطه‌اش تنظیم می‌شود.
 public abstract Object ProvideValue(IServiceProvider serviceProvider)
همانطورکه ملاحظه می‌کنید ProvideValue یک پارامتر IServiceProvider دارد که ازطریق آن می‌توان به IProvideValueTarget دسترسی داشت. ازاین Interface برای گرفتن اطلاعات کنترل(TargetObject) و خصوصیتی (TargetProperty) که فراخوانی را انجام داده در صورت لزوم استفاده می‌شود.
var target = serviceProviderGetService(typeof(IProvideValueTarget))as IProvideValueTarget;
var host = targetTargetObject as FrameworkElement;
Markup Extension‌ها می‌توانند پارامتر‌های ورودی داشته باشند:
public class ValueExtension : MarkupExtension
{
  public ValueExtension () { }
  public ValueExtension (object value1)
  {
    Value1 = value1;
  }
   public object Value1 { get; set; }
   public override object ProvideValue(IServiceProvider serviceProvider)
   {
     return Value1;
   }
}
و برای استفاده در فایل Xaml:
 <TextBox  Text="{app:ValueExtension test}" ></TextBox>
و یا می‌توان خصوصیت هایی ایجاد کرد و  از آنها برای ارسال مقادیر به آن استفاده کرد:
  <TextBox  Text="{app:ValueExtension Value1=test}" ></TextBox>
تا اینجا موارد کلی برای تعریف و استفاده از Markup Extensions گفته شد. در ادامه یک مثال کاربردی می‌آوریم. برای مثال در نظر بگیرید که نیاز دارید DataType مربوط به یک DataTemplate را برابر یک کلاس Generic قرار بدهید:
public class EntityBase
{
   public int Id{get;set}
}

public class MyGenericClass<TType> where TType : EntityBase
{
   public int Id{get;set}
   public string Test{  get;set; }

In XAML:

<DataTemplate DataType="{app:GenericType ؟}">
برای انجام این کار یک Markup Extensions به صورت زیر ایجاد می‌کنیم که Type را به عنوان ورودی گرفته و یک نمونه از کلاس Generic ایجاد می‌کند:
public class GenericType : MarkupExtension
{
  private readonly Type _of;
  public GenericType(Type of)
  {
     _of = of;
  }
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
      return typeof(MyGenericClass<>)MakeGenericType(_of);
 }
}
و برای استفاده از آن یک نمونه از MarkupExtension ایجاد شده ایجاد کرده و نوع Generic را برای آن ارسال می‌کنیم:
 <DataTemplate DataType="{app:GenericType app:EntityBase}">
این یک مثال ساده از استفاده از Markup Extensions است. هنگام کار با WPF می‌توان استفاده‌های زیادی از این مفهوم داشت، برای مثال زمانی که نیاز است ItemsSource یک  Combobox  از Description‌های یک Enum پر شود می‌توان به راحتی با نوشتن یک Markup Extensions ساده این عمل و کارهای مشابه زیادی را انجام داد.  
مطالب
ASP.NET MVC #14

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

ممکن است یک سری از اصطلاحات را در قسمت‌های قبل مانند master page در لابلای توضیحات ارائه شده، مشاهده کرده باشید. این نوع مفاهیم برای برنامه نویس‌های ASP.NET Web forms آشنا است (و اگر با Web forms view engine‌ در ASP.NET MVC کار کنید، دقیقا یکی است؛ البته با این تفاوت که فایل code behind آن‌ها حذف شده است). به همین جهت در این قسمت برای تکمیل بحث، مروری خواهیم داشت بر نحوه‌ی معرفی جدید آن‌ها توسط Razor.
در یک پروژه جدید ASP.NET MVC و در پوشه Views\Shared\_Layout.cshtml آن، فایل Layout آن،‌ مفهوم master page را دارد. در این نوع فایل‌ها، زیر ساخت مشترک تمام صفحات سایت قرار می‌گیرند:

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

<body>
@RenderBody()
</body>
</html>

اگر دقت کرده باشید، در هیچکدام از فایل‌های Viewایی که تا این قسمت به پروژه‌های مختلف اضافه کردیم، تگ‌هایی مانند body، title و امثال آن وجود نداشتند. در ASP.NET مرسوم است کلیه اطلاعات تکراری صفحات مختلف سایت را مانند تگ‌های یاد شده به همراه منویی که باید در تمام صفحات قرار گیرد یا footer‌ مشترک بین تمام صفحات سایت، به یک فایل اصلی به نام master page که در اینجا layout نام گرفته، Refactor کنند. به این ترتیب حجم کدها و markup تکراری که باید در تمام Viewهای سایت قرار گیرند به حداقل خواهد رسید.
برای مثال محل قرار گیری تعاریف Content-Type تمام صفحات و همچنین favicon سایت، بهتر است در فایل layout باشد و نه در تک تک Viewهای تعریف شده:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="shortcut icon" href="@Url.Content("~/favicon.ico")" type="image/x-icon" />


در کدهای فوق یک نمونه پیش فرض فایل layout را مشاهده می‌کنید. در اینجا توسط متد RenderBody، محتوای رندر شده یک View درخواستی، به داخل تگ body تزریق خواهد شد.
تا اینجا در تمام مثال‌های قبلی این سری، فایل layout در Viewهای اضافه شده معرفی نشد. اما اگر برنامه را اجرا کنیم باز هم به نظر می‌رسد که فایل layout اعمال شده است. علت این است که در صورت عدم تعریف صریح layout در یک View، این تعریف از فایل Views\_ViewStart.cshtml دریافت می‌گردد:

@{
Layout = "~/Views/Shared/_Layout.cshtml";
}

فایل ViewStart، محل تعریف کدهای تکراری است که باید پیش از اجرای هر View مقدار دهی یا اجرا شوند. برای مثال در اینجا می‌شود بر اساس نوع مرورگر،‌ layout خاصی را به تمام Viewها اعمال کرد. مثلا یک layout‌ ویژه برای مرورگرهای موبایل‌ها و layout ایی دیگر برای مرورگرهای معمولی. امکان دسترسی به متغیرهای تعریف شده در یک View در فایل ViewStart از طریق ViewContext.ViewData میسر است.
ضمن اینکه باید درنظر داشت که می‌توان فایل ViewStart را در زیر پوشه‌های پوشه اصلی View نیز قرار داد. مثلا اگر فایل ViewStart ایی در پوشه Views/Home قرار گرفت، این فایل محتوای ViewStart اصلی قرار گرفته در ریشه پوشه Views را بازنویسی خواهد کرد.
برای معرفی صریح فایل layout، تنها کافی است مسیر کامل فایل layout را در یک View مشخص کنیم:

@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Index</h2>

اهمیت این مساله هم در اینجا است که یک سایت می‌تواند چندین layout یا master page داشته باشد. برای نمونه یک layout برای صفحات ورود اطلاعات؛ یک layout خاص هم مثلا برای صفحات گزارش گیری نهایی سایت.
همانطور که پیشتر نیز ذکر شد، در ASP.NET حرف ~ به معنای ریشه سایت است که در اینجا ابتدای محل جستجوی فایل layout را مشخص می‌کند.
به این ترتیب زمانیکه یک کنترلر، View خاصی را فراخوانی می‌کند، کار از فایل Views\Shared\_Layout.cshtml شروع خواهد شد. سپس View درخواستی پردازش شده و محتوای نهایی آن، جایی که متد RenderBody قرار دارد، تزریق خواهد شد.
همچنین مقدار ViewBag.Title ایی که در فایل View تعریف شده، در فایل layout جهت رندر مقدار تگ title استفاده می‌شود (انتقال یک متغیر از View به یک فایل master page؛ کلاس layout، مدل View ایی را که قرار است رندر کند به ارث می‌برد).

یک نکته:
در نگارش سوم ASP.NET MVC امکان بکارگیری حرف ~ به صورت مستقیم در حین تعریف یک فایل js یا css وجود ندارد و حتما باید از متد سمت سرور Url.Content کمک گرفت. در نگارش چهارم ASP.NET MVC، این محدودیت برطرف شده و دقیقا همانند متغیر Layout ایی که در بالا مشاهده می‌کنید، می‌توان بدون نیاز به متد Url.Content، مستقیما از حرف ~ کمک گرفت و به صورت خودکار پردازش خواهد شد.


تزریق نواحی ویژه یک View در فایل layout

توسط متد RenderBody، کل محتوای View درخواستی در موقعیت تعریف شده آن در فایل Layout، رندر می‌شود. این ویژگی به نحو یکسانی به تمام Viewها اعمال می‌شود. اما اگر نیاز باشد تا view بتواند محتوای markup قسمت ویژه‌ای از master page را مقدار دهی کند، می‌توان از مفهومی به نام Sections استفاده کرد:
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
<div id="menu">
@if (IsSectionDefined("Menu"))
{
RenderSection("Menu", required: false);
}
else
{
<span>This is the default ...!</span>
}
</div>
<div id="body">
@RenderBody()
</div>
</body>
</html>

در اینجا ابتدا بررسی می‌شود که آیا قسمتی به نام Menu در View جاری که باید رندر شود وجود دارد یا خیر. اگر بله، توسط متد RenderSection، آن قسمت نمایش داده خواهد شد. در غیراینصورت، محتوای پیش فرضی را در صفحه قرار می‌دهد. البته اگر از متد RenderSection با آرگومان required: false استفاده شود، درصورتیکه View جاری حاوی قسمتی به نام مثلا menu نباشد، تنها چیزی نمایش داده نخواهد شد. اگر این آرگومان را حذف کنیم، یک استثنای عدم یافت شدن ناحیه یا قسمت مورد نظر صادر می‌گردد.
نحوه‌ی تعریف یک Section در Viewهای برنامه به شکل زیر است:
@{
ViewBag.Title = "Index";
//Layout = null;
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>
Index</h2>
@section Menu{
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
}

برای مثال فرض کنید که یکی از Viewهای شما نیاز به دو فایل اضافی جاوا اسکریپت برای اجرای صحیح خود دارد. می‌توان تعاریف الحاق این دو فایل را در قسمت header فایل layout قرار داد تا در تمام Viewها به صورت خودکار لحاظ شوند. یا اینکه یک section سفارشی را به نحو زیر در آن View خاص تعریف می‌کنیم:

@section JavaScript
{
<script type="text/javascript" src="@Url.Content("~/Scripts/SomeScript.js")" />;
<script type="text/javascript" src="@Url.Content("~/Scripts/AnotherScript.js")" />;
}

سپس کافی است جهت تزریق این کدها به header تعریف شده در master page مورد نظر، یک سطر زیر را اضافه کرد:

@RenderSection("JavaScript", required: false)

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



مدیریت بهتر فایل‌ها و پوشه‌های یک برنامه ASP.NET MVC به کمک Areas

به کمک قابلیتی به نام Areas می‌توان یک برنامه بزرگ را به چندین قسمت کوچکتر تقسیم کرد. هر کدام از این نواحی، دارای تعاریف مسیریابی، کنترلرها و Viewهای خاص خودشان هستند. به این ترتیب دیگر به یک برنامه‌ی از کنترل خارج شده ASP.NET MVC که دارای یک پوشه Views به همراه صدها زیر پوشه است، نخواهیم رسید و کنترل این نوع برنامه‌های بزرگ ساده‌تر خواهد شد.
برای مثال یک برنامه بزرگ را درنظر بگیرید که به کمک قابلیت Areas، به نواحی ویژه‌ای مانند گزارشگیری، قسمت ویژه مدیریتی، قسمت کاربران، ناحیه بلاگ سایت، ناحیه انجمن سایت و غیره، تقسیم شده است. به علاوه هر کدام از این نواحی نیز هنوز می‌توانند از اطلاعات ناحیه اصلی برنامه مانند master page آن استفاده کنند. البته باید درنظر داشت که فایل viewStart به پوشه جاری و زیر پوشه‌های آن اعمال می‌شود. اگر نیاز باشد تا اطلاعات این فایل به کل برنامه اعمال شود، فقط کافی است آن‌را به یک سطح بالاتر، یعنی ریشه سایت منتقل کرد.


نحوه افزودن نواحی جدید
افزودن یک Area جدید هم بسیار ساده است. بر روی نام پروژه در VS.NET کلیک راست کرده و سپس گزینه Add|Area را انتخاب کنید. سپس در صفحه باز شده، نام دلخواهی را وارد نمائید. مثلا نام Reporting را وارد نمائید تا ناحیه گزارشگیری برنامه از قسمت‌های دیگر آن مستقل شود. پس از افزودن یک Area جدید، به صورت خودکار پوشه جدیدی به نام Areas به ریشه سایت اضافه می‌شود. سپس داخل آن، پوشه‌ی دیگری به نام Reporting اضافه خواهد شد. پوشه reporting اضافه شده هم دارای پوشه‌های Model، Views و Controllers خاص خود می‌باشد.
اکنون که پوشه Areas به ریشه سایت اضافه شده است، با کلیک راست بر روی این پوشه نیز گزینه‌ی Add|Area در دسترس می‌باشد. برای نمونه یک ناحیه جدید دیگر را به نام Admin به سایت اضافه کنید تا بتوان امکانات مدیریتی سایت را از سایر قسمت‌های آن مستقل کرد.


نحوه معرفی تعاریف مسیریابی نواحی تعریف شده
پس از اینکه کار با Areas را آغاز کردیم، نیاز است تا با نحوه‌ی مسیریابی آن‌ها نیز آشنا شویم. برای این منظور فایل Global.asax.cs قرار گرفته در ریشه اصلی برنامه را باز کنید. در متد Application_Start، متدی به نام AreaRegistration.RegisterAllAreas، کار ثبت و معرفی تمام نواحی ثبت شده را به فریم ورک، به عهده دارد. کاری که در پشت صحنه انجام خواهد شد این است که به کمک Reflection تمام کلاس‌های مشتق شده از کلاس پایه AreaRegistration به صورت خودکار یافت شده و پردازش خواهند شد. این کلاس‌ها هم به صورت پیش فرض به نام SomeNameAreaRegistration.cs در ریشه اصلی هر Area توسط VS.NET تولید می‌شوند. برای نمونه فایل ReportingAreaRegistration.cs تولید شده، حاوی اطلاعات زیر است:

using System.Web.Mvc;

namespace MvcApplication11.Areas.Reporting
{
public class ReportingAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Reporting";
}
}

public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Reporting_default",
"Reporting/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}

توسط AreaName، یک نام منحصربفرد در اختیار فریم ورک قرار خواهد گرفت. همچنین از این نام برای ایجاد پیوند بین نواحی مختلف نیز استفاده می‌شود.
سپس در قسمت RegisterArea، یک مسیریابی ویژه خاص ناحیه جاری مشخص گردیده است. برای مثال تمام آدرس‌های ناحیه گزارشگیری سایت باید با http://localhost/reporting آغاز شوند تا مورد پردازش قرارگیرند. سایر مباحث آن هم مانند قبل است. برای مثال در اینجا نام اکشن متد پیش فرض، index تعریف شده و همچنین ذکر قسمت id نیز اختیاری است.
همانطور که ملاحظه می‌کنید، تعاریف مسیریابی و اطلاعات پیش فرض آن منطقی هستند و آنچنان نیازی به دستکاری و تغییر ندارند. البته اگر دقت کرده باشید مقدار نام controller پیش فرض، مشخص نشده است. بنابراین بد نیست که مثلا نام Home یا هر نام مورد نظر دیگری را به عنوان نام کنترلر پیش فرض در اینجا اضافه کرد.


تعاریف کنترلر‌های هم نام در نواحی مختلف
در ادامه مثال جاری که دو ناحیه Admin و Reporting به آن اضافه شده، به پوشه‌های Controllers هر کدام، یک کنترلر جدید را به نام HomeController اضافه کنید. همچنین این HomeController را در ناحیه اصلی و ریشه سایت نیز اضافه نمائید. سپس برای متد پیش فرض Index هر کدام هم یک View جدید را با کلیک راست بر روی نام متد و انتخاب گزینه Add view، اضافه کنید. اکنون برنامه را به همین نحو اجرا نمائید. اجرای برنامه با خطای زیر متوقف خواهد شد:

Multiple types were found that match the controller named 'Home'. This can happen if the route that services this
request ('{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request.
If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.

The request for 'Home' has found the following matching controllers:
MvcApplication11.Areas.Admin.Controllers.HomeController
MvcApplication11.Controllers.HomeController

فوق العاده خطای کاملی است و راه حل را هم ارائه داده است! برای اینکه مشکل ابهام یافتن HomeController برطرف شود، باید این جستجو را به فضاهای نام هر قسمت از نواحی برنامه محدود کرد (چون به صورت پیش فرض فضای نامی برای آن مشخص نشده، کل ناحیه ریشه سایت و زیر مجموعه‌های آن‌را جستجو خواهد کرد). به همین جهت فایل Global.asax.cs را گشوده و متد RegisterRoutes آن‌را مثلا به نحو زیر اصلاح نمائید:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
, namespaces: new[] { "MvcApplication11.Controllers" }
);
}

آرگومان چهارم معرفی شده، آرایه‌ای از نام‌های فضاهای نام مورد نظر را جهت یافتن کنترلرهایی که باید توسط این مسیریابی یافت شوند، تعریف می‌کند.
اکنون اگر مجددا برنامه را اجرا کنیم، بدون مشکل View متناظر با متد Index کنترلر Home نمایش داده خواهد شد.
البته این مشکل با نواحی ویژه و غیر اصلی سایت وجود ندارد؛ چون جستجوی پیش فرض کنترلرها بر اساس ناحیه است.
در ادامه مسیر http://localhost/Admin/Home را نیز در مرورگر وارد کنید. سپس بر روی صفحه در مروگر کلیک راست کرده و سورس صفحه را بررسی کنید. مشاهده خواهید کرد که master page یا فایل layout ایی به آن اعمال نشده است. علت را هم در ابتدای بحث Areas مطالعه کردید. فایل Views\_ViewStart.cshtml در سطحی که قرار دارد به ناحیه Admin اعمال نمی‌شود. آن‌را به ریشه سایت منتقل کنید تا layout اصلی سایت نیز به این قسمت اعمال گردد. البته بدیهی است که هر ناحیه می‌تواند layout خاص خودش را داشته باشد یا حتی می‌توان با مقدار دهی خاصیت Layout نیز در هر view، فایل master page ویژه‌ای را انتخاب و معرفی کرد.


نحوه ایجاد پیوند بین نواحی مختلف سایت
زمانیکه پیوندی را به شکل زیر تعریف می‌کنیم:
@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home")

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

@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home",
routeValues: new { Area = "Admin" } , htmlAttributes: null)


همین نکته را باید حین کار با متد RedirectToAction نیز درنظر داشت:
public ActionResult Index()
{
return RedirectToAction("Index", "Home", new { Area = "Admin" });
}


مطالب
آشنایی با KnownTypeAttribute در WCF
تشریح مسئله : KnownTypeAttribute چیست و چگونه از آن استفاده کنیم؟
پیش نیاز : آشنایی اولیه با مفاهیم WCF برای فهم بهتر مطالب
در ابتدا یک WCf Service Application ایجاد کنید  و مدل زیر را بسازید:
 [DataContract] 
    public abstract class Person
    {
        [DataMember]
        public int Code { get; set; }

        [DataMember]
        public string Name { get; set; }     
}
یک کلاس پایه برای Person ایجاد کردیم به صورت abstract که وهله سازی از آن میسر نباشد و 2 کلاس دیگر می‌سازیم که از کلاس بالا ارث ببرند:
کلاس #1
 [DataContract]  
    public class Student : Person
    {
        [DataMember]
        public int StudentId { get; set; }
    }
کلاس #2
[DataContract]
    public class Teacher : Person
    {
        public int TeacherId { get; set; }
    }
فرض کنید قصد داریم سرویسی ایجاد کنیم که لیست تمام اشخاص موجود در سیستم را در اختیار ما قرار دهد. (هم Student و هم Teacher). ابتدا Contract مربوطه را به صورت زیر تعریف می‌کنیم:
[ServiceContract]
    public interface IStudentService
    {
        [OperationContract]
        IEnumerable<Person> GetAll();
    }
همان طور که می‌بینید خروجی متد GetAll  از نوع Person است (نوع پایه کلاس Student , Teacher) . سرویس مربوطه بدین شکل خواهد شد.
 public class StudentService : IStudentService
    {
        public IEnumerable<Person> GetAll()
        {
            List<Person> listOfPerson = new List<Person>();

            listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Masoud Pakdel" } );
            listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Mostafa Asgari"} );
            listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Saeed Alizadeh"} );          
listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Mahdi Rad"} ); listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Mohammad Heydari" } ); listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Saeed Khatami"} ); return listOfPerson; }
در این سرویس در متد GetAll لیستی از تمام اشخاص رو ایجاد می‌کنیم . 3 تا Student و 3 تا Teacher رو به این لیست اضافه میکنیم. برای نمایش اطلاعات در خروجی یک پروژه Console Application ایجاد کنید و سرویس بالا رو از روش AddServiceReference به پروژه اضافه کنید سپس در کلاس Program کد‌های زیر رو کپی کنید.
 class Program
    {
        static void Main( string[] args )
        {
            StudentService.StudentServiceClient client = new StudentService.StudentServiceClient();

            client.GetAll().ToList().ForEach( _record => 
            {
                Console.Write( "Name : {0}", _record.Name );
                Console.WriteLine( "Code : {0}", _record.Code );
            } );

            Console.ReadLine();
        }
    }
پروژه رو کامپایل کنید. تا اینجا هیچ گونه مشکلی مشاهده نشد و انتظار داریم که خروجی مورد نظر رو مشاهده کنیم. بعد از اجرای پروژه با خطای زیر متوقف می‌شویم:

مشکل از اینجا ناشی می‌شود که هنگام عمل سریالایز ، WCF Runtime با توجه به وهله سازی از کلاس Person می‌دونه که باید کلاس Student یا Teacher رو سریالایز کنه ولی در هنگام عمل دی سریالایز، WCF Runtime این موضوع رو درک نمی‌کنه به همین دلیل یک Communication Exception پرتاب می‌کنه. برای حل این مشکل و برای اینکه WCF Deserialize Engine  رو متوجه نوع وهله سازی کلاس‌های مشتق شده از کلاس پایه کنیم باید از KnownTypeAttribute استفاده کنیم. فقط کافیست که این Attribute رو بالای کلاس Person به ازای تمام کلاس‌های مشتق شده از اون قرار بدید.بدین صورت:

   [DataContract]
    [KnownType( typeof( Student ) )]
    [KnownType( typeof(Teacher) )]
    public abstract class Person
    {
        [DataMember]
        public int Code { get; set; }

        [DataMember]
        public string Name { get; set; }
    }
حالا پروژه سمت سرور رو دوباره کامپایل کنید و سرویس سمت کلاینت رو Update کنید. بعد پروژه رو دوباره اجرا کرده تا خروجی زیر رو مشاهده کنید.


با وجود KnownType دیگه WCF Deserialize Engine میدونه که باید از کدام DataContact برای عمل دی سریالاز نمونه ساخته شده از کلاس Person استفاده کنه. دانستن این مطلب هنگام پیاده سازی مفاهیم ارث بری در ORM ها زمانی که از WCF استفاده می‌کنیم ضروری است.
مطالب دوره‌ها
نگاهی به SignalR Clients
در قسمت قبل موفق به ایجاد اولین Hub خود شدیم. در ادامه، برای تکمیل برنامه نیاز است تا کلاینتی را نیز برای آن تهیه کنیم.
مصرف کنندگان یک Hub می‌توانند انواع و اقسام برنامه‌های کلاینت مانند jQuery Clients و یا حتی یک برنامه کنسول ساده باشند و همچنین Hubهای دیگر نیز قابلیت استفاده از این امکانات Hubهای موجود را دارند. تیم SignalR امکان استفاده از Hubهای آن‌را در برنامه‌های دات نت 4 به بعد، برنامه‌های WinRT، ویندوز فون 8، سیلورلایت 5، jQuery و همچنین برنامه‌های CPP نیز مهیا کرده‌اند. به علاوه گروه‌های مختلف نیز با توجه به سورس باز بودن این مجموعه، کلاینت‌های iOS Native، iOS via Mono و Android via Mono را نیز به این لیست اضافه کرده‌اند.


بررسی کلاینت‌های jQuery

با توجه به پروتکل مبتنی بر JSON سیگنال‌آر، استفاده از آن در کتابخانه‌های جاوا اسکریپتی همانند jQuery نیز به سادگی مهیا است. برای نصب آن نیاز است در کنسول پاور شل نوگت، دستور زیر را صادر کنید:
 PM> Install-Package Microsoft.AspNet.SignalR.JS
برای نمونه به solution پروژه قبل، یک برنامه وب خالی دیگر را اضافه کرده و سپس دستور فوق را بر روی آن اجرا نمائید. در این حالت فقط باید دقت داشت که فرامین بر روی کدام پروژه اجرا می‌شوند:


با استفاده از افزونه SignalR jQuery، به دو طریق می‌توان به یک Hub اتصال برقرار کرد:
الف) استفاده از فایل proxy تولید شده آن (این فایل، در زمان اجرای برنامه تولید می‌شود و یا امکان استفاده از آن به کمک ابزارهای کمکی نیز وجود دارد)
نمونه‌ای از آن‌را در قسمت قبل ملاحظه کردید؛ همان فایل تولید شده در مسیر /signalr/hubs برنامه. به نوعی به آن Service contract نیز گفته می‌شود (ارائه متادیتا و قراردادهای کار با یک سرویس Hub). این فایل همانطور که عنوان شد به صورت پویا در زمان اجرای برنامه ایجاد می‌شود.
امکان تولید آن توسط برنامه کمکی signalr.exe نیز وجود دارد؛ برای دریافت آن می‌توان از طریق NuGet اقدام کرد (بسته Microsoft.AspNet.SignalR.Utils) که نهایتا در پوشه packages قرار خواهد گرفت. نحوه استفاده از آن نیز به صورت زیر است:
 Signalr.exe ghp http://localhost/
در این دستور ghp مخفف generate hub proxy است و نهایتا فایلی را به نام server.js تولید می‌کند.

ب) بدون استفاده از فایل proxy و به کمک روش late binding (انقیاد دیر هنگام)

برای کار با یک Hub از طریق jQuery مراحل ذیل باید طی شوند:
1) ارجاعی به Hub باید مشخص شود.
2) روال‌های رخدادگردان تنظیم گردند.
3) اتصال به Hub برقرار گردد.
4) متدی فراخوانی شود.

در اینجا باید دقت داشت که امکانات Hub به صورت خواص
 $.connection
در سمت کلاینت جی‌کوئری، در دسترس خواهند بود. برای مثال:
 $.connection.chatHub
و نام‌های بکارگرفته شده در اینجا مطابق روش‌های متداول نام گذاری در جاوا اسکریپت، camel case هستند.

خوب، تا اینجا فرض بر این است که یک پروژه خالی ASP.NET را آغاز و سپس فرمان نصب Microsoft.AspNet.SignalR.JS را نیز همانطور که عنوان شد، صادر کرده‌اید. در ادامه یک فایل ساده html را به نام chat.htm، به این پروژه جدید اضافه کنید (برای استفاده از کتابخانه جاوا اسکریپتی SignalR الزامی به استفاده از صفحات کامل پروژه‌های وب نیست).
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
    <script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {
            var chat;
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند
            chat = $.connection.chat; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است
            chat.client.hello = function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            };
            $.connection.hub.start(/*{ transport: 'longPolling' }*/); // فاز اولیه ارتباط را آغاز می‌کند

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                chat.server.sendMessage($("#txtMsg").val());
            });
        });
    </script>
</body>
</html>
کدهای آن‌را به نحو فوق تغییر دهید.
توضیحات:
همانطور که ملاحظه می‌کنید ابتدا ارجاعاتی به jquery و jquery.signalR-1.0.1.min.js اضافه شده‌اند. سپس نیاز است مسیر دقیق فایل پروکسی هاب خود را نیز مشخص کنیم. اینکار با تعریف مسیر signalr/hubs انجام شده است.
<script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
در ادامه توسط تنظیم connection.hub.logging سبب خواهیم شد تا اطلاعات بیشتری در javascript console مرورگر لاگ شود.
سپس ارجاعی به هاب تعریف شده، تعریف گردیده است. اگر از قسمت قبل به خاطر داشته باشید، توسط ویژگی HubName، نام chat را برگزیدیم. بنابراین connection.chat ذکر شده دقیقا به این هاب اشاره می‌کند.
سپس سطر chat.client.hello مقدار دهی شده است. متد hello، متدی dynamic و تعریف شده در سمت هاب برنامه است. به این ترتیب می‌توان به پیام‌های رسیده از طرف سرور گوش فرا داد. در اینجا، این پیام‌ها، به li ایی با id مساوی messages اضافه می‌شوند.
سپس توسط فراخوانی متد connection.hub.start، فاز negotiation شروع می‌شود. در اینجا حتی می‌توان نوع transport را نیز صریحا انتخاب کرد که نمونه‌ای از آن را به صورت کامنت شده جهت آشنایی با نحوه تعریف آن مشاهده می‌کنید. مقادیر قابل استفاده در آن به شرح زیر هستند:
 - webSockets
- forverFrame
- serverSentEvents
- longPolling
سپس به رویدادهای کلیک دکمه send گوش فرا داده و در این حین، اطلاعات TextBox ایی با id مساوی txtMsg را به متد SendMessage هاب خود ارسال می‌کنیم. همانطور که پیشتر نیز عنوان شد، در سمت کلاینت، تعریف متد SendMessage باید camel case باشد.

اکنون به صورت جداگانه یکبار برنامه hub را در مرورگر باز کنید. سپس بر روی فایل chat.htm کلیک راست کرده و گزینه مشاهده آن را در مرورگر نیز انتخاب نمائید (گزینه View in browser منوی کلیک راست).

خوب! پروژه کار نمی‌کند! برای اینکه مشکلات را بهتر بتوانید مشاهده کنید نیاز است به JavaScript Console مرورگر خود مراجعه نمائید. برای مثال در مرورگر کروم دکمه F12 را فشرده و برگه Console آن‌را باز کنید. در اینجا اعلام می‌کند که فاز negotiation قابل انجام نیست؛ چون مسیر پیش فرضی را که انتخاب کرده است، همین مسیر پروژه دومی است که اضافه کرده‌ایم (کلاینت ما در پروژه دوم قرار دارد و نه در همان پروژه اول هاب).
برای اینکه مسیر دقیق hub را در این حالت مشخص کنیم، سطر زیر را به ابتدای کدهای جاوا اسکریپتی فوق اضافه نمائید:
 $.connection.hub.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
اکنون اگر مجدا سعی کنید، باز هم برنامه کار نمی‌کند و پیام می‌دهد که امکان دسترسی به این سرویس از خارج از دومین آن میسر نیست. برای اینکه این مجوز را صادر کنیم نیاز است تنظیمات مسیریابی پروژه هاب را به نحو ذیل ویرایش نمائیم:
using System;
using System.Web;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;

namespace SignalR02
{
    public class Global : HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs(new HubConfiguration
            {
                EnableCrossDomain = true
            });
        }
    }
}
با تنظیم EnableCrossDomain به true اینبار فاز‌های آغاز ارتباط با سرور برقرار می‌شوند:
 SignalR: Auto detected cross domain url. jquery.signalR-1.0.1.min.js:10
SignalR: Negotiating with 'http://localhost:1072/signalr/negotiate'. jquery.signalR-1.0.1.min.js:10
SignalR: SignalR: Initializing long polling connection with server. jquery.signalR-1.0.1.min.js:10
SignalR: Attempting to connect to 'http://localhost:1072/signalr/connect?transport=longPolling&connectionToken…NRh72omzsPkKqhKw2&connectionData=%5B%7B%22name%22%3A%22chat%22%7D%5D&tid=3' using longPolling. jquery.signalR-1.0.1.min.js:10
SignalR: Longpolling connected jquery.signalR-1.0.1.min.js:10
مطابق این لاگ‌ها ابتدا فاز negotiation انجام می‌شود. سپس حالت long polling را به صورت خودکار انتخاب می‌کند.

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

یک نکته:
اگر از ویندوز 8 (یعنی IIS8) و VS 2012 استفاده می‌کنید، برای استفاده از حالت Web socket، ابتدا فایل وب کانفیگ برنامه را باز کرده و در قسمت httpRunTime، مقدار ویژگی targetFramework را بر روی 4.5 تنظیم کنید. اینبار اگر مراحل negotiation را بررسی کنید در همان مرحله اول برقراری اتصال، از روش Web socket استفاده گردیده است.


تمرین 1
به پروژه ساده و ابتدایی فوق یک تکست باکس دیگر به نام Room را اضافه کنید؛ به همراه دکمه join. سپس نکات قسمت قبل را در مورد الحاق به یک گروه و سپس ارسال پیام به اعضای گروه را پیاده سازی نمائید. (تمام نکات آن با مطلب فوق پوشش داده شده است و در اینجا باید صرفا فراخوانی متدهای عمومی دیگری در سمت هاب، صورت گیرد)

تمرین 2
در انتهای قسمت دوم به نحوه ارسال پیام از یک هاب به هابی دیگر اشاره شد. این MonitorHub را ایجاد کرده و همچنین یک کلاینت جاوا اسکریپتی را نیز برای آن تهیه کنید تا بتوان اتصال و قطع اتصال کلیه کاربران سیستم را مانیتور و مشاهده کرد.


پیاده سازی کلاینت jQuery بدون استفاده از کلاس Proxy

در مثال قبل، از پروکسی پویای مهیای در آدرس signalr/hubs استفاده کردیم. در اینجا قصد داریم، بدون استفاده از آن نیز کار برپایی کلاینت را بررسی کنیم.
بنابراین یک فایل جدید html را مثلا به نام chat_np.html به پروژه دوم برنامه اضافه کنید. سپس محتویات آن‌را به نحو زیر تغییر دهید:
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {                        
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند

            var connection = $.hubConnection();
            connection.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
            var proxy = connection.createHubProxy('chat');

            proxy.on('hello', function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            });

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                proxy.invoke('sendMessage', $("#txtMsg").val());
            });

            connection.start();
        });
    </script>
</body>
</html>
در اینجا سطر مرتبط با تعریف مسیر اسکریپت‌های پویای signalr/hubs را دیگر در ابتدای فایل مشاهده نمی‌کنید. کار تشکیل proxy اینبار از طریق کدنویسی صورت گرفته است. پس از ایجاد پروکسی، برای گوش فرا دادن به متدهای فراخوانی شده از طرف سرور از متد proxy.on و نام متد فراخوانی شده سمت سرور استفاده می‌کنیم و یا برای ارسال اطلاعات به سرور از متد proxy.invoke به همراه نام متد سمت سرور استفاده خواهد شد.


کلاینت‌های دات نتی SignalR
تا کنون Solution ما حاوی یک پروژه Hub و یک پروژه وب کلاینت جی‌کوئری است. به همین Solution، یک پروژه کلاینت کنسول ویندوزی را نیز اضافه کنید.
سپس در خط فرمان پاور شل نوگت دستور زیر را صادر نمائید تا فایل‌های مورد نیاز به پروژه کنسول اضافه شوند:
 PM> Install-Package Microsoft.AspNet.SignalR.Client
در اینجا نیز باید دقت داشت تا دستور بر روی default project صحیحی اجرا شود (حالت پیش فرض، اولین پروژه موجود در solution است).
پس از نصب آن اگر به پوشه packages مراجعه کنید، نگارش‌های مختلف آن‌را مخصوص سیلورلایت، دات نت‌های 4 و 4.5، WinRT و ویندوز فون8 نیز می‌توانید در پوشه Microsoft.AspNet.SignalR.Client ملاحظه نمائید. البته در ابتدای نصب، انتخاب نگارش مناسب، بر اساس نوع پروژه جاری به صورت خودکار صورت می‌گیرد.
مدل برنامه نویسی آن نیز بسیار شبیه است به حالت عدم استفاده از پروکسی در حین استفاده از jQuery که در قسمت قبل بررسی گردید و شامل این مراحل است:
1) یک وهله از شیء HubConnection را ایجاد کنید.
2) پروکسی مورد نیاز را جهت اتصال به Hub از طریق متد CreateProxy تهیه کنید.
3) رویدادگردان‌ها را همانند نمونه کدهای جاوا اسکریپتی قسمت قبل، توسط متد On تعریف کنید.
4) به کمک متد Start، اتصال را آغاز نمائید.
5) متدها را به کمک متد Invoke فراخوانی نمائید.

using System;
using Microsoft.AspNet.SignalR.Client.Hubs;

namespace SignalR02.WinClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var hubConnection = new HubConnection(url: "http://localhost:1072/signalr");
            var chat = hubConnection.CreateHubProxy(hubName: "chat");

            chat.On<string>("hello", msg => {
                Console.WriteLine(msg);
            });

            hubConnection.Start().Wait();

            chat.Invoke<string>("sendMessage", "Hello!");

            Console.WriteLine("Press a key to terminate the client...");
            Console.Read();
        }
    }
}
نمونه‌ای از این پیاده سازی را در کدهای فوق ملاحظه می‌کنید که از لحاظ طراحی آنچنان تفاوتی با نمونه ذهنی جاوا اسکریپتی ندارد.

نکته مهم
کلیه فراخوانی‌هایی که در اینجا ملاحظه می‌کنید غیرهمزمان هستند.
به همین جهت پس از متد Start، متد Wait ذکر شده‌است تا در این برنامه ساده، پس از برقراری کامل اتصال، کار invoke صورت گیرد و یا زمانیکه callback تعریف شده توسط متد chat.On فراخوانی می‌شود نیز این فراخوانی غیرهمزمان است و خصوصا اگر نیاز است رابط کاربری برنامه را در این بین به روز کنید باید به نکات به روز رسانی رابط کاربری از طریق یک ترد دیگر دقت داشت.
مطالب
PowerShell 7.x - قسمت ششم - ایجاد Cmdletها توسط #C
تا اینجا با کمک توابع توانستیم PowerShell را به اصطلاح extend کنیم. نوع دیگر دستورات، command letها هستند. این نوع دستورات را با کمک یک زبان دات‌نتی میتوانیم ایجاد کنیم. به این نوع دستورات complied cmdlet گفته میشود. در بیشتر مواقع با کمک advanced functionها میتوانید بیشتر کارها را انجام دهید؛ چراکه به صورت مستقیم امکان استفاده از دات‌نت را درون PowerShell دارید. اما شاید ترجیح دهید از سی‌شارپ یا دیگر زبان‌ها دات‌نتی برای ایجاد یک تابع استفاده کنید.

نحوه‌ی ایجاد یک cmdlet با کمک #C
ابتدا یک دایرکتوری جدید را ایجاد کرده و درون آن یک پروژه‌ی از نوع class library را ایجاد کنید. سپس پکیج PowerShellStandard.Library را درون پروژه ایجاد شده با کمک dotnet cli به پروژه اضافه کنید: 
mkdir ps_cmdlet_with_csharp && cd "$_"
dotnet new classlib
dotnet add package PowerShellStandard.Library
mv Class1.cs GetHelloCommand.cs
در پایان دستورات فوق، نام فایل پیش‌فرض Class1 را نیز به GetHelloCommand تغییر داده‌ایم. در ادامه محتویات فایل را اینگونه ویرایش خواهیم کرد: 
namespace ps_cmdlet_with_csharp;
using System.Management.Automation;

[Cmdlet(VerbsCommon.Get, "Hello")]
public class GetHelloCommand : PSCmdlet
{
    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
    public string Name { get; set; }
    protected override void BeginProcessing()
    {
        WriteObject("Start processing");
    }
    protected override void ProcessRecord()
    {
        WriteObject("Hello " + Name);
    }
    protected override void EndProcessing()
    {
        WriteObject("End processing");
    }
}
همانطور که مشاهده میکنید، کلاس فوق را از PSCmdlet ارث‌بری کرده‌ایم. در کل برای ایجاد یک command let، از یکی از انواع Cmdlet یا PSCmdlet میتوانیم ارث‌بری کنیم. Cmdlet یک کلاس پایه است که PSCmdlet نیز از آن ارث برده است و یکسری امکانات بیشتری را جهت تعامل با PowerShell ارائه میدهد: 
using System.Collections.ObjectModel;
using System.Management.Automation.Host;

namespace System.Management.Automation
{
    public abstract class PSCmdlet : Cmdlet
    {
        protected PSCmdlet();

        public PSEventManager Events { get; }
        public PSHost Host { get; }
        public CommandInvocationIntrinsics InvokeCommand { get; }
        public ProviderIntrinsics InvokeProvider { get; }
        public JobManager JobManager { get; }
        public JobRepository JobRepository { get; }
        public InvocationInfo MyInvocation { get; }
        public PagingParameters PagingParameters { get; }
        public string ParameterSetName { get; }
        public SessionState SessionState { get; }

        public PathInfo CurrentProviderLocation(string providerId);
        public Collection<string> GetResolvedProviderPathFromPSPath(string path, out ProviderInfo provider);
        public string GetUnresolvedProviderPathFromPSPath(string path);
        public object GetVariableValue(string name);
        public object GetVariableValue(string name, object defaultValue);
    }
}
در نهایت command let باید به یک DLL تبدیل شود؛ چون همانطور که قبلآً نیز اشاره شد، هر command let در واقع یک شیء دات‌نتی است. در ادامه پروژه جاری را بیلد کرده و توسط دستور Import-Module فایل DLL تولید شده را درون Shell ایمپورت خواهیم کرد: 
PS /> dotnet build
PS /> Import-Module ./bin/Debug/net7.0/ps_cmdlet_with_csharp.dll
سپس توسط دستور Get-Command میتوانیم مطمئن شویم که ماژول موردنظر با موفقیت ایمپورت شده‌است: 
PS /> Get-Command -Module ps_cmdlet_with_csharp

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Cmdlet          Get-Hello                                          1.0.0.0    ps_cmdlet_with_csharp
همانطور که مشاهده میکنید، درون ماژول ps_cmdlet_with_csharp تنها یک command let تعریف شده‌است. درون یک namespace میتوانیم چندین command let را تعریف کنیم. به عنوان مثال برای مثال قبل میتوانیم: 
namespace ps_cmdlet_with_csharp;
using System.Management.Automation;

[Cmdlet(VerbsCommon.Get, "Hello")]
public class GetHelloCommand : PSCmdlet
{
    // as before
}

[Cmdlet(VerbsCommon.Get, "Greetings")]
public class GetGreetingsCommand : PSCmdlet
{
    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
    public string Name { get; set; }
    protected override void BeginProcessing()
    {
        WriteObject("Start processing");
    }
    protected override void ProcessRecord()
    {
        WriteObject("Greetings " + Name);
    }
    protected override void EndProcessing()
    {
        WriteObject("End processing");
    }
}
اکنون اگر پروژه را بیلد کنیم و ماژول بیلد شده را ایمپورت کنیم، با خطای زیر مواجه خواهیم شد: 
Import-Module: Invalid assembly public key.
برای رفع این مشکل، فایل csproject را باز کرده و خط زیر را به آن اضافه کنید: 
<Project Sdk="Microsoft.NET.Sdk">

  <!-- other tags -->

  <ItemGroup>
    <Compile Include="./GetHelloCommand.cs" />
  </ItemGroup>

</Project>
اکنون پروژه با موفقیت بیلد و ایمپورت خواهد شد: 
PS /> Get-Command -Module ps_cmdlet_with_csharp

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Cmdlet          Get-Greetings                                      1.0.0.0    ps_cmdlet_with_csharp
Cmdlet          Get-Hello                                          1.0.0.0    ps_cmdlet_with_csharp

یک مثال تکمیلی
درون یک کلاس Cmdlet، امکان استفاده از تمامی annotationهایی را که در قسمت قبل بررسی کردیم، اینجا نیز در اختیار داریم؛ بنابراین نیاز به توضیح مجدد آن نیست. در ادامه میخواهیم یک دستور را با عنوان Push-SlackMessage تهیه کنیم که کار ارسال یک پیام را به یک کانال Slack، انجام میدهد: 
namespace ps_cmdlet_with_csharp;
using System.Management.Automation;
using System.Net.Http.Headers;


[Cmdlet(VerbsCommon.Push, "SlackMessage")]
[Alias("ssm")]
[OutputType(typeof(string))]
public class SlackMessageCommand : PSCmdlet
{
    [Parameter(Mandatory = true)]
    [Alias("m")]
    public string Message { get; set; }

    [Parameter(Mandatory = true)]
    [Alias("t")]
    [ValidatePattern(@"xoxp-[0-9]{11}-[0-9]{12}-[0-9]{13}-[0-9a-zA-Z]{32}")]
    public string Token { get; set; }

    [Parameter(Mandatory = true)]
    [Alias("cid")]
    public string ChannelID { get; set; }
    protected async override void ProcessRecord()
    {
        base.ProcessRecord();
        try
        {
            using var client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", this.Token);
            var values = new Dictionary<string, string>
            {
                { "channel", this.ChannelID },
                { "text", this.Message }
            };
            var content = new FormUrlEncodedContent(values);
            var response = await client.PostAsync("https://slack.com/api/chat.postMessage", content);
            var responseString = await response.Content.ReadAsStringAsync();
            WriteObject("Message sent");
        }
        catch (Exception ex)
        {
            WriteObject(ex.Message);
        }
    }
}

یک نکته در مورد ایمپورت کردن ماژول‌ها
دستوراتی که تاکنون ایجاد کردیم (هم توابع و هم compiled cmletها) برای اجرا باید یکبار درون حافظه قرار بگیرند و سپس امکان اجرای آنها را خواهیم داشت. دلیل آن نیز این است که همه چیز درون سشن جاری انجام خواهد شد و به محض بستن آن، تغییرات نیز ار حافظه خارج خواهند شد. یعنی برای command letی که ایجاد کردیم، با هربار باز کردن یک سشن جدید مجبور خواهیم بود که مجدداً آن را ایمپورت کنیم. برای رفع این مشکل میتوانیم از پروفایل‌ها استفاده کنیم. توسط پروفایل، امکان سفارشی‌سازی شل را خواهیم داشت. پروفایل در واقع یک اسکریپت PowerShell است که به محض اجرای PowerShell فراخوانی خواهد شد. بنابراین درون پروفایل این فرصت را خواهیم داشت تا متغیرها، ماژول‌ها، aliaseها و… را قبل از باز کردن شل، درون سشن PowerShell بارگذاری کنیم. PowerShell از چندین نوع پروفایل پشتیبانی میکند و توسط متغیر خودکار PROFILE$ میتوانیم لیست مسیرهای پروفایل‌ها را مشاهده کنیم:  
PS /> $PROFILE | Get-Member -Type NoteProperty | Select-Object Name, Definitionbject Name, Definition

Name                   Definition
----                   ----------
AllUsersAllHosts       string AllUsersAllHosts=/usr/local/microsoft/powershell/7/…
AllUsersCurrentHost    string AllUsersCurrentHost=/usr/local/microsoft/powershell…
CurrentUserAllHosts    string CurrentUserAllHosts=/Users/sirwanafifi/.config/powe…
CurrentUserCurrentHost string CurrentUserCurrentHost=/Users/sirwanafifi/.config/p…
برای ویرایش هر کدام از پروفایل‌های موردنظر میتوانید اینگونه عمل کنید: 
$SlackProjectPath = "/Users/sirwanafifi/Desktop/ps_cmdlet_with_csharp/bin/Debug/net7.0/ps_cmdlet_with_csharp.dll"
Import-Module $SlackProjectPath
اکنون با هر بار باز کردن PowerShell به command let جدید دسترسی خواهید داشت: 
PS /> Get-Command -Module ps_cmdlet_with_csharp

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ----
Cmdlet          Push-SlackMessage                                  1.0.0.0    ps_…
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers
یکی دیگر از تغییرات مهم Razor در ASP.NET Core، معرفی Tag Helpers است که همانند HTML Helpers نگارش‌های پیشین ASP.NET MVC، کار رندر کردن HTML را انجام می‌دهند و در اغلب موارد می‌توان آن‌ها را جایگزین HTML Helpers کرد. مزیت استفاده‌ی از Tag helpers، شبیه بودن آن‌ها به المان‌ها و ویژگی‌های HTML است. در کل اینکه باید از HTML Helpers استفاده کرد و یا از Tag Helpers، بیشتر یک انتخاب شخصی و سلیقه‌ای است.


فعال سازی استفاده‌ی از Tag Helpers برای تمام Viewهای برنامه

برای اینکه تمام Viewهای سایت بتوانند به امکانات Tag Helpers دسترسی پیدا کنند، باید یک سطر ذیل را به فایل ViewImports.cshtml_ اضافه کرد:
 @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
در اینجا * به معنای استفاده‌ی از تمام Tag Helpers موجود در اسمبلی ذکر شده‌است.

Microsoft.AspNetCore.Mvc.TagHelpers به همراه افزودن وابستگی Microsoft.AspNetCore.Mvc در حین فعال سازی ASP.NET MVC، به پروژه اضافه می‌شود:



فعال سازی Intellisense مربوط به Tag Helpers در ویژوال استودیو

هرچند فعال سازی ASP.NET MVC، تنها وابستگی است که برای کار با Tag Helpers نیاز است، اما برای فعال سازی Intellisense آن‌ها باید بسته‌ی Microsoft.AspNetCore.Razor.Tools را نیز به فایل prject.json برنامه، جهت نصب معرفی کرد:
{
    "dependencies": {
         //same as before
         "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0",
         "Microsoft.AspNetCore.Razor.Runtime": "1.0.0",
         "Microsoft.AspNetCore.Razor.Tools": {
            "version": "1.0.0-preview2-final",
            "type": "build"
        }
    },
 
    "tools": {
         //same as before
        "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
    } 
}
ضمنا اگر از ReSharper استفاده می‌کنید (تا نگارش resharper-2016.1)، فعلا مجبور هستید که آن‌را غیرفعال کنید. اطلاعات بیشتر


یک مثال: ایجاد لینکی به یک اکشن متد
 <a asp-controller="Home" asp-action="Index" asp-route-id="123">Home</a>
در اینجا نحوه‌ی ایجاد لینکی را مشاهده می‌کنید که به کنترلر Home و اکشن متد Index آن اشاره می‌کند. این syntax جدید، جایگزین ActionLink مربوط به HTML Helperها است. در اینجا asp-route-id را نیز مشاهده می‌کنید. قسمت asp-route آن جهت مقدار دهی پارامترهای مسیریابی است و قسمت id- بنابر نام پارامتری که قرار است مقدار دهی شود، متغیر خواهد بود.
اگر نیاز به اشاره‌ی به مسیریابی خاصی از طریق نام آن وجود دارد (همان نام‌هایی که در حین تعریف یک مسیریابی ذکر می‌شوند) می‌توان به صورت ذیل عمل کرد:
 <a asp-route="login">Login</a>
و یا برای مشخص سازی پروتکل خاصی و یا ذکر دقیق نام هاست، می‌توان از روش زیر استفاده کرد:
 <a asp-controller="Account"
   asp-action="Register"
   asp-protocol="https"
   asp-host="asepecificdomain.com"
   asp-fragment="fragment">Register</a>


راهنمای تبدیل HTML Helpers به Tag Helpers

در جدول ذیل، مثال‌هایی را از HTML Helpers متداول و معادل‌های Tag Helper آن‌ها مشاهده می‌کنید:

Tag Helper
HTML Helper
<label asp-for="Email" class="col-md-2 control-label"></label>
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<a asp-controller="MyController" asp-action="MyAction" 
class="my-css-classname" my-attr="my-attribute">Click me</a>
@Html.ActionLink("Click me", "MyController", "MyAction", 
{ @class="my-css-classname", data_my_attr="my-attribute"})
<input asp-for="FirstName" style="width:100px;"/>
@Html.TextBox("FirstName", Model.FirstName, new { style = "width: 100px;" })
<input asp-for="Email" class="form-control" />
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
<input asp-for="Password" class="form-control" />
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
<input asp-for="UserName" class="form-control" />
@Html.EditorFor(l => l.UserName,
 new { htmlAttributes = new { @class = "form-control" } })
<form asp-controller="Account" asp-action="Register" 
method="post" class="form-horizontal" role="form">
@using (Html.BeginForm("Register", "Account",
 FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
<span asp-validation-for="UserName" class="text-danger"></span>
@Html.ValidationMessageFor(m => m.UserName, "",
 new { @class = "text-danger" })
<div asp-validation-summary="ValidationSummary.All" class="text-danger"></div>
@Html.ValidationSummary("", new { @class = "text-danger" })


نکات تکمیلی کار با فرم‌ها توسط Tag Helpers

نمونه‌ای از مثال Tag helper کار با فرم‌ها را در جدول فوق ملاحظه می‌کنید. چند نکته‌ی تکمیلی ذیل را می‌توان به آن اضافه کرد:
- در حین کار با Tag Helpers، درج anti forgery token به صورت خودکار صورت می‌گیرد. اگر می‌خواهید که این توکن ذکر نشود، آن‌را توسط ویژگی "asp-anti-forgery="false خاموش کنید.
- برای درج پارامترهای مسیریابی خاص، از asp-route به همراه نام پارامتر مدنظر استفاده کنید:
 <form asp-controller="Account"
      asp-action="Login"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      method="post" >
</form>
که در نهایت به یک چنین حالتی رندر می‌شود
 <form action="/Account/Login?returnurl=%2FHome%2FAbout" method="post">
- همانند action linkها در اینجا نیز برای اشاره‌ی به یک مسیریابی از طریق نام آن می‌توان از ویژگی asp-route استفاده کرد
 <form asp-route="login"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      method="post" >
</form>


Tag helpers مخصوص تعریف اسکریپت‌ها و CSSها

 در اینجا Tag Helpers صرفا به عنوان جایگزین‌های HTML Helpers مطرح نیستند. توسط آن‌ها قابلیت‌های جدیدی نیز ارائه شده‌است. برای مثال اگر تگ اسکریپت را به صورت ذیل تعریف کنیم:
 <script asp-src-include="~/app/**/*.js"></script>
یک چنین خروجی فرضی را تولید می‌کند:
 <script src="/app/app.js"></script>
<script src="/app/controllers/controller1.js"></script>
<script src="/app/controllers/controller2.js"></script>
<script src="/app/controllers/controller3.js"></script>
<script src="/app/controllers/controller4.js"></script>
<script src="/app/services/service1.js"></script>
<script src="/app/services/service2.js"></script>
به این معنا که یک سطر asp-src-include، بر اساس الگویی که دریافت می‌کند، تمام فایل‌های اسکریپت موجود در یک پوشه را یافته و برای آن‌ها، تگ اسکریپت تولید می‌کند. دراینجا ذکر ** به معنای بررسی تمام زیرپوشه‌های app است. اگر تنها پوشه‌ی خاصی مدنظر است، باید ** را حذف کرد.
در این بین اگر می‌خواهید از پوشه‌ی خاصی صرفنظر کنید، از asp-src-exclude استفاده کنید:
 <script asp-src-include="~/app/**/*.js"
        asp-src-exclude="~/app/services/**/*.js">
</script>
همچنین در اینجا امکان تعریف CDN و fallback هم وجود دارد. استفاده‌ی از CDNها جهت کاهش ترافیک سرور و بهبود کارآیی برنامه با ارائه‌ی نمونه‌های کش شده‌ی فریم ورک‌های معروف، متداول هستند که در اینجا نمونه‌ای از نحوه‌ی تعریف آن‌ها را مشاهده می‌کنید. همچنین تعریف fallback در اینجا به این معنا است که اگر CDN در دسترس نبود، به نمونه‌ی محلی موجود بر روی سرور مراجعه شود.
 <link rel="stylesheet" href="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css/bootstrap.min.css"
      asp-fallback-href="~/lib/bootstrap/css/bootstrap.min.css"
      asp-fallback-test-class="hidden"
      asp-fallback-test-property="visibility"
      asp-fallback-test-value="hidden" />
 
<script src="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/bootstrap.min.js"
        asp-fallback-src="~/lib/bootstrap/js/bootstrap.min.js"
        asp-fallback-test="window.jQuery">
</script>

به علاوه اگر ویژگی asp-file-version را نیز ذکر کنید:
 <link rel="stylesheet" href="~/css/site.min.css" asp-file-version="true"/>
یک چنین لینکی تولید می‌شود:
 <link rel="stylesheet" href="/css/site.min.css?v=UdxKHVNJA5vb1EsG9O9uURFDfEE3j1E3DgwL6NiDGMc" />
هدف آن نیز اصطلاحا cache busting است. به این معنا که با تغییر محتوای این فایل‌ها، کوئری استرینگ تولید شده، مجددا محاسبه شده و مرورگر همواره آخرین نگارش موجود را دریافت خواهد کرد و دیگر از نمونه‌ی کش شده‌ی قدیمی استفاده نمی‌کند.

یک نکته: ویژگی asp-file-version را برای تصاویر هم می‌توان بکار برد:
 <img src="~/images/logo.png"
     alt="company logo"
     asp-file-version="true" />
که یک چنین خروجی را تولید می‌کند و هدف آن نیز جلوگیری از کش شدن تصویر، با تغییر محتوای آن است:
 <img src="/images/logo.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk"
     alt="company logo"/>


بررسی Environment Tag Helper

با متغیرهای محیطی و نحوه‌ی تعریف آن‌ها در قسمت‌های قبل آشنا شدیم. در اینجا tag helper سفارشی خاصی برای کار با آن‌ها ارائه شده‌است که شیبه به if/else عمل می‌کنند:
<environment names="Development">    
   <link rel="stylesheet" href="~/css/site1.css" />
   <link rel="stylesheet" href="~/css/site2.css" />
</environment>

<environment names="Staging,Production">
   <link rel="stylesheet" href="~/css/site.min.css" asp-file-version="true"/>
</environment>
هدف این است که اگر متغیر محیطی به Development تنظیم شده بود، لینک‌های ساده و اصلی فایل‌های css یا اسکریپت در HTML نهایی درج شوند و اگر حالت توسعه تنظیم شده بود، لینک‌های min یا فشرده شده‌ی آن‌ها ارائه شوند؛ به همراه asp-file-version که cache busting را فعال می‌کند.


کار با دراپ داون‌ها توسط Tag helpers

فرض کنید ViewModel یک view جهت نمایش یک دراپ داون به این صورت تنظیم شده‌است:
public class CustomerViewModel
{
   public string Vehicle { get; set; }  
   public List<SelectListItem> Vehicles { get; set; }
برای نمایش SelectListItem توسط tag helpers می‌توان به صورت ذیل عمل کرد:
 <select asp-for="Vehicle" asp-items="Model.Vehicles">
</select>
asp-for به نام خاصیتی اشاره می‌کند که در نهایت مقدار انتخاب شده را دریافت می‌کند و asp-items لیست آیتم‌های دراپ داون را رندر می‌کند.
مطالب
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 مدیریت خواهد شد.

مطالب
ASP.NET MVC #16

مدیریت خطاها در یک برنامه ASP.NET MVC


استفاده از فیلتر HandleError

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

using System;
using System.Web.Mvc;

namespace MvcApplication13.Controllers
{
public class HomeController : Controller
{
[HandleError]
public ActionResult Index()
{
throw new InvalidOperationException();
return View();
}
}
}

در اینجا جهت آزمایش برنامه، به عمد یک استثنای دستی را صادر می‌کنیم. برای آزمایش برنامه هم نیاز است آن‌را خارج از دیباگر VS.NET اجرا کرد (آدرس برنامه را مستقیما خارج از VS.NET در یک مرورگر وارد کنید). همچنین یک سطر زیر را نیز لازم است به فایل web.config برنامه اضافه نمائید:

<system.web>
<customErrors mode="On" />

اکنون اگر برنامه را خارج از مرورگر اجرا کنید، با توجه به استفاده از ویژگی HandleError و همچنین بروز یک استثنا در متد Index، خودبخود صفحه Views\Shared\Error.cshtml به کاربر نمایش داده خواهد شد. در غیراینصورت صفحه زرد رنگ پیش فرض خطای ASP.NET به کاربر نمایش داده می‌شود که محتوای آن‌ها بیشتر برای برنامه نویس‌ها مناسب است و نه کاربران نهایی سیستم.
اگر علاقمند باشید که این ویژگی به صورت خودکار به تمام متدهای کنترلرهای برنامه اعمال شود، کافی است یک سطر زیر را به متد Application_Start فایل Global.asax.cs اضافه نمائید:

GlobalFilters.Filters.Add(new HandleErrorAttribute());

البته نیازی به انجام اینکار نیست زیرا اگر به متد RegisterGlobalFilters فایل Global.asax.cs دقت کنیم، اینکار پیشتر توسط قالب پیش فرض VS.NET انجام شده است. فقط برای فعال سازی آن نیاز است تگ customErrors در فایل وب کانفیگ برنامه مقدار دهی و تنظیم شود.



استفاده از صفحه خطای سفارشی دیگری بجای فایل Error.cshtml

امکان تنظیم نمایش صفحه خطای سفارشی دیگری نیز وجود دارد. برای مثال استفاده از فایل Views\Shared\CustomErrorView.cshtml :

[HandleError(View = "CustomErrorView")]



استفاده از صفحات خطای متفاوت به ازای استثناهای مختلف

می‌توان فیلتر HandleError را تنها به یک نوع استثنای خاص محدود کرد. همچنین امکان استفاده از چندین ویژگی HandleError برای یک متد نیز وجود دارد:

[HandleError(ExceptionType = typeof(NullReferenceException), View = "ErrorHandling")]



دسترسی به اطلاعات استثناء در صفحه نمایش خطاها

زمانیکه برنامه به صفحه خطا هدایت می‌شود، نوع Model آن System.Web.Mvc.HandleErrorInfo می‌باشد:

@model System.Web.Mvc.HandleErrorInfo

@{
ViewBag.Title = "DbError";
}

<h2>An Error Has Occurred</h2>

@if (Model != null)
{
<p>@Model.Exception.GetType().Name<br />
thrown in @Model.ControllerName @Model.ActionName</p>
}

البته این نکته را صرفا به عنوان اطلاعات عمومی در نظر داشته باشید. زیرا اگر قرار باشد مجددا اصل استثناء را نمایش دهیم، همان صفحه زرد رنگ ASP.NET شاید بهتر باشد.



استفاده از تگ customErrors در فایل Web.config برنامه

ویژگی حالت تگ customErrors در فایل web.config برنامه، سه مقدار را می‌تواند بپذیرد:
الف) Off : صفحه زرد رنگ معرفی خطای ASP.NET را به همراه تمام اطلاعات مرتبط با استثنای رخ داده نمایش می‌دهد.
ب) RemoteOnly : همان حالت الف است با این تفاوت که صفحه خطا را فقط در کامپیوتری که وب سرور بر روی آن نصب است نمایش خواهد داد.
ج) On : یک صفحه خطای سفارشی شده را نمایش می‌دهد.

بنابراین هیچگاه از حالت Off استفاده نکنید. زیرا خطاهای نمایش داده شده، علاوه بر برنامه نویس، برای مهاجم به یک سایت نیز بسیار دلپذیر است!
حالت RemoteOnly در زمان توسعه برنامه توصیه می‌شود.
حالت On حین توزیع برنامه باید بکارگرفته شود.



مدیریت خطاهای رخ داده خارج از MVC Pipeline

HandleErrorAttribute تنها استثناهای رخ داده داخل ASP.NET MVC Pipeline را مدیریت می‌کند (یا خطاهایی از نوع 500). اگر این نوع استثناها خارج از آن رخ دهند مثلا فایلی یافت نشود (خطای 404) و امثال آن، باید به روش زیر عمل کرد:

<customErrors mode="On" defaultRedirect="error">
<error statusCode="404" redirect="error/notfound" />
<error statusCode="403" redirect="error/forbidden" />
</customErrors>

در اینجا اگر فایلی یافت نشد، کاربر به کنترلری به نام error و متدی به نام notfound هدایت خواهد شد. بنابراین نیاز به کنترلر زیر وجود دارد؛ به علاوه به ازای هر متد هم یک View متناظر باید اضافه شود (کلیک راست روی نام متد و انتخاب گزینه افزودن View جدید).

using System.Web.Mvc;

namespace MvcApplication13.Controllers
{
public class ErrorController : Controller
{
public ActionResult Index()
{
return View();
}

public ActionResult NotFound()
{
return View();
}

public ActionResult Forbidden()
{
return View();
}
}
}

برای آزمایش این قسمت، برنامه را اجرا کرده و سپس مثلا آدرس غیرموجود http://localhost/xyz را وارد کنید.



استفاده از فیلتر HandleError اجباری نیست

در همین قسمت قبل پس از افزودن customErrors و defaultRedirect آن که به نام یک کنترلر اشاره می‌کند، کلیه فیلترهای HandleError اضافه شده به برنامه را حذف کنید. سپس برنامه را خارج از محیط VS.NET اجرا کنید. باز هم متد Index کنترلر Error اجرا خواهد شد. به عبارتی الزاما نیازی به استفاده از فیلتر HandleError نیست و به کمک مقدار دهی صحیح تگ customErrors، کار نمایش خودکار صفحه سفارشی خطاها به کاربر انجام خواهد شد.
البته بدیهی است که گزینه‌های نمایش یک View خاص به ازای استثنایی ویژه، یکی از مزیت‌های استفاده از فیلتر HandleError می‌باشد که امکان تنظیم آن در فایل web.config وجود ندارد.



ثبت اطلاعات استثناهای رخ داده به کمک ELMAH

نمایش صفحه‌ی خطای سفارشی به کاربر، یکی از موارد ضروری تمام برنامه‌های ASP.NET است، اما کافی نیست. ثبت اطلاعات جزئیات استثناهای رخ داده در طول زمان می‌توانند به بالا بردن کیفیت برنامه به شدت کمک کنند. برای این منظور می‌توان همانند سابق از متد Application_Error قابل تعریف در فایل Global.asax.cs کمک گرفت؛ اما با وجود افزونه‌ای به نام ELMAH اینکار اتلاف وقت است و اصلا توصیه نمی‌شود. همچنین به کمک ELMAH می‌توان مشکلات را تبدیل به ایمیل‌های خودکار کرد یا از آن‌ها فید RSS درست نمود.
برای دریافت ELMAH یا به سایت اصلی آن مراجعه نمائید و یا به کمک NuGet هم به سادگی قابل دریافت است. پس از دریافت، ارجاعی را به اسمبلی آن (Elmah.dll) اضافه نمائید. در ادامه فایل web.config برنامه را گشوده و چند سطر زیر را به آن در قسمت configuration اضافه کنید:

<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah"/>
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
<section name="errorTweet" requirePermission="false" type="Elmah.ErrorTweetSectionHandler, Elmah"/>
</sectionGroup>
</configSections>

سپس ذیل قسمت appSettings، تنظیمات پروایدر ذخیره سازی اطلاعات آن‌را وارد نمائید. مثلا در اینجا از فایل‌های XML برای ذخیره سازی اطلاعات استفاده خواهد شد (که امن‌ترین حالت ممکن است؛ از این لحاظ که اگر بانک اطلاعاتی را انتخاب کنید، ممکن است مشکل اصلی از همانجا ناشی شده باشد. بنابراین خطایی ثبت نخواهد شد. همچنین در این حالت نیازی به سایر DLLهای همراه ELMAH هم نیست). در اینجا مسیر ذخیره سازی اطلاعات در پوشه app_data/errorslog تنظیم شده است:

<elmah>
<security allowRemoteAccess="1"/>
<errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/App_Data/ErrorsLog"/>
</elmah>

در ادامه در قسمت system.web، دو تعریف زیر را اضافه نمائید. به این ترتیب امکان دسترسی به آدرس http://server/elmah.axd مهیا می‌گردد:

<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
</httpModules>
<httpHandlers>
<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
</httpHandlers>

البته برای IIS7 تنظیمات ذیل نیز باید اضافه شوند:

<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true">
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
</modules>
<handlers>
<add name="Elmah" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
</handlers>
</system.webServer>

و به این ترتیب تنظیمات اولیه ELMAH به پایان می‌رسد (و با ASP.NET Web forms هیچ تفاوتی ندارد).
مرحله بعد، تنظیمات مسیریابی ASP.NET MVC است برای اینکه آدرس http://server/elmah.axd را وارد سیستم پردازشی خود نکند. البته اینکار پیشتر انجام شده است:

public static void RegisterRoutes(RouteCollection routes)
{
//routes.IgnoreRoute("elmah.axd");
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

بنابراین همین تنظیمات، به همراه قالب پیش فرض یک پروژه جدید ASP.NET MVC برای استفاده از ELMAH کفایت می‌کند. اکنون پروژه جاری را یکبار دیگر خارج از VS.NET اجرا کرده و سپس به مسیر http://localhost/elmah.axd جهت مشاهده خطاهای لاگ شده به همراه جزئیات کامل آن‌ها مراجعه کنید.

مشکل: استثناهای برنامه توسط ELMAH لاگ نمی‌شوند!

فیلتر HandleError با ELMAH سازگار نیست. زیرا با استفاده از آن، متدهای کنترلرها به صورت خودکار داخل یک try/catch اجرا شده و به این ترتیب استثناهای رخ داده، مدیریت گردیده و به ELMAH هدایت نمی‌شوند. بنابراین نیاز است به متد RegisterGlobalFilters فایل Global.asax.cs مراجعه کرده و سطر زیر را حذف کنید:

filters.Add(new HandleErrorAttribute());

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

using System.Web.Mvc;
using Elmah;

namespace MvcApplication13.CustomFilters
{
public class ElmahHandledErrorLoggerFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.ExceptionHandled)
ErrorSignal.FromCurrentContext().Raise(context.Exception);
// all other exceptions will be caught by ELMAH anyway
}
}
}

در اینجا یک فیلتر سفارشی به برنامه اضافه شده است تا خطاهای مدیریت شده برنامه (خطاهای مدیریت شده توسط فیلتر HandleError توکار) را به موتور ELMAH هدایت کند. سایر خطاهای مدیریت نشده به صورت خودکار توسط ELMAH ثبت خواهند شد و نیازی به انجام کار اضافی در این مورد نیست.
سپس این فیلتر جدید را به صورت سراسری تعریف کنید:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ElmahHandledErrorLoggerFilter());
filters.Add(new HandleErrorAttribute());
}

ترتیب این‌ها هم مهم است. ابتدا باید ElmahHandledErrorLoggerFilter معرفی شود.


تذکر مهم!
حین استفاده از ELMAH یک نکته را فراموش نکنید:
اگر allowRemoteAccess آن‌را به عدد 1 تنظیم کرده‌اید، به هیچ عنوان از نام پیش فرض elmah.axd استفاده نکنید (هر نام اختیاری دیگری را که علاقمند بودید و به سادگی قابل حدس زدن نبود، در فایل web.config وارد کنید).


خلاصه بحث
1- در ASP.NET MVC نیازی نیست تا متدهای کنترلرها را با try/catch شلوغ کنید.
2- حتما قسمت customErrors فایل وب کانفیگ برنامه را دهی کنید (این مورد را به چک لیست اجباری تهیه یک برنامه ASP.NET MVC اضافه کنید).
3- استفاده از فیلتر HandleError اختیاری است. اگر از قابلیت فیلتر کردن استثناهای ویژه آن استفاده نمی‌کنید، مقدار دهی customErrors وب کانفیگ برنامه هم همان کار را انجام می‌دهد.
4- برای ثبت جزئیات دقیق استثناهای رخ داده در برنامه، از ELMAH استفاده کنید و بی‌جهت وقت خودتان را صرف بازنویسی این افزونه ارزشمند نکنید.

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