MVC RouteConfig Setting Controller and Action attributes as parameters - asp.net-mvc

I'm currently working on a course application site in MVC. I'm relatively new to MVC and I'm trying to get to grips with the RouteConfig file to achieve the aims of the information architecture and to avoid duplication.
I see this as an opportunity to have just three Controllers/views to handle the bulk of the logic:
View 1. Region/Country Select screen
View 2. Course Select screen
View 3. Course Detail and Application screen
The information architecture should work as follows:
{region}/{country}/{course}
The user selects a country/state where their course will operate from, the course country can exist in one of three geographic areas: AMER, EMEA and APAC
The user selects on of the courses available in their country/state.
~/AMER/USA/SignLanguage
~/EMEA/GBR/FirstAid
~/APAC/AUS/EmploymentLaw
Each of the values for, region, country/state and course are db driven
The user view the course details, and applies for a class on the course.
Looking at the Default MapRoute and searching high and low, I'm struggling to find a way to introduce dynamic control and action source (operating as parameters), ensuring that I don't have to build three controls to handle region and a multitude of actions to accommodate each country being handled.
As the course is effectively an id anyway I'm pretty confident that that isn't a big issue.
I figure I could do something like this to handle the controller logic, but I think I would still have to produce several Controllers even in this scenario as well as the hundreds of actions.
routes.MapRoute(
name: "Courses",
url: "{controller}/{action}/{id}",
defaults: new {
controller = "Region",
action = "Index",
id = UrlParameter.Optional
}
constraints: new {
controller = #"^(AMER|EMEA|APAC).+/i"
});
What is the best way to resolve this issue?

You are close, what about doing this for the route:
routes.MapRoute(
name: "Default",
url: "{region}/{country}/{course}",
defaults: new { controller = "Course", action = "Detail" }
);
And Controller:
public class CourseController : Controller
{
// GET: Course
public ActionResult Detail(string region, string country, string course)
{
//TODO: Validation
var data = new Data
{
Region = region,
Country = country,
Course = course
};
return View(data);
}
}
This worked on all three example URLs you gave. Then you can pass the validation logic over to service/repository/whatever to validate against your datastore of choice. Then you don't need to do code changes when region/country/courses are added.
And to round out my example, here is the Data clasee:
public class Data
{
public string Region { get; set; }
public string Country { get; set; }
public string Course { get; set; }
}
And View:
#model StackOverflowExamples.Mvc.Controllers.Data
#{ ViewBag.Title = "Details"; }
<h2>Details</h2>
<div>
<h4>Data</h4>
<hr />
<dl class="dl-horizontal">
<dt>#Html.DisplayNameFor(model => model.Region)</dt>
<dd>#Html.DisplayFor(model => model.Region)</dd>
<dt>#Html.DisplayNameFor(model => model.Country)</dt>
<dd>#Html.DisplayFor(model => model.Country)</dd>
<dt>#Html.DisplayNameFor(model => model.Course)</dt>
<dd>#Html.DisplayFor(model => model.Course)</dd>
</dl>
</div>

Related

Custom MVC Routing

Okie, I am trying to finish a product dispkay for a client, in my code I have this
#foreach (var item in Model)
{
<div class="itemcontainer">
<p class="button">#Html.ActionLink(item.Category, item.Category) (#item.Count)</p>
</div>
}
Which gives me the link (URL) of Products/Categories, now what do I need to get to my ultimate goal of (for example) Products/Braceletsss. Do I have to write a custom route, if so can someone show me an example, I'm still trying to get my head around this.
**EDIT*
I can provide more code if it's needed :)
[HttpGet, Route("products/{categoryName}")]
public IActionResult GetProductsByCategoryName(string categoryName) {
... code to retrieve products by category name
The above is one way to do it, the way that I prefer at least. When you access the route /products/nine-millimeter-handguns, then in your action, the categoryName variable will have the value nine-millimeter-handguns. You can then use that string value to look up all of the products in that category and return them to the client.
The other way to do it is in your global route config in Startup.cs. If you do it this way, you don't need the [Route] attribute on the action method:
public void Configure(IApplicationBuilder app) {
...
app.UseMvc(routes => {
routes.MapRoute(null, "products/{categoryName}", new {
controller = "Products", action = "GetProductsByCategoryName"
});
});
}
I prefer the former attribute approach because it keeps the routes closer to the controllers & actions that they map to. But both will accomplish the same thing.
In order to render a link to this route from a view, you would pass in the categoryName to the ActionLink html helper method:
#Html.ActionLink(item.Category, item.Category, new {
categoryName = "nine-millimeter-handguns"
})

MVC 4 Views, common or specific?

This is my first time using MVC, first time writing a web application as well.
So far, I managed to have a view for a list of employees, and an edit view for the Employee model.
If I had 25 models that I need displayed as lists, and edited, will I have to create 50 different views?
or is there a way to have one common List View and one common Edit View?
(edit below)
Solved the List View issue.
Sorry for the long code.
I created a ModelPropertyInfo class that describes model properties. For now I only added the Label, but I might add more properties like "Format", "InputType", ...
// Model field information class. Used by views to display model info properly
public class ModelPropertyInfo
{
public ModelPropertyInfo() { }
public string Name { get; set; }
public string Label { get; set; }
}
Then the ShowInListAttribute attribute class to decorate only model properties that I want to appear in the list view
// Attribute class used to specify Labels for model fields
public class ShowInListAttribute : Attribute
{
public ShowInListAttribute(string header)
{
Header = header;
}
public string Header { get; set; }
}
And a ModelBase class that all my models will inherit. This class will give the ability to get any property value from the class by passing its name as string
// Base class for all models
public class ModelBase
{
public static List<ModelPropertyInfo> ModelProperties(Type modelType)
{
List<ModelPropertyInfo> result = new List<ModelPropertyInfo>();
foreach (PropertyInfo pi in modelType.GetProperties())
{
ShowInListAttribute att = (ShowInListAttribute)pi.GetCustomAttributes(typeof(ShowInListAttribute), true).FirstOrDefault();
if (att != null)
result.Add(new ModelPropertyInfo { Label = att.Header, Name = pi.Name });
}
return result;
}
public object GetPropertyValue(string propName)
{
return this.GetType().GetProperty(propName).GetValue(this, null);
}
}
Now, here's my Employee model class
[Table("Employee")]
public class Employee : ModelBase
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public decimal ID { get; set; }
[ShowInList("First Name")]
public string FirstName { get; set; }
[ShowInList("Last Name")]
public string LastName { get; set; }
public decimal DepartmentID { get; set; }
[ShowInList("Department")]
[DatabaseGeneratedAttribute(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Computed)]
public string DepartmentName { get; set; }
}
So, to put all the above to use, here's the Index method in my EmployeeController
public ActionResult Index()
{
ViewBag.Columns = ModelBase.ModelProperties(typeof(Employee));
ViewBag.Title = "Employee List";
return View("ListShared", db.Employees.ToList());
}
Finally, the result, the SharedListView that I will use to display a list of any model I want
#using SharedListView.Models
#model IEnumerable<ModelBase>
<h2>#ViewBag.Title</h2>
<p>
#Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
#foreach (ModelPropertyInfo col in ViewBag.Columns)
{
<th>
#col.Label
</th>
}
<th></th>
</tr>
#foreach (var item in Model) {
<tr>
#foreach (ModelPropertyInfo col in ViewBag.Columns)
{
<td width='100px'>
#item.GetPropertyValue(col.Name).ToString()
</td>
}
<td>
#Html.ActionLink("Edit", "Edit", new { id=item.GetPropertyValue("ID") }) |
#Html.ActionLink("Details", "Details", new { id=item.GetPropertyValue("ID") }) |
#Html.ActionLink("Delete", "Delete", new { id=item.GetPropertyValue("ID") })
</td>
</tr>
}
</table>
Still stuck on a common Edit view, any help would be appreciated.
Again, sorry for the long edit.
You don't need to do that. The ASP.NET MVC supports ContentFor method and EditorFor method. So in your case, you only need to designing your view models, and afterwards on your view, you can use its like
#Html.ContentFor(Model.Employee) // for display - that mean, it should be read-only
#Html.EditorFor(Model.Employee) // for editing.
You can see the post about that topic here
I would suggest you have this structure for each model object:
ListView: Display the list of item. And create a viewmodel class for each item in the view
CreateView: Used when creating new object. Also having ViewModel class for this
EditView: the same as CreateView, except it is for edit mode
This structure will create many views with ViewModel that look similar. However, they are not since in the nature they are different for different purposes. The structure will improve the code in term of separation of concern, help in maintenance. Easy to work with.
checkout Knockout.js. I have written applications like what you are talking about, a view for a collection of data and a view for editing the individual records. knockout makes it fairly easy to integrate the editing portion of the views into the collection viewing. It does help to have some understanding of WPF and Silverlight style data bindings. All of my views now use knockout and I integrate the editing functionality in the collection views with proper data binding using the visible binding to a editor area.
Your view will use a model to display or edit the data. If you have 25 different models, every view should have a different model.
If you want to use just one model, mainly because they share similar properties, this can be done but it is not ideal. The way it can be done is if all the models have similar properties, you can include all the properties in one model. Then you can just use the properties you need in other views. This is not the ideal way of doing it. Every view should have its own model.
You could create some sort of class that generates the html for all the different types of "form inputs" you will need in your app. Then add ability to the class to receive data from the models (ie. takes a string from the model and creates a TEXT input with the value set to that string .... or a SELECT dropdown can receive all it's OPTIONs from a model, etc).
Then all these form inputs can be generated from within the models (utilizing your class) and can be pumped into an array that is passed to your single view. The view would contain all the surrounding divs and other html, but somewhere in the view you would put a small "for loop" that outputs your array's contents. This is valid MVC, in that you are only using a simple for-loop in your view. And your models, to some extent may be partially responsible in deciding how the data if formatted coming out of the database (in this case, form inputs). To style the form inputs, you can keep the styling in a css file or at the top of the view.
Ultimately it depends on the future of your app. This is an idea if your app and models fit in a nice repetitive structure. But if you suspect parts of your app might evolve, where sometimes you might need a view to look much differently, or you want more control over how you arrange each of those generated "form inputs" in the views, then you will probably be creating a higher number of views.

Rebinding MVC Models with multiple array selections

I have read somewhat on the post-redirect-get design pattern and I'm not sure if it works for my purpose as what I have is an MVC site which is design to look like an application, I have multiple dropdowns on the page which all bind to an integer array as below in my controller:
[HttpPost]
public ViewResult ResponseForm(PartyInvites.Models.GuestResponse response, int[] SelectedCustomer)
{
return View(response); // works but resets all my selected dropdowns
// return View(); // gives an error that it can't rebind items in view
}
My View:
#foreach (Schedule sched in Model.Schedules)
{
#Html.DropDownList("MySelectedCustomer", new SelectList(sched.Customers, "Id", "FirstName"), "Select A Customer", new { #class = "SelectedCustomer" })
}
The GuestResponse:
public class GuestResponse
{
[Required(ErrorMessage = "You must enter your name")]
public string Name { get; set; }
public string SomeString = "someString";
public string Email { get; set; }
public string Phone { get; set; }
public bool? WillAttend { get; set; }
public int SelectedSchedule = 0;
public int SelectedCustomer = 0;
public List<Schedule> Schedules
{
get
{
return new List<Schedule>() { new Schedule() { ScheduleName = "party1", ScheduleId = 1 }, new Schedule() { ScheduleId = 2, ScheduleName = "party2" } };
}
set
{
Schedules = value;
}
}
}
The SelectCustomer property is a property on the GuestResponse class. All the dropdowns are bound and if I change a few they bind nicely to the int[] SelectedCustomer collection. However I want to return my View back (so it does nothing essentially) but this resets all the dropdowns to their original state as the response was never fully bound because there was multiple dropdowns and MVC couldn't model bind to it. What it the best way of doing this so it maintains state so to speak?
The correct way to handle this is to use a view model instead of passing your domain models to the view.
But if you don't want to follow good practices you could generate your dropdowns like this as a workaround:
for (int i = 0; i < Model.Schedules.Count; i++)
{
#Html.DropDownList(
"MySelectedCustomer[" + i + "]",
new SelectList(
Model.Schedules[i].Customers,
"Id",
"FirstName",
Request["MySelectedCustomer[" + i + "]"]
),
"Select A Customer",
new { #class = "SelectedCustomer" }
)
}
The correct way is to have a property of type int[] SelectedCustomers on your view model and use the strongly typed version of the DropDownListFor helper:
for (int i = 0; i < Model.Schedules.Count; i++)
{
#Html.DropDownListFor(
x => x.SelectedCustomers,
Model.Schedules[i].AvailableCustomers,
"Select A Customer",
new { #class = "SelectedCustomer" }
)
}
and your POST controller action will obviously take the view model you defined as parameter:
[HttpPost]
public ViewResult ResponseForm(GuestResponseViewModel model)
{
// The model.SelectedCustomers collection will contain the ids of the selected
// customers in the dropdowns
return View(model);
}
And since you mentioned the Redirect-After-Post design pattern, this is indeed the correct pattern to be used. In case of success you should redirect to a GET action:
[HttpPost]
public ViewResult ResponseForm(GuestResponseViewModel model)
{
if (!ModelState.IsValid)
{
// the model is invalid => redisplay the view so that the user can fix
// the errors
return View(model);
}
// at this stage the model is valid => you could update your database with the selected
// values and redirect to some other controller action which in turn will fetch the values
// from the database and correctly rebind the model
GuestResponse domainModel = Mapper.Map<GuestResponseViewModel, GuestResponse>(model);
repository.Update(domainModel);
return RedirectToAction("Index");
}
Note: I'm first addressing why it's not binding anything, but that's not addressing the array issue, which I will get to afterwards. Where most people go wrong with MVC is that they do not take advantage of the built-in features of MVC to deal with these situations. They insist on doing foreach's and manually rendering things, but do not take into account the collection status.
The reason why the values are reset is because you are using Html.DropDownList() rather than Html.DropDownListFor(), and you are renaming the posted property name to a different name than your model property name.
You could simply change it to this:
#Html.DropDownList("SelectedCustomer", // note the removal of "My"
new SelectList(sched.Customers, "Id", "FirstName"),
"Select A Customer", new { #class = "SelectedCustomer" })
However, you would not have had this issue, and saved yourself a huge headache if you had just used the strongly typed version.
#Html.DropDownListFor(x => x.SelectedCustomer,
new SelectList(sched.Customers, "Id", "FirstName"),
"Select A Customer", new { #class = "SelectedCustomer" })
As for the Array, you should use an EditorTemplate for Schedules, and in that EditorTemplate you simply create your html as if it were a single item. That's the great thing about Editor/DisplayTemplates is that they automatically deal with collections.
Create a folder in your Views/Controller folder called EditorTemplates. In that folder, create an empty file called Schedule.cshtml (assuming Schedules is a List or array of Schedule). In that, you have code to render a single schedule.
EDIT:
Darin brings up a good point. I would make a small change to the model and add a Selected property to both Schedule and GuestResponse, then you can use Linq to return the selected schedule and it would simplify things.
EDIT2:
You some conflicts between the problem you've described and the code you've shown. I suggest you figure out exactly what you're trying to do, since your code does not really reflect a viable model for this.

Render Partial View from JQuery

I'm a desktop developer and I'm teaching myself ASP.NET MVC3 with C# (can't use MVC4 yet...). I'm slapping together some small dummy projects for this reason. In this project I use a DropDownListFor with movie categories (we all know this example right?). But because this tutorial was going too fast I'm trying to do something more simple.
My Model:
public class MovieModel {
public int SelectedCategorieID { get; set; }
public List<CategorieModel> Categories { get; set; }
public MovieModel() {
this.SelectedCategorieID = 0;
this.Categories = new List<CategorieModel>() {new CategorieModel {ID = 1,
Name = "Drama"},
new CategorieModel {ID = 2,
Name = "Scifi"}};
}
}
public class CategorieModel {
public int ID { get; set; }
public string Name { get; set; }
}
See? Very simple. I have a strongly typed View in which I can use this model:
#model MvcDropDownList.Models.MovieModel (1st line of Index.cshtml).
The model is filled when the default action of the Home controller is called:
public ActionResult Index() {
ViewBag.Message = "Welcome to ASP.NET MVC!";
Models.MovieModel mm = new Models.MovieModel();
return View(mm);
}
So far so good. No problems. Now I want to show the user the ID of the category it selected in a partial view with unobtrusive ajax... Because I didn't get it to work I started even smaller. Forget the DrowpdownList for now. All I have at the moment is this button:
<input type="button" value="Minicart test" onclick="categoryChosen();" />
And this div:
<div id="minicart">
#Html.Partial("Information")
</div>
The mini cart stuff is from another tutorial, I apologize. Don't let it distract you please.
This javascript:
function categoryChosen() {
var url = "Home/CategoryChosen/" + "2";
$.post(url, function (data) {
debugger;
$("#miniCart").html(data);
});
}
The 2 is indeed solid, from my earlier attempt to get it to work. Eventually I want that to be variable ofcourse...
Which calls this action:
[AcceptVerbs("POST")]
public ActionResult CategoryChosen(string SelectedCategorieID) {
ViewBag.messageString = "2";
return PartialView("Information");
}
Yup, and you see that correctly. I just insert 2 for my test. Because like I said, can't get it to work. The partial view Information looks like this:
#{
ViewBag.Title = "Information";
}
<h2>Information</h2>
<h2>You selected: #ViewBag.messageString</h2>
So, now for the big question. I expected the partial view to render: "You selected: 2". I even see this when I debug the javascript and look what's inside the variable 'data'. Can anyone help me why it doesn't render 2? Then I can move on with teaching myself this stuff. Thank you very much in advance for helping. If you miss any kind of information, do not hesitate to ask.
I think the problem is misspelling id of the minicart div. Your id do not contain any capital letters but your selector does. So instead $("#miniCart") you should use $("#minicart") and it will work.
Make it like this and check if it works
function categoryChosen() {
var url = "Home/CategoryChosen?SelectedCategorieID=" + "2";
$.post(url, function (data) {
debugger;
$("#miniCart").html(data);
});
}
This is provided considering that you haven't did any changes to your routers in global.asax
rather you should add the url like this
UrlHelper

ASP.NET MVC Create on a Master-Detail Relationship

I am displaying an list of Items for a given Order. When a user clicks Add Item I redirect to the Item / Create page. This page collects that necessary input but also needs to know the order id that the item belongs to. What is the appropriate way to pass the OrderID to the Item / Create so that it survives the form post when the newly created item is saved.
I've played with TempData and writing the id out on the detail page via Html.Encode(). It gets me part of the way there in that the id shows up on the item form but the value is lost when the form submits and posts. I suppose because its not part of the formcollection. I am guessing my workaround is not the best way and would like to know if anyone can point out the proper way to do this in asp.net mvc.
I do this by creating a new route for the Item controller that includes the OrderId. It doesn't make sense to have an Item without an Order, so the OrderId is required using the constraints parameter.
routes.MapRoute(
"OrderItems",
"Item/{action}/{orderId}/{id}",
new { controller = "Item" },
new { orderId = #"d+" }
);
So the url would look like http://<sitename>/Item/Create/8, where 8 is the OrderId for which to create an item. Something similar could be done for Delete action routes with http://<sitename>/Item/Delete/8/5, where 8 is the OrderId and 5 is the ItemId.
Your Action methods would look like this:
public ActionResult Create(int orderId)
public ActionResult Delete(int orderId, int id)
You could also set it up so that the urls looked like http://<sitename>/Order/8/Item/Create and http://<sitename>/Order/8/Item/Delete/5 if that seems to more clearly show what's going on.
Then the route would look like this:
routes.MapRoute(
"OrderItems",
"Order/{orderId}/Item/{action}/{id}",
new { controller = "Item" },
new { orderId = #"d+" }
);
I've used this sequence (sorry if there are mistakes, I took this from a working example and modified it for your question):
1) In the Order.Details view (assume Model is of type Order):
...
<%= Html.ActionLink("Create New Item", "Create", "OrderItem", new { orderId = Model.ID }, null)%>
...
2) In the OrderItem.Create action:
public ActionResult Create(int orderId)
{
ViewData["orderId"] = orderId;
return View();
}
3) In the OrderItem.Create view:
...
<% using (Html.BeginForm(new { orderId = ViewData["orderId"] }))
...
4) In the OrderItem.Create POST action:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(int orderId)
{
// omitted code to create item, associated with orderId
return RedirectToAction("Details", "Order", new { orderId = orderId });
}
If anyone can think of how to improve on this, or of a better way altogether, please chime in, I'm sort of new to this myself so I'd like to improve.
To round-trip a field that's not part of the normal data entry, I generally use a hidden field in the view, like this:
<%= Html.Hidden("OrderID", Model.OrderID) %>
It looks like a form field, acts like a form field, but the user cannot see it. Make sure you push the correct OrderID into your model from the controller.

Resources