3
\$\begingroup\$

I am currently in the process of creating a website which requires it to work with and without JavaScript enabled. All callbacks to the server with JavaScript enabled will be made through Ajax callbacks. In order to keep my code very DRY, I wanted to reduce the amount of views that was required to be created.

For that purpose I created the following code:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class UnobtrusiveAjax : ActionFilterAttribute
{
    public enum ActionResultType
    {
        Json,
        PartialView,
        View,
    }

    /// <summary>
    /// The type of Model to be generated from JsonResults.
    /// </summary>
    public Type ModelType { get; set; }

    /// <summary>
    /// The type of ActionResult to be returned if the action was requested via an ajax callback.
    /// </summary>
    public ActionResultType ViewType { get; set; }

    /// <summary>
    /// Determines if we will allow the request to be fulfilled through a Http-Get request.
    /// </summary>
    public bool AllowGet { get; set; }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (filterContext.IsRequest("GET")
            && !AllowGet
            && !filterContext.ActionDescriptor.IsDefined(typeof(HttpGetAttribute), false))
        {
            throw new ArgumentException("Unable to perform action via a Http-Get request, please perform action via a different Http request method.",
                nameof(filterContext.ActionDescriptor));
        }

        base.OnActionExecuted(filterContext);

        if (filterContext.IsAjaxRequest())
        {
            switch (ViewType)
            {
                case ActionResultType.View:
                        filterContext.Result = ConvertToView(filterContext);
                    break;
                case ActionResultType.PartialView:
                        filterContext.Result = ConvertToPartialView(filterContext);
                    break;

                case ActionResultType.Json:
                default:
                    filterContext.Result = ConvertToJson(filterContext);
                    break;
            }
        }
    }

    private ActionResult ConvertToJson(ActionExecutedContext filterContext)
    {
        if (filterContext.Result is ViewResultBase)
        {
            var result = filterContext.Result as ViewResultBase;
            var serializer = new JavaScriptSerializer();
            return new JsonResult()
            {
                Data = serializer.Serialize(result.Model),
                JsonRequestBehavior =
                    (AllowGet) ? JsonRequestBehavior.AllowGet : JsonRequestBehavior.DenyGet
            };
        }

        return filterContext.Result;
    }

    private ActionResult ConvertToPartialView(ActionExecutedContext filterContext)
    {
        if (filterContext.Result is JsonResult)
        {
            var partial = new PartialViewResult();
            var result = filterContext.Result as JsonResult;
            var viewName = filterContext.ActionDescriptor.ActionName;
            dynamic model = null;

            if ((result.Data is string))
            {
                model = DeserializeModel(result.Data as string);
            }


            partial.ViewData.Model = model ?? result.Data;
            partial.ViewName = viewName;
            partial.ViewEngineCollection = ViewEngines.Engines;

            return partial;
        }
        else if (filterContext.Result is ViewResultBase)
        {
            return ConvertToPartialView(filterContext.Result as ViewResultBase);
        }

        return filterContext.Result;
    }

    private ActionResult ConvertToPartialView(ViewResultBase result)
    {
        return new PartialViewResult()
        {
            TempData = result.TempData,
            ViewData = result.ViewData,
            ViewName = result.ViewName
        };
    }

    private ActionResult ConvertToView(ActionExecutedContext filterContext)
    {
        if (filterContext.Result is JsonResult)
        {
            var result = filterContext.Result as JsonResult;
            var viewName = filterContext.ActionDescriptor.ActionName;
            object model = null;

            if ((result.Data is string))
            {
                model = DeserializeModel(result.Data as string);
            }

            var view = new ViewResult();
            view.ViewData.Model = model ?? result.Data;
            view.ViewName = viewName;

            return view;
        }
        else if (filterContext.Result is ViewResultBase)
        {
            return ConvertToView(filterContext.Result as ViewResultBase);
        }

        return filterContext.Result;
    }

    private ActionResult ConvertToView(ViewResultBase result)
    {
        return new ViewResult()
        {
            TempData = result.TempData,
            ViewData = result.ViewData,
            ViewName = result.ViewName
        };
    }

    private object DeserializeModel(string model)
    {
        if (ModelType == null)
        {
            throw new ArgumentNullException("The 'ModelType' cannot be null.",
                nameof(ModelType));
        }

        var serializer = new JavaScriptSerializer();
        return serializer.Deserialize(model, ModelType);
    }
}

public static class ControllerContextExtensions
{
    public static bool IsAjaxRequest(this ControllerContext that)
    {
        return that.HttpContext.Request.IsAjaxRequest();
    }

    public static bool IsRequest(this ControllerContext that, string method)
    {
        return (that.HttpContext.Request.HttpMethod.Equals(method, StringComparison.CurrentCultureIgnoreCase));
    }
}

The code itself works like you'd expect it to work. I'm wondering if there is a more optimized version or if this has been done before and I'm just reinventing the wheel.

\$\endgroup\$
1
\$\begingroup\$

Since it is working, it is fine and I don't see any performance issue as well. However, the code is less testable because of multiple responsibility and logic are in single class + there are too many assumption (if, is, as) makes it hard to test.

  1. Extract conversion logic to different classes per type of views.
  2. Chain them based on preference, return if handled or pass to next class in chain (Chain of Responsibility pattern)
  3. Instead of passing context, pass result to conversion methods
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.