var specifiedCommentId = 4; //عدد 4 در اینجا شماره رکورد کامنتی است که دارای یک سری زیر کامنت است var list = ctx.BlogComments .Include(x => x.Reply) .Where(x => x.Id >= specifiedCommentId && (x.ReplyId == x.Reply.Id || x.Reply == null)) .ToList() // fills the childs list too .Where(x => x.Reply == null) // for TreeViewHelper .ToList();
معرفی کتابخانه PdfReport
اگر «هر شکلی» منظور ساخت گزارشات نامنظم است، شاید این روش استفاده از قالبهای open office بهتر باشد.
خروجی تصویر (jpg یا png)
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class SanitizeInputAttribute : ActionFilterAttribute { var sanitizer = new Ganss.XSS.HtmlSanitizer(); public override void OnActionExecuting(ActionExecutingContext filterContext) { if (filterContext.ActionArguments != null) { foreach (var parameter in filterContext.ActionArguments) { var properties = parameter.Value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(x => x.CanRead && x.CanWrite && x.PropertyType == typeof(string) && x.GetGetMethod(true).IsPublic && x.GetSetMethod(true).IsPublic); foreach (var propertyInfo in properties) { if (propertyInfo.GetValue(parameter.Value) != null) propertyInfo.SetValue(parameter.Value, sanitizer.Sanitize(propertyInfo.GetValue(parameter.Value).ToString())); } } } } }
[SanitizeInput] public IActionResult Add(GroupDto dto)
نحوهی نمایش خطاها در برنامههای Blazor
در حین توسعهی برنامههای Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر میشود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهدهی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ دادهاست اکتفا کرده و گزینهی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد میدهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت میگیرد:
<div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>
نحوهی مدیریت استثناءها در برنامههای Blazor
توصیه شدهاست که کار مدیریت استثناءها باید توسط توسعه دهنده صورت گیرد و بهتر است جزئیات آنها و یا stack-trace آنها را به کاربر نمایش نداد؛ تا مبادا اطلاعات حساسی فاش شوند و یا کاربر مهاجم بتواند توسط آنها اطلاعات ارزشمندی را از نحوهی عملکرد برنامه بدست آورد.
برخلاف برنامههای ASP.NET Core که دارای یک middleware pipeline هستند و برای مثال توسط آنها میتوان مدیریت سراسری خطاهای رخداده را انجام داد، چنین ویژگی در برنامههای Blazor وجود ندارد؛ چون در اینجا مرورگر است که هاست برنامه بوده و processing pipeline آنرا تشکیل میدهد.
اما ... اگر استثنائی مدیریت نشده در یک برنامهی Blazor رخدهد، این استثناء در ابتدا توسط یک ILogger، لاگ شده و سپس در کنسول مرورگر نمایش داده میشود. در اینجا Console Logging Provider، تامین کنندهی پیشفرض سیستم ثبت وقایع برنامههای Blazor است. به همین جهت استثناءهای مدیریت نشدهی برنامه را میتوان در کنسول توسعه دهندگان مرورگر نیز مشاهده کرد. برای مثال اگر سطح لاگ ارائه شده LogLevel.Error باشد، به صورت خودکار به معادل console.error ترجمه میشود.
بنابراین اگر در برنامهی Blazor جاری یک ILoggerProvider سفارشی را تهیه و آنرا به سیستم تزریق وابستگیهای برنامه معرفی کنیم، میتوان از تمام وقایع سیستم (هر قسمتی از آن که از ILogger استفاده میکند)، منجمله تمام خطاهای رخداده (و مدیریت نشده) مطلع شد و برای مثال آنها را به سمت Web API برنامه، جهت ثبت در بانک اطلاعاتی و یا نمایش در برنامهی تلگرام، ارسال کرد و این دقیقا همان کاری است که قصد داریم در ادامه انجام دهیم.
نوشتن یک ILoggerProvider سفارشی جهت ارسال رخدادها برنامهی سمت کلاینت، به یک Web API
برای ارسال تمام وقایع برنامهی کلاینت به سمت سرور، نیاز است یک ILoggerProvider سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
using System; using System.Net.Http; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BlazorWasmTelegramLogger.Client.Logging { public class ClientLoggerProvider : ILoggerProvider { private readonly HttpClient _httpClient; private readonly WebApiLoggerOptions _options; private readonly NavigationManager _navigationManager; public ClientLoggerProvider( IServiceProvider serviceProvider, IOptions<WebApiLoggerOptions> options, NavigationManager navigationManager) { if (serviceProvider is null) { throw new ArgumentNullException(nameof(serviceProvider)); } if (options is null) { throw new ArgumentNullException(nameof(options)); } _httpClient = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<HttpClient>(); _options = options.Value; _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); } public ILogger CreateLogger(string categoryName) { return new WebApiLogger(_httpClient, _options, _navigationManager); } public void Dispose() { } } }
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگیهای برنامه معرفی کنیم، روش آن به صورت زیر است:
using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public static class ClientLoggerProviderExtensions { public static ILoggingBuilder AddWebApiLogger(this ILoggingBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } builder.Services.AddSingleton<ILoggerProvider, ClientLoggerProvider>(); return builder; } } }
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده میکنید:
public ClientLoggerProvider( IServiceProvider serviceProvider, IOptions<WebApiLoggerOptions> options, NavigationManager navigationManager)
مهمترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت میدهد:
public ILogger CreateLogger(string categoryName) { return new WebApiLogger(_httpClient, _options, _navigationManager); }
using System; using System.Net.Http; using System.Net.Http.Json; using BlazorWasmTelegramLogger.Shared; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public class WebApiLogger : ILogger { private readonly WebApiLoggerOptions _options; private readonly HttpClient _httpClient; private readonly NavigationManager _navigationManager; public WebApiLogger(HttpClient httpClient, WebApiLoggerOptions options, NavigationManager navigationManager) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _options = options ?? throw new ArgumentNullException(nameof(options)); _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); } public IDisposable BeginScope<TState>(TState state) => default; public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel; public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } if (formatter is null) { throw new ArgumentNullException(nameof(formatter)); } try { ClientLog log = new() { LogLevel = logLevel, EventId = eventId, Message = formatter(state, exception), Exception = exception?.Message, StackTrace = exception?.StackTrace, Url = _navigationManager.Uri }; _httpClient.PostAsJsonAsync(_options.LoggerEndpointUrl, log); } catch { // don't throw exceptions from the logger } } } }
- متد IsEnabled آن مشخص میکند که چه سطحی از رخدادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت میکند:
using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public class WebApiLoggerOptions { public string LoggerEndpointUrl { set; get; } public LogLevel LogLevel { get; set; } = LogLevel.Information; } }
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "WebApiLogger": { "LogLevel": "Warning", "LoggerEndpointUrl": "/api/logs" } }
namespace BlazorWasmTelegramLogger.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options)); // … } } }
using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Shared { public class ClientLog { public LogLevel LogLevel { get; set; } public EventId EventId { get; set; } public string Message { get; set; } public string Exception { get; set; } public string StackTrace { get; set; } public string Url { get; set; } } }
در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام میدهد، به صورت زیر خواهد بود:
namespace BlazorWasmTelegramLogger.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options)); builder.Services.AddLogging(configure => { configure.AddWebApiLogger(); }); await builder.Build().RunAsync(); } } }
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
ایجاد سرویسی برای ارسال لاگهای برنامه به سمت تلگرام
پیش از اینکه کار تکمیل کنترلر api/logs را در برنامهی Web API انجام دهیم، ابتدا در همان برنامهی Web API، سرویسی را برای ارسال لاگهای رسیده به سمت تلگرام، تهیه میکنیم. علت اینکه این قسمت را به برنامهی سمت سرور محول کردهایم، شامل موارد زیر است:
- درست است که میتوان کتابخانههای مرتبط با تلگرام را به برنامهی سیشارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیمتر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آنرا در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامهی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامهی سمت کلاینت به Web API، مزیت لاگ سمت سرور آنرا مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر میکند و صرفا محدود به تلگرام نیست.
برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه میکنیم:
using System; using System.Text; using System.Threading.Tasks; using BlazorWasmTelegramLogger.Shared; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Telegram.Bot; using Telegram.Bot.Types.Enums; namespace BlazorWasmTelegramLogger.Server.Services { public class TelegramLoggingBotOptions { public string AccessToken { get; set; } public string ChatId { get; set; } } public interface ITelegramBotService { Task SendLogAsync(ClientLog log); } public class TelegramBotService : ITelegramBotService { private readonly string _chatId; private readonly TelegramBotClient _client; public TelegramBotService(IOptions<TelegramLoggingBotOptions> options) { _chatId = options.Value.ChatId; _client = new TelegramBotClient(options.Value.AccessToken); } public async Task SendLogAsync(ClientLog log) { var text = formatMessage(log); if (string.IsNullOrWhiteSpace(text)) { return; } await _client.SendTextMessageAsync(_chatId, text, ParseMode.Markdown); } private static string formatMessage(ClientLog log) { if (string.IsNullOrWhiteSpace(log.Message)) { return string.Empty; } var sb = new StringBuilder(); sb.Append(toEmoji(log.LogLevel)) .Append(" *") .AppendFormat("{0:hh:mm:ss}", DateTime.Now) .Append("* ") .AppendLine(log.Message); if (!string.IsNullOrWhiteSpace(log.Exception)) { sb.AppendLine() .Append('`') .AppendLine(log.Exception) .AppendLine(log.StackTrace) .AppendLine("`") .AppendLine(); } sb.Append("*Url:* ").AppendLine(log.Url); return sb.ToString(); } private static string toEmoji(LogLevel level) => level switch { LogLevel.Trace => "⬜️", LogLevel.Debug => "🟦", LogLevel.Information => "⬛️️️", LogLevel.Warning => "🟧", LogLevel.Error => "🟥", LogLevel.Critical => "❌", LogLevel.None => "🔳", _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) }; } }
- برای کار با API تلگرام، از کتابخانهی معروف Telegram.Bot استفاده کردهایم که به صورت زیر، وابستگی آن به برنامهی Web API اضافه میشود:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Telegram.Bot" Version="15.7.1" /> </ItemGroup> </Project>
public class TelegramLoggingBotOptions { public string AccessToken { get; set; } public string ChatId { get; set; } }
پس از شروع این بات، ابتدا دستور newbot/ را صادر کنید. سپس یک نام را از شما میپرسد. نام دلخواهی را وارد کنید. در ادامه یک نام منحصربفرد را جهت شناسایی این بات خواهد پرسید. پس از دریافت آن، توکن خود را همانند تصویر فوق، مشاهده میکنید.
- مرحلهی بعد تنظیم ChatId است. نحوهی کار برنامه به این صورت است که پیامها را به این بات سفارشی خود ارسال کرده و این بات، آنها را به کانال اختصاصی ما هدایت میکند. بنابراین یک کانال جدید را ایجاد کنید. ترجیحا بهتر است این کانال خصوصی باشد. سپس کاربر test_2021_logs_bot@ (همان نام منحصربفرد بات که حتما باید با @ شروع شود) را به عنوان عضو جدید کانال خود اضافه کنید. در اینجا عنوان میکند که این کاربر چون بات است، باید دسترسی ادمین را داشته باشد که دقیقا این دسترسی را نیز باید برقرار کنید تا بتوان توسط این بات، پیامی را به کانال اختصاصی خود ارسال کرد.
بنابراین تا اینجا یک کانال خصوصی را ایجاد کردهایم که بات جدید test_2021_logs_bot@ عضو با دسترسی ادمین آن است. اکنون باید Id این کانال را بیابیم. برای اینکار بات دیگری را به نام JsonDumpBot@ یافته و استارت کنید. سپس در کانال خود یک پیام آزمایشی جدید را ارسال کنید و در ادامه این پیام را به بات JsonDumpBot@ ارسال کنید (forward کنید). همان لحظهای که کار ارسال پیام به این بات صورت گرفت، Id کانال خود را در پاسخ آن میتوانید مشاهده کنید:
در این تصویر مقدار forward_from_chat:id همان ChatId تنظیمات برنامهی شما است.
در آخر این اطلاعات را در فایل Server\appsettings.json قرار میدهیم:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "TelegramLoggingBot": { "AccessToken": "1826…", "ChatId": "-1001…" } }
namespace BlazorWasmTelegramLogger.Server { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.Configure<TelegramLoggingBotOptions>(options => Configuration.GetSection("TelegramLoggingBot").Bind(options)); services.AddSingleton<ITelegramBotService, TelegramBotService>(); // ... } // ... } }
public class TelegramBotService : ITelegramBotService { private readonly string _chatId; private readonly TelegramBotClient _client; public TelegramBotService(IOptions<TelegramLoggingBotOptions> options) { _chatId = options.Value.ChatId; _client = new TelegramBotClient(options.Value.AccessToken); }
ایجاد کنترلر Logs، جهت دریافت لاگهای رسیدهی از سمت کلاینت
مرحلهی آخر کار بسیار سادهاست. سرویس تکمیل شدهی ITelegramBotService را به سازندهی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آنرا فراخوانی میکنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
using System; using System.Threading.Tasks; using BlazorWasmTelegramLogger.Server.Services; using BlazorWasmTelegramLogger.Shared; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Server.Controllers { [ApiController] [Route("api/[controller]")] public class LogsController : ControllerBase { private readonly ILogger<LogsController> _logger; private readonly ITelegramBotService _telegramBotService; public LogsController(ILogger<LogsController> logger, ITelegramBotService telegramBotService) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _telegramBotService = telegramBotService; } [HttpPost] public async Task<IActionResult> PostLog(ClientLog log) { // TODO: Save the client's `log` in the database _logger.Log(log.LogLevel, log.EventId, log.Url + Environment.NewLine + log.Message); await _telegramBotService.SendLogAsync(log); return Ok(); } } }
آزمایش برنامه
برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار دادهایم:
@page "/counter" <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; throw new InvalidOperationException("This is an exception message from the client!"); } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmTelegramLogger.zip
در ادامه قصد داریم نحوه پیاده سازی آنرا در ASP.NET MVC به کمک امکانات jQuery بررسی کنیم.
مدل برنامه
namespace jQueryMvcSample02.Models { public class BlogPost { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } }
منبع داده فرضی برنامه
using System.Collections.Generic; using System.Linq; using jQueryMvcSample02.Models; namespace jQueryMvcSample02.DataSource { public static class BlogPostDataSource { private static IList<BlogPost> _cachedItems; /// <summary> /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانیها صورت خواهد گرفت /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای /// تهیه یک لیست برای تمام کاربران سایت است /// </summary> static BlogPostDataSource() { _cachedItems = createBlogPostsInMemoryDataSource(); } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> private static IList<BlogPost> createBlogPostsInMemoryDataSource() { var results = new List<BlogPost>(); for (int i = 1; i < 30; i++) { results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i }); } return results; } /// <summary> /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند /// شماره صفحه از یک شروع میشود /// </summary> public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4) { var skipRecords = pageNumber * recordsPerPage; return _cachedItems .OrderByDescending(x => x.Id) .Skip(skipRecords) .Take(recordsPerPage) .ToList(); } } }
تنها نکته مهم آن، نحوه تعریف متد GetLatestBlogPosts میباشد که برای صفحه بندی اطلاعات بهینه سازی شده است. در اینجا توسط متدهای Skip و Take، تنها بازهای از اطلاعات که قرار است نمایش داده شوند، دریافت میگردد. خوشبختانه این متدها معادلهای مناسبی را در اکثر بانکهای اطلاعاتی داشته و استفاده از آنها بر روی یک بانک اطلاعاتی واقعی نیز بدون مشکل کار میکند و تنها بازه محدودی از اطلاعات را واکشی خواهد کرد که از لحاظ مصرف حافظه و سرعت کار بسیار مقرون به صرفه و سریع است.
کنترلر برنامه
using System.Linq; using System.Web.Mvc; using System.Web.UI; using jQueryMvcSample02.DataSource; using jQueryMvcSample02.Security; namespace jQueryMvcSample02.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index() { //آغاز کار با صفحه صفر است var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0); return View(list); //نمایش ابتدایی صفحه } [HttpPost] [AjaxOnly] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public virtual ActionResult PagedIndex(int? page) { var pageNumber = page ?? 0; var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber); if (list == null || !list.Any()) return Content("no-more-info"); //این شرط ما است برای نمایش عدم یافتن رکوردها return PartialView("_ItemsList", list); } [HttpGet] public ActionResult Post(int? id) { if (id == null) return Redirect("/"); //todo: show the content here return Content("Post " + id.Value); } } }
الف) یک متد که بر اساس HttpGet کار میکند. این متد در اولین بار نمایش صفحه فراخوانی میگردد و اطلاعات صفحه آغازین را نمایش میدهد.
ب) متد دومی که بر اساس HttpPost کار کرده و محدود است به درخواستیهای AjaxOnly همانند متد PagedIndex.
از این متد دوم برای پردازش کلیکهای کاربر بر روی دکمه «بیشتر» استفاده میگردد. بنابراین تنها کاری که افزونه جیکوئری تدارک دیده شده ما باید انجام دهد، ارسال شماره صفحه است. سپس با استفاده از این شماره، بازه مشخصی از اطلاعات دریافت و نهایتا یک PartialView رندر شده برای افزوده شدن به صفحه بازگشت داده میشود.
دو View برنامه
همانطور که برای بازگشت اطلاعات نیاز به دو اکشن متد است، برای رندر اطلاعات نیز به دو View نیاز داریم:
الف) یک PartialView که صرفا لیستی از اطلاعات را مطابق سلیقه ما رندر میکند. از این PartialView در متد PagedIndex استفاده خواهد شد:
@model IList<jQueryMvcSample02.Models.BlogPost> <ul> @foreach (var item in Model) { <li> <h5> @Html.ActionLink(linkText: item.Title, actionName: "Post", controllerName: "Home", routeValues: new { id = item.Id }, htmlAttributes: null) </h5> @item.Body </li> } </ul>
@model IList<jQueryMvcSample02.Models.BlogPost> @{ ViewBag.Title = "Index"; var loadInfoUrl = Url.Action(actionName: "PagedIndex", controllerName: "Home"); } <h2> اسکرول نامحدود</h2> @{ Html.RenderPartial("_ItemsList", Model); } <div id="MoreInfoDiv"> </div> <div align="center" style="margin-bottom: 9px;"> <span id="moreInfoButton" style="width: 90%;" class="btn btn-info">بیشتر</span> </div> <div id="ProgressDiv" align="center" style="display: none"> <br /> <img src="@Url.Content("~/Content/images/loadingAnimation.gif")" alt="loading..." /> </div> @section JavaScript { <script type="text/javascript"> $(document).ready(function () { $("#moreInfoButton").InfiniteScroll({ moreInfoDiv: '#MoreInfoDiv', progressDiv: '#ProgressDiv', loadInfoUrl: '@loadInfoUrl', loginUrl: '/login', errorHandler: function () { alert('خطایی رخ داده است'); }, completeHandler: function () { // اگر قرار است روی اطلاعات نمایش داده شده پردازش ثانوی صورت گیرد }, noMoreInfoHandler: function () { alert('اطلاعات بیشتری یافت نشد'); } }); }); </script> }
1) مسیر دقیق اکشن متد PagedIndex توسط متد Url.Action تهیه شده است.
2) در ابتدای نمایش صفحه، متد Html.RenderPartial کار نمایش اولیه اطلاعات را انجام خواهد داد.
3) از div خالی MoreInfoDiv، به عنوان محل افزوده شدن اطلاعات Ajax ایی دریافتی استفاده میکنیم.
4) دکمه بیشتر در اینجا تنها یک span ساده است که توسط css به شکل یک دکمه نمایش داده خواهد شد (فایلهای آن در پروژه پیوست موجود است).
5) ProgressDiv در ابتدای نمایش صفحه مخفی است. زمانیکه کاربر بر روی دکمه بیشتر کلیک میکند، توسط افزونه جیکوئری ما نمایان شده و در پایان کار مجددا مخفی میگردد.
6) section JavaScript کار استفاده از افزونه InfiniteScroll را انجام میدهد.
و کدهای افزونه اسکرول نامحدود
// <![CDATA[ (function ($) { $.fn.InfiniteScroll = function (options) { var defaults = { moreInfoDiv: '#MoreInfoDiv', progressDiv: '#Progress', loadInfoUrl: '/', loginUrl: '/login', errorHandler: null, completeHandler: null, noMoreInfoHandler: null }; var options = $.extend(defaults, options); var showProgress = function () { $(options.progressDiv).css("display", "block"); } var hideProgress = function () { $(options.progressDiv).css("display", "none"); } return this.each(function () { var moreInfoButton = $(this); var page = 1; $(moreInfoButton).click(function (event) { showProgress(); $.ajax({ type: "POST", url: options.loadInfoUrl, data: JSON.stringify({ page: page }), contentType: "application/json; charset=utf-8", dataType: "json", complete: function (xhr, status) { var data = xhr.responseText; if (xhr.status == 403) { window.location = options.loginUrl; } else if (status === 'error' || !data) { if (options.errorHandler) options.errorHandler(this); } else { if (data == "no-more-info") { if (options.noMoreInfoHandler) options.noMoreInfoHandler(this); } else { var $boxes = $(data); $(options.moreInfoDiv).append($boxes); } page++; } hideProgress(); if (options.completeHandler) options.completeHandler(this); } }); }); }); }; })(jQuery); // ]]>
هربار که کاربر بر روی دکمه بیشتر کلیک میکند، progress div ظاهر میگردد. سپس توسط امکانات jQuery Ajax، شماره صفحه (بازه انتخابی) به اکشن متد صفحه بندی اطلاعات ارسال میگردد. در نهایت اطلاعات را از کنترلر دریافت و به moreInfoDiv اضافه میکند. در آخر هم شماره صفحه را یکی افزایش داده و سپس progress div را مخفی میکند.
دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample02.zip
در مقالهی قبل توانستیم یک سری
از مدلهای مربوط به وبلاگ را آماده کنیم. در ادامه به تکمیل آن و همچین
آغاز تهیهی مدلهای مربوط به اخبار و پیغام خصوصی میپردازیم.
همکاران این قسمت:
سلمان معروفی
مدل گزارش دهی
/// <summary> /// Repersents a Report template for every cms section /// </summary> public class Report { #region Ctor /// <summary> /// Create one instance for <see cref="Report"/> /// </summary> public Report() { ReportedOn = DateTime.Now; Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// gets or sets identifier for Report /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets reason of report /// </summary> public virtual string Reason { get; set; } /// <summary> /// gets or sets section that is reported /// </summary> public virtual ReportSection Section { get; set; } /// <summary> /// gets or sets sectionid that is reported /// </summary> public virtual long SectionId { get; set; } /// <summary> /// gets or sets type of report /// </summary> public virtual ReportType Type{ get; set; } /// <summary> /// gets or sets report's datetime /// </summary> public virtual DateTime ReportedOn { get; set; } /// <summary> /// indicate this report is read by admin /// </summary> public virtual bool IsRead { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets id of user that is reporter /// </summary> public virtual long ReporterId { get; set; } /// <summary> /// gets or sets id of user that is reporter /// </summary> public virtual User Reporter { get; set; } #endregion } /// <summary> /// Represents Report Section /// </summary> public enum ReportSection { News, Poll, Announcement, ForumTopic, BlogComment, BlogPost, NewsComment, PollComment, AnnouncementComment, ForumPost, User, ... } /// <summary> /// Represents Type of Report /// </summary> public enum ReportType { Spam, Abuse, Advertising, ... }
قصد داریم در این سیستم به کاربران خاصی دسترسی گزارش دادن در بخشهای مختلف را بدهیم. این دسترسیها در بخش تنظیمات سیستم قابل تغییر خواهند بود (برای مثال براساس امتیاز ، براساس تعداد پست و ... ) . این امکان میتواند برای مدیریت سیستم مفید باشد.
برای سیستم گزارش دهی به مانند سیستم امتیاز دهی عمل خواهیم کرد. در کلاس Report، خصوصیت ReportSection از نوع دادهی شمارشی میباشد که در بالا تعریف آن نیز آماده است و مشخص کنندهی بخشهایی میباشد که لازم است امکان گزارش دهی داشته باشند. خصوصیت Type هم که از نوع شمارشی ReportType میباشد، مشخص کنندهی نوع گزارشی است که داده شده است.
علاوه بر نوع گزارش، میتوان دلیل گزارش را هم ذخیره کرد که برای این منظور خصوصیت Reason در نظر گرفته شدهاست. خصوصیت IsRead هم برای مدیریت این گزارشات در پنل مدیریت در نظر گرفته شده است. اگر در مقالهی قبل دقت کرده باشید، متوجه وجود خصوصیتی به نام ReportsCount در کلاس BaseContent و BaseComment خواهید شد که برای نشان دادن تعداد گزارشهایی است که برای آن مطلب یا نظر داده شده است، استفاده میشود.
کلاس پایه فایلهای ضمیمه
/// <summary> /// Represents a base class for every attachment /// </summary> public abstract class BaseAttachment { #region Ctor public BaseAttachment() { Id = SequentialGuidGenerator.NewSequentialGuid(); AttachedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// sets or gets identifier for attachment /// </summary> public virtual Guid Id { get; set; } /// <summary> /// sets or gets name for attachment /// </summary> public virtual string FileName { get; set; } /// <summary> /// sets or gets type of attachment /// </summary> public virtual string ContentType { get; set; } /// <summary> /// sets or gets size of attachment /// </summary> public virtual long Size { get; set; } /// <summary> /// sets or gets Extention of attachment /// </summary> public virtual string Extension { get; set; } /// <summary> /// sets or gets bytes of data /// </summary> //public byte[] Data { get; set; } /// <summary> /// sets or gets Creation Date /// </summary> public virtual DateTime AttachedOn { get; set; } /// <summary> /// gets or sets counts of download this file /// </summary> public virtual long DownloadsCount { get; set; } /// <summary> /// gets or sets datetime that is modified /// </summary> public virtual DateTime ModifiedOn { get; set; } /// <summary> /// gets or sets section that this file attached there /// </summary> public virtual AttachmentSection Section { get; set; } /// <summary> /// gets or sets information of user agent /// </summary> public virtual string Agent { get; set; } #endregion #region NavigationProperties /// <summary> /// sets or gets identifier of attachment's owner /// </summary> public virtual long OwnerId { get; set; } /// <summary> /// sets or gets identifier of attachment's owner /// </summary> public virtual User Owner { get; set; } #endregion } public enum AttachmentSection { News, Announcement, ForumTopic, Conversation, BlogComment, NewsComment, PollComment, AnnouncementComment, ForumPost, BlogPost, Group, ... }
کلاس بالا اکثر خصوصیات لازم برای مدل Attachment ما را در خود دارد. قصد داریم از ارث بری TPH برای مدیریت فایلهای ضمیمه استفاده کنیم. در سیستم بستهی ما، تنها کاربران احراز هویت شده میتوانند فایل ضمیمه کنند و برای همین منظور OwnerId را که همان ارسال کنندهی فایل میباشد، به صورت Nullable در نظر نگرفتهایم.
یک سری از مشخصات که نیاز به توضیح اضافی ندارند، ولی خصوصیت AttachmentSection که از نوع شمارشی AttachmentSection است، برای دسترسی راحت کاربر به فایلهای ارسالی خود در پنل کاربری در نظر گرفته شده است. برای بخشهای (وبلاگ - اخبار - نظرسنجیها - آگهیها - انجمن) که نیاز به Privacy خاصی نیست و احراز هویت کفایت میکند، مدل زیر را در نظر گرفته ایم:
مدل فایلهای ضمیمه عمومی
/// <summary> /// Repersent the attachment for file /// </summary> public class Attachment : BaseAttachment { }
/// <summary> /// Represents one news item /// </summary> public class NewsItem : BaseContent { #region Ctor /// <summary> /// create one instance of <see cref="NewsItem"/> /// </summary> public NewsItem() { Rating = new Rating(); PublishedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// indicating that this news show on sidebar /// </summary> public virtual bool ShowOnSideBar { get; set; } /// <summary> /// indicate this NewsItem is approved by admin if NewsItem.Moderate==true /// </summary> public virtual bool IsApproved { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets newsitem's Reviews /// </summary> public ICollection<NewsComment> Comments { get; set; } #endregion }
کلاس بالا نشان دهندهی اشتراکهای ما خواهند بود. این مدل ما هم از کلاس پایهی BaseContent بحث شده در مقالهی قبل، ارث بری کرده و علاوه بر آن دو خصوصیت دیگر تحت عنوان IsApproved برای اعمال مدیریتی در نظر گرفته شده است (اگر در بخش تنظیمات سیستم اخبار، مدیریت تصمیم گرفته باشد تا اخبار جدید به اشتراک گذاشته شده با تأیید مدیریتی منتشر شوند) و خصوصیت ShowOnSideBar هم به عنوان یک تنظیم مدیریتی برای خبر خاصی در نظر گرفته شده که لازم است به صورت sticky در سایدبار نمایش داده شود.
برای اخبار نیز امکان ارسال نظر خواهیم داشت که برای این منظور لیستی از مدل زیر (NewsComment) در مدل بالا تعریف شده است .
مدل نظرات اخبار
public class NewsComment : BaseComment { #region Ctor public NewsComment() { Rating = new Rating(); CreatedOn = DateTime.Now; } #endregion #region NavigationProperties /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual NewsComment Reply { get; set; } /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual ICollection<NewsComment> Children { get; set; } /// <summary> /// gets or sets NewsItem that this comment sent to it /// </summary> public virtual NewsItem NewsItem { get; set; } /// <summary> /// gets or sets NewsItem'Id that this comment sent to it /// </summary> public virtual long NewsItemId { get; set; } #endregion }
/// <summary> /// Indicate one conversation /// </summary> public class Conversation { #region Ctor /// <summary> /// create one instance of <see cref="Conversation"/> /// </summary> public Conversation() { Id = SequentialGuidGenerator.NewSequentialGuid(); SentOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier of record /// </summary> public virtual Guid Id { get; set; } /// <summary> /// represents this conversaion is seen /// </summary> public virtual bool IsRead { get; set; } /// <summary> /// gets or sets subject of this conversation /// </summary> public virtual string Subject { get; set; } /// <summary> /// gets or sets Date that this record added /// </summary> public virtual DateTime SentOn { get; set; } /// <summary> /// indicate this record deleted by sender /// </summary> public virtual bool DeletedBySender { get; set; } /// <summary> /// indicate this record deleted by receiver /// </summary> public virtual bool DeletedByReceiver { get; set; } /// <summary> /// gets or sets Messagescount that Unread by sender of this conversation /// </summary> public virtual int UnReadSenderMessagesCount { get; set; } /// <summary> /// gets or sets Messagescount that Unread by receiver of this conversation /// </summary> public virtual int UnReadReceiverMessagesCount { get; set; } /// <summary> /// gets or sets Messagescount of this conversation for increase performance /// </summary> public virtual int MessagesCount { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets if of user that start this conversation /// </summary> public virtual long SenderId { get; set; } /// <summary> /// gets or sets user that start this conversation /// </summary> public virtual User Sender { get; set; } /// <summary> /// gets or sets id of user that is recipient /// </summary> public virtual long ReceiverId { get; set; } /// <summary> /// gets or sets user that is recipient /// </summary> public virtual User Receiver { get; set; } /// <summary> /// get or set Messages of this conversation /// </summary> public virtual ICollection<ConversationReply> Messages { get; set; } /// <summary> /// get or set Attachments that attached in this conversation /// </summary> public virtual ICollection<ConversationAttachment> Attachments { get; set; } #endregion
مدل بالا نشان دهندهی گفتگوی بین دو کاربر میباشد. هر گفتگو امکان دارد با موضوع خاصی ایجاد شود و مسلما یک کاربر بهعنوان دریافت کننده و کاربر دیگری بعنوان ارسال کننده خواهد بود. برای این منظور خصوصیات Receiver و Sender که از نوع User هستند را در این کلاس در نظر گرفتهایم.
خصوصیات DeletedBySender و DeletedByReceiver هم برای این در نظر گفته شدهاند که اگر یک طرف این گفتگو خواهان حذف آن باشد، برای آن کاربر حذف نرم انجام دهیم و فعلا برای کاربر مقابل قابل دسترسی باشد.
UnReadSenderMessagesCount و UnReadReceiverMessagesCount هم برای بالا بردن کارآیی سیستم در نظر گفته شدهاند و در واقع تعداد پیغامهای خوانده نشده در یک گفتگو به صورت متمایز برای هر دو طرف، ذخیره میشود. هر گفتگو شامل یکسری پیغام رد و بدل شده خواهد بود که بدین منظور لیستی از ConversationReplyها را در مدل بالا تعریف کردهایم.
در هر گفتگو یکسری فایل هم ممکن است ضمیمه شود ، برای این منظور هم یک لیستی از کلاس ConversationAttachment در مدل گفتگو تعریف شده است که در ادامه پیاده سازی کلاس ConversationAttachment را هم خواهیم دید.
مدل ConversationReply به شکل زیر میباشد:
/// <summary> /// Represents One Reply to Conversation /// </summary> public class ConversationReply { #region Ctor /// <summary> /// create one instance of <see cref="ConversationReply"/> /// </summary> public ConversationReply() { Id = SequentialGuidGenerator.NewSequentialGuid(); SentOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier of record /// </summary> public virtual Guid Id { get; set; } /// <summary> /// represents this conversaionReply is seen /// </summary> public virtual bool IsRead { get; set; } /// <summary> /// gets or sets body of this conversationReply /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets Date that this record added /// </summary> public virtual DateTime SentOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Parent's Id Of this ConversationReply /// </summary> public virtual Guid? ParentId { get; set; } /// <summary> /// gets or sets Parent Of this ConversationReply /// </summary> public virtual ConversationReply Parent { get; set; } /// <summary> /// get or set Children Of this ConversationReply /// </summary> public virtual ICollection<ConversationReply> Children { get; set; } /// <summary> /// gets or sets if of user that start this conversationReply /// </summary> public virtual long SenderId { get; set; } /// <summary> /// gets or sets user that start this conversationReply /// </summary> public virtual User Sender { get; set; } /// <summary> /// gets or sets Conversation that this message sent in it /// </summary> public virtual Conversation Conversation{ get; set; } /// <summary> /// gets or sets Id of Conversation that this message sent in it /// </summary> public virtual Guid ConversationId { get; set; } #endregion }
مدل بالا نشان دهندهی پیغامهای داده شده در یک گفتگو با موضوعی خاص میباشد. ساختار درختی آن هم برای ایجاد امکان جواب دهی برای پیغامها در نظر گرفته شده است (الزامی نیست). هر پیغام در یک گفتگو ارسال شده و یک ارسال کننده نیز دارد که برای این منظور به ترتیب دو خصوصیت Conversation از نوع کلاس Conversation و Sender از نوع User در نظر گرفتهایم.
با توجه به وجود Privacy در گفتگو نیاز است تا مدل فایل ضمیمه بخش گفتگوها به شکل زیر باشد:
/// <summary> /// Represents the attachment That attached in Conversation /// </summary> public class ConversationAttachment : BaseAttachment { #region NavigationProperties public virtual Conversation Conversation { get; set; } public virtual Guid? ConversationId { get; set; } #endregion }
همانطور که کمی بالاتر بحث شد، قصد اعمال ارث بری TPH را برای مدیریت فایلهای ضمیمه داریم. برای این منظور مدل بالا نیز از کلاس BaseAttachment ارث بری کرده و دو خصوصیت اضافه هم برای اعمال ارتباط یک به چند با گفتگو خواهد داشت. توجه کنید که ConversationId به صورت Nullable تعریف شدهاست.
نتیجه این قسمت
برای تهیه یک RadioButtonList نیز میتوان از همان نکتهی CheckBoxList استفاده کرد: نام عناصر radio button اضافه شده به صفحه را یکسان وارد میکنیم. به این ترتیب یک گروه تشکیل خواهد شد و زمانیکه اطلاعات این عناصر به سرور ارسال میشود، اینبار بجای یک آرایه، تنها مقدار کنترل انتخاب شده، ارسال میگردد. یک مثال:
یک پروژه جدید و خالی ASP.NET MVC را آغاز کنید. سپس کنترلر Home و View خالی Index را نیز ایجاد نمائید. محتویات این دو را به نحو زیر تغییر دهید:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm1 (Normal)</legend>
@using (Html.BeginForm(actionName: "HandleForm1", controllerName: "Home"))
{
@:your favorite tech: <br />
@Html.RadioButton(name: "tech", value: ".NET", isChecked: true) @:DOTNET <br />
@Html.RadioButton(name: "tech", value: "JAVA", isChecked: false) @:JAVA <br />
@Html.RadioButton(name: "tech", value: "PHP", isChecked: false) @:PHP <br />
<input type="submit" value="Submit" />
}
</fieldset>
using System.Collections.Generic;
using System.Web.Mvc;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult HandleForm1(string tech)
{
return RedirectToAction("Index");
}
}
}
در اینجا سه RadioButton با نامی یکسان در صفحه اضافه شدهاند. سپس داخل متد HandleForm1 یک breakpoint قرار دهید. اکنون برنامه را اجرا کنید و فرم را به سرور ارسال نمائید. پارامتر tech با value عنصر انتخابی مقدار دهی خواهد شد.
تهیه یک RadioButtonList عمومی
اطلاعات فوق را میتوان تبدیل به یک HtmlHelper با قابلیت استفاده مجدد نیز نمود:
@helper RadioButtonList(string groupName, IEnumerable<System.Web.Mvc.SelectListItem> items)
{
<div class="RadioButtonList">
@foreach (var item in items)
{
@item.Text
<input type="radio" name="@groupName"
value="@item.Value"
@if (item.Selected) { <text>checked="checked"</text> }
/>
<br />
}
</div>
}
برای مثال یک فایل را در مسیر app_code\Helpers.cshtml ایجاد کرده و اطلاعات فوق را به آن اضافه نمائید.
اینبار برای استفاده از آن خواهیم داشت:
using System.Collections.Generic;
using System.Web.Mvc;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
ViewBag.Tags = new[]
{
new SelectListItem { Text = ".NET", Value = "Val1", Selected = true },
new SelectListItem { Text = "JAVA", Value = "Val2", Selected = false },
new SelectListItem { Text = "PHP", Value = "Val3", Selected = false }
};
return View();
}
[HttpPost]
public ActionResult HandleForm2(string preferredTechnology)
{
return RedirectToAction("Index");
}
}
}
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm2 (Helper)</legend>
@using (Html.BeginForm(actionName: "HandleForm2", controllerName: "Home"))
{
@:your favorite tech: <br />
@Helpers.RadioButtonList("preferredTechnology", (SelectListItem[])ViewBag.Tags)
<input type="submit" value="Submit" />
}
</fieldset>
متد سفارشی تهیه شده، یک آرایه از SelectListItem ها را دریافت کرده و به صورت خودکار تبدیل به RadioButtonList میکند. بر اساس نام آن میتوان به مقدار انتخاب شده ارسالی به سرور در کنترلر مرتبط، دسترسی یافت.
تهیه یک Templated helper سفارشی
در عمل زمانیکه با مدلها کار میکنیم و اطلاعات برنامه قرار است Strongly typed باشند، مرسوم است لیستی از انتخابها را به صورت یک enum تعریف کنند. برای مثال مدل زیر را به برنامه اضافه کنید:
using System.ComponentModel.DataAnnotations;
namespace MvcApplication23.Models
{
public enum Gender
{
[Display(Name = "مرد")]
Male,
[Display(Name = "زن")]
Female,
}
public class User
{
[ScaffoldColumn(false)]
public int Id { set; get; }
[Display(Name = "نام")]
public string Name { set; get; }
[Display(Name = "جنسیت")]
[UIHint("EnumRadioButtonList")]
public Gender Gender { set; get; }
}
}
قصد داریم یک Templated helper سفارشی را به نام EnumRadioButtonList، ایجاد کنیم تا در زمان فراخوانی متد Html.EditorForModel، به صورت خودکار enum تعریف شده را به صورت یک RadioButtonList نمایش دهد.
برای این منظور فایل جدید Views\Shared\EditorTemplates\EnumRadioButtonList.cshtml را به برنامه اضافه کنید. محتوای آنرا به نحو زیر تغییر دهید:
@using System.ComponentModel.DataAnnotations
@using System.Globalization
@model Enum
@{
Func<Enum, string> getDescription = enumItem =>
{
var type = enumItem.GetType();
var memInfo = type.GetMember(enumItem.ToString());
if (memInfo != null && memInfo.Any())
{
var attrs = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
if (attrs != null && attrs.Any())
return ((DisplayAttribute)attrs[0]).GetName();
}
return enumItem.ToString();
};
var listItems = Enum.GetValues(Model.GetType())
.OfType<Enum>()
.Select(enumItem =>
new SelectListItem()
{
Text = getDescription(enumItem),
Value = enumItem.ToString(),
Selected = enumItem.Equals(Model)
});
string prefix = ViewData.TemplateInfo.HtmlFieldPrefix;
ViewData.TemplateInfo.HtmlFieldPrefix = string.Empty;
int index = 0;
foreach (var li in listItems)
{
string fieldName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", prefix, index++);
<div class="editor-radio">
@Html.RadioButton(prefix, li.Value, li.Selected, new { @id = fieldName })
@Html.Label(fieldName, li.Text)
</div>
}
ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
}
در اینجا به کمک Reflection به اطلاعات enum دریافتی دسترسی خواهیم داشت. بر این اساس میتوان نام عناصر آنرا یافت و تبدیل به یک RadioButtonList کرد. البته کار به همینجا ختم نمیشود. در این بین باید دقت داشت که ممکن است از ویژگی Display (مانند مدل نمونه فوق) بر روی تک تک عناصر یک enum نیز استفاده شود. به همین جهت این مورد نیز باید پردازش گردد.
نهایتا برای استفاده از این Templated helper سفارشی، کنترلر و View برنامه را به نحو زیر میتوان تغییر داد:
using System.Collections.Generic;
using System.Web.Mvc;
using MvcApplication23.Models;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
var user = new User { Id = 1, Name = "name 1", Gender = Gender.Male };
return View(user);
}
[HttpPost]
public ActionResult HandleForm3(User user)
{
return RedirectToAction("Index");
}
}
}
@model MvcApplication23.Models.User
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm3 (EditorForModel)</legend>
@using (Html.BeginForm(actionName: "HandleForm3", controllerName: "Home"))
{
@Html.EditorForModel()
<input type="submit" value="Submit" />
}
</fieldset>
برای استفاده از یک templated helper سفارشی چندین روش وجود دارد:
الف) همانند مثال فوق از ویژگی UIHint استفاده شود.
ب) نام فایل را به enum.cshtml تغییر دهیم. به این ترتیب از این پس کلیه enumها در صورت استفاده از متد Html.EditorForModel، به صورت خودکار تبدیل به یک RadioButtonList میشوند.
ج) متد زیر نیز همین کار را انجام میدهد:
@Html.EditorFor(model => model.EnumProperty, "EnumRadioButtonList")