Question

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://stackoverflow.com/a/9288455/292578 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.

Was it helpful?

Solution

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

OTHER TIPS

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">

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";
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top