مطالب
نحوه اضافه کردن Auto-Complete به جستجوی لوسین در ASP.NET MVC و Web forms
پیشنیازها:
چگونه با استفاده از لوسین مطالب را ایندکس کنیم؟
چگونه از افزونه jQuery Auto-Complete استفاده کنیم؟
نحوه استفاده صحیح از لوسین در ASP.NET


اگر به جستجوی سایت دقت کرده باشید، قابلیت ارائه پیشنهاداتی به کاربر توسط یک Auto-Complete به آن اضافه شده‌است. در مطلب جاری به بررسی این مورد به همراه دو مثال Web forms و MVC پرداخته خواهد شد.


قسمت عمده مطلب جاری با پیشنیازهای یاد شده فوق یکی است. در اینجا فقط به ذکر تفاوت‌ها بسنده خواهد شد.

الف) دریافت لوسین
از طریق NuGet آخرین نگارش را دریافت و به پروژه خود اضافه کنید. همچنین Lucene.NET Contrib را نیز به همین نحو دریافت نمائید.

ب) ایجاد ایندکس
کدهای این قسمت با مطلب برجسته سازی قسمت‌های جستجو شده، یکی است:
using System.Collections.Generic;
using System.IO;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Store;
using LuceneSearch.Core.Model;
using LuceneSearch.Core.Utils;

namespace LuceneSearch.Core
{
    public static class CreateIndex
    {
        static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_30;

        public static Document MapPostToDocument(Post post)
        {
            var postDocument = new Document();
            postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            var titleField = new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
            titleField.Boost = 3;
            postDocument.Add(titleField);
            postDocument.Add(new Field("Body", post.Body.RemoveHtmlTags(), Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
            return postDocument;
        }

        public static void CreateFullTextIndex(IEnumerable<Post> dataList, string path)
        {
            var directory = FSDirectory.Open(new DirectoryInfo(path));
            var analyzer = new StandardAnalyzer(_version);
            using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
            {
                foreach (var post in dataList)
                {
                    writer.AddDocument(MapPostToDocument(post));
                }

                writer.Optimize();
                writer.Commit();
                writer.Close();
                directory.Close();
            }
        }
    }
}
تنها تفاوت آن اضافه شدن titleField.Boost = 3 می‌باشد. توسط Boost به لوسین خواهیم گفت که اهمیت عبارات ذکر شده در عناوین مطالب، بیشتر است از اهمیت متون آن‌ها.


ج) تهیه قسمت منبع داده Auto-Complete

namespace LuceneSearch.Core.Model
{
    public class SearchResult
    {
        public int Id { set; get; }
        public string Title { set; get; }
    }
}

using System.Collections.Generic;
using System.IO;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using LuceneSearch.Core.Model;
using LuceneSearch.Core.Utils;

namespace LuceneSearch.Core
{
    public static class AutoComplete
    {
        private static IndexSearcher _searcher;

        /// <summary>
        /// Get terms starting with the given prefix
        /// </summary>
        /// <param name="prefix"></param>
        /// <param name="maxItems"></param>
        /// <returns></returns>
        public static IList<SearchResult> GetTermsScored(string indexPath, string prefix, int maxItems = 10)
        {
            if (_searcher == null)
                _searcher = new IndexSearcher(FSDirectory.Open(new DirectoryInfo(indexPath)), true);

            var resultsList = new List<SearchResult>();
            if (string.IsNullOrWhiteSpace(prefix))
                return resultsList;

            prefix = prefix.ApplyCorrectYeKe();

            var results = _searcher.Search(new PrefixQuery(new Term("Title", prefix)), null, maxItems);
            if (results.TotalHits == 0)
            {
                results = _searcher.Search(new PrefixQuery(new Term("Body", prefix)), null, maxItems);
            }

            foreach (var doc in results.ScoreDocs)
            {
                resultsList.Add(new SearchResult
                {
                    Title = _searcher.Doc(doc.Doc).Get("Title"),
                    Id = int.Parse(_searcher.Doc(doc.Doc).Get("Id"))
                });
            }

            return resultsList;
        }
    }
}
توضیحات:
برای نمایش Auto-Complete نیاز به منبع داده داریم که نحوه ایجاد آن‌را در کدهای فوق ملاحظه می‌کنید. در اینجا توسط جستجوی سریع لوسین و امکانات PrefixQuery آن، به تعدادی مشخص (maxItems)، رکوردهای یافت شده را بازگشت خواهیم داد. خروجی حاصل لیستی است از SearchResultها شامل عنوان مطلب و Id آن. عنوان را به کاربر نمایش خواهیم داد؛ از Id برای هدایت او به مطلبی مشخص استفاده خواهیم کرد.


د) نمایش Auto-Complete در ASP.NET MVC

using System.Text;
using System.Web.Mvc;
using LuceneSearch.Core;
using System.Web;

namespace LuceneSearch.Controllers
{
    public class HomeController : Controller
    {
        static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx";

        public ActionResult Index(int? id)
        {
            if (id.HasValue)
            {
                //todo: do something
            }
            return View(); //Show the page
        }

        public virtual ActionResult ScoredTerms(string q)
        {
            if (string.IsNullOrWhiteSpace(q))
                return Content(string.Empty);

            var result = new StringBuilder();
            var items = AutoComplete.GetTermsScored(_indexPath, q);
            foreach (var item in items)
            {
                var postUrl = this.Url.Action(actionName: "Index", controllerName: "Home", routeValues: new { id = item.Id }, protocol: "http");
                result.AppendLine(item.Title + "|" + postUrl);
            }

            return Content(result.ToString());
        }
    }
}

@{
    ViewBag.Title = "جستجو";
    var scoredTermsUrl = Url.Action(actionName: "ScoredTerms", controllerName: "Home");
    var bulletImage = Url.Content("~/Content/Images/bullet_shape.png");
}
<h2>
    جستجو</h2>

<div align="center">
    @Html.TextBox("term", "", htmlAttributes: new { dir = "ltr" })
    <br />
    جهت آزمایش lu را وارد نمائید
</div>

@section scripts
{
    <script type="text/javascript">
        EnableSearchAutocomplete('@scoredTermsUrl', '@bulletImage');
    </script>
}

function EnableSearchAutocomplete(url, img) {
    var formatItem = function (row) {
        if (!row) return "";
        return "<img src='" + img + "' /> " + row[0];
    }

    $(document).ready(function () {
        $("#term").autocomplete(url, {
            dir: 'rtl', minChars: 2, delay: 5,
            mustMatch: false, max: 20, autoFill: false,
            matchContains: false, scroll: false, width: 300,
            formatItem: formatItem
        }).result(function (evt, row, formatted) {
            if (!row) return;
            window.location = row[1];
        });
    });
}
توضیحات:
- ابتدا ارجاعاتی را به jQuery، افزونه Auto-Complete و اسکریپت سفارشی تهیه شده، در فایل layout پروژه تعریف خواهیم کرد.
در اینجا سه قسمت را مشاهده می‌کنید: کدهای کنترلر، View متناظر و اسکریپتی که Auto-Complete را فعال خواهد ساخت.
- قسمت مهم کدهای کنترلر، دو سطر زیر هستند:
result.AppendLine(item.Title + "|" + postUrl);
return Content(result.ToString());
مطابق نیاز افزونه انتخاب شده در مثال جاری، فرمت خروجی مدنظر باید شامل سطرهایی حاوی متن قابل نمایش به همراه یک Id (یا در اینجا یک آدرس مشخص) باشد. البته ذکر این Id اختیاری بوده و در اینجا جهت تکمیل بحث ارائه شده است.
return Content هم سبب بازگشت این اطلاعات به افزونه خواهد شد.
- کدهای View متناظر بسیار ساده هستند. تنها نام TextBox تعریف شده مهم می‌باشد که در متد جاوا اسکریپتی EnableSearchAutocomplete استفاده شده است. به علاوه، نحوه مقدار دهی آدرس دسترسی به اکشن متد ScoredTerms نیز مهم می‌باشد.
- در متد EnableSearchAutocomplete نحوه فراخوانی افزونه autocomplete را ملاحظه می‌کنید.
جهت آن، به راست به چپ تنظیم شده است. با 2 کاراکتر ورودی فعال خواهد شد با وقفه‌ای کوتاه. نیازی نیست تا انتخاب کاربر از لیست ظاهر شده حتما با عبارت جستجو شده صد در صد یکی باشد. حداکثر 20 آیتم در لیست ظاهر خواهند شد. اسکرول بار لیست را حذف کرده‌ایم. عرض آن به 300 تنظیم شده است و نحوه فرمت دهی نمایشی آن‌را نیز ملاحظه می‌کنید. برای این منظور از متد formatItem استفاده شده است. آرایه row در اینجا در برگیرنده اعضای Title و Id ارسالی به افزونه است. اندیس صفر آن به عنوان دریافتی اشاره می‌کند.
همچنین نحوه نشان دادن عکس العمل به عنصر انتخابی را هم ملاحظه می‌کنید (در متد result مقدار دهی شده).  window.location را به عنصر دوم آرایه row هدایت خواهیم کرد. این عنصر دوم مطابق کدهای اکشن متد تهیه شده، به آدرس یک صفحه اشاره می‌کند.


ه) نمایش Auto-Complete در ASP.NET WebForms

قسمت عمده مطالب فوق با وب فرم‌ها نیز یکی است. خصوصا توضیحات مرتبط با متد EnableSearchAutocomplete ذکر شده.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LuceneSearch.WebForms.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>جستجو</title>
    <link href="Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.autocomplete.js" type="text/javascript"></script>
    <script src="Scripts/custom.js" type="text/javascript"></script>
</head>
<body dir="rtl">
    <h2>
        جستجو</h2>
    <form id="form1" runat="server">
    <div align="center">
        <asp:TextBox runat="server" dir="ltr" ID="term"></asp:TextBox>
        <br />
        جهت آزمایش lu را وارد نمائید
    </div>
    </form>
    <script type="text/javascript">
        EnableSearchAutocomplete('Search.ashx', 'Content/Images/bullet_shape.png');
    </script>
</body>
</html>

using System.Text;
using System.Web;
using LuceneSearch.Core;

namespace LuceneSearch.WebForms
{
    public class Search : IHttpHandler
    {
        static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx";

        public void ProcessRequest(HttpContext context)
        {
            string q = context.Request.QueryString["q"];
            if (string.IsNullOrWhiteSpace(q))
            {
                context.Response.Write(string.Empty);
                context.Response.End();
            }

            var result = new StringBuilder();
            var items = AutoComplete.GetTermsScored(_indexPath, q);
            foreach (var item in items)
            {
                var postUrl = "Default.aspx?id=" + item.Id;
                result.AppendLine(item.Title + "|" + postUrl);
            }

            context.Response.ContentType = "text/plain";
            context.Response.Write(result.ToString());
            context.Response.End();
        }

        public bool IsReusable
        { get { return false; } }
    }
}

در اینجا بجای Controller از یک Generic handler استفاده شده است (Search.ashx).
result.AppendLine(item.Title + "|" + postUrl);
context.Response.Write(result.ToString());
در آن، عنوان مطالب یافت شده به همراه یک آدرس مشخص، تهیه و در Response نوشته خواهند شد.


کدهای کامل مثال فوق را از اینجا می‌توانید دریافت کنید:
همچنین باید دقت داشت که پروژه MVC آن از نوع MVC4 است (VS2010) و فرض براین می‌باشد که IIS Express 7.5 را نیز پیشتر نصب کرده‌اید.
کلمه عبور فایل: dotnettips91
 
نظرات مطالب
الگوی PRG در ASP.NET MVC
البته روش توضیح داده شده همان روش متداول PRG است. اگر حتما نیاز به 303 دارید به روش زیر باید عمل کرد:
using System.Web.Mvc;

namespace TestMvcPRG.Helper
{
    public class Redirect303 : ActionResult
    {
        private string _url;

        public Redirect303(string url)
        {
            _url = url;
        }

        public override void ExecuteResult(ControllerContext context)
        {
            context.HttpContext.Response.StatusCode = 303; // redirect using GET
            context.HttpContext.Response.RedirectLocation = _url;
        }
    }

    public abstract class BaseController : Controller
    {
        public Redirect303 Redirect303(string actionName)
        {
            return new Redirect303(Url.Action(actionName));
        }

        public Redirect303 Redirect303(string actionName, object routeValues)
        {
            return new Redirect303(Url.Action(actionName, routeValues));
        }

        public Redirect303 Redirect303(string actionName, string controllerName)
        {
            return new Redirect303(Url.Action(actionName, controllerName));
        }

        public Redirect303 Redirect303(string actionName, string controllerName, object routeValues)
        {
            return new Redirect303(Url.Action(actionName, controllerName, routeValues));
        }
    }
}
و بعد برای استفاده:
using System.Web.Mvc;
using TestMvcRPG.Helper;

namespace TestMvcPRG.Controllers
{
    public class HomeController : BaseController
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(string data)
        {
            if (ModelState.IsValid)
            {
                return Redirect303("Index"); // post-redirect-get
            }
            return View();
        }
    }
}
مطالب
اضافه کردن Watermark به تصاویر یک برنامه ASP.NET MVC در صورت لینک شدن در سایتی دیگر
درگیر شدن با سایت‌های دیگر که چرا مطالب ما را کپی کرده‌اید نهایتا بجز فرسایش عصبی حاصل دیگری را به همراه ندارد. اساسا زمانیکه مطلبی را به صورت باز در اینترنت انتشار می‌دهید، قید کپی شدن یا نشدن آن‌را باید زد. اما ... می‌توان همین سایت‌ها را تبدیل به تبلیغ کننده‌های رایگان کار خود نمود که در ادامه نحوه انجام آن‌را در یک برنامه ASP.NET MVC بررسی خواهیم کرد:

الف) نیاز است ارائه تصاویر تحت کنترل برنامه باشند.

using System.IO;
using System.Net.Mime;
using System.Web.Mvc;

namespace MvcWatermark.Controllers
{
    public class HomeController : Controller
    {
        const int ADay = 86400;

        public ActionResult Index()
        {
            return View();
        }

        [OutputCache(VaryByParam = "fileName", Duration = ADay)]
        public ActionResult Image(string fileName)
        {
            fileName = Path.GetFileName(fileName); // تمیز سازی امنیتی است
            var rootPath = Server.MapPath("~/App_Data/Images");
            var path = Path.Combine(rootPath, fileName);
            if (!System.IO.File.Exists(path))
            {
                var notFoundImage = "notFound.png";
                path = Path.Combine(rootPath, notFoundImage);
                return File(path, MediaTypeNames.Image.Gif, notFoundImage);
            }
            return File(path, MediaTypeNames.Image.Gif, fileName);
        }
    }
}
در اینجا یک کنترلر را مشاهده می‌کنید که در اکشن متد Image آن، نام یک فایل دریافت شده و سپس این نام در پوشه App_Data/Images جستجو گردیده و نهایتا در مرورگر کاربر Flush می‌شود. از آنجائیکه الزامی ندارد fileName، واقعا یک fileName صحیح باشد، نیاز است توسط متد استاندارد Path.GetFileName این نام دریافتی اندکی تمیز شده و سپس مورد استفاده قرار گیرد. همچنین جهت کاهش بار سرور، از یک OutputCache به مدت یک روز نیز استفاده گردیده است.
نحوه استفاده از این اکشن متد نیز به نحو زیر است:
<img src="@Url.Action(actionName: "Image", controllerName: "Home", routeValues: new { fileName = "EF_Stra_08.gif" })" />


ب) آیا فراخوان تصویر ما را مستقیما در سایت خودش قرار داده است؟

        private bool isEmbeddedIntoAnotherDomain
        {
            get
            {
                return this.HttpContext.Request.UrlReferrer != null &&
                       !this.HttpContext.Request.Url.Host.Equals(this.HttpContext.Request.UrlReferrer.Host,
                                                                   StringComparison.InvariantCultureIgnoreCase);
            }
        }
در ادامه توسط خاصیت سفارشی isEmbeddedIntoAnotherDomain درخواهیم یافت که درخواست رسیده، از دومین جاری صادر شده است یا خیر. اینکار توسط بررسی UrlReferrer ارسال شده توسط مرورگر صورت می‌گیرد. اگر Host این UrlReferrer با Host درخواست جاری یکی بود، یعنی تصویر از سایت خودمان فراخوانی شده‌است.


ج) افزودن خودکار Watermark در صورت کپی شدن در سایتی دیگر

        private byte[] addWaterMark(string filePath, string text)
        {
            var image = new WebImage(filePath);
            image.AddTextWatermark(text);
            return image.GetBytes();
        }
کلاسی در فضای نام System.Web.Helpers وجود دارد به نام WebImage که کار افزودن Watermark را بسیار ساده کرده است. نمونه‌ای از نحوه استفاده از آن‌را در متد فوق ملاحظه می‌کنید.
اما ... پس از امتحان تصاویر مختلف ممکن است گاها با خطای زیر مواجه شویم:
 A Graphics object cannot be created from an image that has an indexed pixel format.
مشکل از اینجا است که تصاویر با فرمت ذیل برای انجام کار Watermark پشتیبانی نمی‌شوند:
PixelFormatUndefined
PixelFormatDontCare
PixelFormat1bppIndexed
PixelFormat4bppIndexed
PixelFormat8bppIndexed
PixelFormat16bppGrayScale
PixelFormat16bppARGB1555
اما می‌توان تصویر دریافتی را ابتدا تبدیل به BMP کرد و سپس Watermark دار نمود:
        private byte[] addWaterMark(string filePath, string text)
        {
            using (var img = System.Drawing.Image.FromFile(filePath))
            {
                using (var memStream = new MemoryStream())
                {
                    using (var bitmap = new Bitmap(img))//avoid gdi+ errors
                    {
                        bitmap.Save(memStream, ImageFormat.Png);                        
                        var webImage = new WebImage(memStream);
                        webImage.AddTextWatermark(text, verticalAlign: "Top", horizontalAlign: "Left", fontColor: "Brown");
                        return webImage.GetBytes();
                    }
                }
            }
        }
در اینجا نمونه اصلاح شده متد addWaterMark فوق را بر اساس کار با تصاویر bmp و سپس تبدیل آن‌ها به png، ملاحظه می‌کنید. به این ترتیب دیگر به خطای یاد شده بر نخواهیم خورد.
در ادامه، قسمت آخر کار، اعمال این مراحل به اکشن متد Image است:
            if (isEmbeddedIntoAnotherDomain)
            {
                var text = Url.Action(actionName: "Index", controllerName: "Home", routeValues: null, protocol: "http");
                var content = addWaterMark(path, text);
                return File(content, MediaTypeNames.Image.Gif, fileName);
            }
            return File(path, MediaTypeNames.Image.Gif, fileName);
در اینجا اگر تشخیص داده شود که تصویر، در دومین دیگری لینک شده است، آدرس سایت ما به صورت خودکار در بالای تصویر درج خواهد شد.

کدهای نهایی این کنترلر را از اینجا می‌توانید دریافت کنید:
HomeController.cs
به همراه نمونه تصویری که استثنای یاد شده را تولید می‌کند؛ جهت آزمایش بیشتر:
EFStra08.gif
مطالب
RadioButtonList در ASP.NET MVC

برای تهیه یک 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")


مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
Ajax.BeginForm در ASP.NET MVC از jQuery Ajax برای ارسال مقادیر فرم، به سرور استفاده می‌کند. در این بین اگر یکی از عناصر فرم، المان ارسال فایل به سرور باشد، مقدار دریافتی در سمت سرور نال خواهد بود. مشکل اینجا است که نمی‌توان به کمک Ajax معمولی (یا به عبارتی XMLHttpRequest) فایلی را به سرور ارسال کرد. یا باید از سیلورلایت یا فلش استفاده نمود و یا از مرورگرهایی که XMLHttpRequest Level 2 را پشتیبانی می‌کنند (از IE 10 به بعد مثلا) که امکان Ajax upload توکار به همراه گزارش درصد آپلود را بدون نیاز به فلش یا سیلورلایت، دارند.
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل می‌شود، مقادیر معمولی فرم (تمام المان‌ها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback می‌شود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر می‌رسد. برای انجام اینکار تنها کافی است از افزونه‌ی AjaxFileUpload استفاده کنیم که در ادامه نحوه‌ی استفاده از آن‌را بررسی خواهیم کرد.


پیشنیازها

در ادامه فرض بر این است که افزونه‌ی AjaxFileUpload را دریافت کرده و به فایل Layout برنامه افزوده‌اید:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.11.1.min.js"></script>
    <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
    <script src="~/Scripts/ajaxfileupload.js"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>


مدل، کنترلر و View برنامه

مدل برنامه مشخصات یک محصول است:
namespace MVCAjaxFormUpload.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

کنترلر آن از سه متد تشکیل شده‌است:
using System.Threading;
using System.Web;
using System.Web.Mvc;
using MVCAjaxFormUpload.Models;

namespace MVCAjaxFormUpload.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Product product)
        {
            var isAjax = this.Request.IsAjaxRequest();

            return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public ActionResult UploadFiles(HttpPostedFileBase image1, int id)
        {
            var isAjax = this.Request.IsAjaxRequest();
            Thread.Sleep(3000); //شبیه سازی عملیات طولانی
            return Json(new { FileName = "/Uploads/filename.ext" }, "text/html", JsonRequestBehavior.AllowGet);
        }
    }
}
Index اول، کار نمایش صفحه‌ی ارسال اطلاعات را انجام خواهد داد.
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام می‌دهد. HttpPost آن معمولی است.

و کدهای View این مثال نیز به شرح زیر است:
@model MVCAjaxFormUpload.Models.Product
@{
    ViewBag.Title = "Index";
}

<h2>Ajax Form Upload</h2>

@using (Ajax.BeginForm(actionName: "Index",
                       controllerName: "Home",
                       ajaxOptions: new AjaxOptions { HttpMethod = "POST" },
                       routeValues: null,
                       htmlAttributes: new { id = "uploadForm" }))
{
    <label>Name:</label>
    @Html.TextBoxFor(model => model.Name)
    <br />
    <label>Image:</label>
    <br />
    <input type="file" name="Image1" id="Image1" />
    <br />
    <input type="submit" value="Submit" />
    <img id="loading" src="~/Content/Images/loading.gif" style="display:none;">
}

@section Scripts
{
    <script type="text/javascript">
        $(function () {
            $('#uploadForm').submit(function () {
                $("#loading").show();
                $.ajaxFileUpload({
                    url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود
                    secureuri: false,
                    fileElementId: 'Image1', // آی دی المان ورودی فایل
                    dataType: 'json',
                    data: { id: 1, data: 'test' }, // اطلاعات اضافی در صورت نیاز
                    success: function (data, status) {
                        $("#loading").hide();
                        if (typeof (data.FileName) != 'undefined') {
                            alert(data.FileName);
                        }
                    },
                    error: function (data, status, e) {
                        $("#loading").hide();
                        alert(e);
                    }
                });
            });
        });
    </script>
}
فرمی که توسط Ajax.BeginForm تشکیل شده‌است، یک فرم معمولی Ajax ایی است و نکته‌ی جدیدی ندارد. تنها در آن یک المان ارسال فایل قرار گرفته‌است و همچنین Id آن‌را نیز جهت استفاده توسط jQuery مشخص کرده‌ایم.
در ادامه نحوه‌ی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده می‌کنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره می‌کند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحه‌است. از قسمت data جهت ارسال اطلاعات اضافه‌تری به اکشن متد UploadFiles استفاده می‌شود. سایر قسمت‌های آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا می‌شوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمه‌ی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمه‌ی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام می‌شوند.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
MVCAjaxFormUpload.zip
مطالب دوره‌ها
به روز رسانی غیرهمزمان قسمتی از صفحه به کمک jQuery در ASP.NET MVC
یک صفحه شلوغ و سنگین را در نظر بگیرید. برای مثال قرار است ابتدا مطلب خاصی در سایت نمایش یابد و سپس ادامه صفحه که شامل انبوهی از لیست نظرات مرتبط با آن مطلب است به صورت غیرهمزمان و Ajax ایی بدون توقف پردازش صفحه، در فرصتی مناسب از سرور دریافت و به صفحه اضافه گردد (به روز رسانی قسمتی از صفحه در فرصت مناسب). در این حالت چون نمایش اولیه صفحه سریع صورت می‌گیرد، کاربر نهایی آنچنان احساس کند بودن بازکردن صفحات سایت را نخواهد داشت. در ادامه نحوه پیاده سازی این روش را به کمک jQuery Ajax بررسی خواهیم کرد.

مدل و کنترلر برنامه

namespace jQueryMvcSample07.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}

using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample07.Models;
using jQueryMvcSample07.Security;

namespace jQueryMvcSample07.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش یک منوی ساده در ابتدای کار
        }

        [HttpGet]
        public ActionResult ShowSynchronous()
        {
            var model = getModel();
            return View(model); //نمایش همزمان
        }

        private static BlogPost getModel()
        {
            //شبیه سازی یک عملیات طولانی
            System.Threading.Thread.Sleep(3000);
            var model = new BlogPost
            {
                Title = "عنوان ... ",
                Body = "مطلب... "
            };
            return model;
        }

        [HttpGet]
        public ActionResult ShowAsynchronous()
        {
            return View(); //نمایش ابتدایی صفحه
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult RenderAsynchronous()
        {
            //دریافت اطلاعات به صورت غیرهمزمان
            var model = getModel();
            return PartialView(viewName: "_Post", model: model);
        }
    }
}
مدل برنامه، بیانگر ساختار اطلاعات مطلبی است که قرار است نمایش یابد.
در کنترلر Home، ابتدا اکشن متد Index آن فراخوانی شده و در این حالت دو لینک زیر نمایش داده می‌شوند:
@{
    ViewBag.Title = "Index";
}
<h2>
    نمایش اطلاعات به صورت همزمان و غیرهمزمان</h2>
<ul>
    <li>
        @Html.ActionLink(linkText: "نمایش همزمان", actionName: "ShowSynchronous", controllerName: "Home")
    </li>
    <li>
        @Html.ActionLink(linkText: "نمایش غیر همزمان", actionName: "ShowAsynchronous", controllerName: "Home")
    </li>
</ul>
لینک اول، به اکشن متد ShowSynchronous اشاره می‌کند و لینک دوم به اکشن متد ShowAsynchronous.
در هر دو صفحه نهایتا از یک Partial View به نام _Post.cshtml برای نمایش اطلاعات استفاده خواهد شد:
@model jQueryMvcSample07.Models.BlogPost
<fieldset>
    <legend>@Model.Title</legend>
    @Model.Body
</fieldset>
زمانیکه کاربر بر روی لینک نمایش همزمان کلیک می‌کند، به صفحه زیر هدایت می‌شود:
@model jQueryMvcSample07.Models.BlogPost
@{
    ViewBag.Title = "ShowSynchronous";
}

<h2>نمایش همزمان</h2>
@{ Html.RenderPartial("_Post", Model); }
این صفحه، یک صفحه متداول است و اطلاعات آن دقیقا در زمان نمایش صفحه اخذ شده و چون در اینجا از یک Sleep عمدی برای تولید اطلاعات استفاده گردیده است، نمایش آن حداقل سه ثانیه طول خواهد کشید.
در حالتیکه کاربر بر روی لینک نمایش غیرهمزمان کلیک می‌کند، صفحه زیر را مشاهده خواهد کرد:
@{
    ViewBag.Title = "ShowAsynchronous";
    var loadInfoUrl = Url.Action(actionName: "RenderAsynchronous", controllerName: "Home");
}
<h2>
    نمایش غیر همزمان</h2>
<div id="info" align="center">
</div>
<div id="progress" align="center" style="display: none">
    <br />
    <img src="@Url.Content("~/Content/images/loadingAnimation.gif")" alt="loading..."  />
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $("#progress").css("display", "block");
            $.ajax({
                type: "POST",
                url: '@loadInfoUrl',
                complete: function (xhr, status) {
                    var data = xhr.responseText;
                    if (xhr.status == 403) {
                        window.location = "/login";
                    }
                    else if (status === 'error' || !data || data == "nok") {
                        alert('خطایی رخ داده است');
                    }
                    else {
                        $("#progress").css("display", "none");
                        $("#info").html(data);
                    }
                }
            });
        });
    </script>
}
نمایش ابتدایی این صفحه بسیار سریع است. در ابتدای کار progress bar ایی فعال شده و سپس از طریق jQuery Ajax درخواست دریافت اطلاعات رندر شده اکشن متدی به نام RenderAsynchronous به سرور ارسال می‌شود. چون عملیات Ajax غیرهمزمان است، کاربر نیازی نیست تا رندر شدن کامل صفحه ابتدا صبر کند و سپس کل صفحه به او نمایش داده شود. در اینجا ابتدا صفحه به صورت کامل نمایان شده و سپس درخواستی Ajax ایی به سرور ارسال می‌گردد. در پایان عملیات، Partial View یاد شده رندر گردیده و در div ایی با id مساوی info نمایش داده می‌شود.
به این ترتیب می‌توان حس سریع بودن سایت را زمانیکه قسمتی از صفحه نیاز به زمان بیشتری برای نمایش اطلاعات دارد، به کاربر منتقل کرد.

دریافت پروژه کامل این قسمت
jQueryMvcSample07.zip 
مطالب
فیلترها در MVC
هنگامی که درخواستی به سرور ارسال می‌گردد، به کنترلر و اکشن مربوطه جهت پاسخگویی هدایت می‌شود. خب شاید مواقعی شما نیاز داشته باشید قبل یا بعد از اجرای اکشن متدی، کدی اجرا گردد. به‌همین جهت در MVC قابلیتی بنام Filter ارائه گردید.
فیلتر، یک کلاس سفارشی است که شما می‌توانید منطق برنامه را جهت اجرا، قبل یا بعد از اجرای یک اکشن متد، در آن پیاده سازی نمایید. فیلترها می‌توانند به یک اکشن متد و یا کنترلری منتسب شوند که در ادامه با این روشها آشنا خواهید شد.

در لیست زیر انواع فیلترها و اینترفیس‌هایی که باید توسط کلاس سفارشی شما پیاده سازی شوند، معرفی شده است.

 نوع توضیح
 فیلتر توکار
 اینترفیس
 Authorization
انجام عملیات احراز هویت و سطح دسترسی، قبل از اجرای کد اکشن متد  
 [Authorize] و [RequireHttps]  
 IAuthorizationFilter 
 Action
اجرای کدهایی قبل از اجرای کدهای اکشن متد 
   IActionFilter 
 Result
اجرای کدهایی قبل یا بعد از تولید ویو (View result) 
 [OutputCache]   IResultFilter 
Exception
اجرای کدهایی در صورت وجود استثنای مدیریت نشده 
[HandleError] 
IExceptionFilter
مثال: هنگامی که خطایی در حین اجرای اکشن متدی رخ می‌دهد، فیلتر توکار MVC بنام HandleError اجرا می‌شود. این فیلتر توکار فایل Error.cshtml را که در فولدر Shared قرار دارد، رندر می‌کند و نمایش می‌دهد.
در تکه کد زیر نحوه‌ی استفاده از این فیلتر را مشاهده می‌کنید:
[HandleError]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        //throw exception for demo
        throw new Exception("This is unhandled exception");
            
        return View();
    }

    public ActionResult About()
    {
        return View();
    }

    public ActionResult Contact()
    {
        return View();
    }        
}

نکته: فیلترهای اعمال شده‌ی به یک کنترلر، به تمام اکشن متدهای آن نیز اعمال می‌گردند. 

در کد بالا خصیصه‌ی HandleError به HomeController اعمال شده است. بنابراین در صورت بروز خطایی در هر کدام از اکشن‌ها، صفحه‌ی Error.cshtml نمایش داده خواهد شد و در تظر داشته باشید که خطاها توسط try-catch هندل نشده‌اند.
باید جهت عملکرد صحیح فیلتر توکار HandleErrorAttribute، مقدار customErrors در قسمت System.web فایل web.config مساوی on باشد.
<customErrors mode="On" />


مهیا کنندگان فیلترها

بصورت پیش فرض MVC از سه طریق زیر فیلترها را جهت استفاده‌ی در برنامه فراهم می‌کند:

  1. خصیصه‌ی GlobalFilters.Filters برای فیلترهای سراسری
  2. کلاس FilterAttributeFilterProvider برای فیلترهای خصیصه‌ای
  3. کلاس ControllerInstanceFilterProvider جهت افزودن کنترلر به یک وهله از FilterProviderCollection

در ادامه با نحوه‌ی ایجاد یک فیلتر، بوسیله‌ی هر یک از روش‌های بالا، با ذکر مثالی بیشتر آشنا خواهید شد.

ترتیب اجرای فیلترها

همانطور که در ابتدا اشاره شد، در MVC چهار نوع فیلتر معرفی شده است که امکان استفاده‌ی از آنها به‌صورت همزمان در سطح کنترلر و یا اکشن متد وجود دارد. اما ترتیب  اجرای آنها متفاوت و به ترتیب زیر است:

  1. فیلترهای Authorization
  2. فیلترهای Action
  3. فیلترهای Result یا Response
  4. فیلترهای Exception

فیلترها براساس ترتیب اشاره شده‌ی در بالا اجرا خواهند شد. در صورتیکه چند فیلتر از یک نوع استفاده شود، جهت تقدم و تاخر در اجرا، از خاصیت Order استفاده خواهد شد. بعنوان مثال در کد زیر بدلیل خاصیت Order=1 ابتدا AuthorizationFilterB  و سپس AuthorizationFilterA اجرا می‌شود.

[AuthorizationFilterA(Order=2)]
[AuthorizationFilterB(Order=1)]
public ActionResult Index()
{          
    return View();
}
علاوه بر خاصیت Order، مقدار Scope نیز سطح سومی از ترتیب اجرای فیلترها می‌باشد. مقادیر Scope بشرح زیر است:
public enum FilterScope
{
    First = 0,
    Global = 10,
    Controller = 20,
    Action = 30,
    Last = 100,
}
این خصیصه‌ی فیلترها در محل بکار گیری آنها مقدار دهی می‌شود. در صورتیکه فیلتری بصورت سراسری رجیستر شود، Scope آن برابر 10 و در سطح کنترلر، برابر 20 خواهد بود و الی آخر.

نکته: مقدار Scope فیلترهای Authorization برابر 0 و فیلترهای Exception برابر 100 می‌باشد.

ایجاد فیلتر سفارشی

روش اول: پیاده سازی اینترفیس یکی از انواع فیلترها و ارث بری از کلاس FilterAttribute

در این روش متدهایی که باید پیاده سازی شوند متفاوت خواهد بود. به همین جهت متدهای هر نوع بشرح زیر معرفی می‌شود:

  • IAuthorizationFilter
// Called when authorization is required
void OnAuthorization(AuthorizationContext filterContext)
  • IActionFilter
// Called after the action method executes
void OnActionExecuted(ActionExecutedContext filterContext)

// Called before an action method executes
void OnActionExecuting(ActionExecutingContext filterContext)
  • IResultFilter
// Called after an action result executes
void OnResultExecuted(ResultExecutedContext filterContext)

// Called before an action result executes
void OnResultExecuting(ResultExecutedContext filterContext)
  • IException
// Called when an exception occurs
void OnException(ExceptionContext filterContext)

یادآوری: همانطور که در ابتدای مقاله اشاره شد، فیلترها قبل یا بعد از اجرای اکشن متدها فراخوانی خواهند شد. بنابراین به کامنت بالای متد فیلترها دقت داشته باشید.

مثال: پیاده سازی اینترفیس IExceptionFilter و ارث بری از کلاس FilterAttribute جهت تهیه‌ی فیلتری سفارشی از نوع Exception

class CustomErrorHandler : FilterAttribute, IExceptionFilter
{
    public override void IExceptionFilter.OnException(ExceptionContext filterContext)
    {
        Log(filterContext.Exception);

        base.OnException(filterContext);
    }

    private void Log(Exception exception)
    {
        //log exception here..
    }
}

روش دوم:
ارث بری از ActionFilterAttribute
کلاس abstract فوق دارای چهار متد زیر جهت تحریف است. همانطور که مشاهده می‌کنید این کلاس علاوه بر دو متد OnActionExecuted و OnActionExecuting دارای دو متد دیگر OnResultExecuting و OnResultExecuted که به‌ترتیب قبل و بعد خروجی (Result) اکشن متد اجرا می‌شوند، نیز می‌باشد. این نوع فیلترها عموما جنبه‌ی استفاده عمومی داشته و می‌توان از آنها جهت logging ،caching و یا authorization استفاده کرد.
// Called by MVC after the action method executes
void OnActionExecuted(ActionExecutedContext filterContext)

// Called by MVC before the action method executes
void OnActionExecuted(ActionExecutedContext filterContext)

// Called by MVC after the action result executes
void OnResultExecuted(ResultExecutedContext filterContext)

// Called by MVC before the action result executes
void OnResultExecuting(ResultExecutingContext filterContext)

مثال: کلاس LogAttribute که از کلاس ActionFilterAttribute ارث بری کرده است، عملیات قبل و بعد از اجرای اکشن متد را لاگ می‌کند.
public class LogAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        Log("OnActionExecuted", filterContext.RouteData); 
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Log("OnActionExecuting", filterContext.RouteData);      
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        Log("OnResultExecuted", filterContext.RouteData);      
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        Log("OnResultExecuting ", filterContext.RouteData);      
    }

    private void Log(string methodName, RouteData routeData)
    {
        var controllerName = routeData.Values["controller"];
        var actionName = routeData.Values["action"];
        var message = String.Format("{0}- controller:{1} action:{2}", methodName, 
                                                                    controllerName, 
                                                                    actionName);
        Debug.WriteLine(message);
    }
}

روش سوم:
پیاده سازی داخل کنترلر
کلاس Controller  می‌تواند هر یک از اینترفیس‌های فیلترها را پیاده سازی نماید. به عبارت دیگر در هر کلاس کنترلر می‌توانید متدهای زیر را تحریف نمایید.
  • OnAuthorization ^
  • OnException ^
  • OnActionExecuting ^
  • OnActionExecuted ^
  • OnResultExecuting ^
  • OnResultExecuted ^


روش چهارم: ارث بری از کلاس فیلترهای توکار و مهیای در MVC و تحریف متدهای آن 
در کد زیر با تحریف و سفارشی سازی متد OnException مخصوص فیلتر توکار HandleError، قابلیت‌های آن افزایش یافته است:

class CustomErrorHandler : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        Log(filterContext.Exception);

        base.OnException(filterContext);
    }

    private void Log(Exception exception)
    {
        //log exception here..
    }
}


رجیستر فیلترها

  • سراسری:

درصورتی که قصد داشته باشید فیلتری بصورت سراسری و در کل برنامه فعال گردد باید آن را در رویداد Application_Start فایل Global.asax.cs بوسیله‌ی متد RegisterGlobalFilters کلاس FiterConfig رجیستر نمایید. بعد از آن فیلتر به کلیه‌ی کنترلرها و اکشن متدها اعمال می‌گردد.

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
          FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    }
}

// FilterConfig.cs located in App_Start folder 
public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());

        // add your new custom filters
        filters.Add(new LogAttribute()); 
        filters.Add(new CustomErrorHandler());
     }
}

در کد بالا فیلتر توکار HandleError و البته فیلترهای سفارشی دیگری نیز به صورت سراسری به تمام اکشن متدهای کنترلرها اعمال گردیده است.

  • کنترلر: در صورتی که فقط بخواهید یک فیلتر به کل اکشن‌های یک کنترلر اعمال گردد. همانند آنچه که در مثال ابتدایی بدان اشاره شد.
[HandleError]
public class HomeController : Controller
  • اکشن متد: اعمال یک فیلتر به یک اکشن متد خاص کنترلر. در کد زیر فیلتر HandleError فقط به اکشن متد Index کنترلر Home اعمال خواهد شد.
public class HomeController : Controller
{
    [HandleError]
    public ActionResult Index()
    {
        return View();
    }
}
مطالب
مخفی کردن کوئری استرینگ‌ها در ASP.NET MVC توسط امکانات Routing
ابتدا مدل و منبع داده نمونه زیر را در نظر بگیرید:
using System.Collections.Generic;

namespace TestRouting.Models
{
    public class Issue
    {
        public int IssueId { set; get; }
        public int ProjectId { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }

    public static class IssuesDataSource
    {
        public static IList<Issue> CreateDataSource()
        {
            var results = new List<Issue>();
            for (int i = 0; i < 100; i++)
            {
                results.Add(new Issue { IssueId = i, ProjectId = i, Body = "Test...", Title = "Title " + i });
            }
            return results;
        }
    }
}
به همراه کنترلر زیر برای نمایش لیست اطلاعات و همچنین نمایش جزئیات یک issue انتخابی:
using System.Linq;
using System.Web.Mvc;
using TestRouting.Models;

namespace TestRouting.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var issuesList = IssuesDataSource.CreateDataSource();
            return View(issuesList); //show the list
        }

        public ActionResult Details(int issueId, int projectId)
        {
            var issue = IssuesDataSource.CreateDataSource()
                                        .Where(x => x.IssueId == issueId && x.ProjectId == projectId)
                                        .FirstOrDefault();
            return View(issue);
        }
    }
}
و View زیر کار نمایش لیست بازخوردهای یک پروژه را انجام می‌دهد:
@model IEnumerable<TestRouting.Models.Issue>
@{
    ViewBag.Title = "Index";
}
<h2>
    Issues</h2>
<ul>
    @foreach (var item in Model)
    {
   
        <li>
            @Html.ActionLink(linkText: item.Title,
                     actionName: "Details",
                     controllerName: "Home",
                     routeValues: new { issueId = item.IssueId, projectId = item.ProjectId },
                     htmlAttributes: null)
        </li>
    }
</ul>
در این حالت اگر پروژه را اجرا کنیم، هر لینک نمایش داده شده، چنین فرمی را خواهد داشت:
 http://localhost:1036/Home/Details?issueId=0&projectId=0
سؤال: آیا می‌شود این لینک‌ها را کمی زیباتر و SEO Friendly‌تر کرد؟
برای مثال آن‌را به نحو زیر نمایش داد:
 http://localhost:1036/Home/Details/0/0
پاسخ: بلی. برای اینکار تنها کافی است از امکانات مسیریابی استفاده کنیم:
using System.Web.Mvc;
using System.Web.Routing;

namespace TestRouting
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "IssueDetails",
                url: "Details/{issueId}/{projectId}", //تطابق با یک چنین مسیرهایی
                defaults: new 
                          { 
                                controller = "Home", //کنترلری که این نوع مسیرها را پردازش خواهد کرد
                                action = "Details", // اکشن متدی که نهایتا پارامترها را دریافت می‌کند
                                issueId = UrlParameter.Optional, //این خواص نیاز است هم نام پارامترهای اکشن متد تعریف شوند
                                projectId = UrlParameter.Optional
                          }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
در اینجا یک route جدید به نام دلخواه IssueDetails پیش از route پیش فرض، تعریف شده است.
این route جدید با مسیرهایی مطابق پارامتر url آن تطابق خواهد یافت. پس از آن کوئری استرینگ متناظر با issueId را به پارامتر issueId اکشن متدی به نام Details و کنترلر Home ارسال خواهد کرد؛ به همین ترتیب در مورد projectId عمل خواهد شد.
ضمنا در url نهایی نمایش داده شده، دیگر اثری از کوئری استرینگ‌ها نبوده و برای نمونه در این حالت، اولین لینک نمایش داده شده شکل زیر را خواهد داشت:
 http://localhost:1036/Home/Details/0/0
 البته باید دقت داشت، یک چنین اصلاح خودکاری تنها در حالت استفاده از متد Html.ActionLink رخ می‌دهد و اگر Urlها را دستی ایجاد کنید، تغییری را مشاهده نخواهید کرد.
نظرات مطالب
سازگار کردن لینک‌های قدیمی یک سایت با ساختار جدید آن در ASP.NET MVC
ابتدا مطمئن شوید که در فایل web.config چنین تنظیمی را دارید (برای پردازش مسیرهای نقطه دار، ضروری است):
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true" >
سپس این مسیریابی را تعریف کنید (پیش از مسیریابی پیش فرض یا default خود ASP.NET MVC):
 routes.MapRoute(
                name: "joomla",
                url: "fa/index.php",
                defaults: new { controller = "Joomla", action = "Index" }
            );
این مسیریابی، تمام آدرس‌های شروع شده‌ی با fa/index.php را به کنترلری به نام Joomla و اکشن متد Index آن ارسال می‌کند. نیازی نیست تا کوئری استرینگ‌ها را در اینجا ذکر کرد. چون به صورت خودکار به پارامترهایی همنام، نگاشت خواهند شد:
using System.Web.Mvc;

namespace MVC5Basic.Controllers
{
    public class JoomlaController : Controller
    {
        public ActionResult Index(string option, string view, string id, string itemid)
        {
            //todo: process parameters and then return something!
            return View();
        }
    }
}
شکل نهایی کنترلر Joomla به صورت فوق است. در اینجا تمام کوئری استرینگ‌ها، تبدیل به یک پارامتر متناظر در اکشن متد Index شدند.
در این حالت اگر برای مثال همان آدرسی را که عنوان کردید، درخواست شود:


به صورت خودکار به این کنترلر جدید هدایت می‌شود؛ به همراه مقادیر تمام پارامترهای آن:

مطالب دوره‌ها
مراحل Refactoring یک قطعه کد برای اعمال تزریق وابستگی‌ها
برای رسیدن به الگوی معکوس سازی وابستگی‌ها عموما مراحل زیر طی می‌شوند:

الف) در متدهای لایه جاری خود واژه‌های کلیدی new و همچنین کلیه فراخوانی‌های استاتیک را بیابید.
ب) وهله سازی این‌ها را به یک سطح بالاتر (نقطه آغازین برنامه) منتقل کنید. اینکار باید بر اساس اتکای به Abstraction و برای مثال استفاده از اینترفیس‌ها صورت گیرد.
ج) اینکار را آنقدر تکرار کنید تا دیگر در کدهای لایه جاری خود واژه کلیدی new یا فراخوانی متدهای استاتیک مشاهده نشود.
د) در آخر وهله سازی object graphهای مورد نیاز را به یک IoC Container محول کنید.


یک مثال: ابتدا بررسی یک قطعه کد متداول

using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Mvc;

namespace DI06.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            string result = string.Empty;
            using (var client = new WebClient { Encoding = Encoding.UTF8 })
            {
                result = client.DownloadString("https://www.dntips.ir/");
            }
            var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(result);
            var title = match.Groups[1].Value.Trim();

            ViewBag.PageTitle = title;
            return View();
        }
    }
}
فرض کنید یک برنامه ASP.NET MVC را به نحو فوق تهیه کرده‌ایم. در کدهای کنترلر آن قصد داریم محتویات Html یک صفحه خاص را دریافت و سپس عنوان آن‌را استخراج کرده و نمایش دهیم.
مشکلات کد فوق:
الف) قرار گرفتن منطق تجاری پیاده سازی کدها مستقیما داخل کدهای یک اکشن متد؛ این مساله در دراز مدت به تکرار شدید کدها منجر خواهد شد که نهایتا قابلیت نگهداری آن‌را کاهش می‌دهند.
ب) در این کد حداقل دو بار واژه کلیدی new ذکر شده است. مورد اول یا new WebClient، از همه مهم‌تر است؛ از این جهت که نوشتن آزمون واحد را برای این کنترلر بسیار مشکل می‌کند. آزمون‌های واحد باید سریع و بدون نیاز به منابع خارجی، قابل اجرا باشند. تعویض آن هم مطابق کدهای تدارک دیده شده کار ساده‌ای نیست.


بهبود کیفیت قطعه کد متداول فوق با استفاده از الگوی معکوس سازی وابستگی‌ها

در اصل معکوس سازی وابستگی‌ها عنوان کردیم لایه بالایی سیستم نباید مستقیما به لایه‌های زیرین در حال استفاده از آن، وابسته باشد. این وابستگی باید معکوس شده و همچنین بر اساس Abstraction یا برای مثال استفاده از اینترفیس‌ها صورت گیرد.
به همین منظور یک پروژه دیگر را از نوع Class library، مثلا به نام DI06.Services به Solution جاری اضافه می‌کنیم.
namespace DI06.Services
{
    public interface IWebClientServices
    {
        string FetchUrl(string url);
        string GetWebPageTitle(string url);
    }
}

using System.Net;
using System.Text;
using System.Text.RegularExpressions;

namespace DI06.Services
{
    public class WebClientServices : IWebClientServices
    {
        public string FetchUrl(string url)
        {
            using (var client = new WebClient { Encoding = Encoding.UTF8 })
            {
                return client.DownloadString(url);
            }
        }

        public string GetWebPageTitle(string url)
        {
            var html = FetchUrl(url);
            var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(html);
            return match.Groups[1].Value.Trim();
        }
    }
}
در این لایه، سرویس WebClient ایی را تدارک دیده‌ایم. این سرویس می‌تواند محتوای Html یک آدرس را برگرداند و یا عنوان آن آدرس خاص را استخراج نماید.
هنوز کار معکوس سازی وابستگی‌ها رخ نداده است. صرفا اندکی تمیزکاری و انتقال پیاده سازی منطق تجاری به یک سری کلاس‌هایی با قابلیت استفاده مجدد صورت گرفته است. به این ترتیب اگر باگی در این کدها وجود داشته باشد و همچنین از آن در چندین نقطه برنامه استفاده شده باشد، اصلاح این کلاس مرکزی، به یکباره تمامی قسمت‌های مختلف برنامه را تحت تاثیر مثبت قرار داده و از تکرار کدها و فراموشی احتمالی بهبود قسمت‌های مشابه جلوگیری می‌کند.
کار معکوس سازی وابستگی‌ها در یک لایه بالاتر صورت خواهد گرفت:
using System.Web.Mvc;
using DI06.Services;

namespace DI06.Controllers
{
    public class HomeController : Controller
    {
        readonly IWebClientServices _webClientServices;
        public HomeController(IWebClientServices webClientServices)
        {
            _webClientServices = webClientServices;
        }

        public ActionResult Index()
        {
            ViewBag.PageTitle = _webClientServices.GetWebPageTitle("https://www.dntips.ir/");
            return View();
        }
    }
}
اینبار کنترلر Home را توسط تزریق وابستگی‌ها در سازنده کنترلر، بازنویسی کرده‌ایم. کد نهایی بسیار تمیزتر از حالت قبل است. دیگر پیاده سازی متد GetWebPageTitle مستقیما داخل یک اکشن متد قرار نگرفته است. همچنین این کنترلر اصلا نمی‌داند که قرار است از کدام پیاده سازی اینترفیس IWebClientServices استفاده کند. اگر در تنظیمات اولیه IoC Container مورد استفاده، کلاس WebClientServices ذکر شده باشد، از آن استفاده خواهد کرد؛ یا اگر حتی کلاس WebClientServicesFake نیز معرفی گردد (جهت انجام آزمون غیر وابسته به new WebClient)، باز هم بدون کوچکترین تغییری در کدهای آن قابل استفاده خواهد بود.

در مورد نحوه تنظیمات اولیه یک IoC Container و یا پیشنیازهای ASP.NET MVC جهت آماده شدن برای تزریق خودکار وابستگی‌ها در سازنده کنترلرها، پیشتر مطالبی را در این سری مطالعه کرده‌اید؛ در اینجا نیز اصول مورد استفاده یکی است و تفاوتی نمی‌کند.