ListBoxFor in .NET Core not binding - asp.net-mvc

I cannot get this ListBox to have a value. It worked in MVC4.5, from which I am translating; cannot get to work in MVC Core. Swear it was working at some point; now it is not.
Have tried:
https://stackoverflow.com/a/19144613/2496266
https://stackoverflow.com/a/40308906/2496266
etc.
View
<div class="tb-field desc-field">
#Html.ListBoxFor(model => model.workerRequests,
new SelectList(ViewBag.workerRequests, "Value", "Text"),
new { id = "workerRequests2_WO-"+ Model.ID, tabindex = "22", style="min-width: 16em;"}
)
</div>
Controller (GET)
wo.workerRequests = new List<ViewModel.WorkerRequest>();
ViewData["workerRequests"] = new SelectList(wo.workerRequests);
return PartialView("Create", wo);
Controller (POST)
public async Task<ActionResult> Create(WorkOrder wo, string userName)
{
ModelState.ThrowIfInvalid();
var modelUpdated = await _adaptor.TryUpdateModelAsync(this, wo);
// ... "wo.WorkerRequests" is empty here ^
}
Entity
public virtual ICollection<WorkerRequest> workerRequests { get; set; }
Additional info:
Apparently it is coming across in the Request, but as a StringValue:
...apparently all of them are string values. It's not getting bound to its desired type.
Basically:
It has getters and setters on the entity.
I'm populating the ViewData before serving the page.
I'm using a SelectList(arg) where arg is of type myModel.myProperty.
We are selecting the Worker in the WorkerRequest from the database (nested type). Is this due to the lazy-loading bugs? What am I doing wrong?

It looks like you are creating 2 SelectList objects for the view.
Once in controller:
ViewData["workerRequests"] = new SelectList(wo.workerRequests);
And again in the view:
#Html.ListBoxFor(model => model.workerRequests,
new SelectList(ViewBag.workerRequests, "Value", "Text"),
You only need to create one and the best place would be in the view. So change your controller code to populate the ViewData with the data and let the view create the SelectList to display:
ViewData["workerRequests"] = wo.workerRequests;
Now you should see the list box populated with the data.

For the curious, this is what we ended up with:
Controller
public ActionResult Create(Domain.WorkOrder wo, string userName, List<Domain.WorkerRequest> workerRequestList)
{
UpdateModel(wo);
Domain.WorkOrder neworder = woServ.Create(wo, userName);
// JSON object with new work order data
var result = map.Map<Domain.WorkOrder, ViewModel.WorkOrder>(neworder);
return Json(new
{
sNewRef = result.tabref,
sNewLabel = result.tablabel,
iNewID = result.ID
},
JsonRequestBehavior.AllowGet);
}
View
<div class="tb-label desc-label">
#Html.LabelFor(model => model.workerRequests)
<br />
<input type="button" value="#Machete.Web.Resources.WorkOrder.requestAdd" class="formButton" id="addRequestBtn-#(Model.ID)"/>
<input type="button" value="#Machete.Web.Resources.WorkOrder.requestRemove" class="formButton" id="removeRequestBtn-#(Model.ID)"/>
</div>
<div class="tb-field desc-field">
#Html.ListBox("workerRequests2", new SelectList(ViewBag.workerRequests, "Value", "Text"), new { id = "workerRequests2_WO-"+ Model.ID, tabindex = "22", style="min-width: 16em;"})
</div>

Related

Creating dynamic forms with .net.core

I have a requirement to have different forms for different clients which can all be configured in the background (in the end in a database)
My initial idea is to create an object for "Form" which has a "Dictionary of FormItem" to describe the form fields.
I can then new up a dynamic form by doing the following (this would come from the database / service):
private Form GetFormData()
{
var dict = new Dictionary<string, FormItem>();
dict.Add("FirstName", new FormItem()
{
FieldType = Core.Web.FieldType.TextBox,
FieldName = "FirstName",
Label = "FieldFirstNameLabel",
Value = "FName"
});
dict.Add("LastName", new FormItem()
{
FieldType = Core.Web.FieldType.TextBox,
FieldName = "LastName",
Label = "FieldLastNameLabel",
Value = "LName"
});
dict.Add("Submit", new FormItem()
{
FieldType = Core.Web.FieldType.Submit,
FieldName = "Submit",
Label = null,
Value = "Submit"
});
var form = new Form()
{
Method = "Post",
Action = "Index",
FormItems = dict
};
return form;
}
Inside my Controller I can get the form data and pass that into the view
public IActionResult Index()
{
var formSetup = GetFormData(); // This will call into the service and get the form and the values
return View(formSetup);
}
Inside the view I call out to a HtmlHelper for each of the FormItems
#model Form
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#using FormsSpike.Core.Web
#{
ViewData["Title"] = "Home Page";
}
#using (Html.BeginForm(Model.Action, "Home", FormMethod.Post))
{
foreach (var item in Model.FormItems)
{
#Html.FieldFor(item);
}
}
Then when posting back I have to loop through the form variables and match them up again. This feels very old school I would expect would be done in a model binder of some sort.
[HttpPost]
public IActionResult Index(IFormCollection form)
{
var formSetup = GetFormData();
foreach (var formitem in form)
{
var submittedformItem = formitem;
if (formSetup.FormItems.Any(w => w.Key == submittedformItem.Key))
{
FormItem formItemTemp = formSetup.FormItems.Single(w => w.Key == submittedformItem.Key).Value;
formItemTemp.Value = submittedformItem.Value;
}
}
return View("Index", formSetup);
}
This I can then run through some mapping which would update the database in the background.
My problem is that this just feels wrong :o{
Also I have used a very simple HtmlHelper but I can't really use the standard htmlHelpers (such as LabelFor) to create the forms as there is no model to bind to..
public static HtmlString FieldFor(this IHtmlHelper html, KeyValuePair<string, FormItem> item)
{
string stringformat = "";
switch (item.Value.FieldType)
{
case FieldType.TextBox:
stringformat = $"<div class='formItem'><label for='item.Key'>{item.Value.Label}</label><input type='text' id='{item.Key}' name='{item.Key}' value='{item.Value.Value}' /></ div >";
break;
case FieldType.Number:
stringformat = $"<div class='formItem'><label for='item.Key'>{item.Value.Label}</label><input type='number' id='{item.Key}' name='{item.Key}' value='{item.Value.Value}' /></ div >";
break;
case FieldType.Submit:
stringformat = $"<input type='submit' name='{item.Key}' value='{item.Value.Value}'>";
break;
default:
break;
}
return new HtmlString(stringformat);
}
Also the validation will not work as the attributes (for example RequiredAttribute for RegExAttribute) are not there.
Am I having the wrong approach to this or is there a more defined way to complete forms like this?
Is there a way to create a dynamic ViewModel which could be created from the origional setup and still keep all the MVC richness?
You can do this using my FormFactory library.
By default it reflects against a view model to produce a PropertyVm[] array:
```
var vm = new MyFormViewModel
{
OperatingSystem = "IOS",
OperatingSystem_choices = new[]{"IOS", "Android",};
};
Html.PropertiesFor(vm).Render(Html);
```
but you can also create the properties programatically, so you could load settings from a database then create PropertyVm.
This is a snippet from a Linqpad script.
```
//import-package FormFactory
//import-package FormFactory.RazorGenerator
void Main()
{
var properties = new[]{
new PropertyVm(typeof(string), "username"){
DisplayName = "Username",
NotOptional = true,
},
new PropertyVm(typeof(string), "password"){
DisplayName = "Password",
NotOptional = true,
GetCustomAttributes = () => new object[]{ new DataTypeAttribute(DataType.Password) }
}
};
var html = FormFactory.RazorEngine.PropertyRenderExtension.Render(properties, new FormFactory.RazorEngine.RazorTemplateHtmlHelper());
Util.RawHtml(html.ToEncodedString()).Dump(); //Renders html for a username and password field.
}
```
Theres a demo site with examples of the various features you can set up (e.g. nested collections, autocomplete, datepickers etc.)
I'm going to put my solution here since I found this searching 'how to create a dynamic form in mvc core.' I did not want to use a 3rd party library.
Model:
public class IndexViewModel
{
public Dictionary<int, DetailTemplateItem> FormBody { get; set; }
public string EmailAddress { get; set; }
public string templateName { get; set; }
}
cshtml
<form asp-action="ProcessResultsDetails" asp-controller="home" method="post">
<div class="form-group">
<label asp-for=#Model.EmailAddress class="control-label"></label>
<input asp-for=#Model.EmailAddress class="form-control" />
</div>
#foreach (var key in Model.FormBody.Keys)
{
<div class="form-group">
<label asp-for="#Model.FormBody[key].Name" class="control-label">#Model.FormBody[key].Name</label>
<input asp-for="#Model.FormBody[key].Value" class="form-control" value="#Model.FormBody[key].Value"/>
<input type="hidden" asp-for="#Model.FormBody[key].Name"/>
</div>
}
<input type="hidden" asp-for="templateName" />
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
You can use JJMasterData, it can create dynamic forms from your tables at runtime or compile time. Supports both .NET 6 and .NET Framework 4.8.
After setting up the package, access /en-us/DataDictionary in your browser
Create a Data Dictionary adding your table name
Click on More, Get Scripts, Execute Stored Procedures and then click on Preview and check it out
To use your CRUD at runtime, go to en-us/MasterData/Form/Render/{YOUR_DICTIONARY}
To use your CRUD at a specific page or customize at compile time, follow the example below:
At your Controller:
public IActionResult Index(string dictionaryName)
{
var form = new JJFormView("YourDataDictionary");
form.FormElement.Title = "Example of compile time customization"
var runtimeField = new FormElementField();
runtimeField.Label = "Field Label";
runtimeField.Name = "FieldName";
runtimeField.DataType = FieldType.Text;
runtimeField.VisibleExpression = "exp:{pagestate}='INSERT'";
runtimeField.Component = FormComponent.Text;
runtimeField.DataBehavior = FieldBehavior.Virtual; //Virtual means the field does not exist in the database.
runtimeField.CssClass = "col-sm-4";
form.FormElement.Fields.Add(runtimeField);
return View(form);
}
At your View:
#using JJMasterData.Web.Extensions
#model JJFormView
#using (Html.BeginForm())
{
#Model.GetHtmlString()
}

DropDownListFor HTML helper not selecting option for Enum model property

I have an ASP.NET MVC 4 site and a domain model that uses an Enum. I'm able to generate a list of SelectListItem objects, but the proper item is not selected.
Domain Model
public enum ApplicationStatus
{
Unknown = 0,
Incomplete = 1,
Submitted = 2,
Error = 4
}
public class Application
{
public ApplicationStatus Status { get; set; }
// ...
}
The "Edit" View
#model Application
#using (Html.BeginForm("Edit", "Applications", new { ... }, FormMethod.Post, new { role = "form", #class = "form-horizontal" }))
{
#Html.Partial("_Form", Model)
<p>
#Html.ActionLink("Cancel", "Details", new { ... }, new { #class = "btn btn-default" })
<button type="submit" class="btn btn-primary">Save</button>
</p>
}
The "_Form" Partial
#model BWE.Models.Entity.BitWeb.Application
#Html.ValidationSummary(true)
<div class="form-group">
#Html.LabelFor(model => model.Status, new { #class = "col-sm-2" })
<div class="col-sm-10">
#Html.DropDownListFor(model => model.Status, SelectHelper.GetApplicationStatusOptions(Model.Status))
#Html.ValidationMessageFor(model => model.Status)
</div>
</div>
SelectHelper
public static class SelectHelper
{
public static IEnumerable<SelectListItem> GetApplicationStatusOptions(ApplicationStatus currentStatus)
{
var items = new List<SelectListItem>()
{
new SelectListItem()
{
Text = "Select",
Value = string.Empty
}
};
IEnumerable<ApplicationStatus> statuses = Enum.GetValues(typeof(ApplicationStatus)).Cast<ApplicationStatus>();
foreach (var status in statuses)
{
if (status == ApplicationStatus.Unknown)
continue;
items.Add(new SelectListItem()
{
Text = status.ToString(),
Value = ((int)status).ToString(),
Selected = status == currentStatus
});
}
return items;
}
}
The "Select" option is always selected in the dropdown even though I can step through the code and see one of the SelectListItem objects get their Selected property set to true.
I've tried the solution recommended in My templated helper using a DropDownListFor does NOT reflect the model of enum. Why?, but this solution was geared towards MVC 3. I tried the solution (passing a SelectList object as the second argument to Html.DropDownListFor) and all I got was a dropdown list with 4 options whose display text was "System.Web.Mvc.SelectListItem" and no values for the <option> tags.
Furthermore, I tried other solutions that created an #Html.EnumDropDownListFor(...) helper function, which behaved the same way. It seems that all though the proper SelectListItem is getting selected, maybe the current Model.Status value is overriding it?
Update #1
I added an integer property called StatusId which gets and sets the Status property as an integer, and this works when calling Html.DropDownListFor(model => model.StatusId, ...) however I was hoping for a solution that allows me to use the enum value directly, not as a proxy through another property.
For some crazy reason, enum values are rendered as their string-based names by Razor, rather than their integer value counterparts. Regardless, my guess is that your SelectHelper is returning options with values as integers, which is why converting your enum value to an int allowed the selection to work. Since this is a custom component you have anyways, I would suggest simply modifying your helper to return the enum string names as the option values instead of ints. Then the property value and the option values will line up properly.

How to access the Selected Keys from a DropDownList in MVC

I want to access the Selected Keys from two DropDowns and then call a Controller Action with the Keys as parameters.
In my MVC view I have then following code:
#model Test.Models.LoginViewModel
<p>
Database #Html.DropDownListFor(x => x.SelectedDatabase, Model.DatabaseList, "-- Select an Item --", new {#id="databaseDD", #name="databaseDD"})
</p>
<p>
Language #Html.DropDownListFor(x => x.SelectedLanguage, Model.LanguageList, "-- Select an Item --", new {#id="languageDD", #name="languageDD"})
</p>
<p>
#using (Html.BeginForm("LoginToService", "Home", new { databaseKey = Model.SelectedDatabase, languageKey = Model.SelectedLanguage }, FormMethod.Post, new { target = "_self" }))
{
<input id="login" type="submit" name="login" value="Login" />
}
</p>
The problem is, the properties Model.SelectedDatabase and Model.SelectedLanguage are not set when the DropDown Selection changes. How can I access the Selected Keys and use them as parameters?
In your Controller you should use a predefined class, something like this :
public class DropDownItem
{
public int Key { get; set; }
public string Value{ get; set; }
}
Use a backend method to map desired fields to the DropDownItem something like:
public SelectList FooListUsed()
{
List<DropDownItem> result = from item in ListOfDataBaseTableElements
select new DropDownItem{
Key = item.id,
Value = item.name
}
result.Insert(0, new DropDownItem{ Key = 0, Value = "Select All" });
return new SelectList(result, "Key", "Value"); //this mapping will take care of your selected value issue
Pass the result of the query to the view either through a Viewbag or through Model ,
lets use the viewbag for this example : ViewBag.FooList=FooListUsed();
Last step ... use your DropdownList:
#Html.DropDownListFor(model => model.SelectedDatabase,(IEnumerable<SelectListItem>)ViewBag.FooList)

ASP.NET MVC edit operation?

#Html.ActionLink("Edit", "EditArticle", new { ArticleID = article.ArticleID })
I retrieved Article by ArticleID and return to edit page like this:
public ActionResult EditArticle(Guid ArticleID)
{
AddArticleModel AddArticleModel = new AddArticleModel();
AddArticleModel.Categories = entity.TBL_CATEGORIES.Select(a => a);
AddArticleModel.Article = dbo.SelectArticleById(ArticleID);
return View(AddArticleModel);
}
There is no problem until here.
And in my editing page I'm changing some attributes of article (not all attributes).For example I'm changing title, content, and updateddate. Like this:
#model DunyaYazilim.Models.AddArticleModel
#{
ViewBag.Title = "EditArticle";
Layout = "~/Views/Shared/_LayoutAuthor.cshtml";
}
#using (Html.BeginForm((string)ViewBag.FormAction, "Author"))
{
#Html.ValidationSummary(true, "Makale gönderilirken bir hata oluştu. Lütfen daha sonra tekrar deneyin.")
<div>
<div class="label_header">#Html.Label("Kategori Seçiniz:")</div>
<div>#Html.DropDownList("CategoryID", new SelectList(Model.Categories, "CategoryID", "Name"),Model.Article.TBL_CATEGORIES.Name)</div>
<div class="label_header">#Html.Label("Makale Başlık:")</div>
<div>#Html.TextBoxFor(m => m.Article.Title, new { #class = "my_textbox" })</div>
<div class="label_header">#Html.Label("Makale Açıklama:")</div>
<div>#Html.TextAreaFor(m => m.Article.Description, new { #class = "my_textarea" })</div>
<div class="label_header">#Html.Label("Makale İçerik:")</div>
<div>#Html.TextAreaFor(m => m.Article.ArticleContent, new { #class = "my_textarea" })</div>
<div><input type="submit" value="Gönder" class="my_button" /></div>
</div>
}
And then I post it to:
[HttpPost]
public ActionResult EditArticle(AddArticleModel AddArticleModel, String CategoryID)
{
//TODO: update database...
return View(AddArticleModel);
}
But unchanged attributes are return null(ArticleID, UserID, etc).So I cant Update the database, Because I dont have ArticleID after posting. What is the reason for this?
Thanks.
MVC doesn't maintain anything for you between requests. When you post to your action, it will post only the values that you have set up in your form. As you don't have your article id or user id in the form (or anywhere else, e.g. route or query string), MVC won't know about them during model binding for your EditArticle action.
If you want the extra details to be sent through with your post, you can put hidden fields in the form, e.g.
#Html.HiddenFor(m => m.Article.Id)

The ViewData item that has the key 'MY KEY' is of type 'System.String' but must be of type 'IEnumerable<SelectListItem>'

I am trying to populate a dropdown list from a database mapped with Linq-2-SQL, using ASP.NET MVC 2, and keep getting this error.
I am so confused because I am declaring a variable of type IEnumerable<SelectListItem> on the second line, but the error makes me think this is not the case. I feel like this should be very simple, but I am struggling. Any help is appreciated.
Here are the interesting bits of my controller:
public ActionResult Create()
{
var db = new DB();
IEnumerable<SelectListItem> basetypes = db.Basetypes.Select(
b => new SelectListItem { Value = b.basetype, Text = b.basetype });
ViewData["basetype"] = basetypes;
return View();
}
And here are the interesting bits of my view:
<div class="editor-label">
<%: Html.LabelFor(model => model.basetype) %>
</div>
<div class="editor-field">
<%: Html.DropDownList("basetype") %>
<%: Html.ValidationMessageFor(model => model.basetype) %>
</div>
Here is the POST action when submitting the Form
// POST: /Meals/Create
[HttpPost]
public ActionResult Create(Meal meal)
{
if (ModelState.IsValid)
{
try
{
// TODO: Add insert logic here
var db = new DB();
db.Meals.InsertOnSubmit(meal);
db.SubmitChanges();
return RedirectToAction("Index");
}
catch
{
return View(meal);
}
}
else
{
return View(meal);
}
}
Thanks.
I had same problem, and finally I got the answer...
The problem is that in the POST action, after submitting the form, the ModelState is not valid, or it's catching an error in try/catch, so the View is returned. But this time the View has not the ViewData["basetype"] correctly set.
You need to populate it again, probably with the same code used before, so repeat this:
var db = new DB();
IEnumerable<SelectListItem> basetypes = db.Basetypes.Select(
b => new SelectListItem { Value = b.basetype, Text = b.basetype });
ViewData["basetype"] = basetypes;
before the return View(meal) in the [HttpPost] method.
exactly this will solve your problem:
[HttpPost]
public ActionResult Create(Meal meal)
{
if (ModelState.IsValid)
{
try
{
// TODO: Add insert logic here
var db = new DB();
db.Meals.InsertOnSubmit(meal);
db.SubmitChanges();
return RedirectToAction("Index");
}
catch
{
var db = new DB();
IEnumerable<SelectListItem> basetypes = db.Basetypes.Select(
b => new SelectListItem { Value = b.basetype, Text = b.basetype });
ViewData["basetype"] = basetypes;
return View(meal);
}
}
else
{
var db = new DB();
IEnumerable<SelectListItem> basetypes = db.Basetypes.Select(
b => new SelectListItem { Value = b.basetype, Text = b.basetype });
ViewData["basetype"] = basetypes;
return View(meal);
}
}
I know this question is very old, but I came here today with the same problem, so other could come here later...
You will receive this error if the SelectList is null.
I've just come across this issue and this article helped me through it - http://odetocode.com/Blogs/scott/archive/2010/01/18/drop-down-lists-and-asp-net-mvc.aspx
The most likely cause it that your collection is repopulated after the po
For future readers, if you are using razor, try to change type of selectlist item from List to IEnumerable.
From
#Html.DropDownListFor(m => m.id, ViewBag.SomeList as List<SelectListItem>)
To
#Html.DropDownListFor(m => m.id, ViewBag.SomeList as IEnumerable<SelectListItem>)
Try adding a string for the name of your dropdown list as the first parameter, and get the item out of your viewdata:
<%= Html.DropDownList("SomeDropdownName", (IEnumerable<SelectListItem>)ViewData["basetype"]) %>
Here is also an extension method you can use so the dropdown list is set up in a similar style to how you have done your other controls:
public static string DropDownList<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel)
where TModel : class
{
string inputName = ExpressionHelper.GetInputName(expression);
return htmlHelper.DropDownList(inputName, selectList, optionLabel);
}
For example
<%= Html.DropDownList(x => x.BaseType, (IEnumerable<SelectListItem>)ViewData["basetype"], "")%>
You are setting the collection as an item in ViewData dictionary and trying to retreive it as property on the model. A simple fix would be to reference it the same way as you set it:
<%var basetype = ViewData["basetype"] as IEnumerable<SelectListItem>;%>
<div class="editor-label">
<%: Html.Label("basetype") %>
</div>
<div class="editor-field">
<%: Html.DropDownList("basetype", basetype) %>
<%: Html.ValidationMessage("basetype") %>
</div>
Alternatively, the below code uses a strongly typed view:
public class ViewModel {
//Model properties
public IEnumerable<SelectListItem> basetype {get;set;}
}
public ActionResult Create()
{
var db = new DB();
IEnumerable<SelectListItem> basetypes = db.Basetypes.Select(b => new SelectListItem { Value = b.basetype, Text = b.basetype });
return View(new ViewModel { basetype=basetypes });
}
Then, in your strongly typed view:
<div class="editor-label">
<%: Html.LabelFor(model => model.basetype) %>
</div>
<div class="editor-field">
<%: Html.DropDownListFor(model=>model.basetype) %>
<%: Html.ValidationMessageFor(model => model.basetype) %>
</div>
If you use Html.DropDownList() method - same error may occur, if your ViewData/Viewbag item not set, as #Peto answered.
But it may be not set in case of controller set item correctly, but in main view you use partial viw call with new ViewDataDictionary values.
if you have #Html.Partial("Partianame", Model,new ViewDataDictionary() { /* ... */ }) then your partial view will not see ViewData and ViewBag data, remove new ViewDataDictionary() parameter
To future readers,
I came across with the problem today and couldn´t fix it. It turned to be really simple after all. I was working with a table + view. When I updated the table (added a few colums) I forgot to update (drop and recreate) the view, which caused the problem for me.
Hope it helps somebody.
I got same error today and my solution is to make model "valid".
In my case, after user submit by clicking "save", I got model state: invalid if user key-in "0", but model state will be valid if user key-in "0.0".
So I override "IsValid" method to return true even user key-in "0".
Hope it helps.

Resources