MvcSiteMapProvider - Multiple Pages Need To Link to One Menu Node - asp.net-mvc

In my MVC3 project, I have installed Maartenba's MvcSiteMapProvider v.3.2.1 and I have a very simple, static, two-level menu that I have created. Below is the conceptual map structure.
- Home
- Member Center
- Member Listing [SELECTED]
- Event Calendar
- Documents
- Administration
Now, there are many sub-pages under Member Listing (e.g. Detail, Edit, etc.), but I don't want these displayed as 3rd level menu items (mainly because they are tied to a specific member ID). However, I do want all these third level pages to be "tied" to the Member Listing menu node so that it shows as selected when on these pages.
I have the following code in my Mvc.SiteMap file:
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<mvcSiteMapNode title="Member Center" area="Members" controller="Home" action="Index" roles="Approved Member" >
<mvcSiteMapNode title="Member Listing" area="Members" controller="Member" action="List" />
<mvcSiteMapNode title="Event Calendar" area="Members" controller="Event" action="List" />
<mvcSiteMapNode title="Documents" area="Members" controller="Document" action="List" />
</mvcSiteMapNode>
<mvcSiteMapNode title="Administration" area="Admin" controller="Home" action="Index" roles="Site Administrator" >
</mvcSiteMapNode>
</mvcSiteMapNode>
To render the menu, I am using the following code in my _Layout.cshtml file:
#Html.MvcSiteMap().Menu(1, true, true, 1, true, true)
Finally, I modified the SiteMapNodeModel.cshtml file so that it adds a "selectedMenuItem" class to the node that correlates to the page the user is viewing. Here's the snippit that renders the menu node.
#model SiteMapNodeModel
#Model.Title
The display and navigation of the map works just fine, until I navigate further into the members area. For example, if I go past Members/Member/List (which displays the menu correctly) and to a page like Members/Member/Detail/1, the child nodes under Member Center ("Member Listing", "Event Calendar", etc.) disappear. Therefore, here are my two issues that I have with my current code:
I want to specify that any given page is part of the "Member Center" parent menu node, so that the child menu nodes of "Member Center" will be displayed, regardless of whether the given page is defined as a specific node in the menu structure.
I want to specify (possibly in the view or controller action) that a specific page should be tied to a specific menu node. For example, when the user is at Members/Member/Detail/1, I simply want the "Member Listing" child node to be specified as IsCurrentNode so that the SiteMapNodeModel.cshtml file properly decorates it with the "selectedMenuItem" class.
Any suggestions?

You can add 3rd level nodes to the sitemap XML and specify visibility to hide them from the menu. Here is the node declaration to display it only in breadcrumbs:
<mvcSiteMapNode area="Members"
controller="Member"
action="Detail"
visibility="SiteMapPathHelper,!*"
title="Member details" />
Edit:
As far as I know, you can not set IsCurrentNode. But you can check if menu node is currently selected with the following code (I use it in SiteMapNodeModel display template):
IList<string> classes = new List<string> ();
if (Model.IsCurrentNode || Model.IsInCurrentPath && !Model.Children.Any ())
{
classes.Add ("menu-current");
}

Adding to Max's answer I would also create an extension method for SiteMapNodeModel. Which you could use to implement all the custom logic needed to do this:
public static class SiteMapNodeModelExtender
{
public static bool IsRealCurrentNode(this SiteMapNodeModel node)
{
// Logic to determine the "real" current node...
// A naive implementation could be:
var currentPath = HttpContext.Current.Request.Url.AbsolutePath;
return currentPath.StartsWith("Members/Member/") && node.Title.Equals("Member Center")
}
}
and change the display template accordingly:
/* Also check IsRealCurrentNode, depending on the use case maybe only
IsRealCurrentNode */
#if ((Model.IsCurrentNode || Model.IsRealCurrentNode()) && Model.SourceMetadata["HtmlHelper"].ToString() != "MvcSiteMapProvider.Web.Html.MenuHelper") {
<text>#Model.Title</text>
} else if (Model.IsClickable) {
#Model.Title
} else {
<text>#Model.Title</text>
}

Further to Max Kiselev's answer, if you want to use that technique but be able to use attributes on your controller actions, I did the following:
Define a custom visibility provider:
public class AlwaysInvisibleVisibilityProvider : ISiteMapNodeVisibilityProvider
{
public bool IsVisible(SiteMapNode node, HttpContext context, IDictionary<string, object> sourceMetadata)
{
return false;
}
}
Then subclass MvcSiteMapNodeAttribute:
public class InvisibleMvcSiteMapNodeAttribute : MvcSiteMapNodeAttribute
{
public InvisibleMvcSiteMapNodeAttribute(string key, string parentKey)
{
Key = key;
ParentKey = parentKey;
VisibilityProvider = typeof (AlwaysInvisibleVisibilityProvider).AssemblyQualifiedName;
}
}
And you can then use it on your controller actions:
[HttpGet]
[InvisibleMvcSiteMapNodeAttribute("ThisNodeKey", "ParentNodeKey")]
public ViewResult OrderTimeout()
{
return View("Timeout");
}

Related

How to send clicked item as parameter in an ListView item binding

I have a dynamic list and it is the source of a ListView.
I have a command in the ViewModel, where I need to receive the item, which was clicked. I want to get the object that represents that item, so I can perform a change on it.
I am using MvvmCross.
That is fairly trivial. I imagine you have a ListView in your layout which looks something like:
<Mvx.MvxListView
...
local:MvxBind="ItemsSource Items" />
To be able to get the clicked item you just add a command to ItemClick:
<Mvx.MvxListView
...
local:MvxBind="ItemsSource Items; ItemClick ItemClickedCommand" />
Then in your ViewModel you should have an ItemClickedCommand:
private MvxCommand<ItemViewModel> _itemClickedCommand;
public ICommand ItemClickedCommand
{
get
{
return _itemClickedCommand = _itemClickedCommand ??
new MvxCommand<ItemViewModel>(item => {
// do something with the item here
}):
}
}
Where ItemViewModel is the type of the items in the collection you have bound to the ItemsSource.

ASP.NET MVC Sitemap provider - non-dynamic node under dynamic node

I have a dynamic node provider which is copied below along with my site map configuration. When I go to my url, /Account/Edit/1475, the breadcrumb shows "Home > Accounts > [Incorrect Account Name] > Edit". It is showing an account name that is different then the 'accountId' in the url, 1475. I assume it is due to the 'preservedRouteParameter=accountId' that is causing it to match the wrong node. Is this right?
Do I need to create another DynamicNodeProvider for the Account Edit node in my sitemap? I started going down this path, but I needed to create a separate Dynamic Node Providers for most of the nodes so I thought I must be doing something wrong. Is there something I am missing in the configuration?
public class AccountDynamicNodeProvider : DynamicNodeProviderBase
{
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
using (var entities = new Entities())
{
foreach (var account in entities.TAccounts)
{
var dynamicNode = new DynamicNode("Account_" + account.AccountId, account.AccountName);
dynamicNode.RouteValues.Add("accountId", account.AccountId);
yield return dynamicNode;
}
}
}
}
Mvc.sitemap:
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<mvcSiteMapNode title="Accounts" controller="Account" action="Index">
<mvcSiteMapNode title="Detail" controller="Account" action="Details" dynamicNodeProvider="my.test.namespace.AccountDynamicNodeProvider, my.assembly">
<mvcSiteMapNode title="Edit" controller="Account" action="Edit" preservedRouteParameters="accountId" />
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMap>
This is the route that is being used:
routes.MapRoute(
name: "Account",
url: "Account/{action}/{accountId}",
defaults: new { controller = "Account", action = "Details" }
);
Use the SiteMapTitleAttribute to dynamically change the title when using preservedRouteParameters.
[SiteMapTitle("MyTitle", Target = AttributeTarget.ParentNode)]
public ViewResult Edit(int accountId) {
var account = _repository.Find(accountId);
// MyTitle is a string property of
// the account model object.
return View(account);
}
Or
[SiteMapTitle("MyTitle", Target = AttributeTarget.ParentNode)]
public ViewResult Edit(int accountId) {
ViewData["MyTitle"] = "This will be the title";
var account = _repository.Find(accountId);
return View(account);
}
In general, when configuring CRUD operations it is best to use preservedRouteParameters all the way. But going that route comes with the caveat that you need to fix the title and node visibility manually.
CRUD operations (other than perhaps Add New) generally don't appear in the Menu or SiteMap, instead a list or table is usually made on the page to dynamically generate commands for each record. So the only thing you typically have to worry about is the SiteMapPath, and for that you can use preservedRouteParameters.
Have a look at the Forcing a Match demo in How to Make MvcSiteMapProvider Remember a User's Position.

Globalization in MVCSiteMapProvider

Hi have a sitemap on my mvc 4 application like this:
<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0"
xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0 MvcSiteMapSchema.xsd">
<mvcSiteMapNode title="Users" controller="User" action="Index" area="" preservedRouteParameters="culture,projectid">
<mvcSiteMapNode title="New" controller="User" action="Create" area="" preservedRouteParameters="culture,projectid"/>
<mvcSiteMapNode title="Edit" controller="User" action="Edit" area="" preservedRouteParameters="culture,projectid,id"/>
<mvcSiteMapNode title="Profile" controller="User" action="Details" area="" preservedRouteParameters="culture,projectid,id"/>
</mvcSiteMapNode>
</mvcSiteMap>
I have few resources files in another project that i use for globalize my app, I need the resources files in a separate project because its used in few projects like ddl.
How can i implement globalization for my sitemap?
The approach I would take would be to switch to external DI and then implement a custom IStringLocalizer class that can read the resources from another assembly. Here is a working example. I have created a demo application on GitHub as well.
using System;
using System.Collections.Specialized;
using System.Resources;
namespace MvcSiteMapProvider.Globalization
{
public class ResourceManagerStringLocalizer
: IStringLocalizer
{
public ResourceManagerStringLocalizer(
ResourceManager resourceManager
)
{
if (resourceManager == null)
throw new ArgumentNullException("resourceManager");
this.resourceManager = resourceManager;
}
protected readonly ResourceManager resourceManager;
/// <summary>
/// Gets the localized text for the supplied attributeName.
/// </summary>
/// <param name="attributeName">The name of the attribute (as if it were in the original XML file).</param>
/// <param name="value">The current object's value of the attribute.</param>
/// <param name="enableLocalization">True if localization has been enabled, otherwise false.</param>
/// <param name="classKey">The resource key from the ISiteMap class.</param>
/// <param name="implicitResourceKey">The implicit resource key.</param>
/// <param name="explicitResourceKeys">A <see cref="T:System.Collections.Specialized.NameValueCollection"/> containing the explicit resource keys.</param>
/// <returns></returns>
public virtual string GetResourceString(string attributeName, string value, bool enableLocalization, string classKey, string implicitResourceKey, NameValueCollection explicitResourceKeys)
{
if (attributeName == null)
{
throw new ArgumentNullException("attributeName");
}
if (enableLocalization)
{
string result = string.Empty;
if (explicitResourceKeys != null)
{
string[] values = explicitResourceKeys.GetValues(attributeName);
if ((values == null) || (values.Length <= 1))
{
result = value;
}
else if (this.resourceManager.BaseName.Equals(values[0]))
{
try
{
result = this.resourceManager.GetString(values[1]);
}
catch (MissingManifestResourceException)
{
if (!string.IsNullOrEmpty(value))
{
result = value;
}
}
}
}
if (!string.IsNullOrEmpty(result))
{
return result;
}
}
if (!string.IsNullOrEmpty(value))
{
return value;
}
return string.Empty;
}
}
}
Then you can inject it into your DI configuration module (StructureMap example shown, but any DI container will do).
First of all, you need to specify not to register the IStringLocalizer interface automatically by adding it to the excludeTypes variable.
var excludeTypes = new Type[] {
// Use this array to add types you wish to explicitly exclude from convention-based
// auto-registration. By default all types that either match I[TypeName] = [TypeName] or
// I[TypeName] = [TypeName]Adapter will be automatically wired up as long as they don't
// have the [ExcludeFromAutoRegistrationAttribute].
//
// If you want to override a type that follows the convention, you should add the name
// of either the implementation name or the interface that it inherits to this list and
// add your manual registration code below. This will prevent duplicate registrations
// of the types from occurring.
// Example:
// typeof(SiteMap),
// typeof(SiteMapNodeVisibilityProviderStrategy)
typeof(IStringLocalizer)
};
Then provide an explicit registration of the ResourceManagerStringLocalizer (and its dependencies) instead.
// Configure localization
// Fully qualified namespace.resourcefile (.resx) name without the extension
string resourceBaseName = "SomeAssembly.Resources.Resource1";
// A reference to the assembly where your resources reside.
Assembly resourceAssembly = typeof(SomeAssembly.Class1).Assembly;
// Register the ResourceManager (note that this is application wide - if you are
// using ResourceManager in your DI setup already you may need to use a named
// instance or SmartInstance to specify a specific object to inject)
this.For<ResourceManager>().Use(() => new ResourceManager(resourceBaseName, resourceAssembly));
// Register the ResourceManagerStringLocalizer (uses the ResourceManger)
this.For<IStringLocalizer>().Use<ResourceManagerStringLocalizer>();
Then it is just a matter of specifying the resources appropriately. You need to start them with the Base Name (in this case SomeAssembly.Resources.Resource1), and then specify the key of the resource as the second argument.
<mvcSiteMapNode title="$resources:SomeAssembly.Resources.Resource1,ContactTitle" controller="Home" action="Contact"/>
Or
[MvcSiteMapNode(Title = "$resources:SomeAssembly.Resources.Resource1,ContactTitle", Controller = "Home", Action = "Contact)]
Note that getting the BaseName right is the key to making it work. See the following MSDN documentation: http://msdn.microsoft.com/en-us/library/yfsz7ac5(v=vs.110).aspx

How to create grouping and clickable nodes from single action simultaneously?

I have controller Account and its method LogOn. How to create sitemap like this:
-Account // non clickable, just grouping
--Log On // clickable
? If I use site map in the form of XML-file (mvc.sitemap) I can write like this:
<mvcSiteMapNode
title="Account"
controller="Account"
action="LogOn"
clickable="false"
key="AccountGroup" >
<mvcSiteMapNode
title="Log On"
controller="Account"
action="LogOn"
key="LogOn" />
</mvcSiteMapNode>
But I want to do it with only MvcSiteMapNodeAttribute attribute. However, only one such attribute can be applied to the method. I also don't want to use some dummy method to create just grouping node.
For now I have only one approach - create grouping nodes in the XML mvc.sitemap file, and clickable nodes - with MvcSiteMapNodeAttribute attribute. But I want to escape to write site map by hands as far as possible. Can it be done without grouping nodes in the XML?
If using v4, you can apply multiple MvcSiteMapNodeAttributes to a single action method.
//
// GET: /Account/LogOn
[MvcSiteMapNodeAttribute(Title = "Account", ParentKey = "Home", Key = "AccountGroup", Clickable = false)]
[MvcSiteMapNodeAttribute(Title = "Log On", ParentKey = "AccountGroup", Key = "LogOn")]
public ActionResult LogOn()
{
return View();
}
You can also put the grouping node on the controller class if that is what you prefer (even in v3).
[MvcSiteMapNodeAttribute(Title = "Account", ParentKey = "Home", Action = "LogOn", Key = "AccountGroup", Clickable = false)]
public class AccountController
{
// Implementation here
}

Grails: tags that supports data binding?

Are there any tags that support databinding other than select? . I use it for one to many relationships
It seems impractical if you have lots of data and scrolling will be longer
It would be awesome if its just a list of checkboxes then there will be a pagination
g.select is just Grails default, but you can customize the view and use any element, since the information is there. Example:
class Parent {
String name
static hasMany = [childrens: Child]
}
class Child {
String name
}
class ParentController {
def create() {
Parent parentInstance = new Parent()
List<Children> childrens = Children.list()
[parentInstance : parentInstance, childrens: childrens]
}
def save() {
def childrens = params.list('childrens')
println childrens //will output all checkbox marked...
}
}
form.gsp
<ul>
<g:each in="${childrens}" var="child">
<li><g:checkBox name="childrens" value="${child in parentInstance.childrens}" /></li>
</g:each>
</ul>
Some key points here:
You may reconsider this approach if your hasMany side can have a lot of records;
All your checkboxes must have the same name to be considered as a list;
When updating the Parent you need to delete the relations before adding the new ones;
Related topic: Grails - Simple hasMany Problem - Using CheckBoxes rather than HTML Select in create.gsp

Resources