I am new to MVC3 and cannot move with this problem.
I am making a simple blog with posts, that are divided into categories and each post may have some tags.
If I show posts to user, I have paging there, where url is like localhost/Posts/1, where "1" is number of the page.
But how can I do this if I want to show posts only from some category or with some tag?
It is in format localhost/Posts/Categories/1, where "1" is id of the category or localhost/Posts/Tags/tag1 where "tag1" is particular tag
I would like to change it all to be in format localhost/Posts/Page/1 or localhost/Posts/Categories/1/Page/1 or localhost/Posts/Tags/tag1/Page/1, but I really cannot find out how to achieve this in controller.
So my question is: How to make methods in controller to accept these complex urls?
I guess it has something to do with routing, but couldn't find any explanation of my problem.
Thanks a lot for any help.
My code:
public ActionResult Tags(string id)
{
Tag tag = GetTag(id);
ViewBag.IdUser = IDUser;
if (IDUser != -1)
{
ViewBag.IsAdmin = IsAdmin;
ViewBag.UserName = model.Users.Where(x => x.IDUser == IDUser).First().Name;
}
return View("Index", tag.Posts.OrderByDescending(x => x.DateTime));
}
public ActionResult Index(int? id)
{
int pageNumber = id ?? 0;
IEnumerable<Post> posts =
(from post in model.Posts
where post.DateTime < DateTime.Now
orderby post.DateTime descending
select post).Skip(pageNumber * PostsPerPage).Take(PostsPerPage + 1);
ViewBag.IsPreviousLinkVisible = pageNumber > 0;
ViewBag.IsNextLinkVisible = posts.Count() > PostsPerPage;
ViewBag.PageNumber = pageNumber;
ViewBag.IdUser = IDUser;
if (IDUser != -1)
{
ViewBag.IsAdmin = IsAdmin;
ViewBag.UserName = model.Users.Where(x => x.IDUser == IDUser).First().Name;
}
return View(posts.Take(PostsPerPage));
}
Create new routes to direct those URL patterns to your controller (or another contoller, as appropriate)
http://www.asp.net/mvc/tutorials/older-versions/controllers-and-routing/asp-net-mvc-routing-overview-cs
For example, this route definition
routes.MapRoute(
"CategoryPage", // Route name
"Posts/Categories/{CategoryID}/Page/{PageID}", // URL with parameters
new { controller = "Home", action = "ViewPage", CategoryID = "", PageID="" } // Parameter defaults
);
Would be picked up by this action in the HomeController:
public ActionResult ViewPage(int CategoryID, int PageID)
Related
[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());
}
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
I am using a third party library for a Grid which uses fixed querystring parameters as shown below.
/Home/GetData/?$skip=0&$top=10
These parameters have a $ in their key and I wanted to know if there is a way to still have the MVC model binding work for these parameters.
i.e.
applying them to this action (which won't compile because of the $ in the parameter names.
public ActionResult GetData(int $skip, int $top)
{
...
return View();
}
Thanks to Andrei for pointing me in the right direction.
The below solutions both do the trick.
Via prefix alias model binding
public ActionResult GetData([Bind(Prefix = "$top")]int top = 0, [Bind(Prefix = "$skip")]int skip = 0)
{
return View();
}
By Request object to get the Querystring values
public ActionResult GetData()
{
var topParam = Request.QueryString["$top"];
var skipParam = Request.QueryString["$skip"];
var top = 0;
int.TryParse(topParam, out top);
var skip = 0;
int.TryParse(skipParam, out skip);
return View();
}
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
In my viewData I have an IList mls.
I want to use this to show in a dropdown. Like so:
<%= Html.DropDownList("ml3Code",
new SelectList(Model.Mls, "Code", "Description", Model.Ml3.Code ?? ""),
Model.T9n.TranslateById("Labels.All"),
new { #class = "searchInput" })%>
This works fine, until there's a myObject.Code == VOC<420 g/l.
I would have expected that an HTML helper would encode its values, but it doesn't.
How should I approach this problem? The only thing I can come up with is first making a dupe list of the objects with encoded values and then feeding it to the selectlist. This would be really bothersome.
P.S. I hope Phill H. and his team will have a long and thorough look at the encoding for asp.net-mvc 2.0...
I'm puzzled. The question "Do ASP.NET MVC helper methods like Html.DropDownList() encode the output HTML?" was asked on SO before, and the answer was "Yes" - and the source-code from the MVC framework was cited to back this assertion up.
Well, you can roll your own Html helper, but if you're like me you won't want to do that.
To me, I see two options here:
Write your select element in plain view without the helper. I've never felt the helpers provide you much save for highlighting an element when an error occurs.
Patch the select box on the client when the page loads, as in:
function encodeHtml(str)
{
var encodedHtml = escape(str);
encodedHtml = encodedHtml.replace(///g,"%2F");
encodedHtml = encodedHtml.replace(/\?/g,"%3F");
encodedHtml = encodedHtml.replace(/=/g,"%3D");
encodedHtml = encodedHtml.replace(/&/g,"%26");
encodedHtml = encodedHtml.replace(/#/g,"%40");
return encodedHtml;
}
window.onload = function()
{
var ml3Code = document.getElementById("ml3Code");
for(var i = 0; i < ml3Code.options.length; ++i)
{
ml3Code.options[i].value = encodeHtml(ml3Code.options[i].value);
}
};
It's a hack, I know. I strongly prefer the first choice.
This is encoded. But dont check with firebug - It shows values decoded.
Check in ViewSource of the Browser and things are encoded.
Controller
public List<CategoryInfo> GetCategoryList()
{
List<CategoryInfo> categories = new List<CategoryInfo>();
categories.Add(new CategoryInfo { Name = "Food<äü", Key = "VOC<420 g/l", ID = 2, Uid = new Guid("C0FD4706-4D06-4A0F-BC69-1FD0FA743B07") });
}
public ActionResult Category(ProductViewModel model )
{
IEnumerable<SelectListItem> categoryList =
from category in GetCategoryList()
select new SelectListItem
{
Text = category.Name,
Value = category.Key
};
model.CategoryList = categoryList;
return View(model);
}
View
<%= Html.DropDownList("Category" , Model.CategoryList) %>
Model
public class ProductViewModel
{
public IEnumerable<SelectListItem> CategoryList { get; set; }
public List<CategoryInfo> Categories { get; set; }
}
HTML
<select id="Category" name="Category"><option value="VOC<420 g/l">Food<äü</option>
</select>