Dropdownlist asp.net - c#

I have an application that allows user to create new items. As it stands now the user need to fill out Title, body and category.
The category is a textbox but I would like to convert it to a dropdownlist. This is all connected to a database and when the user submits it the data should be saved in the database. Everything works fine as it stands now, I am only having trouble implementing the dropdownlist.
My model:
public class NewsItem
{
public int ID { get; set; }
[Required(ErrorMessage = "Title is Required")]
public string Title { get; set; }
[Required(ErrorMessage = "Body is Required")]
public DateTime DateCreate { get; set; }
public string Category { get; set; }
}
What is the quickest/best way implementing this. Should I do it in the model or can I assign the values available in the view?
Thanks in advance!

First, some semantics. Since Entity Framework comes along with MVC, the assumption is that your POCOs are "models". (Unfortunately, Microsoft kind of piles on by putting scaffolded POCOs in a "Models" folder). However, these are not models in terms of the "Model" in Model-View-Controller; instead, they are merely "entities", which is a fancy way of saying "pretty much just a DTO EF can use to stuff data from the database into."
I point that out to say this: no, you shouldn't put your drop down list choices on your entity. But, you shouldn't rely on the view for this either. What you really want here is a view model. You create a class that has just the fields you need to edit and any additional business logic your view needs (such as the choices of categories), and then you map your entity to and from this view model. As an example:
public class NewsItemViewModel
{
[Required(ErrorMessage = "Title is Required")]
public string Title { get; set; }
[Required(ErrorMessage = "Body is Required")]
public DateTime DateCreate { get; set; }
public string Category { get; set; }
public IEnumerable<SelectListItem> CategoryChoices { get; set; }
}
Notice that while this class is mostly the same, it doesn't contain an Id property, because this is not something you'd want the user to edit. Also, it includes a CategoryChoices property to hold the items for your drop down list.
Then in your controller, you would do something like:
public ActionResult CreateNewsItem()
{
var model = new NewsItemViewModel();
model.CategoryChoices = db.Categories.Select(m => new SelectListItem { Value = m.Name, Text = m.Name });
return View(model);
}
Basically, you're just newing up the view model so you can feed it to the view. You fill in your category choices before actually returning it, though. I've assumed they're also entities, but you would use whatever method you needed to fetch them here, otherwise.
For your post action:
[HttpPost]
public ActionResult CreateNewsItem(NewsItemViewModel model)
{
if (ModelState.IsValid)
{
// map view model to entity
var newsItem = new NewsItem
{
Title = model.Title,
Category = model.Category,
// and so on
}
db.NewsItems.Add(newsItem);
db.SaveChanges();
return RedirectToAction("Index");
}
model.CategoryChoices = db.Categories.Select(m => new SelectListItem { Value = m.Name, Text = m.Name });
return View(model);
}
I'm just doing a manual mapping from the view model to a new news item, here, but for real world scenarios, you'd probably want to integrate a mapping library for this, such as AutoMapper. Also, take note that in the case where there's an error, you must refill the category choices before returning the view again. These will not be posted by your form, so the model that was passed in will not have them.
Finally, in your view:
#model Namespace.To.NewsItemViewModel
...
#Html.DropDownListFor(m => m.Category, Model.CategoryChoices)

Related

ViewModel with a SelectList and a LINQ Query result with a single reocrd

How can we populate a ViewModel with following two properties:
A query result type from a LINQ query that returns only a single record
A SelectList type
In other words, suppose we have a view that displays only a single movie based on the selected ID. And we want to assign a year of release to that movie from a dropdown list of years, how do I fill in the ???? in the following ViewModel and Controller?
Note: In the following ViewModel if I use myMovie property of type IQueryable<Movie> and assign this property of ViewModel to the query qrySingleMovie from following controller I get the error the name qrySingleMovie does not exist in the current context
Models
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
public class Year
{
public int YearId {get; set;}
public int MovieYear {get; set;}
}
ViewModel
public class MovieGenreViewModel
{
public ???? myMovie;
public SelectList MovieReleaseYears;
public int ReleasedYr { get; set; }
}
Controller
public async Task<IActionResult> Index(int MovieID)
{
// Use LINQ to get list of Release years.
IQueryable<string> YearQuery = from m in _context.Years
orderby m.MovieYear
select m.MovieYear;
//Following query selects only a single movie based on Primary Key ID
var qrysingleMovie = from m in _context.Movies
where m.ID == movieID
select m;
var movieReleaseYrVM = new MovieGenreViewModel();
movieReleaseYrVM.MovieReleaseYears = new SelectList(await YearQuery.ToListAsync());
movieReleaseYrVM.myMovie = ????
return View(movieReleaseYrVM);
}
UPDATE
In my real project, the model has lots of properties and the corresponding View also displays most of those properties. I was thinking if there is a simpler way to define a ViewModel that does not contain all the properties of the model such as this ASP.NET Article defines a view model MovieGenreViewModel but there they are using List and in their View they are iterating through multiple Movies. I'm trying to mimic their example but in my case it's a single record (not the list of records) with a SelectList. How can I mimic that kind of scenario with a single record and not be able to include in View Model tons of model properties that model has?
Since you want to update a Movie record, all you need to get the movie record is it's unique Id. So why not add a MovieId property to your view model. If you prefer to show the MovieName in the UI, you may add a MovieName property as well.
Remember view models are specific to views. It is not necessary to mimic your entire entity model when you create a view model. Add only those properties your view absolutely need.
public class MovieGenreViewModel
{
public int MovieId;
public string MovieName { set;get;}
public SelectList MovieReleaseYears;
public int ReleasedYr { get; set; }
}
Now in your GET action set these property values.
public async Task<IActionResult> Index(int MovieID)
{
var vm = new MovieGenreViewModel();
var movie = _context.Movies.FirstOrDefault(x=>x.ID==movieID);
if(movie==null)
return Content("Movie not found"); // to do :Return a view with "not found" message
IQueryable<string> YearQuery = from m in _context.Years
orderby m.MovieYear
select m.MovieYear;
// set the property values.
vm .MovieReleaseYears = new SelectList(await YearQuery.ToListAsync());
vm .MovieId= movie.ID;
vm .MovieName= movie.Name;
return View(vm );
}
Now in your view, you keep the movie id in a hidden field so that when you submit the form, it will be submitted to the HttpPost action method which handles the form submit.
#model MovieGenreViewModel
#using(Html.BeginForm("AssignYear","Home"))
{
<p>#Model.MovieName</p>
#Html.HiddenFor(s=>s.MovieId)
#Html.DropDownListFor(d=>d.ReleasedYr, Model.MovieReleaseYears)
<input type="submit"/>
}
You can use the same view model as your AssignYear action method parameter
[HttpPost]
public ActionResult AssignYear(MovieGenreViewModel model)
{
// check model.MovieId and model.ReleasedYr
// to do : Save data and return something
}
If you do not prefer to add only those properties needed by the view to the view model, but want to show a tons of properties of the Movie, you may consider adding a new property of type Movie to your view model
public class MovieGenreViewModel
{
public Movie Movie { set;get;}
public SelectList MovieReleaseYears;
public int ReleasedYr { get; set; }
}
Your existing LINQ statement returns a collection of one element. You cannot assign a collection of Movies to a property of type Movie. You may use the FirstOrDefault() method to get a single object.
var movie = _context.Movies.FirstOrDefault(x=>x.ID==movieID);
//After null check
vm.Movie = movie;
Why canĀ“t you use Movie as the type for your "myMovie" property ?
Then you can use that object to display the name for example.ie movie.Name.
You can also store the movieId in a hidden field so when you post back it will be available for you to use in the server side.
You can use LINQ to retrieve that object from the database like so:
var qrysingleMovie = _context.Movies.Single(m=> m.ID == movieID);
Alternatively you can use 2 properties instead of the whole object. One for the title ie: string MovieName and another one for the id int MovieId
public string MovieName { get; set; }
public int MovieId { get; set; }
Note: Bear in mind that using "Single" means that you need to have the movie in the database otherwise it will throw an exception.

#Html.Dropdownlist list isn't saving to database

In the below example I'm trying to save the Id from the Asp.Net Identity, "aspnetusers" table "Id" column into my "Application" table. The dropdownlist of "fullname" populates, but is not saving to my "application" table when I submit an application. I need the "Id" from the aspnetusers table to be saved to my "application" table after submitting the application. Help would be greatly appreciated!
Controller:
private ApplicationDbContext db = new ApplicationDbContext();
public ActionResult Index()
{
ViewBag.FullNameList = new SelectList(db.Users,"Id","FullName");
return View();
}
// POST: Applications/Create
[ValidateAntiForgeryToken]
public ActionResult Index([Bind(Include = "Id,FullName, FirstName,MiddleInitial,LastName,")] Application application)
{
if (ModelState.IsValid)
{
ViewBag.FullNameList = new SelectList(db.Users, "Id", "FullName", application.ApplicationUser);
db.Applications.Add(application);
db.SaveChanges();
return RedirectToAction("Thanks");
}
}
View:
<p>
#Html.DropDownListFor(m => m.FullName, (SelectList)ViewBag.FullNameList, "Select Loan Officer")
</p>
Model:
public class Application
{
public int ApplicationId { get; set; }
[Required]
[DisplayName("First Name")]
public string FirstName { get; set; }
[DisplayName("Middle Initial")]
public string MiddleInitial { get; set; }
[Required]
[DisplayName("Last Name")]
public string LastName { get; set; }
public virtual string FullName {get; set; }
public virtual ApplicationUser ApplicationUser { get; set; }
}
Many things wrong:
Your post action accepts Application, which doesn't have a FullName property.
Even if it did, your Bind attribute doesn't included it.
You can't have a ViewBag member holding your select list with the same name as the field you're trying to post. Change it to something like ViewBag.FullNameChoices.
The posted value would be the Id of the "loan officer" user and you're doing nothing with it. If you actually had a foreign key property, you could post directly to that, but instead you're just relying on EF to create an implicit property which you have no access to. In your post action, you would need to look up the user with that Id from the database and then set your ApplicationUser property to that.
While not technically wrong, having a property that represents the "loan officer" for an application call ApplicationUser is not intuitive. You should change it to something like LoanOfficer. Also, it looks like your assuming that all users for all time will also be "loan officers", you should probably plan some flexibility by creating a subclass of ApplicationUser for a loan officer or use roles to assign that status.
Are you forgetting to add an [HttpPost]?
Also, your DropDownList might be:
#Html.DropDownList("FullName", ViewBag.FullName, "Select Loan Officer")

ASP.NET MVC4 with EF 6 Model Update Issue

I have two models as below
public class Category
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int ID { get; set; },
[Required]
public string category { get; set; }
[Required]
public string Desc { get; set; }
}
public class Product
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int ID { get; set; },
public int CatID { get; set; },
[ForeignKey("CatID")]
public virtual Category Category { get; set; },
[Required]
public string Desc { get; set; },
public string DisplayName
{
get
{
return string.format("{0} - {1}",this.Category.category,this.Desc);
}
}
}
This is my Edit Action
public ActionResult Edit(int id)
{
ViewBag.PossibleCategories = categoryRepository.All;
return View(productRepository.Find(id));
}
[HttpPost]
public ActionResult Edit(Product product)
{
if (ModelState.IsValid) //<== This becomes false saying category.desc is required
{
productRepository.InsertOrUpdate(product);
productRepository.Save();
return RedirectToAction("Index");
}
else
{
ViewBag.PossibleCategories = categoryRepository.All;
return View();
}
}
I have a scaffolded a Edit view of product and it shows ID and DisplayName as Readonly. All the other fields a editable.
The edit view also has the product -> category -> category has a read-only text field
#Html.TextBoxFor(model => model.Category.category, new Dictionary<string, object>() { { "readonly", "true" } })
The Post back sends this and tries to create a new category. This is not required. The category link will be carried forward using the product.CatID.
How can i display these types of fields??
When the Edit view Post back the Model state appears as invalid because the product's category's desc is null (product -> category -> desc).
if i comment out the DisplayName property in Product this issue doesn't occur.
From my understanding, this is because the DiaplayName property refers to Category property and the view view doesn't have category.desc field so when the model is created back on the POST action, the desc is not populated. Adding the category.desc field to the view is one way of solving this problem.
Is there any other method to solve this?
Note: This is not the only model i'm having this issue. There are many complex models which have the same problem and to me having these fields also included in the view would make for (1) a very cluttered view (2) the amount of data making the round trip will be high.
Simple Solution
Check for null. Really you should be making this a habit anyway.
public string DisplayName
{
get
{
if(this.Category != null)
{
return string.format("{0} - {1}",this.Category.category,this.Desc);
}
else
{
return String.Empty;
}
}
}
Complex Solution
Instead of directly using your database model in your Views another solution is to create ViewModels. These are models meant specifically for your View. As a simplified example, let's take your Product model and create a ViewModel.
Create a folder for your ViewModels
Create ViewModel files that match your Controller
Create a ViewModel that you will use in your View
Say you have a Store Controller. This would be the file structure you would create.
Models
ViewModels
StoreViewModels.cs
Inside the StoreViewModels you would create a ViewModel called ProductViewModel which you would fill in with information from Product.
public class ProductViewModel
{
public int ID { get; set; }
public string Description { get; set; }
public string DisplayName { get; set; }
public ProductViewModel() { }
public ProductViewModel(Product product)
{
this.ID = product.ID;
this.Description = product.Description;
this.DisplayName = product.DisplayName;
}
}
In your View you reference ProductViewModel instead of Product. On the receiving end you then translate the ViewModel fields back to your Model. Let me know if you have any questions.

Null values on model after sending model to controller

I'm using .net MVC.
I have some values in a form like:
#Html.TextBoxFor(model => model.Name, new { #class = "form-control" })
On the controller, I get the data like:
public ActionResult NovaPessoa(Person person)
{
The problem is that I just can get values that I have placed the #Html.TextBoxFor markup.
All the other complex information, like person.ContactInformation is lost after submiting and I can't use the SaveChanges in Entity Framework, because it will give me an invalid object after using the Atach method.
The question is: Do I need to use the #Html.TextBoxFor markup for all my model properties, even if I'm not using then to display anything, just to have them on Controller?
You are correct. What people (incorrectly) do normally, is use HiddenFor:
#Html.HiddenFor(model => model.ContactInformation)
What you should be doing, is cutting down your model into a view model with only the appropriate properties.
So, don't use this model:
public class PersonVM {
public int PersonId { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public string ContactInformation { get; set; }
}
..if all you're doing is updating the contact information. Instead, create a new class for your model:
public class PersonContactInfoEditVM {
public int PersonId { get; set; }
public string ContactInformation { get; set; }
}
That's all you need. This saves you from creating invalid objects when you don't add 30 HiddenFor elements to your page .. resulting in very broken data.
You might be thinking "ugggghhhh, all that manual mapping from PersonContactInfoEditVM to Person... I don't want to be writing that sort of code". No one does.. which is why the following libraries exist:
AutoMapper
ValueInjector

ASP.NET MVC - how to capture and save multiple complex ListBox values to database

So, I've created a View that contains two ListBoxes, AvailableServices and SelectedServices:
#Html.ListBoxFor(x => x.AvailableServices, Enumerable.Empty<SelectListItem>(), new { id = "serviceID" })
#Html.ValidationMessageFor(model => model.AvailableServices)
#Html.ListBoxFor(x => x.SelectedServices, Enumerable.Empty<SelectListItem>(), new { id = "selectedserviceID" })
#Html.ValidationMessageFor(model => model.SelectedServices)
For reference, here is my ViewModel:
namespace Services.ViewModels
{
public class SPServiceTypeViewModel
{
public int Id { get; set; }
public int SPCompanyAccountID { get; set; }
[Display(Name = "Service Category")]
public IEnumerable<SelectListItem> ServiceCategory { get; set; }
[Display(Name = "Available Services")]
public IEnumerable<SelectListItem> AvailableServices { get; set; }
[Display(Name = "Your Services")]
public IEnumerable<SelectListItem> SelectedServices { get; set; }
}
}
My Controller populates the AvailableServices ListBox without issue. And I wrote some JavaScript that lets the user move items (including id and label) from the AvailableServices ListBox to the SelectedServices listbox. No problem there either.
Now here's my problem... I've read a variety of posts but I still don't understand how to best pass data from my SelectedServices ListBox back to my controller upon form submission, because I need to capture both the id and label for each selection.
My goal here is to create a new database row in my SPServiceType table for each item in the SelectedServices ListBox, and I'm clueless. Right now my Controller for saving data looks like this:
[HttpPost]
public ActionResult Create(SPServiceTypeViewModel viewModel)
{
foreach (var item in viewModel.SelectedServices)
{
var spServiceType = new SPServiceType
{
SPCompanyAccountId = viewModel.SPCompanyAccountID,
ServiceCategory = ???,
};
db.SPServiceType.Add(spServiceType);
db.SaveChanges();
return RedirectToAction("Create", "SPServiceLocation");
}
Do I need to not use IENumerable in my ViewModel? Do I need to use JavaScript to pass my SelectedServices values to a hidden List<> prior to submission so that my model binding is easier to accomplish?
To reiterate, I want to capture the id and label values of the selections.
Any code examples on how to approach this within the View, or Post action in the Controller, or general approach advice, would be greatly appreciated.
I had a similar question here.
The recommendation at the time was to accept the selected values in the POST and use a repository pattern (caching if necessary) to translate the values to labels as needed.

Categories