How to set and get multi select dropdown list for on form load using model binding in MVC [duplicate] - dropdownlistfor

I want to bind #Html.DropDownListFor from Model data without using Viewbag and look at many different examples on the web. But most of them use Viewbag or an extension method and I want a better approach to solve this problem. I tried the the following methods but it does not seem to work:
#Html.DropDownListFor(m => m.LabId, new SelectList(Model.Lab, "Id", "Name"),
"---- Select----", new { #class = "selectpicker" } )
Is it possible to bind #Html.DropDownListFor directly from Model without using Viewbag or any extra method in the Controller in ASP.NET MVC5? Could you please give some examples to perform this best?

The strongly typed view model approach which does not use dynamic stuff like ViewBag
You can add a new property to your view model for the SELECT options of type
IEnumrable<SelectListItem>.
view model is a simple POCO class used to transfer data between view to action method and vice versa. They are specific to the views. Add properties only needed for the view.
public class CreateUserVm
{
public IEnumrable<SelectListItem> Labs { set;get;}
public int SelectedLabId { set;get;}
//Add other properties as needed for the view
}
and in your GET action, create an object of this view model, load the Labs property and send that to the view.
public ActionResult Create()
{
var vm= new CreateUserVm();
// Hard coded for demo. You can replace with real data from db
vm.Labs = new List<SelectListItem> {
new SelectListItem { Value="1", Text="One" },
new SelectListItem { Value ="2", Text="Two" }
};
return View(vm);
}
and in the view which is strongly typed to this view model, call the DropDownListFor helper method
#model CreateUserVm
#Html.DropDownListFor(f=>f.SelectedLabId, Model.Labs,"Select one")
Pre-selecting an option in the dropdown
If you like to pre select one option when razor renders the page, You can set the SelectedLabId property value of your view model to the value property value of of the Option item(SelectListItem).
public ActionResult Create()
{
var vm= new CreateUserVm();
// Hard coded for demo. You can replace with real data from db
vm.Labs = new List<SelectListItem> {
new SelectListItem { Value="1", Text="SugarLab" },
new SelectListItem { Value ="2", Text="CandyLab" },
new SelectListItem { Value ="3", Text="SodaLab" }
};
vm.SelectedLabId = 2; // Will set "CandyLab" option as selected
return View(vm);
}
If you want to use real data, instead of the hard coded 2 items, you can do this
vm.Labs = dbContext.Labs.Select(x=>new SelectListItem { Value=x.Id.ToString(),
Text= x.Name }).ToList();
Assuming dbContext is your DbContext class object and it has a Labs property of type DbSet<Lab> where each Lab entity has an Id and Name property.

You are on a good track by having the list items in your model. I don't know how you have implemented that in your code.
The Lab property in your model class should be an IEnumerable, List or Collection of what you want. and then in your razor view, the looks fine.
Perhaps what you're forgetting is to initialise the list from the action method in the controller before sending it to the view. E.g:
var model = new ViewModel{
Labs = repository.GetLabs();
}
Above i assume the repository should be something that has access or means of getting the needed data, and also that the Labs property is defined as IEnumerable<Lab> Labs in the ViewModel class.
All should work. Perhaps you should be clear as to what error you're getting.

Related

Instantiating ModelExpression directly

Let's say I have the following input tag which utilizes the built-in tag helper:
#model ProductViewModel
<label asp-for="Product.Id"></label>
In my case, this expands into the following:
<label for="Product_Id">Id</label>
I see that asp-for is expecting a ModelExpression:
In tag helper implementations, I often see a property like the following:
public ModelExpression For { get; set; }
It appears that this is automatically populated when the tag helper is used.
Is there a way to instantiate a ModelExpression directly in C#?
I.e. something like this:
var exp = new ModelExpression("Product.Id",...)
I'd like to be able to generate "Product_Id" and "Id" from Product.Id as the input tag helper did.
As far as I know, you can specify that your property is to be set to the name of some property on the View's Model object by declaring your property with the ModelExpression type. This will enable any developer using your property to get IntelliSense support for entering a property name from the Model object. More importantly, your code will be passed the value of that property through the ModelExpression's Model property.
Sample code as below:
[HtmlTargetElement("employee-details")]
public class EmployeeDetailTagHelper : TagHelper
{
[HtmlAttributeName("for-name")]
public ModelExpression EmployeeName { get; set; }
[HtmlAttributeName("for-designation")]
public ModelExpression Designation { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "EmployeeDetails";
output.TagMode = TagMode.StartTagAndEndTag;
var sb = new StringBuilder();
sb.AppendFormat("<span>Name: {0}</span> <br/>", this.EmployeeName.Model);
sb.AppendFormat("<span>Designation: {0}</span>", this.Designation.Model);
output.PreContent.SetHtmlContent(sb.ToString());
}
}
Code in the View page:
#model WebApplication7.Models.EmployeeViewModel
<div class="row">
<employee-details for-name="Name" for-designation="Designation"></employee-details>
</div>
Code in the Model
public class EmployeeViewModel
{
public string Name { get; set; }
public string Designation { get; set; }
}
From above code, you can see that we could custom the attribute name. More detail information about using the ModelExpression, check the following links:
Creating Custom Tag Helpers With ASP.NET Core MVC
Expression names
I'd like to be able to generate "Product_Id" and "Id" from Product.Id
as the input tag helper did.
Besides, do you mean you want to change the Product. Id to Product_Id, in my opinion, I'm not suggesting you change it, because generally we can use "_" as a separator in the property name. So, if we are using Product.Id, it means the Product's Id property, and the Product_Id means there have a Product_Id property.
To answer the question:
Is there a way to instantiate a ModelExpression directly in C#"
Yes you can, through IModelExpressionProvider and its CreateModelExpression method. You can get an instance of this interface through DI.
Now, if you're already in your view and working with tag helpers, Zhi Lv's answer is all you need, as the functionality is built-in and much easier to use. You only need IModelExpressionProvider for when you're in your Razor Page, Controller, or perhaps some custom middleware. Personally, I find this functionality useful for my Ajax handlers that need to return one of my ViewComponents that has a ModelExpression argument (so that I can easily call it from my Pages/Views too.)
To call CreateModelExpression, you'll need a strongly-typed instance of ViewData. In Razor Pages, this is as easy as casting the ViewData property to the strongly-typed instance of your PageModel's type (presuming you don't have a page model hierarchy):
var viewData = (ViewDataDictionary<IndexModel>)ViewData;
If you're using MVC and you're in the controller, that won't exist yet. Best you can do is make your own instance.
var viewData = new ViewDataDictionary<ErrorViewModel>(new EmptyModelMetadataProvider(),
new ModelStateDictionary());
Once you get your strongly-typed ViewData instance, you can obtain your desired ModelExpression like this, just like using a lambda expression in your views:
var myPropertyEx = _modelExpressionProvider.CreateModelExpression(viewData,
m => m.MyProperty);

How to bind dynamic complex objects created using partial-view to a collection property in view-model

I'm unable to bind a collection of child-complext objects created dynamically using a partial-view to view-model IEnumerable property.
I have successfully bound objects created dynamically using partial-views to a view-model using a technique I found on this blog https://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/. I have followed the same technique but I'm unable to bind a collection to a IEnumerable property in a view-model.
[BindRequired]
public class EmployeeViewModel
{
other properties....
public IEnumerable<ContactDetailViewModel> EmployeeContact { get; set; }
}
[BindRequired]
public class ContactDetailViewModel
{
// I use this as my indexer for dynamic elements
public string RecordId { get; set; } = Guid.NewGuid().ToString();
public string Telephone { get; set; }
public string EmailAddress { get; set; }
public string ContactDescription { get; set; }
}
I call into this action-method via ajax to add dynamic contact detail elements and it returns the partial-view as html and it works fine.
[Route("[action]", Name = "BlankEmployeeContactDetail"), HttpGet("AddBlankContactDetail")]
public PartialViewResult AddBlankContactDetail()
{
return PartialView("_ContactInformation", new ContactDetailViewModel());
}
The initial contact detail is added to the main-view using the following, kindly follow this link https://1drv.ms/u/s!AkRSHVUtFlKhuHaxH96Ik4ineATE to download the main view and partial-view cshtml files. It is also noteworthy to mention that model binding fails for all other properties when I include this partial-view but works when I comment it out. I'm baffled and would greatly appreciate any help you can afford me.
<section id="widget-grid" class="">
<div class="row contactContainer">
#{ await Html.RenderPartialAsync("_ContactInformation", new ContactDetailViewModel()); }
</div>
</section>
This is the controller action method I'm trying to bind posted data to:
[Route("[action]"), HttpPost, AllowAnonymous, ValidateAntiForgeryToken]
public IActionResult Register([FromForm] EmployeeViewModel model, [FromQuery] string returnUrl = null)
{
if (ModelState.IsValid)
{
}
return View(model);
}
In order to bind, the input names much follow a particular convention that maps to what you're binding to. While it's unclear from your question, my best guess is that you're trying to ultimately bind to an instance of EmployeeViewModel, which means that your contact information inputs would need names like: EmployeeContact[0].Telephone, but when you pass an instance of ContactDetailViewModel along as the "model" of the partial view, the names will be just Telephone, and worse, these same names will be repeated over and over, i.e. each contact information set of fields you create will all have an input named just Telephone.
Long and short, you need the context of the whole model to generate the correct input names. You have a couple of options.
Since you're retrieving the set of fields via an AJAX request, it would be possible to pass the "prefix" to use along with that request. In other words, you can keep track of an index value, counting how many of these sections you've added, and then send along with the request for a new section something like
prefix: 'EmployeeContact[' + (i + 1) + ']',
Then, in your partial view:
#{ await Html.RenderPartialAsync("_ContactInformation", new ContactDetailViewModel(), new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = ViewBag.Prefix } } ); }
That's a little hacky, and honestly probably rather prone to error, though. The better option would be to take an entirely different approach. Instead of calling back to get the partial view, define it just once as a template:
<script type="text/html" id="ContactInformationTemplate">
<!-- HTML for contact information inputs -->
</script>
Then, using a library like Vue, React, Angular, etc., you can set up a "foreach" construction tied to a particular JavaScript array which uses this template to render items in that array. Then, adding a new set of inputs is as simple as adding a new item to the array. You will have to do some works to customize the input names based on the index of the item in the array, but all of these client-side frameworks have ways to do that. That would also have the side benefit of not having to make an AJAX request every time you want to add a new section.

Pass parameter to Partial View in ASP.NET Core

On an ASP.NET Core 2.0 application I need to render a partial view and pass a few parameters:
#Html.Partial("Form", new { File = "file.pdf" })
On the partial view I tried to access it using:
#Model.File
And I get the error:
RuntimeBinderException: 'object' does not contain a definition for 'File'
If I simply use on my partial:
#Model
I get the following printed on the page:
{ File = file.pdf }
So there the model is being passed and there is a property File in it.
So what am I missing?
You are passing untyped (anonymous type) data to partial view. You cannot use #Model.File. Instead, you will need to use ViewData's Eval method to retrieve the value.
#ViewData.Eval("File")
Traditional approach is to create a strongly typed ViewModel class, and pass it to the partial view. Then you can access it as #Model.File.
public class SampleViewModel
{
public string File { get; set; }
}
#Html.Partial("Form", new SampleViewModel { File = "file.pdf" })
Inside Partial View,
#model SampleViewModel
<h1>#Model.File</h1>
You should have dynamic as the model of your partial view, this way, you can pass everything - like your anonymous object - and it will just work. Add:
#model dynamic
To the Form.cshtml file.
When you do new { File = "file.pdf" }, you are passing an object that contains an attribute file. Since this is of type object, you can't access any of its variables in c# directly. There some ugly codes to access a fields from an object such as the one that can be found here: C# .NET CORE how to get the value of a custom attribute?
However in this case the most recommended way (for safety) is the create a class and pass an object of that class.
So if you create the following class:
public class MyFileInfo{
public string File { get; set }
}
Then you can create your object by passing:
#Html.Partial("Form", new MyFileInfo{ File = "file.pdf" })
In your partial view, at the beginning, define your model class
#model MyFileInfo
Then in the same file you will now be able to access
#Model.File
For the sake of completeness: you can pass arbitrary variable via a ViewDictionary
#Html.Partial("_Partial", model, new ViewDataDictionary(ViewData) { { "MyVarName", someValue } })
And then access it like this in the partial:
ViewData["MyVarName"]
Another option, you can simply set a ViewData var before calling the partial, and it will be passed on to it
#{
ViewData["MyVarName"] = "hello";
}
#Html.Partial("_Partial", model)
However, strongly typed models are much easier to work with.

Instantiating ViewModel for a View

Update Binding hidden fields of a viewmodel.
Let me try to explain my situation. I may be completely wrong but this is what I believe causing issue to me.
I have a ViewModel
Project Create View Model
[Bind(Exclude="List")]
public class ProjectCreateViewModel : ProjectViewModelBase
{
public CourseViewModelBase CourseVM { get; set; }
public ProjectCreateViewModel()
: base()
{
this.CourseVM = new CourseViewModelBase();
}
}
Project View Model Base is the base viewModel for a project and all associated actions derive from this so that I don't need to write property names again and again.
Create View Model Base is similar to ProjectViewModelBase(handled or used by ProjectController) but for a course (handled by CourseController).
Now I've created a form for "Create New Project" which uses ProjectCreateViewModel. In Form post action however CourseVM is always null.
Create New Project .cshtml
#model LMSPriorTool.ViewModels.ProjectCreateViewModel
#* --- labels and other stuff -- *#
#using (Html.BeginForm("CreateNewProject", "Project",
FormMethod.Post, new { #class = "form-horizontal",
name = "createNewProjectForm" }))
{
<!-- Hidden Fields -->
#Html.HiddenFor( x => x.ProjectId)
#Html.HiddenFor( x => x.CourseVM) // CourseVM is null in post action
#Html.TextBoxFor(x => x.CourseVM.CourseNumberRoot) // This is displayed properly
}
ProjectController
[HttpGet]
public ActionResult CreateNewProject(CourseViewModelBase courseVM = null)
{
ProjectCreateViewModel projectCreateViewModel = new ProjectCreateViewModel
{
CourseVM = courseVM,
};
// OTHER STUFF
return View("CreateNewProject", projectCreateViewModel);
}
Error
In HTTPPOST action I'm getting CourseVM as null, though I have provided it as a hidden field in form.
Possible Issue I belive issue is with the Constructor of ProjectCreateViewModel as when HTTPPOST action occur, view will try to create new instance of ProjectCreateViewModel and instantiate the CourseVM as null. Then same instance is passed to the HTTPPOST method in which CourseVM is appearing as null.
UPDATE: ISSUE ROOT CAUSE Complex objects cannot be bind to a viewmodel using Hidden Fields.
Any suggestions or thoughts appreciated.
You don't need that HiddenFor of CourseVM. MVC will create the class automatically for you because you are binding CourseVM.CourseNumberRoot
At the moment, you are attempting to bind CourseVM, which is a complex object, from a hidden input which MVC can't do, so it is returning null.

ASP.NET MVC 2 RC model binding with NHibernate and dropdown lists

I have problem with model binding in my ASP.NET MVC 2 RC application that uses NHibernate for data access. We are trying to build the application in a Ruby on Rails way and have a very simple architecture where the domain entities are used all the way from the database to the view.
The application has a couple of domain entities which can be illustrated by the following two classes:
public class Product {
...
public Category Category { get; set; }
}
public class Category {
public int Id { get; set; }
public string Name { get; set; }
}
In the view that renders the edit form has the following statement to display a dropdown list:
<%= Html.DropDownListFor(model => model.Category.Id,
new SelectList(ViewData["categories"] as IList<Category>, "Id", "Name"),
"-- Select Category --" ) %>
Please disregard the use of "non-typed" view data to hold the category collection.
The action method that receives the form post is similar to to the following. Note that the TransactionFilter attribute adds NHibernate transaction handling and commits the transaction if no exceptions occur and validation succeeds.
[HttpPost]
[TransactionFilter]
public ActionResult Edit(int id, FormCollection collection) {
var product = _repository.Load(id);
// Update the product except the Id
UpdateModel(product, null, null, new[] {"Id"}, collection);
if (ModelState.IsValid) {
return RedirectToAction("Details", new {id});
}
return View(product);
}
My issue is that the product.Category.Id is set with the value selected in the form, e.g. Category.Id = "2". Using the default model binder results in the following type of NHibernate exception:
identifier of an instance of Name.Space.Entities.Category was altered from 4 to 2
That makes a lot of sense since the product already has a category assigned and only the primary key of that existing category is being changed. Another category instance should have been assigned instead.
I guess a custom ModelBinder can be created to handle this issue but is there a simpler way to make this work? Can (and should) the domain entities be modified to handle this?
I solved a similar problem with combo boxes on my edit page by changing the following line
#Html.DropDownListFor(x => x.Employee.Id, new SelectList(ViewBag.Employees, "Id", "DisplayName"))
by
#Html.DropDownListFor(x => x.Employee, new SelectList(ViewBag.Employees, "Id", "DisplayName"))
So I removed the '.Id' like Bryan suggested. Before the change, the model only contained the Id of the Employee and after the change, the Binder also loaded all the details of the employee into the model.
I've used similar techniques with Linq to SQL classes before with no problems. I don't think you'd need a custom ModelBinder for this. UpdateModel should be updating the Product class you are passing into it - not the Category sub-class attached to it. Check the html generated by the DropDownListFor helper. What is the name of the element? It should be the name of the foreign-key field in your Products table (e.g. "CategoryID" or "Product.CategoryID" not "Category.Id"). If it's "Category.Id" - try changing the first parameter of the DropDownListFor to either "model => model.Category" or "model => model.CategoryID" (or whatever the foreign key field is). This should cause UpdateModel to only update the foreign-key field in the Product class and not the Category class ID.
The solution we chose at the time was something similar to this:
TryUpdateModel(product, null, null, new[] {"Category"}, collection);
int categoryId;
if (int.TryParse(collection["Category.Id"], NumberStyles.Integer, CultureInfo.InvariantCulture, out categoryId) && categoryId > 0) {
product.Category = _categoryRepository.Load(categoryId);
}
else {
product.Category = null;
}
We simply tell the model binder to exclude the association property and handle that manually. Not pretty but worked at the time....