MVC SiteMap - when different nodes point to same action SiteMap.CurrentNode does not map to the correct route - asp.net-mvc-4

Setup:
I am using ASP.NET MVC 4, with mvcSiteMapProvider to manage my menus.
I have a custom menu builder that evaluates whether a node is on the current branch (ie, if the SiteMap.CurrentNode is either the CurrentNode or the CurrentNode is nested under it). The code is included below, but essentially checks the url of each node and compares it with the url of the currentnode, up through the currentnodes "family tree".
The CurrentBranch is used by my custom menu builder to add a class that highlights menu items on the CurrentBranch.
The Problem:
My custom menu works fine, but I have found that the mvcSiteMapProvider does not seem to evaluate the url of the CurrentNode in a consistent manner:
When two nodes point to the same action and are distinguished only by a parameter of the action, SiteMap.CurrentNode does not seem to use the correct route (it ignores the distinguishing parameter and defaults to the first route that that maps to the action defined in the node).
Example of the Problem:
In an app I have Members.
A Member has a MemberStatus field that can be "Unprocessed", "Active" or "Inactive". To change the MemberStatus, I have a ProcessMemberController in an Area called Admin. The processing is done using the Process action on the ProcessMemberController.
My mvcSiteMap has two nodes that BOTH map to the Process action. The only difference between them is the alternate parameter (such are my client's domain semantics), that in one case has a value of "Processed" and in the other "Unprocessed":
Nodes:
<mvcSiteMapNode title="Process" area="Admin" controller="ProcessMembers" action="Process" alternate="Unprocessed" />
<mvcSiteMapNode title="Change Status" area="Admin" controller="ProcessMembers" action="Process" alternate="Processed" />
Routes:
The corresponding routes to these two nodes are (again, the only thing that distinguishes them is the value of the alternate parameter):
context.MapRoute(
"Process_New_Members",
"Admin/Unprocessed/Process/{MemberId}",
new { controller = "ProcessMembers",
action = "Process",
alternate="Unprocessed",
MemberId = UrlParameter.Optional }
);
context.MapRoute(
"Change_Status_Old_Members",
"Admin/Members/Status/Change/{MemberId}",
new { controller = "ProcessMembers",
action = "Process",
alternate="Processed",
MemberId = UrlParameter.Optional }
);
What works:
The Html.ActionLink helper uses the routes and produces the urls I expect:
#Html.ActionLink("Process", MVC.Admin.ProcessMembers.Process(item.MemberId, "Unprocessed")
// Output (alternate="Unprocessed" and item.MemberId = 12): Admin/Unprocessed/Process/12
#Html.ActionLink("Status", MVC.Admin.ProcessMembers.Process(item.MemberId, "Processed")
// Output (alternate="Processed" and item.MemberId = 23): Admin/Members/Status/Change/23
In both cases the output is correct and as I expect.
What doesn't work:
Let's say my request involves the second option, ie, /Admin/Members/Status/Change/47, corresponding to alternate = "Processed" and a MemberId of 47.
Debugging my static CurrentBranch property (see below), I find that SiteMap.CurrentNode shows:
PreviousSibling: null
Provider: {MvcSiteMapProvider.DefaultSiteMapProvider}
ReadOnly: false
ResourceKey: ""
Roles: Count = 0
RootNode: {Home}
Title: "Process"
Url: "/Admin/Unprocessed/Process/47"
Ie, for a request url of /Admin/Members/Status/Change/47, SiteMap.CurrentNode.Url evaluates to /Admin/Unprocessed/Process/47. Ie, it is ignorning the alternate parameter and using the wrong route.
CurrentBranch Static Property:
/// <summary>
/// ReadOnly. Gets the Branch of the Site Map that holds the SiteMap.CurrentNode
/// </summary>
public static List<SiteMapNode> CurrentBranch
{
get
{
List<SiteMapNode> currentBranch = null;
if (currentBranch == null)
{
SiteMapNode cn = SiteMap.CurrentNode;
SiteMapNode n = cn;
List<SiteMapNode> ln = new List<SiteMapNode>();
if (cn != null)
{
while (n != null && n.Url != SiteMap.RootNode.Url)
{
// I don't need to check for n.ParentNode == null
// because cn != null && n != SiteMap.RootNode
ln.Add(n);
n = n.ParentNode;
}
// the while loop excludes the root node, so add it here
// I could add n, that should now be equal to SiteMap.RootNode, but this is clearer
ln.Add(SiteMap.RootNode);
// The nodes were added in reverse order, from the CurrentNode up, so reverse them.
ln.Reverse();
}
currentBranch = ln;
}
return currentBranch;
}
}
The Question:
What am I doing wrong?
The routes are interpreted by Html.ActionLlink as I expect, but are not evaluated by SiteMap.CurrentNode as I expect. In other words, in evaluating my routes, SiteMap.CurrentNode ignores the distinguishing alternate parameter.

I think it is because you are trying to obtain the route FROM the parameters. Basically, MVC is just trying to GUESS what route you could be referring to.
The correct way would be to handle routes by name. So the sitemap should reference to a Route name rather than the Controller, action, etc.

Addendum - I've got it to work!!:
The big stumbling block:
When using different routes to point at the same controller action, so that one has different nodes, the big problem I had was the following:
YOU MUST GIVE THE DIFFERENT NODES A KEY!!! OTHERWISE THE SITEMAP WON'T RENDER!!
EG, just the route attribute on its own is NOT enough and you MUST give each node that point to the same action a UNIQUE key to distinguish them from each other:
<mvcSiteMapNode title="Edit Staff" area="Admin" controller="EditStaff" route="Admin_AdministratorDetails" action="Start" key="administrators_details" />
<mvcSiteMapNode title="Edit Staff" area="Admin" controller="EditStaff" route="Admin_StaffDetails" action="Start" key="staff_details" />
Once I realised that, it is now plain sailing. Otherwise, all is obscure and obtuse.
NOTE:
In the question, the action that was getting called by different routes was the Process action. I changed that to calls to different actions. But when editing my objects (Solicitors), I couldn't do that, as the editing is done by an MVC Wizard that, like my custom menu builder, I have written and works very well. It just simply wasn't possible (or rather, DRY) to recreate the wizard three times. So I HAD to get my menu higlighting working correctly with different routes pointing at the same action.
No Un-DRY fudges will do. My client doesn't deserve it.
The Solution (see NOTE as to why the actions and routes are different to the question):
The suggestion by Carlos Martinez works, but you need to use Html.RouteLink as opposed to Html.ActionLink, in conjunction with the edited sitemap, that details the routes.
Essentially, in your nodes, you need to use the route attribute:
<mvcSiteMapNode title="Details Active Solicitor" area="Solicitors" controller="EditSolicitor" action="Start" route="Active_Details" key="company_activeSolicitors_details"/>
Then, in your views, instead of action links, you use the RouteLink helper:
#Html.RouteLink("Details", "Active_Details", new { action="Start", controller="EditSolicitor", idSolicitor = item.SolicitorId, returnUrl = Request.RawUrl })
In your route registration file, you can now write routes that call the same action:
context.MapRoute(
"Unprocessed_Details",
"Unprocessed/Edit/{idSolicitor}/{action}",
new { action = "Start", controller = "EditSolicitor", idSolicitor = UrlParameter.Optional }
);
context.MapRoute(
"Inactive_Details",
"Inactive/Edit/{idSolicitor}/{action}",
new { controller = "EditSolicitor", action = "Start", idSolicitor = UrlParameter.Optional }
);
context.MapRoute(
"Active_Details",
"Solicitors/Edit/{idSolicitor}/{action}",
new { controller = "EditSolicitor", action = "Start", idSolicitor = UrlParameter.Optional }
);
As you can see, it is the exact same action that is getting called by the three routes. So long as I specify the route name in the mvcSiteMapNode, the routes are correctly distinguished when my menu is built and the highlighting works as required.
Note on getting befuddled with XML:
I hate XML, deeply and truly. It boggles and befuddles me, especially when I have a cold.
The problem being that adding the route attribute to the mvcSiteMapNodes adds to the potential for befuddlement.
And befuddled I got
I had initially tried Carlos' suggestion, but it didn't work. Not sure what the error was, but went through it with a tooth comb and it is now working. The annoying thing is that I am not sure what I was doing wrong.
Living in Hope:
I hope this documents an albeit fringe aspect of mvcSiteMapNode.

Just in case, can you try the following:
context.MapRoute(
"Process_New_Members",
"Admin/{alternate}/Process/{MemberId}",
new { controller = "ProcessMembers",
action = "Process",
alternate="Unprocessed",
MemberId = UrlParameter.Optional }
);
This way you will be distinguishing between each route by requiring this additional parameter.

You can use url in your mvcSiteMapNode. like below:
<mvcSiteMapNode title="Process" area="Admin" controller="ProcessMembers" action="Process" aurl="/Admin/ProcessMembers/Process/Unprocessed"/>
<mvcSiteMapNode title="Change Status" area="Admin" controller="ProcessMembers" action="Process" url="/Admin/ProcessMembers/Process/Processed" />
Of course, you should use the appropriate RoutConfig for the url to work.
there is a sample here.

Related

Pass values to route

I have a list of items. When the user clicks on an item, the user will be taken to item details page.
I want to pass an object containing item details(like item's image URL) to the route. However, I don't want to expose it in the routes url.
If there were a way to do something like <a route-href="route: details; settings.bind({url: item.url})">${item.name}</a> that would be gold.
I have seen properties can be passed to a route if defined in the route configuration. However, I don't know how to change that from the template. Another way could be is to define a singleton and store the values there and inject the object to the destination route.
Is there a way to pass values to routes from view (like angular ui-routers param object)?
Okay so I figured out a way to achieve something closer to what I wanted:
Objective: Pass data to route without exposing them in the location bar.
Let's say, we have a list of users and we want to pass the username to the user's profile page without defining it as a query parameter.
In the view-model, first inject Router and then add data to the destination router:
goToUser(username) {
let userprofile = this.router.routes.find(x => x.name === 'userprofile');
userprofile.name = username;
this.router.navigateToRoute('userprofile');
}
Now when the route changes to userprofile, you can access the route settings as the second parameter of activate method:
activate(params, routeData) {
console.log(routeData.name); //user name
}
For those #Sayem's answer didn't worked, you can put any additional data (even objects) into setting property like this:
let editEmployeeRoute = this.router.routes.find(x => x.name === 'employees/edit');
editEmployeeRoute.settings.editObject = employeeToEdit;
this.router.navigateToRoute('employees/edit', {id: employeeToEdit.id});
So editObject will be delivered on the other side:
activate(params, routeConfig, navigationInstruction) {
console.log(params, routeConfig, navigationInstruction);
this.editId = params.id;
this.editObject = routeConfig.settings.editObject;
}
hopes this helps others encountering same problem as me. TG.

how to define action link to action from controller in an area in asp.net mvc

I have an area named customer. it contains a controller named customer with following action
public ActionResult BrowseByCategory(int id=0)
{
//some code here............
return View();
}
I need to create a link on for above action on the _layout.cshtml view on the root.
I have written following markup. but its not working
#Html.ActionLink(c.CategoryName, "BrowseByCategory", "Customer", new { area = "customer" },new { id = c.CategoryCode })
Please suggest the change.
There is an overload of ActionLink method that can be used in your case like so:
#Html.ActionLink(c.CategoryName, "BrowseByCategory", new { area = "customer", controller = "Customer", id = c.CategoryCode }, new { #* There should be htmlParameters *# })
You can't reference a model in an action in something as global as _Layout.cshtml, or at least you shouldn't. Technically, you could define a model for _Layout.cshtml, but then every... single.. view that uses this layout could only work with that same model. If you passed a different class type, you'd get runtime errors from your layout.
In your particular situation here, it seems that the link necessary in the layout is conditional on what category is currently being viewed. The best way to handle this, then, would be using sections.
In your layout, you can define a section like:
#RenderSection("CustomLink", required: false)
Just put that wherever you want the link to appear. Making it not required means you won't get runtime errors if some other view doesn't need this custom link.
Then, in your view:
#section CustomLink
{
#Html.ActionLink(c.CategoryName, "BrowseByCategory", "Customer", new { area = "customer" },new { id = c.CategoryCode })
}
Now, this will appear where you want in your layout, but you can define it at the view level.

Correct Route Patterns in MVC4

I Have a calendar at www.server.com/events/calendar. My event query string looks like www.server.com/events/calendar/seofriendly-event-query-string. But users can select the events by year and months using drop down lists, so mu query becomes www.server.com/events/calendar/2013 or even www.server.com/events/calendar/2013/12. So the problem is when I click the www.server.com/events/calendar/seofriendly-event-query-string I got www.server.com/events/calendar. How to arrange my routes to make them understand what I need to show: list or event?
well I would add a custom route like this:
routes.MapRoute(
"NewRoute", // Route name
"{controller}/{action}/{id}/{another_id}", // URL with parameters
new { controller = "Events", action = "Calendar", id = UrlParameter.Optional, another_id = UrlParameter.Optional } // Parameter defaults
);
Your controller would then have an action method like this:
public ActionResult MyAction(string id, string another_id)
{
// in the question you mentioned that a a valid list querystring would contain
// multiple integer parameters, and an event querystring would include a
//seo friendly string
int para;
if (int.TryParse(id,out para))
{
// show list view
}
else
{
//show event view
}
}
you just need to recieve the parameters and run some kind of check to determine if you are going to show an event or a list.

Allow Administrators to impersonate users using an iframe

I have an MVC project with three roles: Users, Account Managers, and Administrators.
Administrators have their own MVC Area where they have full control over Users and Account Managers. I'm trying to implement functionality to allow Administrators to view the site as any User or Account Manager.
In the Admin Area of the site, I have a View of a list of Users and Account Managers. The list contains a "View Site As User" button for each record.
I have never done anything like this before, but the ViewAs Controller Action is currently set up to create a Session with the selected User's information, like so:
ViewBag.SiteSession = Session["SiteSession"] = new SiteSession()
{
ID = user.ID,
AccountID = user.AccountID,
DisplayName = user.DisplayName,
IsManager = user.IsAdmin,
IsAdmin = false
};
The View relevant to this Action has the Model defined as a string, and nothing else but an iframe with the Model as the src attribute, like so:
#model string
<iframe src="#Model" >
</iframe>
What I'm trying to do is render whichever portion of the site was requested in this iframe. When an Administrator clicks "View As User," I'd like to direct to Home. The URL is generated through this call:
Url.Action("Index", "Home", new { Area = "" }));
The Area is set to nothing to avoid rendering the Admin Area's Home.
Currently, this is not working. I don't know where to even begin, minus what I already have.
I'm looking for any suggestions. All help is greatly appreciated, as this doesn't seem like an easy task.
If you don't know how to help, it would also be appreciated if you could direct this question to somebody that can.
Again, thanks in advance.
The way that I've done this in the past has been to use the concept of an an actual user and an effective user. Most display actions use the effective user to generate their content. Typically I've implemented it as "impersonation" rather than "preview" so the user is actually navigating the site as the user rather than displaying in a separate window. In this case I simply set both in the current session. Things that require admin permission (like switching to/from impersonation) obviously use the real user.
If you wanted to do preview then I'd think about using a parameter on each request to set the effective user. The code would need to understand to add this parameter to all links so that you could navigate in the iframe without messing up navigation in the original interface.
As for removing the area from the url, I think what you have (setting to the empty string) should work. If it's not working you might want to try lowercase area, Url.Action("Index", "Home", new { area = "" }). I'm pretty sure that the RouteValueDictionary that gets created under the hood uses a case insensitive key comparison, though, so it shouldn't matter.
For this task, I ended up creating a separate controller, ViewAsController, which had a controller-wide [Authorize] attribute that only allowed users with the Admin role to access its actions.
In the Start action, a Session object containing the selected User's information is created, like so:
[HttpGet]
public ActionResult Start(int id)
{
var user = db.Users
.First(u => u.ID == id);
Session["SiteSession"] = new SiteSession()
{
//Session data...
};
return PartialView("_IFrame");
}
This Action returns a Partial View that I ended up displaying in a jQuery UI modal dialog window.
Here's the code for that Partial View:
#{
ViewBag.SiteSession = (SiteSession)Session["SiteSession"];
}
<h2>Viewing Site As #ViewBag.SiteSession.DisplayName</h2>
<div>
<iframe src="#Url.Action("Index", "Home", new { Area = "" })"></iframe>
</div>
As you can see, it's extremely bare, and that's exactly what it needs to be. The <iframe> acts as a browser in a browser, allowing the Admin user full access to whichever Actions the selected User would.
For the sake of detail, here's the jQuery that creates the dialog and opens it:
$(function () {
$("#viewAsDialog").dialog({
modal: true,
autoOpen: false,
resizable: true,
draggable: true,
closeOnEscape: false,
height: $(window).height() * .9,
width: 1000,
closeText: '',
close: function () {
$.post("#Url.Action("End", "ViewAs", new { Area = "Admin" })")
.success(function (result) {
});
}
});
});
function viewAs(result) {
$("#viewAsDialog").html(result);
$("#viewAsDialog").dialog("open");
}
You can see here that the dialog is initialized on document-ready, and is not opened until the AJAX call that retrieves the Partial View is successfully completed.
Once the Admin closes the dialog, the server calls the End action in the ViewAs Controller, destroying the session:
[HttpPost]
public ActionResult End()
{
Session["SiteSession"] = null;
return new HttpStatusCodeResult(System.Net.HttpStatusCode.OK);
}

Dynamically Adding / Removing Route in Durandal Router when application is loaded

I need help in dynamically adding/removing route in Durandal Router. What I want is after user is logged in then I would be able to add or remove specific route depending upon logged in user's type.
I tried to add/remove route from visibleRoutes/allRoutes array ... but get binding exception from knockout library...
I was hoping it would be common scenario... but still couldn't find any solution ... please help me in fixing this issue.
Thanks.
Wasim
POST COMMENTS:
I tried this function to dynamically hide/show route... and similary tried to add/remove route from allRoutes[] ... but then get exception on knockout bidning
showHideRoute: function (url,show) {
var routeFounded = false;
var theRoute = null;
$(allRoutes()).each(function (route) {
if (url === this.url) {
routeFounded = true;
var rt = this;
theRoute = rt;
return false;
}
});
if (routeFounded)
{
if (show)
{
visibleRoutes.push(theRoute);
}
else
{
visibleRoutes.remove(theRoute);
}
}
}
In Durandal 2.0.
You can enumerate the routes to find the one you wish to show/hide.
Then change the value of: nav property
Then run buildNavigationModel();
here is an example:
// see if we need to show/hide 'flickr' in the routes
for (var index in router.routes) {
var route = router.routes[index];
if (route.route == 'flickr') {
if (vm.UserDetail().ShowFlickr) { // got from ajax call
// show the route
route.nav = true; // or 1 or 2 or 3 or 4; to have it at a specific order
} else if (route.nav != false) {
route.nav = false;
}
router.buildNavigationModel();
break;
}
}
Durandal 2.0 no longer has the method visibleRoutes. I found that the following works for me.
router.reset();
router.map([
{ route: 'home', moduleId: 'home/index', title: 'Welcome', nav: true },
{ route: 'flickr', moduleId: 'flickr/index', title: '', nav: true }
])
.buildNavigationModel()
.mapUnknownRoutes('home/index', 'not-found');
This removes all previous routes, if you want to maintain current routes you could try using the router.routes property to rebuild the array of routes.
I had a similar requirement. If I were you, I would take another approach. Rather than adding/removing routes when application loads - get the right routes to begin with per user type.
Two options, (I use both)
1) have a json service provide the proper routes per user type, this approach would be good if you need to 'protect/obscure' routes... i.e. I don't want the route referenced on any client resource.
2) A simpler solution see Durandal.js: change navigation options per area
You can have a settings property identify the user type.
I hope this helps.
I had a similar problem: First, router.visibleRoutes() is an observable array. In other words, when you change its value, the routes automatically update. However, the items in this array are not observable, so to make a change you need to replace the entire array and not just make a change to a single item in it.
So, all you have to do is find which item in this array you want to remove, and then create a new array without this item, and set router.visibleRoutes() to this new array.
If, for example, you find out the it is the 3rd item, then one way of doing it is:
router.visibleRoutes(router.visibleRoutes().splice(2, 1))
Note that splice() returns a new array where an item is removed. This new array is put into router.visibleRoutes.