window.location = row[1];
دریافت افزونهی jsTree
برای دریافت افزونهی jsTree میتوان به مخزن کد آن در Github مراجعه کرد و همچنین مستندات آنرا در سایت jstree.com قابل مطالعه هستند.
تنظیمات مقدماتی jsTree
در این مطلب فرض شدهاست که فایل jstree.min.js، در پوشهی Scripts و فایلهای CSS آن در پوشهی Content\themes\default کپی شدهاند.
به این ترتیب layout برنامه چنین شکلی را خواهد یافت:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link href="~/Content/Site.css" rel="stylesheet" /> <link href="~/Content/themes/default/style.min.css" rel="stylesheet" /> <script src="~/Scripts/jquery.min.js"></script> <script src="~/Scripts/jstree.min.js"></script> </head> <body dir="rtl"> @RenderBody() @RenderSection("scripts", required: false) </body> </html>
نمایش راست به چپ اطلاعات
در کدهای این افزونه به تگ body و ویژگی dir آن برای تشخیص راست به چپ بودن محیط دقت میشود. به همین جهت این تعریف را در layout فوق ملاحظه میکنید. برای مثال اگر به فایل jstree.contextmenu.js (موجود در مجموعه سورسهای این افزونه) مراجعه کنید، یک چنین تعریفی قابل مشاهده است:
right_to_left = $("body").css("direction") === "rtl";
تهیه ساختاری جهت ارائهی خروجی JSON
با توجه به اینکه قصد داریم به صورت پویا با این افزونه کار کنیم، نیاز است بتوانیم ساختار سلسله مراتبی مدنظر را با فرمت JSON ارائه دهیم. در ادامه کلاسهایی که معادل فرمت JSON قابل قبول توسط این افزونه را تولید میکنند، ملاحظه میکنید:
using System.Collections.Generic; namespace MvcJSTree.Models { public class JsTreeNode { public string id { set; get; } // نام این خواص باید با مستندات هماهنگ باشد public string text { set; get; } public string icon { set; get; } public JsTreeNodeState state { set; get; } public List<JsTreeNode> children { set; get; } public JsTreeNodeLiAttributes li_attr { set; get; } public JsTreeNodeAAttributes a_attr { set; get; } public JsTreeNode() { state = new JsTreeNodeState(); children = new List<JsTreeNode>(); li_attr = new JsTreeNodeLiAttributes(); a_attr = new JsTreeNodeAAttributes(); } } public class JsTreeNodeAAttributes { // به هر تعداد و نام اختیاری میتوان خاصیت تعریف کرد public string href { set; get; } } public class JsTreeNodeLiAttributes { // به هر تعداد و نام اختیاری میتوان خاصیت تعریف کرد public string data { set; get; } } public class JsTreeNodeState { public bool opened { set; get; } public bool disabled { set; get; } public bool selected { set; get; } public JsTreeNodeState() { opened = true; } } }
- هر چند اسامی مانند a_attr، مطابق اصول نامگذاری دات نت نیستند، ولی این نامها را تغییر ندهید. زیرا این افزونه دقیقا به همین نامها و با همین املاء نیاز دارد.
- id، میتواند دقیقا معادل id یک رکورد در بانک اطلاعاتی باشد. Text عنوان گرهای (node) است که نمایش داده میشود. icon در اینجا مسیر یک فایل png است جهت نمایش در کنار عنوان هر گره. توسط state میتوان مشخص کرد که زیر شاخهی جاری به صورت باز نمایش داده شود یا بسته. به کمک خاصیت children میتوان زیر شاخهها را تا هر سطح و تعدادی که نیاز است تعریف نمود.
- خاصیتهای li_attr و a_attr کاملا دلخواه هستند. برای مثال در اینجا دو خاصیت href و data را در کلاسهای مرتبط با آنها مشاهده میکنید. میتوانید در اینجا به هر تعداد ویژگی سفارشی دیگری که جهت تعریف یک گره نیاز است، خاصیت اضافه کنید.
سادهترین مثالی که از ساختار فوق میتواند استفاده کند، اکشن متد زیر است:
[HttpPost] public ActionResult GetTreeJson() { var nodesList = new List<JsTreeNode>(); var rootNode = new JsTreeNode { id = "dir", text = "Root 1", icon = Url.Content("~/Content/images/tree_icon.png"), a_attr = { href = "http://www.bing.com" } }; nodesList.Add(rootNode); nodesList.Add(new JsTreeNode { id = "test1", text = "Root 2", icon = Url.Content("~/Content/images/tree_icon.png"), a_attr = { href = "http://www.bing.com" } }); return Json(nodesList, JsonRequestBehavior.AllowGet); }
بنابراین ساختارهای خود ارجاع دهنده را به خوبی میتوان با این افزونه وفق داد.
فعال سازی اولیه سمت کلاینت افزونه jsTree
برای استفادهی پویای از این افزونه در سمت کلاینت، فقط نیاز به یک DIV خالی است:
<div id="jstree"> </div>
$('#jstree').jstree({ "core": { "multiple": false, "check_callback": true, 'data': { 'url': '@getTreeJsonUrl', "type": "POST", "dataType": "json", "contentType": "application/json; charset=utf8", 'data': function (node) { return { 'id': node.id }; } }, 'themes': { 'variant': 'small', 'stripes': true } }, "types": { "default": { "icon": '@Url.Content("~/Content/images/bookmark_book_open.png")' }, }, "plugins": ["contextmenu", "dnd", "state", "types", "wholerow", "sort", "unique"], "contextmenu": { "items": function (o, cb) { var items = $.jstree.defaults.contextmenu.items(); items["create"].label = "ایجاد زیر شاخه"; items["rename"].label = "تغییر نام"; items["remove"].label = "حذف"; var cpp = items["ccp"]; cpp.label = "ویرایش"; var subMenu = cpp["submenu"]; subMenu["copy"].label = "کپی"; subMenu["paste"].label = "پیست"; subMenu["cut"].label = "برش"; return items; } } });
- multiple : false به این معنا است که نمیخواهیم کاربر بتواند چندین گره را با نگه داشتن دکمهی کنترل انتخاب کند.
- check_callback : true کدهای مرتبط با منوی کلیک سمت راست ماوس را فعال میکند.
- در قسمت data کار تبادل اطلاعات با سرور جهت دریافت فرمت JSON ایی که به آن اشاره شد، انجام میشود. متغیر getTreeJsonUrl یک چنین شکلی را میتواند داشته باشد:
@{ ViewBag.Title = "Demo"; var getTreeJsonUrl = Url.Action(actionName: "GetTreeJson", controllerName: "Home"); }
- در قسمت types که مرتبط است با افزونهای به همین نام، آیکن پیش فرض یک نود جدید ایجاد شده را مشخص کردهایم.
- گزینهی plugins، لیست افزونههای اختیاری این افزونه را مشخص میکند. برای مثال contextmenu منوی کلیک سمت راست ماوس را فعال میکند، dnd همان کشیدن و رها کردن گرهها است در زیر شاخههای مختلف. افزونهی state، انتخاب جاری کاربر را در سمت کلاینت ذخیره و در مراجعهی بعدی او بازیابی میکند. با ذکر افزونهی wholerow سبب میشویم که انتخاب یک گره، معادل انتخاب یک ردیف کامل از صفحه باشد. افزونهی sort کار مرتب سازی خودکار اعضای یک زیر شاخه را انجام میدهد. افزونهی unique سبب میشود تا در یک زیر شاخه نتوان دو عنوان یکسان را تعریف کرد.
- در قسمت contextmenu نحوهی بومی سازی گزینههای منوی کلیک راست ماوس را مشاهده میکنید. در حالت پیش فرض، عناوینی مانند create، rename و امثال آن نمایش داده میشوند که به نحو فوق میتوان آنرا تغییر داد.
با همین حد تنظیم، این افزونه کار نمایش سلسله مراتبی اطلاعات JSON ایی دریافت شده از سرور را انجام میدهد.
ذخیره سازی گرههای جدید و تغییرات سلسله مراتب پویای تعریف شده در سمت سرور
همانطور که عنوان شد، اگر افزونهی اختیاری contextmenu را فعال کنیم، امکان افزودن، ویرایش و حذف گرهها و زیر شاخهها را خواهیم یافت. برای انتقال این تغییرات به سمت سرور، باید به نحو ذیل عمل کرد:
$('#jstree').jstree({ // تمام تنظیمات مانند قبل }).on('delete_node.jstree', function (e, data) { }) .on('create_node.jstree', function (e, data) { }) .on('rename_node.jstree', function (e, data) { }) .on('move_node.jstree', function (e, data) { }) .on('copy_node.jstree', function (e, data) { }) .on('changed.jstree', function (e, data) { }) .on('dblclick.jstree', function (e) { }) .on('select_node.jstree', function (e, data) { });
در تمام این حالات، جایی که data در اختیار ما است، میتوان یک چنین ساختار جاوا اسکریپتی را برای ارسال به سرور طراحی کرد:
function postJsTreeOperation(operation, data, onDone, onFail) { $.post('@doJsTreeOperationUrl', { 'operation': operation, 'id': data.node.id, 'parentId': data.node.parent, 'position': data.position, 'text': data.node.text, 'originalId': data.original ? data.original.id : data.node.original.id, 'href': data.node.a_attr.href }) .done(function (result) { onDone(result); }) .fail(function (result) { alert('failed.....'); onFail(result); }); }
.on('create_node.jstree', function (e, data) { postJsTreeOperation('CreateNode', data, function (result) { data.instance.set_id(data.node, result.id); }, function (result) { data.instance.refresh(); }); })
و معادل سمت سرور دریافت کنندهی این اطلاعات، اکشن متد ذیل میتواند باشد:
[HttpPost] public ActionResult DoJsTreeOperation(JsTreeOperationData data) { switch (data.Operation) { case JsTreeOperation.CopyNode: case JsTreeOperation.CreateNode: //todo: save data var rnd = new Random(); // آی دی رکورد پس از ثبت در بانک اطلاعاتی دریافت و بازگشت داده شود return Json(new { id = rnd.Next() }, JsonRequestBehavior.AllowGet); case JsTreeOperation.DeleteNode: //todo: save data return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); case JsTreeOperation.MoveNode: //todo: save data return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); case JsTreeOperation.RenameNode: //todo: save data return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); default: throw new InvalidOperationException(string.Format("{0} is not supported.", data.Operation)); } }
namespace MvcJSTree.Models { public enum JsTreeOperation { DeleteNode, CreateNode, RenameNode, MoveNode, CopyNode } public class JsTreeOperationData { public JsTreeOperation Operation { set; get; } public string Id { set; get; } public string ParentId { set; get; } public string OriginalId { set; get; } public string Text { set; get; } public string Position { set; get; } public string Href { set; get; } } }
در اینجا Href را نیز مشاهده میکنید. همانطور که عنوان شد، اعضای JsTreeNodeAAttributes اختیاری هستند. بنابراین اگر این اعضاء را تغییر دادید، باید خواص JsTreeOperationData و همچنین اعضای شیء تعریف شده در postJsTreeOperation را نیز تغییر دهید تا با هم تطابق پیدا کنند.
چند نکتهی تکمیلی
اگر میخواهید که با دوبار کلیک بر روی یک گره، کاربر به href آن هدایت شود، میتوان از کد ذیل استفاده کرد:
var selectedData; // ... .on('dblclick.jstree', function (e) { var href = selectedData.node.a_attr.href; alert('selected node: ' + selectedData.node.text + ', href:' + href); // auto redirect if (href) { window.location = href; } // activate edit mode //var inst = $.jstree.reference(selectedData.node); //inst.edit(selectedData.node); }) .on('select_node.jstree', function (e, data) { //alert('selected node: ' + data.node.text); selectedData = data; });
حتی اگر خواستید که با دوبار کلیک بر روی یک گره، گزینهی ویرایش آن فعال شود، کدهای آن را به صورت کامنت مشاهده میکنید.
مثال کامل این بحث را از اینجا میتوانید دریافت کنید:
MvcJSTree.zip
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" });
}
برپایی تنظیمات اولیهی سیستم مسیریابی در AngularJS 2.0
برای کار با سیستم مسیریابی AngularJS 2.0، ابتدا باید اسکریپتهای آن به صفحه اضافه شوند. در ادامه المان پایهای تعریف شده و سپس باید سرویس پروایدر مسیریابی را رجیستر کرد. جزئیات این موارد را در ادامه بررسی میکنیم:
الف) سرویس مسیریابی، جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن باید به صفحهی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفتهاست:
<!-- Required for routing --> <script src="~/node_modules/angular2/bundles/router.dev.js"></script>
ب) افزودن المان base به ابتدای صفحه:
<!DOCTYPE html> <html> <head> <base href="/">
از آنجائیکه فایل index.html در ریشهی سایت قرار گرفتهاست، مقدار آغازین href آن به / تنظیم شدهاست.
ج) شبیه به حالت ثبت پروایدر HTTP در قسمت قبل، برای ثبت پروایدر مسیریابی نیز به فایل App\app.component.ts مراجعه میکنیم:
//same as before ... import { ROUTER_PROVIDERS } from 'angular2/router'; //same as before ... @Component({ //same as before … providers: [ ProductService, HTTP_PROVIDERS, ROUTER_PROVIDERS ] }) //same as before ...
علت ختم شدن نام این سرویسها به PROVIDERS این است که این تعاریف، امکان استفادهی از چندین سرویس زیر مجموعهی آنها را فراهم میکنند و صرفا یک سرویس نیستند.
ساخت کامپوننت نمایش جزئیات محصولات
در ادامه میخواهیم جزئیات هر محصول را با کلیک بر روی نام آن در لیست محصولات، در آدرسی دیگر به صورتی مجزا مشاهده کنیم. به همین منظور به پوشهی products برنامه مراجعه کرده و دو فایل جدید product-detail.component.ts و product-detail.component.html را ایجاد کنید؛ با این محتوا:
الف) محتوای فایل product-detail.component.html
<div class='panel panel-primary'> <div class='panel-heading'> {{pageTitle}} </div> </div>
import { Component } from 'angular2/core'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; }
اگر دقت کنید، این کامپوننت ویژه دارای خاصیت selector نیست. ذکر selector تنها زمانی اجباری است که بخواهیم این کامپوننت را داخل کامپوننتی دیگر قرار دهیم. در اینجا قصد داریم این کامپوننت را به صورت یک View جدید، توسط سیستم مسیریابی نمایش دهیم و نه به صورت جزئی از یک کامپوننت دیگر.
افزودن تنظیمات مسیریابی به برنامه
مسیریابی در AngularJS 2.0، مبتنی بر کامپوننتها است. به همین جهت، ابتدای کار مسیریابی، مشخص سازی تعدادی از کامپوننتها هستند که قرار است به عنوان مقصد سیستم راهبری (navigation) مورد استفاده قرار گیرند و به ازای هر کدام، یک مسیریابی و Route جدید را تعریف میکنیم. تعریف هر Route جدید شامل انتساب نامی به آن، تعیین مسیر مدنظر و مشخص سازی کامپوننت مرتبط است:
{ path: '/products', name: 'Products', component: ProductListComponent },
این تنظیمات به عنوان یک متادیتای جدید دیگر به کلاس AppComponent، در فایل app.component.ts اضافه میشوند:
//same as before … import { ROUTER_PROVIDERS, RouteConfig } from 'angular2/router'; //same as before … @Component({ //same as before … }) @RouteConfig([ { path: '/welcome', name: 'Welcome', component: WelcomeComponent, useAsDefault: true }, { path: '/products', name: 'Products', component: ProductListComponent } ]) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
همانطور که ملاحظه میکنید، یک کلاس میتواند بیش از یک decorator داشته باشد.
()RouteConfig@ را به کامپوننتی الصاق میکنیم که قصد میزبانی مسیریابی را دارد (Host component). این مزین کننده، آرایهای از اشیاء را قبول میکند و هر شیء آن دارای خواصی مانند مسیر، نام و کامپوننت است. باید دقت داشت که نام هر مسیریابی تعریف شده باید pascal case باشد. در غیراینصورت مسیریاب ممکن است این نام را با path اشتباه کند.
همچنین امکان تعریف خاصیت دیگری به نام useAsDefault نیز در اینجا میسر است. از آن جهت تعریف مسیریابی پیش فرض سیستم، در اولین بار نمایش آن، استفاده میشود.
در اینجا نام کامپوننت، رشتهای ذکر نمیشود و دقیقا اشاره دارد به نام کلاس متناظر. بنابراین هر نام کلاسی که در اینجا اضافه میشود، باید به همراه import ماژول آن نیز در ابتدای فایل جاری باشد. به همین جهت اگر تنظیمات فوق را اضافه کنید، ذیل کلمهی WelcomeComponent یک خط قرمز مبتنی بر عدم تعریف آن کشیده میشود. برای تعریف آن، پوشهی جدیدی را به ریشهی سایت به نام home اضافه کنید و به آن دو فایل ذیل را اضافه نمائید:
الف) محتوای فایل welcome.component.ts
import { Component } from 'angular2/core'; @Component({ templateUrl: 'app/home/welcome.component.html' }) export class WelcomeComponent { public pageTitle: string = "Welcome"; }
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <h3 class="text-center">Default page</h3> </div> </div>
پس از تعریف این کامپوننت، اکنون باید import ماژول آنرا به ابتدای فایل app.component.ts اضافه کنیم، تا مشکل عدم شناسایی نام کلاس WelcomeComponent برطرف شود:
import { WelcomeComponent } from './home/welcome.component';
فعال سازی مسیریابیهای تعریف شده
روشهای مختلفی برای دسترسی به اجزای یک برنامه وجود دارند؛ برای مثال کلیک بر روی یک لینک، دکمه و یا تصویر و سپس فعال سازی مسیریابی متناظر با آن. همچنین کاربر میتواند آدرس صفحهای را مستقیما در نوار آدرسهای مرورگر وارد کند. به علاوه امکان کلیک بر روی دکمههای back و forward مرورگر نیز همواره وجود دارند. تنظیمات مسیریابیهای انجام شده، دو مورد آخر را به صورت خودکار مدیریت میکنند. در اینجا تنها باید مدیریت اولین حالت ذکر شده را با اتصال مسیریابیها به اعمال کاربران، انجام داد.
به همین جهت منویی را به بالای صفحهی برنامه اضافه میکنیم. برای این منظور، فایل app.component.ts را گشوده و خاصیت template کامپوننت AppComponent را به نحو ذیل تغییر میدهیم:
@Component({ //same as before … template: ` <div> <nav class='navbar navbar-default'> <div class='container-fluid'> <a class='navbar-brand'>{{pageTitle}}</a> <ul class='nav navbar-nav'> <li><a [routerLink]="['Welcome']">Home</a></li> <li><a [routerLink]="['Products']">Product List</a></li> </ul> </div> </nav> <div class='container'> <router-outlet></router-outlet> </div> </div> `, //same as before … })
سپس جهت تعریف لینکهای هر آیتم، از یک دایرکتیو توکار AngularJS 2.0 به نام routerLink استفاده میکنیم. هر routerLink به یکی از آیتمهای تنظیم شدهی در RouteConfig بایند میشود. بنابراین نامهایی که در اینجا قید شدهاند، دقیقا نامهایی هستند که در خاصیت name هر کدام از اشیاء تشکیل دهندهی RouteConfig، تعریف و مقدار دهی گردیدهاند.
اکنون اگر کاربر بر روی یکی از لینکهای Home و یا Product List کلیک کند، مسیریابی متناظر با آن فعال میشود (بر اساس این نام، در لیست عناصر RouteConfig جستجویی صورت گرفته و عنصر معادلی بازگشت داده میشود) و سپس View آن کامپوننت نمایش داده خواهد شد.
تا اینجا دایرکتیو جدید routerLink به قالب کامپوننت اضافه شدهاست؛ اما AngularJS 2.0 نمیداند که باید آنرا از کجا دریافت کند. به همین جهت ابتدا import آنرا (ROUTER_DIRECTIVES) به ابتدای ماژول جاری اضافه خواهیم کرد:
import { ROUTER_PROVIDERS, RouteConfig, ROUTER_DIRECTIVES } from 'angular2/router';
directives: [ROUTER_DIRECTIVES],
تا اینجا اگر دقت کرده باشید، کامپوننت نمایش لیست محصولات را از کامپوننت ریشهی سایت حذف کردهایم و بجای آن منوی بالای سایت را نمایش میدهیم که توسط آن میتوان به صفحهی آغازین و یا صفحهی نمایش لیست محصولات، رسید. به همین جهت خاصیت directives دیگر شامل ذکر کلاس کامپوننت لیست محصولات نیست.
در انتهای قالب کامپوننت ریشهی سایت، یک دایرکتیو جدید به نام router-outlet نیز تعریف شدهاست. وقتی یک کامپوننت فعال میشود، نیاز است View مرتبط با آن نیز نمایش داده شود. دایرکتیو router-outlet محل نمایش این View را مشخص میکند.
اکنون اگر برنامه را اجرا کنیم، به این شکل خواهیم رسید:
اگر دقت کنید، آدرس بالای صفحه، در اولین بار نمایش آن به http://localhost:2222/welcome تنظیم شده و این مقدار دهی بر اساس خاصیت useAsDefault تنظیمات مسیریابی سایت انجام شدهاست (نمایش welcome به عنوان صفحهی اصلی و پیش فرض).
همچنین با کلیک بر روی لینک لیست محصولات، کامپوننت آن فعال شده و نمایش داده میشود. محل قرارگیری این کامپوننتها، دقیقا در محل قرارگیری دایرکتیو router-outlet است.
ارسال پارامترها به سیستم مسیریابی
در ابتدا بحث، مقدمات کامپوننت نمایش جزئیات یک محصول انتخابی را تهیه کردیم. برای فعال سازی این کامپوننت و مسیریابی آن، نیاز است بتوان پارامتری را به سیستم مسیریابی ارسال کرد. برای مثال با انتخاب آدرس product/5، جزئیات محصول با ID مساوی 5 نمایش داده شود.
برای این منظور:
الف) اولین قدم، تعریف مسیریابی آن است. به همین جهت به فایل app.component.ts مراجعه و دو تغییر ذیل را به آن اعمال کنید:
//same as before … import { ProductDetailComponent } from './products/product-detail.component'; @Component({ //same as before … }) @RouteConfig([ //same as before … { path: '/product/:id', name: 'ProductDetail', component: ProductDetailComponent } ]) //same as before …
تفاوت این مسیریابی با نمونههای قبلی در تعریف id:/ است. پس از ذکر :/، نام یک متغیر عنوان میشود و اگر نیاز به چندین متغیر بود، همین الگو را تکرار خواهیم کرد.
ب) سپس نحوهی فعال سازی این مسیریابی را توسط تعریف لینکی جدید، معرفی میکنیم. بنابراین فایل قالب product-list.component.html را گشوده و سپس بجای نمایش عنوان محصول:
<td>{{ product.productName }}</td>
<td> <a [routerLink]="['ProductDetail', {id: product.productId}]"> {{product.productName}} </a> </td>
اکنون که از دایرکتیو جدید routerLink در این قالب استفاده شدهاست، نیاز است تعریف دایرکتیو آنرا به متادیتای کلاس کامپوننت لیست محصولات نیز اضافه کنیم تا AngularJS 2.0 بداند آنرا از کجا باید تامین کند:
import { Component, OnInit } from 'angular2/core'; import { ROUTER_DIRECTIVES } from 'angular2/router'; //same as before … @Component({ //same as before … directives: [StarComponent, ROUTER_DIRECTIVES] })
در ادامه اگر برنامه را اجرا کنید، عنوانهای محصولات، به آدرس نمایش جزئیات آنها لینک شدهاند:
ج) در آخر زمانیکه View نمایش جزئیات محصول فعال میشود، نیاز است این id را از url جاری دریافت کند. به همین جهت فایل product-detail.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { RouteParams } from 'angular2/router'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; constructor(private _routeParams: RouteParams) { let id = +this._routeParams.get('id'); this.pageTitle += `: ${id}`; } }
در این حالت، id دریافتی، به متغیر pageTitle اضافه شده و در قالب مربوطه به صورت خودکار نمایش داده میشود.
تا اینجا اگر برنامه را اجرا کنید، صفحهی نمایش جزئیات یک محصول، با کلیک بر روی عناوین آنها به صورت زیر نمایش داده میشود:
افزودن دکمهی back با کدنویسی
اکنون برای بازگشت مجدد به لیست محصولات، میتوان از دکمهی back مرورگر استفاده کرد، اما امکان طراحی این دکمه در قالبها نیز پیش بینی شدهاست.
برای این منظور قالب product-detail.component.html را به نحو ذیل بازنویسی میکنیم:
<div class='panel panel-primary'> <div class='panel-heading'> {{pageTitle}} </div> <div class='panel-footer'> <a class='btn btn-default' (click)='onBack()' style='width:80px'> <i class='glyphicon glyphicon-chevron-left'></i> Back </a> </div> </div>
سپس کدهای product-detail.component.ts را به صورت ذیل تکمیل خواهیم کرد:
import { Component } from 'angular2/core'; import { RouteParams, Router } from 'angular2/router'; @Component({ templateUrl: 'app/products/product-detail.component.html' }) export class ProductDetailComponent { pageTitle: string = 'Product Detail'; constructor(private _routeParams: RouteParams, private _router: Router) { let id = +this._routeParams.get('id'); this.pageTitle += `: ${id}`; } onBack(): void { this._router.navigate(['Products']); } }
رفع تداخل مسیریابیهای ASP.NET MVC با مسیریابیهای AngularJS 2.0
در طی بحث جاری عنوان شد که اگر کاربر مسیر http://localhost:2222/product/2 را جایی ثبت کرده یا bookmark کند، پس از فراخوانی مستقیم آن در نوار آدرسهای مرورگر، بلافاصله به این آدرس هدایت خواهد شد. این مورد صحیح است اگر از index.html بجای بکارگیری ASP.NET MVC، جهت هاست برنامه استفاده شود. اگر چنین آدرسی را در یک برنامهی ASP.NET MVC فراخوانی کنیم، ابتدا به دنبال کنترلری به نام product میگردد (ابتدا وارد موتور ASP.NET MVC میشود) و چون این کنترلر در سمت سرور تعریف نشدهاست، پیام 404 و یا یافت نشد را مشاهده خواهید کرد و فرصت به اجرای برنامهی AngularJS نخواهد رسید.
برای حل این مشکل نیاز است یک route جدید را به نام catch all، در انتهای مسیریابیهای فعلی اضافه کنید؛ تا سایر درخواستهای رسیده را به صفحهی نمایش برنامهی AngularJS هدایت کند:
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints: new { controller = "Home" } // for catch all to work, Home|About|SomeName ); // Route override to work with Angularjs and HTML5 routing routes.MapRoute( name: "NotFound", url: "{*catchall}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part9.zip
خلاصهی بحث
حین ایجاد کامپوننتها باید به نحوهی نمایش آنها نیز فکر کرد. اگر کامپوننتی قرار است داخل یک کامپوننت دیگر نمایش یابد، باید دارای selector باشد. یک چنین کامپوننتی نیاز به تعریف مسیریابی ندارد. برای کامپوننتهایی که به عنوان یک View مستقل طراحی میشوند و قرار است در یک صفحهی مجزا نمایش داده شوند، نیازی به تعریف selector نیست؛ اما باید برای آنها مسیریابیهای ویژهای را تعریف کرد. همچنین نیاز است مدیریت اعمال کاربران را جهت فعال سازی آنها نیز مدنظر داشت. برای استفاده از امکانات مسیریابی توکار AngularJS 2.0 نیاز است اسکریپت آنرا به صفحهی اصلی اضافه کرد. سپس باید المان base را جهت نحوهی تشکیل آدرسهای مسیریابی، به صفحه اضافه کرد. در ادامه کار ثبت ROUTER_PROVIDERS در بالاترین سطح سلسه مراتب کامپوننتهای سایت انجام میشود. با استفاده از RouteConfig کار تنظیمات ابتدایی مسیریابی صورت خواهد گرفت. این decorator به کامپوننتی که قرار است کار میزبانی مسیریابی را انجام دهد، متصل میشود. پس از تعریف مسیریابیها با ذکر یک نام منحصربفرد، مسیری که باید توسط کاربر وارد شود و نام کامپوننت مدنظر، با استفاده از دایرکتیو routerLink کار تعریف این آدرسها، در رابط کاربری برنامه انجام میشود. این دایرکتیو جدید، جزئی از مجموعهی ROUTER_DIRECTIVES است که باید به لیست دایرکتیوهای کامپوننت ریشههای سایت و هر کامپوننتی که از routeLink استفاده میکند، اضافه شود. برای بایند این دایرکتیو به مسیریابیهای تعریف شده، سمت راست این اتصال باید به آرایهای از مقادیر مقدار دهی شود که اولین عنصر آن، نام یکی از عناصر مسیریابی تعریف شدهی در RouteConfig است. از دومین عنصر آن برای مقدار دهی پارامترهای ارسالی به سیستم مسیریابی استفاده میشود. کار دایرکتیو router-outlet، مشخص سازی محل نمایش یک View است که عموما در قالب میزبان مسیریابی قرار میگیرد. برای تعیین پارامترهای مسیریابی، از الگوی paramName:/ استفاده میشود. برای دسترسی به این مقادیر در یک کامپوننت، میتوان از سرویس RouteParams استفاده کرد. برای فعال سازی یک مسیریابی با کدنویسی، از سرویس Router و متد navigate آن کمک میگیریم.
کتابخانه iTextSharp نمایش گرادیانی از رنگها را هم پشتیبانی میکند و بدیهی است این نمایش برداری است. روش استفاده از آن هم بسیار ساده است؛ مثلا:
PdfShading shading = PdfShading.SimpleAxial(pdfWriter, x0, y0, x1, y1, BaseColor.YELLOW, BaseColor.RED);
PdfShadingPattern pattern = new PdfShadingPattern(shading);
ShadingColor color = new ShadingColor(pattern);
متد PdfShading.SimpleAxial بر اساس شیء PdfWriter که توسط آن به Canvas صفحه دسترسی پیدا میکند، در مختصاتی مشخص، یک طیف رنگی را ایجاد میکند. بر این اساس میتوان به یک ShadingColor هم رسید که از آن مثلا به عنوان BackgroundColor یک PdfPCell قابل استفاده است.
تا اینجا ساده به نظر میرسد اما واقعیت این است که مختصات ذکر شده، مهم است و آنرا در مورد مثلا سلولهای یک جدول تنها در زمان تهیه نهایی یک جدول میتوان به دست آورد. البته اگر شیء سادهای را روی صفحه رسم کنیم، این مورد بر اساس مختصات ابتدایی شیء واضح به نظر میرسد.
برای حل این مشکل در مورد جداول و سلولهای آن خاصیتی به نام CellEvent وجود دارد که از نوع IPdfPCellEvent است. به عبارتی با ارسال یک وهله از کلاسی که اینترفیس IPdfPCellEvent را پیاده سازی میکند، میتوان در زمان نمایش نهایی یک سلول به مختصات دقیق آن دسترسی پیدا کرد.
یک مثال کامل را در مورد پیاده سازی IPdfPCellEvent و استفاده از آن جهت نمایش گرادیان در Header و footer یک جدول، در ادامه مشاهده خواهید نمود:
using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
namespace iTextSharpGradientTest
{
public class GradientCellEvent : IPdfPCellEvent
{
public void CellLayout(PdfPCell cell, Rectangle position, PdfContentByte[] canvases)
{
var cb = canvases[PdfPTable.BACKGROUNDCANVAS];
cb.SaveState();
var shading = PdfShading.SimpleAxial(
cb.PdfWriter,
position.Left, position.Top, position.Left, position.Bottom,
BaseColor.YELLOW, BaseColor.ORANGE);
var shadingPattern = new PdfShadingPattern(shading);
cb.SetShadingFill(shadingPattern);
cb.Rectangle(position.Left, position.Bottom, position.Width, position.Height);
cb.Fill();
cb.RestoreState();
}
}
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();
var table1 = new PdfPTable(1);
table1.HeaderRows = 2;
table1.FooterRows = 1;
//header row
var headerCell = new PdfPCell(new Phrase("header"))
{
HorizontalAlignment = Element.ALIGN_CENTER,
Border = 0
};
headerCell.CellEvent = new GradientCellEvent();
table1.AddCell(headerCell);
//footer row
var footerCell = new PdfPCell(new Phrase("footer"))
{
HorizontalAlignment = Element.ALIGN_CENTER,
Border = 0
};
footerCell.CellEvent = new GradientCellEvent();
table1.AddCell(footerCell);
//adding some rows
for (int i = 0; i < 70; i++)
{
var rowCell = new PdfPCell(new Phrase("Row " + i)) { BorderColor = BaseColor.LIGHT_GRAY };
table1.AddCell(rowCell);
}
pdfDoc.Add(table1);
}
//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}
با خروجی:
نکته مهم این مثال نحوه مقدار دهی CellEvent است. به این ترتیب در زمان نمایش نهایی یک سلول میتوان در متد CellLayout، به خواص فقط خواندنی آن سلول دسترسی یافت. مثلا position، مختصات نهایی مستطیل مرتبط با سلول جاری را بر میگرداند؛ یا از طریق canvases میتوان برای آخرین بار فرصت یافت تا در یک سلول نقاشی کرد.
اعتبار سنجی میشود (که هنگام استفاده remote validation مشکل دوچندان میشود)، که این شرایط میتواند برای کاربر گیج کننده یا گمراه کننده باشد؛
1) به تنظیمات کاربری رفته و روی تکست باکس نام مستعار کلیک کرده و یک مقدار تکراری وارد کنید (مثلا سیاوش). اکنون روی تکست باکس نام کاربری کلیک کنید: پیغام تکرای بودن نام مستعار نمایش داده میشود. حالا روی تکست باکس نام مستعار کلیک کنید، هر مقداری را که وارد نمایید پیغام تکرای بودن نام مستعار تغییر نمیکند (در کل هیچ نوع اعتبار سنجی انجام نمیگیرد!).
به نظر بنده، اگر موقع کلیک کردن (focus) روی نام مستعار، پیغام تکرای بودن .... پاک میشد، مشکل حل میشد!
معرفی سرویس MatDialog
توسط سرویس MatDialog میتوان modal dialogs بستهی Angular Material را نمایش داد که به همراه طراحی متریال و پویانمایی مخصوص آن است.
let dialogRef = dialog.open(UserProfileComponent, { height: '400px’, width: '600px’ });
dialogRef.afterClosed().subscribe(result => { console.log(`Dialog result: ${result}`); }); dialogRef.close('value');
در این مثال اگر dialogRef را با متد close و پارامتر value فراخوانی کنیم، سبب بسته شدن این دیالوگ خواهیم شد. این پارامتر در قسمت Dialog result پیام دریافتی پس از بسته شدن دیالوگ نیز قابل دسترسی است.
کامپوننتهایی که توسط سرویس MatDialog نمایش داده میشوند، میتوانند توسط سرویس جنریک MatDialogRef، صفحهی دیالوگ باز شده را ببندند:
@Component({/* ... */}) export class YourDialog { constructor(public dialogRef: MatDialogRef<YourDialog>) { } closeDialog() { this.dialogRef.close('Value….!’); } }
نحوهی طراحی یک دیالوگ نیز به کمک تعدادی کامپوننت و دایرکتیو میسر است:
<h2 mat-dialog-title>Delete all</h2> <mat-dialog-content>Are you sure?</mat-dialog-content> <mat-dialog-actions> <button mat-button mat-dialog-close>No</button> <!-- Can optionally provide a result for the closing dialog. --> <button mat-button [mat-dialog-close]="true">Yes</button> </mat-dialog-actions>
ایجاد دکمهی نمایش دیالوگ افزودن تماسها و کاربران جدید
قبل از هر کاری نیاز است دکمهی افزودن یک کاربر جدید را به صفحه اضافه کنیم. برای اینکار یک منوی ویژه را در سمت راست، بالای صفحه ایجاد میکنیم. بنابراین ابتدا به مستندات toolbar و menu مراجعه میکنیم تا با نحوهی تعریف دکمهها و منوها به toolbar آشنا شویم. سپس فایل قالب toolbar\toolbar.component.html را به صورت زیر تکمیل میکنیم:
<mat-toolbar color="primary"> <button mat-button fxHide fxHide.xs="false" (click)="toggleSidenav.emit()"> <mat-icon>menu</mat-icon> </button> <span>Contact Manager</span> <span fxFlex="1 1 auto"></span> <button mat-button [matMenuTriggerFor]="menu"> <mat-icon>more_vert</mat-icon> </button> <mat-menu #menu="matMenu"> <button mat-menu-item>New Contact</button> </mat-menu> </mat-toolbar>
سپس ابتدا یک mat-button را با آیکن more_vert (آیکن علامت بیشتر عمودی) تعریف کردهایم:
این دکمه توسط ویژگی matMenuTriggerFor به template reference variable ایی به نام menu متصل شدهاست تا با کلیک بر روی آن، این mat-menu را نمایش دهد:
ایجاد دیالوگ افزودن تماسها و کاربران جدید
پس از تعریف دکمه و منویی که سبب نمایش عبارت افزودن یک تماس جدید میشوند، به رخداد کلیک آن متدی را جهت نمایش صفحهی دیالوگ جدید اضافه میکنیم:
<button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
ng g c contact-manager/components/new-contact-dialog --no-spec
import { NewContactDialogComponent } from "./components/new-contact-dialog/new-contact-dialog.component"; @NgModule({ declarations: [ NewContactDialogComponent], entryComponents: [ NewContactDialogComponent ] }) export class ContactManagerModule { }
import { Component, EventEmitter, OnInit, Output } from "@angular/core"; import { MatDialog } from "@angular/material"; import { NewContactDialogComponent } from "../new-contact-dialog/new-contact-dialog.component"; @Component({ selector: "app-toolbar", templateUrl: "./toolbar.component.html", styleUrls: ["./toolbar.component.css"] }) export class ToolbarComponent implements OnInit { @Output() toggleSidenav = new EventEmitter<void>(); constructor(private dialog: MatDialog) { } ngOnInit() { } openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe(result => { console.log("The dialog was closed", result); }); } }
تکمیل قالب کامپوننت تماس جدید
در ادامه میخواهیم فرم افزودن یک تماس جدید را به همراه فیلدهای ورودی آن، به قالب new-contact-dialog.component.html اضافه کنیم:
<h2 mat-dialog-title>Add new contact</h2> <mat-dialog-content> <div fxLayout="column"> </div> </mat-dialog-content> <mat-dialog-actions> <button mat-button color="primary" (click)="save()"> <mat-icon>save</mat-icon> Save </button> <button mat-button color="primary" (click)="dismiss()"> <mat-icon>cancel</mat-icon> Cancel </button> </mat-dialog-actions>
import { Component, OnInit } from "@angular/core"; import { MatDialogRef } from "@angular/material"; @Component() export class NewContactDialogComponent implements OnInit { constructor( private dialogRef: MatDialogRef<NewContactDialogComponent> ) { } ngOnInit() { } save() { } dismiss() { this.dialogRef.close(null); } }
تا اینجا اگر برنامه را اجرا کنیم، به چنین شکلی خواهیم رسید:
تکمیل فیلدهای ورود اطلاعات فرم ثبت یک تماس جدید
تا اینجا ساختار فرم دیالوگ ثبت اطلاعات جدید را تکمیل کردیم. این فرم، به شیء user متصل خواهد شد. همچنین لیستی از avatars را هم جهت انتخاب، نمایش میدهد. به همین جهت این دو خاصیت عمومی را به کدهای کامپوننت آن اضافه میکنیم:
import { User } from "../../models/user"; @Component() export class NewContactDialogComponent implements OnInit { avatars = ["user1", "user2", "user3", "user4", "user5", "user6", "user7", "user8"]; user: User = { id: 0, birthDate: new Date(), name: "", avatar: "", bio: "", userNotes: null };
الف) فیلد نمایش و انتخاب avatar کاربر
<mat-form-field> <mat-select placeholder="Avatar" [(ngModel)]="user.avatar"> <mat-select-trigger> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.avatar }} </mat-select-trigger> <mat-option *ngFor="let avatar of avatars" [value]="avatar"> <mat-icon svgIcon="{{avatar}}"></mat-icon> {{ avatar }} </mat-option> </mat-select> </mat-form-field>
در اینجا از کامپوننت mat-select برای انتخاب avatar کاربر استفاده شدهاست که نتیجهی نهایی انتخاب آن به خاصیت user.avatar متصل شدهاست.
گزینههای این لیست (mat-option) بر اساس آرایهی avatars که در کامپوننت تعریف کردیم، تامین میشوند که در اینجا از mat-icon برای نمایش آیکن مرتبط نیز استفاده شدهاست. در این مورد در قسمت قبل چهارم، بخش «بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material» بیشتر توضیح داده شدهاست.
کار mat-select-trigger، سفارشی سازی برچسب نمایشی این کنترل است.
ب) فیلد دریافت نام کاربر به همراه اعتبارسنجی آن
<mat-form-field> <input matInput placeholder="Name" #name="ngModel" [(ngModel)]="user.name" required> <mat-error *ngIf="name.invalid && name.touched">You must enter a name</mat-error> </mat-form-field>
در اینجا فیلد نام کاربر، به user.name متصل و همچنین توسط ویژگی required، پر کردن آن الزامی اعلام شدهاست. به همین جهت در تصویر فوق یک ستاره را نیز کنار آن مشاهده میکند که به صورت خودکار توسط Angular Material نمایش داده شدهاست.
از کامپوننت mat-error برای نمایش خطاهای اعتبارسنجی یک فیلد استفاده میشود که نمونهای از آنرا در اینجا با بررسی خواص invalid و touched فیلد نام که بر اساس ویژگی required فعال میشوند، مشاهده میکنید. بدیهی است در اینجا به هر تعدادی که نیاز است میتوان mat-error را قرار داد.
ج) فیلد دریافت تاریخ تولد کاربر توسط یک date picker
<mat-form-field> <input matInput [matDatepicker]="picker" placeholder="Born" [(ngModel)]="user.birthDate"> <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle> <mat-datepicker #picker></mat-datepicker> </mat-form-field>
در اینجا از کامپوننت mat-datepicker برای انتخاب تاریخ تولید یک شخص استفاده شدهاست و نتیجهی آن به خاصیت user.birthDate متصل خواهد شد.
برای افزودن آن ابتدا یک mat-datepicker را به mat-form-field اضافه میکنیم. سپس یک template reference variable را به آن نسبت خواهیم داد. از آن هم در فیلد ورودی با انتساب آن به ویژگی matDatepicker و هم در کامپوننت mat-datepicker-toggle که سبب نمایش آیکن انتخاب تقویم میشود، در ویژگی for آن استفاده خواهیم کرد.
د) فیلد چند سطری دریافت توضیحات و شرححال کاربر
<mat-form-field> <textarea matInput placeholder="Bio" [(ngModel)]="user.bio"></textarea> </mat-form-field>
در اینجا برای دریافت توضیحات چندسطری، از یک text area استفاده شدهاست که به خاصیت user.bio متصل است.
بنابراین همانطور که ملاحظه میکنید، روش طراحی فرمهای Angular Material ویژگیهای خاص خودش را دارد:
- دایرکتیو matInput را میتوان به المانهای استاندارد input و textarea اضافه کرد تا داخل mat-form-field نمایش داده شوند. این mat-form-field است که کار اعمال CSS ویژهی طراحی متریال را انجام میدهد و امکان نمایش پیامهای خطای اعتبارسنجی و پویانمایی ورود اطلاعات را سبب میشود.
- قسمت mat-dialog-content را توسط fxLayout به حالت ستونی تنظیم کردیم:
<mat-dialog-content> <div fxLayout="column"> </div> </mat-dialog-content>
برای مثال اگر خواستید المانهای فرم با فاصلهی بیشتری از هم قرار بگیرند، میتوان از fxLayoutGap استفاده کرد که در مورد آن در قسمت دوم «معرفی Angular Flex layout» بیشتر توضیح داده شد.
تکمیل سرویس کاربران جهت ذخیرهی اطلاعات تماس کاربر جدید
در ادامه میخواهیم با کلیک کاربر بر روی دکمهی Save، ابتدا این اطلاعات به سمت سرور ارسال و سپس در سمت سرور ذخیره شوند. پس از آن، Id این کاربر جدید به سمت کلاینت بازگشت داده شود، دیالوگ جاری بسته و در آخر این شیء جدید به لیست تماسهای نمایش دادهی شدهی در sidenav اضافه گردد.
الف) تکمیل سرویس Web API سمت سرور
در ابتدا متد Post را به Web API برنامه جهت ذخیره سازی اطلاعات User ارسالی از سمت کلاینت اضافه میکنیم. کدهای کامل آنرا از فایل پیوستی انتهای بحث میتوانید دریافت کنید:
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class UsersController : Controller { private readonly IUsersService _usersService; public UsersController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpPost] public async Task<IActionResult> Post([FromBody] User user) { if (!ModelState.IsValid) { return BadRequest(ModelState); } await _usersService.AddUserAsync(user); return Created("", user); } } }
ب) تکمیل سرویس کاربران سمت کلاینت
سپس به فایل user.service.ts مراجعه کرده و دو تغییر زیر را به آن اضافه میکنیم:
@Injectable({ providedIn: "root" }) export class UserService { private usersSource = new BehaviorSubject<User>(null); usersSourceChanges$ = this.usersSource.asObservable(); constructor(private http: HttpClient) { } addUser(user: User): Observable<User> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post<User>("/api/users", user, { headers: headers }).pipe( map(response => { const addedUser = response || {} as User; this.notifyUsersSourceHasChanged(addedUser); return addedUser; }), catchError((error: HttpErrorResponse) => throwError(error)) ); } notifyUsersSourceHasChanged(user: User) { this.usersSource.next(user); } }
return Created("", user);
بنابراین نیاز است از طریق این سرویس به کامپوننت sidenav، در مورد تغییرات لیست کاربران اطلاعات رسانی کنیم که روش کار آنرا پیشتر در مطلب «صدور رخدادها از سرویسها به کامپوننتها در برنامههای Angular» نیز مرور کردهایم. برای این منظور یک BehaviorSubject از نوع User را تعریف کردهایم که اشتراک به آن از طریق خاصیت عمومی usersSourceChanges میسر است. هر زمانیکه متد next آن فراخوانی شود، تمام مشترکین به آن، از افزوده شدن کاربر جدید، به همراه اطلاعات کامل آن مطلع خواهند شد.
ج) تکمیل متد save کامپوننت new-contact-dialog
پس از تکمیل سرویس کاربران جهت افزودن متد addUser به آن، اکنون میتوانیم از آن در کامپوننت دیالوگ افزودن اطلاعات تماس جدید استفاده کنیم:
import { UserService } from "../../services/user.service"; @Component() export class NewContactDialogComponent { user: User = { id: 0, birthDate: new Date(), name: "", avatar: "", bio: "", userNotes: null }; constructor( private dialogRef: MatDialogRef<NewContactDialogComponent>, private userService: UserService ) { } save() { this.userService.addUser(this.user).subscribe(data => { console.log("Saved user", data); this.dialogRef.close(data); }); } }
د) تکمیل کامپوننت sidenav جهت واکنش نشان دادن به افزوده شدن اطلاعات تماس جدید
اکنون که سرویس کاربران به صفحه دیالوگ افزودن اطلاعات یک تماس جدید متصل شدهاست، نیاز است بتوانیم اطلاعات کاربر جدید را به لیست تماسهای sidenav اضافه کنیم. به همین جهت به sidenav.component مراجعه کرده و مشترک usersSourceChanges سرویس کاربران خواهیم شد:
import { UserService } from "../../services/user.service"; @Component() export class SidenavComponent implements OnInit, OnDestroy { users: User[] = []; subscription: Subscription | null = null; constructor( private userService: UserService) { } ngOnInit() { this.subscription = this.userService.usersSourceChanges$.subscribe(user => { if (user) { this.users.push(user); } }); } ngOnDestroy() { if (this.subscription) { this.subscription.unsubscribe(); } } }
استفاده از کامپوننت Snackbar جهت نمایش موفقیت آمیز بودن ثبت اطلاعات
متد save کامپوننت دیالوگ یک تماس جدید را به صورت زیر تکمیل کردیم:
save() { this.userService.addUser(this.user).subscribe(data => { console.log("Saved user", data); this.dialogRef.close(data); });
openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe(result => { console.log("The dialog was closed", result); }); }
کدهای کامل این تغییرات را در ذیل مشاهده میکنید:
@Component() export class ToolbarComponent { @Output() toggleSidenav = new EventEmitter<void>(); constructor(private dialog: MatDialog, private snackBar: MatSnackBar, private router: Router) { } openAddContactDialog(): void { const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" }); dialogRef.afterClosed().subscribe((result: User) => { console.log("The dialog was closed", result); if (result) { this.openSnackBar(`${result.name} contact has been added.`, "Navigate").onAction().subscribe(() => { this.router.navigate(["/contactmanager", result.id]); }); } }); } openSnackBar(message: string, action: string): MatSnackBarRef<SimpleSnackBar> { return this.snackBar.open(message, action, { duration: 5000, }); } }
برای گشودن snackbar که نمونهای از آنرا در تصویر فوق ملاحظه میکنید، ابتدا نیاز است سرویس MatSnackBar را به سازندهی کلاس تزریق کرد. سپس توسط آن میتوان یک کامپوننت مستقل را همانند دیالوگها نمایش داد و یا میتوان یک متن را به همراه یک Action منتسب به آن، به کاربر نمایش داد؛ مانند متد openSnackBar که در کامپوننت فوق از آن استفاده میشود. این متد در رخداد پس از بسته شدن dialog، نمایش داده شدهاست.
پارامتر اول آن پیامی است که توسط snackbar نمایش داده میشود و پارامتر دوم آن، برچسب دکمه مانندی است کنار این پیام، که سبب انجام عملی خواهد شد و در اینجا به آن Action گفته میشود. برای مدیریت آن باید متد onAction را فراخوانی کرد و مشترک آن شد. در این حالت اگر کاربر بر روی این دکمهی action کلیک کند، سبب هدایت خودکار او به صفحهی نمایش جزئیات اطلاعات تماس کاربر خواهیم شد. به همین جهت سرویس Router نیز به سازندهی کلاس تزریق شدهاست تا بتوان از متد navigate آن استفاده کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-05.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
همانطور که میدانیم پلاگینهای جیکوئری، نقش مهمی را در محیط وب ایفا میکنند. در اینجا با یکی از این پلاگینها و چگونگی استفاده از آن آشنا میشویم.
برای آشنایی با نوشتن Plugin در jQuery، میتوان مباحث پیشین این سایت را دنبال کرد.( JQuery Plugins #1 و JQuery Plugins #2)
این Plugin برای ایجاد یک TextBox برای ورود زمان توسط کاربر استفاده میشود. با توجه به اینکه قبلاً چند Plugin برای این کار نوشته شده است ولی هر کدام از آنها معایب و مزایای خاص خود را داشتند، برای نمونه میتوانید به این سایت مراجعه کنید.
ویژگیهای این Plugin عبارتند از:
1- تنظیم زمان پیش فرض
2- کنترل حداقل و حداکثر زمان وارد شده
3- تغییر ساعت و دقیقه بوسیله کلیدهای جهتی بالا و پایین
4- تغییر انتخاب ساعت و دقیقه بوسیله کلیدهای جهتی چپ و راست
5- تغییر ساعت و دقیقه بوسیله فشردن اعداد روی صفحه کلید
چگونگی استفاده از این Plugin
ابتدا کتابخانه jQuery و این پلاگین را به
صفحه خود اضافه نمایید و سپس کدهای زیر را برای استفاده از این Plugin اضافه نمایید:
jQuery(document).ready(function () { $("#TextBox1").TickTack(); $("#TextBox2").TickTack({ initialTime: '8:44', minHour: 8, minMinute: 0, maxHour: 22, maxMinute: 40 }); });
initialTime : زمان اولیه جهت نمایش به کاربر (حتما بایستی ساعت و دقیقه بوسیله ‘:’ از یکدیگر جداشوند)
minHour : حداقل ساعت ورودی
minMinute : حداقل دقیقه ورودی
maxHour : حداکثر ساعت ورودی
maxMinute : حداکثر دقیقه ورودی
پس از انتخاب TextBox، قسمت ساعت به صورت پیش فرض انتخاب میشود و کاربر باید ساعت مد نظر را وارد کند؛ در اینجا، عدد اول ساعت، مد نظر است.
برای نمونه در اینجا عدد 2 توسط کاربر وارد میشود؛ پس از ورود عدد و با توجه به تنظیمات انجام شده، ساعت به صورت اتوماتیک به حداکثر مقداری که میتواند بپذیرد تغییر میکند (در این مثال چون کاربر عدد 2 را وارد کرده و در تنظیمات انجام شده حداکثر ساعت دریافتی 22 و حداکثر دقیقه 40 تعریف شده است، ساعت به صورت پیشفرض به 22:40 تغییر مییابد)
و پس از وارد کردن عدد دوم ساعت توسط کاربر مکان نما به قسمت دقیقه منتقل میشود که در این جا عدد اول دقیقه مد نظر است
وارد کردن عدد 3 برای دقیقه
وارد کردن عدد دوم دقیقه
پس از وارد کردن کامل دقیقه مکان نما دوباره به قسمت ساعت باز میگردد.
در ادامه دوستان علاقمند لطفا جهت بهبود کیفیت کار، باگ و یا مشکلات کدنویسی را اطلاع دهند.
با تشکر
نحوهی ذخیره سازی لاگها
سطح سایت
- Client IP address
- User name
- Date
- Time
- Service and instance
- Server name
- Server IP address
- Time taken
- Client bytes sent
- Server bytes sent
- (Service status code (A value of 200 indicates that the request was fulfilled successfully
- (Windows status code (A value of 0 indicates that the request was fulfilled successfully.
- Request type
- Target of operation
- (Parameters (the parameters that are passed to a script
192.168.114.201, -, 03/20/01, 7:55:20, W3SVC2, SERVER, 172.21.13.45, 4502, 163, 3223, 200, 0, GET, /DeptLogo.gif, -,
نام فیلد | نوع حالت مقداردهی | توضیح اتفاقات افتاده | |
Client IP address | 192.168.114.201 | آی پی کلاینت | |
User name | - | کاربر ناشناس است | |
Date | 03/20/01 | تاریخ فعالیت | |
Time | 7:55:20 | ساعت فعالیت | |
Service and instance | W3SVC2 | لاگی که مربوط به سایت خاصی میشود به صورت #W3SVC نمایش داده میشود که علامت # شماره سایت میباشد که در اینجا این لاگ مربوط به سایت شماره 2 است | |
Server name | SERVER | نام سرور | |
Server IP | 172.21.13.45 | آی پی سرور | |
Time taken | 4502 | چقدر انجام عملیات این درخواست به طول انجامیده است که بر حسب میلی ثانیه است. | |
Client bytes sent | 163 | تعداد بایت هایی که از طرف کلاینت به سرور ارسال شده است | |
Server bytes sent | 3223 | تعداد بایت هایی که از طرف سرور به سمت کلاینت ارسال شده است | |
Service status code | 200 | درخواست کاملا موفقیت آمیز بوده است | |
Windows status code | 0 | درخواست کاملا موفقیت آمیز بوده است | |
Request type | GET | نوع درخواست کاربر | |
Target of operation | /DeptLogo.gif | کاربر قصد دانلود یک فایل تصویری GIF داشته است که نامش Deptlogo است | |
Parameters | - | پارامتری ارسال نشده است |
فرمت NCSA: این فرمت توسط مرکز علمی کاربردهای ابرمحاسباتی National Center for Supercomputing Applications ایجاد شده و دقیقا مانند قبلی نمیتوان در آن نوع فیلدها را مشخص کرد و برای جدا سازی، از فاصله space استفاده میکند و ثبت مقدار زمان در آن هم به صورت محلی و هم UTC میباشد.
- Remote host address
- (Remote log name (This value is always a hyphen
- User name
- Date, time, and Greenwich mean time (GMT) offset
- Request and protocol version
- (Service status code (A value of 200 indicates that the request was fulfilled successfully
- Bytes sen
نمونه ای از یک لاگ ثبت شده:
172.21.13.45 - Microsoft\JohnDoe [08/Apr/2001:17:39:04 -0800] "GET /scripts/iisadmin/ism.dll?http/serv HTTP/1.0" 200 3401
نام فیلد | مقدار ثبت شده | توضیح اتفاق افتاده |
Remote host address | 172.21.13.45 | آی پی کلاینت |
Remote log name | - | نامی وجود ندارد |
User name | Microsoft\JohnDoe | نام کاربری |
Date, time, and GMT offset | [08/Apr/2001:17:39:04 -0800] | تاریخ و ساعت فعالیت به صورت محلی که 8 ساعت از مبدا گرینویچ بیشتر است |
Request and protocol version | GET /scripts/iisadmin/ism.dll?http/serv HTTP/1.0 | کاربر با متد GET و Http نسخهی یک، درخواست فایل ism.dll را کرده است. |
Service status code | 200 | عملیات کاملا موفقیت آمیز بود. |
Bytes sent | 3401 | تعداد بایتهای ارسال شده به سمت کاربر |
امنیت در برابر کاربران مهاجم مانند همان فرمت قبلی صورت گرفته است.
فرمت W3C: توسط W3C توسط کنسرسیوم جهانی وب ارائه شده است و یک فرمت customizable ASCII text-based است. به این معنی که میتوان فیلدهایی که در گزارش نهایی میآید را خودتان مشخص کنید، که برای اینکار در کنار لیست، دکمهی Select وجود دارد که میتوانید هر کدام از فیلدهایی را که خواستید، انتخاب کنید تا به ترتیب در خط لاگ ظاهر شوند. تاریخ ثبت به صورت UTC است.
نام فیلد | توضیح | به طور پیش فرض انتخاب شده است |
Date | تاریخ رخ دادن فعالیت | بله |
Time | ساعت زخ دادن فعالیت بر اساس UTC | بله |
Client IP Address | آی پی کلاینت | بله |
User Name | نام کاربری که هویت آن تایید شده و در صورتی که هویت تایید شده نباشد و کاربر ناشناس باشد، جای آن - قرار میگیرد | بله |
Service Name and Instance Number | نام و شماره سایتی که درخواست در آن صورت گرفته است | خیر |
Server Name | نام سروری که لاگ روی آن ثبت میشود | خیر |
Server IP Address | آی پی سرور که لاگ روی آن ثبت میشود | بله |
Server Port | شمره پورتی که سرویس مورد نظر روی آن پورت اعمال میشود. | بله |
Method | متد درخواست مثل GET | بله |
URI Stem | هدف درخواست یا Target مثل index.htm | بله |
URI Query | کوئری ارسال شده برای صفحات داینامیک | بله |
HTTP Status | کد وضیعینی HTTP status | بله |
Win32 Status | کد وضعیتی ویندوز | خیر |
Bytes Sent | تعداد بایتهای ارسال شده به سمت کلاینت | خیر |
Bytes Received | تعداد بایتهای دریافت شده از سمت کلاینت | خیر |
Time Taken | زمان به طول انجامیدن درخواست بر حسب میلی ثانیه | خیر |
Protocol Version | درخواست با چه نسخهای از پروتکل http یا ftp ارسال شده است | خیر |
Host | اگر در هدر درخواست ارسالی این گزینه بوده باشد، نوشته خواهد شد. | خیر |
User Agent | اطلاعات را از هدر درخواست میگیرد. | بله |
Cookie | اگر کوکی رد و بدل شده باشد، محتویات کوکی ارسالی یا دریافت شده | خیر |
Referrer | کاربر از چه سایتی به سمت سایت ما آمده است. | خیر |
Protocol Substatus | در صورت رخ دادن خطا در IIS ، کد خطا بازگردانده میشود. در IIS به منظور امنیت بیشتر و کاهش حملات، محتوای خطاهای رخ داده در IIS به صورت متنی نمایش داده نمیشوند و شامل کد خطایی به اسم Substatus Code هستند تا مدیران شبکه با ردیابی لاگها پی به دلیل خطا و درخواستهای ناموفق ببرند. برای مثال Error 404.2 به این معنی است که فایل درخواستی به دلیل قوانین محدود کنده، قفل شده و قابل ارائه نیست. ولی هکر تنها با خطای 404 یعنی وجود نداشتن فایل روبرو میشود. در حالت substatus code، کد شماره 2 را هم خواهید داشت که در لاگ ثبت میشود. هر شخصی که در سرور توانایی دسترسی به لاگها را داشته باشد، میتواند کد دوم خطا را نیز مشاهده کند. برای مثال مدیر سرور متوجه میشود که یکی از فایلهای مورد نظر به کاربران، خطای 404 نمایش میدهد و با بررسی لاگها متوجه میشود که کد خطا 404.9 هست. از آنجا که ما همهی کدها را حفظ نیستیم به این صفحه رجوع میکنیم و متوجه میشویم تعداد کاربرانی که برای این فایل، اتصال connection ایجاد کردهاند بیش از مقدار مجاز است و مدیر میتواند این وضع را کنترل کند. برای مثال تعداد اتصالات مجاز را نامحدود unlimited تعیین کند. | بله |
- uri-query
- host
- (User-Agent)
- Cookie
- Referrer
- substatus
گزینه Custom : موقعی که شما این گزینه را انتخاب کنید ماژول logging غیرفعال خواهد شد. زیرا این امکان در IIS قابل پیکربندی نیست و نوشتن ماژول آن بر عهده شما خواهد بود؛ با استفاده از اینترفیس های ILogPlugin ، ILogPluginEx و ILogUIPlugin آن را پیاده سازی کنید.
ذخیره اطلاعات به انکدینگ UTF-8 و موضوع امنیت
using System.Data.Entity.Core.Metadata.Edm;
public class SoftDeleteAttribute : Attribute { public string ColumnName { get; set; } public SoftDeleteAttribute(string column) { ColumnName = column; } public static string GetSoftDeleteColumnName(EdmType type) { MetadataProperty column = type.MetadataProperties.Where(x => x.Name.EndsWith("customannotation:SoftDeleteColumnName")).SingleOrDefault(); return column == null ? null : (string)column.Value; } }
سؤال: متادیتای "customannotation:SoftDeleteColumnName" از کجا آمد؟ برای پاسخ به این سوال کافیست ادامهی مطلب را کامل مطالعه نمایید.
[SoftDelete("IsDeleted")] public class TblUser { [Key] public int TblUserID { get; set; } [MaxLength(30)] public string Name { get; set; } public bool IsDeleted { get; set; } }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { var Conv = new AttributeToTableAnnotationConvention<SoftDeleteAttribute, string>( "SoftDeleteColumnName", (type, attribute) => attribute.Single().ColumnName); modelBuilder.Conventions.Add(Conv); }
در پنجره باز شده به قسمت سوم یعنی <StorageModels> مراجعه نمایید و بایستی گزینه زیر را مشاهده نمایید .
<EntityType Name="TblUser" customannotation:SoftDeleteColumnName="IsDeleted">
تا اینجای کار ما توانستیم یک Annotation جدید را به Ef اضافه نماییم .
در مرحله بعد بایستی به Ef دستور دهیم که در تولید Query بر روی این Entity، این مورد را نیز لحاظ کند.
برای این کار کلاسی را ایجاد مینماییم که از اینترفیس IDbCommandTreeInterceptor ارث بری مینماید. مانند کد زیر :
public class SoftDeleteInterceptor : IDbCommandTreeInterceptor { public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext) { if (interceptionContext.OriginalResult.DataSpace == System.Data.Entity.Core.Metadata.Edm.DataSpace.SSpace) { var QueryCommand = interceptionContext.Result as DbQueryCommandTree; if (QueryCommand != null) { var newQuery = QueryCommand.Query.Accept(new SoftDeleteQueryVisitor()); interceptionContext.Result = new DbQueryCommandTree(QueryCommand.MetadataWorkspace, QueryCommand.DataSpace, newQuery); } } } }
در ابتدا تشخیص داده میشود که نوع خروجی Query آیا از نوع Storage Model است . ( برای توضیحات بیشتر ) سپس پرس و جوی تولید شده را با استفاده از الگوی visitor تغییر داده و Query جدید را تولید نموده و در انتها Query جدیدی را به جای Query قبلی جایگزین مینماییم.
در اینجا ما نیاز به داشتن کلاس SoftDeleteQueryVisitor برای تغییر دادن Query و اضافه نمودن IsDeleted <>1 به Query میباشیم.
یک کلاس دیگری با نام SoftDeleteQueryVisitor به شکل زیر به برنامه اضافه مینماییم.
public class SoftDeleteQueryVisitor : DefaultExpressionVisitor { public override DbExpression Visit(DbScanExpression expression) { var column = SoftDeleteAttribute.GetSoftDeleteColumnName(expression.Target.ElementType); if (column!=null) { var Binding = DbExpressionBuilder.Bind(expression); return DbExpressionBuilder.Filter(Binding, DbExpressionBuilder.NotEqual(DbExpressionBuilder.Property(DbExpressionBuilder.Variable(Binding.VariableType, Binding.VariableName), column), DbExpression.FromBoolean(true))); } else { return base.Visit(expression); } } }
در نهایت برای اینکه EF تشخیص دهد که یکچنین Interceptor ایی وجود دارد، بایستی در کلاس DbContextConfig، کلاس SoftDeleteInterceptor را اضافه نماییم؛ همانند کد زیر:
public class DbContextConfig : DbConfiguration { public DbContextConfig() { AddInterceptor(new SoftDeleteInterceptor()); } }
تا اینجا در تمام Queryهای تولید شده بر روی Entity که با خاصیت SoftDelete مزین شده است، مقدار IsDeleted <> 1 را به صورت اتوماتیک اعمال مینماید. حتی به صورت هوشمند چنانچه این موجودیت در یک Join استفاده شده باشد این شرط را قبل از Join به Query تولید شده اضافه مینماید.
در مقاله بعدی در مورد تغییر کد Remove به کد Update توضیح داده خواهد شد.
برای مطالعه بیشتر
Entity Framework: Building Applications with Entity Framework 6