How to create grouping and clickable nodes from single action simultaneously? - asp.net-mvc

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
}

Related

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.

ASP.NET MVC - Execute controller action without redirecting

I am trying to execute an action on a controller without redirecting to the associated view for that action. For a good example of what I am trying to achieve take a look at the music.xbox.com website. When you add a song to a selected playlist from a popup menu - the page just shows a notification without any redirect or refresh. how is this possible?
What I have is the following:
I have a _playlistPopupMenu partial view that renders the list of playlists as follows:
_PlaylistPopupMenu
#model List<OneMusic.Models.GetPlaylists_Result>
#if (Model.Count > 0)
{
<li style="height:2px" class="divider"></li>
foreach (var item in Model)
{
<li style="height:30px">#Html.DisplayFor(p => item.Name)
#Html.ActionLink(item.Name, "AddSong", "Playlist", new { playlistId = #item.PlaylistId, songId = 1 }, "")
</li>
}
}
The PlaylistController AddSong action is as follows:
public PartialViewResult AddSong(int? playlistId, int? songId)
{
if (ModelState.IsValid)
{
db.AddSongToPlaylist(playlistId, songId);
db.SaveChanges();
return PartialView("_AddToPlaylist", "");
}
return PartialView("_AddToPlaylist", "");
}
I am struggling with what to put in the _AddToPlaylist partial view which I think I need to be able to display a notification of some kind (Possiblly using PNotify add in for Bootstrap). MVC wants to always redirect to ../Playlist/AddSong?playlistId=1&songId=1
Any ideas on how to complete this last part of the problem would be great.
If you don't want "full page reloads" then you need to approach the problem slightly differently, using javascript to alter the page dynamically. A library such as JQuery might make manipulating the DOM a little easier.
Display the popup dynamically using javascript.
When the user hits OK/Submit on the popup, post the data back to the server using javascript, and have the controller you are posting to return some HTML.
Append the returned HTML block (partial view) to an existing div containing playlist tracks.
The most difficult part of this is the asynchronous post. Help with updating a div without reloading the whole page can be found in this question.
EDIT - Example
If you have a controller action (accepting POSTs) with the URL myapp.com/PlayList/AddSong/, then you'd set up JQuery to post to this URL. You'd also set up the data property with any form data which you'd like to post, in your case you'd add playistId and songId to the data property.
You'd then use the result of the AJAX query (HTML) and append it to the existing playlist HTML on the page. So assuming that you want to append the partial view's HTML to a div with ID playlistDiv, and assuming that your partial view returns HTML which is valid when appended to the existing playlist, then your javascript will look something like this:
var data = { playlistId: 1, songId: 1 };
$.ajax({
type: "POST",
url: 'http://myapp.com/PlayList/AddSong/',
data: data,
success: function(resultData) {
// take the result data and update the div
$("#playlistDiv").append(resultData.html)
},
dataType: dataType
});
Disclaimer: I can't guarantee that this code will work 100% (unless I write the program myself). There may be differences in the version of JQuery that you use, etc, but with a little tweaking it should achieve the desired result.
using System.Web.Mvc;
using System.Web.Mvc.Html;
public ActionResult Index()
{
HtmlHelper helper = new HtmlHelper(new ViewContext(ControllerContext, new WebFormView(ControllerContext, "Index"), new ViewDataDictionary(), new TempDataDictionary(), new System.IO.StringWriter()), new ViewPage());
helper.RenderAction("Index2");
return View();
}
public ActionResult Index2(/*your arg*/)
{
//your code
return new EmptyResult();
}
in your controller you must add bottom code:
public ActionResult Index(string msg)
{
if (Request.Url.ToString().Contains("yourNewExampleUrlWithOutRedirect.com"))
{
string html = "";
using (System.Net.WebClient client = new System.Net.WebClient())
{
client.Encoding = Encoding.UTF8;
html = client.DownloadString("https://NewExampleUrl.com/first/index?id=1");
}
Response.Write(html);
}
...
}
your view must be empty so you add bottom code
#{
ViewBag.Title = "sample title";
if (Request.Url.ToString().Contains("yourNewExampleUrlWithOutRedirect.com"))
{
Layout = null;
}else
{
Layout ="~/Views/Shared/_Layout.cshtml"
}
}
#if (Request.Url.ToString().Contains("yourNewExampleUrlWithOutRedirect.com")==false)
{
before view like :
<div>hello world</div>
}

I'm not getting friendly url in a form with GET method

I setup a route like that:
routes.MapRoute(
name: "Pesquisar",
url: "Pesquisar/{aaa}/{bbb}/{id}",
defaults: new { controller = "Home", action = "Pesquisar",
aaa = UrlParameter.Optional,
bbb = UrlParameter.Optional,
id = UrlParameter.Optional
}
);
When I press Send button in a form (with GET method) the url is like that:
http://localhost:00000/Pesquisar?aaa=One&bbb=Two
But I was expecting for:
http://localhost:00000/Pesquisar/One/Two
When you map a rout, it adds it to the end of a list. When the router looks for the rule to match, it starts at the begining of the list and itterates through it. It will take the first rule that matches, not the most specific rule. Because it is natural to append code to the end, the default rule (which works for almost everything) will be at the start.
Try re-ordering your code to look like this:
///The specific rout which you want to use
routes.MapRoute(
name: "Pesquisar",
url: "{action}/{aaa}/{bbb}/{id}",
defaults: new { controller = "Home", action = "Pesquisar",
aaa = UrlParameter.Optional,
bbb = UrlParameter.Optional,
id = UrlParameter.Optional
}
);
///The generic catch all router
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
More information can be found in this question:
Setting up ASP.Net MVC 4 Routing with custom segment variables
When I press Send button in a form (with GET method) the url is like that:
http://mydomain.com/Pesquisar?aaa=One&bbb=Two
But I was expecting for:
http://mydomain.com/One/Two
This is because the browser is unaware of the fancy url you want, as the standard form Get method is to append form values in the querystring.
What you mostly likely have to do is something like Creating Canonical URLs including an id and title slug, except redirect to the url you want if it's not the url you want to display.
Or you can use jQuery to manually create the url you want on submit, but requires more client side work.
Not sure if you get a clean URL directly from a html form GET.
One suggestion would be to POST it to an action, do what you need to do with the data, then on completion, redirect to your clean URL.
e.g.
ViewModel:
public sealed class PesquisarModel
{
public string aaa { get; set; }
public string bbb { get; set; }
public string id { get; set; }
}
Controller Actions:
[HttpGet]
public ActionResult Pesquisar(PesquisarModel m)
{
return View();
}
[HttpPost]
[ActionName("Pesquisar")]
public ActionResult PesquisarPost(PesquisarModel m)
{
//do stuff
return RedirectPermanent("/pesquisar/" + m.aaa + "/" + m.bbb + "/" + m.id);
}
View:
#model MyApplication.Models.PesquisarModel
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.aaa)
#Html.TextBoxFor(m => m.bbb)
#Html.TextBoxFor(m => m.id)
<button type="submit">Submit</button>
}
This is the browser behavior. When making a GET request browser appends all KeyValue pairs to querystring.
The mentioned route format will be available, when we use Html.ActionLink or Html.RouteUrl etc.
You could probably write few code in OnActionExecuting ( or you can use any handler) to reconstruct RouteData and redirect with appropriate url. Below code is not tested
var queries = this.Request.QueryString;
foreach(var query in queries)
{
// Add/Update to RequestContext.RouteData
}
var redirectUrl = Url.RouteUrl("Pesquisar",this.RequestContext.RouteData);
Response.Redirect(redirectUrl);
That's expected behavior.
The routing system is on server side. The browser knows nothing about routes, and what you're doing happens in the browser.
If you want to get that route you have to compose it on the client side with a custom script which uses the <form>'s action, an the values of the <input type="text"> values.
You cannot generate the Url on the server side (which could be done with some UrlHelper extension methods) because the changes on the text boxes wouldn't be updated.
This is not advisable because if you make changes on the routes, you could forget to update them in your browser scripts, breaking you application.
You could avoid this problem by creating the url in the server side using an UrlHelper extension method with special placeholders, which could be easily replaced on the client side. I.e. generate an url like this:
http://localhost/Pesquisar/$aaa$/$bbb$/$id$
by providing RouteValues like this: new {aaa="$aaa$, bbb="$bbb$, id="$id$"} to an UrlHelper method. This url can be stored in the value property of a hidden field.
Then, make a browser script for the click event of your button, recover the url with the placeholders from the hidden field, and replace the placeholders with the actual values of the textboxes. To execute the get run this: document.location = theUrl;
If you want to d this for many differnt instances you could create a Helper to geenrate the hidden field with the Url, and a javascript which makes the replacements.
The question is... is it worth the effort?

MvcSiteMapProvider - Multiple Pages Need To Link to One Menu Node

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");
}

asp.net-mvc RenderPartial onclick

Greetings,
I have an asp.net mvc application. I have some links that corresponds to clients names. When user clicks on this link I would like to show an information of clicked client and additionally a textarea where user shall be able to write some text (comment) about selected client. How can I achieve it?
EDIT
I've made something like:
<%=Html.ActionLink(operatorWhoAnswered.Operator.FirstName, "ShowSingleConverstationWithAnswerForm", "MyMessages", new { id = operatorWhoAnswered.Operator.ROWGUID }, new AjaxOptions() { UpdateTargetId = "ss" }) %>
and my controller action looks as follows:
public PartialViewResult ShowSingleConverstationWithAnswerForm(string id)
{
SingleConversationWithAnswerFormViewModel vm = new SingleConversationWithAnswerFormViewModel();
PartialViewResult viewResult = new PartialViewResult();
viewResult.ViewName = "SingleConverstationWithAnswerForm";
viewResult.ViewData = new ViewDataDictionary(vm);
return viewResult;
}
but view opens in a new page, instead of div with id="ss"
EDIT2
Solution found! I don't know why I have used Html.ActionLink. Ajax.ActionLink works fine!
Try something like this:
Create a div that should be rendered when the user clicks. Name is something lika blabla. Then where your link is you have something like
<%=Ajax.ActionLink("Click here", "Action", "Controller", new { id = "some test data passed in"}, new AjaxOptions() { UpdateTargetId = "blabla" })%>
And let that action return your view

Resources