MvcSiteMapProvider how to make a node match the current url - mvcsitemapprovider

The current node is coming up null. I can't figure out how to make MvcSiteMapProvider resolve the node under this circumstance.
Here's the node it needs to match
<mvcSiteMapNode title="Policy" route="Details" typeName="Biz.ABC.ShampooMax.Policy" preservedRouteParameters="id" />
Here's the route:
routes.MapRoute(
"Details",
"Details/{id}",
new { Controller = "Object", Action = "Details" }
).RouteHandler = new ObjectRouteHandler();
The link that gets clicked:
http://localhost:36695/MyGreatWebSite/Details/121534455762071
It's hitting the route ok. Just the MvcSiteMapProvider.SiteMaps.Current.CurrentNode is null.

A null result for CurrentNode indicates the incoming request doesn't match any node in the SiteMap In your case there are 4 different problems that may be contributing to this:
The URL you are inputting http://localhost:36695/MyGreatWebSite/Details/121534455762071 does not (necessarily) match the URL pattern you specified in the route, "Details/{id}". It may if your site is hosted as an IIS application under IISROOT/MyGreatWebSite/.
Your mvcSiteMapNode doesn't specify a controller or action to match. route only acts like an additional piece of criteria (meaning only the named route will be considered in the match), but all of the parameters also need to be provided in order for there to be a match with the route.
You are passing in a custom RouteHandler, which could alter how the route matches the URL. Without seeing the code from your ObjectRouteHandler, it is impossible to tell if or how that will affect how the route matches the URL.
You have a custom attribute, typeName in your mvcSiteMapNode configuration. Unless you have specified to ignore this attribute, it will also be required in the URL to match, i.e. http://localhost:36695/MyGreatWebSite/Details/121534455762071?typeName=Biz.ABC.ShampooMax.Policy.
I recommend against using a custom RouteHandler for the purpose of matching URLs. The effect of doing so makes your incoming routes (URLs into MVC) act differently than your outgoing routes (URLs generated to link to other pages). Since MvcSiteMapProvider uses both parts of the route, it will cause URL generation problems if you only change the incoming routes without also changing the outgoing routes to match. Instead, I recommend you subclass RouteBase, where you can control both sides of the route. See this answer for an example of a custom RouteBase subclass.
However, do note that conventional routing is pretty powerful out of the box and you probably don't need to subclass RouteBase for this simple scenario.

Solution
I arrived at the answer by adding the mvc sitemap provider project into my own solution and stepping through the mvc sitemap provider code to see why my node wasn't being matched. A few things had to be changed. I fixed it by doing the following:
Mvc.sitemap
<mvcSiteMapNode title="Policy" controller="Object" action="Details" typeName="Biz.ABC.ShampootMax.Policy" preservedRouteParameters="id" roles="*"/>
RouteConfig.cs
routes.MapRoute(
name: "Details",
url: "details/{id}",
defaults: new { controller = "Object", action = "Details", typeName = "*" }
).RouteHandler = new ObjectRouteHandler();
Now at first it didn't want to work like this, but I modified the provider like so:
RouteValueDictionary.cs (added wildcard to match value)
protected virtual bool MatchesValue(string key, object value)
{
return this[key].ToString().Equals(value.ToString(), StringComparison.OrdinalIgnoreCase) || value.ToString() == "*";
}
SiteMapNode.cs (changed requestContext.RouteData.Values)
/// <summary>
/// Sets the preserved route parameters of the current request to the routeValues collection.
/// </summary>
/// <remarks>
/// This method relies on the fact that the route value collection is request cached. The
/// values written are for the current request only, after which they will be discarded.
/// </remarks>
protected virtual void PreserveRouteParameters()
{
if (this.PreservedRouteParameters.Count > 0)
{
var requestContext = this.mvcContextFactory.CreateRequestContext();
var routeDataValues = requestContext.HttpContext.Request.RequestContext.RouteData.Values;// requestContext.RouteData.Values;
I think the second modification wasn't strictly necessary because my request context wasn't cached; it would have worked if it was. I didn't know how to get it cached.
It's the first modification to make route values honor a wildcard (*) that made it work. It seems like a hack and maybe there's a built in way.
Note
Ignoring the typeName attribute with:
web.config
<add key="MvcSiteMapProvider_AttributesToIgnore" value="typeName" />
makes another node break:
Mvc.sitemap
<mvcSiteMapNode title="Policies" url="~/Home/Products/HarvestMAX/Policy/List" productType="HarvestMax" type="P" typeName="AACOBusinessModel.AACO.HarvestMax.Policy" roles="*">
so that's why I didn't do that.

Related

Kentico MVC culture two letter code in URL pattern

I'm trying to create a more friendly page URL in Kentico with the two letter culture code (en) instead of the full culture code (en-US). I modified the page URL pattern as seen in the image below. Is there a better way of doing this, should I create a custom macro expression?
The other thing I wanted to achieve was having the default culture not in the URL. Maybe I could also use the custom macro expression for it.
I have solved the problem with a custom macro expression. This is used in the URL pattern in combination with the DocumentCulture. The macro expression takes the culture alias name which is in case of the default culture empty and else the two letter code, and returns this with a '/'.
The url pattern for a page:
{%Util.GetUrlCultureCode(DocumentCulture)%}{%NodeAliasPath%}
The macro expression I wrote for fixing this problem is as below.
public class CustomCultureMacroExpression : MacroMethodContainer
{
[MacroMethod(typeof(List<string>), "Returns the url culture code of current document", 1)]
[MacroMethodParam(0, "Culture", typeof(string), "Current culture.")]
public static object GetUrlCultureCode(EvaluationContext context, params object[] parameters)
{
// Branches according to the number of the method's parameters
switch (parameters.Length)
{
case 1:
return GetCultureAliasOfCulture(parameters[0].ToString());
case 2:
// Weird bug causing macro expression in url pattern to have two parameters where the first parameter is null.
return GetCultureAliasOfCulture(parameters[1].ToString());
default:
// No other overloads are supported
return string.Empty;
}
}
private static string GetCultureAliasOfCulture(string cultureCode)
{
var culture = CultureInfoProvider.GetCultureInfo(cultureCode);
var culturealias = culture?.CultureAlias ?? string.Empty;
return string.IsNullOrEmpty(culturealias) ? string.Empty : $"/{culturealias}";
}
}
You may try to update culture codes in Localization -> Cultures, however I'm not sure if it won't have any side effect
If you use the Dynamic Routing module, you can hook into the GetCulture and GetPage global events to do what you are looking for.
For the GetCulture, you would check if the URL has a 2 letter culture code, if it does then use that as the culture.
You can also adjust the GetPage if needed, although the URL slugs generated should work properly, unless you want a fall back that on the GetPage.After, if there is no found page, you can try removing any culture from the URL and looking up the page by that.
I recommended using the NodeAliasPath for the remaining part of the UrlPattern for ease of use.

TagHelper ; how to modify dynamically added children

The asp-items Razor "TagHelper" will add an <option> to a <select> for each value in the SelectList. I want to modify each of those children.
Specifically I want to disable some of them (i.e. add disabled="disabled").
Even more specifically I want to dynamically disable some of them; I'm using angular so I could ng-disabled="{dynamic_boolean_which_determines_disabled}". This means the option could be disabled at first, but after user makes a change, the option could be disabled (without page reload). Angular should take care of this; I think Angular and TagHelpers should work together in theory...
I expected:
I could somehow access an IEnumerable of the children <option> tags that would be created (i.e. one for each item in the SelectList), iterate the children tags, and SetAttribute("disabled") or SetAttribute("ng-disabled")...
I tried:
Creating my own TagHelper which targets the select[asp-items], and tries to GetChildContentAsync() and/or SetContent to reach an IEnumerable <option> tags and iterate them and process each, but I think this will only let me modify the entire InnerHtml as a string; feels hacky to do a String.replace, but I could do it if that's my only option? i.e. ChildrenContent.Replace("<option", "<option disabled=\"...\"")
Creating my own TagHelper which targets the option elements that are children of the select[asp-items], so I can individually process each. This works, but not on the dynamically-added <option> created by asp-items, it only works on "literal" <option> tags that I actually put into my cshtml markup.
I think this'll work but not ideal:
As I said above, I think I can get the result of TagHelper's dynamic asp-items <option></option> <option></option>, as a string, and do a string replace, but I prefer not to work with strings directly...
I suspect (I haven't tried it) that I could just do the work of asp-items myself; i.e. custom-items. But then I'm recreating the wheel by re-doing the work which asp-items could've done for me?
So I hadn't yet read the "AutoLinkHttpTagHelper" in the example which uses string replacement (specifically RegEx replace) to replace every occurrence of a URL, with an <a> pointed at that URL. The cases are slightly different*, but...
Anyway, here's my solution once I learned to stop worrying and love the string modification:
[HtmlTargetElement("select", Attributes = "asp-items")]
public class AspItemsNgDisabledTagHelper : SelectTagHelper
{
//Need it to process *after* the SelectTagHelper
public override int Order { get; } = int.MaxValue;
//https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring#ProcessAsync
public AspItemsNgDisabledTagHelper(IHtmlGenerator gen) : base(gen) {}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
//Notice I'm getting the PostContent;
//SelectTagHelper leaves its literal content (i.e. in your CSHTML, if there is any) alone ; that's Content
//it only **appends** new options specified; that's PostContent
//Makes sense, but I still wasn't expecting it
var generated_options = output.PostContent.GetContent();
//Note you do NOT need to extend SelectTagHelper as I've done here
//I only did it to take advantage of the asp-for property, to get its Name, so I could pass that to the angular function
var select_for = this.For.Name;
//The heart of the processing is a Regex.Replace, just like
//their example https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring#inspecting-and-retrieving-child-content
var ng_disabled_generated_options = Regex.Replace(
generated_options,
"<option value=\"(\\w+)\">",
$"<option value=\"$1\" ng-disabled=\"is_disabled('{select_for}', '$1')\">");
//Finally, you Set your modified Content
output.PostContent.SetHtmlContent(ng_disabled_generated_options);
}
}
Few learning opportunities:
Was thinking I'd find AspForTagHelper and AspItemsTagHelper, (angular background suggested that the corresponding attributes; asp-for and asp-items, would be separate "directives" aka TagHelper).
In fact, TagHelper "matching" focuses on the element name (unlike angular which can match element name... attribute... class... CSS selector)
Therefore I found what I was looking for in SelectTagHelper, which has For and Items as properties. Makes sense.
As I said above, I extend SelectTagHelper, but that's not necessary to answer my original question. It's only necessary if you want access to the this.For.Name as I've done, but there may even be a way around that (i.e. re-bind its own For property here?)
I got on a distraction thinking I would need to override the SelectTagHelper's behavior to achieve my goals; i.e. Object-Oriented Thinking. In fact, even if I did extend SelectTagHelper, that doesn't stop a separate instance of the base SelectTagHelper from matching and processing the element. In other words, element processing happens in a pipeline.
This explains why extending and calling base.Process(), will result in Select doing its job twice; once when your instance matches, and again when the base instance matched.
(I suppose could've prevented SelectTagHelper from matching by creating a new element name like <asp-items-select>? But, not necessary... I just avoid calling base.Process(). So unless that's a bad practice...)
*Different in this way:
They want to create a tag where none exists, whereas I want to add an attribute a tag which is already there; i.e. the <option>
Though the <option> "tag" is generated by the SelectTagHelper in its PostContent (was expecting to find it in Content), and I don't think tags-generated-in-strings-by-content-mods can be matched with their corresponding TagHelper -- so maybe we really are the same in that we're just dealing with plain old strings
Their "data" aka "model" is implied in the text itself; they find a URL and that URL string becomes a unit of meaning they use. In my case, there is an explicit class for Modeling; the SelectList (<select>) which consists of some SelectListItem (<option>) -- but that class doesn't help me either.
That class only gives me attributes like public bool Disabled (remember, this isn't sufficient for me because the value of disabled could change to true or false within browser; i.e. client-side only), and public SelectListGroup Group -- certainly nothing as nonstandard as ng-disabled, nor a "catch-all" property like Attributes which could let me put arbitrary attributes (ng-disabled or anything else) in there.

ASP MVC AttributeRoutes Languagetoken in URL, keep original Url

I'm using attribute routing with asp mvc 5.2.
I want the user to be able to call the urls with a language token like "de" or "en".
mydomain/en/foo
For this I created a routeattribute with a constraint like descriped in this blog post
This works well when the locale part is at the end like:
[LocaleRoute("home/index/{locale}/","de)]
then i could call home/index or home/index/de
But when I move this to the beginning i can't omit the locale any more.
[LocaleRoute("{locale}/home/index","de)]
There i can only call de/home/index but home/index returns a 404 Notfound. Also the constraint isn't called. It looks like the route isn't just found.
Any hints on what i'm doing wrong and what i have to do?
You probably need to specify that the locale is an optional argument :
[LocaleRoute("{locale?}/home/index","de")]
On a side note, are you using different controllers for de & en ? If you use a single controller you don't need LocaleRoute and can use the default attributes (http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx) :
[RouteArea("{locale?}/home/")]
public class HomeController : Controller
{
...
[Route("index")]
public ActionResult Index (string culture)
{
...
}
}
EDIT
You can also register your action with the following additional route as long as you're careful with providing a default value for culture or treating null.
[Route("/home/index")]

IIS gives 404 when MS-DOS reserved names are used in parameters values

I have an ASP.NET Web API application and I have an action that takes one string parameter, such as:
[HttpGet]
public HttpResponseMessage GetById(string id)
{
// ...
}
It is registered accordingly:
config.Routes.MapHttpRoute("Resources", "api/resources/{id}", new
{
controller = "Resources",
action = "GetById"
});
Now, when someone calls api/resources/con, IIS gives 404. Almost any other value is OK, like api/resources/something or api/resources/nothing.
We have looked over and found that none of the reserved MS-DOS Device Driver Names can be used as a parameter value.
This appears to be a global issue, since MSDN has it too:
http://msdn.microsoft.com/con
http://msdn.microsoft.com/prn
http://msdn.microsoft.com/nul
Is there any way to allow these names to be used as route parameters values?
Add this in Web.config, under system.web.
<httpRuntime relaxedUrlToFileSystemMapping="true" />

Orchard Custom User Part is not stored in Database

I can't seem to store additional data in a separate contentpart attached to User. I have done the following:
Created a module
In the module I created a Model for ProfilePart and ProfilePartRecord
In the migration I created a table for ProfilePartRecord (from type ContentPartRecord)
In the migration I altered the typedefinition for User, by setting WithPart ProfilePart
I created a driver class, that has 2 edit methods, one for get and one for post (code snippets are below
I also created a handler that adds a storage filter for profilePartRepository of type ProfilePartRecord
Module Structure
Drivers/ProfilePartDriver.cs
Handlers/ProfileHandler.cs
Models/ProfilePart.cs
Models/ProfilePartRecord.cs
Views/EditorTemplates/Parts/profile.cshtml
Migrations.cs
Placement.info
Since I think the issue is in the Driver. This is my code:
Is it going wrong because the part is attached to User? Or am I missing something else.
public class ProfilePartDriver:ContentPartDriver
{
protected override string Prefix
{
get { return "Profile"; }
}
//GET
protected override DriverResult Editor(ProfilePart part, dynamic shapeHelper)
{
return ContentShape("Parts_Profile_Edit", () =>
shapeHelper.EditorTemplate(TemplateName: "Parts/Profile", Model: part, Prefix: Prefix));
}
//POST
protected override DriverResult Editor(ProfilePart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
I have used Skywalker's blog. There is one chapter about registering customers by using the User and adding your own content parts to it. Worked nice for me.
First of all - is your ProfilePart editor shown at all when you go to Dashboard and edit a given user? I noticed you're using Parts_Profile_Edit as a shape key, but actually use EditorTemplates/Parts/Profile.cshtml as a template. It's perfectly correct, but note that Placement.info file uses shape keys, so you have to use Parts_Profile_Edit as a shape name in there. Otherwise it won't get displayed.
Second - have you tried debugging to see if the second driver Editor method (the one for handling POST) is being called at all?
Like Bertrand suggested, I'd look into one of the existing modules that work (afaik there is one for user profile in the Gallery) and see the difference. It might be something small, eg. a typo.

Resources