Trying to create a strongly typed master page with multi level navigation and would love to hear your opinion.
i'm using the sample recommended by MS here:
http://www.asp.net/mvc/tutorials/passing-data-to-view-master-pages-vb
so i have an ApplicationController that gets all the categories and all the other controller inherits it. and it return a LIST and stores it in ViewData["Nav"]
The master page as a partial view that gets the NAV model and creates the menu.
The roues for the category
Category/{CategoryId}/{CategoryName}/{Page}
The question is how can i display the selected category or sub category as selected when i renders it inside the partialView.
I see some options:
1. Create another property in the applicatin controller :
public class CategoryController : AppliactionController
{
//
// GET: /Category/
public ActionResult Index(string categoryId, string categoryName, int page)
{
base.ActiveCategoryId=int.parse(categoryId);
return View();
}
Check the current action URL in the partial view when creating the menu and set the category as selected if it produces the same action URL (not sure if i can get the categoryid from the action)
Any suggestions?
If you're going to use the Master Controller pattern, then option 1 is a suitable solution. For even better separation, you may consider moving that logic in the action into an action filter.
I would avoid option 2 completely, because you don't want that kind of logic in your view.
A third option you can try is to not use the Master Controller pattern, and instead set up your view to call RenderActions on stuff that is orthogonal to the view's main concern. In this case, your view would look something like Html.RenderAction("Menu", Model.CurrentCategoryId)
Regarding your seggestion:
"you may consider moving that logic in the action into an action filter."
I could do that but is it possible to access the controller case controller from the action filter?
should it be something like
public void OnActionExecuting(ActionExecutingContext filterContext)
{
((AppController)filterContext.Controller.base)).ActiveCategoryId=int.parse( filterContext.ActionParameters["CategoryId"])
Didn't check the code, just wanted to hear your thoughts, is this what you suggested?
Thanks
Related
In my HomeController.cs I have some data, which I need to pass to my /Shared/_Layout.cshtml via ViewBag. But I have no idea, how can I do it.
This is my /Shared/_Layout.cshtml
#foreach (var item in ViewBag.Order)
{
<li>#item.Name [ #item.Count ]</li>
}
And here is HomeController.cs
public ActionResult Index()
{
ViewBag.Order = SELECT FROM DB -> ADD TO LIST
}
You don't have access to the Viewbag in the Layout View unfortunately.
You could however use:
PartialView call to a method in a base controller
Simply define a base controller that is a parent for all your controllers. This can also be handy for some error handling by the way.
And in your layout use #Html.Partial("ViewName") to call your base controller.
Use Ajax call
Use javascript in your layout view to execute a controller function that returns the data you need.
Use Session variables instead
since session variables are accesable in layout views.
There are probably more answers too, but I believe these will probably be the most common solutions to your problem.
(If you need any help implementing one of these solutions please give me a sign and I'll explain it more deeply how to do it.)
A fairly common method of passing content to the layout, is to let all ViewModel classes implement a base class (or interface), which will then be the #model of your _Layout view.
Assuming you want to pass on some text MyText to all of your pages:
// My base view model class
public class ViewModelBase {
public string MyText { get; set; }
}
// Some other view model
public class MyOtherViewModel : ViewModelBase {
// other properties
}
// In the _Layout view, implement the base class
#model ViewModelBase
...
#Html.DisplayFor(m => m.MyText)
...
#RenderBody()
...
This way, your _Layout view can work with all the properties of the ViewModelbase class, while whatever view is rendered after that still will have the properties of their child view model - here MyOtherViewModel - available.
Hope that helps!
On a side note, I would not recommend an extended use of ViewBags for passing data to your view in MVC, simply because it has a very low maintainability compared to other methods, due to it not being strongly typed. In my opition, ViewBags does not have any real benefits compared to - for instance - using viewmodels.
As an alternative, you can create a base controller inheriting from the controller and assigning the ViewBag variable to your list in the base controller constructor, like so
public class BaseController : Controller {
public BaseController(){
ViewBag.Order = SELECT FROM DB -> ADD TO LIST
}
}
Then in other controllers, inherit from the base controller like so
public class HomeController : BaseController {
}
This then allows you to assign the view bag for each controller request and the view bag variable will be available in the Layout View.
I myself have used this technique to show a list of car makes that is shown throughout the website.
As a suggestion though, I would implement some form of caching, to save constant calls to the back end.
However if you want to keep the functionality in the home controller you will need to restructure it so that it can be used via an Ajax call using jQuery/Javascript as #counterflux has suggested in their second point
In MVC 5 I am attempting to use the controller to render a partial view only if the (Windows Authenticated) user belongs to one or more of a list of Active Directory groups. There are over 30 distinct groups I need to account for, so the "hello world" examples don't fit my needs. After playing scavenger hunt on the web, I managed to collect this much. No compile or runtime errors, but the content is showing for all users rather than the specific users. So the desired outcome is not yet achieved.
While I can achieve the desired outcome using if-then logic in the view, it creates a lot of unnecessary duplication and encourages spaghettification. So I'm trying to do this in the controller.
Summary of Desired Outcome:
When the user loads the viewpage, the partial view should only render if the Windows Authenticated user belongs to one or more of a list of groups defined in the controller action. If the user is not authorized, then the partial view is not included.
Controller Block:
[ChildActionOnly]
[Authorize(Roles="Domain\\GroupA,Domain\\GroupB")]
public ActionResult MonitorCSU()
{
return PartialView("MonitorCSU");
}
View Block:
<div class="rowWithCols3">
#Html.Partial("MonitorCSU")
Unsuccessful Iterations:
In the controller block I tried (unsuccessfully) to use an if-then block, the else case being another partial view with no content.
[ChildActionOnly]
public ActionResult MonitorCSU()
{
if (User.IsInRole("Domain\GroupA")) {
return PartialView("_MonitorCSU");
}
else
{
return PartialView("_Unauthorized");
}
}
In Razor, I tried using HTML.Action but when I tried run the page the browser hung in an infinite loop.
#Html.Partial() returns a partial view without calling a controller method. In order to call your controller method, you need to use
#Html.Action("MonitorCSU")
or
#{ Html.RenderAction("MonitorCSU") }
Note this assumes that the MonitorCSU() method is in the same controller as the method that generates the main view (other wise you also need to include a parameter for the controller name)
Refer documentation
While you've found a solution, you're going to have other problems with it. I would suggest a different approach, which is to use EditorTemplates and create a separate model for the html you want to render. Then, at runtime you would check whether the user is in the groups you specify, and if they are, you create an instance of the model, and if they are not you leave the model null. In this way, when the view is rendered with EditorFor(), it will ignore and not render the template for users who do not have access.
Thanks to #Stephen Muecke and and a commenter whose entry has mysteriously vanished, I have the missing pieces.
I was able to test this code with several real users and verified the desired behavior happens consistently.
Controller Block:
Main difference: take out authorization and use an if-then block send one of two partial views.
[ChildActionOnly]
public ActionResult MonitorCSU()
{
if (User.IsInRole("DOMAIN\\GroupA"))
{
return PartialView("MonitorCSU");
}
else
{
return PartialView("Unauthorized");
// this is an empty page
}
}
View Block:
The key difference is using HTML.Action
<div class="rowWithCols3">
#Html.Action("MonitorCSU")
From the sounds of it, it literally is a boolean value of whether or not the action is a child action.
I see this bit of code quite often:
protected override void OnActionExecuting(ActionExecutingContext filterContext) {
if (filterContext.IsChildAction) return;
...
}
It appears to be there to "throttle" unnecessary code execution... but what does filterContext.IsChildAction actually mean?
In view pages, you may often need to inject output of another action into current page - for example, injecting menus. Menu generation may involve lots of business logic (determining rights or users, choosing selected item, etc), so it is not done in the partial view, but in controller.
public class MenuController : Controller
{
[ChildActionOnly]
public ActionResult Menu()
{
MenuViewModel model = GenerateMenu();
return View(model);
}
}
This type of action is called ChildAction, as it cannot(and is not supposed to) be called from outside world(by visiting url). This may only be called by application itself, generally from within the view page.
#Html.Action("Menu", "Menu")
And if you wish(or do not wish) to do some specific stuff when the action being executed is a child action, you inspect filterContext.IsChildAction property.
Maybe it's too late to point out but the accepted answer is slightly misleading, in the sense that:
action marked with ChildActionOnlyAttribute absolutely cannot be run as standalone actions and so it is pointless to test with IsChildAction.
On the other hand if your action is called in two ways
as a regular action
as a child action from within another action
It can be useful to check for IsChildAction so that you can execute additional logic based on the value.
So in order accomplish what I asked in this post I did the following:
[iPhone]
[ActionName("Index")]
public ActionResult IndexIPhone()
{
return new Test.Areas.Mobile.Controllers.HomeController().Index();
}
[ActionName("Index")]
public ActionResult Index()
{
return View();
}
Which still serves the same view as the Index action method in this controller. Even though I can see it executing the Test.Areas.Mobile.Controllers.HomeController().Index() action method just fine. What's going on here? And how do I serve the Index view from Mobile area without changing the request URL (as asked in the original post referenced above)?
You have a few options:
Redirect to the Action you'd like to return: return RedirectToAction("Action-I-Want").
Return the View by name: return View("The-View-I-Want").
Note that with the 2nd approach you'd have to put your view in the "Shared" folder for all controllers to be able to find it and return it. This can get messy if you end up putting all your views there.
As a side note: The reason your work doesn't find the view is because default view engine looks for the view in the folder that "belongs" to the current executing controller context, regardless of what code you're calling.
Edit:
It is possible to group all "mobile" views in the same folder. On your Global.asax (or where ever you're setting up your ViewEngine, just add the path to your mobile View in the AreaViewLocationFormats. Mind you, you'll still have to name your views differently.
You can also write your own view engine. I'd do something like detecting the browser and then serving the right file. You could setup a convention like View.aspx, and View.m.aspx.
Anyhow, just take a look at WebFormViewEngine and you'll figure out what works best for you.
The easiest way to send a request to a view handled by another controller is RedirectToAction("View-Name", "Controller-Name").
There are overloads of View() that take route information that might work as well, but they'd require more effort to set up.
Well actually the easiest way is to make one version of your site programmed on standards instead of browser detection :D -- however in direct response to accomplish what it in a more of a ASP.NET mvc fashion, using:
RedirectToAction("ViewName", "ControllerName");
is a good method however I have found it is more practical if you feel you must program for different browser standards to create a primary view and an alternate "mobile" view under your controllers views. Then instead of writing special code on every controller, instead extend the controller like so.
public class ControllerExtended : Controller
{
private bool IsMobile = false;
private void DetectMobileDevices(){ .... }
}
Then modify your controller classes to instead say ControllerExtended classes and just add the one line to the top of each Action that you have alternate views of like so:
public class ApplicationsController : ControllerExtended
{
// GET: /Applications/Index
public ActionResult Index() {
this.DetectMobileDevices();
if(this.IsMobile){
return RedirectToAction("MobileIndex");
} else {
// actual action code goes here
return View();
}
}
}
Alternately you can use return View("ViewName"); but from my experience you want to actually perform different actions as opposed to just showing the result in a different view as in the case of presenting an HTML table as opposed to a Flex table to help iPhone users since there is no flash support in the iPhone, etc. (as of this writing)
Using MVC out of the box I found the generated URLs can be misleading and I wanted to know if this can be fixed or if my approach/understanding is wrong.
Suppose I have a CreateEgg page, which has a form on it, and once the form is filled in and submitted the user is taken to a ListEggs page with the new egg in it.
So my egg controller will look some thing like this:
public class EggController : Controller
{
public void Add()
{
//do stuff
RenderView("CreateEgg", viewData);
}
public void Create()
{
//do stuff
RenderView("ListEggs", viewData);
}
}
So my first page will have a url of something like http://localhost/egg/add and the form on the page will have an action of:
using (Html.Form<EggController>(c => c.Create())
Meaning the second page will have a url of http://localhost/Egg/Create, to me this is misleading, the action should be called Create, because im creating the egg, but a list view is being displayed so the url of http://localhost/Egg/List would make more scene. How do I achieve this without making my view or action names misleading?
The problem is your action does two things, violating the Single Responsibility Principle.
If your Create action redirects to the List action when it's done creating the item, then this problem disappears.
How A Method Becomes An Action by Phil Haack
ActionVerbs Outlined in Scott Gu's post seem to be a good approch;
Scott says:
You can create overloaded
implementations of action methods, and
use a new [AcceptVerbs] attribute to
have ASP.NET MVC filter how they are
dispatched. For example, below we can
declare two Create action methods -
one that will be called in GET
scenarios, and one that will be called
in POST scenarios
[AcceptVerbs("GET")]
public object Create() {}
[AcceptVerbs("POST")]
public object Create(string productName, Decimal unitPrice) {}