MVC 3 how to pass null parameter using #URL.Action - asp.net-mvc

I have an MVC controller that accepts two string parameters but I will only ever use one or the other. I'm not sure how to best handle the situation. I need to find a way to pass a NULL for which ever parameter will not be used. The way I have it set up now passes the parameters get to the Action but the unused parameter is empty, I need it to be NULL and everything will work as I need. I know I could do an "if" statement and build the #URL.Action link dynamically but that seems clunky. I also saw some posts that suggest a custom route but I would like to hear from users here before I go that route. There must be an easier way.
My function to route to the new URL:
$('#poBtn').on('click', function (e) {
var poId = $('#PoLabel').val();
var reqId = $('#ReqLabel').val();
var url = '#Url.Action("ShipmentsByPo", "Shipping", new {po = "_poId_" , reqId = "_reqId_" })';
url = url.replace('_poId_', poId);
url = url.replace('_reqId_', reqId);
window.location.href = url;Action
})
Action:
public IEnumerable<ShippingHeaderVM> ShipmentsByPo(string po, string reqID )
{
object[] parameters = { po, reqID };
var shipmentsByPo = context.Database.SqlQuery<ShippingHeaderVM>("spSelectLOG_ShippingNoticeByPo {0},{1}",parameters);
return shipmentsByPo.ToList();
}

One possible solution would be to check inside the controller action if a parameter is empty, set it to null before using it.
Another possibility is to compose one or the other url on the client:
$('#poBtn').on('click', function (e) {
e.preventDefault();
var poId = $('#PoLabel').val();
var reqId = $('#ReqLabel').val();
var url = '#Url.Action("ShipmentsByPo", "Shipping", new { po = "_poId_" })';
if (poId != '') { // Might need to adjust the condition based on your requirements
url = url.replace('_poId_', encodeURIComponent(poId));
} else {
url = '#Url.Action("ShipmentsByPo", "Shipping", new { reqId = "_reqId_" })';
url = url.replace('_reqId_', encodeURIComponent(reqId));
}
window.location.href = url;
});

Related

MvcSiteMapProvider bug with pagination

It's continue ASP.NET MVC incorect generation url when using pagination, but there I found how fix it. How fix that when using #Html.MvcSiteMap().SiteMapPath() I can't understand.
Problem in that when in actions ShowForum or ShowTopic and when I using pagination some forum or topic. In #Html.MvcSiteMap().SiteMapPath() I get url at parent page with number of page
UPDATE
For route configuration I'm using route attribute
[HttpGet]
[Route("{forumName}", Name = "showForum", Order = 6)]
[Route("{forumName}/Page/{page}", Order = 5)]
[OutputCache(Duration = 30, VaryByParam = "forumName;page", Location = OutputCacheLocation.ServerAndClient)]
public async Task<ActionResult> ShowForum(string forumName, int page = 1)
[HttpGet]
[RefreshDetectFilter]
[Block(VisibleBlock = false)]
[Route("{forum}/{topicName}", Name = "showTopic", Order = 8)]
[Route("{forum}/{topicName}/Page/{page}", Order = 7)]
[OutputCache(Duration = 30, VaryByParam = "topicName;page", Location = OutputCacheLocation.ServerAndClient)]
public async Task<ActionResult> ShowTopic(string forum, string topicName, int page = 1)
My ForumDynamicNodeProvider
public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
{
var rootTitle = ManagerLocalization.Get("Forums", "FORUMS");
var pageParameter = new List<string> { "page" };
var url = "~/Forums";
var attr = new Dictionary<string, object> { { "Controller", "Forums" } };
var nodes = new List<DynamicNode>
{
new DynamicNode
{
Key = "forum_home",
Title = rootTitle,
Url = url,
Attributes = attr
}
};
var forums = this._forumsService.GetAllForumsForMap();
var topics = this._forumsService.GetAllTopicsForMap();
foreach (var forum in forums)
{
var forumRouteValue = new Dictionary<string, object> { { "forumName", forum.NameTranslit } };
nodes.Add(new DynamicNode
{
ParentKey = forum.ForumId != -1 ? $"forum_{forum.ForumId}" : "forum_home",
Key = $"forum_{forum.Id}",
Title = forum.Name,
PreservedRouteParameters = pageParameter,
Controller = "Forums",
Action = "ShowForum",
RouteValues = forumRouteValue,
});
var forumTopics = topics.Where(item => item.ForumId == forum.Id);
foreach (var topic in forumTopics)
{
var topicRouteValue = new Dictionary<string, object> { { "forum", forum.NameTranslit }, { "topicName", topic.TitleTranslite } };
nodes.Add(new DynamicNode
{
ParentKey = $"forum_{forum.Id}",
Key = $"topic_{topic.Id}",
Title = topic.Title,
PreservedRouteParameters = pageParameter,
Controller = "Forums",
Action = "ShowTopic",
RouteValues = topicRouteValue,
});
}
}
return nodes;
}
The problem is that you are using the same route key name {page} in two different places in the same node ancestry in combination with PreservedRouteParameters. PreservedRouteParameters gets its data from the current request. So, it is important that a route key have the same meaning in each request in the same node ancestry. For it to work correctly with PreservedRouteParamters, you need to do three things:
Use a different route key for each separate page parameter (for example, {forumPage} and {page}).
Ensure the ancestor page parameter is passed to the request of its descendants, so when building the URL to an ancestor node the value is in the current request. The simplest way is to build the URL with the page information of all ancestors ({forumName}/Page/{forumPage}/{topicName}/Page/{page}).
Any route keys that have the same meaning between nodes should stay the same ({forumName} in both routes).
Then you need to add the parameters when building the URL of the child node. You must build the URL manually within your application because the request will not have all of the parameters unless you do.
#Html.ActionLink("TheTopicName", "ShowTopic", "Forums",
new { forumName = 1, forumPage = 2, topicName = "foo", page = 1 }, null)
The reason you must supply all of the data in the child node request is because the ancestor node needs it to build its URL. It pulls this information from the request, so it must be present in the request for it to function. MvcSiteMapProvider has no way of knowing what the current page number of the ancestor node is unless it is provided in the request by a URL that is built outside of your menu.
See the MvcSiteMapProvider-Forcing-A-Match-2-Levels project in the code download for How to Make MvcSiteMapProvider Remember a User's Position for a similar configuration and the solution. In that case, it is using productId instead of forumPage as the parameter that is preserved on the descendant nodes so you can navigate back to the parent product.
Note that you could use a similar configuration (with PreservedRouteParameters and SiteMapTitleAttribute) for your entire forum rather than using a dynamic node provider. However, in that case I would suggest you disable the /sitemap.xml endpoint and roll your own.
I found how this fix, thank you to NightOwl888. I'm not the first time understood what should to do.
First I removed initialization PreservedRouteParameters in ForumDynamicNodeProvider
Second I added in action
if (forumPage > 1)
{
var node = SiteMaps.Current.FindSiteMapNodeFromKey(forumName);
if (node != null)
{
node.RouteValues["forumPage"] = forumPage;
}
}
Also I need change generation tree in ForumDynamicNodeProvider because SiteMaps.Current doesn't work in async

Custom ValidationAttribute trigger on different client property change

I have built a custom validation attribute - LessThanDifference. Basically I give it two properties, and it checks to see if the value of the validated field is less than the difference of the two property names. Basically "is FieldC < (FieldA - FieldB)". That part works.
The IsValid function works fine, here is my client validation rules. (Bonus question - Is there any way to get the display name for firstoperand and secondoperand? It has the property name by attribute parameter.)
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "lessthandifference",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
};
rule.ValidationParameters.Add("firstoperand", FirstOperand);
rule.ValidationParameters.Add("secondoperand", SecondOperand);
yield return rule;
}
Where I'm totally lost is how to trigger the validation If A(firstoperand) or B(secondoperand) changes.
$.validator.addMethod(
'lessthandifference',
function (value, element, params) {
var firstVal = $('#' + params.firstoperand).val();
var secondVal = $('#' + params.secondoperand).val();
return (value <= (firstVal - secondVal));
});
$.validator.unobtrusive.adapters.add(
'lessthandifference', ['firstoperand', 'secondoperand'], function (options) {
var params = {
firstoperand: options.params.firstoperand,
secondoperand: options.params.secondoperand
};
options.rules['lessthandifference'] = params;
options.messages['lessthandifference'] = options.message;
//Set up Trigger?
});
I've tried to pass something like (#' + options.params.secondoperand) into another method, but have been unable to get the prop name for the base attribute (FieldC).
$().change seems like it would be the way to go if I could get it set right.
Thoughts?
I solved the main issue:
function addSecondaryValidatorCheck(mainElement, secondaryElement) {
$(secondaryElement).change(function () {
if ($(mainElement).val() > 0)
$(mainElement).valid();
});
And Implementation from the $.validator.unobtrusive.adapters.add function
addSecondaryValidatorCheck('#' + options.element.id, '#' + options.params.compareAttribute);
Still looking for a good way to pass the display name.

how to create dynamic url in collection and model using backbone

My collection and model like this:
detail_userid = 0;
detail_contactid = 0;
var ContactDetail = Backbone.Model.extend({
urlRoot: URL_CONTACTS1+detail_userid+"/"+detail_contactid
});
var ContactDetailCollection = Backbone.Collection.extend({
model: ContactDetail,
url: URL_CONTACTS1+detail_userid+"/"+detail_contactid
})
The entrance is:
ContactDetailManagePageModel.prototype.init = function(m,n){
detail_userid = m;
detail_contactid = n;
var myContactDetails = new ContactDetailCollection();
var contactDetailListView = new ContactDetailListView({
collection: myContactDetails
});
myContactDetails.fetch({reset:true});
}
But when it runs,the url is :http://localhost:8080/ws/users/contacts/0/0,it means that the assignment to detail_userid and detail_contactid is unsuccessful,I don't know why.
Hope for your help.Thanks.
I think you are statically definining the urlRoot and url properties before you are running the init of the PageModel (not quite sure where you are getting m and n from though...)
Both url and urlRoot can be a function, so you can pass in options during instantiation and have them dynamically set on the model.
Simple example covering defining the collection and then creating one
var ContactDetailCollection = Backbone.Collection.extend({
model: ContactDetail,
url: function(){
return URL_CONTACTS1 + this.options.detail_userid + "/" + this.options.detail_contactid;
}
});
var myContactDetails = new ContactDetailCollection({
detail_userid: foo,
detail_contactid: bar
});
As I mentioned, I'm not sure what your init function is doing, I'm guessing it's something custom from your app that I don't need to worry about.
I'm fairly sure the main thing to take away is to set url and urlRoot dynamically
I would fulfill the accepted answer with few remarks.
First parameter when initializing Backbone.Collection is array of models, then options. To create an empty collection with options you should do next
var c = new Backbone.Collection(null, {opt1: val1, opt2: val2});
Actually, you can't access this.options in url function, bec. there are no options like in a model. What you can do, is assign required properties from options upon initialization.
initialize: function (models, options) {
// `parseInt()` is used for consistency that `id` is numeric, just to be sure
this.detail_userid = parseInt(options.detail_userid);
this.detail_contactid = parseInt(options.detail_contactid);
}
Later you can access them like this:
url: function() {
return URL_CONTACTS1 + this.detail_userid + "/" + this.detail_contactid;
}
I wanted to use the HATEOAS href from one model to fetch data of another model. It worked to simply set the url on the newly created collection instead of defining it right away in the constructor.
var DailyMeasuresCollection = Backbone.Collection.extend({
//url : set dynamically with collection.url = url
model : DailyMeasuresModel,
parse : function(data) {
return data._embedded.dailyMeasures;
}
});
var DailyMeasuresTopicListItemView = Backbone.View.extend({
//...
events : {
'click .select-topic' : 'onClick'
},
onClick : function() {
var topicMeasures = new DailyMeasuresCollection()
topicMeasures.url = this.model.attributes._links.measures.href // <- here assign
var topicMeasuresView = new DailyMeasuresListView({
collection : topicMeasures
});
topicMeasures.fetch()
}
});

Url.Action based on the current route

I'd like to generate a new URL based on the existing route, but will add a new parameter 'page'
Here are a few examples:
old: ~/localhost/something?what=2
new: ~/localhost/something?what=2&page=5
old: ~/localhost/Shoes
new: ~/localhost/Shoes/5
I can not just append &page=5 to existing url because routes may be different.
Some use the query string and some do not.
I had a similar issue, and took the approach of extending the UrlHelper. The code in the View looks like:
Page 2
The UrlHelper extension looks like:
using System.Web.Mvc;
using System.Web.Routing;
using System.Collections.Specialized;
public static class UrlHelperExtension
{
public static string AddPage(this UrlHelper helper, int page)
{
var routeValueDict = new RouteValueDictionary
{
{ "controller", helper.RequestContext.RouteData.Values["controller"] },
{ "action" , helper.RequestContext.RouteData.Values["action"]}
};
if (helper.RequestContext.RouteData.Values["id"] != null)
{
routeValueDict.Add("id", helper.RequestContext.RouteData.Values["id"]);
}
foreach (string name in helper.RequestContext.HttpContext.Request.QueryString)
{
routeValueDict.Add(name, helper.RequestContext.HttpContext.Request.QueryString[name]);
}
routeValueDict.Add("page", page);
return helper.RouteUrl(routeValueDict);
}
}
A couple of notes: I check for the ID, since I don't use it in all my routes. I add the Page route value at the end, so it is the last url parameter (otherwise you could add it in the initial constructor).
This seems like a good approach:
// Clone Current RouteData
var rdata = new RouteValueDictionary(Url.RequestContext.RouteData.Values);
// Get QueryString NameValueCollection
var qstring = Url.RequestContext.HttpContext.Request.QueryString;
// Pull in QueryString Values
foreach (var key in qstring.AllKeys) {
if (rdata.ContainsKey(key)) { continue; }
rdata[key] = qstring[key];
}
// Update RouteData
rdata["pageNo"] = "10";
// Build Url
var url = Url.RouteUrl(rdata);
and it avoids collisions such as ?controller=example&action=problem etc.
You could reconstruct a url by pulling out the parts of the existing route by way of the RouteData object. For instance, the following would render a url with the controller and action of the current route:
<%=Url.RouteUrl(new { controller = ViewContext.RouteData.Values["controller"],
action = ViewContext.RouteData.Values["action"] }) %>
To get you started, you could go with something like a custom extension method that generates the url with an additional "page" parameter. Adjust as necessary:
public static string UrlWithPage(this UrlHelper urlHelper, string name, int page)
{
string url = urlHelper.RouteUrl(
new {
controller = urlHelper.RequestContext.RouteData.Values["controller"],
action = urlHelper.RequestContext.RouteData.Values["action"],
id = urlHelper.RequestContext.RouteData.Values["id"],
page = page
}
);
return "" + name + "";
}
This will construct a properly formatted link based on the routing configuration, whether page is real segment in the url or just appended as a querystring.

ASP.NET MVC - Mapping more than one query string parameter to a pretty url

I am a bit stuck on the design of my seo friendly urls for mvc....Take for example the following url:
http://myapp/venues/resturants.aspx?location=central&orderBy=top-rated
With my mvc app i have mapped it as follows:
http://myapp/venues/list/resturants/central/top-rated
{controller}/{action}/{category}/{location}/{order}
Now the only problem is that location and order are optional...so it should be possible to submit a request like: http://myapp/venues/list/resturants/top-rated . This proves to be a problem when the request hits the controller action, the location parameter has picked up "top-rated", naturally.
Any suggestions? I' am considering using explicit querystrings to handle more than one parameter but this is really my last option as i dont want to sacrifice SEO too much.
Has anyone eles run into such dilemmas? And how did you handle it?
Thanks in advance!
Click on your profile link and look at the URLs for Stats, Recent, Response, etc.
Examples:
https://stackoverflow.com/users/52065?sort=recent#sort-top
https://stackoverflow.com/users/52065?sort=stats#sort-top
with no sort it defaults to stats
https://stackoverflow.com/users/52065
Optional paramters should be query parameters
Assuming that the allowed values for location and order are unique (i.e. when they come in, you can tell them apart, or else if they only supply one, how are you going to know if it's a location or an order?), then you could just take two parameters and work out what they are in the controller.
Route: {controller}/{action}/{param1}/{param2}
Controller action:
public ActionResult MyAction(string param1, string param2)
{
string location;
string order;
if (!ParseLocation(param1, out location))
{ ParseLocation(param2, out location); }
// ...
}
Not particularly elegant, but does let you have the URLs you want.
You will always have this issue if you have multiple optional parameters. Either make one or both of them non-optional (and positioned earlier in the query string than the optional one) or use the querystring parameter notation.
ok guys just posting a solution i've been playing with so far.
I have set up my routes using constraints as follows:
routes.MapRoute(
"VenuesList",
"venues/list/{category}/{location}/{orderBy}",
new { controller = "venues", action = "list", category = "", location = "", orderBy = "" },
new { location = "central|east|west|south", orderBy = "top-rated|price" }
);
routes.MapRoute(
"VenuesListByLocation",
"venues/list/{category}/{location}",
new { controller = "venues", action = "list", category = "", location = "" },
new { location = "central|east|west|south" }
);
routes.MapRoute(
"VenuesListByOrder",
"venues/list/{category}/{orderBy}",
new { controller = "venues", action = "list", category = "", orderBy = "" },
new { orderBy = "top-rated|price" }
);
routes.MapRoute(
"VenuesListDefault",
"venues/list/{category}",
new { controller = "venues", action = "list", category = "" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
The idea is that if the validation fails it will go to the next route in the list...eventually hitting the default.
Needs some more testing but has worked well so far...
Why don't you create a property in the page for each possible querystring parameter?
This way you can handle it any way you choose with just a few lines of code...

Resources