I'm writing a custom WebFormViewEngine and i would like to figure out the name of the Master Page file specified in the view's aspx file when the WebFormViewEngine.FindView method is called.
My end goal is to determine the name of the desired master page file so that i can use a master page file of the same name in a different directory to render the view.
So the challenge is that i have the file path of the view, but before the view is rendered i want to determine the name of the master page file.
I can open the file and search for MasterPageFile="%", but i'm hoping there's a better way to do this.
Thanks.
Master page lookups work differently than what you think. In MVC WebForms view engine there are basically two ways of specifying the master page:
In the <%# Page %> directive of the view page.
This method is not actually MVC-specific, it depends on the functionality built into WebForms control trees. You need to provide the full path and this value never gets looked at by the MVC pipeline, because it only gets set and evaluated once the view starts executing (which happens after WebFormViewEngine.FindView).
In your action method ViewResult: return View("Index", "MyCustomMaster")
You can override the master page from your controller. In this case you can specify just the Master View name or the full path to the master file. This value does get passed into WebFormViewEngine.FindView and it overrides whatever might be specified in the view itself.
If you always only use #2, then the values will always go through WebFormViewEngine.FindView. However if you also use #1, then you'll basically have to do the same thing that MVC does to enable #2: write your own custom Page class:
public class MyViewPage : System.Web.Mvc.ViewPage {
protected override void OnPreInit(EventArgs e) {
base.OnPreInit(e);
// you might not need the following, but perhaps it would be useful to
// differentiate between #1 and #2
bool masterOverridenInController = !String.IsNullOrEmpty(MasterLocation);
string currentPathToMaster = MasterPageFile;
// change currentPathToMaster any way you like
MasterPageFile = currentPathToMaster;
}
}
Related
I am trying to set up Razor Pages routing to allow different views to be rendered for different tenants.
I have a directory structure as follows:
/Pages
Test.cshtml.cs
/Tenant1
Test.cshtml
/Tenant2
Test.cshtml
Given I am already able to decide which tenant is required, how is it possible to configure the routing to map some path eg localhost:8080/Test to either Tenant1/Test or Tenant2/Test views.
Use dynamic view content (via partial views).
With this solution, the Test page will dynamically load a different view depending on the route used to call it.
This means that you only have a single Test page but inside the cshtml file you will grab content from a partial view (more on that in a second).
First you will need to rename the files like so....
/Pages
Test.cshtml.cs
/Tenant1
_Test.cshtml // note it is prefixed with an underscore!
/Tenant2
_Test.cshtml // prefixed with an underscore too.
The naming convention for a partial view is to prefix the file with an underscore (_). This will immediately identify to someone looking at your project files as a "non-routable" page.
Then you add a little bit of logic to render the partial views...
Test.cshtml
#{
switch(...) // used a switch statement to illustrate the solution
{
case "Tenant1":
await Html.PartialAsync("~/Pages/Tenant1/_Test.cshtml");
break;
case "Tenant2":
await Html.PartialAsync("~/Pages/Tenant2/_Test.cshtml");
break;
default:
throw new NotImplementedException();
}
}
You can read about partial views here.
Extra: Using the same page model.
I also noticed that you had wanted to use the same page model (meaning sharing Test.cshtml.cs for both. This is rather trivial, but for the sake of completeness of the answer here is how you would do that...
/Pages/Test.cshtml.cs
namespace Foo.Pages
{
public class MySharedTestModel : PageModel
{
...
}
}
/Pages/Tenant1/Test.cshtml and /Pages/Tenant2/Test.cshtml
#page
#using Foo.Pages
#model MySharedTestModel
...
My Problem: I have an MVC3 application where all views use a common master page. The master page has many links to other (internal) sites. I need to be able to change the domain of these links depending on the deployment environment (e.g. staging.blah.com, www.blah.com, dev.blah.com etc). This domain is stored in the web.config.
There are numerous ways of doing this, but I am looking for some sort of consensus as to the preferred method. Here are some options but I am open to any suggestions:
(1) reference appsettings from master page directly. This is the simplest and most common approach but I am not particularly keen on reading the web.config and concatenate the url throughout the master page code. In fact, I am not sure that I like the idea of the view accessing the web.config at all.
(2) stick the appsetting value in viewdata/viewbag using a custom action filter which reads the config. concatenate in the page as before.
(3) as (2), but inject appsetting value in via contructor injection rather than reading it within the filter.
(4) create a base class for all my strongly typed viewmodels and populate with the appsetting using a custom action filter.
(5) create an htmlhelper that takes in the path and internally reads the appsetting and concatenates.
(6) create a custom view base class, inject in appsetting value and make available as property or function that takes in path and concatenates.
Just to add that typically when the master page requires data, I like to use Html.Action, but this is not possible in the case of these URLs that are used throughout the master page.
Thoughts?
(5) create an htmlhelper that takes in the path and internally reads the appsetting and concatenates.
I would go with this one. Your custom HTML helper could look something like this:
<%= Html.ExternalActionLink(
"link text",
new { path = "/foo/bar.php" }
new { param1 = "value1", param2 = "value2" }
) %>
and could emit the following HTML:
link text
What I have done in the past is use viewdata/viewbag in my master page and populate its values in my base controller. The base controller in turn called another class to do the work of reading the values from the web.config.
This way the view is pretty clean (e.g. it does not contain code to read appsettings) and I don't need to create a base view model that matches all my views that use the master.
This approach has the disadvantage that uses a viewdata/viewbag but I decided that was OK in my case and extremely easy to implement.
Say there's HomeController with an Details-action. return View() will send data to the Detals.aspx in the Home folder. But who makes that binding? and what if I want it to go to Edit.aspx instead?
Background:
Alot of the code in Details.aspx and Edit.aspx is identical, save for one textbox. Maybe by MVC rigor, the view is not supposed to make that kind of decisions, but hey, there's got to be a limit.
You can make it go to Edit.aspx by specifying it as a parameter of the View() function.
return View("Edit");
As to who makes the actual binding happen, it's the View Engine. It receives the returned ViewResult and analyzes it to see which template file to load and display. When it gets the string "Edit", it runs a find routine, using the context of the controller, to search a number of directories for filenames that match the convention. It starts in the controller's View directory, and then searches the Shared directory.
If you want to Edit.aspx to be rendered you could return View("Edit");
I have the requirement to support themeing of my site's pages. The way I am doing this is by dynamically choosing a master page based on the current theme.
I have setup a directory structure like so
/shared/masterpages/theme1/Master1.master
/shared/masterpages/theme1/Master2.master
/shared/masterpages/theme1/Master3.master
/shared/masterpages/theme2/Master1.master
/shared/masterpages/theme2/Master2.master
/shared/masterpages/theme2/Master3.master
And I am still using the page directive in the view
<%# Page Title="" Language="C#" MasterPageFile="~/Views/shared/masterpages/theme1/Master1.Master"%>
I would still like to leverage the view's MasterPageFile property and just change the theme directory.
I can only think of three ways to do this none of them which sound great.
Create a custom BaseView class that uses OnPreInit to change the theme like this
Create some xml file or database table that links each view to a master page file and then set this in the controller.
Build some tool that reads all the views and parses them for their masterpagefile, (similar to 2 but could be done at run time potentially.)
Option 1 seems the best option to me so far. Does anyone else have any thoughts on how to do this?
Updated suggestion:
Since my original suggestion didn't work out as I had expected, here's a possible way to work around it, while still keeping your action methods clean, and minimizing repetition of code:
Create an ActionResult that adds the master name/theme name/whatever info you need to pick the correct master page into ViewData["masterInfo"] (or something similar).
Create a base class which themeable views inherit. Your base class should, of course, inherit from System.Web.Mvc.ViewPage. If you need, also create a generic version that inherits from .ViewPage<T>.
In your base class, create a construction method that selects the correct master page based on ViewData["masterInfo"]. I'm not sure if there's a need or not, but don't forget to run the base constructor, either before or after your code, if there is one that needs to run.
Decorate all relevant actions with the attribute, and set their views to inherit your base class instead of System.Web.Mvc.ViewPage.
Original post:
Why not have an ActionFilter, that can be applied on controller level, that sets the MasterPageFile property of the view? If you override OnActionExecuted, it shouldn't be too tricky to test if the result was a ViewResult and in that case change the property to the correct value.
Take the example of wanting to have a "Latest news items" sidebar on every page of your ASP.NET MVC web site. I have a NewsItemController which is fine for pages dedicating their attention to NewsItems. What about having a news sidebar appear on the HomeController for the home page though? Or any other controller for that matter?
My first instinct is to put the logic for selecting top 5 NewsItems in a user control which is then called in the Master Page. That way every page gets a news sidebar without having to contaminate any of the other controllers with NewsItem logic. This then means putting logic in what I understood to be the presentation layer which would normally go in a Controller.
I can think of about half a dozen different ways to approach it but none of them seem 'right' in terms of separation of concerns and other related buzz-words.
I think you should consider putting it in your master page. Your controller can gather data (asynchronously, of course), store it in a nice ViewModel property for your view (or in TempData) and then you can call RenderPartial() in your master page to render the data.
The keeps everything "separate"
http://eduncan911.com/blog/html-renderaction-for-asp-net-mvc-1-0.aspx
This seems to address the question - even using the instance of a sidebar - but using a feature not included with MVC 1 by default.
http://blogs.intesoft.net/post/2009/02/renderaction-versus-renderpartial-aspnet-mvc.aspx
This also indicates the answer lies in RenderAction.
For anyone else interested, here's how I ended up doing it. Note you'll need to the MVC Futures assembly for RenderAction.
Basically you'd have something like this in your controller:
public class PostController
{
//...
public ActionResult SidebarBox()
{
// I use a repository pattern to get records
// Just replace it with whatever you use
return View(repoArticles.GetAllArticles().Take(5).ToList());
}
//...
}
Then create a partial view for SidebarBox with the content you want displayed, and in your Master Page (or wherever you want to display it) you'd use:
<% Html.RenderAction<PostController>(c => c.SidebarBox()); %>
Not so hard after all.
You can create a user control (.ascx) and then call RenderPartial().
Design a method in your controller with JsonResult as return type. Use it along with jQuery.
Use RenderAction() as suggested by elsewhere.
News section with ASP.NET MVC