Handling Layout properties with custom Razor view engine
Asked Answered
L

3

12

I have implemented a multi-tenant view engine similar to what is described here:

Which let me override the search locations for view like this:

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

In which the %1 is replaced with the correct folder for the active tenant. This is working just fine exception one problem. When I define the Layout path on my view like this:

Layout = "~/Views/Default/Shared/_MyLyout.cshtml";

It kind of defeats the purpose of having the multi-tenancy since I have have to hard code the exact location of the layout page. I want to be able to do something like this:

Layout = "~/Views/%1/Shared/_MyLyout.cshtml";

If I wanted to allow tenants to have their one layout pages, how would I go about supporting this?

I have tried fiddling with the view engine methods that I overrode:

  • CreatePartialView
  • CreateView
  • FileExists

But nothing seems to point itself towards being able to dynamically specify the layout page.

Update:

Here's what I have working so far. I used the answer to this question https://mcmap.net/q/1012120/-how-to-set-razor-layout-file-just-specifying-the-name slightly modified to create a HTML helper:

public static string GetLayoutPageForTenant( this HtmlHelper html, string LayoutPageName )
{
    var layoutLocationFormats = new[]
    {
        "~/Views/{2}/{1}/{0}.cshtml",
        "~/Views/{2}/Shared/{0}.cshtml",
        "~/Views/Default/{1}/{0}.cshtml",
        "~/Views/Default/Shared/{0}.cshtml",
    };

    var controller = html.ViewContext.Controller as MultiTenantController;
    if( controller != null )
    {
        var tenantName = controller.GetTenantSchema();
        var controllerName = html.ViewContext.RouteData.Values["Controller"].ToString();

        foreach( var item in layoutLocationFormats )
        {
            var resolveLayoutUrl = string.Format( item, LayoutPageName, controllerName, tenantName );
            var fullLayoutPath = HostingEnvironment.IsHosted ? HostingEnvironment.MapPath( resolveLayoutUrl ) : System.IO.Path.GetFullPath( resolveLayoutUrl );
            if( File.Exists( fullLayoutPath ) ) return resolveLayoutUrl;
        }
    }

    throw new Exception( "Page not found." );
}

which is similar to what saravanan suggested. Then I can set the layout in my view with this code:

Layout = Html.GetLayoutPageForTenant( "_Home" );

Unfortunately, this duplicates the work that the custom view engine is doing which seems like the wrong way to go.

Lobo answered 10/5, 2013 at 22:53 Comment(0)
I
4

I would like to propose the following idea,

In the _ViewStart.cshtml file, where we set the layout pages, you can use something like this, with the idea of the Tenant based layout url or the folder name is being filled in the controller by fetching from the DB.

@{
    Layout = ViewBag.TenantLayoutPageUrl;
 }

or

 @{
    Layout = string.Format("~/Views/{0}/Shared/_MyLyout.cshtml",ViewBag.TenantId);
 }

If you have some static Tenant data representations, like a static Identity class that will keep track of your tenant's customization, we can use that and minimize the round trip to the db.

Please share your idea on this implementation so it will be useful for the community

Infusible answered 11/5, 2013 at 12:41 Comment(6)
The only problem I have with this approach is that not every tenant will have their own custom layout pages. I still want the search to default to the Default folder if a custom layout page is not found.Lobo
@Sparafusile: Being that case, we can have a fallback id in the ViewBag. So in the Contoller, we will check if the tenant has a custom layout page, if so we will set that folder in the view bag, else we will set the tenant's folder name. ViewBag.TenantFolderName="defaultPath"; if(tenant Has customFolder){ ViewBag.TenantFolderName=tenantFolderName; } IMHO, this will be failsafe.Infusible
While I agree this could work, it kind of defeats the purpose of the custom view engine to do all the work in the controller. I will continue looking for a more elegant solution.Lobo
Sure, We will await a better solution. I will also learn from this.Infusible
While I didn't end up using your answer directly, our discussion pushed me in the direction I needed. Thank you.Lobo
@Sparafusile: Thanks to you too for sharing your ideas. If you feel free to share the logic that you implemented, please share with us.Infusible
H
1

Try,

public class CustomWebViewPage : WebViewPage
{
    public override void ExecutePageHierarchy()
    {
        if (Context.Items["__MainView"] == null)
        {
            this.Layout = String.Format("~/Views/Shared/{0}/_Layout.cshtml", ViewContext.Controller.GetType().Namespace);
            Context.Items["__MainView"] = "Not Null";
        }
        base.ExecutePageHierarchy();
    }

    public override void Execute()
    {
    }
}

public class CustomWebViewPage<T> : WebViewPage<T>
{
    public override void ExecutePageHierarchy()
    {
        if (Context.Items["__MainView"] == null)
        {
            this.Layout = String.Format("~/Views/Shared/{0}/_Layout.cshtml", ViewContext.Controller.GetType().Namespace);
            Context.Items["__MainView"] = "Not Null";
        }
        base.ExecutePageHierarchy();
    }

    public override void Execute()
    {
    }
}

<system.web.webPages.razor>
  <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  <pages pageBaseType="Mv4App.CustomWebViewPage">
Highclass answered 16/5, 2013 at 7:3 Comment(0)
M
0

You can add following _ViewStart.cshtml in the tenant views folder (~/Views/%1/_ViewStart.cshtml). Each tenant can manage their own layout files.

@{
    Layout =  VirtualPathUtility.GetDirectory(PageContext.Page.VirtualPath) + "Shared/_Layout.cshtml";
}
Maclay answered 17/5, 2013 at 14:38 Comment(2)
That's an interesting idea, but I want to be able to use a custom layout with default views and/or use default layout with custom views. Unless I misunderstand your answer, I can't do that with your code.Lobo
@Lobo You can do that, you can put _ViewStart.cshtml in any folders you need to customize, you can put it in even in controller folder or in default folder. By default Razor Engine search controller folder for start page (_ViewStart.cshtml) if it cannot find try parent folder and so on until the application root.Maclay

© 2022 - 2024 — McMap. All rights reserved.