Neilski
Neilski

Reputation: 4415

Modify view path from MVC controller before rendering it

In my MVC 4 Controller, I want to override the View() method

ViewResult View(string viewName, string masterName, object model) {}

So that I can manipulate the view being rendered by the action method. To do this, I want to be able to obtain the physical path of the view file. I have tried the following:

string viewName = this.ControllerContext.RouteData.Route
   .GetVirtualPath(this.ControllerContext.RequestContext, null)
   .VirtualPath;

For example, this might return "/Errors/MissingParameters" when what I really want it to return is something like:

"~/Views/Errors/MissingParameters"

or, even better:

"~/Views/Errors/MissingParameters.cshtml"

Just to add complication, I also need it to cope with Areas, so if I had the same example running in an Area named "Surveys", I would want it to return something like:

"~/Areas/Surveys/Views/Errors/MissingParameters"

The reason I want to do this is that I'm experimenting with using views for globalization, so I might have two views:

"~/Views/Errors/MissingParameters.cshtml"        // default view (en-GB)
"~/Views/Errors/MissingParameters_de-DE.cshtml"  // German view (de-DE)

and I want to be able to check if the view exists for the current language/culture before referencing it.

Any advice would be much appreciated.

Thanks.

Upvotes: 0

Views: 6121

Answers (3)

Noman Chali
Noman Chali

Reputation: 330

i am registering the area but i don't want my

url: "{area}/{controller}/{action}/{id}",

instead i want it to be like

url: "{controller}/{action}/{id}",

so i have registered my area like

context.MapRoute(
                name: "AreaName_default",
                url: "{controller}/{action}/{id}",
                namespaces: new[] { "SolutionName.AreaName.Controllers" }
            );

and i don't want to add the hard code string viewpath while returning view in every action method like

return View("~/Areas/AreaName/Views/ControllerName/ViewName.cshtml", model); 

so i have created one result filter and override OnResultExecuting function

public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        string areaName = AreaNameAreaRegistration.PropoertyName;
        if (filterContext.Result.GetType() == typeof(ViewResult) || filterContext.Result.GetType() == typeof(PartialViewResult))
        {
            dynamic viewResult = filterContext.Result;
            string viewname = string.IsNullOrEmpty(viewResult.ViewName) ? Convert.ToString(filterContext.RouteData.Values["action"]) : viewResult.ViewName;
            string folder = Convert.ToString(filterContext.RouteData.Values["controller"]);
            string lateralHireAreaViewPath = $"~/Areas/{areaName}/Views/";
            string extension = viewname.Contains(".cshtml") ? "" : ".cshtml";
            viewResult.ViewName = string.Concat(lateralHireAreaViewPath, folder, "/", viewname, extension);
            ViewEngineResult result = ViewEngines.Engines.FindView(filterContext.Controller.ControllerContext, viewResult.ViewName, null);
            if (result.View == null)
            {
                //searched in shared folder
                lateralHireAreaViewPath = string.Concat(lateralHireAreaViewPath, "Shared/");
                viewResult.ViewName = string.Concat(lateralHireAreaViewPath, "/", viewname, extension);
            }
        }
    }

Upvotes: 0

JotaBe
JotaBe

Reputation: 39004

EDIT: This part will not work or is hard to implement

You'd rather use an action filter which will let you manipulate the Result before executing it.

Particularly you need a Result filter. Implement the IResultFilter.onResultExecuting method, and change the result there. Particularly when you implement this method:

void OnResultExecuting(ResultExecutingContext filterContext)

You can access the ResultExecutingContext.Result Property. This property will contain your view. If you cast it to System.Web.Mvc.ViewResultBase you'll have access to the ViewName and you'll be able to change it.

If you've never implemented a filter, this is a good hands-on-lab on the subject. In this case it implements another kind of filter, but it's just the same.

As an answer to the OP comment, it's perfectly normal that ViewName is missing, and View is still null. ViewName wouldn't be empty only if the case that the view is returned with name, like this: return View("Index");. And, the ViewName would be just, not the whole path to the view. So this is not a solution. So, to have this solution working you would have to deal with route data, controller context, etc. to find the view. (More on this below.)

EDIT: Solution, register a custom view engine

When MVC has to render a view it gets the information from the route data, the controller context, the view name (that, as explained above can be empty), and the conventions that apply.

Particularly, in MVC there is a collection of registered view engines which are required to find the view calling there FindView() method. The view engine will return a ViewEngineResult which has the found view, if one was found, or a list of the paths where the view has been unsuccesfully sought.

So, to modify the template path, you can override this funcionality: let the original class find the view, and, if it is found, modify the path.

To do show you need to take theses steps:

  1. Inherit the view engine which you're using (my sampel code inherits Razor view engine)
  2. Register your vie engine, so that it's queried before the original view engine (in my sample code I simply clear the registered engines list, and register mine. The original list includes razor and web form view engines)

This is the code for the inherited view engine:

public class CustomRazorViewEngine : FixedRazorViewEngine
{
    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        ViewEngineResult result
            = base.FindView(controllerContext, viewName, masterName, useCache);
        if (result.View != null)
        {
            // Modify here !!
        }
        return result;
    }

    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {
        ViewEngineResult result
            = base.FindPartialView(controllerContext, partialViewName, useCache);
        if (result.View != null)
        {
            // Modify here !!
        }
        return result;
    }

    static readonly PropertyInfo ViewPathProp
        = typeof(RazorView).GetProperty("ViewPath");

    public void SetViewPath(RazorView view, string path)
    {
        ViewPathProp.SetValue(view, path);
    }

}

NOTE 1: where you read // Modify here !! you can modify the path property of the result.View. Cast it to RazorView: (result.View as RazorView).ViewPath. As the ViewPath setter is protected, you need to set it using Reflection: you can use the SetViewPath method for this.

NOTE 2: As you can see I'm not inheriting the RazorViewEngine but the FixedRazorViewEngine. If you loook for this class in MSDN you'll get not results, but if you look the original content of the registered view engines list, you'll find this class. I think this depends on an installed package in the project, and I think it solves a bug in MVC4. If you don't finf it in Microsoft.Web.Mvc namespace, inherit the original RazorViewEngined

NOTE 3: after the view is found, the view engine executes it, using the ViewEngineResult, so, if you change it, it will be executed with the new view path

And finally, you need to change the list of registered engines, in global.asax application start event, like this:

protected void Application_Start()
{
    // Original content:
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);

    // Added content:
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new CustomRazorViewEngine());
}

NOTE: it would be cleaner, if you created a ViewEngineConfig class in App_Start folder, and invoked an static method of this class, just as it's done with all other configurations.

Upvotes: 5

kbvishnu
kbvishnu

Reputation: 15630

Answer was copied from here.

Well if you don't mind having your code tied to the specific view engine you're using, you can look at the ViewContext.View property and cast it to WebFormView

var viewPath = ((WebFormView)ViewContext.View).ViewPath;

I believe that will get you the view name at the end.

EDIT: Haacked is absolutely spot-on; to make things a bit neater I've wrapped the logic up in an extension method like so:

public static class IViewExtensions {
    public static string GetWebFormViewName(this IView view) {
        if (view is WebFormView) {
            string viewUrl = ((WebFormView)view).ViewPath;
            string viewFileName = viewUrl.Substring(viewUrl.LastIndexOf('/'));
            string viewFileNameWithoutExtension = Path.GetFileNameWithoutExtension(viewFileName);
            return (viewFileNameWithoutExtension);
        } else {
            throw (new InvalidOperationException("This view is not a WebFormView"));
        }
    }
}

which seems to do exactly what I was after.

Another solution here ((System.Web.Mvc.RazorView)htmlHelper.ViewContext.View).ViewPath

net-mvc

Upvotes: 1

Related Questions