Theming RazorPages in Dotnet Core 2.1
Asked Answered
A

0

7

I am currently trying to implement a themeable site using Razor Pages in dotnetcore 2.1 but am having some trouble/confusion with why the pages won't load.

Every request to the site causes a theme value to be set based on the accessed domain, by default the theme is "Default" which is stored in the RouteData of each request.

I have implemented the following ThemeViewLocationExpander

public class ThemeViewLocationExpander : IViewLocationExpander
{
    private const string ValueKey = "Theme";

    public void PopulateValues(ViewLocationExpanderContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        context.Values[ValueKey] = (context.ActionContext.RouteData.Values["tenant"] as Tenant)?.Theme;
    }

    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (viewLocations is null)
        {
            throw new ArgumentNullException(nameof(viewLocations));
        }

        context.Values.TryGetValue(ValueKey, out string theme);

        if (!string.IsNullOrEmpty(theme))
        {
            viewLocations = new[] {
                $"/Pages/Themes/{theme}/{{1}}/{{0}}.cshtml",
                $"/Pages/Themes/{theme}/Shared/{{0}}.cshtml",
                $"/Pages/Shared/{{0}}.cshtml",
            };
        }

        return viewLocations;
    }

As you can see I am trying to load the pages (as each theme may have a completely different layout and not just varying CSS) from /Pages/Themes/{THEME_NAME}... instead of /Pages/, with the exception of a top level shared folder /Pages/Shared for things such as partials for validation scripts which may exist in all themes.

I don't want there to be any other pages directly under /Pages such as /Pages/Index.cshtml as EVERY request should end up inside one of the theme folders as having a theme is mandatory.

I have added the following to my Startup.cs

        services.Configure<RazorViewEngineOptions>(options =>
        {
            options.ViewLocationExpanders.Add(new ThemeViewLocationExpander());
        });

I can breakpoint this code on first load, so I know it is registered, however if I run the site and got to https://localhost:44352/ it loads /Pages/Index.cshtml when it should be loading /Pages/Themes/Default/Index.cshtml.

If I delete /Pages/Index.cshtml and run the site I get

This localhost page can’t be found
No webpage was found for the web address: https://localhost:44352/
HTTP ERROR 404

Can anyone offer me any insight or help with this?

Perhaps I need to modify some routing to incorporate the theme, or am I missing something major, it seems as if the view locations I set in the ThemeViewLocationExpander.ExpandViewLocations() never get used.

There is a lot of multi-tenancy stuff that I have read about for DNC 2.1 but it is either for MVC not RazorPages or only implements CSS / _Layout.cshtml pages inside the theme folders and uses the default pages under /Pages/ for layout (I am trying to have individual implementations of all pages inside each theme folder)

Edit:

I have decided it would make more sense to keep the /Pages/Index.cshtml and other pages in the top level folder and use that as the default 'Theme' any new themes would then use these pages unless specifically overridden inside of their /Pages/Themes/{THEME_NAME} folder.

I Have implemented this, but am still at a loss as to why the page loads from /Pages/Index.cshtml even when a theme value is set and the viewLocations have the theme folder added to them.

Edit 2:

Have tested it and with a theme set it definitely used the correct layout page in the theme folder. For example, with the theme 'Fresh' when I load https://localhost:44352/ the layout page /Pages/Themes/Fresh/Shared/_Layout.cshtml is loaded, however the Index.cshtml is loaded from /Pages/Index.cshtml not /Pages/Themes/Fresh/Index.cshtml.

At this point the viewLocations were:

/Pages/Themes/Fresh/{1}/{0}.cshtml
/Pages/Themes/Fresh/Shared/{0}.cshtml
/Pages/Shared/{0}.cshtml
/Pages/{1}/{0}.cshtml
/Pages/Shared/{0}.cshtml
/Views/Shared/{0}.cshtml

Edit 3:

Here is a github link for a simplified version of the project that demonstrates the issue I am having. https://github.com/BenMaxfield/ThemeTest

In order to test it, just run the application and you should see that it loads the DARK theme layout, but the DEFAULT Index content.

To swap to the DEFAULT theme look for the PopulateValues method in ThemePageViewLocationExpander and set context.Values[ThemeKey] = string.Empty;

Edit 4

I have noticed that ThemePageViewLocationExpander.ExpandViewLocations is only every called for partial files, and never for actual pages with code behind.

This explains why the correct _Layout.cshtml page was loaded for the theme but the correct Index.cshtml file was not (as /Pages/Index.cshtml is a page with code behind and has the @page declaration - i.e it is not partial)

To get around this in a hacky way I have created a new folder /Pages/Views/ and put a partial .cshtml that corresponds to each of the @page pages at the top level of /Pages/,

e.g I now have /Pages/Index.cshtml (non-partial) and /Pages/Views/Index.cshtml (partial).

Inside /Pages/Index.cshtml I have added <partial name="Index" model="@Model"/> to load the corresponding partial from the new /Pages/Views/ folder.

As this is now loading a partial ThemePageViewLocationExpander.ExpandViewLocations is invoked and I can then override the view location for it based on the given theme key (if present).

This means all requests to /Index will load OnGet() from /Pages/Index.cshtml which subsequently loads a partial in its view which I can optionally override by adding another Index.cshtml partial inside of /Themes/{THEME_NAME}/Views/.

Here is my current viewlocation override for ThemePageViewLocationExpander.ExpandViewLocations

            viewLocations = new[] {
                $"/Themes/{theme}/{{0}}.cshtml",
                $"/Themes/{theme}/Views/{{0}}.cshtml",
                $"/Pages/Views/{{0}}.cshtml",
            }.Concat(viewLocations);

And an example folder structure:

.
├── Pages
|   ├── Index.cshtml (with code behind, called on localhost:45635/)
|   └── Views
|       └── Index.cshtml (partial)
└── Themes
|   ├── Dark
|      └── Views
|          └── Index.cshtml (partial)
Affined answered 26/5, 2018 at 10:29 Comment(2)
Did you ever get this solved ?Woosley
I used partials as explained in my editsAffined

© 2022 - 2024 — McMap. All rights reserved.