I currently am struggling with mapping the value of a cascading dropdown -
with data driven by a tree constructed by an angular controller - to a primitive
string for posting back to a MVC controller.
I have a tree of data to populate the cascading dropdown, with the basic format of
public class Series
{
public string Text { get; set; }
public virtual ICollection<Depth> Depths { get; set; }
}
public class Depth
{
public int Id { get; set; }
public double Value { get; set; }
public string Text { get; set; }
}
Which is then mapped using angular to the output, which is an MVC type model:
public class LineItemViewModel
{
public string Series { get; set; }
public double Depth { get; set; }
}
Using an angular controller, where joistGroup is a list of series populated on controller creation.
HTML:
<div class="initialRow row"
ng-repeat="joist in floor.Joists">
<select name="series" class="form-control"
data-ng-model="series"
data-ng-options="s as s.Text for s in joistGroup.Series"
data-ng-change="joist.Series = series.Text">
<option></option>
</select>
<select name="depth" class="form-control"
data-ng-model="depth"
data-ng-options="d as d.Text for d in series.Depths"
data-ng-change="joist.Depth = depth.Value;"
data-rule-required="true">
<option></option>
</select>
</div>
Angular controller:
var app = angular.module('MaterialListModule', []);
app.controller('MaterialListController', function($scope, $filter, $http) {
$scope.getMaterialList = function() {
$http.get('url for get call')
.success(function(data, status, headers, config) {
$scope.Model = data;
});
}
$scope.getProducts = function (productSource) {
$http.get('url for get')
.success(function (data, status, headers, config) {
$scope.allItems = data;
$scope.joistGroup = $filter('filter')($scope.allItems, { Text: 'Joists' }, true)[0];
}).finally(function() {
$scope.getMaterialList();
});
}
$scope.getProducts("All");
});
Currently I am using ng-change to trigger an update of the primitive for posting back
to the MVC controller, but as this is both a create and edit page this doesn't work
for when I already have a selected series. I am thinking I can get it working by ng-init
but haven't had any success.
Can anyone point me in the direction of best practices to map a complex model for
cascading dropdowns to a primitive and back?
Instead of writing your ng-change inline, hook it up to a function. In that function, you can then access the 'complex object' and do whatever you want to do with it.
$scope.depthChange = function() {
$scope.myPrimitive = $scope.depth.Value;
//Do something with primitive
}
Also, when you have something already selected, on initialisation/retrieval of the data, just set the $scope.depth to the complex object that should be selected
Related
I'm pretty new regarding frontend, and couldn't find a clear solution to this simple problem.
In a Visual 2017 c# ASP.NET Framework MVC project, for a single page with ONLY ONE SUBMIT button which is already used for updating some string of my model totalInfo, I want to update the integer property ModelltypeForView of my model from the selected value of n radiobuttons.
I learnt I could update my controller model going with an AJAX call, but I can't find the way to update my view model, I must miss something simple but not obvious for a beginner.
Here are the main parts of my MVC.
I am aware of the problems due to the return type of my SettModelltype of my controller, as well as the ajax and foreach loop of my view, so basically: how do I finish this code? Is this problem even fixable without any partial view?
Thank you so much for your time.
// Model TotalInfoModell.cs
public class TotalInfoModell
{
public List<Modelltype> Modelltyper { get; set; }
public int ModelltypeForView { get; set; }
}
public class Modelltype
{
public int MTIndex { get; set; }
public string MTName { get; set; }
public bool MTSelected { get; set; } //? useless?
}
// Controller MainController.cs
static TotalInfoModell _totalInfo;
[HttpGet]
public ActionResult Main()
{
if (_totalInfo == null)
{
_totalInfo = new TotalInfoModell();
}
return View(_totalInfo);
}
[HttpPost]
public ActionResult SettModelltype(TotalInfoModell totalInfoFraView)
{
_totalInfo.ModelltypeForView = totalInfoFraView.ModelltypeForView;
for (int i = 0; i < _totalInfo.Modelltyper.Count; i++)
{
_totalInfo.Modelltyper[i].MTSelected = (i == _totalInfo.ModelltypeForView);
} /// Could be useless
return RedirectToAction("Main"); //????
}
// View Main.cshtml
#foreach (var modelltype in Model.Modelltyper)
{
#Html.RadioButtonFor(i => modelltype.MTIndex == Model.ModelltypeForView, modelltype.MTIndex, new { #class = "MTSelected" }) // ????
#modelltype.MTName<br />
}
...
<script>
$(function () {
$('.MTSelected').change(function () {
var viewModel = {
"ModelltypeForView": $('.MTSelected:checked').val(),
};
$.ajax({
url: '#Url.Action("SettModelltype", "Main")',
data: viewModel,
type: 'POST',
success: function (data) {
},
error: function (xhr) {
alert("It didn't work");
}
}).done(function (data) {
alert("Done");
$('#Id').val(data.ModelltypeForView); //??? Should return totalInfoModell
});
});
});
</script>
I'm not sure I understand. Do you want to pass your viewmodel back with the redirect?
If that's the case you can add it as a parameter. Here is a post that can help
I've got a populated entity filled with checkbox selector, few strings. that has to be built linearly(for each row) in a loop in a div structure.
The entity:
public class Hours
{
[Key]
public int SliceID { get; set; }
public string SliceName { get; set; }
public string Slice { get; set; }
public string StartTime { get; set; }
public string EndTime { get; set; }
public bool Selected { get; set; } //if slice is checked by user on search screen
}
The action which will gather all time slices for display:
public ActionResult Search_Times()
{
int iInstId = 1;
Test.Models.DataLayer db = new Test.Models.DataLayer();
Test.Models.TestDB context = new Models.TestDB();
IEnumerable<Test.Models.Hours> lst = db.GetSlices(context, iInstId).OrderBy(a => a.SliceID);
// ViewBag.SliceList = lst;
return View(lst);
}
I want to render those fields in a specific part of my body page along with some class/div formatting along the way.
For example, If I use EditorForModel :
in Main page:
#model IEnumerable<Test.Models.Hours>
#Html.EditorForModel()
in the Hours EditorTemplate:
#model Test.Models.Hours
<div>
#Html.CheckBoxFor(
x => x.Selected,
new {
#class = "jcf-unselectable",
id = HtmlHelper.GenerateIdFromName("cb_slice." + ViewData.TemplateInfo.GetFullHtmlFieldName(""))
}
)
// this should be a label too, made for example to show text from data.
#Html.DisplayTextFor(x => x.SliceName)
#Html.LabelFor(
x=> x.StartTime,
new {
#class = "info"
})
#Html.LabelFor(
x => x.EndTime,
new {
#class = "info"
})
</div>
The #Html.LabelFor in my case will only show the row's title and not the data (for="" in the source view after rendering), Unlike DisplayTextFor which will show the data but is only a generic text.
I need a way or to fix this current way, To manipulate the data from the model accordingly, Labels will show the data behind their field and I could generate the class,id needed(based on css/html required) inside that label in a loop.
What's the best way to do so, viewbags/templates/etc?
The following is a view models in ASP.NET MVC:
public class Email
{
public string Selected { get; set; }
public string Name { get; set; }
}
public class User
{
public string UserName { get; set; }
public IList<Email> Emails { get; set; }
}
It is passed into the view and looks like this while defining the knockout view model:
var viewModel = {
UserName: ko.observable("#Model.UserName"),
Emails: ko.observableArray(#Html.Json(#Model.Emails) || []),
// many other things
}
I want to make sure that Email.Selected is required. How to do it having the list, rather than single objects. I know it must be the easiest question, but I couldn't find anything on the topic.
I use knockout validation on the client, since Fluent and data annotation aren't working with knockout unfortunately.
As i mentioned on my comment knockout custom validation can helps, here i am adding an example of how to use knockout custom validation in your case :
var viewModel = {
UserName: ko.observable("#Model.UserName"),
Emails: ko.observableArray(#Html.Json(#Model.Emails) || []).
extend({
customRequired: { message: 'Selected is required' }
});
// many other things
}
ko.validation.rules['customRequired'] = {
validator: function (emailList) {
var pass = true;
ko.utils.arrayFirst(emailList(), function(email){
if(email.Selected == '') //if Selected property is empty
{
pass = false;
return true; //break the loop
}
});
return pass;
}
};
I have an MVC view model that looks like this:
public class DirectorySearchModel
{
[Display(Name = "First name contains")]
public string FirstName { get; set; }
[Display(Name = "Last name contains")]
public string LastName { get; set; }
public CountriesCollection Countries { get; set; }
public IEnumerable<Country> SelectedCountries { get; set; }
public IEnumerable<Country> AllCountries { get; set; }
}
The CountriesCollection object (line 9) looks like this:
public class CountriesCollection
{
[Display(Name = "Countries")]
public int[] arrCountries { get; set; }
}
Now, I'm creating a new, blank instance of CountriesCollection and then adding it to a blank instance of the DirectorySearchModel view model and then serialising it all into a javascript view model for Knockout.js:
{
"FirstName":null,
"LastName":null,
"Countries":{"arrCountries":[]},
"SelectedCountries":[{"RelationshipManager":{},"CountryId":1,"CountryName":"UK"},{"RelationshipManager":{},"CountryId":2,"CountryName":"France"},{"RelationshipManager":{},"CountryId":3,"CountryName":"Spain"}],
"AllCountries":[{"RelationshipManager":{},"CountryId":1,"CountryName":"UK"},{"RelationshipManager":{},"CountryId":2,"CountryName":"France"},{"RelationshipManager":{},"CountryId":3,"CountryName":"Spain"}]
}
My checkboxes are rendered as: <input checked="checked" data-bind="checked: Countries.arrCountries" id="Countries_arrCountries30" name="Countries.arrCountries" type="checkbox" value="1">. Checking a couple means you end up with this Knockout.js view model:
{
"FirstName":null,
"LastName":null,
"Countries":{"arrCountries":["1", "3"]},
"SelectedCountries":[{"RelationshipManager":{},"CountryId":1,"CountryName":"UK"},{"RelationshipManager":{},"CountryId":2,"CountryName":"France"},{"RelationshipManager":{},"CountryId":3,"CountryName":"Spain"}],
"AllCountries":[{"RelationshipManager":{},"CountryId":1,"CountryName":"UK"},{"RelationshipManager":{},"CountryId":2,"CountryName":"France"},{"RelationshipManager":{},"CountryId":3,"CountryName":"Spain"}]
}
Submitting my view normally (i.e. via a submit button and not with Knockout.js) to an MVC action that expects a DirectorySearchModel, I'm able to ask for model.Countries.arrCountries to get a list of the checked items, but when I use...
$.post("/MyController/MyAction", ko.toJS(viewModel), function(returnData) {
$("#resultCount").html(returnData);
});
or...
$.post("/MyController/MyAction", viewModel, function(returnData) {
$("#resultCount").html(returnData);
});
to another action that expects the same DirectorySearchModel, model.Countries.arrCountries is always null! I wondered if it's due to Knockout.js posting the arrCountries entries as string[]s when MVC is expecting int[]s, but changing my MVC code to expect string[]s doesn't seem to change much..! The CountriesCollection object within the DirectorySearchModel appears to exist, but it's the arrCountries within that's always null.
Any ideas? Any help much appreciated!
Edit
The action that receives the Knockout.js viewModel:
public MvcHtmlString ResultCount(DirectorySearchModel model)
{
return new MvcHtmlString(getResultCount(model).ToString());
}
The getResultCount method:
public int getResultCount(DirectorySearchModel model)
{
IUserRepository userRepository = new UserRepository();
int count = userRepository.Search(model, null).Count();
return count;
}
FIXED!
Thanks to Konstantin for pointing out that a simple switch from $.post to $.ajax to send my Knockout.js view model back to my mvc action was all that was needed! Here's the $.ajax code I'm using:
$.ajax({
type: "POST",
url: "/MyController/MyAction",
data: ko.toJSON(viewModel),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
$("#resultCount").html(data);
}
});
You cant use $.post you need to go for the underlying $.ajax and add the correct contenttype to make mvc accept the posted json and do the model binding (contenttype should be "application/json; charset=utf-8") google for it and you will se lots of examples
I want to display a string type as checkbox on MVC view, but returns it as string type on HTTP post. The problem is that it returns false on HTTP Post. Below is my code:
View:
#model List<Car>
foreach(var car in Model){
bool isFourWheel = false;
if(bool.TryParse(car.IsFourWheel, out isFourWheel){
#Html.CheckBox("IsFourWheel", isFourWheel); //need to be rendered as checkbox, but returns string type on HTTP POST
}
}
Model:
public class Car
{
public string IsFourWheel { get; set; } //bad naming, but it can contain any type, include boolean
}
Controller:
public ActionResult Index()
{
var cars = new List<Car>(){ new Car(){IsFourWheel = "true"},new Car(){IsFourWheel = "false"} };
return View(cars);
}
[HttpPost]
public ActionResult Index(List<Car> cars) **Problem IsFourWheel is false when true is selected **
{
return View(cars);
}
Any ideal would be very much appreciated.
You can try specifying a template name in your helper:
#Html.EditorFor(car => car.IsFourWheel, "CheckBox")
And defining the template to render the data the way you want, in either ~/Views/{YourControllerName}/EditorTemplates/CheckBox.cshtml or ~/Views/Shared/EditorTemplates/CheckBox.cshtml.
You can find a whole series of post by Brad Wilson on MVC templates here:
Brad Wilson: ASP.NET MVC 2 Templates, Part 1: Introduction
It is for MVC 2, but most concepts still apply to MVC 3 as well (save for the Razor syntax).
Update:
Actually you probably don't need a custom template for this. Try using #Html.CheckBoxFor(car => car.IsFourWheel) instead.
Update 2:
Drop the following template in ~/Views/Shared/EditorTemplates:
IsFourWheel.cshtml
#functions {
private bool IsChecked() {
if (ViewData.Model == null) return false;
return Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture);
}
}
#Html.CheckBox("", IsChecked(), new { #class = "check-box" })
Then from your view, call it like so:
#Html.EditorFor(model => model.IsFourWheel, "IsFourWheel")
I tested it and binding works in both GET and POST scenarios.
You could alter your viewmodel like this:
public class Car
{
public string IsFourWheel { get; set; }
public bool IsFourWheelBool { get { return bool.Parse(IsFourWheel); } }
}
Your view would look like this:
#Html.EditFor(x => x.IsFourWheelBool);
I think it will be easier, if you add an Id to your model. Just like this
Model:
public class Car
{
public int CarID { get; set; }
public string IsFourWheel { get; set; }
}
View:
#model IEnumerable<Car>
foreach (var car in Model)
{
if(car.IsFourWheel == "true"){
<input type="checkbox" name="carID" value="#car.CarID" checked="checked" />
}
else
{
<input type="checkbox" name="carID" value="#car.CarID" />
}
}
Controller:
[HttpPost]
public ActionResult Index(List<int> carID)
{
//handle selected cars here
return View();
}