Rebinding MVC Models with multiple array selections - asp.net-mvc

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.

Related

ASP.NET MVC sever-side validation of select (dropdown) list selected item

When handling form posts in MVC, I find myself writing some, somewhat, tedious code to ensure that the posted drop down list selections are for valid items in the list. The idea is, there's nothing preventing a post that contains a selected ID that was not originally presented in the drop down list. A user could insert their own item into the drop down list (or otherwise post whatever they want) or maybe the form has been sitting in the window for so long that the items that are now available have changed. Regardless of why it could happen, the fact is, you can't control the data that is posted. Here's some example code of how I deal with:
VM:
public class MyViewModel
{
public string SelectedItemID {get; set;}
public List<Items> AvailableItems {get; set;}
}
View:
#using (Html.BeginForm())
{
#Html.DropDownListFor(m => m.SelectedItemID,
Model.AvailableItems.Select(i => new SelectListItem()
{
Value = i.ID.ToString(),
Text = i.Name,
Selected = (Model.SelectedItemID == i.ID)
}), "Select One")
}
Controller:
[HttpPost]
public ActionResult Index(MyViewModel myVM)
{
bool isValid = true;
try
{
//Reload the available items
myVM.AvailableItems = Repository.GetAvailableItems();
if(!ModelState.IsValid)
{
isValid = false;
}
else
{
//Make sure the SelectedItemID is a real item
if(!myVM.AvailableItems.Any(i => i.ID == myVM.SelectedItemID))
{
isValid = false;
myVM.SelectedItemID = null;
ModelState.AddModelError("SelectedItemID", "Required"); //This gets interesting when the selected ID belongs to a nested VM in a collection.
}
}
if(isValid)
{
//Finally I can process the form
}
}
catch(Exception)
{
ModelState.AddModelError("", "Unable to process your submission. Please try again.");
}
//return an ActionResult
}
Setting the error in ModelState gets especially ugly if the SelectedItemID belongs to a nested view model that is inside a collection. This seems like it should be a standard type of validation but, relative to the ease of performing other validation in asp.net MVC, this is pretty ugly and tedious. Is there an easier way to take care of this?
I think you should look at Tim Coker's response here:
Is it possible to update ModelState.IsValid manually?
Basically, you want to make your viewmodel class inherit from IValidatableObject, and then put some validate logic into it. Subsequent validation calls should fail if your criteria isn't met (ie SelectedItemId not in a fresh db query)

How to retrieve multiple item from single call to DB and bind to multiple dropdown

New to MVC.
In my page I have several drop down. Currently this is my approach and I feel like repeating myself.
I have created a model, so that I can reuse it for all the dropdown.
public class NameValueModel
{
public int Value { get; set; }
public string Name { get; set; }
}
and then I wrote a data class to fetch value for each dropdown
public IEnumerable<NameValueModel> GetStatus()
{
return (from m in ne.Status
select new NameValueModel { Value = m.StatusID, Name = m.Name }).ToList();
}
public IEnumerable<NameValueModel> GetStates()
{
return (from m in ne.tblStates
select new NameValueModel { Value = m.StateId, Name = m.Name }).ToList();
}
Like wise same code is repeated for each drop down.
Now in my Controller I am calling each of this public method to populate ViewBag
public ActionResult Index(FormCollection f)
{
ViewBag.StateList = new SelectList(new StudentData().GetStates(), "Value", "Name");
ViewBag.ProgramType = new SelectList(new StudentData().GetStatus(), "Value", "Name");
// More of the above
return View();
}
My concern is that I am populating viewBag one by one for each drop down and I have 10 of them, and I am not happy about it.. Is there more sleeker way to get value for all the dropdown as one single call to DB, Cache it, One Model, One call from controller and get all the values for binding.
You can write stored procedure which will return multiple result set and use the DataSet to store the result. Each DropDownList value will be in a saperate table inside the resultant DataSet.

DropDownListFor not always showing selected value

I'm working on an application that has the option of using a VIN decoder to get car information. You can type in the VIN, or you can select through dropdowns, manufacturer, year, make, model, etc.
The manufacturer dropdown is the only thing initialized on the page, with no selected value. Selecting a manufacturer will find all available years for that manufacturer and return the list of years, as well as returning the list of manufacturers and the one that was selected. Selecting a year will then return list of available manufacturers, years and makes, with the selected manufacturer and selected year both identified, and so on down through the application.
This workflow works fine and all of my dropdowns display correctly. When entering a VIN though, I have selected values for each, and still find the lists of available options, and render the page exactly as I would if someone had selected options by hand up to that point. All of the dropdowns render correctly with the proper selected attributes when I do this except the manufacturer.
I have tried to isolate it as much as possible, and have stripped out everything else, and I have this now:
View:
#model My_Project.Models.Data
#using System.Web.Helpers
#using (Html.BeginForm("Temp", "Home", FormMethod.Post, new { id = "formIndex" }))
{
<div>
VIN:
#Html.TextBox("vin", Model.VIN) <button type="submit">Go!</button>
</div>
<div>
Manufacturer: (#Model.ManufacturerId)
#Html.DropDownListFor(m => m.ManufacturerId, Model.Manufacturers, new { style = "width: 175px;" })
</div>
}
Model:
namespace My_Project.Models
{
[Serializable]
public class Data
{
public string VIN { get; set; }
public int ManufacturerId { get; set; }
public SelectList Manufacturers { get; set; }
}
}
Controller:
public ActionResult Temp()
{
Data model = new Data
{
Manufacturers = DBAccess.getManufacturers()
};
Session["ModelData"] = model;
return View(model);
}
[HttpPost]
public ActionResult Temp(Data newData)
{
Data oldData = Session["ModelData"] as Data;
oldData.ManufacturerId = 20;
Session["ModelData"] = oldData;
return View(oldData);
}
If I set the ManufacturerId in Temp(), then my dropdown list renders correctly with whatever manufacturer selected. If it is set in the post response though, the dropdown list renders with all the correct options, but without the correct manufacturer selected. And if you look in the view, I actually have it displaying the manufacturerId to me to make sure it is getting the data correctly, and manufacturerId is set to a value that is in the list, but it is not selected.
I can't figure out what the difference is between these two instances given that the model used in rendering the view looks identical. On top of that, if the post method is called by selecting the manufacturer (I have that functionality stripped out at this point), it would return the same model but also render correctly.
What would be causing this to not render correctly on the return from the post?
If you need to set a value from controller post method, I think you need to update the ModelState with the new value. I think it is because even if you pass updated model to the view, ModelState is still holding the old value.
Try this:
[HttpPost]
public ActionResult Temp(Data newData)
{
Data oldData = Session["ModelData"] as Data;
oldData.ManufacturerId = 20;
Session["ModelData"] = oldData;
//Update model state with new ManufacturerId here
CultureInfo myCulture = new System.Globalization.CultureInfo("en-GB");
ModelState.SetModelValue("ManufacturerId",
new ValueProviderResult((object)oldData.ManufacturerId,
oldData.ManufacturerId.ToString(), myCulture));
return View(oldData);
}

Simple approach to CRUD intersection table in MVC ASP.NET and EF?

I am using C#, MVC3, EF5, SQL Server 2008 R2.
I have an intersection table ie
Lecturer -< LecturerCourse >- Course
The list of Lecturers are populated.
When I add a course, it would be neat to have a list of Lecturers that I could select from, that teach the course in question. When I save the new Course record, this multiselect also should save its data back to the "LecturerCourse" table via Model Binding.
I am using EF5.
Can you recommended a simple and standard approach to solving CRUD for a join, ie "LecturerCourse", table? I have looked online, but some of the approaches seem very complicated.
Many thanks.
Alright, it's going to be a long one. To allow this to happen in "one page" (through POST, or you could use Ajax, technically), you need a combination of a Get and Post version of the method and to construct your view model correctly. Below are the classes that I will use for demonstration purposes:
public class NewCourse
{
[Required]
public string Name { get; set; }
// And your other properties
public int[] LecturerIds { get; set; }
}
public class ViewLecturer
{
public int Id { get; set; }
public int Name { get; set; }
}
public class NewCourseViewModel
{
public NewCourse Course { get; set; }
public IEnumerable<ViewLecturer> Lecturers { get; set; }
}
NewCourseViewModel will be the model for the View (see below). ViewLecturer will give you a lighter mapping between your available Lecturer and the information required to Add to them.
As for the Controller:
public class CourseController : Controller, IDisposable
{
private Lazy<YourContext> lazyContext =
new Lazy<YourContext>(() => new YourContext());
private YourContext Context
{
get { return lazyContext.Value; }
}
public ActionResult New()
{
var model = new NewCourseViewModel {
Course = new NewCourse(),
Lecturers = Context.Lecturers
.Select(l => new ViewLecturer { Id = l.Id, Name = l.Name })
};
return View(model);
}
[HttpPost]
public ActionResult New(NewCourse course)
{
if(ModelState.IsValid)
{
var lecturers = course.Lecturers
.Select(l => new Lecturer { Id = l.Id })
.ToList();
foreach(var lecturer in lecturers)
Context.Lecturers.Attach(lecturer);
var newCourse = new Course {
Name = course.Name,
// ... and the rest of the mapping
Lecturer = lecturers
};
context.Courses.Add(newCourse);
context.SaveChanges();
// Could have to handle DbUpdateException if you want
return RedirectToAction(...);
}
return View(new NewCourseViewModel {
Course = course,
Lecturers = Context.Lecturers
.Select(l => new ViewLecturer { Id = l.Id, Name = l.Name })
});
}
public void Dispose()
{
if(lazyContext.IsValueCreated)
lazyContext.Value.Dispose();
}
}
Your first New method will give you the entry point for your Course creation page. The rest of the validation and actual adding will be done through the [HttpPost]overload. As for your View (that should be in the ~/Views/Course/New.cshtml):
#model NewCourseViewModel
// ... Then when you are ready to begin the form
#using(Html.BeginForm("New", "Course", FormMethod.Post))
{
// Your List of Lecturers
#Html.ListBoxFor(m => m.Course.LecturerIds,
new MultiSelectList(
Model.Lecturers,
"Id",
"Name",
m.Course.LecturerIds ?? new int[0]
))
// Your Other Model binding
}
When the submit button will be pressed, the action matched will be the New(NewCourse course). The names are important because of the way the HtmlHelpers generate their Ids. Because we are only included one property of the whole view model, it will match the parameter name course based on the view model's Course property. You will get a list of Ids for the Lecturers which you will be able to use to attach to the DbContext and add directly to the new Course model (Entity Framework will do the rest). In cases where there was a problem, we can get back the list of lecturers and re-use the same NewCourse in the view model.
Now this is example is very basic but it should give you a good starting point as to how you can structure your view model.

How do I add a Custom Query for a drop down and retain the View Model Pattern?

I've read many articles which they state that querying should not be placed in the Controller, but I can't seem to see where else I would place it.
My Current Code:
public class AddUserViewModel
{
public UserRoleType UserRoleType { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
public ActionResult AddUser()
{
AddUserViewModel model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
The View:
<li>#Html.Label("User Role")#Html.DropDownListFor(x => Model.UserRoleType.UserRoleTypeID, Model.UserRoleTypes)</li>
How do I retain the View Model and Query and exclude the User Type that should not show up?
I think that you are doing it just fine.
Any way... all you can do to remove the querying logic from controller is having a ServiceLayer where you do the query and return the result.
The MVC pattern here is used correctly... what your are lacking is the other 2 layers (BusinessLayer and DataAccessLayer)... since ASP.NET MVC is the UI Layer.
UPDATE, due to comment:
Using var userroletypes = db.UserRoleTypes.Where(u=> u.UserRoleType != 1);
is OK, it will return a list of UserRoleType that satisfy the query.
Then, just create a new SelectList object using the userroletypes collection... and asign it to the corresponding viewmodel property. Then pass that ViewModel to the View.
BTW, I never used the db.XXXX.Select() method before, not really sure what it does... I always use Where clause.
SECOND UPDATE:
A DropDownList is loaded from a SelectList that is a collection of SelectItems.
So you need to convert the collection resulting of your query to a SelectList object.
var userroletypes = new SelectList(db.UserRoleTypes.Where(u=> u.UserRoleType != 1), "idRoleType", "Name");
then you create your ViewModel
var addUserVM = new AddUserViewModel();
addUserVM.UserRoleTypes = userroletypes;
and pass addUserVM to your view:
return View(addUserVM );
Note: I'm assuming your ViewModel has a property of type SelectList... but yours is public IEnumerable<SelectListItem> UserRoleTypes { get; set; } so you could change it or adapt my answer.
I don't see anything wrong with your code other than this db instance that I suppose is some concrete EF context that you have hardcoded in the controller making it impossible to unit test in isolation. Your controller action does exactly what a common GET controller action does:
query the DAL to fetch a domain model
map the domain model to a view model
pass the view model to the view
A further improvement would be to get rid of the UserRoleType domain model type from your view model making it a real view model:
public class AddUserViewModel
{
[DisplayName("User Role")]
public string UserRoleTypeId { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
and then:
public ActionResult AddUser()
{
var model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
and in the view:
#model AddUserViewModel
<li>
#Html.LabelFor(x => x.UserRoleTypeId)
#Html.DropDownListFor(x => x.UserRoleTypeId, Model.UserRoleTypes)
</li>

Resources