I am working on an MVC 4 application where I am showing a menu on masterpage using Mvc.sitemap. I have a node named say "Tasks" which will appear everytime among other nodes on menu. I need to create child nodes for this node based on values fetched from database. Depending on number of values the child nodes will be created and on clicking each child code a certain function will be performed.
Since I dont know how to generate child nodes according to values from database, I have hardcoded nodes as of now in Mvc.sitemap. Below is the code of how I have been doing it now:
<?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="Home" controller="Home" action="Index">
<mvcSiteMapNode title="Tasks" controller="Home" action="Index">
<mvcSiteMapNode title="Task 1" controller="Home" action="Index" url="http://localhost:...."/>
<mvcSiteMapNode title="Task 2" controller="Home" action="Index" url="http://localhost:...."/>
</mvcSiteMapNode>
<mvcSiteMapNode title="Admin" controller="Home" action="Admin"/>
<mvcSiteMapNode title="About" controller="Home" action="About"/>
<mvcSiteMapNode title="Help" controller="Home" action="Help"/>
</mvcSiteMapNode>
</mvcSiteMap>
As you can see in the above code I have hardcoded the child nodes and also specified the url property.
Please help on how to achieve this dynamically. Thanks in Advance!!
This is what dynamic node providers are for.
<mvcSiteMapNode title="Tasks" controller="Home" action="Index" key="TasksIndex">
<!-- This is the task template node - this node won't be added to the SiteMap,
but will be used to define the defaults of the Dynamic Nodes -->
<mvcSiteMapNode action="Index" dynamicNodeProvider="MyNamespace.TaskDynamicNodeProvider, MyAssembly" />
</mvcSiteMapNode>
namespace MyNamespace
{
public class TaskDynamicNodeProvider
: DynamicNodeProviderBase
{
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
using (var db = new MyEntities())
{
// Create a node for each album
foreach (var task in db.Tasks)
{
var dynamicNode = new DynamicNode();
dynamicNode.Title = task.Name;
dynamicNode.ParentKey = "TasksIndex";
dynamicNode.RouteValues.Add("id", task.Id);
// NOTE: Controller is automatically inherited in the XML from the
// nearest parent node where it is set, and action is set in the
// template node in this example. However, you can override the
// values here if you need to.
// dynamicNode.Controller = "Home";
// dynamicNode.Action = "Index";
yield return dynamicNode;
}
}
}
}
}
Related
I need to add phone number to the registration page and need to save it in the db as well. I followed following link.
http://www.jhipster.tech/tips/022_tip_registering_user_with_additional_information.html
But since here Jhispter version is changed code is bit different than the code in above link. So I am bit confusing to go with it. According to the link instructions I did upto "Updating ManagedUserVM". Then after I need the help since code is differed.
It really didn't change that much, and the logic remains the same.
The registerAccount function should look like this now :
public void registerAccount(#Valid #RequestBody ManagedUserVM managedUserVM) {
if (!checkPasswordLength(managedUserVM.getPassword())) {
throw new InvalidPasswordException();
}
userRepository.findOneByLogin(managedUserVM.getLogin().toLowerCase()).ifPresent(u -> {throw new LoginAlreadyUsedException();});
userRepository.findOneByEmailIgnoreCase(managedUserVM.getEmail()).ifPresent(u -> {throw new EmailAlreadyUsedException();});
User user = userService.registerUser(managedUserVM, managedUserVM.getPassword(), managedUserVM.getPhone());
mailService.sendActivationEmail(user);
}
And the registerUser function in the UserService (which is a rename of the former createUser) :
public User registerUser(UserDTO userDTO, String password, String phone) {
// JHipster code omitted for brevity
...
// Create and save the UserExtra entity
UserExtra newUserExtra = new UserExtra();
newUserExtra.setUser(newUser);
newUserExtra.setPhone(phone);
userExtraRepository.save(newUserExtra);
log.debug("Created Information for UserExtra: {}", newUserExtra);
return newUser;
}
Just note that you may have to manually change your database changelog (if using a SQL database) to correctly link the ids of User and UserExtra, so it looks like this :
<createTable tableName="user_extra">
<column name="phone" type="varchar(255)">
<constraints nullable="true" />
</column>
<column name="user_id" type="bigint">
<constraints primaryKey="true" nullable="false" />
</column>
<!-- jhipster-needle-liquibase-add-column - JHipster will add columns here, do not remove-->
</createTable>
I'm using Spring Integration to parse XML file and i will need to create a thread (and each one have a different rate) for each tag.
Right now (with the help of many users here :)) i'm able to split XML by tag and then route it to the appropiate service-activator.
This works great but i'm not able to redirect to a channel that create "a thread" and then execute the operations. Right now i have the following configuration and in my mind (that i dont know if it is correct...)
Split tag -> Route to the appropiate channel -> Start a thread(from tag configuration) -> Execute the operation
This is my actual configuration that split tag and redirect to the channel.
The router should redirect not toward a channel directly, but schedule them.
In first instance will be enought to redirect it in a pool with fixed rate and later i will use XPATH to get the attribute and then replace this "fixed" rate with the correct value.
I've tried many solutions to create this flow but each one fails or do not compile :(
<context:component-scan base-package="it.mypkg" />
<si:channel id="rootChannel" />
<si-xml:xpath-splitter id="mySplitter" input-channel="rootChannel" output-channel="routerChannel" create-documents="true">
<si-xml:xpath-expression expression="//service" />
</si-xml:xpath-splitter>
<si-xml:xpath-router id="router" input-channel="routerChannel" evaluate-as-string="true">
<si-xml:xpath-expression expression="concat(name(./node()), 'Channel')" />
</si-xml:xpath-router>
<si:service-activator input-channel="serviceChannel" output-channel="endChannel">
<bean class="it.mypkg.Service" />
</si:service-activator>
UPDATE:
Using this configuration for the service this should run a task every 10 seconds (the id=service1) and every 5 seconds the other (the id=service2). In the same way i can have another tag that is handle by another class (because this will have another behaviour)
<root>
<service id="service1" interval="10000" />
<service id="service2" interval="5000" />
<activity id="activity1" interval="50000" />
<root>
I will have a classe (Service) that is general to handle Service tag and this complete some operation and then "return me" the value so i can redirect to another channel.
public class Service {
public int execute() {
// Execute the task and return the value to continue the "chain"
}
}
It's not at all clear what you mean; you split a tag; route it but want to "schedule" it at a rate in the XML. It's not clear what you mean by "schedule" here - normally each message is processed once not multiple times on a schedule.
As I said, I don't understand what you need to do, but a smart poller might be suitable.
Another possibility is the delayer where the amount of the delay can be derived from the message.
EDIT
Since your "services" don't seem to take any input data, it looks like you simply need to configure/start an <inbound-channel-adapter/> for each service, and then start it, based on the arguments in the XML.
<int:inbound-channel-adapter id="service1" channel="foo"
auto-startup="false"
ref="service1Bean" method="execute">
<poller fixed-delay="1000" />
</int:inbound-channel-adapter/>
Note auto-startup="false".
Now, in the code that receives the split
#Autowired
SourcePollingChannelAdapter service1;
...
public void startService1(Node node) {
...
service1.setTrigger(new PeridicTrigger(...));
service1.start();
...
}
I dont know if this is the right way to implement the flow, but i've write the follow code:
applicationContext.xml
<context:component-scan base-package="it.mypkg" />
<!-- Expression to extract interval from XML tag -->
<si-xml:xpath-expression id="selectIntervalXpath" expression="//*/#interval" />
<si:channel id="rootChannel" />
<!-- Split each tag to redirect on router -->
<si-xml:xpath-splitter id="mySplitter" input-channel="rootChannel" output-channel="routerChannel" create-documents="true">
<si-xml:xpath-expression expression="//service|//activity" />
</si-xml:xpath-splitter>
<!-- Route each tag to the appropiate channel -->
<si-xml:xpath-router id="router" input-channel="routerChannel" evaluate-as-string="true">
<si-xml:xpath-expression expression="concat(name(./node()), 'Channel')" />
</si-xml:xpath-router>
<!-- Activator for Service Tag -->
<si:service-activator input-channel="serviceChannel" method="schedule">
<bean class="it.mypkg.Service" />
</si:service-activator>
<!-- Activator for Activity Tag -->
<si:service-activator input-channel="activityChannel" method="schedule">
<bean class="it.mypkg.Activity" />
</si:service-activator>
<!-- Task scheduler -->
<task:scheduler id="taskScheduler" pool-size="10"/>
Each tag will extend an Operation class (to avoid code duplication on bean injection)
Operation.java
public abstract class Operation {
protected TaskScheduler taskScheduler;
protected XPathExpression selectIntervalXpath;
abstract public void schedule(Node document);
#Autowired
public void setTaskScheduler(TaskScheduler taskScheduler) {
this.taskScheduler= taskScheduler;
}
public TaskScheduler getTaskScheduler() {
return this.taskScheduler;
}
#Autowired
public void setSelectIntervalXpath(XPathExpression selectIntervalXpath) {
this.selectIntervalXpath = selectIntervalXpath;
}
public XPathExpression getSelectIntervalXPath() {
return this.selectIntervalXpath;
}
}
And an example of Service class (that handle all tags service provided on .xml)
public class Service extends Operation {
private static final Logger log = Logger.getLogger(Service.class);
#Override
public void schedule(Node document) {
log.debug("Scheduling Service");
long interval = Long.parseLong(this.selectIntervalXpath.evaluateAsString(document));
this.taskScheduler.scheduleAtFixedRate(new ServiceRunner(), interval);
}
private class ServiceRunner implements Runnable {
public void run() {
log.debug("Running...");
}
}
}
Now to continue my flow i will need to find a way to redirect the output of each job to Spring Integration (applicationContext.xml).
I'm using MvcSiteMapProvider for ASP.NET MVC5 project. I want to show a dynamic breadcrumb based on preservedRouteUrlParamters. I have multiple universities and each university has courses. I don't want to list all the university in the mvc.sitemap.
Instead of:
url: /stanford
breadcrumb: home / university
url: /stanford/course1
breadcrumb: home / university / course details
It should look like:
url: /stanford
breadcrumb: home / stanford
url: /stanford/course1
breadcrumb: home / stanford / course details ...where stanford is link to /stanford
url: /mit
breadcrumb: home / mit
url: /mit/course1
breadcrumb: home / mit / course details ...where mit is link to /mit
So this is the pattern:
url: /{university}
breadcrumb: home / {university}
url: /{university}/{course}
breadcrumb: home / {university} / course details
Here is the mvc.sitemap config I have:
<mvcSiteMapNode title="university" controller="Curriculum" action="UniversityDetails" preservedRouteParameters="university">
<mvcSiteMapNode title="course details" action="CourseDetails" preservedRouteParameters="university,course"/>
</mvcSiteMapNode>
This is solution I have so far but I'm not sure if it is a good way.
I use title="{university}" and check for the pattern {university}.
<mvcSiteMapNode title="{university}" controller="Curriculum" action="UniversityDetails" preservedRouteParameters="university">
<mvcSiteMapNode title="course details" action="CourseDetails" preservedRouteParameters="university,course"/>
</mvcSiteMapNode>
I use the SiteMapNodeModel.Url to dynamically generate the breadcrumb.
public static string TitleBreadcrumb(this SiteMapNodeModel m)
{
if (m.Title.StartsWith("{") && m.Title.EndsWith("}"))
{
return m.Url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Last();
}
return m.Title;
}
I then use the above extension method in the SiteMapNodeModel.cshtml display template.
// use #Model.TitleBreadcrumb() instead of #Model.Title
#Model.TitleBreadcrumb()
Similar for #Model.Description.
Is there a better way?
The only thing particularly wrong with your approach is that you are not encoding the value from the URL before displaying it in your HTML. This means that some malicious user could potentially inject HTML and/or JavaScript into your page by manipulating the URL.
However, the most common way to provide a dynamic title is to use the SiteMapTitleAttribute, which uses a value from your Model or a value in your ViewData to populate the title dynamically.
[SiteMapTitle("Name")]
public ViewResult UniversityDetails(string university) {
var model = _repository.Find(university);
// Name is a string property of
// the university model object.
return View(model);
}
[SiteMapTitle("Name", Target = AttributeTarget.ParentNode)]
public ViewResult CourseDetails(string university) {
var model = _repository.Find(university);
// Name is a string property of
// the university model object.
return View(model);
}
Before I revert to the earlier package, does anybody know why this can be? I followed this guide pretty much to the letter.
https://github.com/maartenba/MvcSiteMapProvider/wiki/Upgrading-from-v3-to-v4
Cheers,
J
mvc.sitemap
<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMapNode title="Home" controller="Home" action="Index">
<mvcSiteMapNode title="Opportunity Stream" controller="TaskStream" action="Index"/>
<mvcSiteMapNode title="Opportunity Stream" controller="TaskStream" action="IndexNew"/>
<mvcSiteMapNode title="Appointment" controller="Appointment" action="Index"/>
<mvcSiteMapNode title="Vehicle Search" controller="VehicleSearch" action="Index"/>
<mvcSiteMapNode title="Stock" controller="Stock" action="Index"/>
<mvcSiteMapNode title="Admin" controller="Admin" action="Index">
<mvcSiteMapNode title="Team Management" controller="Admin" action="TeamManagement">
<mvcSiteMapNode title="Manage Team Member" controller="Admin" action="TeamManagementDetails"/>
</mvcSiteMapNode>
<mvcSiteMapNode title="Site Management" controller="Site" action="Index">
<mvcSiteMapNode title="Site" controller="Site" action="SiteOptions" preservedRouteParameters="id">
<mvcSiteMapNode title="Default Calendar" controller="Site" action="DefaultCalendar"/>
<mvcSiteMapNode title="Exception Calendar" controller="Site" action="ExceptionCalendar"/>
<mvcSiteMapNode title="Manage Site" controller="Site" action="Details"/>
<mvcSiteMapNode title="Manage Site" controller="Site" action="Edit"/>
</mvcSiteMapNode>
<mvcSiteMapNode title="Create Site" controller="Site" action="Create"/>
</mvcSiteMapNode>
<mvcSiteMapNode title="Approve Leave Requests" controller="LeaveRequest" action="Index"/>
</mvcSiteMapNode>
<mvcSiteMapNode title="Auction" controller="Auction" action="Index"/>
<mvcSiteMapNode title="Employee" controller="Employee" action="Index">
<mvcSiteMapNode title="Calendar Exceptions" controller="Site" action="TeamExceptions"/>
<mvcSiteMapNode title="Employee Detail" controller="Employee" action="Detail" clickable="false"/>
<mvcSiteMapNode title="Employee Detail" controller="Employee" action="Edit" clickable="false"/>
</mvcSiteMapNode>
<mvcSiteMapNode title="User Profile" controller="UserProfile" action="Index">
<mvcSiteMapNode title="My Holidays" controller="UserProfile" action="MyHolidays"/>
<mvcSiteMapNode title="Create Leave Request" controller="LeaveRequest" action="Create"/>
</mvcSiteMapNode>
</mvcSiteMapNode>
RouteConfig
public static void RegisterRoutes(RouteCollection routes)
{
XmlSiteMapController.RegisterRoutes(RouteTable.Routes);
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{*alljs}", new
{
alljs = #".*\.js(/.*)?"
});
routes.IgnoreRoute("{*allpng}", new
{
allpng = #".*\.png(/.*)?"
});
routes.IgnoreRoute("{*allcss}", new
{
allcss = #".*\.css(/.*)?"
});
routes.IgnoreRoute("{*allgif}", new
{
allgif = #".*\.gif(/.*)?"
});
routes.IgnoreRoute("{*alljpg}", new
{
alljpg = #".*\.jpg(/.*)?"
});
routes.MapRoute("Default", "{controller}/{action}/{id}",
new
{
country = "uk",
lang = "En",
controller = "Home",
action = "Index",
id = UrlParameter.Optional
});
routes.MapRoute("localizedDefault", "{country}/{lang}/{controller}/{action}/{id}",
new
{
country = "uk",
lang = "En",
controller = "Home",
action = "Index",
id = UrlParameter.Optional
});
}
The most likely cause is that you haven't accounted for the country or lang route parameters in your configuration. And since these are ambient values that have nothing to do with page identification, you could use preservedRouteParamters to force them to always match.
<mvcSiteMapNode title="Home" controller="Home" action="Index" preservedRouteParameters="country,lang">
Currently there is no way to specify preservedRouteParameters globally, so you will need to supply this attribute on every node.
However, be aware that your localized pages won't appear in the XML sitemap endpoint at /sitemap.xml when using preservedRouteParameters.
To fix that, you should fix your route configuration so they will instead appear at {country}/{lang}/sitemap.xml. Add the 2 routes to your configuration to create localized sitemap.xml endpoints, like this:
public static void RegisterRoutes(RouteCollection routes)
{
XmlSiteMapController.RegisterRoutes(RouteTable.Routes);
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{*alljs}", new
{
alljs = #".*\.js(/.*)?"
});
routes.IgnoreRoute("{*allpng}", new
{
allpng = #".*\.png(/.*)?"
});
routes.IgnoreRoute("{*allcss}", new
{
allcss = #".*\.css(/.*)?"
});
routes.IgnoreRoute("{*allgif}", new
{
allgif = #".*\.gif(/.*)?"
});
routes.IgnoreRoute("{*alljpg}", new
{
alljpg = #".*\.jpg(/.*)?"
});
// Localized XML Sitemap routes for MvcSiteMapProvider
routes.MapRoute("localizedSitemap", "{country}/{lang}/sitemap.xml",
new
{
country = "uk",
lang = "En",
controller = "XmlSiteMap",
action = "Index",
page = 0
});
routes.MapRoute("localizedSitemapPage", "{country}/{lang}/sitemap-{page}.xml",
new
{
country = "uk",
lang = "En",
controller = "XmlSiteMap",
action = "Index",
page = 0
});
routes.MapRoute("Default", "{controller}/{action}/{id}",
new
{
country = "uk",
lang = "En",
controller = "Home",
action = "Index",
id = UrlParameter.Optional
});
routes.MapRoute("localizedDefault", "{country}/{lang}/{controller}/{action}/{id}",
new
{
country = "uk",
lang = "En",
controller = "Home",
action = "Index",
id = UrlParameter.Optional
});
}
You will need to create an index file so the search engines will know about your localized URLs, and submit the index file to search engines instead of the one at /sitemap.xml.
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<!-- specifies the default culture sitemap En-uk -->
<loc>http://www.example.com/sitemap.xml</loc>
</sitemap>
<sitemap>
<loc>http://www.example.com/de/De/sitemap.xml</loc>
</sitemap>
<sitemap>
<loc>http://www.example.com/mx/Es/sitemap.xml</loc>
</sitemap>
</sitemapindex>
You should also specify this sitemap index in your robots.txt file so bots of minor search engines can pick it up automatically. For example, if you put the above XML into a file named sitemapindex.xml at the root of your site (where it should be), you can add this line to your robots.txt file (anywhere in the file):
Sitemap: http://www.example.com/sitemapindex.xml
Reference: http://www.sitemaps.org/protocol.html#informing
You should also do a 301 redirect for requests of /uk/En/SomeController/SomeAction and /uk/En/SomeController/SomeAction/SomeId to the default versions of these URLs. This can be accomplished either by subclassing RouteBase to make a redirect route or by using an IIS rewrite rule in your web.config file.
Additional Information That Might Be Helpful
There are several things that could cause the SiteMapPath not to appear.
The node intended to match the current page is missing some route or querystring parameters.
Security Trimming is enabled and the user doesn't have permission to view the current node.
The visibility settings are prohibiting the nodes from being rendered.
The HTML helper templates in your /Views/Shared/DisplayTemplates/ folder are prohibiting them from being shown.
The configuration isn't set up to start automatically.
Since you are upgrading, the most likely causes are #1 or #5.
You can rule out #5 by navigating to /sitemap.xml to see if your XML sitemap is rendering.
It might be helpful to temporarily add #Html.MvcSiteMap().SiteMap() to your layout page to see if any nodes are not resolving to the correct URL.
If you do that, #1 is the most likely cause. You must configure every route value to either a specific value...
<mvcSiteMapNode title="Foo" action="Index" controller="Customer" id="3"/>
This works best in conjunction with dynamic node providers.
Or you can force a match with any value by using preservedRouteParameters.
<mvcSiteMapNode title="Foo" action="Index" controller="Customer" preservedRouteParameters="id" />
Have a look at How to Make MvcSiteMapProvider Remember a User's Position for an in depth discussion of this behavior.
Also, keep in mind that if you are using custom attributes that you don't intend to use with routing, you need to add them to the MvcSiteMapProvider_AttributesToIgnore configuration setting.
<mvcSiteMapNode title="Foo" action="Index" controller="Customer" myCustomValue="Something" />
In web.config:
<appSettings>
<add key="MvcSiteMapProvider_AttributesToIgnore" value="myCustomValue"/>
</appSettings>
I'm new to MVCSiteMap and I have a simple question:
I use the default route config like this:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
Now in my controller, I want to create and edit an entity in the same Action:
public ActionResult AddEdit(int? id)
{}
so if the id is null, it means add, and if it is not null then the action is edit.
Now I want the site map to realize the different from add and edit. I tried this:
<mvcSiteMapNode title="Parent" controller="Class" action="Index">
<mvcSiteMapNode title="Add" controller="Class" action="AddEdit" />
<mvcSiteMapNode title="Edit" controller="Class" action="AddEdit" inheritedRouteParameters="Id"/>
</mvcSiteMapNode>
but seems it does not work well. It always use the second one.
What should I do?
Thanks a lot.
There are 2 options.
Option 1
Create a single node that sets preservedRouteParameters="id" on each of the nodes that correspond to an action method with a parameter. This creates a 1-to-1 relationship between the nodes and action methods, but a 1-to-many relationship between the node and the actual entities.
<mvcSiteMapNode title="Products" controller="Product" action="Index">
<mvcSiteMapNode title="Create New" controller="Product" action="Create" visibility="SiteMapPathHelper,!*" />
<mvcSiteMapNode title="Details" controller="Product" action="Details" visibility="SiteMapPathHelper,!*" preservedRouteParameters="id">
<mvcSiteMapNode title="Edit" controller="Product" action="Edit" visibility="SiteMapPathHelper,!*" key="Product_Edit" preservedRouteParameters="id"/>
<mvcSiteMapNode title="Delete" controller="Product" action="Delete" visibility="SiteMapPathHelper,!*" preservedRouteParameters="id"/>
</mvcSiteMapNode>
</mvcSiteMapNode>
This is the recommended way to do it if you are creating pages that edit data, especially if those pages will never be indexed by search engines.
In most cases, you will also need to setup the FilteredSiteMapNodeVisibilityProvider and SiteMapTitleAttribute to fix the visibility and title of the nodes. You won't be able to use this method for anything other than a breadcrumb trail, so it is important to hide these fake nodes from the other HTML helpers like the Menu and SiteMap.
For a complete demo of how this can be done, visit How to Make MvcSiteMapProvider Remember a User's Position.
Option 2
Use a custom IDynamicNodeProvider to create a node per entity (1-to-1 relationship).
public class StoreDetailsDynamicNodeProvider
: DynamicNodeProviderBase
{
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
using (var storeDB = new MusicStoreEntities())
{
// Create a node for each album
foreach (var album in storeDB.Albums.Include("Genre"))
{
DynamicNode dynamicNode = new DynamicNode();
dynamicNode.Title = album.Title;
dynamicNode.ParentKey = "Genre_" + album.Genre.Name;
dynamicNode.RouteValues.Add("id", album.AlbumId);
yield return dynamicNode;
}
}
}
}
To use this, you need to ensure you set up your key and parent keys in code so each node understands what parent node it belongs to. You may need to explicitly set the "key" attribute in your XML in order to do this. You also need to ensure you set the "id" routeValue on each record to ensure your node matches your incoming route.
Use this method when your pages must be indexed by search engines and/or you want to see the nodes in the menu.
Do note that you can combine these 2 options in the same application and it will work fine. Both of these methods will also work for any number of custom route values (other than "id") as well.