در وبسایتی مثل آپارات، چنین آدرسی aparat.com/reporting به منزلهی آدرس دهی به کانال شخصیِ فردی است. حال اگر وبسایت ما نیز چنین سیستم آدرس دهی را داشته باشد و همچنین پیشتر یک Area با نام Reporting را نیز داشته باشیم، توسط چنین آدرسی (درحالت پیش فرض) به آن Area دسترسی خواهیم داشت:
حال اگر یکی از کاربران هنگام ساخت کانالی جدید (برای سناریوی بالا)، بخواهد آدرس کانالش Reporting باشد، با توجه به اینکه هم مسیر دسترسی به Area گزارشات (Reporting) و هم مسیر دسترسی به کانال این شخص از طریق Url بالا است، قطعا به مشکل خواهیم خورد.
برای رفع این مشکل میتوان یک فایل xml، txt و ... درست کرد و نام تمامی Areaها را در آن فایل ثبت کرد و بعد، هنگام ثبت کانال جدید (برای سناریوی بالا) توسط کاربر، فایل مذکور را خوانده و در صورتیکه نام آدرس وارد شده معادل یکی از Areaهای سایتمان بود و در لیست Areaهای از پیش ثبت شده در آن فایل قرار داشت، پیغام لازم را به کاربر نشان میدهیم و از ثبت و یا ویرایش اطلاعات، جلوگیری میکنیم.
روش فوق به درستی کار میکند و مشکلی ندارد، اما ضعف آن این است که به صورت دستی این عملیات باید انجام شود و در صورتیکه یک Area جدید اضافه شود، باید آن فایل ویرایش شود. اما میتوان با استفاده از یک Attribute، این کار را انجام و تمامی عملیات را به صورت داینامیک انجام داد.
برای شروع، یک مدل برای کانال و یک منبع داده را برای آن در نظر میگیریم:
using System.ComponentModel.DataAnnotations;
namespace SampleProject.Models
{
public class Channel
{
public string ChannelTitle { get; set; }
[Required]
public string ChannelUrl { get; set; }
}
}
using System.Collections.Generic;
namespace SampleProject.Models
{
public static class ChannelDataSource
{
static ChannelDataSource() => Channels = new List<Channel>();
public static List<Channel> Channels { get; private set; }
public static void Add(Channel channel) => Channels.Add(channel);
}
}
منبع داده، شامل یک خاصیت است که لیست تمامی کانالهای از قبل اضافه شده را بر میگرداند و یک متد افزودن که به این لیست، یک کانال را اضافه میکند.
حال یک کنترلر به نام Channel را اضافه میکنیم:
using SampleProject.Models;
using System.Linq;
using System.Web.Mvc;
namespace SampleProject.Controllers
{
public class ChannelController : Controller
{
// GET: Channel
public ActionResult Index()
{
var channels = ChannelDataSource.Channels;
return View(channels);
}
public ActionResult Channel(string channelUrl)
{
if (string.IsNullOrWhiteSpace(channelUrl))
{
return new HttpNotFoundResult("channel not found!");
}
var channel = ChannelDataSource.Channels.SingleOrDefault(ch => ch.ChannelUrl == channelUrl.ToLower());
if (channel == null)
{
return new HttpNotFoundResult("channel not found!");
}
return View(channel);
}
public ActionResult Create() => View();
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Channel channel)
{
if (!ModelState.IsValid)
{
ModelState.AddModelError(string.Empty, "Please check your inputs!");
return View(channel);
}
ChannelDataSource.Add(channel);
TempData["Message"] = "Channel added successfully!";
return RedirectToAction(nameof(Index));
}
}
}
در اکشن Index، لیستی از تمامی کانالهای موجود را نمایش میدهیم. در اکشن Channel، آدرسی را که وارد شده است، در منبع داده به دنبال آن میگردیم و یک ویوو با Template جزئیات (Details)، از مدل کانال را به کاربر نمایش میدهیم؛ در غیر اینصورت صفحه 404 را نمایش میدهیم. در اکشنهای Create، صفحه افزودن را به کاربر نمایش داده و در آن یکی اکشن، عمل افزودن را در صورتیکه اطلاعات وارد شده صحیح باشند، انجام میدهیم.
با توجه به اینکه میخواهیم سیستم مسیر دهی سایت برای کانالها تغییر کند، فایل RouteConfig در پوشهی App_Start را به شکل ذیل تغییر میدهیم:
using System.Web.Mvc;
using System.Web.Routing;
namespace SampleProject
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "ChannelUrls",
url: "{channelurl}",
defaults: new { controller = "Channel", action = "Channel", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Channel", action = "Index", id = UrlParameter.Optional }
);
}
}
}
در مسیر دهی بالا اگر "نام سایت، اسلش، نام کانال" را وارد کند اولین سیستم مسیریابی فعال میشود و او را به اکشن Channel کنترلر Channel، راهنمایی میکند.
حال برای اینکه هنگام ساخت کانال جدید، نام تکراری یکی از Areaها را وارد نکند، به این ترتیب عمل میکنیم:
ابتدا یک متد کمکی را نوشته که لیست Areaهای پروژهمان را برگشت دهد (
+ ):
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;
namespace SampleProject.Models
{
public static class Utility
{
public static List<string> GetAllAreaNames()
{
var areaNames = RouteTable.Routes.OfType<Route>()
.Where(d => d.DataTokens != null)
.Where(d=> d.DataTokens.ContainsKey("area"))
.Select(r => r.DataTokens["area"].ToString().ToLower())
.ToList();
return areaNames;
}
}
}
و بعد Attribute مورد نظر را ایجاد میکنیم:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using SampleProject.Models;
namespace SampleProject.CustomValidators
{
public class CheckForAreaExisting : ValidationAttribute, IClientValidatable
{
public List<string> AreaNames
{
get
{
return Utility.GetAllAreaNames();
}
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "checkforareaexisting",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
};
rule.ValidationParameters.Add("areanames", string.Join(",", AreaNames));
yield return rule;
}
public override bool IsValid(object value)
{
if (value != null)
{
return Utility.GetAllAreaNames()
.SingleOrDefault(area => area == value.ToString().ToLower()) == null;
}
return true;
}
}
}
در کلاس بالا توسط متد IsValid بررسی میکنیم که آیا مقدار وارد شده ( Channel Url ) با یکی از نامهای Areaهای پروژهمان تطابق دارد یا خیر، که اگر این چنین بود، مقدار false برگشت داده میشود.
توسط واسط IClientValidatable و متود GetClientValidationRules کارهای اعتبارسنجی سمت کلاینت را نیز انجام میدهیم (
+ ). مقدار خاصیت ValidationType نام متدی است که در سمت کلاینت این کار را انجام میدهد. مقدار خاصیت ValidationParameters، مقداری است که به سمت کلاینت به عنوان param فرستاده میشود تا از آن جهت اینکه آیا مقدار وارد شده توسط کاربر، یکی از Areaهای سایت هست یا خیر، استفاده کرد. در اینجا نام Areaها را با یک رشته و با یک جداکننده، توسط این خاصیت به سمت کلاینت میفرستیم.
حال در سمت کلاینت یک فایل Js را با نام CustomValidation و محتوای زیر ایجاد میکنیم:
jQuery.validator.addMethod("checkforareaexisting",
function (value, element, param) {
var isValueOneOfTheAreaNames = $.inArray(value.toLowerCase(), param.areaNames) === -1;
return isValueOneOfTheAreaNames;
});
$.validator.unobtrusive.adapters.add('checkforareaexisting', ['areanames'], function (options) {
options.rules['checkforareaexisting'] = { areaNames: options.params.areanames.split(',') };
options.messages['checkforareaexisting'] = options.message;
});
در بخش اول، نام متد که در بالا (Attribute) به آن اشاره شده است آمده است، و بعد بررسی میکنیم که آیا مقدار آمده توسط کاربر، یکی از نامهای Areaهای موجود سایت است یا خیر که اگر این طور باشد، false برگشت داده میشود و پیغام خطا به کاربر نمایش داده میشود. در بخش Onubtrusive توسط پارامتری که در Attribute برای فرستادن نام Areaها نوشته بودیم (areanames)، نامهای Areaها را میگیریم و بعد آن را Split و به Rule انتساب میدهیم و ErrorMessage ـی را که به خاصیت ChannelUrl مدلمان نسبت میدهیم، به عنوان پیغام خطا در نظر میگیریم.
فایلهای Js در Layout باید به این صورت باشند:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>_Layout</title>
<style>
</style>
</head>
<body>
<div>
@RenderBody()
</div>
<script src="~/Scripts/Jquery.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
<script src="~/Scripts/CustomValidation.js"></script>
</body>
</html>
حال کافی است به خاصیت ChannelUrl مدلمان این Attribute را نسبت دهیم:
using SampleProject.CustomValidators;
using System.ComponentModel.DataAnnotations;
namespace SampleProject.Models
{
public class Channel
{
public string ChannelTitle { get; set; }
[Required]
[CheckForAreaExisting(ErrorMessage = "You can't use this url for your channel!")]
public string ChannelUrl { get; set; }
}
}
اکنون نوبت آزمایش برنامه است. کافی است که یک یا چند Area جدید را با نامهای متفاوت، اضافه کنید و الان اگر به صفحه افزودن کانال مراجعه کنید و نام یکی از Areaهای سایت را در قسمت Channel Url وارد کنید، پیغام خطا نمایش داده میشود.
نکته: در این حالت اسامی تمامی Areaهای سایت به کلاینت ارسال میشود. اگر از این بابت احساس رضایت نمیکنید، میتوانید از خاصیت Remote توکار MVC بهره ببرید.
برای اینکار این اکشن را به کنترلر Channel اضافه میکنیم:
[HttpPost]
public ActionResult CheckForAreaExisting(string channelUrl)
{
var isValueOneOfTheAreaNames = Utility.GetAllAreaNames()
.SingleOrDefault(area => area == channelUrl.ToLower()) == null;
return Json(isValueOneOfTheAreaNames);
}
و بعد مدل نیز به این صورت تغییر میکند:
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace SampleProject.Models
{
public class Channel
{
public string ChannelTitle { get; set; }
[Required]
[Remote("CheckForAreaExisting", "Channel",
ErrorMessage = "You can't use this url for your channel!",
HttpMethod = "Post")]
public string ChannelUrl { get; set; }
}
}
به این ترتیب هر بار درخواستی به سمت سرور ارسال و طی آن بررسی میشود که مقدار وارد شده یکی از Areaهای سایت هست یا خیر؟ بدیهی است که در این حالت، دیگر نیازی به واسط IClientValidatable در کلاس CheckForAreaExisting موجود در پوشه CustomValidators وجود ندارد.