ViewEngine

Mihai Mogosanu edited this page Jun 26, 2014 · 5 revisions

Note: This feature is aimed at experienced developers who really need advanced view engine flexibility.

It's all about making the view engine play nice with your style (project structure, dynamic views names, themes etc). And since the default RazorViewEngine is too rigid, you can use the FlexibleViewEgine

 FlexibleViewEngine.Enable(cfg =>
            {
                cfg.RegisterConventions(typeof (ConfigTask_ViewEngine).Assembly);
            },removeOtherEngines:true);

By default, it comes with the standard asp.net mvc view conventions, so your app should work without a change, the code above is the only change you need, in your app start up. And this allows you to customize things further, by creating your very own conventions.

As an example, I want to have a Themes folder to keep all my views organized by.. themes. Great for CMS, blog engines and everything requiring skin/theme support. I want only the views there as a simple list, not organized by controller or in a Shared folder.

 public class ThemedViews : IFindViewConvention
    {
        public virtual bool Match(ControllerContext context, string viewName)
        {
            return context.Controller.GetType().ShouldBeThemeable();
        }

        /// <summary>
        /// Gets relative path for view. 
        /// </summary>
        /// <param name="controllerContext"/><param name="viewName"/>
        /// <returns/>
        public string GetViewPath(ControllerContext controllerContext, string viewName)
        {
            var theme = controllerContext.HttpContext.GetThemeInfo();
            if (theme == null)
            {
                throw new InvalidOperationException("No theme is set. Bug!");
            }
            return theme.ViewsPath + "/" + viewName + ".cshtml";
        }

        /// <summary>
        /// Gets relative path for master (layout). If master name is empty, it should return empty
        /// </summary>
        /// <param name="controllerContext"/><param name="masterName"/>
        /// <returns/>
        public string GetMasterPath(ControllerContext controllerContext, string masterName)
        {
            return "";
        }
    }

Here I'm using the predefined themes support, by getting the current theme using an extension method. Then I'm composing the path of the view. Simple code.

Here's a more advanced example: I want to select a view based on a view model value.I have a Page which can support templates.If the Template property of the view model is empty, use a default view, else use the specified template as the view. I know this can be implemented in the Controller, by this is view engine logic, it belongs to a view engine (Separation of Concerns). Think about mobile support in asp .net mvc, you have a 'viewname.Mobile.chstml' pattern, used automatically by the framework. It's the same thing, but now YOU are in control.

public class ViewPage : IFindViewConvention
    {
        public bool Match(ControllerContext context, string viewName)
        {
            return context.Controller is ViewPageController;
        }

        public string GetViewPath(ControllerContext controllerContext, string viewName)
        {
            var model = controllerContext.Controller.ViewData.Model as ViewPageModel;
            var theme = controllerContext.HttpContext.GetThemeInfo();
            if (model.Template == null)
            {
                return theme.ViewsPath + "/Page.cshtml";            
            }
                return theme.ViewsPath + "/"+model.Template+".page.cshtml";            
        }

        public string GetMasterPath(ControllerContext controllerContext, string masterName)
        {
            return "";
        }
    }

If I Match the above convention to apply to mobile devices, and return 'viewName+"Mobile.cshtml"', I'm implementing the same Mvc mobile functionality.

You want even more cool stuff? How about, when rendering a display/editor template (via HtmlConventions) you can get and use the template model data to select which view should be used?

In this example, I have an editor template for the DocumentContent type. But, I want to have multiple templates for the same model :) . The partials' names should be like "DocumentContent.html.cshtml" or "DocumentContent.markdown.cshtml" etc, i.e the partial name depends on a model property,in this case the Format property.

 [Order(1)]
    public class DocumentContentTemplates : IFindViewConvention
    {
        public bool Match(ControllerContext context, string viewName)
        {
            return viewName.Contains("DocumentContent");            
        }

        public string GetViewPath(ControllerContext controllerContext, string viewName)
        {
            var model = controllerContext.TemplateModel<DocumentContent>(viewName);
            var path = "~/Admin/EditorTemplates/DocumentContent."+model.Format.ToLower()+".cshtml";
            return path;
        }

        public string GetMasterPath(ControllerContext controllerContext, string masterName)
        {
            return "";
        }
    }

You can see the [Order] annotation. You can use that attribute to specify in which order the view engine should consider the conventions. The engine stops at the first matching conventions, so when you have many conventions, the specific ones should come first, then the generic ones.

Important: A convention doesn't distinguish between views and partials. If you need more flexible rules for partials, it's best to create specific conventions for views and partials.

For example, I have these rules (I'm in an Admin context/folder/namespace):

  • views are either in the same folder as the controller or in the ~/Admin folder;
  • views are named after their controllers (I'm using controller as a handler convention i.e one action per controller with method for GET and POST);
  • a partial name always starts with '_';
  • a partial exists in the same folder as the controller or in the ~/Admin folder .

Here are the conventions

 [Order(1)]
    public class AdminFindViews : BaseAdminFindViews
    {
        public override bool Match(ControllerContext context, string viewName)
        {
            return base.Match(context, viewName) && !viewName.StartsWith("_") && !FlexibleViewEngine.IsMvcTemplate(viewName);
        }

        /// <summary>
        /// Gets relative path for view. 
        /// </summary>
        /// <param name="controllerContext"/><param name="viewName"/>
        /// <returns/>
        public override string GetViewPath(ControllerContext controllerContext, string viewName)
        {
            var ctrl = controllerContext.Controller.GetType();

           //converts a namespace string to an url path
            var path = ctrl.ToWebsiteRelativePath(GetType().Assembly);
            return path + ".cshtml";
        }
    }

 [Order(2)]
    public class AdminFindPartialViews : BaseAdminFindViews
    {
        public override bool Match(ControllerContext context, string viewName)
        {
            return base.Match(context, viewName) && (viewName.StartsWith("_") || FlexibleViewEngine.IsMvcTemplate(viewName));
        }

        /// <summary>
        /// Gets relative path for view. 
        /// </summary>
        /// <param name="controllerContext"/><param name="viewName"/>
        /// <returns/>
        public override string GetViewPath(ControllerContext controllerContext, string viewName)
        {
            var nspace = controllerContext.Controller.GetType().Namespace.Urlize(GetType().Assembly);
            return "~/" + nspace +"/"+ viewName + ".cshtml";
        }
    }

 [Order(3)]
    public class AdminDefaultViewsPath : BaseAdminFindViews
    {
        /// <summary>
        /// Gets relative path for view. 
        /// </summary>
        /// <param name="controllerContext"/><param name="viewName"/>
        /// <returns/>
        public override string GetViewPath(ControllerContext controllerContext, string viewName)
        {
            if (!viewName.StartsWith("_"))
            {
                viewName = controllerContext.RouteData.GetRequiredString("controller");
            }
            return "~/Admin/" + viewName + ".cshtml";
        }
    }

So, first search for the view/partial in the controller's (namespace)folder then search in the ~/Admin folder. And here's how the directory structure looks like.

Imgur

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.