ASP.NET MVC #20
تهیه گزارشات تحت وب به کمک WebGrid
WebGrid از ASP.NET MVC 3.0 به صورت توکار به شکل یک Html Helper در دسترس میباشد و هدف از آن سادهتر سازی تهیه گزارشات تحت وب است. البته این گرید، تنها گرید مهیای مخصوص ASP.NET MVC نیست و پروژه MVC Contrib یا شرکت Telerik نیز نمونههای دیگری را ارائه دادهاند؛ اما از این جهت که این Html Helper، بدون نیاز به کتابخانههای جانبی در دسترس است، بررسی آن ضروری میباشد.
صورت مساله
لیستی از کارمندان به همراه حقوق ماهیانه آنها در دست است. اکنون نیاز به گزارشی تحت وب، با مشخصات زیر میباشد:
1- گزارش باید دارای صفحه بندی بوده و هر صفحه تنها 10 ردیف را نمایش دهد.
2- سطرها باید یک در میان دارای رنگی متفاوت باشند.
3- ستون حقوق کارمندان در پایین هر صفحه، باید دارای جمع باشد.
4- بتوان با کلیک بر روی عنوان هر ستون، اطلاعات را بر اساس ستون انتخابی، مرتب ساخت.
5- لینکهای حذف یا ویرایش یک ردیف نیز در این گزارش مهیا باشد.
6- لیست تهیه شده، دارای ستونی به نام «ردیف» نیست. این ستون را نیز به صورت خودکار اضافه کنید.
7- لیست نهایی اطلاعات، دارای ستونی به نام مالیات نیست. فقط حقوق کارمندان ذکر شده است. ستون محاسبه شده مالیات نیز باید به صورت خودکار در این گزارش نمایش داده شود. این ستون نیز باید دارای جمع پایین هر صفحه باشد.
8- تمام اعداد این گزارش در حین نمایش باید دارای جدا کننده سه رقمی باشند.
9- تاریخهای موجود در لیست، میلادی هستند. نیاز است این تاریخها در حین نمایش شمسی شوند.
10- انتهای هر صفحه گزارش باید بتوان برچسب «صفحه y/n» را مشاهده کرد. n در اینجا منظور تعداد کل صفحات است و y شماره صفحه جاری میباشد.
11- انتهای هر صفحه گزارش باید بتوان برچسب «رکوردهای y تا x از n» را مشاهده کرد. n در اینجا منظور تعداد کل رکوردها است.
12- نام کوچک هر کارمند، ضخیم نمایش داده شود.
13- به ازای هر شماره کارمندی، یک تصویر در پوشه images سایت وجود دارد. برای مثال images/id.jpg. ستونی برای نمایش تصویر متناظر با هر کارمند نیز باید اضافه شود.
14- به ازای هر کارمند، تعدادی پروژه هم وجود دارد. پروژههای متناظر را توسط یک گرید تو در تو نمایش دهید.
راه حل به کمک استفاده از WebGrid
ابتدا یک پروژه خالی ASP.NET MVC را آغاز کنید. سپس مدلهای زیر را به آن اضافه نمائید (یک کارمند که میتواند تعداد پروژه منتسب داشته باشد):
using System;
using System.Collections.Generic;
namespace MvcApplication17.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime AddDate { get; set; }
public double Salary { get; set; }
public IList<Project> Projects { get; set; }
}
}
namespace MvcApplication17.Models
{
public class Project
{
public int Id { set; get; }
public string Name { set; get; }
}
}
سپس منبع داده نمونه زیر را به پروژه اضافه کنید. به عمد از ORM خاصی استفاده نشده تا بتوانید پروژه جاری را به سادگی در یک پروژه آزمایشی جدید، تکرار کنید.
using System;
using System.Collections.Generic;
namespace MvcApplication17.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
var rnd = new Random();
for (int i = 1; i <= 1000; i++)
{
list.Add(new Employee
{
Id = i + 1000,
FirstName = "fName " + i,
LastName = "lName " + i,
AddDate = DateTime.Now.AddYears(-rnd.Next(1, 10)),
Salary = rnd.Next(400, 3000),
Projects = CreateRandomProjects()
});
}
return list;
}
private static IList<Project> CreateRandomProjects()
{
var list = new List<Project>();
var rnd = new Random();
for (int i = 0; i < rnd.Next(1, 7); i++)
{
list.Add(new Project
{
Id = i,
Name = "Project " + i
});
}
return list;
}
}
}
در ادامه یک کنترلر جدید را با محتوای زیر اضافه نمائید:
using System.Web.Mvc;
using MvcApplication17.Models;
namespace MvcApplication17.Controllers
{
public class HomeController : Controller
{
[HttpPost]
public ActionResult Delete(int? id)
{
return RedirectToAction("Index");
}
[HttpGet]
public ActionResult Edit(int? id)
{
return View();
}
[HttpGet]
public ActionResult Index(string sort, string sortdir, int? page = 1)
{
var list = EmployeeDataSource.CreateEmployees();
return View(list);
}
}
}
علت تعریف متد index با پارامترهای sort و غیره به URLهای خودکاری از نوع زیر بر میگردد:
http://localhost:3034/?sort=LastName&sortdir=ASC&page=3
همانطور که ملاحظه میکنید، گرید رندر شده، از یک سری کوئری استرینگ برای مشخص سازی صفحه جاری، یا جهت مرتب سازی (صعودی و نزولی بودن آن) یا فیلد پیش فرض مرتب سازی، کمک میگیرد.
سپس یک View خالی را نیز برای متد Index ایجاد کنید. تا اینجا تنظیمات اولیه پروژه انجام شد.
کدهای کامل View را در ادامه ملاحظه میکنید:
@using System.Globalization
@model IList<MvcApplication17.Models.Employee>
@{
ViewBag.Title = "Index";
}
@helper WebGridPageFirstItem(WebGrid grid)
{
@(((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1));
}
@helper WebGridPageLastItem(WebGrid grid)
{
if (grid.TotalRowCount < (grid.PageIndex + 1 * grid.RowsPerPage))
{
@grid.TotalRowCount;
}
else
{
@((grid.PageIndex + 1) * grid.RowsPerPage);
}
}
<h2>Employees List</h2>
@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
<div id="container">
@grid.GetHtml(
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
mode: WebGridPagerModes.All,
columns: grid.Columns(
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
)
)
<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount
@*
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
*@
</div>
@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}
توضیحات ریز جزئیات View فوق
تعریف ابتدایی شیء WebGrid و مقدار دهی آن
در ابتدا نیاز است یک وهله از شیء WebGrid را ایجاد کنیم. در اینجا میتوان تنظیم کرد که آیا نیاز است اطلاعات نمایش داده شده دارای صفحه بندی (canPage) خودکار باشند؟ منبع داده (source) کدام است. در صورت فعال سازی صفحه بندی خودکار، چه تعداد ردیف (rowsPerPage) در هر صفحه نمایش داده شود. آیا نیاز است بتوان با کلیک بر روی سر ستونها، اطلاعات را بر اساس فیلد متناظر با آن مرتب (canSort) ساخت؟ همچنین در صورت نیاز به مرتب سازی، اولین باری که گرید نمایش داده میشود، بر اساس چه فیلدی (defaultSort) باید مرتب شده نمایش داده شود:
@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
در اینجا همچنین سه متغیر کمکی هم تعریف شده که از اینها برای تهیه جمع ستونهای حقوق و مالیات و همچنین نمایش شماره ردیف جاری استفاده میشود. فرمول نحوه محاسبه اولین ردیف هر صفحه را هم ملاحظه میکنید. شماره ردیفهای بعدی، rowIndex++ خواهند بود.
تعریف رنگ و لعاب گرید نمایش داده شده
در ادامه به کمک متد grid.GetHtml، رشتهای معادل اطلاعات HTML صفحه جاری، بازگشت داده میشود. در اینجا میتوان یک سری خواص تکمیلی را تنظیم نمود. برای مثال:
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
هر کدام از این رشتهها در حین رندر نهایی گرید، تبدیل به یک class خواهند شد. برای نمونه:
<div id="container">
<table class="webgrid" id="MyGrid">
<thead>
<tr class="webgrid-header">
به این ترتیب با اندکی ویرایش css سایت، میتوان انواع و اقسام رنگها را به سطرها و ستونهای گرید نهایی اعمال کرد. برای مثال اطلاعات زیر را به فایل css سایت اضافه نمائید:
/* Styles for WebGrid
-----------------------------------------------------------*/
.webgrid
{
width: 100%;
margin: 0px;
padding: 0px;
border: 0px;
border-collapse: collapse;
font-family: Tahoma;
font-size: 9pt;
}
.webgrid a
{
color: #000;
}
.webgrid-header
{
padding: 0px 5px;
text-align: center;
border-bottom: 2px solid #739ace;
height: 20px;
border-top: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-header th
{
background-color: #eaf0ff;
border-right: 1px solid #ddd;
}
.webgrid-footer
{
padding: 6px 5px;
text-align: center;
background-color: #e8eef4;
border-top: 2px solid #3966A2;
height: 25px;
border-bottom: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-alternating-row
{
height: 22px;
background-color: #f2f2f2;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-row-style
{
height: 22px;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-selected-row
{
font-weight: bold;
}
.text-align-center-col
{
text-align: center;
}
.total-row
{
background-color:#f9eef4;
}
همانطور که ملاحظه میکنید، رنگهای ردیفها، هدر و فوتر گرید و غیره در اینجا تنظیم میشوند.
به علاوه اگر دقت کرده باشید در تعاریف گرید، htmlAttributes هم مقدار دهی شده است. در اینجا به کمک یک anonymously typed object، مقدار id گرید مشخص شده است. از این id در حین کار با jQuery استفاده خواهیم کرد.
تعیین نوع Pager
پارامتر دیگری که در متد grid.GetHtml تنظیم شده است، mode: WebGridPagerModes.All میباشد. WebGridPagerModes یک enum با محتوای زیر است و توسط آن میتوان نوع Pager گرید را تعیین کرد:
[Flags]
public enum WebGridPagerModes
{
Numeric = 1,
//
NextPrevious = 2,
//
FirstLast = 4,
//
All = 7,
}
نحوه تعریف ستونهای گرید
اکنون به مهمترین قسمت تهیه گزارش رسیدهایم. در اینجا با مقدار دهی پارامتر columns، نحوه نمایش اطلاعات ستونهای مختلف مشخص میگردد. مقداری که باید در اینجا تنظیم شود، آرایهای از نوع WebGridColumn میباشد و مرسوم است به کمک متد کمکی grid.Columns، اینکار را انجام داد.
متد کمکی grid.Column، یک وهله از شیء WebGridColumn را بر میگرداند و از آن برای تعریف هر ستون استفاده خواهیم کرد. توسط پارامتر columnName آن، نام فیلدی که باید اطلاعات ستون جاری از آن اخذ شود مشخص میشود. به کمک پارامتر header، عبارت سرستون متناظر تنظیم میگردد. پارامتر format، مهمترین و توانمندترین پارامتر متد grid.Column است:
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
پارامتر format، به نحو زیر تعریف شده است:
Func<dynamic, object> format
به این معنا که هر بار پیش از رندر سطر جاری، زمانیکه قرار است سلولی رندر شود، یک شیء dynamic در اختیار شما قرار میگیرد. این شیء dynamic یک رکورد از اطلاعات Model جاری است. به این ترتیب به اطلاعات تمام سلولهای ردیف جاری دسترسی خواهیم داشت. بر این اساس هر نوع پردازشی را که لازم بود، انجام دهید (شبیه به فرمول نویسی در ابزارهای گزارش سازی، اما اینبار با کدهای سی شارپ) و مقدار فرمت شده نهایی را به صورت یک رشته بر گردانید. این رشته نهایتا در سلول جاری درج خواهد شد.
اگر از پارامتر فرمت استفاده نشود، همان مقدار فیلد جاری بدون تغییری رندر میگردد.
حداقل به دو نحو میتوان پارامتر فرمت را مقدار دهی کرد:
format: @<span style='font-weight: bold'>@item.FirstName</span>
or
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
}
مستقیما از توانمندیهای Razor استفاده کنید. مثلا یک تگ کامل را بدون نیاز به محصور سازی آن بین "" شروع کنید. سپس @item به وهلهای از رکورد در دسترس اشاره میکند که در اینجا وهلهای از شیء کارمند است.
و یا همانند روشی که برای محاسبه جمع حقوق هر صفحه مشاهده میکنید، مستقیما از lambda expressions برای تعریف یک anonymous delegate استفاده کنید.
نحوه اضافه کردن ستون ردیف
ستون ردیف، یک ستون محاسبه شده (calculated field) است:
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
نیازی نیست حتما یک grid.Column، به فیلدی در کلاس کارمند اشاره کند. مقدار سفارشی آن را به کمک پارامتر format تعیین خواهیم کرد. هر بار که قرار است یک ردیف رندر شود، یکبار این پارامتر فراخوانی خواهد شد. فرمول محاسبه rowIndex ابتدای صفحه را نیز پیشتر ملاحظه نمودید.
نحوه اضافه کردن ستون سفارشی تصاویر کارمندها
ستون تصویر کارمندها نیز مستقیما در کلاس کارمند تعریف نشده است. بنابراین میتوان آنرا با مقدار دهی صحیح پارامتر format ایجاد کرد:
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
در این مثال، تصاویر کارمندها در پوشه images واقع در ریشه سایت، قرار دارند. به همین جهت از متد Url.Content برای مقدار دهی صحیح آن استفاده کردیم. به علاوه در اینجا @item.Id به Id رکورد در حال رندر اشاره میکند.
نحوه تبدیل تاریخها به تاریخ شمسی
در ادامه بازهم به کمک پارامتر format، یک وهله از شیء dynamic اشاره کننده به رکورد در حال رندر را دریافت میکنیم. سپس فرصت خواهیم داشت تا بر این اساس، فرمول نویسی کنیم. دست آخر هم رشته مورد نظر نهایی را بازگشت میدهیم:
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),
اضافه کردن ستون سفارشی مالیات
در کلاس کارمند، خاصیت حقوق وجود دارد اما مالیات خیر. با توجه به آن میتوانیم به کمک پارامتر format، به اطلاعات شیء dynamic در حال رندر دسترسی داشته باشیم. بنابراین به اطلاعات حقوق دسترسی داریم و سپس با کمی فرمول نویسی، مقدار نهایی مورد نظر را بازگشت خواهیم داد. همچنین در اینجا میتوان نحوه بازگشت مقدار حقوق را به صورت رشتهای حاوی جدا کنندههای سه رقمی نیز مشاهده کرد:
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
اضافه کردن گردیدهای تو در تو
متد Grid.GetHtml، یک رشته را بر میگرداند. بنابراین در هر چند سطح که نیاز باشد میتوان یک گرید را بر اساس اطلاعات دردسترس رندر کرد و سپس بازگشت داد:
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
در اینجا کار اصلی از طریق پارامتر format شروع میشود. سپس به کمک item.Projects به لیست پروژههای هر کارمند دسترسی خواهیم داشت. بر این اساس یک گرید جدید را تولید کرد و سپس رشته معادل با آن را به کمک متد subGrid.GetHtml دریافت و بازگشت میدهیم. این رشته در سلول جاری درج خواهد شد. به نوعی یک گزارش master detail یا sub report را تولید کردهایم.
اضافه کردن دکمههای ویرایش، حذف و انتخاب
هر سه دکمه ویرایش، حذف و انتخاب در ستونهایی سفارشی قرار خواهند گرفت. بنابراین مقدار دهی header و format متد grid.Column کفایت میکند:
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
نکته جدیدی که در اینجا وجود دارد متد item.GetSelectLink میباشد. این متد جزو متدهای توکار گرید است و کار آن بازگشت دادن شیء grid.SelectedRow میباشد. این شیء پویا، حاوی اطلاعات رکورد انتخاب شده است. برای مثال اگر نیاز باشد این اطلاعات به صفحهای ارسال شود، میتوان از روش زیر استفاده کرد:
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
نمایش برچسبهای صفحه x از n و رکوردهای x تا y از z
در یک گزارش خوب باید مشخص باشد که صفحه جاری، کدامین صفحه از چه تعداد صفحه کلی است. یا رکوردهای صفحه جاری چه بازهای از تعداد رکوردهای کلی را تشکیل میدهند. برای این منظور چند متد کمکی به نامهای WebGridPageFirstItem و WebGridPageLastItem تهیه شدهاند که آنها را در ابتدای View ارائه شده، مشاهده نمودید:
<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount
نمایش جمع ستونهای حقوق و مالیات در هر صفحه
گرید توکار همراه با ASP.NET MVC در این مورد راه حلی را ارائه نمیدهد. بنابراین باید اندکی دست به ابتکار زد. مثلا:
@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}
در این مثال به کمک jQuery با توجه به اینکه id گرید ما MyGrid است، یک ردیف سفارشی که همان جمع محاسبه شده است، به tbody جدول نهایی تولیدی اضافه میشود. از tbody:first هم در اینجا استفاده شده است تا ردیف اضافه شده به گریدهای تو در تو اعمال نشود.
سپس فایل Views\Shared\_Layout.cshtml را گشوده و از section تعریف شده، برای مقدار دهی master page سایت، استفاده نمائید:
<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>
@RenderSection("script", required: false)
</head>
ما طراحی میکنیم تا علاوه بر نیازهای عملیاتی، به نیازهای غیر عملیاتی (Non Functional Requirements) نیز فکر کنیم؛ در حالیکه در زمان برنامه نویسی صرفا به Functionality فکر میکنیم.
کتاب Object Oriented Design Heuristics اولین کتاب در زمینه طراحی و توسعه شیء گرا میباشد. خواندن آن برای برنامه نویسان در هر رده ای که هستند، مفید خواهد بود و میتوانند از این Heuristicها (قواعد شهودی) به عنوان ابزاری برای تبدیل شدن به یک توسعه دهنده برتر، استفاده کنند.
در این کتاب بیشتر، بهبود طراحی شیء گرا هدف قرار داده شدهاست و در این راستا بیش از 60 دستورالعمل که هیچ وابستگی به زبان خاصی هم ندارند، ارائه شده است. قواعد شهودی در واقع قوانین سخت گیرانهای نیستند. بلکه میتوان آنها را به عنوان یک مکانیزم هشدار در نظر گرفت که در زمان نیاز حتی میتوان آنها را نقض کرد.
پیشنهاد میکنم حداقل برای اینکه ادبیات فنی خود را سامان ببخشید و با ادبیات یکسانی باهم صحبت کنیم، این کتاب را مطالعه کنید.
Introduction to Classes and Objects
پارادایم شیء گرا از مفاهیم کلاس و آبجکت، به عنوان بلوکهای ساختاری پایهای در شکل گیری یک مدل سازگار و استوار برای تحلیل، طراحی و پیاده سازی نرم افزار، استفاده میکند.
این مفاهیم را با یک مثال واقعی، بهتر میتوان شرح داد. یک اتاق پر از جمعیت را درنظر بگیرید؛ اگر شما میپرسیدید «چه تعداد از حاضرین در این اتاق میتوانند یک ساعت زنگدار(alarm clock ) را با در دست داشتن تمام قطعات آن، بسازند؟» در بهترین حالت یک یا دو نفر تمایل داشتند دست خود را بالا ببرند. اگر در همین اتاق میپرسیدید، «چه تعداد از حاضرین در این اتاق میتوانند یک ساعت زنگدار را برای ساعت 9 صبح تنظیم کنند؟» بدون شک بیشتر جمعیت تمایل داشتند دست خود را بالا ببرند.
آیا نامعقول نیست که این تعداد جمعیت زیاد، ادعا دارند که میتوانند از ساعت زنگدار استفاده کنند، درحالیکه حتی نمیتوانند یک ساعت زنگدار بسازند؟ پاسخ بی درنگ برای این سوال «البته که نه! سوال شما نامعقول است» میباشد.
در دنیای واقعی خیلی چیزها هستند که ما میتوانیم از آنها استفاده کنیم، بدون آنکه دانشی درباره پیاده سازی آنها داشته باشیم؛ مانند: یخچالها، اتومبیلها، دستگاههای فتوکپی، کامپیوترها و غیره. چون آنها برای استفاده شدن از طریق واسط عمومی خودشان، تعریف و طراحی شدهاند. لذا حتی بدون داشتن دانشی از پیاده سازی آنها، استفاده از آنها آسان میباشد. این واسط عمومی وابسته به دستگاه مورد نظر است. اما جزئیات پیاده سازی دستگاه را از دید کاربرانش پنهان میکند. این استراتژی طراحی، چیزی است که به سازنده اجازه میدهد بدون آنکه کاربران رنجیده شوند، با آزادی عمل، 60 مؤلفه کوچک استفاده شده در ساخت ساعت زنگدار را تعویض کند.
مثال دیگری از واسط عمومی در مقابل جزئیات پیاده سازی، میتواند در حوزه اتومبیلها دیده شود. زمانیکه تولید کنندگان اتومبیل از سیستمهای احتراق مکانیکی به سمت سیستمهای احتراق الکترونیکی کوچ کردند، تعداد خیلی کمی از کاربران اتومبیلها نگران این موضوع بودند. اما چرا؟ چون واسط عمومی آنها مانند سابق ماند و تنها پیاده سازی تغییر کرد. فرض کنید که شما به قصد خرید اتومبیل به یک فروشنده اتومبیل مراجعه میکنید و فروشنده یک سوئیچ را به شما داده و از شما میخواهد برای تست آن را برانید. بعد از تلاشی که برای استارت زدن داشتید، فروشنده اعلام میکند که در این مدل برای استارت زدن باید ابتدا کاپوت را بالا زده و دکمه قرمز را فشار دهید. در این حالت، بدلیل اینکه واسط عمومی اتومبیل دستخوش تغییر بوده است، باعث ناراحتی شما خواهد شد.
این فلسفه، دقیقا یکی از ایدههای پایهای در پارادایم شیءگرا میباشد. تمام جزئیات پیاده سازی در سیستم شما باید در پشت یک واسط عمومی مستحکم و سازگار، از کاربران آنها پنهان باشد. نیاز کاربران، دانستن درباره واسط عمومی میباشد؛ اما هرگز مجاز به دیدن جزئیات پیاده سازی آنها نیستند. با این روش، پیاده ساز میتواند به هرشکلی که مناسب است، پیاده سازی را تغییر دهد؛ درحالیکه واسط عمومی مانند سابق میباشد. به عنوان مسافری که مکرر سفر میکنم، به شما اطمینان میدهم که استفاده از ساعتهای زنگدار با وجود عدم اطلاع از پیاده سازی آنها، فواید عظیمی دارند. در هتلهای زیادی که از دسته بندیهای گستردهای از ساعتها مانند الکتریکی، قابل کوک (windup)، باتری خور، در هر دو مدل دیجیتال و آنالوگ استفاده میکنند، اقامت کردهام. یکبار هم اتفاق نیفتادهاست در حالیکه در هواپیما نشسته باشم، نگران این باشم که قادر نخواهم بود از ساعت زنگی اتاقم در هتل استفاده کنم.
بیشتر خوانندگان این کتاب، با وجود اینکه در نزدیکی آنها شاید ساعت زنگداری هم نباشد، ولی منظور بنده را با عبارت «ساعت زنگدار» متوجه شدند. به چه دلیل؟ شما در زندگی خودتان ساعتهای زنگدار زیادی را میبینید و متوجه میشوید که همه آنها از یکسری خصوصیات مشترک مانند زمان، یک زمان هشدار و طراحیای که مشخص میکند هشدار روشن یا خاموش است، بهره میبرند. همچنین متوجه میشوید که همه ساعتهای زنگداری که دیدهاید امکان تنظیم کردن زمان، تنظیم زمان هشدار و روشن و خاموش کردن هشدار را به شما میدهند. در نتیجه، شما الان مفهومی را به نام «ساعت زنگدار» دارید که مفهومی را از داده و رفتار، در یک بسته بندی مرتب برای همه ساعتهای زنگدار، تسخیر میکند. این مفهوم به عنوان یک Class (کلاس) شناخته میشود. یک ساعت زنگدار فیزیکی که شما در دست خود آن را نگه داشتهاید، یک Object (وهله، Instance) ای از کلاس ساعت زنگدار میباشد. رابطه بین مفهوم کلاس و وهله، Instantiation Relationship (وهله سازی) نام دارد. به یک object، ساعت زنگدار وهله سازی شده (Instantiated) از کلاس ساعت زنگدار گفته میشود؛ در حالیکه از کلاس ساعت زنگدار به عنوان تعمیم (Generalization) از همه objectهای کلاس ساعت زنگدار که شما با آنها روبرو شدهاید، یاد میشود.
اگر من به شما میگفتم که ساعت زنگدارم از روی پاتختی (میز کوچک کنار تخت که دارای کشو میباشد) من پرید، من را گاز گرفت، سپس گربهی همسایه را دنبال کرد، قطعا مرا دیوانه به حساب میآوردید. اگر به شما میگفتم که سگ من کارهای مشابهای را انجام میدهد، کاملا منطقی میبود. چون نام یک کلاس تنها به مجموعهای از خواص اشاره نمیکند، بلکه رفتارهای موجودیت (entity) را نیز مشخص میکند. این رابطه دوسویه بین داده و رفتار، اساس پارادایم شیء گرا میباشد.
- هویت خود (ممکن است آدرس آن در حافظه باشد) - its own identity
- خواص کلاس خود (معمولا استاتیک) و مقادیر این خواص (معمولا پویا) - attributes of its class
- رفتار کلاس خود (از دید پیاده ساز) - behavior of its class
- واسط منتشر شده کلاس خود (از دید استفاده کننده) - published interface of its class
یک کلاس را می توان با record definition (ساختار داده پایه، struct) و لیستی از عملیاتی که مجاز به کار بر روی این record definition هستند، پیاده سازی کرد. در زبانهای رویهای (Procedural) یافتن وابستگی دادهها در یک تابع معین، آسان میباشد. این کار را میتوان به سادگی با بررسی کردن جزئیات پیاده سازی تابع و مشاهده نوع داده پارامترهای آن، مقادیر بازگشتی و متغییرهای محلیای که تعریف شدهاند، انجام داد. اگر قصد شما پیدا کردن وابستگیهای تابعی بر روی یک داده میباشد، باید همه کد را بررسی کرده و به دنبال توابعی باشید که به داده شما وابسته هستند. در مدل شیء گرا، هر دو نوع وابستگی (داده به رفتار و رفتار به داده) به راحتی در دسترس میباشند. وهلهها، متغیرهایی از یک نوع داده کلاس هستند. جزئیات داخلی آنها باید فقط برای لیست توابع مرتبط با کلاسهایشان آشکار باشد. این محدودیت دسترسی به جزئیات داخلی وهلهها، Information Hiding نامیده میشود. اختیاری بودن این بحث در خیلی از زبانهای شیء گرا ما را به سمت اولین قاعده شهودی هدایت میکند.
همه دادهها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class)
با نقض این قاعده، امکان نگهداری را هم از دست میدهید. اجبار به پنهان کردن اطلاعات در مراحل طراحی و پیاده سازی، بخش عظیمی از فواید پارادایم شیء گرا میباشد. اگر داده به صورت عمومی تعریف شده باشد، تشخیص اینکه کدام بخش از عملیات (functionality) سیستم به آن داده وابسته است، سخت و مشکل خواهد بود. در واقع، نگاشت تغییرات داده به عملیات سیستم، همانند طراحی و پیاده سازی در دنیای action-oriented میباشد. ما مجبور میشویم برای تشخیص اینکه کدام عملیات به داده مورد نظر ما وابسته است، تمام عملیات سیستم را بررسی کنیم، تا به این ترتیب متوجه شویم.
برخی اوقات، یک توسعه دهنده استدلال میکند «نیاز دارم این بخش از داده را عمومی تعریف کنم زیرا ....» در این وضعیت، توسعه دهنده باید از خود سوال کند «کاری که تلاش دارم با این داده انجام دهم چیست و چرا کلاس این عملیات را خودش برای من انجام نمیدهد؟» در همه موارد این کلاس است که به سادگی عملیات ضروری را فراموش کردهاست. کمی بر روی شکل 2.2 فکر کنید. توسعه دهنده به صورت تصادفی فکر کرده است که عضو byte_offset را برای مجاز ساختن دسترسی تصادفی I/O، به صورت عمومی تعریف کند. اما چیزی که واقعا برای انجام این کار به آن نیاز داشت، تعریف یک operation بود (در زبان سی، توابع fseek و ftell برای ممکن کردن دسترسی تصادفی I/O، موجود هستند).
مراقب توسعه دهندههایی که جسورانه میگویند: «ما میتوانیم این بخش از داده را تغییر دهیم، زیرا هیچوقت تغییر نخواهد کرد!» باشید. طبق قانون برنامه نویسی مورفی، اولین بخشی که نیاز به تغییر خواهد داشت همین بخش از داده است.
به عنوان مثال دیگر برای روشنتر شدن بحث، کلاس Point را که پیاده سازی آن به روش مختصات دکارتی میباشد، در نظر بگیرید. یک طراح بیتجربه ممکن است دلیل تراشی کند که ما میتوانیم دادههای X و Y را به صورت عمومی تعریف کنیم؛ چرا که هیچ موقع تغییر نخواهند کرد. فرض کنید نیاز جدیدی مبنی بر اینکه پیاده سازی Point به ناچار باید از دکارتی به قطبی تغییر کند، به دست شما برسد. به این صورت استفاده کنندگان از این کلاس Point نیز باید تغییر کنند. حال اگر دادهها پنهان بودند و عمومی نبودند، در نتیجه فقط لازم بود پیاده سازهای این کلاس، کد خود را تغییر دهند.
در برنامهی backend این سری (که از انتهای مطلب قابل دریافت است)، به Controllers\MoviesController.cs مراجعه کرده و متدهای Get/Delete/Create آنرا با فیلتر [Authorize] مزین میکنیم تا دسترسی به آنها، تنها به کاربران لاگین شدهی در سیستم، محدود شود. در این حالت اگر به برنامهی React مراجعه کرده و برای مثال سعی در ویرایش رکوردی کنیم، اتفاقی رخ نخواهد داد:
علت را نیز در برگهی network کنسول توسعه دهندگان مرورگر، میتوان مشاهده کرد. این درخواست از سمت سرور با Status Code: 401، برگشت خوردهاست. برای رفع این مشکل باید JSON web token ای را که در حین لاگین، از سمت سرور دریافت کرده بودیم، به همراه درخواست خود، مجددا به سمت سرور ارسال کنیم. این ارسال نیز باید به صورت یک هدر مخصوص با کلید Authorization و مقدار "Bearer jwt" باشد.
به همین جهت ابتدا به src\services\authService.js مراجعه کرده و متدی را برای بازگشت JWT ذخیره شدهی در local storage به آن اضافه میکنیم:
export function getLocalJwt(){ return localStorage.getItem(tokenKey); }
import * as auth from "./authService"; axios.defaults.headers.common["Authorization"] = "Bearer " + auth.getLocalJwt();
مشکل! اگر برنامه را در این حالت اجرا کنید، یک چنین خطایی را مشاهده خواهید کرد:
Uncaught ReferenceError: Cannot access 'tokenKey' before initialization
برای رفع این خطا باید ابتدا مشخص کنیم که کدامیک از این ماژولها، اصلی است و کدامیک باید وابستهی به دیگری باشد. در این حالت httpService، ماژول اصلی است و بدون آن و با نبود امکان اتصال به backend، دیگر authService قابل استفاده نخواهد بود.
به همین جهت به httpService مراجعه کرده و import مربوط به authService را از آن حذف میکنیم. سپس در همینجا متدی را برای تنظیم هدر Authorizationاضافه کرده و آنرا به لیست default exports این ماژول نیز اضافه میکنیم:
function setJwt(jwt) { axios.defaults.headers.common["Authorization"] = "Bearer " + jwt; } //... export default { // ... setJwt };
http.setJwt(getLocalJwt());
تا اینجا اگر تغییرات را ذخیره کرده و سعی در ویرایش یکی از رکوردهای فیلمهای نمایش داده شده کنیم، اینکار با موفقیت انجام میشود؛ چون اینبار درخواست ارسالی، دارای هدر ویژهی authorization است:
روش بررسی انقضای توکنها در سمت کلاینت
اگر JWT قدیمی و منقضی شدهی از روز گذشته را آزمایش کنید، باز هم از سمت سرور، Status Code: 401 دریافت خواهد شد. اما اینبار در لاگهای برنامهی سمت سرور، OnChallenge error مشخص است. در این حالت باید یکبار logout کرد تا JWT قدیمی حذف شود. سپس نیاز به لاگین مجدد است تا یک JWT جدید دریافت گردد. میتوان اینکار را پیش از ارسال اطلاعات به سمت سرور، در سمت کلاینت نیز بررسی کرد:
function checkExpirationDate(user) { if (!user || !user.exp) { throw new Error("This access token doesn't have an expiration date!"); } user.expirationDateUtc = new Date(0); // The 0 sets the date to the epoch user.expirationDateUtc.setUTCSeconds(user.exp); const isAccessTokenTokenExpired = user.expirationDateUtc.valueOf() < new Date().valueOf(); if (isAccessTokenTokenExpired) { throw new Error("This access token is expired!"); } }
محدود کردن حذف رکوردهای فیلمها به نقش Admin در Backend
تا اینجا تمام کاربران وارد شدهی به سیستم، میتوانند علاوه بر ویرایش فیلمها، آنها را نیز حذف کنند. به همین جهت میخواهیم دسترسی حذف را از کاربرانی که ادمین نیستند، بگیریم. برای این منظور، در سمت سرور کافی است در کنترلر MoviesController، ویژگی [Authorize(Policy = CustomRoles.Admin)] را به اکشن متد Delete، اضافه کنیم. به این ترتیب اگر کاربری در سیستم ادمین نبود و درخواست حذف رکوردی را صادر کرد، خطای 403 را از سمت سرور دریافت میکند:
در برنامهی مثال backend این سری، در فایل Services\UsersDataSource.cs، یک کاربر ادمین پیشفرض ثبت شدهاست. مابقی کاربرانی که به صورت معمولی در سایت ثبت نام میکنند، ادمین نیستند.
در این حالت اگر کاربری ادمین بود، چون در توکن او که در فایل Services\TokenFactoryService.cs صادر میشود، یک User Claim ویژهی از نوع Role و با مقدار Admin وجود دارد:
if (user.IsAdmin) { claims.Add(new Claim(ClaimTypes.Role, CustomRoles.Admin, ClaimValueTypes.String, _configuration.Value.Issuer)); }
{ // ... "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", // ... }
نکته 1: اگر در اینجا چندین بار یک User Claim را با مقادیر متفاوتی، به لیست claims اضافه کنیم، مقادیر آن در خروجی نهایی، به شکل یک آرایه ظاهر میگردند.
نکته 2: پیاده سازی سمت سرور backend این سری، یک باگ امنیتی مهم را دارد! در حین ثبت نام، کاربران میتوانند مقدار خاصیت isAdmin شیء User را:
public class User : BaseModel { [Required, MinLength(2), MaxLength(50)] public string Name { set; get; } [Required, MinLength(5), MaxLength(255)] public string Email { set; get; } [Required, MinLength(5), MaxLength(1024)] public string Password { set; get; } public bool IsAdmin { set; get; } }
راه حل اصولی مقابلهی با آن، داشتن یک DTO و یا ViewModel خاص قسمت ثبت نام و جدا کردن مدل متناظر با موجودیت User، از شیءای است که اطلاعات نهایی را از کاربر، دریافت میکند. شیءای که اطلاعات را از کاربر دریافت میکند، نباید دارای خاصیت isAdmin قابل تنظیم در حین ثبت نام معمولی کاربران سایت باشد. یک روش دیگر حل این مشکل، استفاده از ویژگی Bind و ذکر صریح نام خواصی است که قرار است bind شوند و نه هیچ خاصیت دیگری از شیء User:
[HttpPost] public ActionResult<User> Create( [FromBody] [Bind(nameof(Models.User.Name), nameof(Models.User.Email), nameof(Models.User.Password))] User data) {
نکته 3: اگر میخواهید در برنامهی React، با مواجه شدن با خطای 403 از سمت سرور، کاربر را به یک صفحهی عمومی «دسترسی ندارید» هدایت کنید، میتوانید از interceptor سراسری که در قسمت 24 تعریف کردیم، استفاده کنید. در اینجا status code = 403 را جهت history.push به یک آدرس access-denied سفارشی و جدید، پردازش کنید.
نمایش یا مخفی کردن المانها بر اساس سطوح دسترسی کاربر وارد شدهی به سیستم
میخواهیم در صفحهی نمایش لیست فیلمها، دکمهی new movie را که بالای صفحه قرار دارد، به کاربرانی که لاگین نکردهاند، نمایش ندهیم. همچنین نمیخواهیم اینگونه کاربران، بتوانند فیلمی را ویرایش و یا حذف کنند؛ یعنی لینک به صفحهی جزئیات ویرایشی فیلمها و ستونی که دکمههای حذف هر ردیف را نمایش میدهد، به کاربران وارد نشدهی به سیستم نمایش داده نشوند.
در قسمت قبل، در فایل app.js، شیء currentUser را به state اضافه کردیم و با استفاده از ارسال آن به کامپوننت NavBar:
<NavBar user={this.state.currentUser} />
<Route path="/movies" render={props => <Movies {...props} user={this.state.currentUser} />} />
پس از این تغییر به فایل src\components\movies.jsx مراجعه کرده و شیء user را در متد رندر، دریافت میکنیم:
class Movies extends Component { // ... render() { const { user } = this.props; // ...
{user && ( <Link to="/movies/new" className="btn btn-primary" style={{ marginBottom: 20 }} > New Movie </Link> )}
در این تصویر همانطور که مشخص است، کاربر هنوز به سیستم وارد نشدهاست؛ بنابراین به علت null بودن شیء user، دکمهی New Movie را مشاهده نمیکند.
روش دریافت نقشهای کاربر وارد شدهی به سیستم در سمت کلاینت
همانطور که پیشتر در مطلب جاری عنوان شد، نقشهای دریافتی از سرور، یک چنین شکلی را در jwtDecode نهایی (یا user در اینجا) دارند:
{ // ... "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", // ... }
function addRoles(user) { const roles = user["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { if (Array.isArray(roles)) { user.roles = roles.map(role => role.toLowerCase()); } else { user.roles = [roles.toLowerCase()]; } } }
export function isAuthUserInRoles(user, requiredRoles) { if (!user || !user.roles) { return false; } if (user.roles.indexOf(adminRoleName.toLowerCase()) >= 0) { return true; // The `Admin` role has full access to every pages. } return requiredRoles.some(requiredRole => { if (user.roles) { return user.roles.indexOf(requiredRole.toLowerCase()) >= 0; } else { return false; } }); } export function isAuthUserInRole(user, requiredRole) { return isAuthUserInRoles(user, [requiredRole]); }
در این کدها، adminRoleName به صورت زیر تامین شدهاست:
import { adminRoleName, apiUrl } from "../config.json";
{ "apiUrl": "https://localhost:5001/api", "adminRoleName": "Admin" }
اکنون که امکان بررسی نقشهای کاربر لاگین شدهی به سیستم را داریم، میخواهیم ستون Delete ردیفهای لیست فیلمها را فقط به کاربری که دارای نقش Admin است، نمایش دهیم. برای اینکار نیاز به دریافت شیء user، در src\components\moviesTable.jsx وجود دارد. یک روش دریافت کاربر جاری وارد شدهی به سیستم، همانی است که تا به اینجا بررسی کردیم: شیء currentUser را به صورت props، از بالاترین کامپوننت، به پایینتر کامپوننت موجود در component tree ارسال میکنیم. روش دیگر اینکار، دریافت مستقیم کاربر جاری از خود src\services\authService.js است و ... اینکار سادهتر است! به علاوه اینکه همیشه بررسی تاریخ انقضای توکن را نیز به صورت خودکار انجام میدهد و در صورت انقضای توکن، کاربر را در قسمت catch متد getCurrentUser، از سیستم خارج خواهد کرد.
بنابراین در src\components\moviesTable.jsx، ابتدا authService را import میکنیم:
import * as auth from "../services/authService";
class MoviesTable extends Component { columns = [ ... ]; // ... deleteColumn = { key: "delete", content: movie => ( <button onClick={() => this.props.onDelete(movie)} className="btn btn-danger btn-sm" > Delete </button> ) };
constructor() { super(); const user = auth.getCurrentUser(); if (user && auth.isAuthUserInRole(user, "Admin")) { this.columns.push(this.deleteColumn); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-28-backend.zip و sample-28-frontend.zip
طراحی یک تامین کنندهی عمومی سشن
public interface ISessionProvider { object Get(string key); T Get<T>(string key) where T : class; void Remove(string key); void RemoveAll(); void Store(string key, object value); }
یک نمونه پیاده سازی عمومی آن نیز برای کار با سشنها در برنامههای وب ASP.NET وب فرم و MVC، میتواند به صورت زیر باشد:
public class DefaultWebSessionProvider : ISessionProvider { private readonly HttpSessionStateBase _session; public DefaultWebSessionProvider(HttpSessionStateBase session) { _session = session; } public object Get(string key) { return _session[key]; } public T Get<T>(string key) where T : class { return _session[key] as T; } public void Remove(string key) { _session.Remove(key); } public void RemoveAll() { _session.RemoveAll(); } public void Store(string key, object value) { _session[key] = value; } }
private static Container defaultContainer() { return new Container(ioc => { // session manager setup ioc.For<ISessionProvider>().Use<DefaultWebSessionProvider>(); ioc.For<HttpSessionStateBase>() .Use(ctx => new HttpSessionStateWrapper(HttpContext.Current.Session)); ioc.Policies.SetAllProperties(properties => { properties.OfType<ISessionProvider>(); }); }); }
استفاده از تامین کنندهی سفارشی سشن در برنامه
پس از طراحی تامین کنندهی سفارشی سشن و همچنین معرفی آن به IoC Container خود، اکنون استفادهی از آن به سادگی ذیل است:
public class HomeController : Controller { private readonly ISessionProvider _sessionProvider; public HomeController(ISessionProvider sessionProvider) { _sessionProvider = sessionProvider; }
معرفی کتابخانهی DNTScanner.Core
اشتباه 1:
استفاده از
throw ex;
throw;
اشتباه 2:
درک اشتباه عملکرد متد replace :
string s = "Take this out";
s.Replace("this", "that"); //wrong
s = s.Replace("this", "that"); //correct
اگر از fxCop استفاده کنید، اینگونه خطاها را (عدم استفاده از مقدار بازگشتی) گوشزد میکند.
اشتباه 3:
استفادهی بی دقت از متغیرهای استاتیک در یک برنامه وب. دو مثال زیر را در نظر بگیرید:
public static string GetCookieName(Cookie c)
{
return c.Name;
}
static List<string> cookieList = new List<string>();
public static void AddToCookieList(Cookie c)
{
cookieList.Add(c.Name);
}
برنامههای وب ذاتا چند ریسمانی هستند و زمانیکه یک متغیر را از نوع استاتیک تعریف میکنید، این متغیر، هنگام مراجعهی کاربران بین آنها به اشتراک گذاشته میشود و امکان تخریب یا استفادهی ناصحیح از مقادیر آنها وجود خواهد داشت. در حالت اول نیازی به مباحث همزمانی و قفل کردن منابع نیست زیرا متغیری که در متد استفاده میشود، thread safe است اما cookieList در مثال دوم خیر و حتما هنگام استفاده از آن باید مباحث قفل کردن منابع را درنظر داشت. (حتی اگر برنامهی شما از نوع وبی هم نیست اما چند ریسمانی است این مطلب باز هم صادق میباشد)
اشتباه 4:
مقابله با خطاها به شکلی نادرست: (اصطلاحا خفه کردن خطاها!)
try
{
//something
File.Delete(blah);
}
catch{}
هرچند گاهی از اوقات اگر خطای حاصل برای ما اهمیتی نداشت میتوان از آن استفاده نمود، در غیراینصورت باید حتما از این روش پرهیز کرد.
اشتباه 5:
ارائهی برنامههای ASP.Net با گزینهی پیش فرض Debug=true در web.config که پیشتر در مورد آن در این سایت بحث شده است.
اشتباه 6:
عدم استفاده از امکانات ویژهی دات نت فریم ورک هنگام کار با رشتهها:
string s = "This ";
s += "is ";
s += "not ";
s += "the ";
s += "best ";
s += "way.";
StringBuilder sb = new StringBuilder();
sb.Append("This ");
sb.Append("is ");
sb.Append("much ");
sb.Append("better. ");
زمانیکه نیاز به کنار هم قرار دادن رشتههای مختلف وجود داشت و تعداد آنها نیز زیاد بود، مثال دوم توصیه میشود.
البته در مثال فوق که تعداد کمی رشته قرار است با هم جمع شوند، کامپایلر به اندازهی کافی هوشمند خواهد بود که تمام آنها را کنار هم قرار دهد و تفاوتی در کارآیی احساس نشود، حتی روش اول سریعتر از روش دوم خواهد بود، اما زمان استفاده از هزاران رشته، این تفاوت محسوس است.
اشتباه 7:
عدم استفاده از عبارت using هنگام استفاده از اشیایی از نوع Idisposable . مثالی در این مورد.
using (StreamReader reader=new StreamReader(file))
{
//your code here
}
اشتباه 8:
فراموش کردن بررسی نال بودن یک شیء هنگام استفاده از آن.
string val = Session["xyz"].ToString();
این نوع کد نویسی یکی از اشتباهات متداول تمامی تازه واردان به ASP.Net است. حتما باید پیش از استفاده از متد ToString بررسی شود که آیا این سشن نال است یا نه. در غیراینصورت حاصل کار فقط یک exception خواهد بود. (استفاده از افزونهی ری شارپر در این موارد کمک بزرگی است، زیرا به محض قرار گرفتن مکان نما روی شیءایی که احتمال نال بودن آن میسر است، یک راهنما را به شما ارائه خواهد کرد)
اشتباه 9:
بازگشت دادن یک property عمومی از نوع لیستهای جنریک.
با توجه به اینکه این نوع لیستها فقط خواندنی نیستند و امکان دستکاری اطلاعات آن توسط فراخوان وجود دارد، توصیه میشود از نوع جنریک IEnumerable استفاده شود. همچنین توصیه شده است هنگام انتخاب نوع پارامترهای ورودی یک متد نیز به این مورد دقت شود.
2- استفاده از {}less.
Dotless یک پیاده سازی از کتابخانه جاوا اسکریپتی LESS برای دات نت میباشد. پکیج نیوگت DotLess را نیز میتوانید از اینجا دریافت کنید. بعد از اضافه شدن فایلهای آن، یک ارجاع به dotless.core به پروژه تان اضافه خواهد شد. همچنین در فایل Web.Config در قسمت HttpHandler خط زیر اضافه خواهد شد:
<add type="dotless.Core.LessCssHttpHandler,dotless.Core" validate="false" path="*.LESS" verb="*" />
خط فوق یعنی به محض مواجه شدن با فایل LESS، پردازشگر فایلهای LESS وارد عمل میشود. همچنین خط زیر نیز جهت پیکربندی به قسمت configSections در فایل Web.Config اضافه میشود:
<section name="dotless" type="dotless.Core.configuration.DotlessConfigurationSectionHandler,dotless.Core" />
همچنین اگر مایل بودید میتوانید تنظیمات مربوط به فشرده سازی و caching را نیز فعال کنید:
<dotless minifyCss="false" cache="true" />
3- استفاده از افزونهی Web Essentials
Web Essentials برای کامپایل فایلهای LESS از کامپایلر node استفاده میکند. کار با این افزونه خیلی ساده است. کافی است پسوند فایل CSS موجود در پروژه تان را درون ویژوال استودیو، به less. تغییر دهید. با دوبار کلیک بر روی فایل، ویرایشگر فایلهای LESS برای شما نمایش داده میشود، همزمان نیز فایل یک فایل CSS و یک نسخه از فایل CSS را به صورت فشرده، برایتان تولید میکند. خب، هر بار که فایل LESS را تغییر دهید، Web Essentials به صورت خودکار فایلهای css. و min.css. را برایتان روز رسانی میکند.
خوب با کلیک بر روی فایل less، ویرایشگر فایلهای less نمایش داده میشود که با تغییر فایل css میتوانید پیش نمایش آنرا در سمت راست مشاهده کنید:
تعریف متغیر
با استفاده از syntax زیر میتوانید متغیرهای خود را تعریف کنید:
@variable-name: variableValue;
یکی از قابلیتهای جالب در حین مقداردهی متغیرها به خصوص زمانیکه مقدار یک کد رنگی باشد، نمایش کادر انتخاب رنگ است، این کادر بلافاصله بعد از نوشتن علامت # در ابتدای مقدار متغیر نمایش داده میشود:
به طور مثال با تعریف متغیر فوق هر جایی میتوانیم برای تعیین رنگ از آن استفاده کنیم:
@primary-color: #ff6a00; body { background-color: @primary-color; }
استفاده از توابع
LESS شامل تعداد زیادی توابع از پیش نوشته شده است که میتوانید به راحتی از آنها استفاده کنید، توابعی از جمله کار با رنگ ها، اعمال ریاضی و غیره. استفاده از آنها خیلی ساده است. به طور مثال در کد زیر از تابع percentage جهت تبدیل 0.5 به 50% استفاده کرده ایم:
.myClass { width: percentage(0.5); }
استخراج یک فایل
یکی دیگر از قابلیتهای Web Essentials استخراج(Extract) یک فایل میباشد به طور مثال فایل LESS شما شامل متغیرهای زیر است:
@primary-color: #7BA857; @primary-color-light: #B6DE8F; @primary-color-lighter: #D3EFC3; @primary-color-lightest: #EFFAE6; @secondary-color: #AE855C; @text-color-light: #666666; @text-color-dark: #0444;
به راحتی میتوانید تعاریف فوق را درون یک فایل LESS دیگر با نام colors.less قرار دهید:
تغییر تنظیمات پیش فرض Web Essentials
افزونه Web Essentials دارای یک قسمت جهت تغییر تنظیمات پیش فرض برای کار با LESS میباشد که با مراجعه به منوی Tools در ویژوال استودیو و سپس Options میتوانید آنها را تغییر دهید:
Auto-compile dependent files on save: توسط این گزینه میتوانیم تعیین کنیم که فایلهای که import کرده ایم تنها در صورتی که تغییر کرده و ذخیره شده باشند، در فایل CSS جاری کامپایل شوند.
Compile files on build: توسط این گزینه میتوانیم تعیین کنیم که فایلهای less در زمان Build پروژه کامپایل شوند.
Compile files on save: توسط این گزینه میتوانیم تعیین کنیم که فایلهای less در زمان ذخیره کردن پروژه کامپایل شوند.
Create source map files: اگر این گزینه True باشد فایل map. نیز تولید خواهد شد.
Custom output directory: اگر میخواهید خروجی در پوشهی موردنظر شما نمایش داده شود میتوانید آدرس آن را تعیین کنید.
Don't save raw compilation output: با فعال بودن این گزینه فایل CSS عادی ایجاد نخواهد شد.
Process source maps: توسط این گزینه میتوانید قابلیتهای ویرایشگر فایلهای source map را فعال یا غیرفعال کنید.
Strict Math: با فعال بودن این گزینه LESS تمام اعمال ریاضی درون فایل CSS را پردازش خواهد کرد.
Show preview pane: از این گزینه نیز جهت نمایش یا عدم نمایش preview window استفاده میشود.
نصب پرباد و انجام تنظیمات اولیهی آن
بستههای نیوگت پرباد را در دو پروژهی زیر نصب خواهیم کرد:
الف) پروژهی Web API (و یا همان BlazorWasm.WebApi در مثال این سری):
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Parbad.AspNetCore" Version="1.1.0" /> <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" /> </ItemGroup> </Project>
ب) پروژهای که محل قرارگیری فایلهای Migration است (و یا همان BlazorServer.DataAccess) در این مثال:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" /> </ItemGroup> </Project>
پس از نصب این بستهها، به کلاس آغازین پروژهی Web API مراجعه کرده و تنظیمات سرویسها و همچنین میانافزار پرباد را انجام میدهیم:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { // ... var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddParbad() .ConfigureHttpContext(httpContextBuilder => httpContextBuilder.UseDefaultAspNetCore()) .ConfigureGateways(gatewayBuilder => { gatewayBuilder .AddParbadVirtual() .WithOptions(gatewayOptions => gatewayOptions.GatewayPath = "/MyVirtualGateway"); }) .ConfigureStorage(storageBuilder => { storageBuilder.UseEfCore(efCoreOptions => { var assemblyName = typeof(ApplicationDbContext).Assembly.GetName().Name; efCoreOptions.ConfigureDbContext = db => db.UseSqlServer( connectionString, sqlServerOptionsAction: sqlOptions => sqlOptions.MigrationsAssembly(assemblyName) ); }); }) .ConfigureAutoTrackingNumber(opt => opt.MinimumValue = 1) .ConfigureOptions(parbadOptions => { // parbadOptions.Messages.PaymentSucceed = "YOUR MESSAGE"; }); // ... } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); if (env.IsDevelopment()) { app.UseParbadVirtualGatewayWhenDeveloping(); } else { app.UseParbadVirtualGateway(); } } } }
- در متد ConfigureGateways میتوان چندین درگاه را معرفی کرد که برای مثال در اینجا از درگاه مجازی و محلی آن استفاده شدهاست.
- در متد ConfigureStorage، تنظیمات EF-Core آنرا مشاهده میکنید. پرباد به همراه DbContext خاص خودش است. یعنی در این حالت برنامهی شما حداقل دو DbContext خواهد داشت؛ یکی ApplicationDbContext و دیگری ParbadDataContext.
- میخواهیم شمارهی تراکنشها را به صورت خودکار توسط پرباد مدیریت کنیم. به همین جهت میتوان عدد ابتدای آنرا توسط متد ConfigureAutoTrackingNumber مشخص کرد.
- در پایان هم تعاریف مسیریابی میانافزار آنرا مشاهده میکنید که میتواند برای حالت توسعه و ارائهی نهایی متفاوت باشد.
تکمیل خواص موجودیت RoomOrderDetail جهت کار با پرباد
موجودیت RoomOrderDetail را در قسمت قبل معرفی کردیم. پرباد به ازای هر تراکنش بانکی که صورت میگیرد، یا نیاز به یک TrackingNumber خودکار را دارد و یا دستی. یعنی یا میتوانیم شماره تراکنش خاص خودمان را تولید کنیم و در اختیار آن قرار دهیم و یا از آن درخواست کنیم تا این شماره را مدیریت کرده و به صورت خودکار تولید کند. در هر دو حالت نیاز است این شماره را به ردیفهای جدول جزئیات سفارشات اتاقهای هتل اضافه کرد که در این مثال ParbadTrackingNumber نام دارد:
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace BlazorServer.Entities { public class RoomOrderDetail { // ... [Required] public long ParbadTrackingNumber { get; set; } public bool IsPaymentSuccessful { get; set; } public string Status { get; set; } } }
ایجاد جداول متناظر با ParbadDataContext
همانطور که عنوان شد، اکنون برنامه به همراه دو DbContext است. بنابراین در این حالت در حین اجرای مهاجرتها، ذکر نام Context مدنظر اجباری است.
برای ایجاد مهاجرتهای متناظر با ParbadDataContext، از طریق خط فرمان به پوشهی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا میکنیم:
dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbadFields --context ApplicationDbContext dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbad --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext
روش یکپارچه سازی پرباد با یک برنامهی SPA
روش متداول کار با پرباد، بر اساس طراحی مخصوص ASP.NET Core آن است. ابتدا درخواستی را به آن ارسال میکنید. سپس پرباد شماره تراکنشی را تولید کرده و شروع تراکنش را در بانک اطلاعاتی ثبت میکند. در ادامه به صورت خودکار، کار ارسال اطلاعات به درگاه بانکی (برای مثال ارسال تمام فیلدهای یک فرم ویژهی آن بانک، بر اساس مستندات آن) و هدایت به درگاه بانکی را انجام میدهد. پس از پایان کار پرداخت، کار هدایت به اکشن متد دریافت تائیدیهی نهایی صورت میگیرد و همینجا کار به پایان میرسد. این روش هرچند برای برنامههای سمت سرور ASP.NET Core کار میکند، اما ... به همین نحو با برنامههای تک صفحهای وب مانند Blazor WASM قابل استفاده نیست. در اینجا روش تبادل اطلاعات با اکشن متدهای وب سرویسهای برنامه از طریق یک HttpClient است و در این حالت دیگر نمیتوان از مزایای Post و Redirect خودکار پرباد که در سمت سرور صورت میگیرد استفاده کرد. با استفاده از HttpClient، یک شیء را به سمت Web API ارسال میکنیم و در پاسخ، فقط یک شیء را دریافت میکنیم. در اینجا دیگر خبری از Redirect به درگاه اصلی بانکی و Post اطلاعات به آن نیست. بنابراین روش کار با پرباد در اینجا به صورت زیر خواهد بود:
الف) شماره Id سفارش و مبلغ نهایی آنرا از طریق یک درخواست Get معمولی به اکشن متدی در سمت سرور ارسال میکنیم. یعنی نیاز است ابتدا Url زیر را تشکیل داد که شماره سفارش و مبلغ آن، به صورت کوئری استرینگهایی به اکشن متد PayRoomOrder ارسال میشوند:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
ب) اکنون چون یک redirect سمت سرور صورت گرفته، به صورت معمولی در اکشن متد PayRoomOrder با پرباد پردازش صورت گرفته و به سمت درگاه هدایت میشویم. پس از پرداخت نهایی، باز هم به صورت خودکار به اکشن متد دیگری جهت تائید عملیات هدایت خواهیم شد.
ج) در پایان کار، اکشن متد سمت سرور، ما را به سمت کامپوننتی در برنامهی کلاینت Redirect خواهد کرد:
https://localhost:5002/payment-result/OrderId/TrackingNumber/Message
بنابراین روش یکپارچه سازی پربابد با برنامههای SPA، بر اساس Redirectهای کامل است که سبب بارگذاری مجدد کل صفحه و آدرسها میشوند و در اینجا از HttpClient برای کار با پرباد استفاده نخواهیم کرد؛ چون تمام اعمال خودکار آنرا از دست خواهیم داد و مجبور به بازنویسی آنها خواهیم شد که در دراز مدت با تغییرات این کتابخانه، قابل نگهداری نخواهند بود. بنابراین بهتر است خود پرباد کار Redirectها و ارسال اطلاعات به درگاههای بانکی را مدیریت کند و نه ما از طریق کار با یک HttpClient.
آشنایی با گردش کار برنامه
در این مثال، مراحل زیر را طی خواهیم کرد:
1- شروع به انتخاب یک بازهی زمانی و تعداد شب اقامت
2- انتخاب یک اتاق از لیست اتاقها با کلیک بر روی دکمهی Book آن
3- کلیک بر روی دکمهی checkout، در صفحهی مشاهدهی جزئیات اتاق و شروع به پرداخت
4- هدایت به درگاه مجازی پرباد در سمت برنامهی Web API
5- پرداخت و هدایت خودکار به سمت برنامهی Web API، جهت تائید نهایی
6- هدایت نهایی به سمت برنامهی کلاینت، جهت نمایش اطلاعات پرداخت
ایجاد کنترلر پرداخت، توسط درگاه مجازی پرباد
پس از آشنایی با گردش کاری اطلاعات در اینجا، نیاز است بتوان لینک زیر را در برنامهی کلاینت تولید کرد و سپس کاربر را به سمت اکشن متد PayRoomOrder هدایت نمود:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]/[action]")] [ApiController] public class ParbadPaymentController : Controller { private readonly IConfiguration _configuration; private readonly IOnlinePayment _onlinePayment; private readonly IRoomOrderDetailsService _roomOrderService; public ParbadPaymentController( IConfiguration configuration, IOnlinePayment onlinePayment, IRoomOrderDetailsService roomOrderService) { _configuration = configuration; _onlinePayment = onlinePayment ?? throw new ArgumentNullException(nameof(onlinePayment)); _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService)); } [HttpGet] public async Task<IActionResult> PayRoomOrder(int orderId, long amount) { var verifyUrl = Url.Action( action: nameof(ParbadPaymentController.VerifyRoomOrderPayment), controller: nameof(ParbadPaymentController).Replace("Controller", string.Empty), values: null, protocol: Request.Scheme); var result = await _onlinePayment.RequestAsync(invoiceBuilder => invoiceBuilder.UseAutoIncrementTrackingNumber() .SetAmount(amount) .SetCallbackUrl(verifyUrl) .UseParbadVirtual() ); if (result.IsSucceed) { await _roomOrderService.UpdateRoomOrderTrackingNumberAsync(orderId, result.TrackingNumber); // It will redirect the client to the gateway. return result.GatewayTransporter.TransportToGateway(); } else { return Redirect(getClientReturnUrl(orderId, result.TrackingNumber, result.Message)); } } [HttpGet, HttpPost] public async Task<IActionResult> VerifyRoomOrderPayment() { var invoice = await _onlinePayment.FetchAsync(); var orderDetail = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(invoice.TrackingNumber); if (invoice.Status == PaymentFetchResultStatus.AlreadyProcessed) { return Redirect(getClientReturnUrl(orderDetail.Id, invoice.TrackingNumber, "The payment is already processed.")); } var verifyResult = await _onlinePayment.VerifyAsync(invoice); if (verifyResult.Status == PaymentVerifyResultStatus.Succeed) { var result = await _roomOrderService.MarkPaymentSuccessfulAsync(verifyResult.TrackingNumber, verifyResult.Amount); if (result == null) { return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, "Can not mark payment as successful")); } return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message)); } return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message)); } private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage) { var clientBaseUrl = _configuration.GetValue<string>("Client_URL"); return new Uri(new Uri(clientBaseUrl), $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString(); } } }
در اینجا کدهای کامل ParbadPaymentController مشاهده میکنید.
- گردش کاری پرداخت، با فراخوانی اکشن متد PayRoomOrder شروع میشود که دو پارامتر شماره سفارش و مبلغ آنرا دریافت میکند.
[HttpGet] public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
- در اکشن متد PayRoomOrder، نیاز است لینک بازگشت از درگاه بانکی را مشخص کنیم. پس از اینکه کاربر پرداختی را انجام داد، مجددا به صورت خودکار، به سمت آدرسی در همین Web API و نه برنامهی سمت کلاینت هدایت میشود؛ چون هنوز کار پرباد به پایان نرسیده و باید عملیات انجام شده را تصدیق کند. به همین جهت ابتدا آدرس اکشن متدی که کار تائید نهایی را انجام میدهد، تولید کرده و به متد RequestAsync آن به همراه مبلغ نهایی و نوع درگاه، ارسال میکنیم.
- استفاده از UseAutoIncrementTrackingNumber سبب میشود تا پرباد خودش مدیریت TrackingNumber را انجام دهد که پس از پایان عملیات، توسط خاصیت result.TrackingNumber در دسترس خواهد بود.
- پس از پایان عملیات ابتدایی RequestAsync که سشن پرباد را ایجاد کرده و همچنین رکوردی را در بانک اطلاعاتی نیز ثبت میکند (در جداول درونی خود پرباد)، نیاز است رکورد سفارشی را که با آن کار را شروع کردیم یافته و TrackingNumber آنرا با مقدار واقعی دریافتی از پرباد، به روز رسانی کنیم. اینکار توسط متد UpdateRoomOrderTrackingNumberAsync انجام میشود:
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { // ... public async Task UpdateRoomOrderTrackingNumberAsync(int roomOrderId, long trackingNumber) { var order = await _dbContext.RoomOrderDetails.FindAsync(roomOrderId); if (order == null) { return; } order.ParbadTrackingNumber = trackingNumber; _dbContext.RoomOrderDetails.Update(order); await _dbContext.SaveChangesAsync(); } } }
- اکنون با فراخوانی متد ()result.GatewayTransporter.TransportToGateway، دو کار مهم رخ میدهند:
الف) ارسال خودکار اطلاعات به سمت درگاه بانکی
ب) Redirect خودکار به سمت درگاه بانگی
به همین جهت است که علاقمند نبودیم تا این مراحل را توسط HttpClient برنامهی Blazor WASM مدیریت و بازنویسی کنیم.
- پس از هدایت به سمت درگاه بانکی و تکمیل پرداخت، اکنون مجددا به همان verifyUrl هدایت میشویم. یعنی اکنون به مرحلهی پردازش اکشن متد VerifyRoomOrderPayment در سمت Web API رسیدهایم.
[HttpGet, HttpPost] public async Task<IActionResult> VerifyRoomOrderPayment()
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { // ... public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber) { var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails .Include(u => u.HotelRoom) .ThenInclude(x => x.HotelRoomImages) .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration) .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber); roomOrderDetailsDTO.HotelRoomDTO.TotalDays = roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days; return roomOrderDetailsDTO; } } }
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { // ... public async Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(long trackingNumber, long amount) { var order = await _dbContext.RoomOrderDetails.FirstOrDefaultAsync(x => x.ParbadTrackingNumber == trackingNumber); if (order?.IsPaymentSuccessful != false || order.TotalCost != amount) { return null; } order.IsPaymentSuccessful = true; order.Status = BookingStatus.Booked; var markPaymentSuccessful = _dbContext.RoomOrderDetails.Update(order); await _dbContext.SaveChangesAsync(); return _mapper.Map<RoomOrderDetailsDTO>(markPaymentSuccessful.Entity); } } }
- در تمام این مراحل، کار Redirect به سمت کلاینت و کامپوننت payment-result آن، با فراخوانی متد return Redirect اکشن متدها صورت میگیرد که Url آن به صورت زیر تامین میشود:
private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage) { var clientBaseUrl = _configuration.GetValue<string>("Client_URL"); return new Uri(new Uri(clientBaseUrl), $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString(); }
{ "Client_URL": "https://localhost:5002/" }
تکمیل قسمت سمت کلاینت عملیات پرداخت بانکی، توسط درگاه مجازی پرباد
تا اینجا کنترلری که کار پرداخت آنلاین را مدیریت میکند، پیاده سازی کردیم. قسمت آخر این بحث به تکمیل جزئیات این گردش کاری که شامل شروع عملیات سفارش و پرداخت از سمت کلاینت و نمایش پیام خطا یا موفقیت پرداخت به کاربر است، اختصاص دارد.
الف) تکمیل کامپوننت RoomDetails.razor جهت شروع به پرداخت آنلاین
کامپوننت RoomDetails.razor را در قسمت قبل آغاز کردیم و توسعهی آنرا تا جائی پیش بردیم که اعتبارسنجیهای آنرا به علت استفادهی از خواص تو در تو، به صورت دستی انجام دادیم. پس از مرحلهی اعتبارسنجی، اکنون میخواهیم کاربر را به سمت درگاه بانکی جهت پرداخت، هدایت کنیم:
@page "/hotel-room-details/{Id:int}" @inject IJSRuntime JsRuntime @inject ILocalStorageService LocalStorage @inject IClientHotelRoomService HotelRoomService @inject IClientRoomOrderDetailsService RoomOrderDetailsService @inject NavigationManager NavigationManager @inject HttpClient HttpClient // ... @code { // ... private async Task HandleCheckout() { if (!await HandleValidation()) { return; } try { HotelBooking.OrderDetails.ParbadTrackingNumber = -1; HotelBooking.OrderDetails.RoomId = HotelBooking.OrderDetails.HotelRoomDTO.Id; HotelBooking.OrderDetails.TotalCost = HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount; var roomOrderDetailsSaved = await RoomOrderDetailsService.SaveRoomOrderDetailsAsync(HotelBooking.OrderDetails); await LocalStorage.SetItemAsync(ConstantKeys.LocalRoomOrderDetails, roomOrderDetailsSaved); var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder")) .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString()) .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString()) .Uri; NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true); } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } // ... }
namespace BlazorWasm.WebApi.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class RoomOrderController : Controller { private readonly IRoomOrderDetailsService _roomOrderService; public RoomOrderController(IRoomOrderDetailsService roomOrderService) { _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService)); } [HttpPost] public async Task<IActionResult> Create([FromBody] RoomOrderDetailsDTO details) { var result = await _roomOrderService.CreateAsync(details); return Ok(result); } [HttpGet] public async Task<IActionResult> GetOrderDetail(int trackingNumber) { var result = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(trackingNumber); return Ok(result); } } }
- متد GetOrderDetail، بر اساس trackingNumber دریافتی از پرباد، کار بازگشت رکورد متناظری را انجام میدهد. از آن در پایان کار، جهت نمایش وضعیت پرداخت، استفاده میکنیم.
این دو متد در سرویس سمت سرور RoomOrderDetailsService، به صورت زیر تامین شدهاند:
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { // ... public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details) { var roomOrder = _mapper.Map<RoomOrderDetail>(details); roomOrder.Status = BookingStatus.Pending; var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder); await _dbContext.SaveChangesAsync(); return _mapper.Map<RoomOrderDetailsDTO>(result.Entity); } public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber) { var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails .Include(u => u.HotelRoom) .ThenInclude(x => x.HotelRoomImages) .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration) .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber); roomOrderDetailsDTO.HotelRoomDTO.TotalDays = roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days; return roomOrderDetailsDTO; } // ... } }
namespace BlazorWasm.Client.Services { public interface IClientRoomOrderDetailsService { Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details); Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber); } } namespace BlazorWasm.Client.Services { public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService { private readonly HttpClient _httpClient; public ClientRoomOrderDetailsService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber) { // How to url-encode query-string parameters properly var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/roomorder/GetOrderDetail")) .AddParameter("trackingNumber", trackingNumber.ToString()) .Uri; return _httpClient.GetFromJsonAsync<RoomOrderDetailsDTO>(uri); } public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details) { details.UserId = "unknown user!"; var response = await _httpClient.PostAsJsonAsync("api/roomorder/create", details); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { return JsonSerializer.Deserialize<RoomOrderDetailsDTO>(responseContent); } else { //var errorModel = JsonSerializer.Deserialize<ErrorModel>(responseContent); throw new InvalidOperationException(responseContent); } } } }
- متد SaveRoomOrderDetailsAsync، یک رکورد سفارش جدید را ایجاد میکند. در اینجا با روش مشاهدهی خطای کامل بازگشتی از سمت سرور (در صورت وجود) هم آشنا شدهایم که در مواقع لزوم میتواند راهگشا باشد.
- در متد SaveRoomOrderDetailsAsync فعلا مقدار UserId اجباری را به عبارتی دلخواه، تنظیم کردهایم. این مورد را در قسمتهای بعدی با معرفی اعتبارسنجی و احراز هویت سمت کلاینت، تکمیل خواهیم کرد.
این سرویس جدید را هم باید به سیستم تزریق وابستگیهای برنامهی کلاینت معرفی کرد تا قابل استفاده شود:
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { // ... builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
سپس به قطعه کد مهم زیر میرسیم:
var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder")) .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString()) .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString()) .Uri; NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
نمایش وضعیت پرداخت، به کاربر در پایان گردش کاری آن
پس از این مراحل، مرحلهی آخر کار باقی ماندهاست؛ یعنی بازگشت از اکشن متد VerifyRoomOrderPayment سمت سرور، به کامپوننت PaymentResult سمت کلاینت، برای نمایش نتیجهی عملیات. به همین جهت کامپوننت جدید Pages\HotelRooms\PaymentResult.razor را ایجاد کرده و به صورت زیر تکمیل میکنیم:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}" @inject ILocalStorageService LocalStorage @inject IClientRoomOrderDetailsService RoomOrderDetailService @inject IJSRuntime JsRuntime @inject NavigationManager NavigationManager @if (IsLoading) { <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;"> <img src="images/ajax-loader.gif" /> </div> } else { <div class="container"> <div class="row mt-4 pt-4"> <div class="col-10 offset-1 text-center"> @if(IsPaymentSuccessful) { <h2 class="text-success">Booking Confirmed!</h2> <p>Your room has been booked successfully with order id @OrderId & tracking number @TrackingNumber .</p> } else { <h2 class="text-warning">Booking Failed!</h2> <p>@Message</p> } <a class="btn btn-primary" href="hotel-rooms">Back to rooms</a> </div> </div> </div> } @code { private bool IsLoading; private bool IsPaymentSuccessful; [Parameter] public int OrderId { set; get; } [Parameter] public long TrackingNumber { set; get; } [Parameter] public string Message { set; get; } protected override async Task OnInitializedAsync() { IsLoading = true; try { var finalOrderDetail = await RoomOrderDetailService.GetOrderDetailAsync(TrackingNumber); var localOrderDetail = await LocalStorage.GetItemAsync<RoomOrderDetailsDTO>(ConstantKeys.LocalRoomOrderDetails); if(finalOrderDetail is not null && finalOrderDetail.IsPaymentSuccessful && finalOrderDetail.Status == BookingStatus.Booked && localOrderDetail is not null && localOrderDetail.TotalCost == finalOrderDetail.TotalCost) { IsPaymentSuccessful = true; await LocalStorage.RemoveItemAsync(ConstantKeys.LocalRoomOrderDetails); await LocalStorage.RemoveItemAsync(ConstantKeys.LocalInitialBooking); } else { IsPaymentSuccessful = false; } } catch(Exception ex) { await JsRuntime.ToastrError(ex.Message); } finally { IsLoading = false; } } }
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
جلوگیری از ثبت سفارش اتاقی که رزرو شدهاست
پس از پایان عملیات سفارش یک اتاق، بهتر است امکان سفارش اتاقی را که دیگر در دسترس نیست، غیرفعال کنیم (تصویر فوق) که اینکار را میتوان توسط خاصیت IsBooked مدل UI کامپوننت نمایش لیست اتاقها انجام داد:
public class HotelRoomDTO { public bool IsBooked { get; set; } // ... }
namespace BlazorServer.Services { public class HotelRoomService : IHotelRoomService { // ... public async Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDateStr, DateTime? checkOutDatestr) { var hotelRooms = await _dbContext.HotelRooms .Include(x => x.HotelRoomImages) .Include(x => x.RoomOrderDetails) .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .ToListAsync(); foreach (var hotelRoom in hotelRooms) { hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDateStr, checkOutDatestr); } return hotelRooms; } public async Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate) { var hotelRoom = await _dbContext.HotelRooms .Include(x => x.HotelRoomImages) .Include(x => x.RoomOrderDetails) .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .FirstOrDefaultAsync(x => x.Id == roomId); hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDate, checkOutDate); return hotelRoom; } private bool isRoomBooked(HotelRoomDTO hotelRoom, DateTime? checkInDate, DateTime? checkOutDate) { if (checkInDate == null || checkOutDate == null) { return false; } return hotelRoom.RoomOrderDetails.Any(x => x.IsPaymentSuccessful && //check if checkin date that user wants does not fall in between any dates for room that is booked ((checkInDate < x.CheckOutDate && checkInDate.Value.Date >= x.CheckInDate) //check if checkout date that user wants does not fall in between any dates for room that is booked || (checkOutDate.Value.Date > x.CheckInDate.Date && checkInDate.Value.Date <= x.CheckInDate.Date)) ); } } }
اکنون که خاصیت IsBooked مقدار دهی شدهاست، در دو قسمت از آن استفاده خواهیم کرد:
الف) در کامپوننت نمایش لیست اتاقها
@if (room.IsBooked) { <button disabled class="btn btn-secondary btn-block">Sold Out</button> } else { <a href="@($"hotel-room-details/{room.Id}")" class="btn btn-success btn-block">Book</a> }
@if (HotelBooking.OrderDetails.HotelRoomDTO.IsBooked) { <button disabled class="btn btn-secondary btn-block">Sold Out</button> } else { <button type="submit" class="btn btn-success form-control">Checkout Now</button> }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-30.zip
در بعضی از شرایط پیش رفته، ممکن است نمونه سازی از یک Implementation Type، نیاز به دخالت مستقیم ما را داشته باشد. Implementation Factory کنترل بیشتری بر چگونگی استفادهی از Implementation Typeها را به ما ارائه میدهد. در هنگام ثبت سرویسی که Implementation Factory را در اختیار ما قرار میدهد، ما یک Delegate را برای فراخوانی سرویس استفاده میکنیم. این Delegate مسئول ساخت یک نمونه از Service Type است. برای مثال وقتی از الگوهای builder یا factory برای ساخت یک شیء استفاده میکنید، شاید نیاز باشد که Implementation Factory را به صورت دستی پیاده سازی کنید. اولین قدم این است که کدتان را در صورت امکان چنان refactor کنید تا DI Container بتواند آن را به صورت خودکار بسازد؛ ولی اینکار همیشه ممکن نیست. برای مثال بعضی از برنامه نویسان ترجیح میدهند یک Config را مستقیما از IOptionMonitor بگیرند و بعد در هر جائیکه خواستند، بجای تزریق IOptionMonitor به سرویس، مستقیما از همان سرویس ثبت شده استفاده کنند:
services.AddSingleton<ILiteDbConfig>(sp =>sp.GetRequiredService<IOptionsMonitor<LiteDbConfig>>().CurrentValue);
یک کاربرد بالقوهی دیگر برای Implementation Factory ، استفاده از Composite Pattern است. هر چند Microsoft DI Container به صورت پیش فرض از Composite Pattern پشتیبانی نمیکند، ولی ما میتوانیم آنرا پیاده سازی کنیم. فرض کنید که قبلا به ازای انجام کاری، به کاربر یک ایمیل را میفرستادیم؛ ولی حالا مالک محصول میآید و میگوید که علاوه بر ایمیل، باید پیامک هم بفرستید و ما یا این سرویس پیامک را از قبل داریم و یا باید آن را بسازیم که فرض میکنیم از قبل آن را داریم. برای این کار ما یک اینترفیس کلیتر به نام INotificationService میسازیم که دو سرویس IEmailNotificationService و ISmsNotificaitonService از آن ارث بری میکنند:
public interface INotificationService { void SendMessage(string msg, int userId); }
public class CompositeNotificationService : INotificationService { private readonly IEnumerable<INotificationService> _notificationServices; public CompositeNotificationService(IEnumerable<INotificationService> notificationServices) { _notificationServices = notificationServices; } public void SendMessage(string msg, int userId) { foreach (var notificationServicei in _notificationServices) { notificationServicei.SendMessage(msg, userId); } } }
services.AddScoped<IEmailNotificationService, EmailNotificationService>(); services.AddScoped<ISMSNotificationService, SMSNotificationService>(); services.AddSingleton<INotificationService>(sp => new CompositeNotificationService( new INotificationService[] { sp.GetRequiredService<IEmailNotificationService>() , sp.GetRequiredService<ISMSNotificationService>() } ));
وهله سازی سفارشی
در مثال بعدی نشان میدهیم که چطور میتوانیم از Implementation Factory برای برگرداندن پیادهسازی سرویسهایی که Service Provider امکان ساخت
خودکار آنها را ندارد، استفاده کنیم. فرض کنید یک کلاس Account داریم که از IAccount ارث بری میکند
و برای ساخت آن باید از متدهای IAccountBuilder که فرآیند ساخت را انجام میدهند، استفاده کنیم. بنابراین امکان ساخت مستقیم یک
شیء از IAccount وجود ندارد. در این حالت بدین صورت عمل میکنیم:
services.AddTransient<IAccountBuilder, AccountBuilder>(); services.AddScoped<IAccount>(sp => { var builder = sp.GetService<IAccountBuilder>(); builder.WithAccountType(AccountType.Guest); return builder.Build(); });
ثبت یک نمونه برای چندین اینترفیس
ممکن است بنا به دلایلی مجبور باشیم یک implementation Type را برای چند سرویس (اینترفیس) به ثبت برسانیم. در این حالت
نمونهی شیء ساخته شده، توسط هر کدام از اینترفیسها قابل استفاده است. فرض کنید یک سرویس Greeting
داریم که پیش از این فقط اینترفیس IHomeGreetingService را پیاده سازی میکرد؛ ولی بنا به دلایلی تصمیم گرفتیم که سرویسی
جامعتر را با نیازمندیهای دیگری نیز تعریف کنیم و GreetingService آن را پیاده سازی کند:
public class GreetingService : IHomeGreetingService , IGreetingService
{ // code here }
احتمالا اولین چیزی که به ذهنمان میرسد این است:
services.TryAddSingleton<IHomeGreetingService, GreetingService>(); services.TryAddSingleton<IGreetingService, GreetingService>();
مشکل روش بالا این است که دو نمونه از GreetingService ساخته میشود و درون حافظه باقی میماند و در حقیقت برای هر اینترفیس، یک نوع جداگانه از GreetingService ثبت میشود؛ در حالیکه ما میخواهیم هر دو اینترفیس فقط از یک نمونه از شیء GreetingService استفاده کنند. دو راه حل برای این مشکل وجود دارد:
var greetingService = new GreetingService(Environment); services.TryAddSingleton<IHomeGreetingService>(greetingService); services.TryAddSingleton<IGreetingService>(greetingService);
در اینجا سازندهی کلاس GreetingService فقط به environment نیاز داشت که یکی از سرویسهای پایهی فریم ورک هست و در این مرحله در دسترس است. به صورت کلی مشکل روش بالا این است که ما مسئول نمونه سازی از سرویس GreetingService هستیم! اگر GreetingService برای ساخته شدن به سرویسها یا ورودی هایی نیاز داشته باشد که فقط در زمان اجرا در دسترس باشند، ما امکان نمونه سازی آنها را نداریم؛ علاوه بر این نمیتوان از روشهای بالای برای حالتهای Scoped یا Transient استفاده کرد.
روش بعدی همان روش استفاده
از Implementation Factory است که در ادامه آن را میبینید:
services.TryAddSingleton<GreetingService>(); services.TryAddSingleton<IHomeGreetingService>(sp => sp.GetRequiredService<GreetingService>()); services.TryAddSingleton<IGreetingService>(sp => sp.GetRequiredService<GreetingService>());
در این روش خود DI Container مسئول نمونه سازی از GreetingService است. علاوه بر این میتوان با استفاده از روش فوق از طول حیاتهای Scoped و Transient هم استفاده کرد؛ در حالیکه در روش قبلی این کار امکان پذیر نبود.
Open Generics Service
گاهی از اوقات میخواهید
سرویسهایی را ثبت کنید که از اینترفیسی جنریک ارث بری میکنند. هر نوع جنریک در
زمان اجرا، نوع مخصوص به خود را واکشی میکند. ثبت کردن دستی این سرویسها میتواند خسته کننده باشد. برای همین مایکروسافت در DI Container خود قابلیت ثبت و واکشی سرویسهای جنریک را نیز در
اختیار ما گذاشتهاست. بیایید نگاهی به سرویس ILogger<T> بیندازیم. این یک سرویس درونی فریمورک است و میتواند به ازای هر نوع، کارهای
مربوط به ثبت log را
انجام بدهد و در پروژهها معمولا از این اینترفیس برای ثبت لاگها در سطح
کنترلر و سرویسها استفاده میشود:
public interface ILogger<out TCategoryName> : ILogger { }
در حالت عادی اگر سرویسی
مشابه سرویس فوق را داشته باشیم، برای ثبت کردن هر سرویس با نوع جنریک اختصاصی آن،
مجبوریم به صورت دستی آن را درون DI Container ثبت کنیم؛ مثلا باید به این صورت عمل کنیم:
services.TryAddScoped<ILogger<HomeController>,Logger<HomeController>>();
services.TryAddScoped(typeof(ILogger<>) , typeof(Logger<>));
دسته بندی سرویسها درون متدهای مختلف و پاکسازی متد ConfigurationService
تا اینجای کار ما سرویسهای مختلفی را به روشهای مختلفی ثبت کردهایم. حتی در همین آموزش ساده، تعداد زیاد سرویسهای ثبت شده، باعث شلوغی و در هم ریختگی کدهای ما میشوند که خوانایی و در ادامه اشکال زدایی و توسعهی کدها را برای ما سختتر میکنند. سادهترین کار برای دسته بندی کدها، استفاده از متدهای private محلی یا استفاده از متدهای توسعهای(الحاقی) است که در اینجا مثالی از استفاده از متدهای توسعهای را آوردهام:namespace AspNetCoreDependencyInjection.Extensions { public static class DICRegisterationExetnsion { /// <summary> /// مثال ثبت برای اپن جنریت /// </summary> /// <param name="services"></param> public static void OpenGenericRegisterationExample(this IServiceCollection services) { services.TryAddScoped<ILogger<HomeController>, Logger<HomeController>>(); services.TryAddScoped(typeof(ILogger<>), typeof(Logger<>)); } /// <summary> /// ثبت تنظیمات به روشهای مختلف /// </summary> public static void RegisterConfiguration(this IServiceCollection services, IConfiguration configuration) { services.AddSingleton(services => new AppConfig { ApplicationName = configuration["ApplicationName"], GreetingMessage = configuration["GreetingMessage"], AllowedHosts = configuration["AllowedHosts"] }); services.AddSingleton(services => new AccountTypeBalanceConfig( new List<(AccountType, long)> { (AccountType.Guest , Convert.ToInt64 (configuration["AccountInitialBalance.Guest"]) ) , (AccountType.Silver , Convert.ToInt64 (configuration["AccountInitialBalance.Silver"]) ) , (AccountType.Gold , Convert.ToInt64 (configuration["AccountInitialBalance.Gold"]) ) , (AccountType.Platinum , Convert.ToInt64 (configuration["AccountInitialBalance.Platinum"]) ) , (AccountType.Titanium , Convert.ToInt64 (configuration["AccountInitialBalance.Titanium"]) ) , }) ); services.AddSingleton(services => new LiteDbConfig { ConnectionString = configuration["LiteDbConfig:ConnectionString"], }); services.Configure<UserOptionConfig>(configuration.GetSection("UserOptionConfig")); } } }
حالا در کلاس ConfigureServices ، درون متدStartup ، به این صورت از این متدهای توسعهای استفاده میکنیم:
services.RegisterConfiguration(this.Configuration); services.OpenGenericRegisterationExample();
میتوانید کد منبع این آموزش را در اینجا ببینید.