Статьи

Прогрессивное улучшение в MVC 3 с помощью пользовательских ActionResults

На прошлой неделе я написал учебник по прогрессивному улучшению с помощью ASP.NET MVC 3 и jQuery . Это может быть хорошей идеей, если вы не знакомы с некоторыми понятиями.

Комментатор в последней статье задал отличный вопрос, который вызвал некоторые улучшения исходного кода. Спасибо за предложение!

Быстрый обзор

Вкратце, наша цель — создать форму «Свяжитесь с нами», которая отлично работает во всех браузерах, но предлагает расширенные возможности для современных браузеров с включенным JavaScript.

JavaScript включен

SNAGHTML1a3b6b08_thumb4

Как вы можете видеть, когда пользователь заходит на наш сайт с включенным JavaScript и щелкает ссылку «Связаться с нами», ему предоставляется приятное диалоговое окно jQuery UI. Они могут заполнить форму и получить хорошее подтверждающее сообщение в диалоге, и, наконец, закрыть ее, не покидая страницы, на которой они находились.

JavaScript отключен

SNAGHTML1a48993d_thumb1

Посетитель без JavaScript все равно получит ту же функциональность, только немного меньше опыта. Без логики JavaScript, прикрепленной к нашей ссылке «Свяжитесь с нами», она ведет себя как простая старая гиперссылка, переходящая браузер на новую страницу. Как только они заполнили форму и нажали «Отправить сообщение», мы перенаправили их обратно на домашнюю страницу с сообщением подтверждения.

 

Новый контроллер

По предложению комментатора я абстрагировал логику в два пользовательских ActionResults с именами AjaxableViewResult и AjaxableActionResult . Наш новый контроллер гораздо более лаконичен и удобочитаем, если мы придерживаемся некоторых соглашений для именования наших представлений.

[HttpGet]
public ActionResult ContactUs()
{
    return new AjaxableViewResult();
}
 
[HttpPost]
public ActionResult ContactUs(ContactUsInput input)
{
    if (!ModelState.IsValid)
        return new AjaxableViewResult(input);
 
    // TODO: A real app would send some sort of email here
 
    TempData["Message"] = string.Format("Thanks for the feedback, {0}! We will contact you shortly.", input.Name);
    return new AjaxableActionResult
                {
                    DefaultResult = () => RedirectToAction("Index"),
                    AjaxResult = () => PartialView("_ThanksForFeedback", input)
                };
}

 

Код

Ниже вы найдете код, который абстрагирует нашу логику прогрессивного улучшения. Код документирован и должен быть относительно простым, если вы знакомы с концепцией ActionResults MVC.

AjaxableActionResult

/// <summary>
/// Executes a specific action result depending on whether the incoming request is an Ajax request or not
/// </summary>
public class AjaxableActionResult : ActionResult
{
    /// <summary>
    /// The result to execute for non-Ajax requests
    /// </summary>
    public Func<ActionResult> DefaultResult { get; set; }
 
    /// <summary>
    /// The result the execute for Ajax requests
    /// </summary>
    public Func<ActionResult> AjaxResult { get; set; }
 
    public override void ExecuteResult(ControllerContext context)
    {
        if(DefaultResult == null)
            throw new ArgumentException("The DefaultResult property must be set");
 
        if (AjaxResult == null)
            throw new ArgumentException("The AjaxResult property must be set");
 
 
        if (context.HttpContext.Request.IsAjaxRequest())
        {
            AjaxResult().ExecuteResult(context);
        }
        else
        {
            DefaultResult().ExecuteResult(context);
        }
    }
}

 

AjaxableViewResult

/// <summary>
/// Inspects the incoming request and returns a PartialViewResult for Ajax requests and a full ViewResult for non-Ajax requests
/// </summary>
public class AjaxableViewResult : ActionResult
{
    /// <summary>
    /// Determines the convention for looking up a PartialView for the incoming request.
    /// The default convention looks for an underscore followed by the action name.
    /// For example: _ContactUs.cshtml or _ContactUs.ascx
    /// </summary>
    public static Func<ControllerContext, string> AjaxViewNameConvention = context => "_" + context.RouteData.GetRequiredString("action");
 
    /// <summary>
    /// The view name for non-Ajax requests
    /// </summary>
    public string NonAjaxViewName { get; set; }
 
    /// <summary>
    /// The view name for Ajax requests
    /// </summary>
    public string AjaxViewName { get; set; }
 
    /// <summary>
    /// The model that is rendered to the view
    /// </summary>
    public object Model { get; set; }
 
    /// <summary>
    /// Creates a new AjaxableViewResult using the default conventions
    /// </summary>
    public AjaxableViewResult() : this(null, null, null)
    {
             
    }
 
    /// <summary>
    /// Creates a new AjaxableViewResult using the default conventions with custom model data
    /// </summary>
    public AjaxableViewResult(object model) : this(null, null, model)
    {
     
    }
 
    /// <summary>
    /// Creates a new AjaxableViewResult with a specific ajax partial view
    /// </summary>
    public AjaxableViewResult(string ajaxViewName) : this(ajaxViewName, null)
    {
             
    }
 
    /// <summary>
    /// Creates a new AjaxableViewResult with a specific ajax partial view and model
    /// </summary>
    public AjaxableViewResult(string ajaxViewName, object model) : this(ajaxViewName, null, model)
    {
             
    }
 
    /// <summary>
    /// Creates a new AjaxableViewResult with a specific ajax view, non-ajax view, and model
    /// </summary>
    public AjaxableViewResult(string ajaxViewName, string defaultViewName, object model)
    {
        NonAjaxViewName = defaultViewName;
        AjaxViewName = ajaxViewName;
        Model = model;
    }
 
 
    public override void ExecuteResult(ControllerContext context)
    {
        context.Controller.ViewData.Model = Model;
 
        if (context.HttpContext.Request.IsAjaxRequest())
        {
            var view = new PartialViewResult
                            {
                                ViewName = GetAjaxViewName(context),
                                ViewData = context.Controller.ViewData
                            };
 
            view.ExecuteResult(context);
        }
        else
        {
            var view = new ViewResult
            {
                ViewName = GetViewName(context),
                ViewData = context.Controller.ViewData
            };
 
            view.ExecuteResult(context);   
        }
 
    }
 
    private string GetViewName(ControllerContext context)
    {
        return !string.IsNullOrEmpty(NonAjaxViewName) ? NonAjaxViewName : context.RouteData.GetRequiredString("action");
    }
 
    private string GetAjaxViewName(ControllerContext context)
    {
        return !string.IsNullOrEmpty(AjaxViewName) ? AjaxViewName : AjaxViewNameConvention(context);
    }
}

 

Overriding the AjaxViewName convention

For completeness I decided to add a simple extensibility point to change the Ajax view lookup convention. In your Global.asax you can use the static AjaxViewNameConvention to define your own partial view convention, the default looks for an underscore before the action name.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
 
    AjaxableViewResult.AjaxViewNameConvention =
        context => "_" + context.RouteData.GetRequiredString("action");
 
    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

 

Wrap-up and source code

Thanks again to the commenter who sparked these improvements!  The full source code is attached to this post, hopefully it helps someone!

download the source