Fetching and Adding Checkbox dynamically on selection of Dropdown boxes - asp.net-mvc

I have a form as above. I am trying to work as when user selects Table Name & Permission, it goes back to server side, fetches the columns of the selected table & display all column names as check boxes. When use selects Save btn, HttpPost will execute and when user selects Cancel, to return back to home page.
I have created a ViewModel for this :
// Actual EF Model
public partial class TablePermission
{
public int Id { get; set; }
public int UserId { get; set; }
public int PermissionLevelId { get; set; }
public string TableName { get; set; }
public string RestrictViewFields { get; set; }
public string RestrictEditFields { get; set; }
public virtual PermissionLevel PermissionLevel { get; set; }
public virtual User User { get; set; }
}
// View Model for the View
public class TablePermissionsVM
{
public TablePermissionsVM()
{
TablePermission = new TablePermission();
RestrictViewFields = new List<FieldList>();
// Created for trial to see Checkboxes
RestrictViewFields.Add(new FieldList() { FieldName = "UserId", Selected = false });
RestrictViewFields.Add(new FieldList() { FieldName = "fName", Selected = false });
RestrictViewFields.Add(new FieldList() { FieldName = "lName", Selected = false });
RestrictEditFields = new List<FieldList>();
}
public TablePermission TablePermission { get; set; }
public List<FieldList> RestrictViewFields { get; set; }
public IEnumerable<FieldList> RestrictEditFields { get; set; }
}
// Model to save field names & it's selected status
public class FieldList
{
public string FieldName { get; set; }
public bool Selected { get; set; }
}
}
Controller UPDATED : ADDED THE NEW ACTION (FillFields() ) METHOD that has to called onChange event
[Authorize]
[HttpGet]
public ActionResult TablePermissions(TablePermissionsVM tablePermissionVm)
{
return View(tablePermissionVm);
}
// Action Method to Fill Column names for the List<>.
public ActionResult FillFields(string tableName, string tblPermLevel)
{
// WANT TO RETURN HERE ANOTHER LIST (2 LIST OBJECTS) IN JSON
// restrictView & restrictEdit
var restrictView = DbUtilities.GetColumnNames(tableName);
var restrictEdit = DbUtilities.GetColumnNames(tableName);
return Json(restrictView, JsonRequestBehavior.AllowGet);
}
View - UPDATED CODE : aDDED Bound fields for TableName & TableLevelPermission, Script that I use on the event of change of Table Selected.
UPDATED - aDDED FORM ID, SCRIPT METHOD
#model DataStudio.Models.TablePermissionsVM
using (Html.BeginForm("TablePermissions", "Admin", FormMethod.Post, new { id = "tblPermForm" }) ))
{
#Html.AntiForgeryToken()
<div class="form-group">
#Html.LabelFor(model => model.TablePermission.TableName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="editor-field">
#Html.DropDownListFor(model => model.TablePermission.TableName,
DbUtilities.GetTableNames(), "Select Table",
new { #class = "form-control", #onchange="FillFields()" })
#Html.ValidationMessageFor(model => model.TablePermission.TableName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.TablePermission.PermissionLevelId, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="editor-field">
#Html.DropDownListFor(model => model.TablePermission.PermissionLevelId, DbUtilities.GetPermissionLevelList(), "Select Permission Level", new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.TablePermission.PermissionLevelId, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.RestrictViewFields, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="editor-field">
**// NEED TO ADD CHECK BOXES HER RECEIVED THRU SCRIPT**
</div>
</div>
}
<script>
function FillFields() {
var tblName = $('#TablePermission_TableName').val();
var tblPermLevel = $('#TablePermission_PermissionLevelId').val();
//($('tblPermForm').valid()) { ERROR - OBJECT DOESN'T HAVE valid()'
if (tblName != null && tblPermLevel != null) {
$.ajax({
url: '/Admin/FillFields',
type: 'GET',
dataType: "JSON",
data: { TableName: tblName, TablePermLevel: tblPermLevel },
success: function (restrictView) {
$("#RestrictViewFields").html(""); // Clear before appending new ones
$.each(restrictView, function (i, field) {
$("#RestrictViewFields").append(
$('<option></option>').val(field.FieldName).html(field.FieldName))
// WANT TO ADD AS 3 CHECKBOX IN A ROW
});
}
});
}
}
</script>
Their are couple of things that I am not able to figure out & get confused with it. Mainly, on making sure that both the drop down boxes has value, I need to perform again a "Get" and fetch column names for the table selected & display the columns as check boxes.
The way I have implemented Checkboxes, I will get proper selected Values in HttpPost, Right ! Are am I anywhere wrong ?
How to make the Get Request when both the drop down's are selected ??
Any help is highly appreciated. Thanks a lot in advance.
Update I started to try with only TableName selection (I want for both dropdown), but the event doesn't occur and go to FillFields() in script. Where am I going wrong ? I tried this logic from here . Can't get why it doesn't get fired only ???
Btw, this is a full form i mean, their is no partial form in it. I want to fill the check box controls in those 2 RestrictXFields on selection of TableName & Permssion check box & on Save btn, send all to Request & save to db.
UPDATE : THANKS a lot, Stephen & Chethan. With your support, I identified the cause for event not getting triggered. Event is triggered, I am able to retrieve the column names from db, the HTML part on success is not being updated. Stephen, I also added form Id & tried form.valid() as you instructed, but I get error script doesn't identify valid(). Both the fields in Model are marked as Required in MetaData class. Currently, testing both var != null works. But, I liked you valid() option.

Per my understanding, You should fetch the checkboxes using an ajax call.
Create an action method in your controller which accepts selected values of TableName and TableLevelPermisson dropdown. Use these values to fetch
List<FieldList> RestrictViewFields
IEnumerable<FieldList> RestrictEditFields.
and use as data/model to your partial view.
Call this action method using ajax, on change of the dropdown list value.
Get the HTML returned from partial view and use it in your DOM.
How to make the Get Request when both the drop down's are selected ??
If you are using jQuery: Just google for "dropdown change events in jquery" and ajax call examples.

Related

Set The selected Value of a Multiselect from Controller to View after submitting an invalid Model

I am using a Multiselect DropDownList to generate a multiple <select>
I was able to generate it and was working fine.
But If I try to submit it using the parameters:
Name = null
ObjAOption = [1,2] // assume I selected 2 options in my multiselect
ObjAOption will just select option value='1' instead of select options 1, and 2.
Is there any way I can get back the selected options and pass it back to my view by setting it in my controller? I would love to use HTML helper and not to use jQuery or javascript on this part.
Controller:
public ActionResult AddObjectA(AddModel am){
if(ModelState.IsValid){
//Save
}
else {
am.ObjA = // new List of ObjectA with atleast 4 option
return View("MyView",am);
}
}
View:
#Html.LabelFor(model => model.ObjA, "Object A")
#Html.DropDownList("ObjAOption", new MultiSelectList(Model.ObjA, "Key", "Name"), "-- Select Object A--", new { #class = "custom-select custom-select-sm", multiple="" })
#Html.ValidationMessageFor(m => m.ObjAOption, "", new { #class = "text-danger" })
Model:
public class AddModel {
[Required]
public String Name {get;set;}
public IEnumerable<ObjectA> ObjA{ get; set; }
[Required(ErrorMessage = "Please select at least one option")]
public List<int>ObjAOption{ get; set; }
}
public class ObjectA {
public int Key {get;set;}
public string Name {get;set;}
}
Have you tried to use the helper Hiddenfor ? It generate a field that keep your element value, name and attribute :
View:
#Html.LabelFor(model => model.ObjA, "Object A")
#Html.DropDownList("ObjAOption", new MultiSelectList(Model.ObjA, "Key", "Name"), "-- Select Object A--", new { #class = "custom-select custom-select-sm", multiple="" })
#Html.ValidationMessageFor(m => m.ObjAOption, "", new { #class = "text-danger" })
#Html.HiddenFor(m => m.ObjAOption)
Solution:
I scrapped out my DropDownList and tried using ListBoxFor as discussed here

BindingNested List Property to Send to MVC Controller

I have a view where I need to be able to select an item from a dropdown list and add it cumulatively to a list to display. The only way I've been able to think to do this so far is to have a Property which binds to the Dropdownlist, send this back with the cumulative list and have the server add to the List and send it back for display. This works well for the first item because the list to be added to has a Count of 0. Any subsequent additions will cause the controller binding to wipe the List.
Here are the important parts of my code:
Model:
public class CaseAppealViewModel
{
//needs to accumulate records as I add them and retain between server calls
public List<CaseFile> SelectedCases
{
get { return _selectedCases; }
set { _selectedCases = value; }
}
private List<CaseFile> _selectedCases = new List<CaseFile>();
public Nullable<int> ChosenCase { get; set; }
}
View:
#model MySystem.Models.CaseAppealViewModel
#using (Html.BeginForm("AddCase", "AppealLogs", Model))
{
<div class="col-md-6">
#Html.DisplayName("Cases:")
#Html.DropDownListFor(model => model.ChosenCase, CaseAppealViewModel.CaseDropDownList, "", new {#class = "form-control"})
#Html.HiddenFor(model => model.SelectedCases)
#Html.HiddenFor(model => model.Appeals)
</div>
<div class="col-md-6">
<input type="submit" id="AddCase" value="Add"/>
</div>
}
Controller:
public ActionResult AddCase(CaseAppealViewModel cavm)
{
var tempCase = db.CaseFiles.Find(cavm.ChosenCase);
if (tempCase != null)
{
cavm.SelectedCases.Add(db.CaseFiles.Find(cavm.ChosenCase));
}
return View("Details", cavm);
}
Is there a way I can send the SelectedCases List back and forth from the view to the controller without the Count resetting to 0?

Issue with Model Binding

I have created a View Model called CompetitionRoundModel which is partially produced below:
public class CompetitionRoundModel
{
public IEnumerable<SelectListItem> CategoryValues
{
get
{
return Enumerable
.Range(0, Categories.Count())
.Select(x => new SelectListItem
{
Value = Categories.ElementAt(x).Id.ToString(),
Text = Categories.ElementAt(x).Name
});
}
}
[Display(Name = "Category")]
public int CategoryId { get; set; }
public IEnumerable<Category> Categories { get; set; }
// Other parameters
}
I have structured the model this way because I need to populate a dropdown based on the value stored in CategoryValues. So for my view I have:
#using (Html.BeginForm())
{
<div class="form-group">
#Html.LabelFor(model => model.CategoryId, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.CategoryId, Model.CategoryValues, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.CategoryId, "", new { #class = "text-danger" })
</div>
</div>
// Other code goes here
}
I have selected model.CategoryId in the DropDownListFor() method since I want to bind the selected value to CategoryId. I really don't care for CategoryValues, I just need it to populate the DropDown.
My problem now is that when my Controller receives the values for my Model in the action method, CategoryValues is null which causes the system to throw a ArgumentNullException (the line that is highlighted is the return Enumerable line.
I have even tried [Bind(Exclude="CategoryValues")] but no change at all. Any help would be much appreciated.
Your not (and should not be) creating form controls for each property of each Category in your IEnumerable<Category> collection so in your POST method, the value of Categories is null (it never gets initialized). As soon as you attempt CategoryValues and exception is thrown by your .Range(0, Categories.Count()) line of code in the getter.
Change you view model to give CategoryValues a simple geter/setter, and delete the Categories property
public class CompetitionRoundModel
{
public IEnumerable<SelectListItem> CategoryValues { get; set; }
[Display(Name = "Category")]
public int CategoryId { get; set; }
.... // Other properties
}
and populate the SelectList in the controller methods, for example
var categories db.Categories; // your database call
CompetitionRoundModel model = new CompetitionRoundModel()
{
CategoryValues = categories.Select(x => new SelectListItem()
{
Value = x.Id.ToString(),
Text = x.Name
},
....
};
return View(model);
or alternatively
CompetitionRoundModel model = new CompetitionRoundModel()
{
CategoryValues = new SelectList(categories, "Id", "Name" ),
Note also that if you return the view (because ModelState is invalid, the you need to repopulate the value of CategoryValues (refer The ViewData item that has the key 'XXX' is of type 'System.Int32' but must be of type 'IEnumerable' for more detail)
Since CategoryValues just populates the drop down, it will never post back to the server and you'll need to rebuild the list from the database before using it in the GET or POST operation. The CategoryId property is the value that will be posted back to the server from the DropDownList.

How do I carry a complex object model through a POST request

I have the following entity models:
public class AssetLabel
{
public string QRCode { get; set; }
public string asset { get; set; }
public virtual IEnumerable<Conversation> Conversations { get; set; }
}
public class Conversation
{
public int ID { get; set; }
public virtual AssetLabel AssetLabel{ get; set; }
public string FinderName { get; set; }
public string FinderMobile { get; set; }
public string FinderEmail { get; set; }
public ConversationStatus Status{ get; set; }
public IEnumerable<ConversationMessage> Messages { get; set; }
}
public class ConversationMessage
{
public int ID { get; set; }
public DateTime MessageDateTime { get; set; }
public bool IsFinderMessage { get; set; }
public virtual Conversation Conversation { get; set; }
}
public enum ConversationStatus { open, closed };
public class FinderViewModel : Conversation
{/*used in Controllers->Found*/
}
My MVC application will prompt for a QRCode on a POST request. I then validate this code exists in the database AssetLabel and some other server-side logic is satisfied. I then need to request the user contact details to create a new Conversation record.
Currently I have a GET to a controller action which returns the first form to capture the Code. If this is valid then I create a new FinderViewModel, populate the AssetLabel with the object for the QRCode and return a view to consume the vm and show the fields for the Name, Mobile and Email.
My problem is that although the AssetLabel is being passed to the view as part of the FinderViewModel and I can display fields from the AssetLabel; graphed object the AssetLabel does not get passed back in the POST. I know I could modify the FinderViewModel so that it takes the Conversation as one property and set up the QRCode as a separate property that could be a hidden field in the form and then re-find the the AssetLabel as part of the processing of the second form but this feels like a lot of work seeing as I have already validated it once to get to the point of creating the second form (this is why I am moving away from PHP MVC frameworks).
The first question is HOW?, The second question is am I approaching this design pattern in the wrong way. Is there a more .NETty way to persist the data through multiple forms? At this point in my learning I don't really want to store the information in a cookie or use ajax.
For reference the rest of the code for the 1st form POST, 2nd view and 2nd form POST are shown below (simplified to eliminate irrelevant logic).
public class FoundController : Controller
{
private ApplicationDbContext db = new ApplicationDbContext();
// GET: Found
public ActionResult Index()
{
AssetLabel lbl = new AssetLabel();
return View(lbl);
}
[HttpPost]
public ActionResult Index(string QRCode)
{
if (QRCode=="")
{
return Content("no value entered");
}
else
{
/*check to see if code is in database*/
AssetLabel lbl = db.AssetLables.FirstOrDefault(q =>q.QRCode==QRCode);
if (lbl != null)
{
var vm = new FinderViewModel();
vm.AssetLabel = lbl;
vm.Status = ConversationStatus.open;
return View("FinderDetails", vm);
}
else
{/*Label ID is not in the database*/
return Content("Label Not Found");
}
}
}
[HttpPost]
public ActionResult ProcessFinder(FinderViewModel vm)
{
/*
THIS IS WHERE I AM STUCK! - vm.AssetLabel == NULL even though it
was passed to the view with a fully populated object
*/
return Content(vm.AssetLabel.QRCode.ToString());
//return Content("Finder Details posted!");
}
FinderView.cshtml
#model GMSB.ViewModels.FinderViewModel
#{
ViewBag.Title = "TEST FINDER";
}
<h2>FinderDetails</h2>
#using (Html.BeginForm("ProcessFinder","Found",FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Finder Details</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.ID)
#Html.HiddenFor(model => model.AssetLabel)
<div class="form-group">
#Html.LabelFor(model => model.FinderName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.FinderMobile, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderMobile, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderMobile, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.FinderEmail, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FinderEmail, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.FinderEmail, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
Rendered HTML snippet for AssetLabel
<input id="AssetLabel" name="AssetLabel" type="hidden"
value="System.Data.Entity.DynamicProxies.AssetLabel_32653C4084FF0CBCFDBE520EA1FC5FE4F22B6D9CD6D5A87E7F1B7A198A59DBB3"
/>
You cannot use #Html.HiddenFor() to generate a hidden output for a complex object. Internally the method use .ToString() to generate the value (in you case the output is System.Data.Entity.DynamicProxies.AssetLabel_32653C4084FF0CBCFDBE520EA1FC5FE4F22B6D9CD6D5A87E7F1B7A198A59DBB3 which cannot be bound back to a complex object)
You could generate a form control for each property of the AssetLabel - but that would be unrealistic in your case because AssetLabel contains a property with is a collection of Conversation which in turn contains a collection of ConversationMessage so you would need nested for loops to generate an input for each property of Conversation and ConversationMessage.
But sending a whole lot of extra data to the client and then sending it all back again unchanged degrades performance, exposes unnecessary details about your data and data structure to malicious users, and malicious users could change the data).
The FinderViewModel should just contain a property for QRCode (or the ID property of AssetLabel) and in the view
#Html.HiddenFor(m => m.QRCode)
Then in the POST method, if you need the AssetLabel, get it again from the repository just as your doing it in the GET method (although its unclear why you need to AssetLabel in the POST method).
As a side note, a view model should only contain properties that are needed in the view, and not contain properties which are data models (in in your case inherit from a data model) when editing data. Refer What is ViewModel in MVC?. Based on your view, it should contain 4 properties FinderName, FinderMobile, FinderEmail and QRCode (and int? ID if you want to use it for editing existing objects).
Thanks Stephen. The QRCode is the PK on AssetLabel and the FK in Conversation so it needs to be tracked through the workflow. I was trying to keep the viewModel generic so that is can be used for other forms rather than tightly coupling it to this specific form and I was trying to pass the AssetLabel around as I have already done a significant amount of validation on it's state which I didn't want to repeat. I worked out what I need to do - If you use #Html.Hidden(model => model.AssetLabel.QRCode) then the form field name becomes AssetLabel_QRCode and is automatically mapped to the correct place in the POST viewmodel. To promote code reuse and avoid any rework later I have created this logic in a display template with the fields defined as hidden and then #Html.Partial() using the overload method that allows me to define the model extension to the form names
#Html.Partial
(
"./Templates/Assetlabel_hidden",
(GMSB.Models.AssetLabel)(Model.AssetLabel),
new ViewDataDictionary()
{
TemplateInfo = new TemplateInfo()
{
HtmlFieldPrefix = "AssetLabel"
}
}
)
But you are absolutely right, this exposes additional fields and my application structure. I think I will redraft the viewModel to only expose the necessary fields and move the AssetLabel validation to a separate private function that can be called from both the initial POST and the subsequent post. This does mean extra code in the controller as the flat vm fields need to be manually mappped to the complex object graph.

Pass Select into Controller via Response

Hy,
I'm new to ASP.NET MVC 5. I'm trying to get the value of an HTML select with no success.
My View (essential part):
<div class="form-group">
#Html.Label("Country", new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.DropDownList("Countries", (IEnumerable<SelectListItem>)ViewBag.Countries, new { #class = "form-control", id = "Country", name = "Country" })
</div>
</div>
My Controller (essential part):
public ActionResult Index()
{
string country = Request["Country"]; // here I always get null
}
I need a newbie like explanation why this is not working and how I get it to work, please :)
First, I agree with #Maess. Don't use ViewBag. It's horrible and someone at Microsoft should be slapped for ever adding it as an option in the first place.
That said, your error is pretty obvious here. You named your select "Countries" and you're trying to pull "Country" out of the request.
Since you're new, I'll be nice and lay out how to use a view model for this. First, create a model:
public class IndexViewModel
{
public int SelectedCountry { get; set; }
public IEnumerable<SelectListItem> CountryChoices { get; set; }
}
Then in your action:
// GET
public ActionResult Index()
{
var model = new IndexViewModel();
// get your country list somehow
// where `Id` and `Name` are properties on your country instance.
model.CountryChoices = countries.Select(m => new SelectListItem { Value = m.Id, Text = m.Name });
return View(model);
}
And in your view:
#model Namespace.IndexViewModel
...
#Html.DropDownListFor(m => m.SelectedCountry, Model.CountryChoices, new { #class = "form-control" })
And finally, in your POST action:
[HttpPost]
public ActionResult Index(IndexViewModel model)
{
// use model.SelectedCountry
}

Resources