2

I've made myself a modular framework with ninject for MVC.

Each module can register it's own routes and contains it's own views.

Module dir (dll location):
~/Modules/<module name>/

Module views sit inside:
<Module dir>/Views/
They are arranged exactly like a normal mvc app, IE a folder for each controller and a shared folder.

I want to render a view with a layout, however I want the layout location to be set by the core framework (so that i can change themes).

I have a view that has layout = _layout.cshtml and when i run the app it returns:

The layout page "_Layout.cshtml" could not be found at the following path: "~/Modules/Module2/Views/Home/_Layout.cshtml".

The view that was called was here ~/Modules/Module2/Views/Home/Index.cshtml. But I want it to look for the layout in another location without setting it in each view. Is there anyway I can do that in the core framework? Note i set it MasterLocationFormats to look in shared too, which it apparently does not (I tested that by placing a _layout.cshtml in there).


Custom View Engine:

public NinjectRazorViewEngine(): base()
    {
        ViewLocationFormats = new[] {
            "~/Modules/%1/Views/{1}/{0}.cshtml",
            "~/Modules/%1/Views/{1}/{0}.vbhtml",
            "~/Modules/%1/Views/Shared/{0}.cshtml",
            "~/Modules/%1/Views/Shared/{0}.vbhtml"
        };

        MasterLocationFormats = new[] {
            "~/Modules/%1/Views/{1}/{0}.cshtml",
            "~/Modules/%1/Views/{1}/{0}.vbhtml",
            "~/Modules/%1/Views/Shared/{0}.cshtml",
            "~/Modules/%1/Views/Shared/{0}.vbhtml",
        };

        PartialViewLocationFormats = new[] {
            "~/Modules/%1/Views/{1}/{0}.cshtml",
            "~/Modules/%1/Views/{1}/{0}.vbhtml",
            "~/Modules/%1/Views/Shared/{0}.cshtml",
            "~/Modules/%1/Views/Shared/{0}.vbhtml"
        };

        PartialViewLocationFormats = ViewLocationFormats;
        AreaPartialViewLocationFormats = AreaViewLocationFormats;
    }

    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
        object moduleName;
        if(controllerContext.RequestContext.RouteData.Values.TryGetValue("module",out moduleName))
            return base.CreatePartialView(controllerContext, partialPath.Replace("%1", (string)moduleName));
        return base.CreatePartialView(controllerContext, partialPath);
    }

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        object moduleName;
        if (controllerContext.RequestContext.RouteData.Values.TryGetValue("module", out moduleName))
            return base.CreateView(controllerContext, viewPath.Replace("%1", (string)moduleName), masterPath.Replace("%1", (string)moduleName));
        return base.CreateView(controllerContext, viewPath, masterPath);
    }

    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        object moduleName;
        if (controllerContext.RequestContext.RouteData.Values.TryGetValue("module", out moduleName))
            return base.FileExists(controllerContext, virtualPath.Replace("%1", (string)moduleName));
        return base.FileExists(controllerContext, virtualPath);
    }
4

3 回答 3

1

This took a large amount of work.

Changes had to be made to the view engine to properly expose the FindView and FindPartialView methods. the method outlined in the question is wrong.

This is how the viewEngineClass should look

public NinjectRazorViewEngine(): base()
    {
        ViewLocationFormats = new[] {
            "~/Modules/{2}/Views/{1}/{0}.cshtml",
            "~/Modules/{2}/Views/{1}/{0}.vbhtml",
            "~/Modules/{2}/Views/Shared/{0}.cshtml",
            "~/Modules/{2}/Views/Shared/{0}.vbhtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/{1}/{0}.vbhtml",
            "~/Views/Shared/{0}.cshtml",
            "~/Views/Shared/{0}.vbhtml"
        };

        MasterLocationFormats = new[] {
            "~/Modules/{2}/Views/{1}/{0}.cshtml",
            "~/Modules/{2}/Views/{1}/{0}.vbhtml",
            "~/Modules/{2}/Views/Shared/{0}.cshtml",
            "~/Modules/{2}/Views/Shared/{0}.vbhtml",
        };

        PartialViewLocationFormats = new[] {
            "~/Modules/{2}/Views/{1}/{0}.cshtml",
            "~/Modules/{2}/Views/{1}/{0}.vbhtml",
            "~/Modules/{2}/Views/Shared/{0}.cshtml",
            "~/Modules/{2}/Views/Shared/{0}.vbhtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/{1}/{0}.vbhtml",
            "~/Views/Shared/{0}.cshtml",
            "~/Views/Shared/{0}.vbhtml"
        };

        PartialViewLocationFormats = ViewLocationFormats;
        AreaPartialViewLocationFormats = AreaViewLocationFormats;

        //Used to test cache
        //ViewLocationCache = new DefaultViewLocationCache();
    }
    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {
        return FindView(controllerContext, partialViewName, "", useCache);
    }
    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        //Implement defualt exceptions
        if(controllerContext == null)
            throw new ArgumentNullException("The controllerContext parameter is null");
        if(string.IsNullOrEmpty(viewName))
            throw new ArgumentException("The viewName parameter is null or empty.");

        //Check cache if specified
        if(useCache && this.ViewLocationCache != null){
            string cachedLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, generateCacheKey(controllerContext, viewName));
            if (!string.IsNullOrEmpty(cachedLocation))
                return new ViewEngineResult(CreateView(controllerContext, cachedLocation, masterName), this);
        }

        //Create arguments for location formatting
        string trimmedViewName = string.Empty;
        if (viewName.EndsWith(".cshtml"))
            trimmedViewName = viewName.Remove(viewName.Length - 7);
        else
            trimmedViewName = viewName;
        object[] args = new object[] { trimmedViewName, controllerContext.RouteData.GetRequiredString("controller"), controllerContext.RouteData.GetRequiredString("module") };

        //Attempt to locate file
        List<string> searchedLocations = new List<string>();
        foreach(string location in ViewLocationFormats){
            string formatedLocation = string.Format(location,args);
            searchedLocations.Add(formatedLocation);
            if (FileExists(controllerContext, formatedLocation))
            {
                //File has been found. Add to cache and return view
                if(this.ViewLocationCache != null)
                    ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, generateCacheKey(controllerContext, viewName), formatedLocation);

                return new ViewEngineResult(CreateView(controllerContext, formatedLocation, masterName), this);
            }
        }

        //Couldnt find view, return searched locations
        return new ViewEngineResult(searchedLocations);
    }
    public string generateCacheKey(ControllerContext controllerContext, string viewName)
    {
        return string.Format("{0}|{1}", controllerContext.RouteData.GetRequiredString("module"), viewName);
    }

You then need to implement a custom System.Web.Mvc.WebViewPage<T> like so:

public abstract class WebViewPage<T> : System.Web.Mvc.WebViewPage<T>
{
    public override string Layout
    {
        get
        {
            return base.Layout;
        }
        set
        {
            NinjectRazorViewEngine viewEngine = new NinjectRazorViewEngine();
            System.Web.Mvc.ViewEngineResult engineResult = viewEngine.FindView(this.ViewContext.Controller.ControllerContext, value, string.Empty, true);
            System.Web.Mvc.RazorView razorView = engineResult.View as System.Web.Mvc.RazorView;
            if (razorView == null)
            {
                string searchedIn = "";
                foreach (string item in engineResult.SearchedLocations)
                {
                    searchedIn += item + "\n";
                }
                throw new HttpException(500, "Could not find views in locations:\n" + searchedIn);
            }
            base.Layout = razorView.ViewPath;
        }
    }
}

Hope that helps :)

于 2013-01-03T12:46:27.437 回答
0

You can implement your own ViewEngine, that will look for views in custom locations.

public class MyViewEngine : RazorViewEngine {
   public MyViewEngine() {
       this.MasterLocationFormats = new string[] {
           "PATH TO YOUR LAYOUT FILES", "ALTERNATIVE PATH"
       }
   }
}

and then during start up of your application (e.g. in Global.asax.cs) setup your application to use your custom engine

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ThemableViewEngine());
于 2012-11-29T07:13:23.167 回答
0

You should really try to use precompiled views with RazorGenerator.

Compiling views allows you to drop modules as single DLLs, which is much easier than also having to bring with the DLL all the content (like cshtml views) plus, you can load your modules at startup time or runtime with MEF or any other Reflection mechanism, wich makes your MVC app really modular and reduces coupling.

I have found myself that this implementation is not cost-effective on modular websites.

于 2012-12-10T11:57:20.937 回答