Pass parameter with Html.ActionLink based upon value - c#

In my website with paging and search options i have to redirect the user and pass the search options. With this action I don't want to pass the search model with the URL (ex. http://localhost:1234/?Project.Models.TestModel=....).
My actionlink right now is as follows:
#Html.ActionLink(i.ToString(), "Index", new { idtrips = Model.idTrips, inituser = Model.initUser, date = Model.date.ToString("ddMMyyyy"), page = i})
When clicked this results in a following header:
http://localhost:58763/?idtrips=0&inituser=0&date=05042016&page=5
My question is:
Can you somehow add if clauses to the Html.ActionLink to only give values if they are needed. For example I only have a date set, then I want the Url to be: http://localhost:58763/?date=05042016&page=5. The other values have a default value in my controller so this url will work but i'm not able to generate it.
My controller:
public ActionResult Index(long idtrips = 0, long inituser = 0, string date = "", int page = 1)
{ ... }
What i'm looking for is something like this,:
#Html.ActionLink(i.ToString(), "Index", new { if(Model.idTrips > 0) { idtrips = Model.idTrips,} page = i})

You could probably accomplish this with a ternary statement (e.g. an inline-if statement) :
new { idtrips = Model.idTrips > 0 ? Model.IdTrips : 0, page = i}
Since you have a default parameter value for idtrips, this should only set it to something else if it is greater than 0.
If you don't want the value to appear at all, you could consider simply making it a nullable long long? parameter (and ensure this matches on your Model as well):
public ActionResult Index(long? idtrips = 0, ... ) { ... }
Using this approach, you could simply set it as expected :
new { idtrips = Model.idTrips, page = i}
And if Model.idTrips was null (e.g. the default value), then it would point to :
Controller/Index?page=1
However, if it was any non-null value, it would render the querystring parameter as expected :
Controller/Index?idtrips=42&page=1

Related

Optional parameter in .NET Core Web API route template

[HttpGet("{pageNumber}{pageSize?}{filter?}{sortOrder?}", Name = "GetEntriesPaged")]
public ActionResult<List<Entry>> GetEntriesPaged(
int pageNumber, int pageSize = 10, string filter = "", string sortOrder = "desc") {
Run-time exception:
System.ArgumentException: An optional parameter must be at the end of
the segment. In the segment
'{pageNumber}{pageSize?}{filter?}{sortOrder?}', optional parameter
'pageSize' is followed by 'filter'. Parameter name: routeTemplate
What is the point? I have an optional parameter at the end of the segment, as asked....
PS. The more complete code:
[HttpGet]
public ActionResult<List<Entry>> GetAll() {
var result = _db.Entries.OrderByDescending(x => x.Date).ToList();
return result;
}
[HttpGet("{pageNumber}{pageSize?}{filter?}{sortOrder?}", Name = "GetEntriesPaged")]
public ActionResult<List<Entry>> GetEntriesPaged(int pageNumber = 1, int pageSize = 10, string filter = "", string sortOrder = "desc") {
int take = pageSize;
int skip = ((pageNumber - 1) * pageSize);
IQueryable<Entry> result;
if (sortOrder == "asc") {
result = _db.Entries.OrderBy(x => x.Date);
}
else {
result = _db.Entries.OrderByDescending(x => x.Date);
}
return result.Skip(skip).Take(take).Where(x => x.Origin.Contains(filter)).ToList();
}
[HttpGet("{id}", Name = "GetEntry")]
public ActionResult<Entry> GetById(long id) {
var item = _db.Entries.Find(id);
if (item == null) {
return NotFound();
}
return item;
}
I need for /entries the GetAll() method to be used, but with /esntries?pageNumber=3 the GetEntriesPaged(...) one
No you don't. sortOrder is at the end, but pageSize and filter are not. In short, you cannot have multiple optional parameters like this. It creates too many combinations of routes, which would render it impossible to determine how to route the request or what particular route params to fill. For example, what if you fill in pageSize and sortOrder but not filter? How is ASP.NET Core supposed to know that what you supplied for sortOrder is not actually meant for filter?
FWIW, you also need slashes between these route params. Otherwise, there's no way to know where one ends and the next begins. For example, is a route like /111 saying page one with a size of 11, page 11 with a size of 1, or page 111, and no size set? That still doesn't help you with having them all be optional though.
If you need multiple optional things in the URL, it's best to just use the query string to supply them.
UPDATE
You don't need and really shouldn't have multiple actions for paged or not. There's too much common functionality and the difference is too slight. The typical formula is:
public async Task<IActionResult> GetAll(int? page = null, int pageSize = 10)
{
var query = _context.Foos;
if (page.HasValue)
{
query = query.Skip((page.Value - 1) * pageSize).Take(pageSize);
}
return Ok(await query.ToListAsync());
}

ASP.NET MVC 5 Attribute Routing: Url.Action returns null when using multiple parameters

This kind of question has been asked before on SO (post 1, post 2, post 3), but none of them was able to help me.
I have the following route (defined inside a LandingController class):
[HttpGet]
[Route("connections/{city:name}/map{page?}", Order = 2)]
public Task<ActionResult> Connections(string city, int page = 1)
for which I am trying to generate an URL in my Razor template like this:
#Url.Action("Connections", "Landing", new { city = "madrid", page = 1 })
or
#Url.Action("Connections", new { city = "madrid", page = 1 })
Both these calls return null. Anyone has an idea why?
However if I omit the page parameter in the route template (doing [Route("connections/{city:name}/map", Order = 2)]), the following URL is generated: /connections/madrid/map/?page=1, which is not what I want, but it shows that I am doing something wrong with my calls above.
[HttpGet]
[Route("connections/{city:name}/map{page?}", Order = 2)]
public Task<ActionResult> Connections(string city, int page = 1)
Pretty sure your routing is incorrect, {city:name} what is name? I don't recall any having a constraint type name.
Your attribute routing should be as simple as this:
[Route("connections/{city}/map/{page?}", Order = 2)]
If you want to add constraint to page as integer it would be:
[Route("connections/{city}/map/{page:int?}", Order = 2)]

Unable to pass a native value in ActionLink under Razor while POCO instances work great

First I thought that I'm using the wrong overload again (a very common gotcha in the API - everybody trips over that one). But wouldn't you know? That's not it. I actually had the HTML attributes parameter too and I verified with intellisense that it's the route values I'm entering.
#Html.ActionLink("Poof", "Action", "Home", 10, new { #class = "nav-link" })
Nevertheless, it seem that the receiving method below only sees null and crashes as it can't make an integer out of it.
public ActionResult Record(int count) { ... }
I've tried a few things: changed parameter type to int? and string (the program stops crashing but the value is still null). I've tested to package the passed value as an object (with/without #).
#Html.ActionLink("Poof", "Record", "Home",
new { count = "bamse" },
new { #class = "nav-link" })
I can see that the anchor produced has my value as a query string, so the changes are there. However, I still get null only in the method.
What am I missing?
The weird thing is that the following works fine.
#Html.ActionLink("Poof", "Record", "Home",
new Thing(),
new { #class = "nav-link" })
public ActionResult Record(Thing count) { ... }
Your using the overload of #Html.ActionLink() that expects the 4th parameter to be typeof object. Internally the method builds a RouteValueDictionary by using the .ToString() value of each property in the object.
In your case your 'object' (an int) has no properties, so no route values are generated and the url will be just /Home/Action(and you program crashes because your method expects a non null parameter).
If for example you changed it to
#Html.ActionLink("Poof", "Action", "Home", "10", new { #class = "nav-link" })
i.e. quoting the 4th parameter, the url would now be /Home/Action?length=2 because typeof string has a property length and there a 2 characters in the value.
In order to pass a native value you need to use the format
#Html.ActionLink("Poof", "Action", "Home", new { count = 10 }, new { #class = "nav-link" })
Which will generate /Home/Action?count=10 (or /Home/Action/10 if you create a specific route definition with Home/Action/{count})
Note also that passing a POCO in your case only works correctly because your POCO contains only value type properties. If for example, it also contained a property which was (say) public List<int> Numbers { get; set; } then the url created would include ?Numbers=System.Collections.Generic.List[int] (and binding would fail) so be careful passing complex objects in an action link
Hard to say what might be wrong with your code from the information provided in your question but assuming total defaults (a newly created ASP.NET MVC application in Visual Studio), if you add the following markup in your ~/Views/Home/Index.cshtml:
Html.ActionLink(
"Poof",
"Record",
"Home",
new { count = "bamse" },
new { #class = "nav-link" }
)
and the following action in your HomeController:
public ActionResult Record(string count)
{
return Content(count);
}
upon clicking on the generated anchor, the correct action will be invoked and the correct parameter passed to it.
The generated markup will look like this:
<a class="nav-link" href="/Home/Record?count=bamse">Poof</a>
So I guess that now the question that you should be asking yourself is: how does my setup differs with what Darin has outlined here? Answering this question might hold the key to your problem.
UPDATE:
OK, now you seem to have changed your question. You seem to be trying to pass complex objects to your controller action:
public ActionResult Record(Thing count) { ... }
Of course this doesn't work as you might expect. So make sure that you pass every single property that you want to be available when constructing your anchor:
Html.ActionLink(
"Poof",
"Record",
"Home",
new { ThingProp1 = "prop1", ThingProp2 = "prop2" },
new { #class = "nav-link" }
)
Alternatively, and of course a much better approach to handle this situation is to attribute an unique identifier to your models, so that all its needed in order to retrieve this model from your backend is this identifier:
Html.ActionLink(
"Poof",
"Record",
"Home",
new { id = "123" },
new { #class = "nav-link" }
)
and then in your controller action simply use this identifier to retrieve your Thing:
public ActionResult Record(int id)
{
Thing model = ... fetch the Thing using its identifier
}

How to page with Custom method/ActionLink?

I am probably going about this completely wrong here, but that is partly what I am asking.
I am creating a blog using MVC3 and I am having some issues. My homepage currently lists each blog post with their corresponding comments and topics correctly. I want it to be limited to a number of posts, so here is my code in the HomeController.
public class HomeController : Controller
{
private MyDB db = new MyDB();
public ActionResult Index()
{
var posts = (from p in db.Set<BlogPost>()
orderby p.DateCreated descending
select new PostViewModel
{
Title = p.Title,
DateCreated = p.DateCreated,
Content = p.Content,
Topics = p.Topics,
Comments = p.Comments,
CommentCount = p.Comments.Count
}).Take(5).ToList();
IEnumerable<Topic> topics = from t in db.Topics
select t;
var blog = new BlogViewModel
{
Post = posts,
Topics = topics.Select(t => new SelectListItem {
Value = Convert.ToString(t.id),
Text = t.Name
})
};
return View(blog);
}
}
This works fine as I've said. I have the topics coming in separately because I want to eventually sort by those (which I also don't know where to start but that's another story).
My main problem is that I would like to have a "Next" and "Previous" button under the 5 selected posts, that when clicked, grab the next 5 or previous 5. I've been told to use...
#Html.ActionLink("Next >>", "ActionName", "Home", Custom arguement?)
type of solution where I write a custom method in my HomeController and grab the next or previous 5. Is this at all correct? I'd like to understand the best use scenario for something like this. I am completely new to MVC3, so I am not looking for shortcuts, and I feel like I maybe already have made a few.
Thanks for your help.
https://github.com/TroyGoode/PagedList
public class HomeController : Controller
{
public ActionResult Index(int? page)
{
var posts = (from p in db.Set<BlogPost>()
orderby p.DateCreated descending
select new PostViewModel
{
Title = p.Title,
DateCreated = p.DateCreated,
Content = p.Content,
Topics = p.Topics,
Comments = p.Comments,
CommentCount = p.Comments.Count
});
var pageNumber = page ?? 1; // if no page was specified in the querystring, default to the first page (1)
posts = posts.ToPagedList(pageNumber, 25); // will only contain 25 posts max because of the pageSize
var blog = new BlogViewModel
{
Post = posts,
Topics = topics.Select(t => new SelectListItem {
Value = Convert.ToString(t.id),
Text = t.Name
})
};
return View(blog);
}
}
I see two best scenarios:
One you could use Ajax/Jquery and do this.
Two you use a 'Partial' View. This way you are not affecting the ENTIRE view when someone presses the action link for the next five. So you would make a partial view for just the posts, then the controller receives a request for the next five and returns a partial view with a model that has five post items.
Choose which one you feel more comfortable with, but if you are learning MVC, choice two may be better for you for practice.
You can take a look at: http://nerddinnerbook.s3.amazonaws.com/Part8.htm .
Also if you want to get the data with an ajax request i recommend you to convert partial view containing the data to string ( Return Razor partial view using JSON (ASP MVC 3)) and use jquery to load it (http://api.jquery.com/load/ ).
I'd request another action method on your controller (either with Ajax, or normal), and just reuse the same View you have been using for your initial page render.
public ActionResult Index(int? pageNumber)
{
if(!pageNumber.HasValue)
return Index();
var posts = ...; // get posts
var set = posts.Skip(pageNumber * itemsPerPage).Take(itemsPerPage);
// or pageNumber - 1 if you want to be 1-index based
return View(...); //or PartialView() if doing ajax, or even Json() if you want to bind on the client side
}
You could create your Controller like the example code below:
public ViewResult List(string category, int page = 1) {
ProductsListViewModel viewModel = new ProductsListViewModel {
Products = repository.Products
.Where(p => category == null ? true : p.Category == category)
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = page,
ItemsPerPage = PageSize, //4 for example
TotalItems = category == null ?
repository.Products.Count() :
repository.Products.Where(e => e.Category == category).Count()
},
CurrentCategory = category
};
return View(viewModel);
}
Inside your View, you put this custom HTML Helper:
<div class="pager">
#Html.PageLinks(Model.PagingInfo, x => Url.Action("List",
new {page = x, category = Model.CurrentCategory}))
</div>
Here is the code of the Helper:
public static class PagingHelpers {
public static MvcHtmlString PageLinks(this HtmlHelper html,
PagingInfo pagingInfo,
Func<int, string> pageUrl) {
StringBuilder result = new StringBuilder();
for (int i = 1; i <= pagingInfo.TotalPages; i++) {
TagBuilder tag = new TagBuilder("a"); // Construct an <a> tag
tag.MergeAttribute("href", pageUrl(i));
tag.InnerHtml = i.ToString();
if (i == pagingInfo.CurrentPage)
tag.AddCssClass("selected");
result.Append(tag.ToString());
}
return MvcHtmlString.Create(result.ToString());
}
}
This example was taken from this book

Why won't my drop down default to the given value?

I've created a SelectList from a enum. The enum has a description, and the int value is being set to the value I want to store in the database.
The problem is, the default (BLANK) I set on construction isn't being used.
This is my enum:
public enum Stage
{
[Description("")]
BLANK = -99,
[Description("No Data")]
NoData = 9999,
[Description("Nil")]
Nil = 0,
[Description("Action")]
SAction = 1,
[Description("Action Plus")]
SActionPlus = 2,
[Description("Full")]
Full = 3
}
I create it in my controller:
private static IEnumerable<SelectListItem> GenerateSenStageList()
{
var values = from Stage e in Enum.GetValues(typeof(Stage))
select new { ID = (int)e, Name = e.ToDescription() };
return new SelectList(values, "Id", "Name", (int)Stage.BLANK);
}
Where I thought the final parameter set the selected item.
I assign it as ViewData, and access it like:
<%= Html.DropDownList("Stage", (IEnumerable<SelectListItem>)ViewData["StageList"])%>
However, Nil is always the selected value.
What am I missing here??
Thanks!
The last parameter does determine the selected value. However, you are passing a Stage enumerated value as the last parameter, while the actual elements of your list are made up of an ID value and a Stage value. To make this work, you have to pass it the actual object from values with a Stage value of BLANK.
Iainie,
Using your code, i managed to get this working first time. here's my amended code (using the accountcontroller for testing) [using .net 3.5]:
// from account controller - put the enum, etc in there for brevity
public enum Stage
{
[Description("")]
BLANK = -99,
[Description("No Data")]
NoData = 9999,
[Description("Nil")]
Nil = 0,
[Description("Action")]
SAction = 1,
[Description("Action Plus")]
SActionPlus = 2,
[Description("Full")]
Full = 3
}
public static IEnumerable<SelectListItem> GenerateSenStageList()
{
var values = from Stage e in Enum.GetValues(typeof(Stage))
select new { ID = (int)e, Name = e.ToDescription() };
var sellist= new SelectList(values, "Id", "Name", (int)Stage.BLANK);
return sellist;
}
public virtual ActionResult LogOn()
{
var res = GenerateSenStageList();
ViewData["StageList"] = res;
return View();
}
// the ToDescription() extension method
public static class Extn
{
public static string ToDescription(this Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
var attributes =
(DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute),
false);
if (attributes != null &&
attributes.Length > 0)
return attributes[0].Description;
else
return value.ToString();
}
}
// then in the LogOn view:
<%= Html.DropDownList("Stage", (IEnumerable<SelectListItem>)ViewData["StageList"])%>
this all works exactly as you'd hoped for, so I'm wondering if your invocation from the view is somehow getting a bit fuddled. try my example above and see if there are any subtle differences in the selectlist generated code etc.
Just a guess, but:
You cast the ViewData["StageList"] to IEnumerable<SelectListItem>. That enumerable may be the SelectList that you created in the Controller, but it does not have the SelectedValue property.
Maybe it works if you cast ViewData["StageList"] to SelectList instead?
<%= Html.DropDownList("Stage", (SelectList)ViewData["StageList"])%>
In this case using the interface may be the wrong thing, because you actually need the information provided by the SelectList object.
#Ken Wayne VanderLinde is right. If you are using C# 4.0, then you can do this to populate the selected value:
private static IEnumerable<SelectListItem> GenerateSenStageList()
{
var values = from Stage e in Enum.GetValues(typeof(Stage))
select new { ID = (int)e, Name = e.ToDescription() };
return new SelectList(values, "Id", "Name", values.Cast<dynamic>().Where(x => (x.ID == (int)Stage.BLANK)));
}

Categories