ASP.NET MVC - Getting selected value from a dropdown as a model - asp.net-mvc

I would like to know if there is a way to popuplate a model from dropdownlist selection. For example:
My view is bound to the model Employee and the Employee class has a property 'Department' which is another class with its own properties:
My Employee View Model:
public class Employee
{
public string EmployeeName{get;set;}
public Department EmployeeDepartment{get;set;}
public List<Department> AvailableDepartments {get;set;}
}
Department Model:
public class Department
{
public string Code{get;set}
public string Name{get;set;}
public string Description{get;set;}
}
In my view where I enter the employee details I use a dropdown to let the user choose employee department.
#Html.DropDownListFor(
m => m.EmployeeDepartment,
new SelectList(#Model.AvailableDepartments , "Code", "Name")
)
When I submit the form, I get an instance of 'Employee Class' at the controller but obviously the EmployeeDepartment property will be null.
I am aware that if I add a string property 'EmployeeDepartmentCode' and map it to the dropdownlist, it will work. But is there any way I can populate the Department model property rather than using a string property? - because this view model is also used in a grid where it shows the employee department name.

Well, if your EmployeeDepartment type looks like this:
public class EmployeeDepartment {
public int ID { get; set; }
// + other properties
}
Then you can just change your razor code to:
#Html.DropDownListFor(
m => m.EmployeeDepartment.ID,
new SelectList(#Model.AvailableDepartments , "Code", "Name")
)
And then, when you receive the model back in again, the model will contain a non-null EmployeeDepartment with the ID set.
However, I think you're asking if you can then have the model binder automatically go and fetch the rest of the record from the database and fill in the rest of the data? If so, that's a more complex problem - but you can easily patch that up in the controller.

Related

Validation message not showing for property of sub class of model

Model:
public class Person : IValidatableObject
{
public Address ResidentialAddress { get; set; }
public Address PostalAddress { get; set; }
}
public class Address
{
public string Address1 { get; set; }
}
in Model:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(PostalAddress.Address1))
{
yield return
new ValidationResult("Postal address is required",
new[] { nameof(PostalAddress.Address1) });
}
}
View: (A partial view for address inside the View for Person)
#Html.ValidationMessageFor(m => m.Address1)
In the html this comes out with the name PostalAddress.Address1 and the id PostalAddress_Address1
Unfortunately nameof(PostalAddress.Address1) just returns Address1.
I have tried replacing it with PostalAddress.Address1 and PostalAddress_Address1 and can't get the error to show up.
What's the secret?
I think #MikeDebela is right in the comment below your answer. Your model needs to implement IValidatableObject if you're going to use custom model validation like that. However, that's not your only problem.
First, is there a particular reason you're not just relying on the [Required] attribute for this? Custom model validation is a bit of a waste for something this simple. If the issue is that this is your actual entity class, and you don't want the Address1 column non-nullable at the database-level, well, that's what view models are for. Use them. You can make the property required on just your view model. As a best practice, you should never utilize your entity classes directly to post to.
Also, you're never newing up PostalAddress. When the model binder does its thing on post, if no properties of a related class are posted, it leaves the value of the that related class as null. Then, any related classes that are null, are also not validated. As a result, if the only property is Address1 and you don't post Address1, then PostalAddress is null, and no property on it, specifically, Address1, will participate in validation.

Editing some properties of View Model in ASP.NET MVC

I'm using Entity Framework Database First approach. Let's say I have a model class called Product and that class has a NumberOfViews property. In the Edit page I pass an instance of the product class to the controller.
The problem is I can't add #Html.EditorFor(model => model.NumberOfViews) in the Edit page, because it's supposed that NumberOfViews is updated with every visit to the product page, and NOT by the website Admin.
And I can't add it as #Html.HiddenFor(model => model.NumberOfViews), because if the Admin Inspected the element, he can edit it manually.
Also If I try to programmatically set the value on the server-side (e.g., Product.NumberOfViews = db.Products.Find(Product.Id).NumberOfViews;), I get the following error:
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
And if I don't add it to either the view or the controller, the value will be null, thus overriding any previous value.
So what should I do?
I have noticed a lot of people use the same model for their Entity Framework as they do for their MVC Controller. I generally discourage this practice. In my opinion, a database model is not the same as a view model.
Sometimes a view needs less information than what the database model is supplying. For example while modifying account password, view does not need first name, last name, or email address even though they may all reside in the same table.
Sometimes it needs information from more than one database table. For example if a user can store unlimited number of telephone numbers for their profile, then user information will be in user table and then contact information with be in contact table. However when modifying user profile, they may want to add/edit/delete one or more of their numbers, so the view needs all of the numbers along with first name, last name and email address.
This is what I would do in your case:
// This is your Entity Framework Model class
[Table("Product")]
public class Product
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ProductId { get; set; }
public string Name { get; set; }
public int NumberOfPageViews { get; set; }
}
// This is the model you will use in your Edit action.
public class EditProductViewModel
{
public int ProductId { get; set; }
public string Name { get; set; }
}
public class ProductController : Controller
{
IProductService service;
//...
[HttpGet]
public ActionResult Edit(int productId)
{
var product = service.GetProduct(productId);
var model = new EditProductViewModel()
{
ProductId = product.ProductId,
Name = product.Name
};
return View(model);
}
[HttpPost]
public ActionResult Edit(EditProductViewModel model)
{
if (ModelState.IsValid)
{
var product = service.GetProduct(model.ProductId);
product.Name = model.Name;
service.Update(product);
}
// ...
}
}

how to combine a viewmodel with various other fields

In my mvc4 application I need to create a view where customers can choose from a list of services, subscribe (by selecting the yes/no option) and give details of the last service date they had the service done and also provide a proposed date for the future service. It should roughly look like as below
.
I have a services table in the database like Services(Id,Name etc) but don't know how shall I combine the other values which I m showing like yes/no and the two dates in a single viewModel and pass it to view and retrieve all the values on post back. In simple words which fields will my viewmodel have? Any ideas. thanks
It sounds like you're asking for more than just a view model. To expand on shenku's answer, this would be my rough/untested approach in VB. It's no way all-inclusive, but hopefully gives you an idea on how to manipulate data, pass it to a view, and get data back on post-back.
Model/DB objects:
Public Class Service
Public Property ServiceID As Integer
Public Property Name As String
End Class
Public Class CustomerService
Public Property CustomerID As Integer
Public Property ServiceID As Integer
Public Property Selected As Boolean
Public Property LastDate As DateTime
Public Property ProposedDate As DateTime
End Class
ViewModel:
Public Class ViewRow
Public Property ServiceID As Integer
Public Property Name As String
Public Property YesSelected As Boolean
Public Property NoSelected As Boolean
Public Property LastDate As String
Public Property ProposedDate As String
End Class
Public Class ViewModel
Public Property TableHeaders As String() = {"Services","Yes","No","Date of Last Service", "Proposed Date"}
Public Property ServiceDetails As List(Of ViewRow)
End Class
Controller:
Public Class HomeController
Inherits System.Web.Mvc.Controller
' Simulating EntityFramework
Protected db As New MyEntities
Function ServiceList() As ActionResult
Dim thisCustomerID As Integer
' *Set user's customer ID*
' Using a LINQ join to combine with other service information
Dim vm As New ViewModel With {
.ServiceDetails = ( _
From custService In db.CustomerService().ToList()
Join service In db.Service().ToList()
On custService.ServiceID Equals service.ServiceID
Where custService.CustomerID.Equals(thisCustomerID)
Select New ViewRow With {
.ServiceID = service.ServiceID,
.Name = service.Name,
.YesSelected = custService.Selected,
.NoSelected = Not custService.Selected,
.LastDate = custService.LastDate.ToString("MMM yyyy"),
.ProposedDate = custService.ProposedDate.ToString("MMM yyyy")
}).ToList()
}
' Passing to a strongly-typed view of type "ViewModel"
Return View("serviceList",model:=vm)
End Function
' This is where you post back, and data can be bound to type "ViewModel"
<HttpPost()> _
Function ServiceList(data As ViewModel) As ActionResult
' *Model Validation / Insert / Update*
' Refresh the page (if you want)
RedirectToAction("ServiceList","Home")
End Function
End Class
Razor View (serviceList.vbhtml):
#ModelType ViewModel
<div>
<table>
<tr>
#For Each head In Model.TableHeaders
#<th>#(head)</th>
Next
</tr>
#For Each detail In Model.ServiceDetails
#<tr id=#(detail.ServiceID)>
<td>#(detail.Name)</td>
<td>#(If(detail.YesSelected,"X",""))</td>
<td>#(If(detail.NoSelected,"X",""))</td>
<td>#(detail.LastDate)</td>
<td>#(detail.ProposedDate)</td>
</tr>
Next
</table>
</div>
To post-back, you'll have to have javascript grab data entered into any input fields (I didn't include any here), and construct a JSON object--with the appropriate data--that reflects the argument in the Controller's post action. I provided an example with an argument of type ViewModel. This means your JSON fields have to match those defined in the ViewModel model, and their values have to match the respective property's data type. ASP.NET will bind the data on post back. Additionally ViewModel is complex, so you can post a list of ViewRow (for multiple record updates). To bind this, your JSON object needs to have the ServiceDetails property that contains an array of objects that in turn have properties of ServiceID, Name, YesSelected, etc.
A collection of services in your viewmodel should do it, the Selected bool of course would represent the yes/no option and probably be bound to a checkbox.
public class ViewModel
{
public IList<Service> Services {get;set;}
}
public class Service
{
public bool Selected {get;set;}
public DateTime LastDate {get;set;}
public DateTime ProposedDate {get;set;}
}

Updating many-to-many relationship entity framework

I have problem with updating entites that have many-to many relationship. Below my User and category class:
public class User : IEntity
{
[Key]
public virtual long Id { get; set; }
private ICollection<Category> _availableCategories;
public virtual ICollection<Category> AvailableCategories
{
get { return _availableCategories ?? (_availableCategories = new List<Category>()); }
set { _availableCategories = value; }
}
}
public class Category : IEntity
{
[Key]
public long Id { get; set; }
/// <summary>
/// Full name or description of a category
/// </summary>
[StringLength(255)]
public string FullName { get; set; }
}
This is code snippet from my repository
public override void Edit(User user)
{
var dbUser = _context.Users.Include(x => x.AvailableCategories)
.Single(x => x.Id == user.Id);
var categories = _context.Categories;
dbUser.AvailableCategories.Clear();
foreach (var cat in user.AvailableCategories)
{
dbUser.AvailableCategories.Add(cat);
}
_context.Entry(dbUser).State = EntityState.Modified;
}
However the categories don't get updated. What EF does is insert empty rows into category table and sets relations to this new rows with user.
How can I update User so that I change only categories that already exist in the database?
User that I pass to Edit method has AvailableCategories with only Ids set (rest of properties are empty).
When you're doing something like posting back M2M relationships, you either must post the full object, as in every single property on those objects, or simply post a list of ids and then use those to query the associated objects back from the database. Otherwise, Entity Framework understands your purpose to be to update the properties on the objects as well, in this case with empty values.
Obviously the first option is quite unwieldy, so the second way is the preferred and standard way. Generally, for this, you'd want to use a view model so you could have a property like the following, that you would post into:
public List<long> SelectedCategories { get; set; }
But, if you insist on using the entity directly, you can get much the same result by simply doing:
var selectedCategories = user.AvailableCategories.Select(m => m.Id)
Once you have the ids:
var newAvailableCategories = _context.Categories.Where(m => selectedCategories.Contains(m.Id));
And then finally set that on your user:
dbUser.AvailableCategories = newAvailableCategories;
I notice you are also adding the user.AvailableCategories directly into dbUser.AvailableCategories. I've noticed when binding back complex objects from an MVC view that DB Entities are no longer attached to the DbContext. If you look at the entity, you can verify by checking dbContext.Entry(cat).State is "detached" (or something unexpected) I believe.
You must query those entities back out of the dbContext (possibly by using the returned cat.Id's). Or otherwise manually set the entities as "unchanged". And then add those "non-detached" items into dbUser.AvailableCategories. Please see Chris's answer as it shows with specific code how to get this done.
Also, I might use a linking entity. Possibly something like this:
public class UserCategory
{
public User User {get;set;}
public Category Category {get;set;}
}
And add it to DB context. Also, drop the linking lists in your current User and Category class. This way you can manipulate the UserCategory class (and DbSet) to manage your many-to-many relationship.

Do I have to have 2 fields for one property in order to fill a dropdownlist?

My view is strongly typed to the following model:
public class CitizenDocument{
public string Name{get;set;}
public SocialStatus SocialStat{get;set;}
public AppType ApplicationType{get;set;}
public int Age{get;set;}
public ReplyMethod PreferredReplyMethod{get;set;}
.......//omitted for brevity
}
Here are the three custom types I used above
public Enum SocialStatus{
Servant=1,Worker,Peasent,Student,Unemployed,Pensioner
}
public Enum AppType{
Appliation=1,Complaint,Proposal,Query
}
public Enum ReplyMethod{
Letter=1,Verbal
}
Of course I'll need dropdownlists to present them in the view. The DropDownListFor<> method needs at least two things- one for storing the selected option value, the other one is the SelectList to populate the dropdownlist from. I don't want to pass the SelectList through ViewBag, which means the only way remaining is passing it inside the model. But this way I'll have to have a separate SelectList for all of the above three properties. Am I right? Or is there any other way that I'm not aware of? I've read lots of SO post regarding this but almost everyone recommends going model way as if there will always be one property of a model.
#{
var dictionary = Enum.GetValues(typeof(SocialStatus)).Cast<SocialStatus>().ToDictionary(item => (int)item, item => item.ToLocalizedString());
var selectList = new SelectList(dictionary, "Key", "Value", Model.SubscriptionTypeId);
}
#Html.DropDownListFor(m => m.SocialStatusId, selectList)
So just store id of selected enum in model.
P.S.: ToLocalizedString() is our custom method for getting the name of enum item

Resources