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)