How to hide id in the url (MVC3) - asp.net-mvc

Problem
In my project i decided to imlement a custom menu provider using a db stored entity "Section".
So the section is mapped to the following Model:
public class TopMenuItemModel : BaseTrivitalModel
{
public TopMenuItemModel()
{
ChildItems = new List<TopMenuItemModel>();
}
public int ItemId { get; set; }
public string RouteUrl { get; set; }
public string Title { get; set; }
public string SeName { get; set; }
public IList<TopMenuItemModel> ChildItems { get; set; }
}
And the view for the model:
#model TopMenuModel
<nav id="main-nav">
#T("HomePage")
#foreach (var parentItem in Model.MenuItems)
{
#parentItem.Title
}
</nav>
My Default route is:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "Trivital.Web.Controllers" }
);
Controller for the menu:
public class CommonController : BaseTrivitalController
{
...
public ActionResult TopMenu()
{
var sections = _sectionService.GetCollectionByParentId(0, true);
var model = new TopMenuModel();
model.MenuItems = sections.Select(x =>
{
var item = new TopMenuItemModel()
{
ItemId = x.Id,
Title = x.GetLocalized(s => s.Title, _workContext.WorkingLanguage.Id, true, true),
SeName = x.GetSeName(),
RouteUrl = "",
};
return item;
})
.ToList();
return PartialView(model);
}
}
}
Now I have a SectionController where I have an ActionResult method:
//section main page
public ActionResult Section(string seName)
{
var section = _sectionService.Get(1);
if (section == null || section.Deleted || !section.Published)
return RedirectToAction("Index", "Home");
//prepare the model
var model = PrepareSectionPageModel(section);
return View(model);
}
My current Route for the Section (that gives me host/sectionSeName-id):
routes.MapLocalizedRoute(
"section", // Route name
"{SeName}"+ "-" + "{sectionId}", // URL with parameters
new { controller = "Sections", action = "Section" },
new { sectionId = #"\d+" }
);
Now I need to get my Url looks like this (without id, just the section name):
host/sectionSeName
Is there anyway to hide the Id in the url to make the urls look SEO-friendly, but available for the controller?

You can try utilizing the urlMappings in your web.config. Specify something like the following:
<urlMappings enabled="true">
<add url="~/somedirectory/" mappedUrl="~/somedirectory/1/"/>
</urlMappings>
Though, I don't think anything will work unless each section has it's own unique name. Otherwise you'll have some conflicting URLs.
You may also want to consider doing some custom work as well using IIS's rewrite module:
http://www.iis.net/learn/extensions/url-rewrite-module/using-the-url-rewrite-module
The company I work for uses this for it's KB article system, which is similar to your situation, and it works pretty well. (folder/id)

Related

MVC Error using Lists and Model

I am very new to this and am doing a little project to get to know how it all works.
So I'm looking to create a header image area on each page by placing the code in the "_Layout.cshtml" file and attempting to control what image displays according to "ViewBag.Title".
I have broken the code up into the pages, followed by a pic. of the error. I just can't work out what the problem is.
HomeController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace WebSite_Project_1.Controllers
{
public partial class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
[ActionName("Funny-Bones")]
public ActionResult FunnyBones()
{
ViewBag.Message = "This is funny bones.";
return View();
}
public class Headers
{
public string HeaderName { get; set; }
public string PageName { get; set; }
public int HeaderWidth { get; set; }
public int HeaderHeight { get; set; }
public string HeaderType { get; set; }
}
public ActionResult HeaderImages()
{
var model = new List<Headers>();
model.Add(new Headers { HeaderName = "home", PageName = "Home Page", HeaderWidth = 2200, HeaderHeight = 1172, HeaderType = ".png" });
model.Add(new Headers { HeaderName = "about", PageName = "About", HeaderWidth = 2200, HeaderHeight = 1172, HeaderType = ".png" });
model.Add(new Headers { HeaderName = "contact", PageName = "Contact", HeaderWidth = 2200, HeaderHeight = 1172, HeaderType = ".png" });
model.Add(new Headers { HeaderName = "funnybones", PageName = "Funny Bones", HeaderWidth = 2200, HeaderHeight = 1172, HeaderType = ".png" });
return View(model);
}
}
}
_Layout.cshtml
#model IEnumerable<WebSite_Project_1.Controllers.HomeController.Headers>
<div class="headersImage">
#foreach (var item in Model)
{
if (#item.PageName == #ViewBag.Title)
{
<img src="~/Content/Images/#item.HeaderName+#item.HeaderType" title="#item.HeaderName" />
}
}
</div>
#RenderBody()
The problem starts when I try and publish it and then i get this error pointing to Model in the "foreach" loop.
I'm not a 100% sure the loop is right, but haven't been able to get that far yet.
Link to MVC error
You should never specify a model for a layout. The model passed in will always be the one for the view, which almost invariably, will not be the same one the layout is wanting.
For things like this, you should use child actions. Essentially, just take your existing HeaderImages action, add the [ChildActionOnly] attribute (which prevents it from being routed to directly), and change the return to:
return PartialView("_HeaderImages", model);
You can call the view whatever you want, but essentially it would just have the following:
#model IEnumerable<WebSite_Project_1.Controllers.HomeController.Headers>
<div class="headersImage">
#foreach (var item in Model)
{
if (#item.PageName == #ViewBag.Title)
{
<img src="~/Content/Images/#item.HeaderName+#item.HeaderType" title="#item.HeaderName" />
}
}
</div>
Finally, in your layout, remove the model definition line and replace the header image code currently there with:
#Html.Action("HeaderImages", "Home")
EDIT
Sorry, I missed one thing. The child action will render in a separate context from the main view/layout (that's sort of the point). However, that means it has its own ViewBag, so you can't access the ViewBag from the main action directly. Instead, you'll need to pass it in as a route value:
#Html.Action("HeaderImages", "Home", new { title = ViewBag.Title })
Then, modify your child action to accept this param, and set its ViewBag:
[ChildActionOnly]
public ActionResult HeaderImages(string title)
{
...
ViewBag.Title = title;
return PartialView("_HeaderImages", model);
}

Server Error in '/' Application in ActionLink (but submit works fine)

Execution of the following lines
#Html.ActionLink("Open",actionName: "DownloadExcelFile",
controllerName: "Excel",
routeValues: new { model = new ExcelModel(5, "JobName", "item.UserName") },
htmlAttributes: null)
returns Server Error in '/' Application, could you help me to fix them?
Note that when I change the name of the parameter, model -> id, I get an Error 404 instead of Server Error in '/' Application.
The model is
public class ExcelModel
{
public int InputA { get; set; }
public string InputB { get; set; }
public string UserName { get; set; }
public ExcelModel(int inputA, string inputB, string userName)
{
InputA = inputA;
InputB = inputB;
UserName = userName;
}
public ExcelModel()
{
InputA = 1;
InputB = "B1";
UserName = "NiceUser";
}
...
}
Controller is
public class ExcelController : Controller
{
public ActionResult Index()
{
var model = new ExcelModel(1, "B1", User.Identity.Name);
return View(model);
}
[HttpPost]
public ActionResult DownloadExcelFile(ExcelModel id)
{
// assume we create an an excel stream...
MemoryStream excelStream = id.BuildSpreadsheet();
return new FileStreamResult(excelStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = "myfile.xslx"
};
}
}
RouteConfig is the standard one
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Finally, as mentioned earlier, the method itself is fine, since it works perfectly with submit, as below:
#using (Html.BeginForm("DownloadExcelFile", "Excel"))
{
<fieldset>
// fields names and values
<p>
<input type="submit" value="Open Excel"/>
</p>
</fieldset>
}
1) You can't pass an entire class as a route value param. The helper has to be able to put whatever you pass into a URL, which means it has to be something that can be converted to a string value. It might be possible to JSON encode the model and then pass the JSON string for the param, but the helper isn't going to make such assumptions for you, nor would it necessarily know how to JSON encode it for you, if it did.
2) When you just pass the id, you get a 404 because your action doesn't not accept an int for id, but rather expects ExcelModel, which as we discussed in #1, is not possible to pass via URL.
Your controller method is marked with the HttpPost attribute. This means that it only accepts POST-requests and not GET-requests. Normal link visits are GET-requests, so that is probably the problem. (Read more here)
Remove the HttpPost attribute and see if that fixes the problem.

How to post one of four text boxes value to controller from view using FormMethod.Post instead from query string?

I have a search page with four text boxes (ID, batchID, EmployeeNumber, RefNumber)all are numbers. I don't want to use query string to send any of these values to controller. So I am using Form.Post method like below:
#using (Html.BeginForm("Details", "Search", FormMethod.Post, new { #id = Model.id }))
But I want to make it global, so that based on which text box user uses to search, that value should be send to the controller and it's type also if possible(Like they entered ID or batchID or....) so that it will be easy for me to search the database accordingly. Please somebody help.
FYI: my route looks like this in global.asax
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
I am actually thinking to send value from a javascript method where i do all the conditions check.
You could define a view model:
public class SearchViewModel
{
public int? Id { get; set; }
public int? BatchID { get; set; }
public int? EmployeeNumber { get; set; }
public int? RefNumber { get; set; }
}
and then have the controller action you are posting to take this view model as parameter and you will be able to retrieve which values did the user entered in the textboxes:
[HttpPost]
public ActionResult Details(SearchViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
if (model.Id != null)
{
// the user entered something into the textbox containing the id
}
if (model.BatchId != null)
{
// the user entered something into the textbox containing the batch id
}
...
}

Error when passing Model object to my view

I have created the following model object:-
namespace MvcApplication1.Models
{
public class listofpack
{
public string packageName { get; set; }
public string packageId { get; set; }
}
}
Which i am populating its value on the controller as follow:-
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
using (var client = new WebClient())
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["j_username"] = "kermit";
query["hash"] = "9449B5ABCFA9AFDA36B801351ED3DF66";
query["loginAs"] = "admin";
query["loginAs"] = User.Identity.Name;
var url = new UriBuilder("http://localhost:8080/jw/web/json/workflow/package/list");
url.Query = query.ToString();
string json = client.DownloadString(url.ToString());
var model = new JavaScriptSerializer().Deserialize<listofpack>(json);
return View(model);
// return Content(json, "application/json");
}
}
After that on the view i am trying to loop through the model as follow:-
#model IEnumerable<MvcApplication1.Models.listofpack>
#{
ViewBag.Title = "Home Page";
}
#foreach(var item in Model) {
#Html.ActionLink(item.packageId.ToString(), "about", "Home", new { id = "crm"},null)
}
but when i run the view i will get the following error:-
The model item passed into the dictionary is of type 'MvcApplication1.Models.listofpack', but this dictionary requires a model item of type 'System.Collections.Generic.IEnumerable`1[MvcApplication1.Models.listofpack]'.
BR
:::UPDATE::::
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
i have updated the controller code to be:-
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
using (var client = new WebClient())
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["j_username"] = "kermit";
query["hash"] = "9449B5ABCFA9AFDA36B801351ED3DF66";
query["loginAs"] = "admin";
query["loginAs"] = User.Identity.Name;
var url = new UriBuilder("http://localhost:8080/jw/web/json/workflow/package/list");
url.Query = query.ToString();
string json = client.DownloadString(url.ToString());
var model = new JavaScriptSerializer().Deserialize <List<listofpack>>(json);
return View(model);
}
}
and on the view:-
#model List<MvcApplication1.Models.listofpack>
#Model.Count();
#foreach(var item in Model) {
#Html.ActionLink(item.packageId.ToString(), "about", "Home", new { id = "crm"},null)
}
But the problem is that nothing will be displayed in the view and the .count() will return zero although there should be 5 jason objects passed to the view. So what might be going wrong??
Best Regards
var model = new JavaScriptSerializer().Deserialize<listofpack>(json);
is deserializing into a single listofpack object. Your view expects
#model IEnumerable<MvcApplication1.Models.listofpack>
maybe try something along this lines:
var model = new JavaScriptSerializer().Deserialize<IEnumerable<listofpack>>(json);
Read this question :
No parameterless constructor defined for type of 'System.String' during JSON deserialization
No parameterless constructor defined for type of 'System.Collections.Generic.IEnumerable`1[[MvcApplication1.Models.listofpack, MvcApplication1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]'.
This exception means that the generic type "IEnumerable< listofpack >" u used doesn't have a constructor because it's an interface,so you should use "Dictionary" or "List"
Edit:
Here's an example:
I created a class and name it Employee:
public class Employee
{
public string lastName { get; set; }
public string firstName { get; set; }
}
then i wrote the following statements in the controller :
string jsonEmployees="{'employees': [{ 'firstName':'John' , 'lastName':'Doe' }, { 'firstName':'Anna' , 'lastName':'Smith' }, { 'firstName':'Peter' , 'lastName':'Jones' }]}";
var model = new JavaScriptSerializer().Deserialize<Dictionary<string,List<Employee>>>(jsonEmployees);
return View(model);
so try using "Dictionary< string,List< listOfpack >>".

Minimal way to handle static content routes/controllers/views from data driven menus?

I have a ListItem class that is used to represent menu items in my application:
public class ListItem : Entity
{
public virtual List List { get; set; }
public virtual ListItem ParentItem { get; set; }
public virtual ICollection<ListItem> ChildItems { get; set; }
public int SortOrder { get; set; }
public string Text { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public string Area { get; set; }
public string Url { get; set; }
}
I use this data to construct the routes for the application, but I was wondering if there was a clean way to handle controller/views for static content? Basically any page that doesn't use any data but just views. Right now I have one controller called StaticContentController, which contains a unique action for each static page that returns the appropriate view like so:
public class StaticContentController : Controller
{
public ActionResult Books()
{
return View("~/Views/Books/Index.cshtml");
}
public ActionResult BookCategories()
{
return View("~/Views/Books/Categories.cshtml");
}
public ActionResult BookCategoriesSearch()
{
return View("~/Views/Books/Categories/Search.cshtml");
}
}
Is there some way I could minimize this so I don't have to have so many controllers/actions for static content? It seems like when creating my ListItem data I could set the Controller to a specific controller that handles static content, like I have done, but is there anyway to use one function to calculate what View to return? It seems like I still need separate actions otherwise I won't know what page the user was trying to get to.
The ListItem.Url contains the full URL path from the application root used in creating the route. The location of the View in the project would correspond to the URL location to keep the organization structure parallel.
Any suggestions? Thanks.
Edit: My Route registration looks like so:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("Shared/{*pathInfo}");
routes.MapRoute("Access Denied", "AccessDenied", new { controller = "Shared", action = "AccessDenied", area = "" });
List<ListItem> listItems = EntityServiceFactory.GetService<ListItemService>().GetAllListItmes();
foreach (ListItem item in listItems.Where(item => item.Text != null && item.Url != null && item.Controller != null).OrderBy(x => x.Url))
{
RouteTable.Routes.MapRoute(item.Text + listItems.FindIndex(x => x == item), item.Url.StartsWith("/") ? item.Url.Remove(0, 1) : item.Url, new { controller = item.Controller, action = item.Action ?? "index" });
}
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
You can use a single Action with one parameter (the View name) which will return all the static pages
public class StaticContentController : Controller
{
public ActionResult Page(string viewName)
{
return View(viewName);
}
}
You will also need to create a custom route for serving these views, for example:
routes.MapRoute(
"StaticContent", // Route name
"page/{viewName}", // URL with parameters
new { controller = "StaticContent", action = "Page" } // Parameter defaults
);
I see in your example that you specify different folders for your views. This solution will force you to put all static views in the Views folder of the StaticContentController.
If you must have custom folder structure, then you can change the route to accept / by adding * to the {viewName} like this {*viewname}. Now you can use this route: /page/Books/Categories. In the viewName input parameter you will receive "Books/Categories" which you can then return it as you like: return View(string.Format("~/Views/{0}.cshtml", viewName));
UPDATE (Avoiding the page/ prefix)
The idea is to have a custom constraint to check whether or not a file exists. Every file that exists for a given URL will be treated as static page.
public class StaticPageConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string viewPath = httpContext.Server.MapPath(string.Format("~/Views/{0}.cshtml", values[parameterName]));
return File.Exists(viewPath);
}
}
Update the route:
routes.MapRoute(
"StaticContent", // Route name
"{*viewName}", // URL with parameters
new { controller = "StaticContent", action = "Page" }, // Parameter defaults
new { viewName = new StaticPageConstraint() } // Custom route constraint
);
Update the action:
public ActionResult Page(string viewName)
{
return View(string.Format("~/Views/{0}.cshtml", viewName));
}

Resources