کلیک بر روی لینک ها
کاربران امروزه با عناصری که به نحوی خاص درون صفحه عمل میکنند، آشنا شدهاند. بنابراین انتخاب مناسب برای اتخاذ این عناصر زمانی مناسب است که به تکمیل کارآیی و رضایت کاربر کمک کند.
یک کامپوننت در واقع از دو بخش تشکیل شدهاست:
1 - اول اینکه چگونه به نظر میرسد ( UI ).
2 – دوم اینکه چگونه کار میکند ( UX ).
این عناصر رابط ( component ) شامل :
Input Controls : check boxes, radio buttons, drop down lists, list boxes, buttons, toggles, text fields, date field
Navigational Components : breadcrumb, slider, search field, pagination, slider, tags, icons
Informational Components : tool tips, icons, progress bar, notifications, message boxes, modal windows
Containers : accordion
اما باید توجه داشت که فقط به این موارد محدود نمیشوند.
در این
قسمت به طور مختصر با این دست از کامپوننتها ( UI Component ) آشنا میشویم.
کامپوننت V-Alert برای انتقال اطلاعات مهم به کاربر مورد استفاده قرار میگیرد. این کامپوننت چهار نوع اطلاعات را به کاربر گوشزد میکند که شامل موفقیتها، اطلاعات، هشدارها و خطاها میباشد.
هشدارها میتوانند یک رنگ خاص را داشته باشند که به طور پیش فرض نمایش داده نمیشوند.
در مثال پایین، کامپوننت v-alert شامل دو مقدار است که برای آن تنظیم شدهاست. مقدار (value) که شامل یک مقدار Boolean است و مقدار (type) که مشخص کننده نوع هشدار است (موفقیت ، اطلاعات ، هشدار و خطا).
در قطعه کد پایین، این چهار نوع اطلاعات قابل نمایش به کاربر مشخص شدهاند:
<div id="app"> <v-app id="inspire"> <div> <v-alert :value="true" type="success"> This is a success alert. </v-alert> <v-alert :value="true" type="info"> This is a info alert. </v-alert> <v-alert :value="true" type="warning"> This is a warning alert. </v-alert> <v-alert :value="true" type="error"> This is a error alert. </v-alert> </div> </v-app> </div>
برای کامپوننت V-Alert میتوان propertiesهای مختلفی را مشخص نمود که از جمله آنها میتوان به موارد زیر اشاره کرد:
Color : به وسیله این property میتوان رنگ پیغام را مشخص نمود. هم به وسیله نام رنگ میتوان رنگ مورد نظر را مشخص کرد و هم به وسیلهی کد RGB این کار را انجام داد. dismissible : این تنظیم مشخص میکند که پیغام، قابلیت بسته شدن را دارد یا خیر که حاوی یک مقدار Boolean است.
icon : مشخص کننده یک نماد خاص است که درون جعبه پیغام قرار میگیرد.
type : مشخص کننده نوع پیام است که پیشتر در مورد آن توضیح داده شد.
کامپوننت v-avatar برای تغییر اندازه تصاویر مورد استفاده قرار میگیرد که معمولا جهت نمایش عکس پروفایل استفاده میشود.
طریقه استفاده :
avatar، دارای یک اندازهی پویا است که میتواند برای هر وضعیتی تغییر کند.
برای این کامپوننت سه properties قابل تنظیم است:
color : به وسیله این property میتوان رنگ دلخواهی را برای آواتار مشخص نمود. هم به وسیله نام رنگ میتوان رنگ مورد نظر را مشخص کرد و هم به وسیله کد RGB این کار را صورت داد.
size : به طور پیشفرض برای avatar، سایز 48 تنظیم شدهاست که میتوان این میزان را کم و یا زیاد کرد.
tile : همانند border radius در css عمل میکند. با تنظیم این گزینه میتوانیم یک آواتار گرد داشته باشیم.
به وسیله این کامپوننت میتوان نمادهایی را برای نمایش اطلاعاتی به کاربر یا جلب توجه کاربر به یک عنصر خاص، ایجاد نمود.
این کامپوننت نیز properties خاص خود را دارد که از جمله آن میتوان به color , left , mode , overlab و غیره اشاره کرد.
قطعه کد پایین نشان دهنده چگونگی عملکرد این کامپوننت است:
<div id="app"> <v-app id="inspire"> <div> <v-badge color="purple" left overlap> <template v-slot:badge> <v-icon dark small> done </v-icon> </template> <v-icon color="grey lighten-1" large> account_circle </v-icon> </v-badge> <v-badge overlap color="orange"> <template v-slot:badge> <v-icon dark small> notifications </v-icon> </template> <v-icon large color="grey darken-1"> account_box </v-icon> </v-badge> </div> </v-app> </div>
نتیجه قطعه کد بالا بدین ترتیب است:
این کامپوننت را میتوان جایگزین sidebarها نمود. این کامپوننت در درجه اول در موبایل مورد استفاده قرار میگیرد که میتواند شامل متن و یا آیکن باشد.
به وسیله این کامپوننت امکان انتقال از یک بخش از صفحه به بخشی دیگر امکان پذیر میشود.
این کامپوننت نیز properties خاص خود را دارد که از جمله آن میتوان به active-sync (برای نشان دادن فعال یا غیر فعال بودن گزینه انتخاب شده)، fixed ( برای مشخص کردن موقعیت کامپوننت در صفحه) و موارد دیگر اشاره کرد.
تقسیم بندی اجزاء این کامپوننت به شرح زیر است:
1 - محل قرار گیری کامپوننت
2- آیکن غیر فعال
3- برچسب غیر فعال
4 - آیکن فعال
5- برچسب فعال
قطعه کد پایین نشان دهنده چگونگی یک bottom navbar است:
<div id="app"> <v-app id="inspire"> <v-card height="200px" flat> <div> Active: {{ bottomNav }} // </div> <v-bottom-nav :active.sync="bottomNav" :value="true" absolute color="transparent"> <v-btn color="teal" flat value="recent"> <span>Recent</span> <v-icon>history</v-icon> </v-btn> <v-btn color="teal" flat value="favorites"> <span>Favorites</span> <v-icon>favorite</v-icon> </v-btn> <v-btn color="teal" flat value="nearby"> <span>Nearby</span> <v-icon>place</v-icon> </v-btn> </v-bottom-nav> </v-card> </v-app> </div>
این کامپوننت برای ایجاد یک دکمه چه به صورت متن و یا آیکن مورد استفاده قرار میگیرد. دکمهها به کاربران این امکان را میدهند تا اقداماتی را انجام دهند و انتخابهای خود را تنها با یک کلیک انجام دهند.
از دکمهها ممکن است در جاهای مختلف صفحه به خصوص در دیالوگ باکسها، فرمها و ابزارها مورد استفاده قرار گیرد.
کامپوننت v-btn نیز مانند سایر کامپوننتها تنظیمات خاص خود را دارد که از جمله آن میتوان به کوچکی و بزرگی دکمه، فعال یا غیر فعال بودن دکمه، نوع متن یا آیکن بودن دکمه اشاره نمود.
حالتهای مختلفی از دکمهها وجود دارند که میتوانند به بهتر شدن UI برنامه ما کمک کنند. برای مثال میتوان به موارد زیر اشاره کرد:
button drop-down variants : دکمههای کرکرهای که معمولا برای نظم و کم جا بودن در صفحه مورد استفاده قرار میگیرند.
icons : آیکنها میتوانند برای محتوای اصلی یک دکمه مورد استفاده قرار بگیرند تا ظاهر زیباتری را به دکمه ما بدهند.
floating : این دکمهها حالت آیکن را دارند؛ با این تفاوت که آیکن مورد نظر، درون یک محتوا قرار میگیرد.
loaders : به وسیله این دکمهها میتوان کاربر را متوجه انجام یک پردازش نمود. به صورت پیشفرض بعد از فشردن این نوع دکمهها محتوای دکمه فشرده شده تغییر ظاهر داده و به شکل یک دایره در حال چرخش در میآید. البته میتوان این پیشفرض را به حالتهای دیگری نیز تغییر داد.
round : این نوع دکمهها دقیقا کارآیی دکمههای معمولی را دارند؛ با این تفاوت که این دکمهها دارای لبههایی گرد هستند.
یک نمونه از ایجاد انواع دکمهها در زیر آمده است:
<div id="app"> <v-app id="inspire"> <div> <v-btn color="success">Success</v-btn> <v-btn color="error">Error</v-btn> <v-btn color="warning">Warning</v-btn> <v-btn color="info">Info</v-btn> </div> </v-app> </div>
یکی از کامپوننتهایی که به تازگی به vuetify اضافه شده است، کامپوننت تقویم یا v-calendar است. از این کامپوننت برای نمایش تاریخ، روز، هفته، ماه و سال استفاده میشود. یک تقویم دارای یک نوع و یک مقدار است که تعیین میکند چه نوع تقویمی، در طول چه مدت زمانی نمایش داده شود.
حالتهای مختلفی برای نمایش تقویم در صفحه وجود دارد که برای مثال میتوان به موارد زیر اشاره کرد:
events : به وسیله این گزینه میتوان برای هر روز یک رخداد خاص را مشخص نمود که به وسیله کلیک بر روی آن، اطلاعات آن رخداد نمایش داده شود.
weekly : میتوان یک تقویم هفتگی را ایجاد نمود و رخدادهای هفتگی را برای آن تنظیم کرد.
نمونه ایجاد یک تقویم در پایین آمده است:
<div id="app"> <v-app id="inspire"> <v-layout wrap> <v-flex xs12> <v-sheet height="500"> <v-calendar ref="calendar" v-model="start" :type="type" :end="end" color="primary"> </v-calendar> </v-sheet> </v-flex> <v-flex sm4 xs12> <v-btn @click="$refs.calendar.prev()"> <v-icon dark left> keyboard_arrow_left </v-icon> Prev </v-btn> </v-flex> <v-flex sm4 xs12> <v-select v-model="type" :items="typeOptions" label="Type"> </v-select> </v-flex> <v-flex sm4 xs12> <v-btn @click="$refs.calendar.next()"> Next <v-icon right dark> keyboard_arrow_right </v-icon> </v-btn> </v-flex> </v-layout> </v-app> </div>
js قطعه کد new Vue({ el: '#app', data: () => ({ type: 'month', //مشخص کننده نوع تقویم که در اینجا تقویم به صورت ماهانه است start: '2019-01-01', end: '2019-01-06', typeOptions: [ { text: 'Day', value: 'day' }, { text: '4 Day', value: '4day' }, { text: 'Week', value: 'week' }, { text: 'Month', value: 'month' }, { text: 'Custom Daily', value: 'custom-daily' }, { text: 'Custom Weekly', value: 'custom-weekly' } ] }) })
در فریمورک NET. ابزارهای مختلفی برای کار با دادههای XML در نظر گرفته شدهاست که بعد از نسخه 3.5 آن، انتخاب اول LINQ to XML می باشد. در این مطلب قصد داریم API ای را برای خواندن اطلاعات فایلهای XML با استفاده از LINQ to XML و انقیاد پویا در سیشارپ (Dynamic Binding) تهیه کنیم.
راه حل اول: استفاده از ExpandoObject
public static class ExpandoXml { public static dynamic AsExpando(this XDocument document) { return CreateExpando(document.Root); } private static dynamic CreateExpando(XElement element) { var result = new ExpandoObject() as IDictionary<string, object>; if (element.Elements().Any(e => e.HasElements)) { var list = new List<ExpandoObject>(); result.Add(element.Name.ToString(), list); foreach (var childElement in element.Elements()) { list.Add(CreateExpando(childElement)); } } else { foreach (var leafElement in element.Elements()) { result.Add(leafElement.Name.ToString(), leafElement.Value); } } return result; } }
در تکه کد بالا از طریق متد CreateExpando به صورت بازگشتی ابتدا بررسی میشود که آیا عنصر جاری دارای عناصری میباشد و همچنین آیا آنها دارای فرزند میباشند یا خیر؛ در صورت برقراری شرط، نتیجهی اجرای متد CreateExpando بر روی تک تک عناصر فرزند را درون لیستی از ExpandoObject قرار داده و سپس آن لیست نیز به عنوان Value عنصر جاری در نظر گرفته میشود. در صورت عدم برقراری شرط مذکور، مقادیر مربوط به عناصر فرزند را در قالب یک ExpandoObject به عنوان خروجی بازگشت خواهد داد.
راه حل دوم: استفاده از DynamicObject
public class DynamicXml : DynamicObject, IEnumerable { private readonly dynamic _xml; public DynamicXml(string fileName) { _xml = XDocument.Load(fileName); } public DynamicXml(dynamic xml) { _xml = xml; } public IEnumerator GetEnumerator() { foreach (var item in _xml.Elements()) { yield return new DynamicXml(item); } } public override bool TryGetMember(GetMemberBinder binder, out object result) { var xml = _xml.Element(binder.Name); if (xml != null) { result = new DynamicXml(xml); return true; } var attribute = _xml.Attribute(binder.Name); if (attribute != null) { result = new DynamicXml(attribute); return true; } result = null; return false; } public static implicit operator string(DynamicXml xml) { return xml._xml.Value; } }
کلاس DynamicXml از طریق سازنده اول، نام فایل را دریافت کرده و از طریق LINQ to XML با استفاده از متد Load کلاس XDocument، فایل مورد نظر بارگذاری شده و درون فیلدی به نام xml_ از نوع dynamic نگه داشته میشود. کار بعدی، بازنویسی متد TryGetMember میباشد. در بدنه بازنویسی شده این متد ابتدا بررسی میشود که آیا با نام خصوصت درخواست شده عنصری در داده XML وجود دارد یا خیر؛ در صورت موجود بودن، پارامتر result با یک وهله جدید از DynamicXml مقدار دهی میشود که عنصر یافت شده از طریق سازنده دوم، به عنوان داده xml برای مقدار دهی فیلد xml_ به عنوان آرگومان ارسال میشود. در صورت عدم وجود عنصر مذکور، بدنبال خصوصیتی با آن نام بوده و در صورت یافت شدن، باز به عنوان یک وهله DynamicXml برای مقدار دهی result استفاده میشود.
در ادامه برای نسبت دادن یک وهله از DynamicXml به یک متغیر string و دستیابی به مقدار یک عنصر که از طریق خصوصیت، درخواست میشود نیاز است تا اپراتور ضمنی string را نیز برای کلاس بالا نظر بگیریم. همچنین برای ایجاد امکان پیمایش برروی عناصر فرزند از طریق foreach، لازم است واسط IEnumerable را نیز پیاده سازی کرده باشیم.
طریقه استفاده
class Program { static void Main(string[] args) { var doc1 = XDocument.Load("Employees.xml"); foreach (var element in doc1.Element("Employees").Elements("Employee")) { Console.WriteLine(element.Element("FirstName").Value); } dynamic doc2 = XDocument.Load("Employees.xml").AsExpando(); foreach (var employee in doc2.Employees) { Console.WriteLine(employee.FirstName); } dynamic doc3 = new DynamicXml("Employees.xml"); foreach (var employee in doc3.Employees) { Console.WriteLine(employee.FirstName); Console.WriteLine(employee.Id); } } }
<?xml version="1.0" encoding="utf-8" ?> <Employees> <Employee Id="1"> <FirstName> Employee1 </FirstName> </Employee> <Employee Id="2"> <FirstName> Employee2 </FirstName> </Employee> <Employee Id="3"> <FirstName> Employee3 </FirstName> </Employee> <Employee Id="4"> <FirstName> Employee4 </FirstName> </Employee> </Employees>
چرا LINQ؟
اجزای سازندهی LINQ
- Elements عناصر
- Sequences توالیها
int[] fibonacci = {0, 1, 1, 2, 3, 5};
int[] fibonacci = { 0, 1, 1, 2, 3, 5 }; int numberOfElements = fibonacci.Count(); Console.WriteLine($"{numberOfElements}"); IEnumerable<int> distinictNumbers = fibonacci.Distinct(); Console.WriteLine("Elements in output sequence:"); foreach (var number in distinictNumbers) { Console.WriteLine(number); }
6 Elements in output sequence: 0 1 2 3 5
مفهوم Deffer Execution ( اجرای به تاخیر افتاده )
int[] fibonacci = { 0, 1, 1, 2, 3, 5 }; // ایجاد پرس و جو IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2); // در این مرحله پرس و جو ایجاد شده ولی هنوز اجرا نشده است // تغییر عنصر اول توالی fibonacci[0] = 99; // حرکت بر روی عناصر توالی باعث اجرای پرس و جو میشود foreach (var number in numbersGreaterThanTwoQuery) { Console.WriteLine(number); }
99 3 5
int[] fibonacci = { 0, 1, 1, 2, 3, 5 }; // ساخت پرس و جو IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2) .ToArray(); // در این مرحله به خاطر عملگر استفاده شده پرس و جو اجرا میشود // تغییر اولین عنصر توالی fibonacci[0] = 99; // حرکت بر روی نتیجه foreach (var number in numbersGreaterThanTwoQuery) { Console.WriteLine(number); }
3 5
{ "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" ] }
"default_icon": { // optional "19": "images/icon19.png", // optional "38": "images/icon38.png" // optional }
صفحات پسزمینه
- صفحات مصر یا Persistent Page
- صفحات رویدادگرا یا Events Pages
"background": { "scripts": ["background.js"], "persistent": false }
Content Script یا اسکریپت محتوا
"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>
$(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( 'تنظیمات جدید اعمال شد'); }); }); });
"permissions": [ "storage" ]
chrome.storage.sync.set
{ "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" ] }
chrome://extensions
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"]); });
"page_action": { "default_icon": { "19": "images/icon19.png", "38": "images/icon38.png" }, "default_popup": "popup.html"
"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);
"omnibox": { "keyword" : ".net" }
"background": { "scripts": ["omnibox.js"]
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: "آخرین نظرات اشتراک ها"} ]); });
onInputStarted | بعد از اینکه کاربر کلمه کلیدی را وارد کرد اجرا میشود |
onInputChanged | بعد از وارد کردن کلمه کلیدی هربار که کاربر تغییری در ورودی نوارد آدرس میدهد اجرا میشود. |
onInputEntered | کاربر ورودی خود را تایید میکند. مثلا بعد از وارد کردن، کلید enter را میفشارد |
onInputCancelled | کاربر از وارد کردن ورودی منصرف شده است؛ مثلا کلید ESC را فشرده است. |
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 }); }); });
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/' }) } }); });
نصب ابتدایی آن بسیار ساده است و نکته خاصی ندارد.
پس از نصب، یکبار VS.NET را بسته و سپس باز کنید. این قالب جدید، ذیل قسمت پروژههای ویندوز مرتبط با ویژوال سی شارپ، ظاهر خواهد شد:
در این حال اگر یک پروژه جدید را آغاز کنید، این قالب تدارک دیده شده، لایهها و قسمتهای مختلف را به صورت خودکار اضافه خواهد کرد:
نکته مهم! برنامه کامپایل نمیشود!
به عمد، جهت کاهش حجم قالب دریافتی فوق، فایلهای باینری آن پیوست نشدهاند. وگرنه باید بالای 30 مگابایت را دریافت میکردید که واقعا نیازی نیست.
اما اگر به هر پروژه داخل Solution دقت کنید، فایل متنی packages.config آن نیز پیوست شده است. به کمک این فایلها به سادگی میتوان تمام وابستگیها را از طریق NuGet بازیابی کرد.
برای این منظور ابتدا به اینترنت متصل شده (مهم) و سپس بر روی Solution کلیک راست کرده و گزینه فعال سازی Restore کلیه بستههای نیوگت را انتخاب کنید.
پس از اینکار، آخرین نگارش NuGet.exe از اینترنت دریافت و به پروژه اضافه میشود:
اکنون اگر Solution را Build کنید، اولین کاری که صورت خواهد گرفت، دریافت کلیه وابستگیها از سایت NuGet است. اندکی تامل کنید تا اینکار تمام شود.
پس از پایان کار دریافت، پوشه packages در کنار فایل sln پروژه ایجاد شده، تشکیل میشود.
یکبار وجود آنرا بررسی کنید.
اکنون اگر برنامه را Build کنید احتمالا پیغام میدهد که Fody را نمیتواند پیدا کند با اینکه دریافت شده و در پوشه packages موجود است. هر زمان، هر پیغام خطایی در مورد Fody مشاهده کردید، فقط یکبار VS.NET را بسته و باز کنید. مشکل حل میشود!
تنها کاری که پس از بازسازی پوشه packages بهتر است صورت گیرد (اختیاری است البته)، صدور دستور ذیل در خط فرمان پاور شل است:
PM> Update-Package
پس از اینکار، نیاز است یکبار VS.NET را بسته و مجددا باز کنید (مهم) تا تمام وابستگیها به درستی بارگذاری شوند. خصوصا بسته Fody که کار AOP را انجام میدهد. در غیر اینصورت موفق به Build پروژه نخواهید شد.
بنابراین به صورت خلاصه:
الف) یکبار گزینه فعال سازی Restore کلیه بستههای نیوگت را انتخاب کنید.
ب) پروژه را Build کنید تا وابستگیها را از سایت NuGet دریافت کند.
ج) دستور Update-Package را اجرا نمائید (اختیاری).
ج) VS.NET را پس از سه مرحله فوق، یکبار بسته و باز کنید.
در کل بستههای مورد استفاده به این شرح هستند:
PM> Install-Package MahApps.Metro -Pre PM> Install-Package MahApps.Metro.Resources PM> Install-Package EntityFramework PM> Install-Package structuremap PM> Install-Package PropertyChanged.Fody PM> Install-Package MvvmLight PM> Install-Package Microsoft.SqlServer.Compact
یک نکته جانبی
فید NuGet در VS.NET به Https تنظیم شده است. اگر دسترسی به Https برای شما به کندی صورت میگیرد فقط کافی است مسیر فید آنرا در منوی Tools، گزینهی Options، ذیل قسمت Package manager یافته و به http://nuget.org/api/v2 تغییر دهید؛ یعنی به Http خالی، بجای Https؛ تا سرعت دریافت بستههای NuGet مورد نظر افزایش یابند.
اجرای پروژه و برنامه
پس از این مراحل و Build موفقیت آمیز پروژه، برنامه را اجرا کنید. در اولین بار اجرای برنامه به صورت خودکار بانک اطلاعاتی به همراه ساختار جداول تشکیل میشوند. اما ... با خطای زیر مواجه خواهید شد:
The path is not valid. Check the directory for the database. [ Path = D:\...\bin\Debug\Db\db.sdf ]
<connectionStrings> <clear/> <add name="MyWpfFrameworkContext" connectionString="Data Source=|DataDirectory|\Db\db.sdf;Max Buffer Size=30720;File Mode=Read Write;" providerName="System.Data.SqlServerCE.4.0" /> </connectionStrings>
برای ورود به برنامه از نام کاربری Admin و کلمه عبور 123456 استفاده نمائید.
این کاربر پیش فرض در کلاس MyWpfFrameworkMigrations لایه دادههای برنامه، توسط متد addRolesAndAdmin اضافه شده است.
خوب! تا اینجا با نحوه نصب و راه اندازی این قالب جدید آشنا شدیم. در قسمتهای بعد، به جزئیات ارتباطات و نحوه استفاده از آن به عنوان پایه یک کار و پروژه جدید، خواهیم پرداخت.
به روز رسانی 1.1
جهت سازگاری با EF 6 ، StructureMap 3 و همچنین VS 2013، پروژه به روز شد: WpfFrmwork_1.1.zip
به روز رسانی نهایی
به روز شدهی این پروژه را بر اساس آخرین وابستگیهای آن از اینجا دریافت کنید.
تولید پویای کد در زمان اجرا توسط Reflection.Emit
Reflection چیست؟
Reflection چیزهایی هستند که با نگاه در یک آینه قابل مشاهدهاند. در این حالت شخص میتواند قسمتهای مختلف ظاهر خود را برانداز کرده یا قسمتی را تغییر دهید. اما این مساله چه ربطی به دنیای دات نت دارد؟ در دات نت با استفاده از Reflection میتوان به اطلاعات اشیاء یک برنامهی در حال اجرا دسترسی یافت. برای مثال نام کلاسهای مختلف آن چیست یا درون کلاسی خاص، چه متدهایی قرار دارند. همچنین با استفاده از Reflection میتوان رفتارهای جدیدی را نیز به کلاسها و اشیاء افزود یا آنها را تغییر داد.
همواره عنوان میشود که از Reflection به دلیل سربار بالای آن پرهیز کنید و تنها از آن به عنوان آخرین راه حل موجود استفاده نمائید و این دقیقا موردی است که در مباحث جاری بیشتر از آن استفاده خواهد شد: ساخت اشیاء جدید در زمان اجرا به کمک کدهای IL و امکانات Reflection
نگاهی به امکانات متداول Reflection
در مثال بعد، نگاهی خواهیم داشت به امکانات متداول Reflection، مانند دسترسی به متدها و خواص یک کلاس و تعویض مقدار یا فراخوانی آنها:
using System; namespace FastReflectionTests { class Person { public string Name { set; get; } public string Speak() { return string.Format("Hello, my name is {0}.", this.Name); } } class Program { static void Main(string[] args) { //روش متداول var vahid = new Person { Name = "Vahid" }; Console.WriteLine(vahid.Speak()); var type = vahid.GetType(); //نمایش متدهای یک کلاس var methods = type.GetMethods(); foreach (var method in methods) { Console.WriteLine(method.Name); } //تغییر مقدار یک خاصیت var setNameMethod = type.GetMethod("set_Name"); setNameMethod.Invoke(obj: vahid, parameters: new[] { "Ali" }); //فراخوانی یک متد var speakMethod = type.GetMethod("Speak"); var result = speakMethod.Invoke(obj: vahid, parameters: null); Console.WriteLine(result); } } }
Hello, my name is Vahid. set_Name get_Name Speak ToString Equals GetHashCode GetType Hello, my name is Ali.
در اینجا یک کلاس شخص با خاصیت نام او تعریف شده است؛ به همراه متدی که رشتهای را نمایش خواهد داد.
در متد Main برنامه، ابتدا یک وهله جدید از این شخص ایجاد شده و سپس به روش متداول، متد Speak آن فراخوانی گردیده است. در ادامه کار از امکانات Reflection برای انجام همین امور کمک گرفته شده است.
کار با دریافت نوع یک وهله شروع میشود. برای نمونه در اینجا توسط vahid.GetType به نوع وهله ساخته شده دسترسی یافتهایم. سپس با داشتن این type، میتوان به کلیه امکانات Reflection دسترسی یافت. برای مثال توسط GetMethods، لیست کلیه متدهای موجود در کلاس شخص بازگشت داده میشود.
اگر به خروجی فوق دقت کنید، پس از سطر اول، 7 سطر بعدی نمایانگر متدهای موجود در کلاس شخص هستند. شاید عنوان کنید که این کلاس به نظر یک متد بیشتر ندارد. اما در دات نت اشیاء از شیء Object مشتق میشوند و چهار متد ToString، Equals، GetHashCode و GetType متعلق به آن هستند. همچنین خواص تعریف شده نیز در اصل به دو متد set و get به صورت خودکار در کدهای IL برنامه ترجمه خواهند شد. از همین متد set_Name در ادامه برای مقدار دهی خاصیت نام وهله ایجاد شده استفاده شده است.
همانطور که ملاحظه میکنید برای فراخوانی یک وهله از طریق Reflection، ابتدا توسط متد type.GetMethod میتوان به آن دسترسی یافت و سپس با فراخوانی متد Invoke، میتوان متد مدنظر را بر روی یک شیء مهیا با پارامترهایی که ذکر میکنیم، فراخوانی کرد. اگر این متد پارامتری ندارد، آنرا نال قرار خواهیم داد.
تا اینجا مقدمهای را ملاحظه نمودید که بیشتر جهت تکمیل بحث، حفظ روابط منطقی قسمتهای مختلف آن و یادآوری مباحث مرتبط با Reflection ذکر شدند.
ایجاد اشیاء در زمان اجرای برنامه
یکی از کلاسهای مهم Reflection که در منابع مختلف کمتر به آن پرداخته شده است، کلاس DynamicMethod آن است که از آن میتوان برای ایجاد اشیاء و یا متدهایی پویا در زمان اجرا استفاده کرد. این کلاس قرار گرفته در فضای نام System.Reflection.Emit، دارای یک ILGenerator است که میتوان به آن OpCodeهایی را اضافه کرد. زمانیکه کار ایجاد این متدپویا به پایان رسید، با استفاده از Delegates امکان دسترسی و اجرای این متد پویا وجود خواهد داشت.
یک مثال کامل را در این زمینه در ادامه ملاحظه مینمائید:
using System; using System.Reflection.Emit; namespace FastReflectionTests { class Program { static double Divider(int a, int b) { return a / b; } delegate double DividerDelegate(int a, int b); static void Main(string[] args) { //روش متداول Console.WriteLine(Divider(10, 2)); //تعریف امضای متد var myMethod = new DynamicMethod( name: "DividerMethod", returnType: typeof(double), parameterTypes: new[] { typeof(int), typeof(int) }, m: typeof(Program).Module); //تعریف بدنه متد var il = myMethod.GetILGenerator(); il.Emit(opcode: OpCodes.Ldarg_0); //بارگذاری پارامتر اول بر روی پشته ارزیابی il.Emit(opcode: OpCodes.Ldarg_1); //بارگذاری پارامتر دوم بر روی پشته ارزیابی il.Emit(opcode: OpCodes.Div); // دو پارامتر از پشته ارزیابی دریافت و تقسیم خواهند شد il.Emit(opcode: OpCodes.Ret); // دریافت نتیجه نهایی از پشته ارزیابی و بازگشت آن //فراخوانی متد پویا //روش اول var result = myMethod.Invoke(obj: null, parameters: new object[] { 10, 2 }); Console.WriteLine(result); //روش دوم var method = (DividerDelegate)myMethod.CreateDelegate(delegateType: typeof(DividerDelegate)); Console.WriteLine(method(10, 2)); } } }
در ابتدای این مثال جدید یک متد متداول تقسیم کننده دو عدد را ملاحظه میکنید. در ادامه قصد داریم overload دیگری از این متد را توسط کدهای MSIL در زمان اجرا ایجاد کنیم که دو پارامتر int را قبول میکند.
کار با وهله سازی کلاس DynamicMethod موجود در فضای نام System.Reflection.Emit شروع میشود. در اینجا کار تعریف امضای متد جدید باید صورت گیرد. برای مثال نام آن چیست، نوع خروجی آن کدام است. نوع پارامترهای آن چیست و نهایتا این متدی که قرار است به صورت پویا به برنامه اضافه شود، باید در کجا قرار گیرد. برای اینکار از Module خود کلاس Program برنامه استفاده شده است.
پس از تعریف امضای متد پویا، نوبت به تعریف بدنهی آن میرسد. کار با دریافت یک ILGenerator که میتوان در آن کدهای IL را وارد کرد شروع میشود. مابقی آن تعریف کدهای IL توسط متد Emit است و پیشتر با مقدمات اسمبلی دات نت در قسمتهای قبلی مبحث جاری آشنا شدهایم. ابتدا دو Ldarg فراخوانی شدهاند تا دو پارامتر ورودی متد را دریافت کنند. سپس Div بر روی آنها صورت گرفته و نهایتا نتیجه بازگشت داده شده است.
خوب؛ تا اینجا موفق شدیم اولین متد پویای خود را ایجاد نمائیم. برای اجرا آن حداقل دو روش وجود دارد:
الف) فراخوانی متد Invoke بر روی آن. با توجه به اینکه قرار نیست این متد بر روی وهلهی خاصی اجرا شود، اولین پارامتر آن null وارد شده است و سپس پارامترهای این متد پویا توسط آرگومان دوم متد Invoke وارد شدهاند.
ب) میتوان این عملیات را اندکی شکیلتر کرد. برای اینکار پیش از متد Main برنامه یک delegate به نام DividerDelegate تعریف شده است. سپس با استفاده از متد CreateDelegate، خروجی این متد پویا را تبدیل به یک delegate کردهایم. اینبار فراخوانی متد پویا بسیار شبیه به متدهای معمولی میشود.
اکثر متدهای این کلاس thread-safe طراحی شدهاند؛ اما با یک استثناء: متد GetOrAdd آن thread-safe نیست:
TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory);
بررسی نحوهی کار با متد GetOrAdd
این متد یک کلید را دریافت کرده و سپس بررسی میکند که آیا این کلید در مجموعهی جاری وجود دارد یا خیر؟ اگر کلید وجود داشته باشد، مقدار متناظر با آن بازگشت داده میشود و اگر خیر، delegate ایی که به عنوان پارامتر دوم آن معرفی شدهاست، اجرا خواهد شد، سپس مقدار بازگشت داده شدهی توسط آن به مجموعه اضافه شده و در آخر این مقدار به فراخوان بازگشت داده میشود.
var dictionary = new ConcurrentDictionary<string, string>(); var value = dictionary.GetOrAdd("key1", x => "item 1"); Console.WriteLine(value); value = dictionary.GetOrAdd("key1", x => "item 2"); Console.WriteLine(value);
item 1 item 1
دسترسی همزمان به متد GetOrAdd امن نیست
ConcurrentDictionary برای اغلب متدهای آن به صورت توکار مباحث قفلگذاری چند ریسمانی را اعمال میکند؛ اما نه برای متد GetOrAdd. زمانیکه valueFactory آن در حال اجرا است، دسترسی همزمان به آن thread-safe نیست و ممکن است بیش از یکبار فراخوانی شود.
یک مثال:
using System; using System.Collections.Concurrent; using System.Threading.Tasks; namespace Sample { class Program { static void Main(string[] args) { var dictionary = new ConcurrentDictionary<int, int>(); var options = new ParallelOptions { MaxDegreeOfParallelism = 100 }; var addStack = new ConcurrentStack<int>(); Parallel.For(1, 1000, options, i => { var key = i % 10; dictionary.GetOrAdd(key, k => { addStack.Push(k); return i; }); }); Console.WriteLine($"dictionary.Count: {dictionary.Count}"); Console.WriteLine($"addStack.Count: {addStack.Count}"); } } }
dictionary.Count: 10 addStack.Count: 13
علت اینجا است که در این بین، متد GetOrAdd توسط ترد A فراخوانی میشود، اما key را در دیکشنری جاری پیدا نمیکند. به همین جهت شروع به اجرای valueFactory آن خواهد کرد. در همین زمان ترد B نیز به دنبال همین key است. ترد قبلی هنوز به پایان کار خودش نرسیدهاست که مجددا valueFactory متعلق به همین key اجرا خواهد شد. به همین جهت است که در ConcurrentStack اجرا شدهی در valueFactory، بیش از 10 آیتم موجود هستند.
الگویی برای مدیریت دسترسی همزمان امن به متد GetOrAdd
یک روش برای دسترسی همزمان امن به متد GetOrAdd، توسط تیم ASP.NET Core به صورت ذیل ارائه شدهاست:
// 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the pipeline more // once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple // threads but only one of the objects succeeds in creating a pipeline. private readonly ConcurrentDictionary<Type, Lazy<RequestDelegate>> _pipelinesCache = new ConcurrentDictionary<Type, Lazy<RequestDelegate>>();
یک مثال:
namespace Sample { class Program { static void Main(string[] args) { var dictionary = new ConcurrentDictionary<int, Lazy<int>>(); var options = new ParallelOptions { MaxDegreeOfParallelism = 100 }; var addStack = new ConcurrentStack<int>(); Parallel.For(1, 1000, options, i => { var key = i % 10; dictionary.GetOrAdd(key, k => new Lazy<int>(() => { addStack.Push(k); return i; })); }); // Access the dictionary values to create lazy values. foreach (var pair in dictionary) Console.WriteLine(pair.Value.Value); Console.WriteLine($"dictionary.Count: {dictionary.Count}"); Console.WriteLine($"addStack.Count: {addStack.Count}"); } } }
10 1 2 3 4 5 6 7 8 9 dictionary.Count: 10 addStack.Count: 10
در این مثال دو تغییر صورت گرفتهاند:
الف) مقادیر ConcurrentDictionary به صورت Lazy معرفی شدهاند.
ب) متد GetOrAdd نیز یک مقدار Lazy را بازگشت میدهد.
زمانیکه از اشیاء Lazy استفاده میشود، خروجیهای بازگشتی از GetOrAdd، توسط این اشیاء Lazy محصور خواهند شد. اما نکتهی مهم اینجا است که هنوز value factory آنها فراخوانی نشدهاست. این فراخوانی تنها زمانی صورت میگیرد که به خاصیت Value یک شیء Lazy دسترسی پیدا کنیم و این دسترسی نیز به صورت thread-safe طراحی شدهاست. یعنی حتی اگر چند ترد new Lazy یک key مشخص را بازگشت دهند، تنها یکبار value factory متد GetOrAdd با دسترسی به خاصیت Value این اشیاء Lazy فراخوانی میشود و مابقی تردها منتظر مانده و تنها مقدار ذخیره شدهی در دیکشنری را دریافت میکنند و سبب اجرای مجدد value factory سنگین و زمانبر آن، نخواهند شد.
بر این مبنا میتوان یک LazyConcurrentDictionary را نیز به صورت ذیل طراحی کرد:
public class LazyConcurrentDictionary<TKey, TValue> { private readonly ConcurrentDictionary<TKey, Lazy<TValue>> _concurrentDictionary; public LazyConcurrentDictionary() { _concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>(); } public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) { var lazyResult = _concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value; } }
در مثال فوق، به صورت صریحی پارامتر LazyThreadSafetyMode نیز مقدار دهی شدهاست. هدف از آن اطمینان حاصل کردن از آغاز این شیء Lazy با دسترسی به خاصیت Value آن، تنها توسط یک ترد است.
نمونهی دیگر کار با خاصیت ویژهی Value شیء Lazy را در مطلب «پشتیبانی توکار از ایجاد کلاسهای Singleton از دات نت 4 به بعد» پیشتر در این سایت مطالعه کردهاید.
کار کردن با مسیریابی برای یک پروژه ساده ، نیاز به طراحی پیچیده ندارد. مسیریابی پیش فرض موجود در فایل RoutConfig.cs برای کارهای ابتدایی کافیست. اما اگر کمی کار پیچیده شود و صفحات مختلفی با منطقهای متفاوتی ایجاد کنیم، ممکن است با مشکل روبرو شویم. در MVC5 به کمک دخالت ویژگیها در مسیریابی، کار ساده شده است اما در MVC4 و قبل از آن چه باید کرد؟ پیش از بسط مساله، ابتدا این سوال را پاسخ میدهیم که چگونه صفحهی start پروژه انتخاب میشود؟
مسیریابی پیش فرض یک پروژه MVC به شکل زیر است :
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
وقتی یک پروژه MVC را بررسی کنید، مشاهده میکنید که در شاخهی اصلی آن، فایل index یا default وجود ندارد و اصولا منطق کار با اکشنها و صدا زدن آنها و دیدن پاسخها توسط View، این اتفاق را توجیه میکند. پس وقتی درخواستی به سمت شاخهی اصلی ما فرستاده میشود، مسیریابی وارد عمل میشود. وقتی پروژه را اجرا میکنید، متد RegisterRoutes از شیءایی که از کلاس RoutingConfig.cs ساخته شده (به فایل global.asax نگاه کنید) فراخوانی میشود و شیء Routes از " قالب آدرس " هایی که ما تعیین کرده ایم پُر میشود. بخشی از فایل RoutConfig.cs را در تصویر زیر میبینیم.
به Url کد فوق نگاه کنید که در حقیقت آدرسی نسبی است. آدرس ما به طور کامل به شکل زیر قابل تعریف است:
http://mydomain.com/{controller}/{action}/{id}
واضح است که درخواست اولیه به سایت ما دارای بخشهای controller و action و بخش اختیاری id نیست. پس به شکل پیشفرض بر اساس آنچه در جلوی خصوصیت defaults نوشته شده است، فراخوانی خواهد شد. یعنی اکشن Index از داخل کنترلر Home صدا زده میشود و چون id نداریم، هیچ نوع id به متد (اکشن) index ارسال نخواهد شد.
یک روتینگ دیگر به شکل زیر در بالای کد فوق اضافه کنید :
routes.MapRoute( name: "Default1", url: "{controller}/{action}/{id}", defaults: new { controller = "News", action = "Index", id = UrlParameter.Optional } );
حتما متوجه شدید که اینبار با اجرای پروژه، متد(اکشن) Index از کنترلر News فراخوانی خواهد شد. در حقیقت اولین روتینگ ساخته شده همان چیزیست که Asp.NET MVC پس از دریافت ریکوئست به آن رجوع میکند. دقت کنید که مقدار name در دو MapRouting فوق متفاوت است.
اما این اسمها(name ها) به چه دردی میخورند؟
قبل از توضیح در این مورد، default1 را از پروژه حذف میکنیم و با همان MapRouting پیش فرض به کار خود ادامه میدهیم. ابتدا مروری بر عملکرد MapRouting انجام میدهیم.
همانطور که میدانید برای ایجاد یک لینک از طریق ویوی Razor از چیزی شبیه دستور زیر میتوانیم استفاده کنیم:
<a href="@Url.Action("post", "News", new { id = @item.PostName }, null)"> @item.PostTitle </a>
فرض کنید یک سایت خبری داریم و گروههای مختلف آن شامل مطالب متفاوتی هستند. کنترلری به نام News ایجاد کردهایم، که دارای یک اکشن به نام post است که نام مطلب را دریافت کرده و یک View به ما برمیگرداند. بخش هایی از صفحه اول را در تصاویر زیر میبینید. تصویر آخر، بخش پایین صفحه اخبار است، که بوسیله لینک (لینک های) موجود صفحات جابجا میشوند.
وقتی کد فوق در یک صفحه فراخوانی میشود، کالکشن Routes بررسی میشود و لینک مورد نظر، با توجه به ورودیهای Url.Action ساخته میشود. ما یک MapRouting به نام Default در کالکشن Routes داریم، پس Url.Action لینک زیر را میسازد:
http://mydomain.com/news/post/نام-مقاله
استفاده از MapRouting مثل یک جعبه است که ورودی آن مقادیر موجود در Url.Action میباشد و آنچه به ما بر میگرداند یک آدرس برای فراخوانی اکشن مورد نیاز است. (آخرین پارامتر، null قرار داده شده که برای تعیین نوع پروتکل استفاده میشود که http یا https میتواند باشد.)
برای فراخوانی صفحهی اخبار (لیست اخبار) میتوانید دستور زیر را بنویسید :
@Html.ActionLink("اخبار", "Index", "News", null, new { @class="btn btn-success"})
این دستور تگ anchor را نیز میسازد و به کمک MapRouting آدرسی شبیه آدرس زیر را به ما بر میگرداند. (به تفاوت دو دستور دقت کنید):
<a href="/News">اخبار</a>
واضح است که چون MapRouting یک حالت پیش فرض درونی دارد که دارای اکشن دیفالت index است، پس ActionLink اگر ببیند لینکش در صفحه قرار است به شکل /news/index تعریف شود خود بخود بخش index را حذف میکند.
فرض کنید بعد از کلیک روی لینک فوق، اکشن index ، یک آبجکتِ لیستی با 10 خبر آخر، ایجاد میکند. پس ما میخواهیم قابلیت صفحه بندی را برای لیست اخبار فعال کنیم و در هر صفحه 10 مطلب را نمایش دهیم. مشکل از همینجا آغاز میشود. MapRouting فعلی جوابگوی ما نخواهد بود و آدرس را به شکل زیر نمایش میدهد.
<a href="/News/index?pid=2">صفحه بعد</a>
و آنچه ما در View نوشتهایم چیزی شبیه کد زیر است :
@Html.ActionLink("صفحه بعد", "index", "News", new { pid = …. }, null)
مشکلی در ارجاع به صفحات وجود ندارد و با کلیک روی لینک "صفحه بعد" مقدار عدد 2 به اکشن index ارسال میشود و اگر کد نویسی را برای take و skip کردن لیست، درست انجام شده باشد، نتیجه مورد نظر نمایش داده خواهد شد. اما آدرس فوق آدرس زیبایی نیست. اولین فکری که به ذهن برنامه نویس میرسد، ایجاد یک مسیریابی دیگر است. فکر درستیست؛ اما اگر چند بار دیگر این اتفاق بیفتد و در بخش هایی از برنامه نیاز به روتینگ پیدا کنید و روتینگهای جدید ایجاد کنید متوجه خواهید شد که مدیریت این MapRouting ها کار خسته کننده و طاقت فرسایی خواهد شد، مخصوصا اگر بدانید که فقط مجاز به استفاده از یک پارامتر optional در هر MapRouting هستید! دست شما کاملا بسته است. لینکهای بالای سایت را اصلاح میکنید ولی لینکهای پایین سایت خراب میشوند و بالعکس.
به هر حال MapRouting زیر را به RoutConfig.cs اضافه میکنیم :
اینکار بستگی به پروژهی شما دارد، مپ روتینگ فوق این مزیت را دارد که با مپ روتینگهای دیگر به سختی قاطی میشود! یعنی حتا اگر قبل از مپ روتینگ دیفالت نوشته شود برنامه با مشکل مواجه نخواهد شد، چون اصلا شکل درخواست اولیه به سایت، چیزی شبیه این آدرس نیست. اما خاص بودن آن و همچنین نوع بهره گیری از آن با کمک Action یا ActioLink شاید شما را سردرگم خواهد کند.- اسم این MapRouting ، دیگر Default نیست.routes.MapRoute("PostPaging", "{controller}/{action}/{id}/{pid}", defaults: new { controller = "News", action = "Index", id = "all", pid = UrlParameter.Optional } );
- یک پارامتر pid اضافهتر از MapRouting اولی دارد.
- pid به عنوان یک پارامتر اختیاری تعریف شده است، پس "قالب آدرس" بسیار شبیه مپ روتینگ قبلی است.
- مقدار id اختیاری نیست، چون قرار است در آینده بتوانیم گروههای مختلف موجود در بخش اخبار را صفحه بندی کنیم و قرار نیست پشت سر هم MapRouting ایجاد کنیم و کافیست به جای id اسم گروه را بنویسیم. در حالتیکه اسمی از گروه درلینکهایمان نبرده باشیم به شکل پیشفرض all قرار داده میشود که یعنی کل اخبار مد نظر است. (در اکشن مربوطه باید این تصمیمات را لحاظ کنیم)
- حتما این MapRouting را بعد از MapRouting اولیه بنویسید، کمی پیشتر، علت این امر توضیح داده شد و گفته شد اولین چیزی که MVC پس از درخواست ما میبیند به عنوان Routing بررسی میکند (درخواست اولیه) و چون ساختار MapRouting فوق تا اندازه ای شبیه ساختار Default MapRouting است ممکن است با فراخوانی سایت مشکل ایجاد شود.
- میتوانید MapRouting را کمی خاصتر هم بنویسیم :
routes.MapRoute("NewsPaging", "News/index/{id}/{pid}", defaults: new { controller = "News", action = "Index", id = "all", pid = UrlParameter.Optional } );
اما مشکل این MapRouting ها چیست؟
درخواست به سایت آمده و قرار است سایت بارگذاری شود؛ ترتیب زیر در شیء routes ثبت شده است :
در صفحه اول ما لینکی به شکل زیر گذاشته ایم :
@Html.ActionLink("اخبار", "Index", "News", null, new { @class="btn btn-success"})
در صفحه اخبار، لینک "صفحه بعد" وجود دارد ، آیا این لینک به شکل صحیح نمایش داده میشود؟ خیر! نتیجه کار را ببینید :
<a href="/News/index?pid=2">صفحه بعد</a>
به نظر میرسد یک طرح دقیق برای آدرس دهی شاید این مسائل را حل کند ولی نه وقت داریم و نه اعصاب.
1. ساخته شدن لینکها توسط هلپرها (که از مپ روتینگهای ثبت شده ما پیروی میکند)
2. واکنش پروژه به درخواستهای دریافتی و هدایت آن به اکشنهای مورد نظر که کاملا به ترتیب ثبت مپ روتینگها در کالکشن Routes بستگی دارد .
@Html.RouteLink("صفحه بعد", "PostPaging", new { action = "cat", controller = "News", id =. .., pid = ...})
نتیجه برای لینک موجود در صفحه اخبار چیزی شبیه شکل زیر خواهد شد :
<a href="/News/cat/all/2">صفحه بعد</a>
@Html.RouteLink("اخبار", "Default", new { controller = "news" }) @Html.RouteLink("درباره ما", "pages", new RouteValueDictionary(new { controller="Page", action="Index", pagename="درباره-ما"}), new Dictionary<string, Object> { { "data-toggle", "popover" }, { "data-placement", "top" } })
همانطور که میبینید در RoutLink
اولی، اخبار را به کمک MapRouting
با نام default
بازنویسی میکنیم و نتیجه چیزی شبیه کد زیر خواهد شد :
<a href="/News">اخبار</a>
در RoutLink دومی اولا از یک RoutValueDictionary به جای یک آبجکت ساده استفاده کرده ایم و مقادیر را به شکل فوق به کنترلر و اکشن و ...نسبت داده ایم ثانیا برای بخش HTML نیز پراپرتیها را به کمک یک دیکشنری ارسال میکنیم، به خاطر وجود "-" در یکی از خواص، راه دیگری غیر از اینکار نداریم.
اما دقت کنید که از یک MapRouting جدید استفاده کردیم که نامش pages است،
این MapRoutnig را قبل از دیگر Routing ها مینویسیم؟ وسط دو MapRouting قبلی مینویسیم؟ آخر MapRouting ها مینویسیم؟ آیا فرقی میکند؟ اگر سریع بگوییم خــیر! اشتباه کرده ایم. واقعا فرق میکند.
دقت کنید موضوع MapRouting فقط ایجاد یک لینکتر و تمیز نیست؛ RoutLink یک لینک تمیز بر اساس مپ روتینگی که نامش برده شده ایجاد میکند
اما تضمین نمیکند که با کلیک بر روی لینک به هدف برسیم و به خطای 404 برخورد نکنیم! اگر روی لینک کلیک کنید آدرس شروع به تفسیر شدن
میکند و این تفسیر اصلا ربطی به نامی که به RoutLink داده ایم ندارد و ترتیب موجود در کالکشن ایجاد شده در RoutConfig تعیین کننده است.(آبجکت Routes ) اگر MapRouting فوق را در انتهای بقیه بگذاریم صفحه اول لود میشود ولی با کلیک
روی "درباره ما" صفحه پیغام خطا خواهد داد.
باید به یاد
داشته باشیم برای اجرای درخواست (کلیک روی لینک)، آنچه برای ASP.NET MVC اهمیت دارد، ترتیب قرار گیری MapRouting ها در RouteRegister است و
ما به کمک RoutLink تنها
مشکل ساخت لینکها بر اساس قالب MapRouting مورد
نظرمان را حل کردیم و این به ما تضمینی برای هدایت آن لینک به مکان درست را نخواهد
داد.
اگر ترتیب به شکل زیر باشد :
1باشد. درخواست اولیه برای بالا آمدن سایت به مشکل برخورد نمیکند چون همان مپ روتینگ 1 اجرا میشود. اما مشکل فوق به وجود خواهد آمد و خطای 404 با کلیک بر روی "درباره ما" نمایش داده خواهد شد چون با کلیک روی "درباره ما" مپ روتینگ شماره 1 وارد عمل میشود.
2
3
اگر ترتیب به شکل زیر باشد :
3آیا اصلا صفحه اول سالم لود خواهد شد؟ خیر! درخواست نسبی " / " (یا به طور کامل http://mydomain.com ) شماره 3 را به خیر پشت سر میگذارد، چون اصلا چیزی به نام page در آدرس وجود ندارد که از این MapRouting بخواهد پیروی کند. اما در شماره 2 گیر میافتد چون این فرمت را حفظ کرده است :
2
1
"{controller}/{action}/{id}/{pid}"
اگر ترتیب به شکل زیر باشد :
3این همان چیزیست که مد نظر ماست. اولا 1 قبل از 2 است و صفحه اول برای لود شدن به مشکل برخورد نمیکند.
1
2
ثانیا وقتی روی "درباره ما" کلیک میکنیم همان شماره 3 فراخوانی میشود و بقیه مپ روتینگها اعمال نمیشوند.